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 Ground-up Python replacement for **Blastware**, Instantel's Windows-only software for
managing MiniMate Plus seismographs. Connects over direct RS-232 or cellular modem 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, `stop_after_metadata=True` (default) stops the 5A loop as soon as `b"Project:"` appears,
then sends the termination frame. 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) ### 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: `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 TCP session, so the index may reflect session-scoped state rather than
device-wide storage. device-wide storage.
This method issues 1E (first key) then 1F repeatedly until the null key This method issues 1E (first key) then 1F repeatedly until the null
b'\\x00\\x00\\x00\\x00', counting as it goes. No 0A/0C/5A reads are sentinel, counting as it goes. No 0A/0C/5A reads are performed, so it
performed, so it is much faster than get_events(). 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: Returns:
Number of stored waveform events (0 if device is empty). Number of stored waveform events (0 if device is empty).
""" """
proto = self._require_proto() proto = self._require_proto()
try: try:
key4, _ = proto.read_event_first() key4, data8 = proto.read_event_first()
except ProtocolError as exc: except ProtocolError as exc:
log.warning("count_events: 1E failed: %s — returning 0", exc) log.warning("count_events: 1E failed: %s — returning 0", exc)
return 0 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 count = 0
while key4 != b"\x00\x00\x00\x00": while data8[4:8] != b"\x00\x00\x00\x00":
count += 1 count += 1
try: try:
key4 = proto.advance_event() key4, data8 = proto.advance_event()
except ProtocolError as exc: except ProtocolError as exc:
log.warning("count_events: 1F failed after %d events: %s", count, exc) log.warning("count_events: 1F failed after %d events: %s", count, exc)
break break
@@ -234,11 +242,14 @@ class MiniMateClient:
log.info("get_events: requesting first event (SUB 1E)") log.info("get_events: requesting first event (SUB 1E)")
try: try:
key4, _event_data8 = proto.read_event_first() key4, data8 = proto.read_event_first()
except ProtocolError as exc: except ProtocolError as exc:
raise ProtocolError(f"get_events: 1E failed: {exc}") from 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") log.info("get_events: device reports no stored events")
return [] return []
@@ -246,7 +257,7 @@ class MiniMateClient:
idx = 0 idx = 0
is_first = True 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()) log.info("get_events: record %d key=%s", idx, key4.hex())
ev = Event(index=idx) ev = Event(index=idx)
ev._waveform_key = key4 # stored so download_waveform() can re-use it 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 # SUB 1F — advance to the next full waveform record key
try: try:
key4 = proto.advance_event() key4, data8 = proto.advance_event()
except ProtocolError as exc: except ProtocolError as exc:
log.warning("get_events: 1F failed: %s — stopping iteration", exc) log.warning("get_events: 1F failed: %s — stopping iteration", exc)
break break

View File

@@ -518,24 +518,33 @@ class MiniMateProtocol:
return frames_data 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 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 In download mode (token=0xFE), the device skips partial histogram bins
and returns the key of the next FULL record directly. This is the and returns the key of the next FULL record directly. This is the
Blastware-observed behaviour for iterating through all stored events. Blastware-observed behaviour for iterating through all stored events.
Returns: Returns:
key4 — 4-byte next waveform key from data[11:15]. (key4, event_data8) where:
Returns b'\\x00\\x00\\x00\\x00' when there are no more events. 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: Raises:
ProtocolError: on timeout, bad checksum, or wrong response SUB. 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) rsp_sub = _expected_rsp_sub(SUB_EVENT_ADVANCE)
length = DATA_LENGTHS[SUB_EVENT_ADVANCE] # 0x08 length = DATA_LENGTHS[SUB_EVENT_ADVANCE] # 0x08
@@ -549,12 +558,14 @@ class MiniMateProtocol:
self._send(build_bw_frame(SUB_EVENT_ADVANCE, length, params)) self._send(build_bw_frame(SUB_EVENT_ADVANCE, length, params))
data_rsp = self._recv_one(expected_sub=rsp_sub) 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( log.debug(
"advance_event: next key=%s done=%s", "advance_event: next key=%s data8=%s done=%s",
key4.hex(), key4 == b"\x00\x00\x00\x00", key4.hex(), event_data8.hex(), is_done,
) )
return key4 return key4, event_data8
def read_compliance_config(self) -> bytes: def read_compliance_config(self) -> bytes:
""" """