From 2cb95cd45e9772ea230402315e1ea80ce496eec1 Mon Sep 17 00:00:00 2001 From: Brian Harrison Date: Fri, 3 Apr 2026 16:02:10 -0400 Subject: [PATCH] feat: implement reliable event counting via 1E/1F chain and update device info --- minimateplus/client.py | 49 +++++++++++++++++++++++++++++++++++++----- sfm/server.py | 6 +++++- 2 files changed, 49 insertions(+), 6 deletions(-) diff --git a/minimateplus/client.py b/minimateplus/client.py index a6e3d22..9e89064 100644 --- a/minimateplus/client.py +++ b/minimateplus/client.py @@ -163,6 +163,43 @@ class MiniMateClient: log.info("connect: %s", device_info) return device_info + def count_events(self) -> int: + """ + Count stored events by iterating the 1E → 1F key chain. + + This is the only reliable way to get the true event count. The SUB 08 + event index payload has a field that was assumed to be an event count + (uint32 BE at offset +3) but empirically always returns 1 regardless of + how many events are stored — Blastware appears to download one event per + 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(). + + Returns: + Number of stored waveform events (0 if device is empty). + """ + proto = self._require_proto() + try: + key4, _ = proto.read_event_first() + except ProtocolError as exc: + log.warning("count_events: 1E failed: %s — returning 0", exc) + return 0 + + count = 0 + while key4 != b"\x00\x00\x00\x00": + count += 1 + try: + key4 = proto.advance_event() + except ProtocolError as exc: + log.warning("count_events: 1F failed after %d events: %s", count, exc) + break + + log.info("count_events: %d event(s) found via 1E/1F chain", count) + return count + def get_events(self, full_waveform: bool = False, debug: bool = False) -> list[Event]: """ Download all stored events from the device using the confirmed @@ -475,11 +512,13 @@ def _decode_event_count(data: bytes) -> int: log.warning("event index payload too short (%d bytes), assuming 0 events", len(data)) return 0 - # Log the raw bytes so we can verify this decode against known event counts - log.warning( - "event_index raw (first 16 bytes): %s", - " ".join(f"{b:02x}" for b in data[:16]), - ) + # Log the full payload so we can reverse-engineer the format + log.warning("event_index raw (%d bytes total):", len(data)) + for off in range(0, len(data), 16): + chunk = data[off:off+16] + hex_part = " ".join(f"{b:02x}" for b in chunk) + asc_part = "".join(chr(b) if 0x20 <= b < 0x7f else "." for b in chunk) + log.warning(" [%04x]: %-47s %s", off, hex_part, asc_part) # Try the uint32 at +3 first count = struct.unpack_from(">I", data, 3)[0] diff --git a/sfm/server.py b/sfm/server.py index fc87f8e..8a57408 100644 --- a/sfm/server.py +++ b/sfm/server.py @@ -264,7 +264,11 @@ def device_info( try: def _do(): with _build_client(port, baud, host, tcp_port) as client: - return client.connect() + info = client.connect() + # SUB 08 event_count is unreliable (always returns 1 regardless of + # actual storage). Count via 1E/1F chain instead. + info.event_count = client.count_events() + return info info = _run_with_retry(_do, is_tcp=_is_tcp(host)) except HTTPException: raise