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.
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:
Update the system package index and install the foundational dependencies:
sudo apt-get update
sudo apt-get install -y ca-certificates curl gnupgInstall Docker via the terminal:
sudo apt update
sudo apt install git docker.io docker-buildx -yGrant the current user permission to manage Docker without elevated privileges:
sudo usermod -aG docker $USER
newgrp dockerClone the Standard Bots ROS2 repository:
mkdir ~/robot_projects
cd ~/robot_projects
git clone https://github.com/standardbots/ros2-realtime-api.git
cd ros2-realtime-apiWith the Ethernet cable connected between the host computer and the control box, identify the name of the active Ethernet interface:
ip aThe 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.xmlPress 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.shOn the Ubuntu host, open System Settings and navigate to the wired connection settings. Set the IPv4 method to Manual and assign the following values:
192.168.50.101255.255.255.0On the robot tablet, open the Network Settings and switch the wired adapter from Automatic (DHCP) to Manual. Configure the following values:
192.168.50.100255.255.255.0192.168.50.1Make 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.
Disable the Ubuntu firewall to allow ROS communication to reach the host computer from the robot:
sudo ufw disableIssue a ping request from the host to confirm the link is operational:
ping 192.168.50.100A 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.
Launch the container using the helper script provided in the repository:
./run_shell.shOnce 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.xmlList the active topics:
ros2 topic listTopics 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:
CYCLONEDDS_URI environment variable was not
exported in the current session.cyclonedds.xml does not match the
output of ip a.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.
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.
BEST_EFFORT. A subscriber left on the default
RELIABLE profile silently receives zero messages. This is
the single most common failure./<BOT_ID>/ro1/hardware/.... Call
detect_bot_id() to discover it at runtime.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.
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 tookThe 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,
)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())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.
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 loopThings to notice in this sample:
frame_id = "world". Targets are
expressed in the world frame.- i * 0.001 to reverse direction.spin_once(timeout_sec=0.2) paces the
publish loop and lets callbacks run. Tighten it for a higher command
rate.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>.
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 examplePractical notes for tracking loops:
create_timer at a fixed rate
rather than a for loop. It is steadier, and it decouples
your control rate from message arrival.dx,
dy, and dz. Large jumps from a noisy detector
translate directly into aggressive arm motion.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.
BEST_EFFORT,
and a default RELIABLE subscriber gets silence. Use the
BEST_EFFORT profile shown above.detect_bot_id
hangs. Confirm that the ROS2 bridge is enabled on the robot and
that your client environment is set up, as described in the ROS2 Setup
section. List what is live with ros2 topic list.NameError: name 'os' is not defined.
write_poses.py reads ROBOT_URL and
ROBOT_TOKEN through os.getenv but never
imports os. Either pass --url and
--token on the command line, or add import os
if you want the environment variable fallback.unbrake() and Enabled calls succeeded, and
that there are no active errors on the Move page.finally block that sets control to Disabled
was skipped. Always disable on exit.The standardbots/ros2-realtime-api repository cloned
during setup contains the samples referenced above:
src/write_poses.py — the Cartesian pose-streaming
sample from use case C.src/read_joint_states.py — the joint state subscriber
from use case A.src/detect_bot_id.py — runtime bot ID discovery.