Repository navigation
feat(calibration): lerobot-calibrate --auto for the SO-101 leader and follower - #4826
stepankandrushin wants to merge 2 commits into
Conversation
… follower `lerobot-calibrate --robot.type=so101_follower --robot.port=... --robot.id=... --auto=true` (or --teleop.type=so101_leader ...) calibrates an arm without moving it by hand: every joint is driven into its two mechanical end stops at a limited torque, and Homing_Offset and the position limits come from where it stalls. The result goes to the same calibration file and servo registers as a manual calibration. A rework of the automatic calibration in huggingface#3282, along its review: - No separate CLI: SOFollower and SOLeader get auto_calibrate(), which saves through their own _save_calibration(), so a leader's file lands under calibration/teleoperators. - The engine (motors/feetech/auto_calibration.py) uses the bus's read/write/sync_read/sync_write and their retries. What moves when is data (robots/so_follower/auto_calibration_plan.py): a plan for the follower and one for the leader, whose servos are geared to be moved by hand and run at 80 % torque instead of 50 %. - Every register the run changes is written with Lock=1 (RAM only) and restored in a finally, also on Ctrl-C; a restore that fails leaves the previous calibration in place and writes nothing. - Options under --auto_calibration.*: velocity (default 200 steps/s, was 1000), torque_limit, the checks. Changed from huggingface#3282 after runs on hardware (STS3215, firmware 3.10): - End stops are found in position mode, the goal stepped at most 12 degrees ahead of the joint. In velocity mode, firmware 3.10 reports Present_Position without Homing_Offset, and a servo switched back to position mode with torque on can stay in velocity mode while Operating_Mode reads 0. A setpoint travels to a far goal at the Goal_Velocity it started with and keeps a blocked joint pushing, so every move is stepped, with Goal_Velocity 0. - Goal = present position, written and read back one servo at a time, before torque comes on; a joint that moves at torque-on ends the run. - The plan states each joint's unfold direction instead of guessing it, and assumes and checks a folded start pose (gripper hanging down: turned up, the folded arm boxes the wrist in). - Abort instead of guessing: a timeout, a joint that cannot unfold, a range more than about 10 degrees off an SO-101's, or a load change on a held joint (the arm pressing on the table or a clamp). Nothing is written. - A wrist roll without a stopper gets 0-4095, as in the manual calibration, and turns back to where it started. tests/motors/test_feetech_auto_calibration.py runs the plans against a simulated SO-101 arm whose servos behave as above, and the SO classes and lerobot-calibrate --auto against mocks. Docs in docs/source/so101.mdx. Co-authored-by: Isaac Sin <sinweitengisaac@gmail.com>
…options draccus turns the comments of AutoCalibrationConfig into argparse help, and argparse %-formats help text: the "% o" of "in 0.1 % of the servo's maximum torque" made `lerobot-calibrate --help` raise "TypeError: %o format: an integer is required, not dict". The comments say "tenths of a percent" now; a test runs --help and looks for the options.
There was a problem hiding this comment.
Copilot review overview
🔵 Needs a closer look
It is a large, intricate feature that physically drives a robot arm into mechanical end stops under torque, with behavior tuned to specific hardware/firmware and author-acknowledged uncertainties, so it needs human review and on-hardware validation.
Review effort: Balanced
Findings: None
What changed in this PR
This PR adds opt-in automatic calibration for SO-101 leader and follower arms via lerobot-calibrate ... --auto=true. Instead of a person hand-moving each joint, the arm drives every joint into its two mechanical end stops at a limited torque, and Homing_Offset plus the position limits are derived from where each joint stalls. It is a reworked continuation of #3282, re-engineered around observed STS3215 firmware 3.10 behavior (position-mode-only end-stop search, stepped goals, goal-before-torque, full register restore on any abort/Ctrl-C). The result flows through each device's existing _save_calibration()/write_calibration() path, so a leader's file lands under calibration/teleoperators, with no changes to the manual calibration flow.
Changes:
- New calibration engine (
motors/feetech/auto_calibration.py) with a data-drivenAutoCalibrationPlan,FeetechAutoCalibrator, stall/contact/range-plausibility checks, and guaranteed register restoration. - SO-101 plan data (
robots/so_follower/auto_calibration_plan.py) for follower and leader (leader at higher torque), plusauto_calibrate()methods onSOFollower/SOLeaderand an--auto/--auto_calibration.*CLI path inlerobot_calibrate.py. - Documentation in
so101.mdxand a 52-test simulation/mocks suite.
| File | Description |
|---|---|
src/lerobot/motors/feetech/auto_calibration.py |
New engine: plan types, decision helpers, stall detection, end-stop driving, and fail-safe register restore. |
src/lerobot/robots/so_follower/auto_calibration_plan.py |
Follower and leader choreography/range data for the self-calibration. |
src/lerobot/robots/so_follower/so_follower.py |
Adds auto_calibrate() that prompts for start pose and persists the result like manual calibration. |
src/lerobot/teleoperators/so_leader/so_leader.py |
Adds auto_calibrate() using SO_LEADER_PLAN, saving under teleoperators. |
src/lerobot/scripts/lerobot_calibrate.py |
Adds --auto and --auto_calibration config and dispatch to auto_calibrate(). |
docs/source/so101.mdx |
User-facing "Automatic calibration" section with warnings, flow, and options table. |
tests/motors/test_feetech_auto_calibration.py |
Simulated-arm and mock-based tests of the engine, plans, SO classes, and CLI. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Summary / Motivation
lerobot-calibrate --robot.type=so101_follower --robot.port=... --robot.id=... --auto=true(or--teleop.type=so101_leader ...) calibrates an arm without moving it by hand: every joint is driven into its two mechanical end stops at a limited torque, and Homing_Offset and the position limits come from where it stalls. The result goes to the same calibration file and servo registers as a manual calibration.Related issues
lerobot_calibrate.pyand the SO classes. refactor(motors): one rustypot-backed SerialMotorsBus for Feetech and Dynamixel #4672 replaces the bus transport; this PR only usesread/write/sync_read/sync_write.What changed
Along the review of #3282:
--auto=trueonlerobot-calibrate.SOFollowerandSOLeadergetauto_calibrate(), which saves through their own_save_calibration(), so a leader's file lands undercalibration/teleoperators.motors/feetech/auto_calibration.py) uses the bus's methods and theirnum_retry.finally, also on Ctrl-C. The changes are written with Lock=1, so they stay in RAM. If a restore fails, the previous calibration is put back and nothing is written.--auto_calibration.*, among themtorque_limitandvelocity(default 200 steps/s, down from 1000). The leader is documented inso101.mdx.Changed after running #3282 on hardware (STS3215, firmware 3.10):
robots/so_follower/auto_calibration_plan.py), with one plan for the follower and one for the leader. The leader's plan runs at 80 % torque instead of 50 %, because its servos are geared to be moved by hand. The plan states each joint's unfold direction and assumes a folded start pose, with the gripper or handle hanging down in front. Turned up, the folded arm boxes the wrist in.feat(calibration): add automatic calibration for SO101 leadera and follower w/ Feetech STS servos #3282 wrote timeouts as end stops.
No breaking changes: the manual calibration is unchanged.
How was this tested (or how to run locally)
Tests added:
tests/motors/test_feetech_auto_calibration.py, 52 tests. They run the plans against a simulated SO-101 whose servos behave as above, and the SO classes andlerobot-calibrate --autoagainst mocks. Run them withpytest -q tests/motors/test_feetech_auto_calibration.py.Hardware: one SO-101 follower and one SO-101 leader (STS3215, firmware 3.10), both filmed. The feature commit took 121 s on the follower and 125 s on the leader (the commit after it only rewords three comments for
--helpand adds a test). Every end was found by stall and is in free air in the video frames, and both arms returned to the start pose. A second follower run, from a fresh clone and a new uv environment, took 119 s. Earlier versions of this branch completed four runs on the follower and three on the leader. Across all of them, every register agrees within 5 steps (0.4°) on the follower, and within 3 on the leader except the trigger (8 steps, 0.7°).Video, 70 s, sped up: a follower run on an earlier version of this branch, the terminal next to the arm: https://www.youtube.com/watch?v=yAzojrfhfS0. The final version moves the same way, except that each unfold now goes straight in the plan's direction instead of first trying the blocked one.
To try it: put the arm in the start pose from
so101.mdx("Automatic calibration"), thenlerobot-calibrate --robot.type=so101_follower --robot.port=<port> --robot.id=<id> --auto=true. Keep a hand near the power: Ctrl-C stops the run, and the arm goes limp.Checklist (required before merge)
pre-commit run -a)pytest)Reviewer notes
auto_calibration_plan.py) are about ±10° around what one follower and one leader measured. Other builds may need--auto_calibration.check_ranges=false, and I'd welcome ranges from other arms. The leader's pan accepts 185-250°: ours turns 239°, 45° more than the follower, for reasons I haven't found.SOFollower/SOLeader), but only SO-101 arms have run it.CalibrateConfigasauto_calibration; happy to move them.