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
arm limits probes in exactly one pose — whatever pose the arm happens to be resting in.
For four of the six joints that is enough. For shoulder_lift and wrist_flex it is
structurally insufficient: in the rest pose they are boxed in by the arm's own geometry, and
what gets measured is the pose, not the joint.
Both verbs refused to draw a conclusion, which is the contract working exactly as designed.
But it means we currently have no honest limits for two of six joints, and no path to them.
The evidence (follower /dev/ttyACM1, 2026-07-13)
shoulder_lift — arm limits shoulder_lift --threshold-joint shoulder_lift=180
kind = UNDETERMINED swept = 53 ticks (!)
low : WALL peak=500 loaded_run=23 free_run=30
high: TORQUE_LIMITED peak=500 loaded_run=0 free_run=0 <- it never moved AT ALL
30 ticks of free travel, then a stop. In the other direction it did not move a single tick —
the arm's own mass against gentle_move's 500 Torque_Limit cap. It swept 53 ticks total
on a joint whose real span is ~683.
wrist_flex — arm limits wrist_flex (default threshold 250)
It tripped hardware overload protection at both ends. This is a RACE, not a threshold
problem — wrist_flex's walls saturate at 500 and its threshold is 250, so contact could
have fired. The STS3215's dynamic-current protection (~200 ms, Protection_Time addr 35) simply
beats the software stall rule (_DEFAULT_STALL_SAMPLES = 8 @ 25 ms = 200 ms) to the punch. Lowering the threshold does not help — the stall-sample requirement is what costs the time.
Why one pose can never be enough
This is the mechanical-vs-environmental distinction, and the rule is asymmetric (already
recorded in limits.py, but not yet acted on):
A WALL found in one pose may be an obstacle (the table, the arm's own link). Obstacles
only ever shrink a range, so the joint's true mechanical envelope is the max over
poses. Max-over-poses is SOUND for a WALL.
A TORQUE_LIMITED end is NOT rescued by posing. The arm's own weakness is present in every pose, so it shrinks the range everywhere. A torque-limited end is a LOWER BOUND
FOREVER — LowerBoundEnd.mechanical_limit is permanently None, by design.
So: any shoulder_lift / wrist_flex number taken in a single pose is a lower bound and must
not be written anywhere as a limit.arm_spec must not learn these joints' bounds from a
single-pose run.
What v1 has, and what it doesn't
arm limits --pose LABELalready exists — but it is only a LABEL. It stamps the EndObservation.pose field so observations can be told apart later. It does not set a pose,
it does not sequence poses, and nothing drives the arm into one.
The merge machinery, however, is already built and tested: limits.merge_end_observations() / merge_joint_travel() implement exactly the asymmetric rule
above (a WALL wins by max-over-poses; a TORQUE_LIMITED end can never be promoted to a wall). So the hard part is done. What is missing is the thing that actually moves the arm into a pose
and probes from there.
arm limits --pose-file <jsonl> (or a small built-in pose set): for each pose, drive the other joints to their targets, then probe the joint under test from there.
Feed every pose's EndObservation into the existing merge_joint_travel() and take its
verdict. Do not re-implement the merge — it is already correct and tested.
Safety, and this is not optional.shoulder_lift carries the whole arm. Posing it is the
single most dangerous thing this tool does:
#33's torque guard is "hold on success, release on abnormal" — an abnormal exit while shoulder_lift is raised will DROP the arm. That is the designed behaviour and it is
right for an unattended arm, but a pose run must be human-gated and the operator told.
wrist_flex tripping hardware overload (above) needs handling before it is driven
repeatedly into stops; see the race described above.
How many poses is enough? There is no principled stopping rule yet. "Max over poses"
converges only if you sample the right poses, and nothing tells you when you have.
Does the wrist_flex overload race need fixing FIRST? Probably. Repeatedly driving a joint
into a hardware overload latch is not a measurement strategy. Options: slower approach, a lower Torque_Limit cap, a shorter stall_samples, or accepting TORQUE_LIMITED as final for it.
uv run arm101 arm limits shoulder_lift --threshold-joint shoulder_lift=180 --port /dev/ttyACM1 --apply --json
# -> UNDETERMINED, swept 53 ticks
uv run arm101 arm limits wrist_flex --port /dev/ttyACM1 --apply --json
# -> UNDETERMINED, both ends TORQUE_LIMITED via servo overload latch (error=32)# NOTE: recovery from a latched motor is `bus.clear_overload(motor)`, which as of v0.24.0# VERIFIES the latch actually dropped (it is not instant — see #43 thread).
Summary
arm limitsprobes in exactly one pose — whatever pose the arm happens to be resting in.For four of the six joints that is enough. For
shoulder_liftandwrist_flexit isstructurally insufficient: in the rest pose they are boxed in by the arm's own geometry, and
what gets measured is the pose, not the joint.
Both verbs refused to draw a conclusion, which is the contract working exactly as designed.
But it means we currently have no honest limits for two of six joints, and no path to them.
The evidence (follower
/dev/ttyACM1, 2026-07-13)shoulder_lift—arm limits shoulder_lift --threshold-joint shoulder_lift=18030 ticks of free travel, then a stop. In the other direction it did not move a single tick —
the arm's own mass against
gentle_move's 500Torque_Limitcap. It swept 53 ticks totalon a joint whose real span is ~683.
wrist_flex—arm limits wrist_flex(default threshold 250)It tripped hardware overload protection at both ends. This is a RACE, not a threshold
problem —
wrist_flex's walls saturate at 500 and its threshold is 250, so contact couldhave fired. The STS3215's dynamic-current protection (~200 ms,
Protection_Timeaddr 35) simplybeats the software stall rule (
_DEFAULT_STALL_SAMPLES = 8@ 25 ms = 200 ms) to the punch.Lowering the threshold does not help — the stall-sample requirement is what costs the time.
Why one pose can never be enough
This is the mechanical-vs-environmental distinction, and the rule is asymmetric (already
recorded in
limits.py, but not yet acted on):WALLfound in one pose may be an obstacle (the table, the arm's own link). Obstaclesonly ever shrink a range, so the joint's true mechanical envelope is the max over
poses. Max-over-poses is SOUND for a WALL.
TORQUE_LIMITEDend is NOT rescued by posing. The arm's own weakness is present inevery pose, so it shrinks the range everywhere. A torque-limited end is a LOWER BOUND
FOREVER —
LowerBoundEnd.mechanical_limitis permanentlyNone, by design.So: any
shoulder_lift/wrist_flexnumber taken in a single pose is a lower bound and mustnot be written anywhere as a limit.
arm_specmust not learn these joints' bounds from asingle-pose run.
What v1 has, and what it doesn't
arm limits --pose LABELalready exists — but it is only a LABEL. It stamps theEndObservation.posefield so observations can be told apart later. It does not set a pose,it does not sequence poses, and nothing drives the arm into one.
The merge machinery, however, is already built and tested:
limits.merge_end_observations()/merge_joint_travel()implement exactly the asymmetric ruleabove (a WALL wins by max-over-poses; a TORQUE_LIMITED end can never be promoted to a wall). So
the hard part is done. What is missing is the thing that actually moves the arm into a pose
and probes from there.
Proposed work
reported ticks; see profiles.JointCalibration persists REPORTED ticks while its docstring says raw — every profile written before the elbow re-zero is silently wrong by ~1071 ticks #45/ReachMap.reachable_ranges persists REPORTED ticks — a re-zero silently rewrites every map #46 for what happens otherwise).
arm limits --pose-file <jsonl>(or a small built-in pose set): for each pose, drive theother joints to their targets, then probe the joint under test from there.
EndObservationinto the existingmerge_joint_travel()and take itsverdict. Do not re-implement the merge — it is already correct and tested.
shoulder_liftcarries the whole arm. Posing it is thesingle most dangerous thing this tool does:
#33's torque guard is "hold on success, release on abnormal" — an abnormal exit whileshoulder_liftis raised will DROP the arm. That is the designed behaviour and it isright for an unattended arm, but a pose run must be human-gated and the operator told.
wrist_flextripping hardware overload (above) needs handling before it is drivenrepeatedly into stops; see the race described above.
Open questions (do NOT guess — think first)
vs derived from a reachability map (
arm explore, arm explore's grid is derived from the wrong things — EEPROM bounds and a global bucket size (r2) #34 — which is itself blocked on honestbounds, so beware the circularity).
converges only if you sample the right poses, and nothing tells you when you have.
wrist_flexoverload race need fixing FIRST? Probably. Repeatedly driving a jointinto a hardware overload latch is not a measurement strategy. Options: slower approach, a lower
Torque_Limitcap, a shorterstall_samples, or acceptingTORQUE_LIMITEDas final for it.arm explorerun hit 50 C in 25 minutes. A multi-pose limits runis a lot of motion. Duty cycle may be the real budget, not moves (already parked as a risk on
the spec+plan: make
arm explorework on the real arm (#33, #34, #35) #36 spec).Acceptance criteria
shoulder_liftandwrist_flexeach get > 1 pose, and their results are merged throughthe existing
merge_joint_travel().TORQUE_LIMITEDend is never promoted to a wall by adding poses (regression test —this is the asymmetry that makes the whole thing sound).
shoulder_liftsweeps materially more than the 53 ticks it managed in the rest pose,or we conclude and record that it cannot be measured and say why.
wrist_flexoverload race is either fixed or explicitly accepted, in writing.shoulder_liftis raised.Where the code is
arm101/hardware/limits.pymerge_end_observations,merge_joint_travelarm101/hardware/limits.pyWallEnd/LowerBoundEnd--pose(a LABEL today, nothing more)arm101/cli/_commands/arm.py,arm limitsEndObservation.posearm101/hardware/limits.pyarm101/hardware/gentle.py_DEFAULT_STALL_SAMPLES=8,_DEFAULT_POLL_INTERVAL=0.025arm101/hardware/gentle.py_CONTACT_TORQUE_LIMIT = 500arm101/hardware/safety.pyReproduce
Depends on / relates to
merges unreliable end verdicts just launders the noise into an authoritative-looking answer.
Get the verdicts trustworthy on ONE pose first.
arm explore's grid) wants these bounds — but do not close the circle by derivingposes from a map that was built from bad bounds.