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