From 4fb1bbfe35cbb06097d8b6c745a02c6d9b2d9447 Mon Sep 17 00:00:00 2001 From: Brian Harrison Date: Fri, 3 Apr 2026 17:30:47 -0400 Subject: [PATCH] fix: fix token position in params and enhance event iteration logic for MiniMateClient --- CLAUDE.md | 41 ++++++++++++++++++++++++++++++----------- minimateplus/client.py | 11 +++++++++++ minimateplus/framing.py | 16 +++++++++++----- 3 files changed, 52 insertions(+), 16 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 4755277..6f9438e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -135,21 +135,40 @@ the setup at record time, not the current device config — this is why we fetch `stop_after_metadata=True` (default) stops the 5A loop as soon as `b"Project:"` appears, then sends the termination frame. -### SUB 1E / 1F — event iteration null sentinel (FIXED, do not re-introduce) +### SUB 1E / 1F — event iteration null sentinel and token position (FIXED, do not re-introduce) -**The null sentinel for end-of-events is `event_data8[4:8] == b"\x00\x00\x00\x00"`, NOT -`key4 == b"\x00\x00\x00\x00"`.** +**token_params bug:** The token byte was at `params[6]` (wrong). Both 3-31-26 and 4-3-26 +BW TX captures confirm it belongs at **`params[7]`** (raw: `00 00 00 00 00 00 00 fe 00 00`). +With the wrong position the device ignores the token and 1F returns null immediately. -Event 0's waveform key is `00000000` — all-zero key4 is a valid event address. -Checking `key4 == b"\x00\x00\x00\x00"` exits the loop immediately after the 1E call, -seeing event 0's key and incorrectly treating it as "no events." - -Confirmed from the 4-3-26 two-event capture (`bridges/captures/4-3-26-multi_event/`): +**0A context requirement:** `advance_event()` (1F) only returns a valid next-event key +when a preceding `read_waveform_header()` (0A) or `read_waveform_record()` (0C) call has +established device waveform context for the current key. Calling 1F cold (after only 1E, +with no 0A/0C) returns the null sentinel regardless of how many events are stored. +**1F response layout:** The next event's key IS at `data_rsp.data[11:15]` (= payload[16:20]). +Confirmed from 4-3-26 browse-mode S3 captures: ``` -1E response (event 0): key4=00000000 data8=0000000000011100 ← valid, trailing bytes non-zero -1F response (event 1): key4=0000fe00 data8=0000fe0000011100 ← valid -1F null sentinel: key4=0000fe00 data8=0000fe0000000000 ← done, trailing 4 bytes = 00 +1F after 0A(key0=01110000): data[11:15]=0111245a data[15:19]=00001e36 ← valid +1F after 0A(key1=0111245a): data[11:15]=01114290 data[15:19]=00000046 ← valid +1F null sentinel: data[11:15]=00000000 data[15:19]=00000000 ← done +``` + +**Null sentinel:** `data8[4:8] == b"\x00\x00\x00\x00"` (= `data_rsp.data[15:19]`) +works for BOTH 1E trailing (offset to next event key) and 1F response (null key +echo) — in both cases, all zeros means "no more events." + +**1E response layout:** `data_rsp.data[11:15]` = event 0's actual key; `data_rsp.data[15:19]` += sample-count offset to the next event key (key1 = key0 + this offset). If offset == 0, +there is only one event. + +**Correct iteration pattern (confirmed from 4-3-26 capture):** +``` +1E(all zeros) → key0, trailing0 ← trailing0 non-zero if event 1 exists +0A(key0) ← establishes device context +1F(token=0xFE at params[7]) → key1, trailing1 +0A(key1) ← establishes context for next advance +1F(token=0xFE) → null ← done ``` `advance_event()` returns `(key4, event_data8)`. diff --git a/minimateplus/client.py b/minimateplus/client.py index ff1e5f8..03fd9d2 100644 --- a/minimateplus/client.py +++ b/minimateplus/client.py @@ -201,9 +201,20 @@ class MiniMateClient: log.info("count_events: 1E returned null sentinel — device is empty") return 0 + # Iterate via 0A → 1F. Each 0A call establishes waveform context so that + # 1F (EVENT_ADVANCE) returns the actual next-event key. Without the 0A + # call, 1F immediately returns the null sentinel regardless of how many + # events are stored. Confirmed from 4-3-26 two-event BW/S3 capture: + # browse-mode sequence is 0A(keyN) → 1F → 0A(keyN+1) → 1F → … → null. count = 0 while data8[4:8] != b"\x00\x00\x00\x00": count += 1 + try: + # 0A establishes device context for this key before 1F advances. + proto.read_waveform_header(key4) + except ProtocolError as exc: + log.warning("count_events: 0A failed for key=%s: %s", key4.hex(), exc) + break try: key4, data8 = proto.advance_event() log.warning( diff --git a/minimateplus/framing.py b/minimateplus/framing.py index 5ba8992..4f0949c 100644 --- a/minimateplus/framing.py +++ b/minimateplus/framing.py @@ -223,21 +223,27 @@ def token_params(token: int = 0) -> bytes: Build the 10-byte params block that carries a single token byte. Used for SUBs 1E (EVENT_HEADER) and 1F (EVENT_ADVANCE). - The token goes at params[6], which maps to payload[12]. + The token goes at params[7], which maps to payload[13]. + + Confirmed from BOTH 3-31-26 and 4-3-26 BW TX captures: + raw params bytes: 00 00 00 00 00 00 00 fe 00 00 + token is at index 7 (not 6 — that was wrong). - Confirmed from 3-31-26 capture: - token=0x00: first-event read / browse mode (no download marking) - token=0xfe: download mode (causes 1F to skip partial bins and advance to the next full record) + The device echoes the token at data[8] of the S3 response (payload[13]), + distinct from the next-event key at data[11:15] (payload[16:20]). + Args: - token: single byte to place at params[6] / payload[12]. + token: single byte to place at params[7] / payload[13]. Returns: - 10-byte params block with token at position [6]. + 10-byte params block with token at position [7]. """ p = bytearray(10) - p[6] = token + p[7] = token return bytes(p)