Skip to content

ROS 2

Adamo connects to a robot’s ROS 2 graph directly. Two things come out of that:

  1. Video — a camera topic (sensor_msgs/Image, CompressedImage, or an H.264 stream) becomes a video track, hardware-encoded on the robot and streamed like any other camera.
  2. Control — operator input (gamepad, VR) arrives as native ROS 2 topics that any local node can subscribe to, with no Adamo code in your control stack.

The bridge runs inside the robot process and speaks DDS to the local graph, so it works with stock ROS 2 (Humble, Jazzy, Kilted) using the default middleware. It needs no rclpy, no changes to your ROS installation, and no extra process. Nothing ROS-formatted leaves the robot: camera frames are encoded before they are published, and operator input arrives already in ROS wire format.

  • ROS 2 nodes using a DDS middleware (the default on every current distro).
  • A ROS-enabled Adamo build.
  • ROS_DOMAIN_ID is honored; pass domain_id to override it.
import adamo
robot = adamo.Robot(api_key="ak_...", name="my-arm")
robot.attach_video(
"front",
ros="/camera/color/image_raw",
bitrate_kbps=4000,
fps=30,
)
robot.run()

The topic name selects the input handling:

Topic carriesHandling
Raw sensor_msgs/Image (rgb8, bgra8, mono8, nv12, …)Encoded directly
sensor_msgs/CompressedImage (JPEG, topics ending in /compressed)Decoded, then encoded
H.264 / ffmpeg_image_transport packetsDecoded, then re-encoded at your bitrate

If the inference picks wrong, set the type explicitly in the config file: ros_image, ros_compressed_image, ros_h264, ros_h264_transcode, ros_ffmpeg_packet, or ros_ffmpeg_packet_transcode. The non-_transcode H.264 variants forward the stream as-is instead of re-encoding — cheap, but bitrate and keyframes stay whatever the ROS publisher produces, so adaptive bitrate does not apply.

Only topics attached to a track are read from the graph. The bridge creates a DDS reader when the track starts and destroys it when the track stops, so an idle Adamo integration costs your camera publishers nothing.

Enable the control fan-out and operator input from operate.adamohq.com is published into the local graph:

robot = adamo.Robot(api_key="ak_...", name="my-arm")
robot.enable_ros_control()
robot.run()

Your nodes then subscribe like to any other topic:

ROS topicTypeSource
/joysensor_msgs/msg/JoyGamepad
/head_posegeometry_msgs/msg/PoseStampedVR headset pose
/controller/{left,right}geometry_msgs/msg/PoseStampedVR controller grip pose
/controller/{left,right}/tipgeometry_msgs/msg/PoseStampedVR controller aim pose
/controller/{left,right}/joysensor_msgs/msg/JoyVR controller axes and buttons
/teleop/clutchstd_msgs/msg/BoolVR clutch state
cmd_from_joy.py — plain rclpy, no Adamo imports
import rclpy
from rclpy.node import Node
from sensor_msgs.msg import Joy
rclpy.init()
node = Node("teleop")
node.create_subscription(Joy, "/joy", lambda msg: print(msg.axes), 10)
rclpy.spin(node)

A message for a topic that no local node subscribes to is dropped on the robot — fan-out is driven by your graph’s subscriptions. To additionally restrict which topics may be published at all, pass an allowlist:

robot.enable_ros_control(control_topics=["/joy", "/teleop/clutch"])
  • The bridge participates in local DDS discovery only. It does not extend your DDS domain off the robot, and remote operators cannot reach topics beyond the control fan-out.
  • One robot process runs one bridge; all ROS video tracks and the control fan-out share it.
  • Control topics follow the priority rules of the rest of the control plane — they are never queued behind video.