Commit Graph
2 Commits
Author SHA1 Message Date
serversdownandClaude Opus 5 9c28b586bb fix(micromate): the 0x5A chunk offset is a uint32 -- a 64 KB cap was one event away
UM20147 holds a 72,560-byte event.  chunk_params() wrote the offset as a uint16
at params[2:4], because every offset THOR was observed to send fits in two bytes
(largest 0x3400 = 13,312), which caps a download at 65,536 B -- so that event
could not have been fetched at all.

params[0:4] is demonstrably ONE 4-byte field: chunk 0 puts the 4-byte event key
there.  Writing the offset as a uint32 BE in the same slot is BYTE-IDENTICAL for
every offset below 65,536, so nothing verified against THOR's 74 captured frames
changes -- the replay test still matches all of them -- and the range extends to
4 GB.

Above 64 KB this is inference and the docstring says so: the field's width is
established, the device's handling of a non-zero high byte is not.

Also newly reachable: at offset 1 MiB params[1] is 0x10, which must go out as
`10 10`.  Nothing below 64 KB can produce that, so the uint32 change is what
first makes the case possible -- and an unescaped 0x10 in 5A params is the exact
bug that cost the Series III walk a release.  Tested.

THIS IS THE SERIES III 64 KB PAGE-BOUNDARY BUG WEARING A DIFFERENT HAT.  There,
parse_strt_end_offset() discards the key's page byte and the walk crashes once a
unit's buffer crosses 64 KB; that one is still open.  The transferable lesson:
an address field whose high bytes are zero in every capture is not a narrow
field, it is an untested one.  Same mistake, found twice, in code written years
apart.

Confirmed in the same run: 0x06 content[0:4] IS the event count -- UM20147 holds
5 events and reads 5, making it three for three across both firmware lines
(0->0, 6->6, 5->5).  And content[4:8] is a CONSTANT, not a count: it reads 9 on
a unit with 6 events and on a unit with 5.  list_events() still walks to the
sentinel; the count is worth adopting as a pre-check, not a replacement.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ru8Lg9HkkYvX9VWWo65SmL
2026-10-01 00:41:08 -04:00
serversdownandClaude Opus 5 a7e3a8f20a feat(micromate): protocol layer -- reads only, verified against Thor's frames
Step 2 of docs/micromate_client_spec.md: micromate/protocol.py plus 35
offline tests.  Reads only; nothing here writes, erases or changes
monitoring state.

The tests replay Thor's captured responses through a scripted transport and
assert the bytes we emit are the bytes Thor emits -- including a full replay
of the six-event download session, all 56 0x5A frames byte-for-byte.  A
passing test therefore means a real unit has already answered exactly that
frame.

Measuring the spec's command table against the captures found three more
errors in it, on top of the three the framing work found:

1. SUB 0x0A is the MONITOR-LOG WALK, not a keyed "event header, 30 B list
   record" read.  The same request repeated returns successive 297-byte
   records -- serial, mode, thresholds -- until an 11-byte ack ends the
   list.  The device holds the cursor; nothing in the request selects a
   record, and all nine captured frames carry identical params.

   Structural divergence worth noting: Series III reaches the same data via
   a record-type discriminator on its event chain, so partials and events
   share one walk.  Here the monitor log has its own cursor and the event
   chain never sees it.

2. 0x1E/0x1F carry token 0xFE at params[7].  The protocol reference
   documents all-zero params -- that was our own browse probing, which also
   worked.  Thor sends 0xFE on browse and download alike.

3. SUB 0x01 (device info) is never read by Thor in any captured session.
   Its 0xFFFF offset comes from our own probes, so it is the one read in
   the table with no Thor precedent.  Flagged in the docstring.

Two useful negatives, both from absence rather than presence:

- No SESSION_RESET (41 03).  Series III needs that 2-byte signal or a
  monitoring unit will not answer POLL over TCP.  Zero occurrences across
  all 8 sessions, including 40 frames exchanged with a unit that WAS
  monitoring.
- No universal preamble.  The only invariant is that a session opens with
  POLL; POLL -> SERIAL -> 0x49 -> POLL is Thor's connection check and
  appears in 3 of 8 sessions.  Setup pushes and scheduler reads open
  differently.

Two deliberate divergences from the Series III sibling:

- strict_checksums defaults True and RAISES.  minimateplus logs and
  continues because its parser cannot always tell an inner-frame delimiter
  from a checksum byte; that does not apply here, where the rule is exact
  on 251/251 frames.  The lenient default is instructive -- it hid a wrong
  checksum rule for two days.
- read_event_file() raises ShortRead rather than returning a truncated
  event.  The expected length is known up front, so the check is free, and
  a silently short event is the failure mode this codebase keeps hitting.

Every exchange resets the parser before sending, so a leftover frame is
discarded rather than answered with -- expected_sub catches a mismatched
SUB, but a same-SUB leftover would sail through with data for the wrong key.

File transfer (0x94/0x48) is deliberately out of scope: it needs a
data-carrying request frame, which is the frame type writes use, and that
boundary is worth keeping crisp in a read-only pass.

Full suite unchanged at 16 pre-existing failures (missing gitignored
fixtures); 419 passed, up 35.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ru8Lg9HkkYvX9VWWo65SmL
2026-09-28 19:58:22 -04:00