Guide · SO-101 hardware

How to calibrate an SO-101 arm in LeRobot

Short answer. Run lerobot-calibrate once for the follower arm and once for the leader. For each arm you hold every joint in the middle of its range and press Enter, then move each joint through its full range and press Enter again. LeRobot writes a homing offset into each servo so that the middle pose reads as half a turn, records the minimum and maximum position of every joint except wrist roll, writes those limits into the servos and saves everything to a JSON file named after the arm's id. It matters more than it looks: every policy sees your arm's joint positions through this file, so a sloppy calibration does not crash anything. The arm just moves to the wrong place.

What are the exact commands?

From the official SO-101 page in the LeRobot docs. Use the serial port that lerobot-find-port reported for each arm, and give each arm a unique id:

lerobot-calibrate \
    --robot.type=so101_follower \
    --robot.port=/dev/tty.usbmodem58760431551 \
    --robot.id=my_awesome_follower_arm

lerobot-calibrate \
    --teleop.type=so101_leader \
    --teleop.port=/dev/tty.usbmodem58760431551 \
    --teleop.id=my_awesome_leader_arm

The docs say to do this after the motors have their ids and baudrate set (lerobot-setup-motors) and the arm is assembled. The calibration script accepts either a robot or a teleoperator, never both in one run.

What does SO-101 calibration actually do?

The LeRobot docs put the purpose in one sentence: calibration ensures "the leader and follower arms have the same position values when they are in the same physical position", and that is what "allows a neural network trained on one robot to work on another."

In the code (SerialMotorsBus.record_calibration in motors_bus.py), a run does five things:

  1. Frees the joints. Torque is switched off and every motor is put in position mode, so you can move the arm by hand.
  2. Sets the home. When you press Enter with the joints mid-range, LeRobot resets each motor's calibration, reads its present position and writes a Homing_Offset that makes that pose read as half the encoder's range (2047 on a 12-bit encoder, per the code's own docstring).
  3. Records the range. As you sweep each joint, a live table shows MIN, POS and MAX per motor. It keeps the lowest and highest raw position seen until you press Enter.
  4. Treats wrist roll differently. wrist_roll is calibrated as a full-turn motor. It is not recorded; it is given the whole encoder range.
  5. Writes and saves. The homing offsets and the recorded limits are written to each servo as Min_Position_Limit and Max_Position_Limit, and the same values are saved to disk.

Where is the SO-101 calibration file saved?

By default the follower's file is

~/.cache/huggingface/lerobot/calibration/robots/so_follower/<robot.id>.json

and the leader's is

~/.cache/huggingface/lerobot/calibration/teleoperators/so_leader/<teleop.id>.json

The root moves if you set HF_HOME, HF_LEROBOT_HOME or HF_LEROBOT_CALIBRATION, and a calibration_dir in the robot or teleoperator config overrides it entirely. Note the folder is so_follower and so_leader, not so101_*: in current LeRobot the SO-100 and SO-101 share one class, so the id is the only thing that tells two arms' files apart. The script prints the exact path when it finishes ("Calibration saved to …").

What is inside the calibration file?

One entry per motor (shoulder_pan, shoulder_lift, elbow_flex, wrist_flex, wrist_roll, gripper), each with five fields from the MotorCalibration dataclass:

FieldWhat it is
idThe motor's bus id (1–6 on the SO-101)
drive_mode0 or 1. On Feetech servos an inversion is applied in software when values are normalised
homing_offsetThe offset written to the servo so your middle pose reads as half a turn
range_minLowest raw position recorded while you swept the joint
range_maxHighest raw position recorded

How do raw servo positions become the numbers a policy sees?

This is the part most guides skip, and it is where calibration reaches your model. LeRobot never hands a policy raw encoder ticks. It normalises them using range_min and range_max:

use_degrees defaults to true for both the SO follower and SO leader, and the config comment says this is "for backward compatibility with previous policies/dataset". Two consequences follow directly from those formulas:

Why did my SO-101 calibration fail?

The failures the code itself reports:

And the failures it cannot report, because nothing is technically wrong:

This is the "calibration offsets" item in the four silent misconfigurations on the AVIOS homepage: it loads cleanly, emits plausible numbers and moves the arm wrongly.

Is SO-101 calibration required?

Yes. Without a calibration LeRobot cannot normalise positions at all (the bus raises "has no calibration registered"), and connecting an uncalibrated arm starts the calibration routine.

FAQ

How long does SO-101 calibration take?
Two key presses per arm plus the time to sweep five joints by hand. The official calibration video shows the motion.

Do I need to calibrate the leader arm too?
Yes. The leader runs the same routine with --teleop.type=so101_leader. The point is that both arms read the same numbers in the same pose.

Why is wrist roll not recorded?
LeRobot calibrates wrist_roll as a full-turn motor and gives it the whole encoder range instead of a swept range.

Can I copy a calibration file to another arm?
Not usefully. The homing offsets and ranges describe one arm's servos and assembly. Calibrate each physical arm, and give each a unique id.

Should I recalibrate before running a policy?
Only if the hardware changed. Recalibrating moves the numbers your policy was trained on.

Sources

Behaviour described here is LeRobot's main branch at commit afbf3f1 (20 October 2026). Older releases may differ; check the version you installed. AVIOS has not yet run this on its own arm; this guide is read from the docs and source.