bgd10y-workshop

Workshop for the Synapticon@Belgrade 10-year anniversary
🎉🎉🎉🤖🎉🎉🎉🎉🤖🎉🎉🎉🤖🎉🎉🎉🤖🎉🎉🎉🎉🤖🎉🎉🎉

Install the required software

You will need a text editor like Visual Studio Code and the Node.js runtime environment.

For the Windows operating system, it is recommended to install the official 18.16.0 LTS. However, for Linux and Mac, installing the Node Version Manager (NVM) and then Node.js has proven to be easier. Here's how to do it:

❯ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh | bash
❯ nvm install 18

Reopen your terminal and check if the installation was successful:

❯ node -v
v18.16.0

Create the project

You will use a modern version of the React framework for the web called Next.js Once you run the command for creating the project, it will be installed automatically.

Run the following command:

❯ npx create-next-app@latest

Please provide the following answers when creating the project:

Need to install the following packages:
  create-next-app@13.4.6
Ok to proceed? (y) y
? What is your project named? › bgd10y-workshop
? Would you like to use TypeScript with this project? › Yes
? Would you like to use ESLint with this project? › Yes
? Would you like to use Tailwind CSS with this project? › Yes
? Would you like to use `src/` directory with this project? › No
? Use App Router (recommended)? › Yes
? Would you like to customize the default import alias? › No

Once the project has been created, open it with Visual Studio Code (File / Open Folder...).

Run the project

Open your terminal, navigate to the bgd10y-workshop directory, and run the following command:

❯ npm run dev

The previous command will start a web server, and your app will be accessible at http://localhost:3000.

Clean up the generated code and set up the styling

Open the app/layout.tsx and change its metadata object to:

export const metadata = {
  title: "bgd10y-workshop",
  description: "Workshop for the Synapticon@Belgrade 10-year anniversary",
};

Open the app/page.tsx file and replace its content with this:

"use client";

export default function Home() {
  return (
    <main className="flex min-h-screen flex-col items-center p-24 gap-y-8">
      <h2 className="text-5xl font-thin">bgd10y-workshop</h2>
    </main>
  );
}

Then, replace the content of the app/globals.css with this file.

Create the client object

You will use the client object to make requests to the Motion Master server. In order to create it, you first need to install the motion-master-client library as a dependency. From the bgd10y-workshop directory, run the following command:

› npm install motion-master-client

Then, create the app/client.ts file and add the following code to it:

import { createMotionMasterClient } from "motion-master-client";

export const client = createMotionMasterClient("192.168.200.253");

client.whenReady().then(() => {
  client.request.download(0, 0x6060, 0, 1);
});

The previous code will create the client object and, in doing so, connect to the running instance of the Motion Master server at 192.168.200.253. When the client is ready, we set the modes of operation to the profile position mode.

Create your first component and test the client object

The first component you will create will use the client object to get the system version, where the system refers to Motion Master.

Create the app/system-version.tsx file and set its content to:

"use client";

import { useEffect, useState } from "react";
import { client } from "./client";

export default function SystemVersion() {
  const [systemVersion, setSystemVersion] = useState("0.0.0");

  useEffect(() => {
    client.request.getSystemVersion(3000).subscribe((status) => {
      setSystemVersion(status.version ?? "0.0.0");
    });
  }, []);

  return (
    <div>
      <strong>System Version</strong>: <span className="font-mono">v{systemVersion}</span>
    </div>
  );
}
Code explanation

We create the systemVersion state variable. Simple variables won't suffice because they don't persist the data between renders. The component will initally display '0.0.0', but when setSystemVersion is called, it will trigger a re-render and display the actual system version. Functions to make requests to Motion Master reside in the client.request object. All requests have a client timeout, such as 3 seconds for the getSystemVersion function. They typically return an observable that you need to subscribe to in order to receive a response sometime in the future. All request functions are asynchronous.

Now that you have your first component, you need to display it. Open the app/page.tsx and import it:

"use client";

import SystemVersion from "./system-version";

Then, add it to the main element of the Home component:

…
<main className="flex min-h-screen flex-col items-center p-24 gap-y-8">
  <h2 className="text-5xl font-thin">bgd10y-workshop</h2>
  <SystemVersion />
</main>
…

This is what you should see:

System Version

Set up monitoring

Monitoring refers to a feature in which the client requests from Motion Master to send parameter values at regular intervals. Once it starts sending the data, the client can subscribe to the data emissions and later unsubscribe when they no longer need that data, effectively requesting Motion Master to stop sending it.

You will now create the Monitoring component that will begin monitoring the target and actual positions, velocity, and torque parameters. You will display the data in a table.

Create the app/monitoring.tsx file and set its content to:

"use client";

import { useEffect, useState } from "react";
import { DeviceParameterIds, ParameterValueType } from "motion-master-client";
import { client } from "./client";

const ids: DeviceParameterIds = [
  [0, 0x607a, 0],
  [0, 0x6064, 0],
  [0, 0x606c, 0],
  [0, 0x6077, 0],
];

export default function Monitoring() {
  const [values, setValues] = useState<ParameterValueType[]>(Array(ids.length).fill(0));

  useEffect(() => {
    const subscription = client.startMonitoring(ids, 500000).subscribe(setValues);
    return () => {
      subscription.unsubscribe();
    };
  }, []);

  const rows = values.map((value, index) => <td key={index}>{value}</td>);

  return (
    <table className="table">
      <thead>
        <tr>
          <th>0x607A: Target Position</th>
          <th>0x6064: Actual Position</th>
          <th>0x607A: Actual Velocity</th>
          <th>0x6077: Actual Torque</th>
        </tr>
      </thead>
      <tbody>
        <tr>{rows}</tr>
      </tbody>
    </table>
  );
}
Code explanation

With useEffect, the code inside the block runs only once, the first time this component renders. In it, we start monitoring a list of tuples where each tuple consists of the device position, parameter index, and subindex. When parameter values are emitted, we change the state of the component using setValues, which triggers a re-render of the table. Since the monitoring interval is set to half a second, the table will be re-rendered every half a second.

Import and display the Monitoring component in app/page.tsx below the SystemVersion component:

…
import SystemVersion from "./system-version";
import Monitoring from "./monitoring";
…
  <SystemVersion />
  <Monitoring />
</main>
…

This is what you should see:

Monitoring Table

Change parameter values

You are now going to create a reusable component that will allow you to change the value of any device parameter. Once you have this component, you will create four instances of it in order to be able to change the values of the position profile-related parameters.

Create the app/parameter.tsx file and set its content to:

"use client";

import { useEffect, useState } from "react";
import { makeParameterId } from "motion-master-client";
import { client } from "./client";

interface ParameterProps {
  name: string;
  index: number;
  subindex: number;
}

export default function Parameter({ name, index, subindex }: ParameterProps) {
  const [value, setValue] = useState(0);
  const id = makeParameterId(index, subindex);

  useEffect(() => {
    client.request.upload(0, index, subindex).then(setValue);
  }, []);

  const text = (
    <span>
      <strong>{makeParameterId(index, subindex)}</strong>: {name}
    </span>
  );

  function handleInputChange(e: React.FormEvent<HTMLInputElement>) {
    const newValue = parseInt(e.currentTarget.value, 10);
    if (!isNaN(newValue)) {
      setValue(newValue);
    }
  }

  async function handleOnSubmit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault();
    await client.request.download(0, index, subindex, value);
    console.info(`${id} set to ${value}`);
  }

  return (
    <form onSubmit={handleOnSubmit}>
      <div className="flex gap-x-2 items-end">
        <label className="flex flex-col">
          {text}
          <input className="control" type="number" defaultValue={value} onChange={handleInputChange} />
        </label>
        <button type="submit" className="btn">
          SET
        </button>
      </div>
    </form>
  );
}
Code explanation

The Parameter component receives three properties: name, index, and subindex. This allows it to be reusable for any parameter. Initially, it fetches the current parameter value and displays it. When the form is submitted, it takes the value from the input and sends a request to Motion Master to set the new parameter value.

Import and create four instances of the Parameter component in app/page.tsx below the Monitoring component:

…
import Monitoring from "./monitoring";
import Parameter from "./parameter";
…
<Monitoring />
<div className="flex gap-x-8 items-end">
  <Parameter name="Profile velocity" index={0x6081} subindex={0} />
  <Parameter name="Profile acceleration" index={0x6083} subindex={0} />
  <Parameter name="Profile deceleration" index={0x6084} subindex={0} />
  <Parameter name="Target position" index={0x607a} subindex={0} />
</div>
…

This is what you should see:

Parameter Inputs

One way to test if changing the parameter values works is to modify the "0x607A:00: Target position" and observe if the change is reflected in the monitoring table above.

Control operation

Now, you will proceed to create several buttons that will enable you to control the operation and CiA402 state machine. These buttons will allow you to perform actions such as enabling operation, quick stopping the device, resetting faults, applying set points, and halting the position profile motion.

Create the app/quick-stop.tsx file and set its content to:

"use client";
    
import { client } from "./client";

export default function QuickStop() {
  return (
    <button className="btn" type="button" onClick={() => client.request.quickStop(0)}>
      QUICK STOP
    </button>
  );
}

Create the app/reset-fault.tsx file and set its content to:

"use client";

import { client } from "./client";

export default function ResetFault() {
  return (
    <button className="btn" type="button" onClick={() => client.request.resetFault(0)}>
      RESET FAULT
    </button>
  );
}

Create the app/enable-operation.tsx file and set its content to:

"use client";

import { Cia402State } from "motion-master-client";
import { client } from "./client";

export default function EnableOperation() {
  function handleClick() {
    client.request.transitionToCia402State(0, Cia402State.OPERATION_ENABLED);
  }

  return (
    <button className="btn" type="button" onClick={handleClick}>
      ENABLE OPERATION
    </button>
  );
}

Create the app/apply-set-point.tsx file and set its content to:

"use client";

import { client } from "./client";

export default function ApplySetPoint() {
  return (
    <button className="btn" type="button" onClick={() => client.request.applySetPoint(0)}>
      APPLY SET POINT
    </button>
  );
}

Create the app/halt.tsx file and set its content to:

"use client";

import { client } from "./client";

export default function Halt() {
  return (
    <div className="flex items-center gap-x-2 ml-12">
      <span>HALT</span>
      <button className="btn" type="button" onClick={() => client.request.setHaltBit(0, true)}>
        ON
      </button>
      <button className="btn" type="button" onClick={() => client.request.setHaltBit(0, false)}>
        OFF
      </button>
    </div>
  );
}

Now, import and display all the buttons you have created in the app/page.tsx file, below the parameters:

…
import QuickStop from "./quick-stop";
import ResetFault from "./reset-fault";
import EnableOperation from "./enable-operation";
import ApplySetPoint from "./apply-set-point";
import Halt from "./halt";
…
<div className="flex gap-x-2">
  <QuickStop />
  <ResetFault />
  <EnableOperation />
  <ApplySetPoint />
  <Halt />
</div>
…

This is what you should see:

Operation Buttons

Run the position profile

You now have all the necessary components to execute the position profile. Set the profile's velocity, acceleration, deceleration, and target position. Then, enable the operation and apply the set point. You can set another target position and apply the set point again. At any point during the motion, you can halt it. Take the time to interact with the system and ensure that it functions as expected.

Say it

This is the final component you will create. Begin by downloading the provided file and placing it in the app directory. Next, create a file called app/say.tsx and set its content as follows:

"use client";

import { say } from "./mem";

interface SayProps {
  message: string;
}

export default function Say({ message }: SayProps) {
  return (
    <button className="btn" type="button" onClick={() => say(message)}>
      SAY {message.toUpperCase()}
    </button>
  );
}

Import and display both the Say component that you have created and the Mem component from the previously downloaded file. In the app/page.tsx file, place them below the operation buttons.

…
import Say from "./say";
import Mem from "./mem";
…
<Say message="Synapticon" />
<Mem />
…

Click on the Say button and observe what it does!

Write your own message

Now that you have the full user interface at your disposal, with which you can operate the drive in the position profile mode, your task is to write a single word, a person's name, or a thing that makes your life joyful.

If you're up for a challenge, can you figure out how the message is encoded? What cipher are we using?

Finito

You've made it this far, and that's awesome! Thank you for having participated in this workshop, you nice human being 🤗, not a robot 🤖.