Fundamentals

This chapter will show the fundamental concepts of motorcortex-cpp.

Fundamentals

With three fundamental concepts of motorcortex c++ clients, you can create powerful client applications that interact with Motorcortex applications.

These examples are all done via motorcortex-cpp.


Connecting to Motorcortex Applications

Motorcortex-cpp 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-cpp 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 and PATH_TO_CERTIFICATE, LOGIN and PASSWORD variables to match your setup.

#include <mcx-cpp/mcx-cpp.h> // Include the Motorcortex C++ library
#include <cstdio> // for printf
#include <string> // for std::string


using namespace std;
using namespace mcx_cpp;

int main() {
    printf("MCX version: %s\n", mcx_cpp::version()); // Print the Motorcortex C++ library version

    std::string url = "wss://IP_ADDRESS"; // Replace with your Motorcortex server address
    std::string username = "LOGIN"; // Replace with your username
    std::string password = "PASSWORD"; // Replace with your password
    mcx_cpp::ConnectionOptions options{.certificate = "PATH_TO_CERTIFICATE"}; // Replace with the path to your certificate
    options.conn_timeout_ms = 10000;

    // Creating a parameter tree;
    ParameterTree parameter_tree;
    Request req{parameter_tree};
    Subscribe sub{req};

    // Connect the request channel to the server
    auto conn_status = req.connect(url + ":5568", options).get();
    if (conn_status.status() != StatusCode::OK) {
        fprintf(stderr, "Failed to connect: %s\n", conn_status.info().c_str());
        return 1;  // Exit with error code
    }
    printf("Connected to %s\n", req.url().c_str());

    // Authenticate with the server
    auto login_status = req.login(username, password).get();
    if (login_status.status() != StatusCode::OK) {
        fprintf(stderr, "Login failed: %s\n", login_status.info().c_str());
        return 1;
    }
    printf("Logged in successfully\n");

    // Connect the subscribe channel to receive real-time updates
    auto sub_status = sub.connect(url + ":5567", options).get();
    if (sub_status.status() != StatusCode::OK) {
        fprintf(stderr, "Subscribe connection failed: %s\n", sub_status.info().c_str());
        return 1;
    }
    printf("Subscribe connected to %s\n", sub.url().c_str());
}

While Python can do the connection to the Request and Subscription class together with req,sub = connect(), in c++ the Request and Subscription connections are done seperate. The subscription can also be established before the request connection if desired or even without the request connection. But Request req{parameter_tree}; must be passed to the Subscription class when creating it: Subscription sub{parameter_tree};

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
req.close();
sub.close();

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:

#include <mcx-cpp/mcx-cpp.h> // Include the Motorcortex C++ library
#include <cstdio> // for printf
#include <string> // for std::string
#include <thread> // for std::this_thread::sleep_for
#include <chrono> // for std::chrono::seconds

using namespace std;
using namespace mcx_cpp;


void print_array(const std::array<double, 6>& arr) {
    // Helper function to print array contents
    printf("[");
    for (size_t i = 0; i < arr.size(); ++i) {
        printf("%f", arr[i]);
        if (i < arr.size() - 1) printf(", ");
    }
    printf("]\n");
}

int main() {
    // ADD CONNECTION CODE HERE FROM PREVIOUS EXAMPLE

    // Loop to get the parameter value every second
    while (true) {
        // Get the parameter value
        auto param_result = req.getParameter("root/ManipulatorControl/manipulatorToolPoseActual").get();
        if (param_result.status() == StatusCode::OK) {
            std::array<double, 6> pose{};
            param_result.value(pose); // fills the pose array
            print_array(pose);
        } else {
            printf("Failed to get parameter value: %s\n", param_result.info().c_str());
        }
        std::this_thread::sleep_for(std::chrono::seconds(1));
    }
}

Move the robot around and you should see the end-effector position updating in the console output.

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.

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:

#include <mcx-cpp/mcx-cpp.h> // Include the Motorcortex C++ library
#include <cstdio> // for printf
#include <string> // for std::string
#include <thread> // for std::this_thread::sleep_for
#include <chrono> // for std::chrono::seconds

using namespace std;
using namespace mcx_cpp;


int main() {
    // ADD CONNECTION CODE HERE FROM PREVIOUS EXAMPLE

    // Subscribing to the parameters
    auto subscription_done = sub.subscribe({"root/ManipulatorControl/manipulatorToolPoseActual"}, "group1", 10);
    auto subscription = subscription_done.get();

    if (subscription.status() != StatusCode::OK) {
        fprintf(stderr, "Subscription failed: %s\n", subscription.info().c_str());
        return 1;
    }
    printf("Subscribed to parameter updates successfully\n");
    subscription.notify([](auto params){
        for (const auto& param : params) {
            std::array<double, 6> pose{};
            param.value(pose); // fills the pose array
            print_array(pose);
        }
    });
    this_thread::sleep_for(chrono::seconds(5));
    sub.unsubscribe(subscription); // Unsubscribe from the updates
}

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.

As the notify function runs in a separate thread, you need to keep the main application running (here for 5 seconds) to continue receiving updates. You also do not want to create too many subscriptions with each one parameter, as this will create new threads for each subscription. Instead, you can subscribe to multiple parameters in a single subscription by passing a list of parameter paths to the sub.subscribe() function. As they are grouped together the data will also be synchronized. Here is an example of subscribing to both the end-effector position and the joint positions of the robot in a single subscription:

#include <mcx-cpp/mcx-cpp.h> // Include the Motorcortex C++ library
#include <cstdio> // for printf
#include <string> // for std::string
#include <thread> // for std::this_thread::sleep_for
#include <chrono> // for std::chrono::seconds

using namespace std;
using namespace mcx_cpp;


int main() {
    // ADD CONNECTION CODE HERE FROM PREVIOUS EXAMPLE

    // Subscribing to the parameters
    auto subscription_done = sub.subscribe({"root/ManipulatorControl/manipulatorToolPoseActual", "root/ManipulatorControl/jointPositionsActual"}, "group2", 10);
    auto subscription = subscription_done.get();

    if (subscription.status() != StatusCode::OK) {
        fprintf(stderr, "Subscription failed: %s\n", subscription.info().c_str());
        return 1;
    }
    printf("Subscribed to parameter updates successfully\n");
    subscription.notify([](const std::vector<mcx_cpp::GetParameter>& params){
        for (const auto& param : params) {
            std::string path = param.path();
            if (path == "root/ManipulatorControl/manipulatorToolPoseActual") {
                std::array<double, 6> pose{};
                param.value(pose);
                printf("manipulatorToolPoseActual: ");
                print_array(pose);
            } else if (path == "root/ManipulatorControl/jointPositionsActual") {
                std::array<double, 6> joints{};
                param.value(joints);
                printf("jointPositionsActual: ");
                print_array(joints);
            }
        }
    });
    this_thread::sleep_for(chrono::seconds(5));
    sub.unsubscribe(subscription); // Unsubscribe from the updates
}

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.

In the c++ client library, the first step after connection is to load the parameter tree from the Motorcortex Application.

int main() {
    // ADD CONNECTION CODE HERE FROM PREVIOUS EXAMPLE

    auto parameter_tree_done = req.getParameterTree(1000);
    auto parameter_tree_msg = parameter_tree_done.get();
    if (parameter_tree_msg.status() != StatusCode::OK) {
        fprintf(stderr, "Failed to get parameter tree: %s\n", parameter_tree_msg.info().c_str());
        return 1;
    }
    parameter_tree.load(parameter_tree_msg);
}

Afterwards, parameters can be set with req.setParameter. Let us start up the robot!

McxStatus result = req.setParameter("root/Logic/stateCommand", {2}).get();
printf("Set parameter result: %s\n", result.info().c_str());

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
for (int i = 0; i < 5; ++i) {
    auto reply0 = req.setParameter("root/UserParameters/IO/gripper", 0).get();
    std::this_thread::sleep_for(std::chrono::seconds(1));
    auto reply1 = req.setParameter("root/UserParameters/IO/gripper", 1).get();
    std::this_thread::sleep_for(std::chrono::seconds(1));
}
printf("Finished gripper open/close\n");

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:

// Set to manual joint mode
auto reply_mode = req.setParameter("root/Logic/modeCommand", 3).get();
printf("Set to manual joint mode: %s\n", reply_mode.info().c_str());
std::this_thread::sleep_for(std::chrono::seconds(2));

// Set joint velocity
std::array<double, 6> joint_velocities = {0.1, 0.3, 0.0, 0.0, 0.0, 0.3};
auto reply_vel = req.setParameter("root/ManipulatorControl/hostInJointVelocity", joint_velocities).get();
printf("Set joint velocity: %s\n", reply_vel.info().c_str());

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.