TL;DR
- Calibration is not a setup chore. It is the first hardware QA gate.
- For the follower arm, NVIDIA's SO-101 calibration stats are useful enough to catch bad ranges before motion.
- For the leader arm, use broad sanity bands and do not compare the handle directly to the follower gripper.
- Only wrist_roll should behave like a full-turn joint. If shoulder_lift reports a ~4050 range, redo it.
- After saving calibration, run a no-motion read test before teleop.
The robot learning starts before the model
We calibrated a Hugging Face LeRobot SO-101 leader/follower pair.
The useful lesson was not:
run the calibration command.
The useful lesson was:
do not let a bad hardware state become your dataset.
If the motor IDs are swapped, the USB ports are confused, the middle pose is wrong, or a joint range crosses an encoder wrap, then teleoperation can still appear to work for a moment. But the data will already be contaminated. The model will later inherit an error that looked like setup trivia.
That is why calibration is hardware QA.
The LeRobot SO-101 guide says the calibration process makes the leader and follower arms share the same physical interpretation. The NVIDIA SO-101 calibration guide says the same thing more operationally: without good calibration, control is inaccurate and can become unpredictable.
That framing is right.
Before teleop, before recording demonstrations, before imitation learning, the first artifact is a calibration file.
The setup we actually had
Our hardware was a two-arm SO-101 setup:
- follower arm: the orange clamp/gripper arm, the one that will move during teleop;
- leader arm: the white no-clamp teleoperation arm, the one held by the human;
- both connected over USB-C to Feetech motor buses;
- LeRobot
0.5.1with Python3.12; - calibration saved under
~/.cache/huggingface/lerobot/calibration.
The two serial ports were:
leader /dev/tty.usbmodem5B3E0892321
follower /dev/tty.usbmodem5B610364511
The serial mapping matters. NVIDIA explicitly warns that USB device assignments can change when cables are unplugged and replugged, and recommends identifying the teleop and robot ports before running commands. LeRobot also uses the id field as part of the calibration-file path, so the same physical setup should keep stable IDs when you calibrate, teleoperate, record, and evaluate.
For us, the stable IDs became:
so101_leader_5B3E089232
so101_follower_5B61036451
The files landed here:
~/.cache/huggingface/lerobot/calibration/teleoperators/so_leader/so101_leader_5B3E089232.json
~/.cache/huggingface/lerobot/calibration/robots/so_follower/so101_follower_5B61036451.json
That sounds like bookkeeping. It is not.
It is the first reproducibility boundary.
What calibration is doing
LeRobot calibration has two conceptual steps.
First, put each joint in the middle of its physical range and press Enter. LeRobot uses that pose to establish homing offsets.
Second, move joints through their full safe ranges so the system can record minimum and maximum positions.
The Feetech motor documentation in LeRobot describes the three calibration fields that matter:
homing_offset: the offset from motor zero to robot zero;range_min: the lower recorded safe position;range_max: the upper recorded safe position.
The simple version is:
middle pose -> homing offsets
full joint sweeps -> range_min / range_max
The important operational detail is that the “middle pose” is not whatever looks aesthetically neutral.
It has to be the true middle of the joint’s physical range.
We learned that on the leader arm.
The leader looked fine when it was lying too flat. But shoulder_lift kept recording fake ranges around 4050, because the encoder crossed through 4095 -> 0. That is not extra range of motion. That is a rollover artifact.
The fix was to physically recenter the shoulder:
- move
shoulder_lifttoward one safe end stop; - move it toward the other safe end stop;
- place it halfway between them;
- only then press Enter and record ranges.
After that, shoulder_lift measured 2356, which is exactly the kind of number we wanted.
The table worth saving
The most valuable artifact from the session is the range table.
NVIDIA publishes follower-arm calibration statistics in the SO-101 guide. Their checker compares each captured range against a small calibration dataset and reports mean, standard deviation, and pass/fail. We used those follower stats as a benchmark for the follower arm.
For the leader arm, we did not invent official averages. The leader handle is mechanically different from the follower gripper, and the public stats table is for the follower. So the leader values below should be read as our measured reference plus sanity evidence, not as an NVIDIA benchmark.
| Joint | NVIDIA follower mean | Follower 2σ band | Our follower | Follower deviation | Our leader | Read this as |
|---|---|---|---|---|---|---|
| shoulder_pan | 2725 | 2661-2789 | 2685 | -1.25σ | 2702 | both pass |
| shoulder_lift | 2350 | 2196-2504 | 2299 | -0.66σ | 2356 | leader fixed after wrap failures |
| elbow_flex | 2222 | 2204-2240 | 2204 | -2.00σ | 2202 | edge/pass on follower, clean leader |
| wrist_flex | 2329 | 2295-2363 | 2301 | -1.65σ | 2285 | both plausible |
| wrist_roll | 4026 | 3798-4254 | 4095 | +0.61σ | 4095 | full-turn joint |
| gripper / handle | 1475 | 1409-1541 | 1441 | -1.03σ | 1119 | leader handle is not follower gripper |
The NVIDIA mean/std values are follower-arm stats from the Isaac SO-101 calibration checker. We use the leader values as our measured reference, not as an official leader benchmark.
The follower result was close enough to trust:
shoulder_pan range=2685
shoulder_lift range=2299
elbow_flex range=2204
wrist_flex range=2301
wrist_roll range=4095
gripper range=1441
The leader result became clean only after we fixed the middle pose:
shoulder_pan range=2702
shoulder_lift range=2356
elbow_flex range=2202
wrist_flex range=2285
wrist_roll range=4095
handle range=1119
The asymmetry in the last row is the important part. The follower has a clamp/gripper. The leader has a handle. Comparing the leader handle directly to the follower gripper stat would be a category error.
The failure mode to watch for
The clearest bug was encoder wrap.
Only wrist_roll should behave like a full-turn joint. In LeRobot’s SO-101 calibration logic, wrist_roll is treated specially as the full-turn motor while the other joints get recorded min/max ranges.
That means this is normal:
wrist_roll range=4095
This is suspicious:
shoulder_lift range=4054
That second number is probably not a great shoulder sweep. It probably means the shoulder crossed the encoder boundary, so the recorder saw both a near-zero value and a near-4095 value.
If you save that range, your limits are wrong.
The bad capture looked like this:
shoulder_lift min=0 max=4054 range=4054
The corrected capture looked like this:
shoulder_lift min=849 max=3205 range=2356
This is why I prefer one-joint-at-a-time calibration with live range printing. The official flow works, but if you are new to the hardware, streaming one joint at a time makes mistakes visible.
The rule I would use:
If any non-wrist-roll joint reports a range near the full encoder span, stop and redo before writing calibration.
The second rule:
If a joint reports
range=0, you moved the wrong joint or did not move enough.
We hit that too.
The no-motion check before teleop
After calibration, do not jump straight to teleoperation.
Run a no-motion test.
The test is simple:
- load the saved calibration file;
- connect to the correct port;
- disable torque;
- read
Torque_Enable; - read current positions;
- disconnect.
For the follower, the passive read returned:
connected: yes
torque_enable: all 0
raw_position:
shoulder_pan 3584
shoulder_lift 910
elbow_flex 1499
wrist_flex 1011
wrist_roll 3628
gripper 1959
disconnected: yes
For the leader:
connected: yes
torque_enable: all 0
raw_position:
shoulder_pan 3426
shoulder_lift 857
elbow_flex 1784
wrist_flex 963
wrist_roll 3815
gripper 2047
disconnected: yes
This check does not prove teleop is safe.
It proves something narrower and useful:
- the port mapping is still right;
- all six motors respond;
- the calibration file loads;
- the bus can read positions;
- torque is disabled before motion.
That is the right stopping point before the first bounded teleop test.
What I would record on video
If you are turning this into a tutorial, the video should not only show the robot moving.
Show the checks.
Record these clips:
- The two arms connected and the port mapping.
- The middle pose before pressing Enter.
- One clean follower joint sweep.
- One failed leader
shoulder_liftwrap, if you can reproduce it safely, or at least the terminal output. - The corrected leader
shoulder_liftsweep. - The final calibration table.
- The no-motion read test with
torque_enable: 0. - The next-day bounded teleop run.
The value is not a smooth montage.
The value is showing which evidence you trusted before the follower moved.
The takeaway
Low-cost robot arms make robot learning feel accessible, which is good.
But the first serious problem is not the policy.
It is evidence.
Ports. Motor IDs. Calibration files. Middle poses. Encoder ranges. Torque state. Saved JSON. No-motion reads.
Once those are honest, teleop and data collection can begin.
Before that, the robot is only vibes with servos.