diff --git a/CLAUDE.md b/CLAUDE.md index 54af19d..229c554 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -178,16 +178,37 @@ echo) — in both cases, all zeros means "no more events." there is only one event. **Correct iteration pattern (confirmed empirically with live device, 2+ events):** + +`count_events` (browse mode only, no 5A): ``` 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 +1F(all zeros / browse=True) → key1 ← use all-zero params 0A(key1) ← REQUIRED before each advance -0C(key1) [+ 5A(key1) for get_events] 1F(all zeros) → null ← done ``` +`get_events` (download mode, with 5A): +``` +1E(all zeros) → key0, trailing0 ← trailing0 non-zero if event 1 exists +0A(key0) ← REQUIRED: establishes device context +1E(token=0xFE) ← REQUIRED: arms device for 5A; CONFIRMED 4-2-26 + 4-3-26 +0C(key0) ← read waveform record +5A(key0) ← bulk stream (metadata or full waveform) +1F(all zeros / browse=True) → key1 +0A(key1) +1E(token=0xFE) ← re-arm for next event's 5A +0C(key1) +5A(key1) +1F(all zeros) → null ← done +``` + +**The 1E(token=0xFE) arm step is the root cause of the 5A timeout (FIXED 2026-04-06):** +The device silently ignores all 5A probe frames unless a second SUB 1E with token=0xFE +has been issued between 0A and 0C. This step is present in EVERY download cycle in both +the 4-2-26 and 4-3-26 BW TX captures. Without it, `read_bulk_waveform_stream()` blocks +for the full 120 s timeout. + `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)`. diff --git a/minimateplus/client.py b/minimateplus/client.py index 9a841ef..67aa97f 100644 --- a/minimateplus/client.py +++ b/minimateplus/client.py @@ -301,6 +301,19 @@ class MiniMateClient: proceed = False if proceed: + # SUB 1E (download-arm) — MUST be sent between 0A and 0C. + # The device ignores 5A probe frames unless this second 1E with + # token=0xFE has been issued first. Confirmed from both 4-2-26 + # and 4-3-26 BW TX captures (2026-04-06). + # The returned key is the same event key we already hold — ignore it. + try: + proto.read_event_first(token=0xFE) + except ProtocolError as exc: + log.warning( + "get_events: 1E download-arm failed for key=%s: %s", + key4.hex(), exc, + ) + # SUB 0C — full waveform record (peak values, timestamp, "Project:" string) try: record = proto.read_waveform_record(key4) @@ -317,8 +330,6 @@ class MiniMateClient: # is found — fetches only ~8 frames for event-time metadata. # When full_waveform=True: fetch the complete stream (stop_after_metadata=False, # max_chunks=128) and decode raw ADC samples into ev.raw_samples. - # The full waveform MUST be fetched here, inside the 1E→0A→0C→5A→1F loop. - # Issuing 5A after 1F has advanced the event context will time out. try: if full_waveform: log.info( @@ -364,11 +375,10 @@ class MiniMateClient: ) # 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. + # Uses browse mode (all-zero params) — empirically confirmed to work + # with 2+ events stored (4-3-26 browse-mode capture). BW itself uses + # token=0xFE here in download mode, but browse mode is simpler and + # avoids complications with the download-arm state machine. try: key4, data8 = proto.advance_event(browse=True) log.info( diff --git a/minimateplus/protocol.py b/minimateplus/protocol.py index ef285b0..51b1520 100644 --- a/minimateplus/protocol.py +++ b/minimateplus/protocol.py @@ -287,17 +287,22 @@ class MiniMateProtocol: log.debug("read_event_index: got %d bytes", len(raw)) return raw - def read_event_first(self) -> tuple[bytes, bytes]: + def read_event_first(self, token: int = 0) -> tuple[bytes, bytes]: """ Send the SUB 1E (EVENT_HEADER) two-step read and return the first waveform key and accompanying 8-byte event data block. - This always uses all-zero params — the device returns the first stored - event's waveform key unconditionally. + Args: + token: Token byte placed at params[7]. Use 0 (default) for the + initial browse-mode call that returns the first event key. + Use 0xFE for the second "download-arm" call that must be + sent between 0A and 0C when full_waveform=True; the device + will ignore 5A probe frames unless this arm step has been + issued. Confirmed from 4-2-26 and 4-3-26 BW TX captures. Returns: (key4, event_data8) where: - key4 — 4-byte opaque waveform record address (data[11:15]) + key4 — 4-byte opaque waveform record address (data[11:15]) event_data8 — full 8-byte data section (data[11:19]) Raises: @@ -310,13 +315,14 @@ class MiniMateProtocol: """ rsp_sub = _expected_rsp_sub(SUB_EVENT_HEADER) length = DATA_LENGTHS[SUB_EVENT_HEADER] # 0x08 + params = token_params(token) - log.debug("read_event_first: 1E probe") - self._send(build_bw_frame(SUB_EVENT_HEADER, 0)) + log.debug("read_event_first: 1E probe (token=0x%02X)", token) + self._send(build_bw_frame(SUB_EVENT_HEADER, 0, params)) self._recv_one(expected_sub=rsp_sub) - log.debug("read_event_first: 1E data request offset=0x%02X", length) - self._send(build_bw_frame(SUB_EVENT_HEADER, length)) + log.debug("read_event_first: 1E data request offset=0x%02X (token=0x%02X)", length, token) + self._send(build_bw_frame(SUB_EVENT_HEADER, length, params)) data_rsp = self._recv_one(expected_sub=rsp_sub) event_data8 = data_rsp.data[11:19]