#4368·lerobot

RFC: ROS 2 integration strategy (ecosystem survey and proposed direction)

Author: alejodosrCreated Aug 7, 2026Updated Sep 16, 2026
LabelsdocumentationenhancementquestionpoliciesdatasetsimulationtestsCI

tldr

The release proposal (#3832) lists "ROS2 integration exploration" as a community-open item. Six community projects already connect LeRobot and ROS 2, each covering a different piece, with incompatible approaches and only two of them listed in the official docs. This issue surveys them, names the gaps, and proposes five deliverables. React if this is worth pursuing, and comment with which deliverables you'd use and what your setup is (see Community participation).

Why now

  • The release proposal (#3832) leaves ROS 2 integration open to the community, and the team has signaled openness to the direction being driven by what people actually find useful.
  • The plugin system (lerobot_robot_*, lerobot_teleoperator_*, lerobot_camera_*) provides a clean extension point, and the README already mentions community ROS 2 bridges without listing them.
  • ROS 2 is where most deployed robots, lab setups and sim stacks live. A lab with a ROS 2 robot that wants to try ACT, pi0, SmolVLA, etc. today has to pick between incompatible community bridges or write their own. (Context: I contribute to LeRobot, see #4323, and have worked with ROS/ROS 2 for years, mostly on aerial robots, sim-to-real and perception)

Ecosystem survey

Everything below comes from each project's README, package metadata and repo data (checked Aug 2026). Corrections welcome, especially from the authors.

Robot bridges (online): ycheng517/lerobot-ros (arms, most complete, pinned to lerobot 0.4.x), ngres/leros2 (config-driven plugin + bag converter, pinned to current 0.6.x), ROBOTIS-GIT/lerobot_robot_ros2_zenoh (Zenoh transport, alpha), sacovo/lerobot_ros (LeRobot-inside-ROS, recorder + policy nodes), astroyat/lerobot-ros (LeKiwi base + Nav2). Dataset converters (offline): forge (multi-format), openral-dataset (recorder with LeRobot v3 sink). Full tables below.

Full table: robot bridges (teleoperation, recording, inference) (click to unfold)
Project What it does Transport Scope Stars Last push License Notes
ycheng517/lerobot-ros LeRobot robot plugin wrapping any ros2_control / MoveIt arm (joint position, joint trajectory, MoveIt Servo EE velocity, 2 gripper modes), plus gamepad/keyboard teleoperators rclpy Arms only 211 Nov 2025 Apache-2.0 Most complete arm bridge. Jazzy only, conda workaround needed for rclpy. Pins lerobot>=0.4.0,<0.5.0, doesn't install against current 0.6.x
ngres/leros2 LeRobot robot + teleoperator plugins with a composable config-driven topic↔feature mapping (joint positions, EE poses, torques/wrenches, grippers, compressed images), plus a leros2-convert CLI (bag → dataset) rclpy Arms (composable arm + gripper + camera setups) 5 Jul 2026 Apache-2.0 Only project pinned to current lerobot (>=0.6,<0.7). Not on PyPI, installs from source. Listed in the official third-party docs. Install-tested against lerobot 0.6.0 (plugin discovery level); README install commands reference unpublished packages, source checkout required
ROBOTIS-GIT/lerobot_robot_ros2_zenoh LeRobot robot plugin talking to ROS 2 topics through Zenoh. Joint states in, joint trajectories out (split-controller support), ROS image topics as cameras Zenoh (no ROS 2 install on LeRobot side) Arms (preset for ROBOTIS FFW BG2) 9 Jul 2026 Apache-2.0 Self-labeled alpha. Needs a Zenoh router and a zenoh_ros2_sdk not on PyPI. lerobot dependency unpinned. README documents plugin-discovery problems with the PyPI lerobot build
sacovo/lerobot_ros ROS 2 colcon package: dataset_recorder node (records LeRobotDatasets from topics, controlled via ROS services) and policy_controller node (runs LeRobot policies with topics as observations/actions), plus replay and SO-101 nodes rclpy, colcon workspace Any topic-based setup, SO-101 built in 5 Aug 2026 MIT LeRobot inside ROS instead of ROS inside LeRobot. Closest existing thing to policy-as-a-node. Actively developed (FHNW Rover project)
astroyat/lerobot-ros LeKiwi mobile base over /cmd_vel, LIDAR streaming, rf2o odometry, SLAM Toolbox + Nav2 bringup. Arm teleop WIP rclpy One mobile base (LeKiwi) 36 Feb 2026 Apache-2.0 Only base bridge found. Needs a patched lerobot fork, doesn't work against upstream
Full table: dataset converters (offline) (click to unfold)
Project What it does Direction Stars Last push License Notes
arpitg1304/forge Multi-format toolkit (pip install forge-robotics): RLDS, LeRobot v2/v3, HDF5, MCAP, Zarr, ROS1/ROS2 bags, through a shared Episode/Frame intermediate representation. Episode quality scoring bag → dataset, dataset → other formats 161 Jul 2026 MIT Broadest converter found. LeRobot v3 output not audited against current spec
openral-dataset (PyPI) RolloutRecorder with pluggable sinks: LeRobotDataset v3 sink writing through the actual LeRobotDataset API, and a rosbag2/mcap sink live recording → dataset, → bag n/a (PyPI, no repo listed) v0.2.0 Apache-2.0 Part of a bigger framework. Going through the real lerobot API avoids format drift

leros2 also ships a bag → dataset CLI, see the bridges table.

Deployments in the wild: ROS2SmolVLA (Univ. of Augsburg, arXiv:2608.23320) runs SmolVLA on an industrial UR10e through ROS 2 Jazzy: a fork of ycheng517/lerobot-ros modified for Cartesian velocity actions, plus ros2smolvla_interface_camera, a ROS image topic → LeRobot camera adapter (the one lerobot_camera_*-shaped piece found).

Gaps

  • No path I could install against current lerobot. The most adopted bridge is pinned to 0.4.x and inactive for ~9 months. The base bridge needs a patched fork. The Zenoh plugin needs a non-PyPI SDK. The colcon package means adopting its workspace and config format. In my testing, no bridge installs against lerobot 0.6.x with pip and standard instructions — corrections welcome if I missed a working setup. Edit (Aug 13): verified against lerobot 0.6.0 at the plugin discovery level (source install; the README install path is currently broken, packages not on PyPI). Details in the comments.
  • Every bridge hardcodes one form factor. Two arm bridges, one LeKiwi bridge, nothing generic, even though both sides already have the right abstractions (see layer 2).
  • No shared conventions. Each bridge invents its own topic↔feature mapping, QoS settings and timestamp handling, so datasets recorded through different bridges aren't comparable.
  • Sparse documentation. The third-party docs (third_party_robots.mdx, added in #4026) have a ROS 2 Bridges section listing two of the six projects, one line each. No converters, no caveats, no conventions.
  • Unverified data fidelity. Neither converter's v3 output has been checked against the current dataset code (episode model, video encoding, stats).
  • No upstream-compatible deployment story. The one policy-as-a-node implementation lives in a colcon workspace with its own config format.

Three layers

Everything above falls into one of three layers. Training is not one of them: it stays entirely on the LeRobot side. ROS 2 only appears where data comes in (layers 1 and 2) and where actions go out (layer 3).

Layer 1: Offline data (bidirectional)

  • What it is: conversion between the two storage formats, rosbag2 ↔ LeRobotDataset, for any robot. A bag is topics and timestamps regardless of form factor.
  • What works:
    • bag → dataset: existing teleop bags become training data.
    • dataset → bag: datasets become inspectable with standard ROS tooling (Foxglove, rviz, ros2 bag play, etc.) and replayable in existing ROS pipelines.
    • The pure-Python rosbags library reads and writes rosbag1/rosbag2 with no ROS dependency, plain pip install.
  • Challenges:
    • Bags are multi-rate (e.g. 30 Hz camera, 100 Hz joint states), so a converter must take a position on resampling and alignment. Existing practice (leros2-convert): sample-and-hold, taking the latest message per topic at each sync tick, with the tick driven either by a fixed rate or by a clock topic such as a camera. #3832 lists a streaming-first dataset redesign and multi-frequency sensor work, and the converter should follow that. Question for the team: is sample-and-hold with fixed-rate or clock-topic sync an acceptable v1, with the multi-frequency work as the migration path? High-rate force pathways (200+ Hz) will stress it and belong to #3832's scope.

Layer 2: Online robot interface

  • What it is: a typed topic↔feature mapper, so that lerobot-record and lerobot-teleoperate work against anything that speaks topics.
  • What works:
    • A LeRobot robot is a dict of named observation features and a dict of named action features, and policies only see names and shapes.
    • ROS standardizes the other side with message types (JointState, JointTrajectory, Twist, Image, etc.) and the robot description (URDF: frames, joints, actuators).
    • Consequence: arms, bases and quadrotors are configurations of the same mapper, not separate plugins.
  • Challenges:
    • LeRobot's loop is pull-based (get_observation()), ROS is push-based (subscriptions). Two patterns exist: a background executor filling a latest-value cache read with staleness checks, and (leros2, current main) a depth-1 subscription polled at the control rate with no executor, which avoids deserializing samples the loop never consumes and fits the measurement that encode/decode dominates cost. Open question: staleness handling as a documented convention or as a core utility, and which pattern is the recommended default?

Layer 3: Deployment

  • What it is: a trained policy as a ROS 2 node in any robot's graph. Observation topics in, select_action(), actions out.
  • What works:
    • The LeRobot half is already generic: all policies share PreTrainedPolicy.select_action(observation), so one wrapper node serves every current and future policy.
    • The ROS half reuses the layer 2 config: the same topic↔feature mapping defines the node's subscriptions and its output message type (JointTrajectory for an arm, Twist for a base, setpoints for a quadrotor's low-level controller, etc.).
  • Challenges:
    • Action semantics differ per platform (position, velocity, effort, setpoints, etc.). The node should declare which action types it supports.
    • The ROS interface has to be chosen (options are not mutually exclusive):
      • Topics: continuous publishing, controller-like.
      • Actions: goal-oriented ("run this policy until success/cancel"), with feedback.
      • Services: request/response, a bad fit for high-rate loops.
      • Native import: no upstream wrapper is shipped, and each user writes their own node that imports the policy and calls select_action() (roughly what sacovo does). Zero maintenance for LeRobot, but every user re-implements the same glue, and the policy's Python environment still gets pulled into the ROS node's environment. This is the baseline that P4 is measured against.
    • Policy runtime: the options above assume running the policy in-process in Python (PyTorch). For real deployments, the alternative is exporting the policy to ONNX or TensorRT and running inference from a C++ (rclcpp) node: no Python environment in the ROS graph, and a better fit for embedded targets (e.g. Jetson) and tight control loops. The trade-off is an export and validation step per policy, and parts of select_action() (action chunking, normalization, etc.) live outside the exported graph and must be reproduced in the node. The supported story should say whether it covers Python in-process only, or Python plus an export path.
    • Where the node lives is a maintenance question (next section): (a) pip package in core, only possible if the transport allows pip; (b) recommended community repo with its own release cycle; (c) docs only.
    • Safety: heartbeat and emergency stop should live on the ROS side, close to the actuators, regardless of transport. Any deployment story must state how the policy is stopped from within the graph.

Maintenance and CI

ROS integrations fall behind quickly, and the survey shows it (three lerobot minor versions in nine months for the most-starred bridge). Whatever gets blessed upstream needs an answer for testing and upkeep, and not everything has to carry the same weight. A workable split is two tiers: core-owned (lives in the lerobot repo, team owns the bugs) and recommended community repo (separate repo and release cycle, upstream links to it, with a declared and CI-tested lerobot version-range contract). The tier proposed for each deliverable is stated in the table in Proposals.

  • Layer 1 is cheap. Built on rosbags (pure pip), testable in plain pytest with fixture bags. No ROS in CI. Compatible with the core-owned tier.
  • For layers 2 and 3, the CI cost depends on the transport. With an rclpy-native transport (option (a) below), the LeRobot-side code imports ROS libraries, so CI needs a ROS container job (e.g. ros:jazzy) running record/teleop/inference smoke tests, plus a policy on which ROS distros are supported. With a transport like (b) or (c), the LeRobot-side code has no ROS imports and can be tested with plain pytest, and only the small bridge component needs ROS tooling. To be clear, the integration is still fully about ROS 2 robots in every case; the question is only which side of the boundary carries the ROS dependency, and therefore the CI and packaging burden.
  • Repo placement follows from that. If CI needs ROS containers and distro tracking, the recommended-community-repo tier is easier to sustain than core.
  • Version pins. Whatever ships should declare and CI-test a lerobot version range, so the 0.4-pin situation doesn't repeat.

Proposals

Five deliverables, roughly ordered by cost. P-numbers are for referencing in comments. The table summarizes where each one sits: Layer indicates where implementation depth is needed ("—" means the deliverable is layer-independent), Tier refers to the split in Maintenance and CI, and the last column marks which deliverables hang on the transport question.

# Deliverable Layer Tier Depends on transport?
P1 Docs page core docs no
P2 rosbag2 ↔ dataset 1 core-owned no
P3 Generic robot plugin 2 community repo yes
P4 Generic policy node 3 community repo yes
P5 Conventions doc 1–2 (spec) core docs no
  • P1: Docs page "LeRobot and ROS 2"
    • Extend the existing third-party docs (ROS 2 Bridges section) with the remaining projects, the converters and the caveats, either in place or as a dedicated page.
    • Descriptive only, no compatibility promises to maintain.
  • P2: Bidirectional rosbag2 ↔ LeRobotDataset v3 path (e.g. a lerobot[rosbag] extra)
    • Audit forge, openral and leros2-convert against the current spec and adopt what holds up, or build a small extra on rosbags.
    • Pure pip, pytest-only CI. Does not depend on any transport decision.
  • P3: Generic ROS 2 robot plugin (with a declared lerobot version-range contract)
    • One config-driven plugin: declare which topics and message types map to which features.
    • Form factors are presets (configs, not code): arm via JointState/JointTrajectory, base via Twist/Odometry, etc., with more added downstream.
    • Scoped to a small set of message types done well.
    • leros2 and the ROBOTIS plugin already have this shape (config-driven core plus presets); this generalizes across form factors.
    • Possible later enhancement: read joint names from the URDF.
    • Config includes the image transmission choice (raw vs compressed, see P5); P4 inherits it through the shared config.
  • P4: Generic policy node (reusing P3's repo and config)
    • One wrapper over select_action(), serving any policy.
    • Reuses the P3 config for its ROS interface.
    • Builds on what sacovo's policy_controller demonstrates.
    • Open sub-question: Python in-process only, or also an ONNX/TensorRT export path for a C++ node (see the policy runtime point in layer 3)?
  • P5: Conventions document
    • Topic↔feature mapping, QoS defaults, timestamp and staleness rules, and conversion placement (raw, compressed, tensors) as a documented option with defaults per link type (onboard, wired, wireless).
    • Goal: bridges over different transports produce compatible datasets.
Coverage map: surveyed projects vs P2–P4 (click to unfold)

Legend: ✅ covers as proposed · partial (see note) · announced by the author, not in the repo yet · — not in scope. P1 and P5 are docs deliverables, so no project can cover them. Authors: corrections welcome, same as the survey.

Project P2 (rosbag2 ↔ dataset) P3 (generic robot plugin) P4 (generic policy node)
ycheng517/lerobot-ros plugin shape, arms only, pinned to 0.4.x
ngres/leros2 bag → dataset only (leros2-convert), no reverse direction config-driven plugin, arms only, verified on lerobot 0.6.0
ROBOTIS lerobot_robot_ros2_zenoh generic core + vendor preset, alpha, non-PyPI SDK
sacovo/lerobot_ros bag annotation GUI announced generic topic↔feature mapper, but LeRobot-inside-ROS: not reachable from lerobot-record/lerobot-teleoperate policy_controller (any policy, action queue, service control), own config format
astroyat/lerobot-ros one mobile base, needs patched fork
forge bag → dataset plus dataset → other formats, v3 output unaudited
openral-dataset live recording → dataset and → bag sink, no bag → dataset

No cell is ✅, which is the point of this RFC: the pieces exist, but none has the proposed shape end to end. For P3 and P4 the cheapest path is likely generalizing one of the partials above, together with its author, rather than starting fresh.

Open design question: transport

The decision only P3 and P4 hang on — P1, P2 and P5 proceed regardless, and if this question needs more room it can move to a follow-up issue so it doesn't hold up the rest. Opinions welcome, especially from production experience.

Evidence update (Aug 13): measurements by @sacovo on real hardware: encode/decode cost dominates, transport cost is negligible for image topics, and image messages are not eligible for