diff --git a/CLAUDE.md b/CLAUDE.md index 6f9438e..7926acc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -137,14 +137,23 @@ then sends the termination frame. ### SUB 1E / 1F — event iteration null sentinel and token position (FIXED, do not re-introduce) -**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`). +**token_params bug (FIXED):** 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. +**all-zero params required (empirically confirmed):** Even with the correct token position, +sending `token=0xFE` causes the device to return null from 1F in multi-event sessions. +All callers (`count_events`, `get_events`) must use `advance_event(browse=True)` which +sends all-zero params. The 3-31-26 capture that "confirmed" token=0xFE had only one event +stored — 1F always returns null at end-of-events, so we never actually observed 1F +successfully returning a second key with token=0xFE. Empirical evidence from live device +testing with 2+ events is definitive: **always use all-zero params for 1F.** + **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. +when a preceding `read_waveform_header()` (0A) call has established device waveform +context for the current key. Call 0A before every event in the loop, not just the first. +Calling 1F cold (after only 1E, with no 0A) 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: @@ -162,15 +171,19 @@ echo) — in both cases, all zeros means "no more events." = 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):** +**Correct iteration pattern (confirmed empirically with live device, 2+ events):** ``` -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 +1E(all zeros) → key0, trailing0 ← trailing0 non-zero if event 1 exists +0A(key0) ← REQUIRED: establishes device context +0C(key0) [+ 5A(key0) for get_events] ← read record data +1F(all zeros / browse=True) → key1 ← use all-zero params, NOT token=0xFE +0A(key1) ← REQUIRED before each advance +0C(key1) [+ 5A(key1) for get_events] +1F(all zeros) → null ← done ``` +`advance_event(browse=True)` sends all-zero params; `advance_event()` default (browse=False) +sends token=0xFE and is NOT used by any caller. `advance_event()` returns `(key4, event_data8)`. Callers (`count_events`, `get_events`) loop while `data8[4:8] != b"\x00\x00\x00\x00"`. diff --git a/minimateplus/client.py b/minimateplus/client.py index 65e696a..7bb17e2 100644 --- a/minimateplus/client.py +++ b/minimateplus/client.py @@ -274,34 +274,31 @@ class MiniMateClient: return [] events: list[Event] = [] - idx = 0 - is_first = True + idx = 0 while data8[4:8] != b"\x00\x00\x00\x00": log.info("get_events: record %d key=%s", idx, key4.hex()) ev = Event(index=idx) ev._waveform_key = key4 # stored so download_waveform() can re-use it - # First event: call 0A to verify it's a full record (0x30 length). - # Subsequent keys come from 1F(0xFE) which guarantees full records, - # so we skip 0A for those — exactly matching Blastware behaviour. + # Always call 0A before 0C to establish device waveform context. + # The device requires 0A context for both 0C and the subsequent 1F. + # (Earlier code skipped 0A for events after the first — confirmed wrong.) proceed = True - if is_first: - try: - _hdr, rec_len = proto.read_waveform_header(key4) - if rec_len < 0x30: - log.warning( - "get_events: first key=%s is partial (len=0x%02X) — skipping", - key4.hex(), rec_len, - ) - proceed = False - except ProtocolError as exc: + try: + _hdr, rec_len = proto.read_waveform_header(key4) + if rec_len < 0x30: log.warning( - "get_events: 0A failed for key=%s: %s — skipping 0C", - key4.hex(), exc, + "get_events: key=%s is partial (len=0x%02X) — skipping", + key4.hex(), rec_len, ) proceed = False - is_first = False + except ProtocolError as exc: + log.warning( + "get_events: 0A failed for key=%s: %s — skipping 0C", + key4.hex(), exc, + ) + proceed = False if proceed: # SUB 0C — full waveform record (peak values, timestamp, "Project:" string) @@ -357,9 +354,18 @@ class MiniMateClient: events.append(ev) idx += 1 - # SUB 1F — advance to the next full waveform record key + # SUB 1F — advance to the next record key. + # Uses browse mode (all-zero params) — empirically confirmed to work on + # this device. token=0xFE (download mode) returns null regardless of + # context, even after 0A+0C+5A. The 3-31-26 capture only had one event + # so we never saw 1F actually return a second key in download mode; + # the token=0xFE assumption was never validated for multi-event iteration. try: - key4, data8 = proto.advance_event() + key4, data8 = proto.advance_event(browse=True) + log.info( + "get_events: 1F → key=%s trailing=%s", + key4.hex(), data8[4:8].hex(), + ) except ProtocolError as exc: log.warning("get_events: 1F failed: %s — stopping iteration", exc) break