Skip to content

feat: cover the enable window and expose stale state - #8

Closed
thetooler wants to merge 1 commit into
mainfrom
feat/enable-window-and-stale-state
Closed

thetooler wants to merge 1 commit into
mainfrom
feat/enable-window-and-stale-state

Conversation

@thetooler

Copy link
Copy Markdown

Summary

Three defects that all come from the same place: a DM motor in MIT mode keeps executing
the last target frame it was given, and a disabled motor sends nothing at all.

The enable window. The 0xFC enable carries no target of its own and does not clear
the target registers, so the instant it takes effect the motor resumes whatever target a
previous session left behind — if you Ctrl+C'd mid-close(), that register still says
q=closed, kp=100. Enable now streams zero-gain frames over that window, waits for a
fresh status frame, and then leaves the motor holding the position it actually measured.

The silent wait. initialize() used to wait quietly for feedback while the motor was
already enabled. An enabled motor that hears nothing for ~900 ms latches the 0xD
comm-loss fault, so that wait manufactured the failure it was looking for. It now keeps
feeding the motor while it waits.

Stale state. A disabled motor does not stream status frames, so get_state()
silently returned the last decoded values — or constructor defaults, i.e. position 0.0
with temperatures 0/0 — while the jaw might be anywhere. MotorState and GripperState
now carry data_age_s, and get_state() warns when the snapshot is older than
STALE_AFTER_S.

Consumers that read get_state() now have a way to tell a measurement from a leftover,
and refresh_status() lets them read the position before enabling instead of guessing.
No signature becomes incompatible: the added parameters are optional and the return types
are unchanged.

Changes

  • Add _hold_at_current() and _enable_and_hold() and route initialize(), enable() and clear_fault() through them, so every re-enable is covered by zero-gain frames and ends holding the measured position.
  • Drop the bare enable() from initialize()'s retry loop: an uncovered enable is exactly the window being closed.
  • Keep feeding the motor during the feedback wait, so the wait cannot itself trip the comm-loss fault.
  • Add MotorState.has_data / data_age_s and GripperState.has_data / is_stale / STALE_AFTER_S.
  • Add LiteGrip.refresh_status() and LiteGripCAN.refresh_status() (the 0xCC refresh command), which the motor answers regardless of enable state. It sends no motion command.
  • Add the 0x8 (over-voltage), 0xD (comm loss) and 0xE (overload) error codes. 0xD in particular is the fault a disabled motor sits in, and it was being reported as "unknown error" — a normal state that looks like a hardware failure.
  • Extend the two test doubles with the fields the code now reads.

The old err in (0, 1) reading of initialize() is deliberately not adopted: this
repository requires err == 1 and tests/test_enable.py pins that. Only the freshness
half of the change is taken.

Testing

python3 -m unittest discover -s tests -t tests

Result: Ran 52 tests — OK. That is the 40 that shipped with the repository, plus 12
in a new tests/test_stale_state.py covering the freshness contract (never-received vs
measured-but-old), the threshold boundary, the new error codes, and refresh_status() on
both the answering and the silent path.

Two existing test doubles needed the fields the code now reads, and tests/fake_can.py
and tests/test_enable.py were updated for that. No assertion was removed or relaxed:
test_enable.py still requires err == 1 for success, still requires err == 0 to raise
rather than lie, and still requires retries to happen. Its fake controller now advances
its scripted error on the enable call rather than on every poll, because the enable
window polls repeatedly during its zero-gain cover — which is also a more faithful model
of what the test's own comment describes ("the first two enable frames were lost, the
third really enabled").

Issues

No tracking issue. Nothing is closed by this pull request.

Three defects that all come from the same place: a DM motor in MIT mode
keeps executing the last target frame it was given, and a disabled motor
sends nothing at all.

The enable window. The 0xFC enable carries no target of its own and does
not clear the target registers, so the instant it takes effect the motor
resumes whatever target a previous session left behind -- if you Ctrl+C'd
mid-close(), that register still says q=closed, kp=100. Enable now streams
zero-gain frames over that window, waits for a fresh status frame, and then
leaves the motor holding the position it actually measured. initialize(),
enable() and clear_fault() all go through that one path, and the bare
enable() in initialize()'s retry loop is gone, since an uncovered enable is
exactly the window being closed.

The silent wait. initialize() used to wait quietly for feedback while the
motor was already enabled. An enabled motor that hears nothing for ~900 ms
latches the 0xD comm-loss fault, so that wait manufactured the failure it
was looking for. It now keeps feeding the motor while it waits.

Stale state. A disabled motor does not stream status frames, so get_state()
silently returned the last decoded values -- or constructor defaults, i.e.
position 0.0 with temperatures 0/0 -- while the jaw might be anywhere.
MotorState and GripperState now carry data_age_s, and get_state() warns when
the snapshot is older than STALE_AFTER_S. A new refresh_status() sends the
0xCC refresh command, which the motor answers regardless of enable state,
so the position can be read before the first enable instead of guessed.

Also adds the three error codes the SDK was rendering as "unknown error":
0x8 (over-voltage), 0xE (overload) and 0xD (comm loss) -- the last one being
the fault a disabled motor sits in, which made a normal state look like a
hardware failure.

The old `err in (0, 1)` reading of `initialize()` is deliberately NOT
adopted: this repository requires `err == 1` and tests/test_enable.py pins
that. Only the freshness half of the change is taken here.

Verified: 52 unittest cases, 0 failures -- the 40 that shipped plus 12 in
tests/test_stale_state.py covering the freshness contract, the new error
codes, and refresh_status on both paths.
@cao-xiao-hao
cao-xiao-hao deleted the feat/enable-window-and-stale-state branch September 29, 2026 05:20
@cao-xiao-hao

Copy link
Copy Markdown
Contributor

提交重复

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.

2 participants