Back to Integrations
/ INTEGRATION · Middleware and tooling

ROS 2

Record robot demonstrations from a live ROS 2 graph with rosbag2, avoid the QoS and clock pitfalls that corrupt recordings, and export to training formats.

Updated Aug 20266 min read
SHORT ANSWER

ROS 2 is the pub/sub middleware most manipulation and mobile-robot stacks run on, and rosbag2 is its native recorder. Demonstration data collection means picking the right topics and QoS overrides, keeping every message on one clock, and converting the resulting bag into a training format like LeRobotDataset or RLDS rather than training against raw ROS messages directly.

ROS 2 is the publish/subscribe middleware underneath most manipulation arms, mobile bases, and humanoid stacks — nodes exchange typed messages over topics, and almost everything a robot produces during a demonstration passes through that graph at some point. For data collection, ROS 2 is rarely the training format itself; it's the layer you record from, using rosbag2, ROS 2's built-in recorder, and then convert into whatever a policy actually trains on.

How the data gets there

ros2 bag record subscribes to a set of topics and writes every message to disk with its arrival timestamp, in MCAP or the legacy sqlite3 format:

# Record everything, with dynamic discovery of new topics
ros2 bag record -a -o demo_episode_014
 
# Record an explicit topic set (recommended for demonstration data)
ros2 bag record \
  --storage mcap \
  -o demo_episode_014 \
  /wrist_cam/image_raw/compressed \
  /joint_states \
  /tf \
  /tf_static \
  /gripper_cmd

-a records every topic the graph exposes and keeps discovering new ones as nodes come up — convenient for exploratory sessions, but it also captures diagnostics, logging, and camera streams you didn't mean to keep. For a repeatable demonstration-collection protocol, an explicit topic list (or --regex / --exclude) is the more disciplined choice; disable the live-discovery behavior with --no-discovery if you need a strictly fixed set for a reproducibility test.

A demonstration episode is rarely one message type. The topics that typically constitute one:

  • Visionsensor_msgs/Image or, more commonly on bandwidth-constrained links, sensor_msgs/CompressedImage, one topic per camera.
  • Proprioceptionsensor_msgs/JointState for joint position, velocity, and effort.
  • End-effector posegeometry_msgs/PoseStamped when the stack tracks a Cartesian target directly, alongside or instead of joint state.
  • Kinematic treetf2_msgs/TFMessage on /tf and /tf_static, which carries the transforms needed to relate camera frames, gripper frames, and the base frame to each other after the fact.

QoS overrides that actually matter

ros2 bag record tries to match the QoS a publisher offers automatically, but two situations still need an explicit override, passed as --qos-profile-overrides-path:

# qos_overrides.yaml
/wrist_cam/image_raw/compressed:
  reliability: best_effort
  durability: volatile
  history: keep_last
  depth: 5
/tf_static:
  durability: transient_local
  history: keep_all

Camera topics are usually published best_effort with a shallow keep_last history — overriding the recorder's subscription to match avoids a reliable subscription silently throttling a high-rate image publisher. /tf_static is the opposite case: it's published once with transient_local durability so late-joining subscribers still receive it, and a recorder that doesn't match that durability can start after the one-shot publish and simply never see it.

Clock and tf: where recordings quietly break

Two related failure modes show up constantly in ROS 2 demonstration data and neither one throws an error.

use_sim_time inconsistency. Every node has a use_sim_time parameter, defaulting to false, that has to be set before the node starts — it's not something you flip live and expect nodes to pick up mid-run. If some nodes on a rig honor sim time and others use the wall clock (common when a simulator, a recorder, and a real driver share a machine), their message timestamps land on two different clocks that both look reasonable in isolation but don't correspond to the same instant.

Recording against /clock before it's publishing. ros2 bag play --clock publishes a /clock topic so any node with use_sim_time:=true derives time from playback instead of the wall clock. If you record while replaying — a common setup for regression-testing a perception pipeline against old episodes — and the recorder starts before the first /clock message arrives, messages written in that window get stamped with epoch zero (January 1, 1970) rather than a sane timestamp, corrupting the start of the recording. Start the clock source first, confirm /clock is publishing, then start the recorder.

Replaying into a graph and converting to training formats

ros2 bag play republishes a recording's messages onto their original topic names at (by default) the rate they were recorded:

ros2 bag play demo_episode_014 --clock --rate 1.0

Because it republishes on the same topic names, playing a bag into a graph that also has a live robot running on those topics causes both sources to collide — remap topics or play into an isolated namespace when you need a recording alongside live nodes, for example replaying a reference episode next to a policy under evaluation.

For training, the bag itself is the wrong shape: it's a topic-indexed, timestamp-ordered log, not the shuffled, batched, per-episode structure a dataloader wants. The conversion path reads messages back out with the mcap or rosbags Python libraries and writes a LeRobotDataset or RLDS episode per recorded session — see choosing a robot data format for how to pick between them. Keep the original bag; regenerating a training export from it is a scripted rerun, regenerating the bag from a training export is not possible once you've discarded raw fields in conversion.

MCAPROS 2 bag (sqlite3)LeRobotRLDS

The workflow in practice

A team collecting demonstrations on a ROS 2 stack typically settles into a routine that looks like this:

  1. Fix the topic list and QoS overrides once, in a launch file or a checked-in YAML, rather than re-deriving them per session — this is what makes episode N and episode N+40 actually comparable.
  2. Record to MCAP per episode, one bag per successful (or explicitly labeled failed) attempt, with episode metadata — task, operator, success — captured alongside rather than reconstructed from filenames later.
  3. Spot-check in Foxglove against the live topics or the freshly recorded bag before ending a session, catching a disconnected camera or a stale /tf_static early rather than after forty episodes.
  4. Batch-convert at the end of a session, not per-episode, into the training format the policy code expects, so the conversion script only has to be debugged once against a representative pile of bags instead of live during collection.
  5. Archive the bags, not just the converted dataset — MCAP's embedded schemas mean a bag from a workspace that no longer builds is still readable years later, which the converted training format alone doesn't guarantee if its own internals change shape.

Telemanual's ROS 2 adapter follows the same shape from the other direction: it subscribes into an existing graph, handles the QoS matching and clock bookkeeping described above per rig, and streams straight to MCAP or a converted export without a separate manual conversion pass.

Gotchas

  • -a is not a substitute for a topic contract. It's convenient for a first exploratory recording, but a demonstration-collection protocol that changes which topics get recorded from session to session makes datasets impossible to compare.
  • QoS mismatches drop messages, they don't error. A recorder subscribed with the wrong reliability or durability policy against a given publisher can silently receive fewer messages than were published, with no exception raised anywhere.
  • use_sim_time is not dynamically reconfigurable on most nodes in practice — set it before launch, not after, and verify it rather than assuming a launch argument propagated everywhere you expected.
  • Bags are large. Uncompressed camera streams dominate file size; MCAP's per-chunk zstd compression helps, but --max-bag-size to split long sessions into seekable chunks is worth setting before a collection run, not after the first unopenable multi-gigabyte file.

KEY FACTS

DEFAULT BAG FORMAT
MCAP, default rosbag2 storage plugin since ROS 2 Iron (2023)
CORE DEMONSTRATION TOPICS
camera image, /joint_states, /tf + /tf_static, gripper/end-effector command
TOPIC DISCOVERY
ros2 bag record -a polls for new topics live; disable with --no-discovery
COMMON EXPORT TARGETS
LeRobotDataset, RLDS, or archived as MCAP

/ QUESTIONS

Frequently asked

Put this into practice.

Tell us what your robots need to learn. We will scope the rig, the operators, the protocol, and the first datasets — usually in one call.