4 ROS2

With ROS you will be able to use fine grained controls and force feedback. At this time for ROS2, the level of control possible is Joint Trajectory Controller and Tooltip Pose in beta. ROS Nodes are not available at this time through the Standard Bots UI. ROS2 is not required for basic applications. The cobot can run predefined programs without ROS 2. ROS 2 is only needed for custom applications, integration with external systems, or advanced programming. Please reach out to learn more as this is in Private Beta.

4.1 ROS2 Setup

This procedure establishes deterministic, low-latency ROS2 communication with the robot. The runtime stack routes real-time commands across a direct Ethernet connection encapsulated within a Docker container. No prior Docker or ROS2 experience is required to complete this deployment.

Before you begin, make sure you have:

4.1.1 Docker Engine Installation and Host Configuration

Update the system package index and install the foundational dependencies:

sudo apt-get update
sudo apt-get install -y ca-certificates curl gnupg

Install Docker via the terminal:

sudo apt update
sudo apt install git docker.io docker-buildx -y

Grant the current user permission to manage Docker without elevated privileges:

sudo usermod -aG docker $USER
newgrp docker

Clone the Standard Bots ROS2 repository:

mkdir ~/robot_projects
cd ~/robot_projects
git clone https://github.com/standardbots/ros2-realtime-api.git
cd ros2-realtime-api

With the Ethernet cable connected between the host computer and the control box, identify the name of the active Ethernet interface:

ip a

The relevant interface name will typically begin with the letter e (for example, eth0, enx..., or ether1...). If the correct interface cannot be identified, disconnect the Ethernet cable and re-run the command to determine which interface is no longer present.

Once the interface name has been determined, open cyclonedds.xml in the ros2-realtime-api folder and update the value within the <NetworkInterface> element to reflect that interface name:

nano cyclonedds.xml

Press Ctrl + X to save and exit.

Build the Docker image using the Dockerfile provided in the repository with the build file inside the ros2-realtime-api folder:

./build.sh

4.1.2 IP Address Configuration

On the Ubuntu host, open System Settings and navigate to the wired connection settings. Set the IPv4 method to Manual and assign the following values:

On the robot tablet, open the Network Settings and switch the wired adapter from Automatic (DHCP) to Manual. Configure the following values:

Make sure you press the connect button after entering the values to save the IP addresses.

WARNING – Distinct IP Addresses Required

The host computer must not be assigned the same IP address as the robot (192.168.50.100). Both devices must occupy distinct addresses on the same subnet.

Save the changes, then navigate to the Developer API screen and ensure that Enable ROS2 Bridge is toggled ON.

4.1.3 Verifying the Network Connection

Disable the Ubuntu firewall to allow ROS communication to reach the host computer from the robot:

sudo ufw disable

Issue a ping request from the host to confirm the link is operational:

ping 192.168.50.100

A successful response will display sequential 64 bytes from 192.168.50.100... lines. Press Ctrl + C to terminate the request.

If the request stalls or drops packets and the control box has recently been power-cycled, disconnect the Ethernet cable, wait approximately two minutes for the system to fully initialize, and then reconnect the cable.

4.1.4 Launching the Docker Container

Launch the container using the helper script provided in the repository:

./run_shell.sh

4.1.5 Verifying ROS2 Topic Visibility

Once inside the container (the prompt will appear as root@...:/app#), point the session to the CycloneDDS configuration file:

export CYCLONEDDS_URI=file:///etc/standardbots/configuration/cyclonedds.xml

List the active topics:

ros2 topic list

Topics formatted as /bot_0sapi_.../... indicate a successful connection to the robot.

If only topics of the form /None/ro1/hardware/... are visible, the container can resolve the local middleware but cannot reach the robot. Common causes include:

4.1.6 Executing a Motion Script

Before executing any script that initiates motion of the arm, ensure that the script running through the ROS2 bridge unbrakes the robot using the Python SDK before sending any movement messages.

WARNING – Absolute Joint Targets

Motion scripts use absolute joint targets rather than relative offsets. All joints will move toward their target positions simultaneously upon execution. Ensure the workspace surrounding the arm is fully clear before launching the script.

Retrieve the bot ID, token, and URL from the API panel on the tablet, then execute:

python3 ./src/write_poses.py \
  --bot-id="bot_xxxxxxxxxxxxxxxxxxxx" \
  --token="xxxx-xxxxx-xxxxxxxx-xxxxxxxx" \
  --url="https://cbXXXX.sb.app"

The arm will trace a smooth trajectory to the target pose.

4.2 ROS2 Python Scripting

The sections that follow cover writing Python scripts against the RO1 ROS2 topics. Complete the ROS2 Setup section above before you start, because they assume the Developer API and the ROS2 bridge are already enabled and that your client environment is running. You will also need the robot URL and API token from the Configure Developer API screen for any script that moves the arm.

4.3 Before You Write Code

4.4 Topic Reference

All topics are namespaced by your bot ID: /<BOT_ID>/ro1/hardware/....

Topic Message type Direction Use
.../joint_state sensor_msgs/JointState Robot to you Live joint angles and velocities
.../pose geometry_msgs/PoseStamped Robot to you Live TCP pose
.../pose/write geometry_msgs/PoseStamped You to robot Command a target TCP pose
.../joint_trajectory trajectory_msgs/JointTrajectory You to robot Stream a joint trajectory to execute
.../jacobian std_msgs/Float64MultiArray Robot to you Jacobian, for advanced Cartesian control
.../end_effector_imu sensor_msgs/Imu Robot to you Wrist IMU

To see what is actually live, list the topics from a ROS2 shell with ros2 topic list.

4.5 The Mandatory Pattern: Enable, Work, Disable

Every motion script follows the same skeleton. Two ideas do all of the work: flip the control state with the SDK over HTTPS, then stream over ROS2. On exit, flip control back off so the arm does not stay live.

Connect the SDK and enable ROS2 control

from standardbots import StandardBotsRobot, models

sdk = StandardBotsRobot(
    url=ROBOT_URL,          # from Configure Developer API
    token=ROBOT_TOKEN,
    robot_kind=StandardBotsRobot.RobotKind.Live,   # .Simulated for a sim
)

with sdk.connection():
    sdk.movement.brakes.unbrake().ok()          # release the brakes
    sdk.ros.control.update_ros_control_state(   # hand control to ROS2
        models.ROSControlUpdateRequest(
            action=models.ROSControlStateEnum.Enabled,
        )
    )
    sdk.ros.status.get_ros_control_state().ok()  # confirm it took

The SDK connection only needs to be open to change the control state. The realtime data itself flows over ROS2 topics, so it is normal to close this with block and then do all of your publish and subscribe work outside of it. This is exactly what write_poses.py does.

Always disable on exit

Wrap the realtime loop in try and finally, and switch ROS2 control back to Disabled:

finally:
    rclpy.shutdown()
    with sdk.connection():
        sdk.ros.control.update_ros_control_state(
            models.ROSControlUpdateRequest(
                action=models.ROSControlStateEnum.Disabled,
            )
        )

Both calls hit the same REST endpoint that the SDK wraps: GET and POST /api/v1/movement/ros/state.

Match the publisher’s QoS

Do not skip this step. The hardware topics publish BEST_EFFORT, and a subscriber on the default RELIABLE profile will silently receive zero messages:

from rclpy.qos import QoSProfile, ReliabilityPolicy, HistoryPolicy

qos = QoSProfile(
    reliability=ReliabilityPolicy.BEST_EFFORT,
    history=HistoryPolicy.KEEP_LAST,
    depth=10,
)

4.6 Use Case A – Monitoring Live Joint Angles

Use this when you want to log or visualize what the arm is doing in real time, for telemetry, dashboards, or data capture. This is read-only, so no unbrake or enable step is needed. You only need to subscribe.

import rclpy
from rclpy.node import Node
from rclpy.qos import QoSProfile, ReliabilityPolicy, HistoryPolicy
from sensor_msgs.msg import JointState
from detect_bot_id import detect_bot_id


class ReadJointState(Node):
    def __init__(self, robot_id=None):
        super().__init__("read_joint_state")
        self.robot_id = robot_id or detect_bot_id(self)
        qos = QoSProfile(reliability=ReliabilityPolicy.BEST_EFFORT,
                         history=HistoryPolicy.KEEP_LAST, depth=10)
        self.create_subscription(
            JointState,
            f"/{self.robot_id}/ro1/hardware/joint_state",
            self.cb, qos)

    def cb(self, msg):
        print([v for v in msg.position])   # swap in your own processing


rclpy.init()
rclpy.spin(ReadJointState())

4.7 Use Case B – Reading the Live TCP Pose

This has the same shape as use case A, but subscribes to .../pose with geometry_msgs/PoseStamped. The message gives you the tool center point position (msg.pose.position.x, .y, and .z) and the orientation quaternion (msg.pose.orientation.w, .x, .y, and .z). This is the signal you close the loop on in use cases C and D.

4.8 Use Case C – Commanding a Cartesian Move

Use this to nudge the tool along an axis from wherever it currently is. This is the canonical sample, write_poses.py, and the template for any pose-streaming task.

The strategy is to subscribe to the current pose to learn the start pose, then publish a series of slightly shifted target poses to .../pose/write.

class WritePose(Node):
    def __init__(self, robot_id=None):
        super().__init__("write_pose")
        self.robot_id = robot_id or detect_bot_id()

        qos = QoSProfile(reliability=ReliabilityPolicy.BEST_EFFORT,
                         history=HistoryPolicy.KEEP_LAST, depth=10)

        # READ the current pose
        self.create_subscription(
            PoseStamped, f"/{self.robot_id}/ro1/hardware/pose",
            self.pose_callback, qos)

        # WRITE target poses
        self.publisher = self.create_publisher(
            PoseStamped, f"/{self.robot_id}/ro1/hardware/pose/write", 10)

        self.start_pose = None

    def pose_callback(self, msg):
        if self.start_pose is None:      # latch the first pose we see
            self.start_pose = msg.pose

    def start(self):
        while not self.start_pose:       # wait until we know where we are
            rclpy.spin_once(self, timeout_sec=0.5)

        for i in range(100):
            p = PoseStamped()
            p.header.frame_id = "world"
            # copy the start pose...
            p.pose.position.x = self.start_pose.position.x
            p.pose.position.y = self.start_pose.position.y
            p.pose.orientation = self.start_pose.orientation
            # ...then shift 1 mm along Z each step, 10 cm total
            p.pose.position.z = self.start_pose.position.z + i * 0.001
            self.publisher.publish(p)
            rclpy.spin_once(self, timeout_sec=0.2)   # paces the loop

Things to notice in this sample:

Because this script moves the arm, it takes the connection arguments it needs in order to unbrake and enable: --bot-id=<BOT_ID> --token=<TOKEN> --url=<ROBOT_URL>.

4.9 Use Case D – Closing the Loop on a Moving Target

Use this to follow a part on a conveyor, a detected object, or an external setpoint in real time, as in visual servoing, conveyor tracking, and fast-target following.

Use case C streams a path that was decided in advance. Real applications instead compute each new target from live input. The shape is identical, and only the line that decides the next pose changes:

def control_step(self):
    if self.latest_pose is None:
        return
    target = PoseStamped()
    target.header.frame_id = "world"

    # 1) get your external signal (camera detection, conveyor
    #    encoder, network setpoint, and so on) - your code:
    dx, dy, dz = self.get_target_offset()

    # 2) command current pose + correction
    target.pose.position.x = self.latest_pose.position.x + dx
    target.pose.position.y = self.latest_pose.position.y + dy
    target.pose.position.z = self.latest_pose.position.z + dz
    target.pose.orientation = self.latest_pose.orientation

    self.publisher.publish(target)


# drive control_step on a fixed-rate timer, not a for loop:
self.create_timer(0.02, self.control_step)   # 50 Hz example

Practical notes for tracking loops:

4.10 Use Case E – Streaming a Joint Trajectory

For planner-driven motion, publish trajectory_msgs/JointTrajectory to .../joint_trajectory with one or more JointTrajectoryPoint entries, each carrying positions plus time_from_start. Read .../jacobian and .../end_effector_imu when you need Cartesian-space or inertial awareness in your controller. The same enable and disable handshake and the same QoS rules described above still apply.

4.11 Troubleshooting

4.12 Known Limitations

4.13 Sample Scripts

The standardbots/ros2-realtime-api repository cloned during setup contains the samples referenced above: