ROS 2¶
Three ROS 2 transports, when to use each, one operator gate, what a ROS 2 node can still tell apart.
This page shows the three ways an agent or a Robot reaches a ROS 2 graph, which fits your machine, how a real arm joins the graph in both directions, and where the operator gate sits on each.
from strands import Agent
from strands_robots import use_ros, use_rosbridge, use_rtps
agent = Agent(tools=[use_rtps]) # no ROS install needed on this machine
agent("List the topics on the graph, then echo /odom once.")
Three transports¶
| tool | wire | needs on this machine | reaches | verbs |
|---|---|---|---|---|
use_ros |
in-process rclpy |
a sourced ROS 2 distro (source /opt/ros/jazzy/setup.bash); rclpy is not on PyPI |
any ROS 2 graph, with services and actions | status, list_topics, list_nodes, list_services, list_actions, info, echo, publish, service_call, action_send_goal |
use_rosbridge |
WebSocket to rosbridge_server on the robot |
pip install 'strands-robots[rosbridge]' (roslibpy), nothing else; works from macOS and CI |
ROS 1 and ROS 2 robots running rosbridge with rosapi; port 9090 |
status, list_topics, list_services, echo, publish, service_call |
use_rtps |
raw DDS/RTPS as a first-class participant | pip install 'strands-robots[ros2]' (cyclonedds); no distro, no rclpy |
every ROS 2 distro (Humble, Jazzy, Rolling) over one implementation; topics only | status, types, advertise, publish, subscribe, echo |
Pick use_ros on the robot or a ROS workstation for services or actions; use_rosbridge from a laptop when the robot runs rosbridge; use_rtps with no ROS install near the agent, or when the agent must be a robot: an RTPS participant advertises topics a node consumes and subscribes to command topics.
Types are resolved dynamically on use_ros (rosidl_runtime_py, any interface installed in the distro) and on use_rosbridge (ROS 1 style two-segment names, geometry_msgs/Twist). use_rtps must own a type definition locally, so it ships a curated IDL bundle of common messages (strands_robots.rtps.idl, listed by action="types"); a custom message waits on dynamic types in cyclonedds. Graph metadata differs too (below).
rosbridge is unauthenticated by default. Use it on a network you trust.
The gate is the same on all three¶
A publish, service_call or action_send_goal aimed at a blocklisted name goes through strands_robots._command_gate.gate_command, whichever transport carries it. The blocklist matches the final path segment, so /cmd_vel covers /robot1/cmd_vel:
/cmd_vel, /cmd_vel_unstamped, /manual_drive, /joint_command, /joint_trajectory, /joint_trajectory_controller/joint_trajectory, /emergency_stop, /e_stop, /motor_enable, /enable_motor, /disable_motor, /vehicle_state, /enable_state, /navigate_to_pose, /follow_path.
Reads are never gated, nor is use_rtps's advertise (it creates a publisher and writes nothing). STRANDS_ROS2_COMMAND_ALLOW pre-approves names, comma-separated, by base name: a bare /cmd_vel lifts the gate on every namespaced /cmd_vel, so write /robot_a/cmd_vel to scope it (* matches nothing here on purpose); BYPASS_TOOL_CONSENT=true lifts the gate with a warning. Every transport asks the gate after the backend probe and argument checks and before any lock or dial (the operator gate).
A real arm on the graph¶
arm = Robot("so101", mode="real", port="/dev/ttyACM0", driver="lerobot",
ros2_bridge=True, ros2_transport="rtps", ros2_domain=0, ros2_commands=True,
dds_security_config={"identity_ca": "file:ca.pem", "certificate": "file:arm.pem",
"private_key": "file:arm.key", "governance": "file:gov.p7s",
"permissions": "file:perm.p7s"})
ros2_bridge=True on the lerobot-backed Robot publishes /<robot>/joint_states (sensor_msgs/msg/JointState) and /<robot>/<camera>/image_raw (sensor_msgs/msg/Image, rgb8) from the control loop, and subscribes /<robot>/joint_command (JointState), forwarding each into send_action so MoveIt or teleop can drive the arm. ros2_transport is "rclpy" (HardwareRosBridge) or "rtps" (HardwareRtpsBridge); ros2_commands=False is publish-only; joint_limits= clamps inbound targets per joint.
On the rtps transport an enabled command surface lets any DDS participant on the domain move the arm, so it requires dds_security_config (identity CA, certificate, private key, governance, permissions, each a path or file:/data: URI) or the explicit opt-out STRANDS_ROS2_BRIDGE_I_KNOW_THIS_IS_INSECURE=1. A missing rclpy on the rclpy transport is a named refusal suggesting ros2_transport='rtps'.
A sim Robot(..., ros2_bridge=True) publishes joint_states and image_raw over rclpy only.
What a ROS 2 node can still tell apart¶
The payload is identical; the graph metadata is not. The rtps bridge is a bare DDS participant: ros2 topic echo decodes it field for field, ros2 node list omits it, and ros2 topic info -v shows Node name: _CREATED_BY_BARE_DDS_APP_, an INVALID type hash and KEEP_LAST (1) history with no depth knob. Tooling that waits for a node or a matching type hash wants rclpy.
Linux aarch64 (Jetson)¶
No cyclonedds release ships a Linux aarch64 wheel, so on a Jetson pip install 'strands-robots[ros2]' builds the sdist against a Cyclone DDS C install named by CYCLONEDDS_HOME:
# (a) a sourced ROS 2 distro already ships it
sudo apt install ros-$ROS_DISTRO-cyclonedds
CYCLONEDDS_HOME=/opt/ros/$ROS_DISTRO pip install 'strands-robots[ros2]'
# (b) no ROS 2: build from https://github.com/eclipse-cyclonedds/cyclonedds
cmake -S cyclonedds -B cyclonedds/build -DCMAKE_INSTALL_PREFIX=$HOME/cyclonedds && cmake --build cyclonedds/build --target install
export CYCLONEDDS_HOME=$HOME/cyclonedds # keep exported at runtime
pip install 'strands-robots[ros2]'
The loader tries a wheel's bundled copy, then $CYCLONEDDS_HOME/lib, then the system path, so an install to the default prefix that ldconfig knows needs no variable; a set but stale CYCLONEDDS_HOME fails hard with CycloneDDSLoaderException instead of falling back.
A ROS robot on the mesh¶
RosBridgedRobot (rclpy), RosbridgeRobot (WebSocket) and RtpsRobot (DDS) in strands_robots.mesh wrap a mobile base that speaks /cmd_vel and /odom as a mesh peer answering status, execute and stop next to the arms (fleet); AckermannRosRobot does the same for a car-like base. The Yahboom M3 Pro driver is the shipped example of a whole robot driven through its ROS 2 graph (drivers).