Skip to content

feat(arm): manual (hand-guided) teach mode#27

Merged
Nick Hehr (HipsterBrown) merged 22 commits into
mainfrom
nhehr/manual-mode
Jul 15, 2026
Merged

feat(arm): manual (hand-guided) teach mode#27
Nick Hehr (HipsterBrown) merged 22 commits into
mainfrom
nhehr/manual-mode

Conversation

@HipsterBrown

@HipsterBrown Nick Hehr (HipsterBrown) commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Manual (hand-guided) "teach" mode for devrel:so101:arm

Adds a live jog-by-hand mode: an operator can push the arm around by hand and it follows, then holds whatever pose it's left in — without the arm ever going limp. Entered/exited via DoCommand, state exposed via Status(), tuned via a manual_mode config object.

How it works (and how we got here)

The servos stay in position mode the whole time (so the arm always holds against gravity — never limp). Two ideas make it hand-guidable, both arrived at through bench iteration on the real hardware:

  1. Position-deviation admittance. A ~50 Hz loop reads each joint's actual position; when the operator backdrives a joint past a small angular deadband (pos_deadband_deg), the goal follows the hand, trailing by the deadband so a small restoring force still holds gravity. Release → it holds where left.
    • An earlier load-based admittance law was tried and abandoned: the STS3215 present_load register saturates on the gentlest push and doesn't encode push direction, so it ran away on hardware. Position is clean, low-noise, and in the same calibrated frame as the goal.
  2. torque_limit compliance. Position mode at full torque is too stiff to backdrive by hand. Capping the servo's torque_limit (RAM reg 48) just above the gravity-hold load makes a hand easily overpower it while it still holds the arm's weight. Bench-validated config: torque_limit ≈ 50, pos_deadband_deg ≈ 0.5, p_gain ≈ 8 gave easy guiding that still held a fully-extended arm.

API

  • DoCommands: enter_manual_mode (accepts inline tuning overrides; re-entering while active re-applies the new params for live tuning), exit_manual_mode. Plus set_torque_limit — a diagnostic that writes torque_limit directly (with readback) to find the lowest value that still holds.
  • Status(): returns mode ("manual"/"auto") plus live per-joint telemetry (actual_deg, goal_deg, dev_deg, load) for tuning. Emitted as structpb-native types (guarded by a serialization regression test).
  • Config: a manual_mode object — pos_deadband_deg, loop_hz, p_gain, torque_limit (all optional; validated).

Safety

  • Any motion command (MoveToPosition, MoveToJointPositions, MoveThroughJointPositions, motion-service planning via GoToInputs) and Stop/Close auto-exit manual mode first and re-stiffen at the current pose. Deadlock-freedom independently verified (the control loop never takes the arm mutex).
  • Joint limits act as soft walls (goals clamped to calibration limits).
  • Compliance-register changes (p_gain/torque_limit) capture-before-write and restore fail-safe.
  • Coupling to respect (documented): a torque_limit set below a joint's gravity-hold load makes it sag, which past the deadband re-triggers following — so it's operator-tuned (watch dev_deg at rest ≈ 0), and defaults to 0 (unchanged/safe).

Testing

  • Pure control-law unit tests (follow direction, trailing deadband, clamp/saturation, boundary) and session-lifecycle tests behind a fake IO seam; full suite passes with -race.
  • Serialization regression test for the Status() wire format.
  • Bench-validated on hardware.

🤖 Generated with Claude Code

https://claude.ai/code/session_01CuMdoGZLt6murwFTxr8nZg

Nick Hehr (HipsterBrown) and others added 22 commits July 12, 2026 22:29
Guard MoveToPosition, MoveToJointPositions, MoveThroughJointPositions, and
Close with exitManualLocked so any powered motion command (or arm shutdown)
tears down an active manual-mode session and restores full stiffness before
proceeding. GoToInputs delegates to MoveThroughJointPositions so it is
covered transitively.
GetStatus serializes the arm's Status() via structpb.NewStruct, which
rejects a typed []manualJointStatus slice ("proto: invalid type"). Emit
the joints as []interface{} of map[string]interface{} instead. Adds a
regression test that runs structpb.NewStruct over the Status output.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CuMdoGZLt6murwFTxr8nZg
…racterization

Read-only diagnostic to characterize the STS3215 present_load signal (sign +
magnitude at rest vs under a gentle hand push, per joint) that manual mode's
admittance law depends on. Writes no goals and does not change torque; refuses
to run while manual mode is active. Used to debug manual-mode runaway.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CuMdoGZLt6murwFTxr8nZg
Bench testing showed present_load saturates on the gentlest push and
doesn't encode direction, causing runaway. The servo now stays in
position mode holding a goal; when backdriven past a small angular
deadband the goal follows the hand, trailing by the deadband so a
restoring force still holds gravity. Load becomes telemetry only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CuMdoGZLt6murwFTxr8nZg
The manual-mode Status regression test imports structpb directly, so
go mod tidy moves protobuf from the indirect to the direct require block.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CuMdoGZLt6murwFTxr8nZg
Read-only readback of p_gain/torque_limit/max_torque/torque_enable per
arm servo (works while manual mode is active), plus a direct torque_limit
writer with readback to test in isolation whether register 48 governs
holding torque on this firmware. Used to debug manual-mode compliance.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CuMdoGZLt6murwFTxr8nZg
…gnostics

Keep set_torque_limit (the register-write-with-readback tuning aid); drop
the load/position sampler and register readback now that manual-mode
tuning is settled.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CuMdoGZLt6murwFTxr8nZg
Manual mode now works out of the box with no manual_mode config: p_gain 8,
torque_limit 50, pos_deadband_deg 0.5 — the settings validated on the bench as
the lightest feel that still holds a fully extended arm against gravity.

Previously p_gain/torque_limit defaulted to 0 ("leave the register unchanged"),
which meant the default experience was a position-mode arm too stiff to
backdrive. A config zero is indistinguishable from omitted, so an explicit 0
inline override to enter_manual_mode is now the escape hatch for leaving the
compliance registers untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QqF98UsXVbDDXQgcWq9DWq
@HipsterBrown
Nick Hehr (HipsterBrown) deleted the nhehr/manual-mode branch July 15, 2026 17:26
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