Skip to content

Report per-joint drive faults, and make the status rate settable - #39

Open
Jepson2k wants to merge 2 commits into
mainfrom
claude/waldo-diagnostics-redesign-i6w0ao
Open

Report per-joint drive faults, and make the status rate settable#39
Jepson2k wants to merge 2 commits into
mainfrom
claude/waldo-diagnostics-redesign-i6w0ao

Conversation

@Jepson2k

@Jepson2k Jepson2k commented Sep 5, 2026

Copy link
Copy Markdown
Collaborator

Moved from the Jepson2k fork, unchanged: Waldo-Commander's CI branch-matches parol6 against this repository, so the branch has to live here for its diagnostics tests to pick it up.

What

  • STATUS carries drive_health.faults: one tuple of labels per joint, empty when that drive is healthy. The drivers report fault bits over the serial link rather than temperature or current registers, so faults is all drive_health holds. A list of empty tuples is an all-clear; an empty list is a backend with no fault reporting at all, and the two are deliberately distinct so a display cannot show a bus it never asked about as healthy. The label view is rebuilt only when a bit moves, so the 100 Hz path allocates nothing while the drives are fine, and the wire entry is replaced only on change so a snapshot's shallow copy keeps the labels current when it was taken.
  • SET_STATUS_RATE / STATUS_RATE: the broadcast rate is a session knob rather than a boot constant, for a capture or tuning run that needs resolution the 50 Hz default cannot give. Status is emitted every Nth control tick, so the achievable rates are the divisors of the control rate; STATUS_RATE reports the loop rate so a caller computes the set itself. A rate that does not divide is refused with a remedy naming the ones that do, never rounded to a neighbour.
  • The control loop re-derives its broadcast interval when the rate moves instead of capturing it once at startup, and the status cache adopts the new period too, since it differentiates TCP position against it.

Companions

Same branch name on waldoctl (Jepson2k/waldoctl#23), par6 (Jepson2k/par6#62) and Waldo-Commander (Jepson2k/Waldo-Commander#46). CI installs waldoctl from the matching branch until it is tagged.

Verification

tests/integration/test_drive_faults.py and tests/integration/test_status_rate.py cover the fault labels and the divisor refusal against the fake-serial controller. The commit is the fork's 337237f as-is, one commit ahead of current main; the suite was not re-run for the move, so CI here is the check.

— written by Claude on behalf of @Jepson2k

🤖 Generated with Claude Code

The drivers report fault bits over the serial link rather than analog
temperature or current registers, so drive_health carries faults and
nothing else here: one label tuple per joint, empty when that drive is
healthy. A list of empty tuples is deliberately not the same as an empty
list — the first is an all-clear, the second is a backend with no fault
reporting at all, and a display that cannot tell them apart will show a
bus it never asked about as healthy. The label view is rebuilt only when
a bit moves, so the 100 Hz path allocates nothing while the drives are
fine, and the wire entry is replaced only on change so a snapshot's
shallow copy keeps the labels current when it was taken.

SET_STATUS_RATE makes the broadcast rate a session knob rather than a
boot constant, which is what lets a capture or a tuning run get
resolution the 50 Hz default cannot. Status is emitted every Nth control
tick, so the achievable rates are the divisors of the control rate; the
STATUS_RATE query reports that loop rate rather than a list of legal
values, letting a caller compute the set itself. A rate that does not
divide evenly is refused with a remedy naming the ones that do, never
rounded to a neighbour — a capture taken at a rate nobody asked for is
wrong in a way nothing reports.

The control loop re-derives its broadcast interval when the rate moves
instead of capturing it once at startup, and the status cache adopts the
new period too, since it differentiates TCP position against it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DsMBr94VMTsLJtPJq7h6A5
The validator divided before it screened: control % int(hz) ran ahead of the
integrality check, so 0.5 Hz floored to a zero divisor and NaN could not be
made an int at all. Both escaped as a generic tick failure, which is the one
thing a refusal must not do -- whoever asked for 0.5 Hz got a crash instead
of the rates that would have worked. The guard now establishes finite, in
range and integral before the modulo sees anything, and refuses through the
command protocol rather than an exception the executor has to unwrap.

make_error() formatted title and cause only, so the one template that puts
its parameter in the remedy -- the remedy being where a refusal says what
would have worked -- reached the client reading "Pick a rate that divides
{control} Hz: {allowed}". All four fields format now.

The rate also had more owners than one. STATUS_BROADCAST_INTERVAL was
computed at import from the boot rate, the status cache kept a
_status_rate_hz shadow of its own, and StatusBroadcaster took a rate_hz it
never read. ControllerState owns it now: status_broadcast_interval(hz)
derives the tick count on demand, and the TCP-speed derivative divides by
the live broadcast period while keeping the period its previous sample was
taken at, so the frame that straddles a rate change is not differentiated
against a period it never spanned.

Drive-fault labels are a pre-built lookup keyed by the two bits, because
that path runs at 100 Hz and the hot path does not allocate.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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