c7ffdab5708b999a0084e2c1861d924348e1d73b
4
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
f09b7dcaaf |
feat(micromate): client layer -- connect, state, setups; read-only
Step 3 of docs/micromate_client_spec.md: micromate/client.py, two models in micromate/models.py, 26 offline tests. Every response constant in the tests is a real captured data section from UM12947. Field offsets were measured rather than taken from the spec, which turned up one general rule and one trap: THE RESPONSE SHAPE. Every response carries an 11-byte prefix and content starts at data[11]. One rule, every command. THE TRAP: data[0] is the content length & 0xFF, with no high byte anywhere in the prefix. It is therefore correct for every response under 256 bytes -- most of them -- and then reports 44 for a 2,092-byte setup block, 30 for a 286-byte monitor-log record, and 0 for a 1,024-byte download chunk. 182 of 251 captured responses agree with a naive read; the 69 that disagree are exactly the ones >= 256 bytes. That is the third length in this protocol read too narrow, after payload[9]-vs-payload[8:10] in the probe response. The client takes content as data[11:] and lets the frame's own length bound it -- nothing needs the declared length, since the frame already knows how long it is. Also measured: - POLL content[3] is 0x50, printable as "P", immediately before "Instantel". A generic printable-run scan therefore returns "PInstantel" -- it caught a test, not a unit. Vendor comes from a fixed offset; the model is found by searching for "MM/", which is structural rather than positional and so survives the Thor line's shorter "MM/ISEE/S". - The setup walk terminates on an EMPTY NAME, not an error: 23 responses, 22 names, factory.MMB first through TEST1.mmb last. - The 0x1C clock has an unidentified byte at content[6]; the hour is at content[7]. The protocol reference's 0x1C section already had this right and names the byte -- its one-line summary in the divergences list reads as six contiguous fields and is the version not to trust. Re-verified against three captures: 19:12:25, 19:13:34 and 01:14:05 against filenames stamped 19:12:14, 19:12:14 and 01:14:03. - Battery and memory are read FORWARD from content start, never backward from the end. This block is 4 bytes longer on the Thor line; the Series III from-the-end offsets give a 11.0BD unit 577.92 V. A test appends the four trailing bytes and asserts the forward offsets survive. connect() is narrower than the spec asked. The spec said to mirror Thor's POLL -> SERIAL -> 0x49 -> POLL "because it is known-good"; measurement showed that is Thor's connection check (3 of 8 sessions) and its fourth frame repeats its first. So connect() sends the three reads that gather something, and 0x01 is not read at all -- Thor never reads it, its layout is unmapped, and firmware_line comes free from any response's flags byte. If a unit ever refuses the next command after a cold connect, put the fourth POLL back and record it. A dead clock battery yields device_time=None rather than failing the whole state read; an unreadable active setup yields active_setup=None rather than failing connect. Both are real device states. Still verified only against 11.0CB and only over USB. The BD offsets follow from the extra bytes being trailing, which is documented but not something this code has seen. Full suite unchanged at 16 pre-existing failures; 445 passed, up 26. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ru8Lg9HkkYvX9VWWo65SmL |
||
|
|
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 |
||
|
|
5fe99568a2 |
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 |
||
|
|
8a1d1c3f17 |
docs(series4): spec the live client, so tomorrow is implementation not design
Turns docs/micromate_protocol_reference.md into a build plan for the missing half of micromate/ -- it is codec-only today with no way to talk to a unit. Layout mirrors minimateplus/: framing, protocol, client. Transport is REUSED -- minimateplus/transport.py is byte-level and protocol-agnostic, and already handles the RV50/RV55 habit of emitting RING/CONNECT to a caller. But minimateplus/framing is deliberately NOT shared: the two framings differ in ways that look small and are not, and a shared module would accumulate `if series ==` branches until neither case is readable. Records the three things that make S3FrameParser useless on Series IV (bare STX, 0xC5/0x03 flags, uniform 10 XX -> XX destuffing with no inner-frame carve-out), and the traps that have already cost time: the declared length is a uint16 BE and reading it as a byte under-reads SUB 0x1A by 47x; SUB 0x1C is four bytes longer on the Thor line so parse forward not backward, or a BD unit reports 577.92 V; the monitoring flag must be tested non-zero; and the SUB byte itself can arrive DLE-escaped, so destuff before indexing. Scope is READ-ONLY, stated with the reasoning: no command has ever been originated against a unit by this project, and keeping that true through the read client means the first thing we ever send to a customer's instrument is a deliberate decision rather than a side effect of a client that grew a method. Tests are specified to embed frames as hex constants rather than read fixtures, since bridges/captures/ and tests/fixtures/ are both gitignored -- with a table of which cases matter and why, and a round-trip assertion against scratch/mm_frame_parse.py, which is already known good across three sessions at zero bad checksums. Steps 1-2 need no hardware. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ru8Lg9HkkYvX9VWWo65SmL |