Skip to content

fix: make the homing tick the kinematic zero, not the recorded-range midpoint - #49

Merged
Nick Hehr (HipsterBrown) merged 3 commits into
mainfrom
fix/homing-tick-zero
Sep 8, 2026
Merged

Nick Hehr (HipsterBrown) merged 3 commits into
mainfrom
fix/homing-tick-zero

Conversation

@HipsterBrown

@HipsterBrown Nick Hehr (HipsterBrown) commented Sep 8, 2026 •

Copy link
Copy Markdown
Contributor

Problem

With the gripper physically touching the table, the follower-gripper TCP frame read z ≈ −13 mm at a mid pose and −25 mm at full stretch. Frame config, served URDF, TCP constant, and viewer all checked out; the error was angular and grew with reach.

Root cause: two different zeros. set_homing centers each servo's position_offset so the homed pose reads tick 2047, but NormModeDegrees defined 0° as (RangeMin+RangeMax)/2 of the later-recorded travel range. The printed stops aren't symmetric about the homed pose, so the kinematic zero drifted off it. On this arm the gap was 6.9° on the elbow, 2.3° shoulder-lift, 1.8° wrist-flex, 9.9° pan.

Hardware measurement at all-zeros: shoulder-lift and elbow horn heights matched the model (119 / 232 mm); the wrist horn was 13 mm high (250 vs 237). Only the forearm sits between them, so the elbow zero carried the error.

Fix

  • internal/servo: new HomingTick = 2047; the degrees branch of Normalize/Denormalize centers on it. Range min/max keep their clamping role. Other norm modes (gripper) untouched. Denormalize now rounds like its siblings. The calibration workflow's homing target references the same constant so the two can't drift again.
  • components/arm: calculateJointLimits was a latent no-op ((min−mid)/halfRange is identically ±1, so every joint got ±180°) and panicked on a partial servo_ids. Replaced by jointLimitsFor, which runs both range ends through the same Normalize and indexes calibration by servo id. clampPositions now genuinely binds: it warns once per move naming every clamped joint, and stays silent on the streamed (teleop) path.
  • Docs: docs/arm.md "Where 0° is", docs/calibration.md note that the homing pose is the arm's zero, CLAUDE.md gotcha.

Verified on hardware

Deployed to the live follower arm with its existing calibration file: TCP reads z = 0.3 mm with the tip on the table, approach axis plumb, at both the mid pose and full stretch.

Behaviour changes to know about

  • Existing calibration files shift every arm joint by (2047 − midpoint) × 360/4095 degrees. Recorded trajectories and saved joint poses from before this change replay shifted by that amount. The gripper is unaffected.
  • Joint limits are now the recorded range. A planner target beyond an under-recorded joint gets clamped (the servo's register limits already truncated it physically; this makes the divergence visible). Manual mode's soft wall at the range edge is now real too, at the same p_gain 8 / torque_limit 50 it always specified.
  • Homing instructions still say "middle of its range of motion"; a follow-up will change that to the model's zero pose and add verification poses after save_calibration.

Why LeRobot does the same thing

LeRobot's MotorNormMode.DEGREES uses the identical midpoint formula. Investigated: the mid-range zero was a deliberate 2025 redesign decision (drop ±180° wrap-around), but the DEGREES formula landed in the same commit as a thin rescaling of the percentage mode, undocumented in its PR, and its kinematics apply no compensating offset. Open issues #4442 and #3587 ask for exactly this. A maintainer's own description ("the position you start calibration with will be the 0") matches this PR, not their formula.

🤖 Generated with Claude Code

The calibration workflow's setHomingPosition already writes each servo's
position_offset so the pose the user homed at reads back as raw tick 2047.
But NormModeDegrees defined 0 degrees as (RangeMin+RangeMax)/2 -- the
midpoint of the recorded travel range -- which drifts off that homed pose
whenever the recorded range is asymmetric around 2047. Measured on hardware
as ~6.9 degrees on an elbow joint and ~13mm at the gripper TCP.

Introduce servo.HomingTick (2047) as the one named constant for this value,
used as the center in NormModeDegrees's Normalize/Denormalize, and have
workflow.go's targetCenter reference it so the two can't drift apart again.
RangeMin/RangeMax keep clamping Denormalize's output; only the zero moved.

NormModeRange100 (gripper), NormModeRangeM100, and NormModeRaw are untouched.

components/arm/motion.go's calculateJointLimits independently re-derives
degrees from RangeMin/RangeMax using the old midpoint-centered math (not
MotorCalibration.Normalize), so the arm's joint limits still center on the
range midpoint rather than HomingTick -- left alone as out of scope for this
change. DefaultSO101FullCalibration's arm-joint 500/3500 defaults (midpoint
2000) are similarly untouched; they now imply a zero offset by 47 ticks from
a real calibration file's homing tick until a machine is actually calibrated.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qv1ZbWg7v4RH7HcR6xUgAi
…ing-tick zero

calculateJointLimits normalized RangeMin/RangeMax against the range midpoint, which
made every joint's limits exactly [-pi, pi] regardless of calibration: (min - mid) /
halfRange is always -1. It also used a different zero than the joint positions now do.
Run both ends of the recorded range through MotorCalibration.Normalize instead, so
clampPositions and manual mode bound the arm to its real recorded travel in the same
frame the positions report in.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qv1ZbWg7v4RH7HcR6xUgAi
…e, document the zero

Review follow-ups. jointLimitsFor takes the full calibration and the servo id list, so a
partial servo_ids config indexes calibration by id instead of by position (the old code
panicked on fewer than five). clampPositions emits one warning per move naming every
clamped joint, and none on the streamed path, where teleop would otherwise log at 100 Hz.
Denormalize rounds in degrees mode like its siblings. Docs state where 0 degrees is and
the angle shift an existing calibration file sees on upgrade.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qv1ZbWg7v4RH7HcR6xUgAi
@HipsterBrown
Nick Hehr (HipsterBrown) merged commit c568974 into main Sep 8, 2026
2 checks passed
@HipsterBrown
Nick Hehr (HipsterBrown) deleted the fix/homing-tick-zero branch September 8, 2026 20:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant