Skip to content

arm limits probes ONE pose — shoulder_lift and wrist_flex cannot be measured in it, so two of six joints have no honest limits #52

Description

@OriNachum

Summary

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_liftarm 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_flexarm limits wrist_flex (default threshold 250)

kind = UNDETERMINED     swept = 298 ticks
  low : TORQUE_LIMITED  peak=500   <- tripped the servo's OWN overload latch (error=32)
  high: TORQUE_LIMITED  peak=500

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 LABEL already 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
.

Proposed work

  1. A pose is a set of joint targets (raw ticks, per the persist-RAW rule from Every factory-offset joint is clamped exactly ON its encoder seam — #35 may affect all six #43 — never
    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).
  2. 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.
  3. 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.
  4. 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.

Open questions (do NOT guess — think first)

  • Where do the poses come from? Hand-authored (an operator poses the arm and we record it)
    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 honest
    bounds, so beware the circularity).
  • 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.
  • Thermal. The first honest arm explore run hit 50 C in 25 minutes. A multi-pose limits run
    is a lot of motion. Duty cycle may be the real budget, not moves (already parked as a risk on
    the spec+plan: make arm explore work on the real arm (#33, #34, #35) #36 spec).

Acceptance criteria

  • A pose can be specified, and the arm drives the non-probed joints into it before probing.
  • shoulder_lift and wrist_flex each get > 1 pose, and their results are merged through
    the existing merge_joint_travel().
  • A TORQUE_LIMITED end is never promoted to a wall by adding poses (regression test —
    this is the asymmetry that makes the whole thing sound).
  • shoulder_lift sweeps 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.
  • The wrist_flex overload race is either fixed or explicitly accepted, in writing.
  • Human-gated. The operator is warned before shoulder_lift is raised.

Where the code is

what where
the merge (ALREADY CORRECT — reuse it) arm101/hardware/limits.py merge_end_observations, merge_joint_travel
the asymmetry (WALL promotes; TORQUE_LIMITED never does) arm101/hardware/limits.py WallEnd / LowerBoundEnd
--pose (a LABEL today, nothing more) arm101/cli/_commands/arm.py, arm limits
EndObservation.pose arm101/hardware/limits.py
the stall rule that loses the race to the servo arm101/hardware/gentle.py _DEFAULT_STALL_SAMPLES=8, _DEFAULT_POLL_INTERVAL=0.025
the torque cap arm101/hardware/gentle.py _CONTACT_TORQUE_LIMIT = 500
torque ownership / release-on-abnormal (#33) arm101/hardware/safety.py

Reproduce

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).

Depends on / relates to

Metadata

Metadata

Assignees

No one assigned

    Labels

    blockedCannot start until its stated dependency landsenhancementNew feature or requesthardware-gatedNeeds the physical arm and a human operator

    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