diff --git a/CLAUDE.md b/CLAUDE.md index 229c554..f8b545f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -194,20 +194,33 @@ there is only one event. 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 +1F(all zeros / browse=True) → key1 ← MUST come BEFORE 5A (BW sequence, confirmed 4-2-26) +POLL × 3 ← REQUIRED: 3 full POLL cycles before 5A (BW frames 68-73) +5A(key0) ← bulk stream; key0 used even though 1F already advanced 0A(key1) 1E(token=0xFE) ← re-arm for next event's 5A 0C(key1) +1F(all zeros) → key2 +POLL × 3 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 1E(token=0xFE) arm step is required (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. +the 4-2-26 and 4-3-26 BW TX captures. + +**1F must come BEFORE 5A (FIXED 2026-04-06):** +BW always calls 1F (advance event) before starting the 5A bulk stream. 5A still uses the +pre-advance key — the device streams the waveform for the key that was set up with 0A+1E-arm+0C +even after 1F has moved the internal pointer to the next event. + +**POLL × 3 required before 5A (FIXED 2026-04-06):** +BW sends exactly 3 complete POLL (SUB 5B) probe+data cycles between the last 1F and the +first 5A probe frame. Confirmed from 4-2-26 BW TX capture frames 68-73. Without these +POLLs the device does not respond to the 5A probe. Use `proto.poll()` (not `startup()` — +`startup()` drains the boot string, which is only needed on initial connect). `advance_event(browse=True)` sends all-zero params; `advance_event()` default (browse=False) sends token=0xFE and is NOT used by any caller. diff --git a/minimateplus/client.py b/minimateplus/client.py index 3088e0d..a75f8d3 100644 --- a/minimateplus/client.py +++ b/minimateplus/client.py @@ -277,68 +277,85 @@ class MiniMateClient: idx = 0 while data8[4:8] != b"\x00\x00\x00\x00": - log.info("get_events: record %d key=%s", idx, key4.hex()) + cur_key = key4 # key for this event's 0A/1E-arm/0C/5A calls + log.info("get_events: record %d key=%s", idx, cur_key.hex()) ev = Event(index=idx) - ev._waveform_key = key4 # stored so download_waveform() can re-use it + ev._waveform_key = cur_key - # 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.) + # SUB 0A — MUST be called first to establish device waveform context. + # Required before 0C, 1E-arm, and 1F. proceed = True try: - _hdr, rec_len = proto.read_waveform_header(key4) + _hdr, rec_len = proto.read_waveform_header(cur_key) if rec_len < 0x30: log.warning( "get_events: key=%s is partial (len=0x%02X) — skipping", - key4.hex(), rec_len, + cur_key.hex(), rec_len, ) proceed = False except ProtocolError as exc: log.warning( - "get_events: 0A failed for key=%s: %s — skipping 0C", - key4.hex(), exc, + "get_events: 0A failed for key=%s: %s — skipping", + cur_key.hex(), exc, ) 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. - log.info("get_events: 1E download-arm (token=0xFE) for key=%s", key4.hex()) + # Device ignores 5A probe frames without this second 1E(token=0xFE). + # Confirmed from both 4-2-26 and 4-3-26 BW TX captures (2026-04-06). + log.info("get_events: 1E download-arm (token=0xFE) for key=%s", cur_key.hex()) try: proto.read_event_first(token=0xFE) log.info("get_events: 1E download-arm OK") except ProtocolError as exc: log.warning( "get_events: 1E download-arm failed for key=%s: %s", - key4.hex(), exc, + cur_key.hex(), exc, ) - # SUB 0C — full waveform record (peak values, timestamp, "Project:" string) + # SUB 0C — full waveform record (peak values, timestamp, project string) try: - record = proto.read_waveform_record(key4) + record = proto.read_waveform_record(cur_key) if debug: ev._raw_record = record _decode_waveform_record_into(record, ev) except ProtocolError as exc: log.warning( - "get_events: 0C failed for key=%s: %s", key4.hex(), exc + "get_events: 0C failed for key=%s: %s", cur_key.hex(), exc ) - # SUB 5A — bulk waveform stream. - # By default (full_waveform=False): stop early after frame 7 ("Project:") - # 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. + # SUB 1F — advance BEFORE 5A (matches BW sequence from 4-2-26 capture). + # Save next key now; 5A will still use cur_key. + try: + 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 after this event", exc) + key4, data8 = b"\x00\x00\x00\x00", b"\x00\x00\x00\x00\x00\x00\x00\x00" + + # POLL × 3 — BW sends 3 full POLL cycles between 1F and 5A. + # Confirmed from 4-2-26 BW TX capture (frames 68-73 before 5A at 74). + log.info("get_events: POLL × 3 before 5A") + for _p in range(3): + try: + proto.poll() + except ProtocolError as exc: + log.warning("get_events: POLL %d failed: %s", _p, exc) + + # SUB 5A — bulk waveform stream (uses cur_key, NOT the advanced key4). + # By default (full_waveform=False): stop after frame 7 for metadata only. + # When full_waveform=True: fetch all chunks and decode raw ADC samples. try: if full_waveform: log.info( - "get_events: 5A full waveform download for key=%s", key4.hex() + "get_events: 5A full waveform download for key=%s", cur_key.hex() ) a5_frames = proto.read_bulk_waveform_stream( - key4, stop_after_metadata=False, max_chunks=128 + cur_key, stop_after_metadata=False, max_chunks=128 ) if a5_frames: _decode_a5_metadata_into(a5_frames, ev) @@ -348,8 +365,11 @@ class MiniMateClient: len((ev.raw_samples or {}).get("Tran", [])), ) else: + log.info( + "get_events: 5A metadata-only download for key=%s", cur_key.hex() + ) a5_frames = proto.read_bulk_waveform_stream( - key4, stop_after_metadata=True + cur_key, stop_after_metadata=True ) if a5_frames: _decode_a5_metadata_into(a5_frames, ev) @@ -360,36 +380,28 @@ class MiniMateClient: ) except ProtocolError as exc: log.warning( - "get_events: 5A failed for key=%s: %s — event-time metadata unavailable", - key4.hex(), exc, + "get_events: 5A failed for key=%s: %s — metadata unavailable", + cur_key.hex(), exc, ) - # Include all full records regardless of sub_code / record_type. - # Partial records (proceed=False, rec_len < 0x30 or 0A failed) are - # the only thing we skip — we have no data to decode for those. - if proceed: events.append(ev) idx += 1 - else: - log.info( - "get_events: key=%s — skipping partial/failed record (rec_len < 0x30)", - key4.hex(), - ) - # SUB 1F — advance to the next record key. - # 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) + else: + # Partial/failed record — skip 5A, just advance with 1F. log.info( - "get_events: 1F → key=%s trailing=%s", - key4.hex(), data8[4:8].hex(), + "get_events: key=%s — skipping partial/failed record", + cur_key.hex(), ) - except ProtocolError as exc: - log.warning("get_events: 1F failed: %s — stopping iteration", exc) - break + try: + 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 log.info("get_events: downloaded %d event(s)", len(events)) return events diff --git a/minimateplus/protocol.py b/minimateplus/protocol.py index 51b1520..af4cbd2 100644 --- a/minimateplus/protocol.py +++ b/minimateplus/protocol.py @@ -241,6 +241,29 @@ class MiniMateProtocol: log.debug("read SUB=0x%02X: received %d data bytes", sub, len(data_rsp.data)) return data_rsp.data + def poll(self) -> S3Frame: + """ + Send a single POLL (SUB 5B) probe+data cycle and return the data response. + + This is a bare POLL cycle with no boot-string drain — use during an active + session (contrast with startup(), which drains the "Operating System" boot + string first). + + Confirmed from 4-2-26 BW TX capture: BW sends exactly 3 of these POLL + cycles between the last 1F and the first 5A probe frame during every + waveform download. Without them the device ignores the 5A probe. + """ + self._send(POLL_PROBE) + self._recv_one( + expected_sub=_expected_rsp_sub(SUB_POLL), + timeout=self._recv_timeout, + ) + self._send(POLL_DATA) + return self._recv_one( + expected_sub=_expected_rsp_sub(SUB_POLL), + timeout=self._recv_timeout, + ) + def send_keepalive(self) -> None: """ Send a single POLL_PROBE keepalive without waiting for a response.