feat(micromate): framing layer -- and three spec rules the bytes refuted
Step 1 of docs/micromate_client_spec.md: micromate/framing.py plus 31 offline tests. Every rule was checked against the captures BEFORE being written, which is the only reason this commit is not a bug. Three things the spec asserted are wrong, all of which fail silently: 1. Requests are NOT plain Series III frames. A Micromate escapes four byte values -- 0x02 0x03 0x04 0x10 -- where Series III escapes one. minimateplus.build_bw_frame reproduces 161 of Thor's 218 captured read frames; build_request() reproduces 218/218. The 57 it missed include EVERY 0x5A download (offset 0x0400 puts a literal 0x04 in offset_hi) and the scheduler enable. An unescaped 0x03/0x04 terminates the frame, so the unit never answers -- indistinguishable from a dead unit, and event download would have hit it on the first request ever sent. 2. The checksum is plain SUM8 of the destuffed payload, not the DLE-aware variant. 251/251 both directions. The DLE-aware form is correct paired with Series III destuffing, which leaves an escaped byte as two bytes; after uniform destuffing it subtracts the correction twice and disagrees with the wire on 55 of 251 responses. scratch/mm_frame_parse.py shipped with exactly that pairing and looked clean only because it accepts either rule -- so it labelled those 55 "SUM8" and never flagged one bad. "Zero bad checksums" was true and carried no information. A tool that tries N candidate rules cannot falsify any of them. Fixed to validate against SUM8 alone. 3. SUB 0x5A is a 1024-byte chunk loop, not one request per event. Thor's form, verified on all six bench events (4,076 -> 13,424 B): chunks = ceil(size/1024), offset = min(1024, size - 1024*i) as a byte count, params[2:4] = the byte offset, response data = offset + 11. sum(offsets) == size exactly, every time. This does not retract the earlier single-request observation -- that used offset_hi = 0x10, which in Series III is the bulk-stream marker, so it is plausibly a distinct streaming mode returning several frames. Those captures never landed in the repo, so it cannot be re-derived. Implement Thor's form; the other is worth one bench test. Also: a 0x10 inside request params needs no special handling (settled -- Thor sends it, the wire doubles it), so the planned NotImplementedError guard is gone. declared_length -> probe_length, because it is only meaningful in a probe reply and Thor never probes. Synthesised test frames are marked and each says what it stands in for. The flags=0x03 case is the only coverage of the Thor firmware line -- it wants a real 11.0BD capture next time UM20147 is on a bench. No writes. Read-path framing only; nothing here can originate a command. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ru8Lg9HkkYvX9VWWo65SmL
This commit is contained in:
+31
-10
@@ -22,10 +22,30 @@ One rule covers both directions: after the leading doubled `BW_CMD`, every
|
||||
escapes literal `0x03` bytes in write data so they are not mistaken for ETX,
|
||||
exactly as Blastware does.
|
||||
|
||||
That rule was chosen by evidence, not assumption: of the four candidates tried
|
||||
against the 9-24-26 capture's four data-carrying write frames, it is the only
|
||||
one under which all four checksums validate. See
|
||||
`docs/micromate_protocol_reference.md` → *The write path*.
|
||||
A Micromate escapes exactly four byte values — `0x02 0x03 0x04 0x10` — and
|
||||
nothing else, which is what makes the uniform rule exact rather than merely
|
||||
convenient. Established by re-stuffing all 502 captured frames and comparing
|
||||
to the wire: 251/251 each direction, where `{0x10}` alone gets 130 and 177.
|
||||
|
||||
⚠ Checksum, corrected 2026-09-27
|
||||
--------------------------------
|
||||
With uniform destuffing the checksum is **plain SUM8 of the destuffed
|
||||
payload**. This script used to try SUM8 *and* a "DLE-aware" variant that
|
||||
excludes `0x10` bytes, and report whichever matched — which is why it never
|
||||
flagged a bad frame and why the protocol reference carried the wrong rule for
|
||||
two days. The DLE-aware form belongs with *Series III* destuffing, where an
|
||||
escaped byte survives as two bytes; applying it after uniform destuffing
|
||||
subtracts the correction twice and disagrees with the wire on 55 of 251
|
||||
responses.
|
||||
|
||||
The lesson generalises: a tool that accepts any of N candidate rules cannot
|
||||
falsify any of them. It now validates against SUM8 alone, and reports
|
||||
`DLE-aware` only to name what a mismatch *would* have been — never as a pass.
|
||||
See `docs/micromate_protocol_reference.md` → *Checksum*, and
|
||||
`tests/test_micromate_framing.py`, which pins it.
|
||||
|
||||
`micromate/framing.py` is the production implementation; this stays as the
|
||||
one-shot capture-inspection tool.
|
||||
|
||||
Usage
|
||||
-----
|
||||
@@ -119,12 +139,13 @@ def frames(blob: bytes, *, is_request: bool):
|
||||
except ValueError:
|
||||
i += 1
|
||||
continue
|
||||
sum8 = sum(payload) & 0xFF
|
||||
dle_aware = (sum(b for b in payload if b != DLE) & 0xFF)
|
||||
if sum8 == chk:
|
||||
kind = "SUM8"
|
||||
elif dle_aware == chk:
|
||||
kind = "DLE-aware"
|
||||
# SUM8 of the destuffed payload is THE rule -- 502/502 captured frames.
|
||||
# The DLE-aware variant is reported only to name a near-miss; it is
|
||||
# never a pass. See the module docstring.
|
||||
if (sum(payload) & 0xFF) == chk:
|
||||
kind = "ok"
|
||||
elif (sum(b for b in payload if b != DLE) & 0xFF) == chk:
|
||||
kind = "BAD(dle-aware)"
|
||||
else:
|
||||
kind = "BAD"
|
||||
yield i, payload, chk, kind
|
||||
|
||||
Reference in New Issue
Block a user