From 95f2becf21e07e4064fe5cbee22685cd256e41e6 Mon Sep 17 00:00:00 2001 From: Brian Harrison Date: Fri, 3 Apr 2026 16:29:10 -0400 Subject: [PATCH] feat: update event iteration logic to use null sentinel for end-of-events detection --- CLAUDE.md | 22 +++++++++++++++++++++- minimateplus/client.py | 31 +++++++++++++++++++++---------- minimateplus/protocol.py | 33 ++++++++++++++++++++++----------- 3 files changed, 64 insertions(+), 22 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 164cd62..4755277 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,7 +2,7 @@ Ground-up Python replacement for **Blastware**, Instantel's Windows-only software for managing MiniMate Plus seismographs. Connects over direct RS-232 or cellular modem -(Sierra Wireless RV50 / RV55). Current version: **v0.6.0**. +(Sierra Wireless RV50 / RV55). Current version: **v0.7.0**. --- @@ -135,6 +135,26 @@ 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) + +**The null sentinel for end-of-events is `event_data8[4:8] == b"\x00\x00\x00\x00"`, NOT +`key4 == b"\x00\x00\x00\x00"`.** + +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/`): + +``` +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 +``` + +`advance_event()` returns `(key4, event_data8)`. +Callers (`count_events`, `get_events`) loop while `data8[4:8] != b"\x00\x00\x00\x00"`. + ### SUB 1A — compliance config — orphaned send bug (FIXED, do not re-introduce) `read_compliance_config()` sends a 4-frame sequence (A, B, C, D) where: diff --git a/minimateplus/client.py b/minimateplus/client.py index 9e89064..0380481 100644 --- a/minimateplus/client.py +++ b/minimateplus/client.py @@ -174,25 +174,33 @@ class MiniMateClient: TCP session, so the index may reflect session-scoped state rather than device-wide storage. - This method issues 1E (first key) then 1F repeatedly until the null key - b'\\x00\\x00\\x00\\x00', counting as it goes. No 0A/0C/5A reads are - performed, so it is much faster than get_events(). + This method issues 1E (first key) then 1F repeatedly until the null + sentinel, counting as it goes. No 0A/0C/5A reads are performed, so it + is much faster than get_events(). + + Null sentinel: event_data8[4:8] == b'\\x00\\x00\\x00\\x00'. + DO NOT check key4 — key4 is all-zeros for event 0 and would falsely + signal end-of-events on the very first iteration. Returns: Number of stored waveform events (0 if device is empty). """ proto = self._require_proto() try: - key4, _ = proto.read_event_first() + key4, data8 = proto.read_event_first() except ProtocolError as exc: log.warning("count_events: 1E failed: %s — returning 0", exc) return 0 + if data8[4:8] == b"\x00\x00\x00\x00": + log.info("count_events: 1E returned null sentinel — device is empty") + return 0 + count = 0 - while key4 != b"\x00\x00\x00\x00": + while data8[4:8] != b"\x00\x00\x00\x00": count += 1 try: - key4 = proto.advance_event() + key4, data8 = proto.advance_event() except ProtocolError as exc: log.warning("count_events: 1F failed after %d events: %s", count, exc) break @@ -234,11 +242,14 @@ class MiniMateClient: log.info("get_events: requesting first event (SUB 1E)") try: - key4, _event_data8 = proto.read_event_first() + key4, data8 = proto.read_event_first() except ProtocolError as exc: raise ProtocolError(f"get_events: 1E failed: {exc}") from exc - if key4 == b"\x00\x00\x00\x00": + # Null sentinel: trailing 4 bytes of the 8-byte event data block are + # all zero. DO NOT use key4 == b"\x00\x00\x00\x00" — event 0 has + # key4=00000000 which would falsely signal an empty device. + if data8[4:8] == b"\x00\x00\x00\x00": log.info("get_events: device reports no stored events") return [] @@ -246,7 +257,7 @@ class MiniMateClient: idx = 0 is_first = True - while key4 != b"\x00\x00\x00\x00": + 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 @@ -328,7 +339,7 @@ class MiniMateClient: # SUB 1F — advance to the next full waveform record key try: - key4 = proto.advance_event() + key4, data8 = proto.advance_event() except ProtocolError as exc: log.warning("get_events: 1F failed: %s — stopping iteration", exc) break diff --git a/minimateplus/protocol.py b/minimateplus/protocol.py index 7103624..8694845 100644 --- a/minimateplus/protocol.py +++ b/minimateplus/protocol.py @@ -518,24 +518,33 @@ class MiniMateProtocol: return frames_data - def advance_event(self) -> bytes: + def advance_event(self) -> tuple[bytes, bytes]: """ Send the SUB 1F (EVENT_ADVANCE) two-step read with download-mode token - (0xFE) and return the next waveform key. + (0xFE) and return the next waveform key and the full 8-byte event data + block. In download mode (token=0xFE), the device skips partial histogram bins and returns the key of the next FULL record directly. This is the Blastware-observed behaviour for iterating through all stored events. Returns: - key4 — 4-byte next waveform key from data[11:15]. - Returns b'\\x00\\x00\\x00\\x00' when there are no more events. + (key4, event_data8) where: + key4 — 4-byte opaque waveform record address (data[11:15]). + event_data8 — full 8-byte block (data[11:19]). + + End-of-events sentinel: event_data8[4:8] == b'\\x00\\x00\\x00\\x00'. + DO NOT use key4 == b'\\x00\\x00\\x00\\x00' as the sentinel — key4 is + all-zeros for event 0 (the very first stored event) and will cause the + loop to terminate prematurely. + + Confirmed from 4-3-26 two-event capture: + - event 0 1E response: key4=00000000 data8=0000000000011100 (valid) + - event 1 1F response: key4=0000fe00 data8=0000fe0000011100 (valid) + - null 1F response: key4=0000fe00 data8=0000fe0000000000 ← trailing zeros Raises: ProtocolError: on timeout, bad checksum, or wrong response SUB. - - Confirmed from 3-31-26 capture: 1F uses token=0xFE at params[6]; - loop termination is key4 == b'\\x00\\x00\\x00\\x00'. """ rsp_sub = _expected_rsp_sub(SUB_EVENT_ADVANCE) length = DATA_LENGTHS[SUB_EVENT_ADVANCE] # 0x08 @@ -549,12 +558,14 @@ class MiniMateProtocol: self._send(build_bw_frame(SUB_EVENT_ADVANCE, length, params)) data_rsp = self._recv_one(expected_sub=rsp_sub) - key4 = data_rsp.data[11:15] + event_data8 = data_rsp.data[11:19] + key4 = data_rsp.data[11:15] + is_done = event_data8[4:8] == b"\x00\x00\x00\x00" log.debug( - "advance_event: next key=%s done=%s", - key4.hex(), key4 == b"\x00\x00\x00\x00", + "advance_event: next key=%s data8=%s done=%s", + key4.hex(), event_data8.hex(), is_done, ) - return key4 + return key4, event_data8 def read_compliance_config(self) -> bytes: """