feat: update event iteration logic to use null sentinel for end-of-events detection

This commit is contained in:
Brian Harrison
2026-04-03 16:29:10 -04:00
parent 2cb95cd45e
commit 95f2becf21
3 changed files with 64 additions and 22 deletions

View File

@@ -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:

View File

@@ -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

View File

@@ -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:
"""