Commit Graph
2 Commits
Author SHA1 Message Date
serversdownandClaude Opus 5 423608ccd1 perf(micromate): 16 KB per request, not 1024 -- 14x fewer round trips
The chunk-size ceiling, measured on UM20147 with every size checked
byte-for-byte against a known-good download:

  1024 / 2048 / 4096 / 8192 / 16384  ->  served in full
  32768 / 65535                      ->  SILENTLY CLAMPED to 16384

The clamp is the important part: a 32,768 B request returns 16,384 B of perfectly
good data and no error.  Nothing in the response says it was truncated; the
length is the only signal.

THAT DICTATES HOW THE LOOP MUST BE WRITTEN.  read_event_file() now tracks its
offset by BYTES RECEIVED rather than striding by chunk index.  A fixed stride
would either fail on a clamp or skip the bytes it never collected; tracking what
actually arrived makes a clamp cost one extra request, and makes the loop
self-correcting against any short response -- precisely the failure mode that has
bitten the Series III side repeatedly.

Consequence of that change, recorded because it reverses an earlier decision: a
short chunk is NO LONGER AN ERROR.  It used to raise, on the principle that a
silently short event is this codebase's recurring bug.  But the device returns
short legitimately, and the real protection is the offset arithmetic plus the
final total-length check -- which still raises on a genuinely truncated event.
The test was rewritten rather than deleted, and says why.

CHUNK_SIZE = 16384.  For UM20147's events: 4,796 B goes 5 requests -> 1,
30,230 B goes 30 -> 2, and 72,560 B goes 71 -> 5.  Over cellular at ~0.65 s per
round trip that is ~46 s -> ~3.2 s on the large one.

Measured on one unit over USB, so THOR_CHUNK_SIZE = 1024 stays available and the
replay tests pin it -- reproducing THOR's exact traffic is one argument away, and
client.download_event()/get_event() take chunk_size for a link where large
responses are not surviving.

Full suite: 472 passed, 16 pre-existing failures unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ru8Lg9HkkYvX9VWWo65SmL
2026-10-02 14:14:02 -04:00
serversdownandClaude Opus 5 d7d72cadf8 feat(micromate): the event chain -- walk, records, download, and a self-check
Step 4 of docs/micromate_client_spec.md.  MicromateEventRef plus list_events(),
iter_events(), download_event(), get_event() and decode_error().  14 new tests;
112 micromate tests total.

The load-bearing test replays THOR's captured six-event download session through
iter_events() + get_event() and asserts EVERY BYTE WE EMIT MATCHES THOR'S, in
order -- 74 frames -- while decoding all six events and cross-checking each
waveform's peak vector sum against the float the device computed itself.

Two design decisions worth recording:

1. iter_events() EXISTS BECAUSE THOR INTERLEAVES.  Its captured order is
   0x93 -> 1E -> 0C -> 5A*n -> 0x93 -> 1F -> 0C -> 5A*n, downloading each event
   before advancing the chain.  list_events() walks to the end first, which is
   fine for browsing but leaves the device cursor parked past the event a later
   download addresses.  0x5A is key-addressed so it very probably does not care
   -- but nothing observed says either way, so the interleaved path is the one
   offered for downloads, and it is the one the replay test exercises.

2. get_event(verify=True) RECOMPUTES THE PEAK VECTOR SUM from the decoded
   samples and compares it against the device's own 0x0C float.  Two independent
   computations over the same samples, so a disagreement means our decode is
   wrong.  Agreement on the bench events is 0.000%.  Cheap insurance in a
   codebase whose decode failures have historically been silent -- unhandled
   block tags shorten a channel and nothing raises.  It is a decode-correctness
   check, NOT a truncation detector: a channel cut after its peak still yields
   the right PVS, and the docstring says so.  A test corrupts a stored peak to
   prove the check actually fires.

MicromateEventRef.uid is SERIAL:key, because the key alone is ambiguous across
units, and .filename generates THOR's name (<serial>_<YYYYMMDDHHMMSS>.IDFW/H)
-- returning None rather than guessing when the record type is unknown, since
read_idf_file() dispatches on exactly that suffix.

Recorded as a CANDIDATE, not used: 0x06 content[0:4] looks like the EVENT COUNT
-- 6 on a unit holding 6 events, zeros on an empty one, and THOR reads it BEFORE
the walk then downloads exactly six events without ever reading the chain
sentinel.  If it holds it lets a caller decide whether to walk at all, which
over cellular is the useful part.  Two samples on one unit, and content[4:8]
reads 9 unexplained, so list_events() still walks to the sentinel: slower by one
round trip and correct on evidence rather than inference.

Full suite: 465 passed, 16 pre-existing failures unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ru8Lg9HkkYvX9VWWo65SmL
2026-09-30 18:29:32 -04:00