mm_client_check now drives the step-4 client methods rather than the protocol
layer directly -- list_events(), iter_events(), get_event() and decode_error()
-- so a bench run exercises the code that will actually be used.
Prints each ref with the filename it would be stored under, and reports the PVS
self-check result per event. The download uses iter_events(), which reproduces
THOR's interleaved order.
Adds the 0x06 test: reads storage range BEFORE the chain walk, prints
content[0:4] as the candidate event count and content[4:8] as the unexplained
companion value, then reports AGREES or DISAGREES against the chain length.
Two samples on one unit said the count is right; a third value from a different
unit either confirms it or kills it, and the tool now answers that in one run.
The 0x06 read is wrapped in the same step() helper as everything else, so a unit
that does not answer it degrades to a FAILED line rather than aborting the run.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ru8Lg9HkkYvX9VWWo65SmL
UM20147 read over USB by mm_client_check.py. The Thor firmware line was
entirely inference until now, and two of its three differences were covered
only by SYNTHESISED test frames. All three held:
- flags = 0x03 identifies the line -> reported firmware_line="thor". That is
only reachable if `10 03` in the flags position destuffs before indexing,
since 0x03 is ETX -- so the escaped-flags case is confirmed too.
- the model string is shorter -> reported "MM/ISEE/S" exactly. Anchoring the
search on b"MM/" rather than a fixed span is what made this work.
- the 0x1C block is 4 bytes longer with the extras TRAILING, so from-the-start
offsets survive -> battery 3.55 V and a clock correct to the second.
That last one is the one that mattered. Series III's from-the-end offsets
would have given this unit 577.92 V, which is why mm_client_check watches for
an impossible voltage: it is a free self-check on exactly the inference most
likely to be wrong.
Also clean: serial UM20147, 5 setups (factory.MMB -> test2.mmb), active setup
test2.mmb, 15,000,000 B total and free, 21 reads / 2,165 B in.
"One protocol stack drives the whole fleet regardless of firmware line" -- the
headline finding of 2026-09-23 -- is now demonstrated by a working client
rather than by matching response SUBs.
Test and docstring claims downgraded from inference to confirmed where the
hardware settled them, and left as synthesised-frame notes where it did not:
the BEHAVIOUR is confirmed but raw BD bytes are still not in the repo.
Added --capture DIR to mm_client_check: writes a raw_bw_*/raw_s3_* pair in the
layout scratch/mm_frame_parse.py already reads, so a run on an unfamiliar unit
becomes a test fixture without setting up a relay. Verified by round-tripping
its own output through that parser: 28 frames, 0 bad checksums.
Still not covered: a download from a BD unit (UM20147 had no events stored, so
the chunk walk remains CB-only), raw BD fixture bytes, a monitoring unit, a
nearly-full buffer, and the inbound call-home session.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ru8Lg9HkkYvX9VWWo65SmL
Run against UM12947 by bridges/mm_client_check.py, read-only, over the USB-B
CDC-ACM port and over the RX55 to TCP :9034.
THE WIRE BYTES ARE IDENTICAL ON BOTH PATHS -- 11,580 B in and 761 B out, to
the byte, with matching identity, state and a 24-entry setup walk. The
protocol does not care which physical layer it runs over, which is the thing
the modem path needed to prove.
Three findings, one of them a correction to my own prediction:
1. THE MODEM NEEDS FEWER READS, NOT MORE. 36 against USB's 83. The tool's
banner claimed the opposite. The modem coalesces -- it buffers ~1 s then
forwards one large TCP segment, where CDC-ACM delivers many small chunks.
That is reassuring rather than alarming: the risk was a frame arriving
split across reads, and the modem splits LESS than USB does. Banner fixed
to state the measured numbers instead of a guess.
2. ~0.65 s PER ROUND TRIP over cellular, independent of payload size. A
1,024 B chunk and a 16 B state read cost the same. list_setups() takes
16.05 s over the modem against 0.46 s over USB, for 24 commands.
This is the number that matters for SFM's design: over cellular, minimise
round trips, not bytes. Enumerating setups costs 16 s -- cache it, never
refresh it on a timer. A 13 KB event is 14 chunks ~ 8.4 s of latency
against ~0.03 s of data, which makes the unconfirmed single-request 0x5A
streaming mode worth its two-minute bench test on its own.
3. THERE IS A RECORD-TYPE FIELD: 0x0C content[11], 0x07 waveform, 0x08
histogram. The reference says no type field is known and that the type
must be carried out of the chain walk. Found by diffing the six bench
events' 0x0C records against their known types -- exactly one byte
separates the groups and is constant within each -- then confirmed by
predicting the right suffix for 6 of 6 on a blind re-run.
Six events split 4/2 is thin evidence for a byte that could be a counter or
a channel count, so it is recorded as a strong candidate, not settled, and
mm_client_check keeps a fallback: it tries the other suffix on failure and
says when the guess was wrong.
Also flagged: the reference's claim that the type comes from SUB 0x0A's
length is not visible in the download capture, where 0x0A is a standalone
monitor-log walk after the last chain entry, not a per-event probe.
END TO END: all six bench events assembled by read_event_file() from captured
0x5A responses decode with the existing codec -- 4 waveforms at 12,288 /
12,288 / 12,288 / 8,192 samples and 2 histograms. No new codec work needed;
/db/import/idf_file ingests a directly downloaded event unchanged.
Not covered: 11.0BD (still pure inference), a monitoring unit, a nearly-full
buffer, and the inbound call-home session.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ru8Lg9HkkYvX9VWWo65SmL
Hit on the bench: mint-mac has no pyserial, and `pip install pyserial` is
refused outright by PEP 668 (externally-managed-environment) on Mint 22 /
Ubuntu 24.04 / Debian 12. A field diagnostic that needs a pip install on a
locked-down host is one you cannot run at the moment you need it -- which is
exactly when this tool is for.
Replaced minimateplus.SerialTransport with a ~30-line stdlib `termios` port,
the same approach bridges/mm_link.py and scratch/fake_unit.py already take in
this repo. Runs on a stock Python 3 anywhere.
Not a general SerialTransport replacement: no flow control, no parity options,
Linux/macOS only. Enough for a Micromate, which is 8N1 with no handshaking.
Verified end to end against a fake unit on a pty, dribbling responses in
64-byte pieces: 194 reads for 10,958 B, identity/state/setup-walk all correct,
and a 4,076 B event downloaded across 4 chunks at the exact expected length.
Also clarified --baud in the help: it applies to the USB-A/FTDI path, and is
ignored by the USB-B "PC" port, which is CDC-ACM and negotiates its own rate.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ru8Lg9HkkYvX9VWWo65SmL
Read-only: POLL, SERIAL, state, monitor status, the setup walk, and optionally
one event download. Never writes, erases, or changes monitoring state.
Exists because everything in micromate/{framing,protocol,client}.py is
verified against captures taken over USB on one firmware line (11.0CB). Two
things that cannot be verified that way:
- The modem path. An RX55/RV55 buffers up to ~1 s before forwarding, so one
logical response arrives as many small reads. The client reads to frame
completion rather than using read_until_idle's idle-gap detection, which
should be strictly more robust for this -- but that needed proving.
- The 11.0BD firmware line, which reports flags=0x03, a shorter model string,
and a 0x1C block 4 bytes longer. All inference from one 2026-09-23 sweep
whose captures never landed in the repo. The tool says so loudly when it
meets one, and flags an impossible battery voltage as the signature of the
from-the-end offset bug.
Run it over both paths and diff; anything differing beyond timings is a
finding. It counts reads and bytes per transport, because a higher read count
for the same bytes IS the buffering, made visible.
Smoke-tested against a scripted TCP unit replaying captured response bytes in
37-byte dribbles: 289 reads to carry 10,958 B, every frame reassembled, a
4,076 B event downloaded across 4 chunks with the assembled length exact.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ru8Lg9HkkYvX9VWWo65SmL