Part II: Teleoperation and data collection · §20 of 43

Both stacks read the same raw motor angles, but present them differently.

Names

LeRobot single armLeRobot bimanualROS 2 joint
joint_1.pos … joint_7.posleft_joint_1.pos …, right_joint_1.pos …openarm_left_joint1 …, openarm_right_joint1 …
gripper.posleft_gripper.pos, right_gripper.posopenarm_left_finger_joint1, openarm_right_finger_joint1

Units

QuantityLeRobotROS 2Conversion
Arm jointsdegrees (motor angle)radians (motor angle)rad = deg × π/180. Same zero, same sign, no flips: LeRobot’s follower and OpenArmHW both pass the raw motor angle through.
Gripperdegrees of the gripper motor, 0 = closed, ≈ −60 … −65 = openmetres of finger travel, 0 = closed, 0.044 = openm = 0.044 × deg / (−60) (ROS uses motor −1.0472 rad = −60° ↔ 0.044 m)

Control parameters (MIT mode in both)

LeRobot followerROS 2 OpenArmHW
kp (j1…j7, gripper)240, 240, 240, 240, 24, 31, 25, 2570, 70, 70, 60, 10, 10, 10, 5
kd5, 5, 3, 5, 0.3, 0.3, 0.3, 0.32.75, 2.5, 2.0, 2.0, 0.7, 0.6, 0.5, 0.1
Command rate--fps of the loop (60 Hz teleop default, 30 Hz record default)750 Hz (bringup) / 100 Hz (MoveIt demo)
Interpolationnone: each action is a new setpointJTC: splines on zeus (upstream main: none, i.e. steps; §4)
Gravity compensationnonenone

LeRobot is ~3.4× stiffer on the big joints, which tracks the leader tightly but hits harder on contact. Override with --robot.position_kp='[…8 values…]' and --robot.position_kd='[…]' (single arm), or --robot.left_arm_config.position_kp=… (bimanual).

Joint limits (degrees)

LeRobot clips every commanded position to per-side limits from config_openarm_follower.py. These are only used when side is set. With side unset, the defaults are ±5° (gripper −5…0) and the arm barely moves.

JointLeRobot side=leftLeRobot side=rightROS v1.0 URDF (both arms)
joint_1−75 … 75−75 … 75−80 … 200
joint_2−90 … 9−9 … 90−100 … 100
joint_3−85 … 85−85 … 85−90 … 90
joint_40 … 1350 … 1350 … 140
joint_5−85 … 85−85 … 85−90 … 90
joint_6−40 … 40−40 … 40−45 … 45
joint_7−80 … 80−80 … 80−90 … 90
gripper−65 … 0−65 … 00 … 0.044 m

The asymmetric joint_2 limits show the arms are mirrored: shoulder abduction away from the body is negative on the left and positive on the right. LeRobot’s limits are slightly tighter than the URDF, a sensible margin from the mechanical stops. Always pass the correct side. Swapping sides with these limits lets joint_2 swing into the torso.

Converting a LeRobot action to a ROS trajectory point

import math
def lerobot_to_ros(action: dict, side: str) -> tuple[list[str], list[float]]:
    names = [f"openarm_{side}_joint{i}" for i in range(1, 8)]
    q = [math.radians(action[f"joint_{i}.pos"]) for i in range(1, 8)]
    gripper_m = 0.044 * action["gripper.pos"] / -60.0
    return names + [f"openarm_{side}_finger_joint1"], q + [gripper_m]

This is what you need to replay a LeRobot dataset in RViz/MoveIt (see §25).