Skip to content

elbow_flex and wrist_roll encoders wrap — a [min, max] range cannot describe them, and gentle_move cannot converge across the seam #35

Description

@OriNachum

Two of the six joints cannot be described by a [min, max] pair

elbow_flex and wrist_roll both have encoder positions that are not monotonic with their
angle
— driven far enough, they cross the 4095→0 seam and read back near zero. Everything in
the codebase that reasons about joint position assumes a linear tick axis, and for these two
joints that assumption is false.

Confirmed on the follower (/dev/ttyACM1), 2026-07-12.

elbow_flex

Driven toward 4095 it rotated past the seam and read back as ~1. So its physical position
111 is above 4095, not below 2040.

wrist_roll — and "free all the way round" is exactly why

This one is the tell. wrist_roll was mapped as free through its entire range, which
necessarily means it crosses the seam. Its recorded range [21, 4073] is therefore not a
pair of walls — it is the seam
, and the joint passes straight through it.

Parked at position 4 (i.e. sitting on the seam) and commanded to 304 — a 300-tick move —
it could not converge. It ran the full 7.0 s travel budget and exited on timeout at 3055.

What breaks

  1. ReachMap.reachable_ranges is a (min, max) tuple per joint. For a wrapping joint
    there is no such pair. Worse, for elbow_flex the sorted endpoints describe exactly the
    region the joint cannot reach — the map is not merely imprecise, it is inverted.
  2. gentle_move's arrival check is linear (abs(position - target) <= tolerance), and its
    goal stepper walks a linear axis. A move across the seam cannot converge; it degrades to a
    timeout.
  3. clamp_goal(target, min_angle, max_angle) will happily clamp a perfectly reachable
    angle out of range, or pass an unreachable one through, depending on which side of the seam
    the joint is sitting.

The timeout is the safe failure — and note that post-#32 the primitive is at least honest
about it: it reports the position it actually reached rather than claiming arrival (the pre-fix
code would have returned in ~40 ms asserting final_position=304). But a joint that silently
cannot be commanded to a legal angle is still a real defect.

Fix options

  • Encoder re-zero (calibration). Move each joint's zero so the seam falls outside its
    physical travel. Cleanest, and it makes the linear assumption true rather than working
    around it. Needs an EEPROM write, so it belongs with the existing calibration surface and the
    EEPROM-lock handling (see fix: persist EEPROM id/baud writes by opening the Lock register #21) — not in the motion path.
  • Wrap-aware distance in gentle_move. Compute travel modulo 4096 and pick the short way
    round. Keeps EEPROM untouched, but it infects every position comparison in the codebase and
    is only correct for a joint that really is continuous — a wrapping joint with a hard wall
    would get driven the wrong way into it. Riskier than it looks.
  • Model the reachable set as a list of arcs rather than one (min, max) pair, so a
    continuous joint can say "all of it" and a wrapped one can say "these two intervals."

Recommendation: re-zero the two joints, and only then trust a linear map. The alternative
spreads modular arithmetic through the whole stack to work around a hardware config that can
simply be corrected once.

Current state

Both joints are flagged in arm-explore-follower.map.json (_caveat_elbow_flex,
_caveat_wrist_roll), and the ranges recorded there are the contiguous, wrap-free regions
rather than the inverted ones. Written up as t13 in docs/hardware-validation-arm-read-flex.md.

Note for whoever next powers the arm: elbow_flex is currently resting at ~126, i.e. past
its wrap. A linear command will rotate it the long way round.

Found while re-verifying the #32 refactor on hardware.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingmeasurement-integrityThe instrument may be lying — a wrong number here becomes a permanent false fact

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions