Part II: Teleoperation and data collection · §20 of 43
Both stacks read the same raw motor angles, but present them differently.
Names
| LeRobot single arm | LeRobot bimanual | ROS 2 joint |
|---|---|---|
joint_1.pos … joint_7.pos | left_joint_1.pos …, right_joint_1.pos … | openarm_left_joint1 …, openarm_right_joint1 … |
gripper.pos | left_gripper.pos, right_gripper.pos | openarm_left_finger_joint1, openarm_right_finger_joint1 |
Units
| Quantity | LeRobot | ROS 2 | Conversion |
|---|---|---|---|
| Arm joints | degrees (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. |
| Gripper | degrees of the gripper motor, 0 = closed, ≈ −60 … −65 = open | metres of finger travel, 0 = closed, 0.044 = open | m = 0.044 × deg / (−60) (ROS uses motor −1.0472 rad = −60° ↔ 0.044 m) |
Control parameters (MIT mode in both)
| LeRobot follower | ROS 2 OpenArmHW | |
|---|---|---|
| kp (j1…j7, gripper) | 240, 240, 240, 240, 24, 31, 25, 25 | 70, 70, 70, 60, 10, 10, 10, 5 |
| kd | 5, 5, 3, 5, 0.3, 0.3, 0.3, 0.3 | 2.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) |
| Interpolation | none: each action is a new setpoint | JTC: splines on zeus (upstream main: none, i.e. steps; §4) |
| Gravity compensation | none | none |
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.
| Joint | LeRobot side=left | LeRobot side=right | ROS 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_4 | 0 … 135 | 0 … 135 | 0 … 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 … 0 | 0 … 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).