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
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