Fundamentals
9 minute read
Fundamentals
With three fundamental concepts of motorcortex javascript clients, you can create powerful client applications that interact with Motorcortex applications.
These examples are all done via motorcortex-javascript.
Connecting to Motorcortex Applications
Motorcortex-javascript applications connect to Motorcortex applications using WebSockets. After establishing a connection the client application can request the parameter tree from the server and get/set parameter values or subscribe to data streams. This is the bases for all Motorcortex-javascript applications. They can be used to control robots, log data, run tests etc. and is as flexible as the Motorcortex platform itself.
The following code example shows how to connect to a motorcortex application. Make sure to change the IP_ADDRESS, LOGIN and PASSWORD variables to match your setup.
async function main() {
const IP_ADDRESS = "192.168.2.100";
const LOGIN = "admin";
const PASSWORD = "vectioneer";
const motorcortex = window.motorcortex; // Import motorcortex library
console.log("Motorcortex Browser Test");
// Loading messages
let motorcortex_types = new motorcortex.MessageTypes(); // Load message types
let req_sync = new motorcortex.Request(motorcortex_types); // Create request manager
let sub_sync = new motorcortex.Subscribe(req_sync); // Create subscribe manager
let session = new motorcortex.SessionManager(req_sync, sub_sync); // Create session manager
console.log("SessionManager initialized");
// Register session state callback
session.notify((msg) => {
if (session.hasError()) {
console.error("Session error: " + motorcortex.SessionState.getInfo(msg.state));
} else {
console.log("Session state: " + motorcortex.SessionState.getInfo(msg.state));
}
});
// Open connection
try {
await session.open(
{
host: IP_ADDRESS,
security: true,
request_port: 5568,
subscribe_port: 5567,
timeout_ms: 5000,
queue_length: 5000,
},
{
login: LOGIN,
password: PASSWORD,
}
);
console.log("Connection ready");
} catch (e) {
console.error("Connection error: " + JSON.stringify(e));
}
}
Note
With Rust, Python, and C++ client APIs, you need to add wss:// in front of the IP_ADDRESS while in javascript this is NOT required.
Recovering a lost connection
The Motorcortex library handles temporary lost connections automatically. The client does not have to handle this. However, if the server is restarted, the client cannot automatically reconnect because the server has created a new session-id. The client may handle this case by discarding the old connection and creating a new connection and log-in again.
Close connection
When the client application is done communicating with the server, it should close the connections properly:
// close request and subscribe connections
session.close();
console.log("Session closed");
Requesting Data
With the connection created in the previous example, you can now request data from the application. For example, the position of the end-effector of the robot can be requested and printed to the console every second. You can test it by adding the following code after the connection code in the previous example:
session.notify((msg) => {
if (session.hasError()) {
console.error("Session error: " + motorcortex.SessionState.getInfo(msg.state));
} if (session.ready()) {
console.log("Session is established");
// Get Parameter example
console.log("Getting parameter...");
req_sync.getParameter("root/ManipulatorControl/manipulatorToolPoseActual").then((param) => {
console.log("Full parameter response: " + JSON.stringify(param));
console.log("manipulatorToolPoseActual: " + param.value);
})
.catch((err) => {
console.error("Get Parameter error: " + JSON.stringify(err));
});
} else {
console.log("Session state: " + motorcortex.SessionState.getInfo(msg.state));
}
});
Move the robot around and you should see the end-effector position updating in the console output when you restart the script.
By changing the parameter path in the req.getParameter() function you can request any parameter that is available in the parameter tree of the connected application. Make sure to pass a variable of the correct type to the value function to retrieve the parameter value.
You can also request multiple parameters at once by passing a list of parameter paths to the req.getParameterList() function. Here is an example of requesting both the end-effector position and the joint positions:
session.notify((msg) => {
if (session.hasError()) {
console.error("Session error: " + motorcortex.SessionState.getInfo(msg.state));
} if (session.ready()) {
console.log("Session is established");
// Get Parameter List example
console.log("Getting parameter list...");
req_sync.getParameterList(["root/ManipulatorControl/manipulatorToolPoseActual",
"root/ManipulatorControl/jointPositionsActual"]).then((params) => {
console.log("Full parameter list response: " + JSON.stringify(params));
console.log("manipulatorToolPoseActual: " + params[0].value);
console.log("jointPositionsActual: " + params[1].value);
})
.catch((err) => {
console.error("Get Parameter List error: " + JSON.stringify(err));
});
} else {
console.log("Session state: " + motorcortex.SessionState.getInfo(msg.state));
}
});
Individual parameter requests are performed in parallel and responses can arrive at different times. This means separate req.getParameter(...) calls may not represent a single synchronized snapshot and their order may not match the parameter-tree order. If you need synchronized values, request parameters together with req.getParameterList(paths) (returns a single snapshot), or use subscriptions with the desired timing.
If you require higher rates for specific signals, prefer using subscriptions for those high-rate parameters (or create separate subscriptions with different group/divider settings) and only request the slower parameters as needed.
Subscribing to Data
Instead of requesting data in a loop, you can also subscribe to a parameter and receive updates whenever the value changes. This is more efficient for parameters that change frequently. You can subscribe to the same end-effector position parameter as follows:
session.notify((msg) => {
if (session.hasError()) {
console.error("Session error: " + motorcortex.SessionState.getInfo(msg.state));
} if (session.ready()) {
console.log("Session is established");
// Get Parameter example
console.log("Setting up subscriber...");
let log = sub_sync.subscribe(["root/ManipulatorControl/manipulatorToolPoseActual"], "log", 1000);
log.notify((val) => {
console.log(`manipulatorToolPoseActual: ${val[0].value}`);
});
} else {
console.log("Session state: " + motorcortex.SessionState.getInfo(msg.state));
}
});
With this code, the function inside subscription.notify will be called whenever the value of the end-effector position gets updated through the subscription. This does not mean the value has changed necessarily, but that a new value is available. You can move the robot around to see the updates in the console output.
You can group multiple parameters into a single subscription. This ensures that the parameters are synchronized when received. Here is an example of subscribing to both the end-effector position and the joint positions in a single subscription:
session.notify((msg) => {
if (session.hasError()) {
console.error("Session error: " + motorcortex.SessionState.getInfo(msg.state));
} if (session.ready()) {
console.log("Session is established");
// Get Parameter example
console.log("Setting up subscriber...");
let log = sub_sync.subscribe(["root/ManipulatorControl/manipulatorToolPoseActual", "root/ManipulatorControl/jointPositionsActual"], "group2", 1000);
log.notify((val) => {
console.log(`manipulatorToolPoseActual: ${val[0].value}`);
console.log(`jointPositionsActual: ${val[1].value}`);
});
} else {
console.log("Session state: " + motorcortex.SessionState.getInfo(msg.state));
}
});
The client can create multiple subscriptions for different groups, for instance at different data-rates (by setting the “divider” parameter differently for different groups).
The group name is shared between clients and a group is published by the server once to all clients that have subscription to the group with that name. This way the communication can easily scale across large numbers of clients.
If a group name already exists on the server and another client requests a subscription to a set of signals with the same group name, the server adds any requested signals that are not already in the group to the group. The other clients using the same group name will then also receive the additional signals.
Setting parameter values
Now that we can listen to Data from the Motorcortex Application, we can try to send some Data to it. This is the way of interacting with the Motorcortex Application and controlling Robots or Machines.
Parameters can be set with req.setParameter. Let us start up the robot!
session.notify((msg) => {
if (session.hasError()) {
console.error("Session error: " + motorcortex.SessionState.getInfo(msg.state));
} if (session.ready()) {
console.log("Session is established");
// Get Parameter example
console.log("Setting State Command to 2...");
req_sync.setParameter("root/Logic/stateCommand", 2).catch((err) => {
console.error("Set Parameter error: " + JSON.stringify(err));
});
} else {
console.log("Session state: " + motorcortex.SessionState.getInfo(msg.state));
}
});
This code will send a command to the Logic Module of the Motorcortex Application to start up the Robot. The value 2 corresponds to the STARTUP command of the stateCommand parameter.
Using the same technique you can also open and close the gripper of the robot:
// Open/close the gripper 5 times with 1 second interval
session.notify((msg) => {
if (session.hasError()) {
console.error("Session error: " + motorcortex.SessionState.getInfo(msg.state));
} if (session.ready()) {
console.log("Session is established");
for (let i = 0; i < 5; i++) {
(async () => {
try {
const reply0 = await req_sync.setParameter("root/UserParameters/IO/gripper", 0);
console.log("reply0: " + JSON.stringify(reply0));
await new Promise(res => setTimeout(res, 1000));
const reply1 = await req_sync.setParameter("root/UserParameters/IO/gripper", 1);
console.log("reply1: " + JSON.stringify(reply1));
await new Promise(res => setTimeout(res, 1000));
} catch (err) {
console.error("Set Parameter error: " + JSON.stringify(err));
}
})();
}
} else {
console.log("Session state: " + motorcortex.SessionState.getInfo(msg.state));
}
});
This code will open and close the gripper 5 times with a delay of 1 second in between.
Setting Array Values
Instead of setting single scalar values, you can also set array values. For example, to set the joint positions of the robot, you can use the following code:
session.notify((msg) => {
if (session.hasError()) {
console.error("Session error: " + motorcortex.SessionState.getInfo(msg.state));
} else if (session.ready()) {
console.log("Session is established");
(async () => {
try {
const replyMode = await req_sync.setParameter("root/Logic/modeCommand", 3);
const modeInfo = (replyMode && typeof replyMode.info === 'function') ? replyMode.info() : JSON.stringify(replyMode);
console.log("Set to manual joint mode: " + modeInfo);
await new Promise(res => setTimeout(res, 2000));
const joint_velocities = [0.1, 0.3, 0.0, 0.0, 0.0, 0.3];
const replyVel = await req_sync.setParameter("root/ManipulatorControl/hostInJointVelocity", joint_velocities);
const velInfo = (replyVel && typeof replyVel.info === 'function') ? replyVel.info() : JSON.stringify(replyVel);
console.log("Set joint velocity: " + velInfo);
} catch (err) {
console.error("Set Parameter error: " + JSON.stringify(err));
}
})();
} else {
console.log("Session state: " + motorcortex.SessionState.getInfo(msg.state));
}
});
You will notice that the robot turns on manual joint mode and starts moving the joints with the specified velocities. However, it only briefly moves. This is because the command is sent to root/ManipulatorControl/hostInJointVelocity which also has a watchdog running in the Motorcortex Application. If no new command is received within a certain time, the command is reset to zero for safety reasons.
Note
A reply from setParameter only confirms that the value was received and queued to be updated in the next update tick; it does not guarantee the parameter has been updated in the application tree. If your script runs faster than the parameter-update loop (for example a 1000 Hz control tick), the set can overload the input buffer of the server, then the values will be discarded. To avoid this, introduce a small delay, wait for an explicit confirmation when available, or synchronize with the application’s parameter loop.