You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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
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.
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.
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.
Two of the six joints cannot be described by a
[min, max]pairelbow_flexandwrist_rollboth have encoder positions that are not monotonic with theirangle — 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_flexDriven 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 whyThis one is the tell.
wrist_rollwas mapped as free through its entire range, whichnecessarily means it crosses the seam. Its recorded range
[21, 4073]is therefore not apair 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
ReachMap.reachable_rangesis a(min, max)tuple per joint. For a wrapping jointthere is no such pair. Worse, for
elbow_flexthe sorted endpoints describe exactly theregion the joint cannot reach — the map is not merely imprecise, it is inverted.
gentle_move's arrival check is linear (abs(position - target) <= tolerance), and itsgoal stepper walks a linear axis. A move across the seam cannot converge; it degrades to a
timeout.
clamp_goal(target, min_angle, max_angle)will happily clamp a perfectly reachableangle 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 silentlycannot be commanded to a legal angle is still a real defect.
Fix options
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.
gentle_move. Compute travel modulo 4096 and pick the short wayround. 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.
(min, max)pair, so acontinuous 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 regionsrather than the inverted ones. Written up as
t13indocs/hardware-validation-arm-read-flex.md.Note for whoever next powers the arm:
elbow_flexis currently resting at ~126, i.e. pastits wrap. A linear command will rotate it the long way round.
Found while re-verifying the #32 refactor on hardware.