Implement a new mc_rtc robot interface

An interface is the piece of software that connects mc_rtc to a robot (or a simulator). On every iteration it:

  1. reads the robot state (joint positions, velocities, torques, IMU, force sensors...)
  2. gives it to mc_rtc, and runs the controller
  3. sends the result (joint positions, velocities or torques) back to the robot

This tutorial shows how to write one for a new robot. There are two ways to do it:

  • Recommended: write a small driver plugin for URI (Unified Robot Interface). URI already contains everything that is not specific to your robot.
  • Legacy: write a dedicated interface executable that embeds mc_control::MCGlobalController. This is how most of the interfaces listed on the interfaces page are written.
This tutorial assumes that you are familiar with mc_rtc controllers and robot modules, and that you have a RobotModule for your robot (see this tutorial).

Writing a driver for URI

What is URI

URI splits the traditional interface into three parts:

  • uri manager: one process that runs a solver (mc_rtc) for all your robots. It is configured with the usual mc_rtc.yaml plus a Robots section.
  • uri interface: one generic process per robot. It runs on the robot's PC or on the control PC and loads your driver at startup.
  • a communication layer (Zenoh and FlatBuffers) between the two. You never have to use it directly.

The only part you write is the driver: a shared library that wraps your robot's SDK a templated driver. It does not depend on mc_rtc at all. As a bonus, a controller can drive several different robots at once, each at its own control rate, and robots can live on different computers.

Install URI

URI is installed next to mc_rtc. If you build mc_rtc with mc-rtc-superbuild, enable the WITH_URI option:

cd mc-rtc-superbuild
cmake --preset relwithdebinfo -DWITH_URI=ON
cmake --build --preset relwithdebinfo

Other installation methods (manual, devcontainer) are described in the URI documentation.

Remember the install prefix you used. uri interface only looks for drivers in <prefix>/lib/robot_interface, so your driver must be installed with the same prefix.

Check if a driver already exists

Drivers already exist for Universal Robots (RTDE), Kinova Gen3, xArm, any robot with a ros2_control controller manager, and more. See the list of supported robots. If your robot is there, skip to Use the driver with mc_rtc.

Create the driver project

Additional information about this topic can be found on URI website (see Create a new robot driver).

URI generates a ready-to-build project for you:

uri create_new_driver MyRobot <new_driver_path>

This creates a standalone CMake project, my_robot_driver, with a class named RobotDriverMyRobot. It already compiles: it is a skeleton that reports an empty state and ignores commands.

Implement the driver

Open src/RobotDriverMyRobot.cpp and fill in the methods. The ones you will implement in most cases are:

Method What it must do
constructor Connect to the robot using the ip, port and optional config_path you receive. Throw if the connection fails.
sync() Block until the robot publishes a new state. This sets the control rate: use a blocking read from the SDK, not a sleep().
getActualQ() Joint positions [rad]. Return an empty vector if no state is available yet.
getActualQd() Joint velocities [rad/s] (optional)
getJointTorques() Joint torques [Nm]
servoJ(q) Send joint positions [rad] to the robot
speedJ(qd) Send joint velocities [rad/s] to the robot
tauJ(tau) Send joint torques [Nm] to the robot
getIMUs(), getForceSensors() Sensor readings (optional)

You only need to implement the command methods for the control mode your robot supports (servoJ for position control, and so on). The others can throw or do nothing.

Three rules matter more than everything else:

  • Joint order: every vector, in and out of the driver, follows the ref_joint_order of your RobotModule. If your SDK uses a different order, reorder in the driver.
  • Units: rad, rad/s, Nm, N. Quaternions are (w, x, y, z).
  • Sensors: the names used as keys in getIMUs() and getForceSensors() must match the body sensors and force sensors of your RobotModule.

If your SDK needs extra libraries, add them to the generated CMakeLists.txt files with the usual find_package and target_link_libraries. Settings that do not fit in ip and port (topic names, gains...) can go in a YAML file of your own, whose path is passed to the constructor as config_path.

Build and install

Build the driver with the same prefix as URI (and mc_rtc):

cd my_robot_driver
cmake -S . -B build \
  -DCMAKE_PREFIX_PATH=/path/to/install \
  -DCMAKE_INSTALL_PREFIX=/path/to/install
cmake --build build
cmake --install build

The generated source keeps the line ROBOT_DRIVER_EXPORT_ABI_VERSION(). Do not remove it: URI refuses to load a driver without it. When URI changes the driver interface, drivers built against an older version fail to load with a clear message and must be rebuilt.

Test the driver on its own

Before involving mc_rtc, check that the driver connects and that the state looks right. Write a small file with the robot's address:

robot_interface:
  driver: RobotDriverMyRobot
  ip: 192.168.1.10
  port: 0

Then run the viewer:

uri viewer -c viewer.yaml

It loads your driver exactly as uri interface does and prints joint positions, velocities and torques, and the time spent in sync(). Move the robot by hand (or in simulation) and check the joint order, signs and units. The viewer never sends any command, so it is safe to use on a real robot.

Use the driver with mc_rtc

Add your robot to the Robots section of an mc_rtc.yaml. Everything else (MainRobot, Enabled, observers, plugins...) keeps its usual meaning:

MainRobot: MyRobot
Enabled: Posture
Timestep: 0.005            # mc_rtc runs at 200 Hz

Robots:
  my_robot:                # robot name
    module: MyRobot        # mc_rtc RobotModule
    controller:
      mode: position       # position | velocity | torque
      time_step: 0.001     # the robot's own control period (1 kHz)
    network_interface:
      protocol: zenoh
    robot_interface:
      driver: RobotDriverMyRobot
      ip: 192.168.1.10
      port: 0
      autostart: true

Some entries deserve an explanation:

  • module is the mc_rtc RobotModule of your robot.
  • controller.mode is the type of command sent to the robot: position, velocity or torque. It selects which of servoJ, speedJ and tauJ is called.
  • controller.time_step is the control period of the robot. Timestep (mc_rtc) must be an exact multiple of it. In the example, the robot runs at 1 kHz and mc_rtc at 200 Hz; the last command is sent again in between.
  • robot_interface.driver is the class name of your driver. ip and port are always required, even if your driver ignores them.
  • autostart: true makes uri manager start uri interface itself. Use it when the robot is reachable from the control PC.

Now start the controller with uri manager instead of an interface executable:

uri manager -c mc_rtc.yaml

You should see the driver being loaded, the robot initialized from its real joint positions, and mc_rtc starting your controller. As usual, you can interact with the controller using any mc_rtc GUI client.

The controller only starts once the first state of every robot has been received, so the robot never jumps toward a default posture.

Robot on a different computer

If the robot is connected to its own PC (e.g. a real-time PC next to the arm), remove autostart and install the driver on that PC. Start uri interface there before uri manager. It only needs to know the robot's name:

# my_robot_interface.yaml, on the robot PC
name: my_robot             # same as the key under Robots:
network_interface:
  protocol: zenoh
# On the robot PC
uri interface -c my_robot_interface.yaml
# On the control PC
uri manager -c mc_rtc.yaml

The driver name, IP address and control mode are sent by the manager, so you can change drivers without touching the robot's PC. If the connection is lost, the manager stops the controller and the interface holds the robot in place.

Several robots

Add more entries under Robots. Each robot gets its own driver and its own control rate, and all of them are controlled by the same mc_rtc controller. The base key copies another entry so you only write what differs.

Going further

This tutorial only covers the essentials. The URI documentation has more details on:

Legacy: a dedicated interface executable

This is the historical way to integrate a robot with mc_rtc. It is still supported, but it is discouraged for new robots.

In this approach you write an executable that links with mc_rtc and embeds a mc_control::MCGlobalController. It owns the whole real-time loop: communication with the robot, timing, and calling mc_rtc. Each iteration looks like this:

// Sketch of a dedicated interface
mc_control::MCGlobalController controller("/path/to/mc_rtc.yaml");
auto & robot = controller.robot();

// 1. Initialize the controller from the real robot configuration
controller.init(readJointPositions());

// 2. Real-time loop, paced by the robot
while(running)
{
  waitForNextRobotState();

  // 3. Feed mc_rtc with the sensors
  controller.setEncoderValues(readJointPositions());
  controller.setJointTorques(readJointTorques());
  controller.setSensorOrientations(Accelerometer);

  // 4. Run one iteration of the controller
  if(controller.run())
  {
    // 5. Send the result to the robot
    sendJointPositions(robot.mbc().q);
  }
}

The functions you will use most are:

Compared to a URI driver, a dedicated interface has several drawbacks:

  • Every robot needs its own executable that links with mc_rtc and its dependencies, and often its own build of them.
  • Timing, safety (connection loss, holding the robot) and multi-rate handling are re-implemented each time.
  • Controlling several robots with different SDKs or rates from one controller is not supported out of the box.
  • The robot's SDK must run on the same machine as mc_rtc.

Existing interfaces (see the interfaces page) are good examples to read if you need to maintain or port one. If you do need a new standalone interface, for example to integrate a simulator with its own main loop, you can start from one of them.