Hardware · Post 2 of 6
Setting up and calibrating the SO-101
Bringing up the leader and follower arms: ports, motors and calibration.
Everything in this project runs on the SO-101, LeRobot's flagship low-cost arm: a leader arm that a person moves by hand, and a follower arm that copies it. Both are driven by Feetech STS3215 servo motors over USB serial. Before any data can be collected, each arm needs its motors identified and its joint ranges calibrated.
What we followed
- Assemble SO-101 for parts, printing, motor IDs and assembly.
- Installation for the Python environment. We installed from source with
uv. - Feetech motors for the motor bus and servo details.
- Imitation learning on real robots for the overall workflow.
Setting motor IDs
New servos all ship with ID 1 and must each get a unique ID (1 to 6) and a common baudrate before the arm is assembled. LeRobot walks you through it one motor at a time: connect a single motor to the adapter, press Enter, and it writes the ID into the motor's memory. Run it once per arm:
lerobot-setup-motors --robot.type=so101_follower --robot.port=/dev/ttyACM1
lerobot-setup-motors --teleop.type=so101_leader --teleop.port=/dev/ttyACM0
The wiring that goes with this is in the assembly post.
Finding the serial ports
Each arm shows up as a /dev/ttyACM* device. On our machine the leader is
/dev/ttyACM0 and the follower is /dev/ttyACM1. The numbering depends on plug-in order, so it is
worth re-checking with lerobot-find-port after every reconnect.
Calibration
Calibration records each joint's range so the leader's angles map to the follower's. We wrapped the command in a script so it is repeatable:
lerobot-calibrate \
--teleop.type=so101_leader \
--teleop.port=/dev/ttyACM0 \
--teleop.id=my_awesome_leader_arm
The follower is calibrated the same way with --robot.type=so101_follower. LeRobot stores the results under
~/.cache/huggingface/lerobot/calibration/, keyed by the id you pass, so the same
id must be used later when recording.
A small robustness change
We made the motor scan in motors_bus.py retry each ping up to three times
(self.ping(id_, num_retry=3)), so a single missed reply during discovery does not make a motor look absent.