Runnable example
Both scripts, a camera check and a dataset export, ready to clone and run.
Hardware
You need two arms: a follower that moves, and a leader a person holds. They are the same design, built with different motors.Telling the two arms apart
Both arms plug in through the same USB adapter, and those adapters carry no serial number. Linux cannot tell them apart by name, so it hands out/dev/ttyACM0 and /dev/ttyACM1 in whatever order it happened to find them.
That order changes between reboots.
What does not change is which socket each arm is plugged into, and Linux
publishes that as a by-path name:
Calibration
Calibration is LeRobot’s, and its SO-101 guide covers assembly, motor setup and the calibration itself. The steps below are what that guide gives you and how the result plugs in here. Do this once per arm. You end up with two files and two ids that every later command refers to.1
Calibrate the follower
2
Calibrate the leader
--teleop. here, --robot. above.3
Keep the two ids
my_follower and my_leader are now the names of those files, under
~/.cache/huggingface/lerobot/calibration/. Followers and leaders are
filed separately, so the same name on both would still be two files.4
Pass each id back in
Every script gives its arm the id it was calibrated with, and LeRobot loads
the matching file:
Calibrating does not add position limits.
limits() returns {} on these
adapters, so slew is the only thing holding the arm back.That is the hardware done. Both arms are wired, named and calibrated, and
none of it has needed a network yet.Two scripts from here and they are on the internet, one driving the other
from wherever you happen to be.
Usage
One script per machine. Pass each arm theid you calibrated it with.
follower.py
id="my_follower"is the id you passed tolerobot-calibrate. LeRobot loads that arm’s calibration by this name, so a typo means the wrong travel, or none at all.FOURCC = "MJPG"is not optional with two cameras on one USB bus. Uncompressed video is about 18 MB per second each, and the second camera fails to open.slewandwatchdog_timeout_sare the two that matter on hardware. The first caps how fast a joint may move, the second holds the arm when commands stop arriving.on_starvation=HOLDfreezes the arm where it is.TORQUE_OFFis rejected here, since these adapters cannot cut power.record="./sessions"writes the session to disk as you drive.
leader.py
id="my_leader"is this arm’s calibration id, filed separately from the follower’s. Giving the leader the follower’s id is the mistake that produces numbers that look right and an arm that moves wrongly.arm.connect()is yours to call. The leader adapter has noconnect()hook, so it never opens your bus for you.- Check what
claim_control()returns.Falsemeans you do not have control, and everything after it moves nothing. hold_deadman(True)is what starts commands flowing. Nothing moves before it, and releasing it holds the far arm where it is.
Add
max_misalignment=20.0 to the adapter and call
leader.arm_when_aligned() instead if you want it to refuse to arm until the
two arms match.Run it
One terminal per machine, both with the same room id.Output
holding (starvation)is the starting state. The arm holds position and applies nothing until a leader takes control.activemeans commands are arriving and reaching the motors.rejected seq=398: staleis one command that arrived later thanmax_staleness_msand was dropped rather than applied late.holding (lease_lost)is the leader disconnecting. The arm holds where it is, it does not fall.wrist_flex -24.31on the leader is that joint sitting 24 units away from the follower’s. The far arm travels to meet it once armed.
Methods
The Follower and
Leader pages list the rest, along with every
constructor argument and event.
Units and telemetry
use_degrees on the LeRobot config decides the units you read, send and record.
It defaults to True, and both machines must pass the same value.
Body and gripper do not share units, so one
slew value means two things. Set
it per joint:
4.0 on a body joint means 4° of travel per tick, which is
200°/s at 50 Hz. The same 4.0 on the gripper would be 4% of its grip, which is
why the two are set apart.
The limit is per tick, so a slower loop gives a slower arm. Run at 25 Hz
instead of 50 and that same 4.0 allows 100°/s. Start low, then raise it until
the arm keeps up with you and on_limited stops firing constantly.
telemetry() reads load and temperature per servo, and it is slow: twelve round
trips on a bus already busy with position reads and writes.
A bad reading can pass the checksum and still be nonsense, such as a shoulder
at 150°C beside neighbours at 45 and 72. Those are dropped rather than
recorded, so do not build a thermal cut-out on the dataset alone.
Torque
The two arms are powered differently, on purpose. The leader’s motors come up unpowered, so a person can move it by hand. The follower’s stay powered throughout, including after your script exits, because an arm holding a load falls the moment you cut power to it. That is whyon_starvation stays on HOLD. When commands stop, the follower
freezes where it is rather than going slack.
Related
Overview
The two adapters, and the full two-machine setup.
SO-101 on ROS 2
The same arm behind your own ROS 2 driver instead.
Safety
The checks a command passes before it reaches the motors.
Troubleshooting
Fix an arm that will not move or arm, and reach support if it persists.