From fa887b85d92067f6d4494b3f3c913829ba1e036f Mon Sep 17 00:00:00 2001 From: Brian Harrison Date: Thu, 23 Apr 2026 18:28:14 -0400 Subject: [PATCH] fix(protocol): update extra chunk fetching logic to stop at silence detection --- minimateplus/protocol.py | 35 ++++++++++++++++++++--------------- sfm/server.py | 2 +- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/minimateplus/protocol.py b/minimateplus/protocol.py index a578297..effaf0f 100644 --- a/minimateplus/protocol.py +++ b/minimateplus/protocol.py @@ -652,13 +652,12 @@ class MiniMateProtocol: # and primes the device to return a valid footer in the termination # response. Without it, termination returns an empty ack with no # footer bytes (confirmed 2026-04-23 from HxD comparison). - # Download extra chunks until we find the footer chunk (contains - # 0x0e 0x08 near its end) or hit the extra_chunks_after_metadata - # cap. The footer bytes are embedded in the last meaningful data - # chunk, NOT in the termination response. For short events (1-sec) - # the footer is in extra chunk 1. For longer events it is in a - # later chunk. Confirmed 2026-04-23 from BW file comparison. - log.debug("5A A5[%d] metadata found — fetching extra chunks until footer found", + # Download extra chunks until we hit post-event silence (all-FF + # ADC values) or the cap. The device returns the footer in the + # termination response only when we stop at the right point — + # right after the last real data chunk, before silence starts. + # Silence detection: >80% of payload bytes are 0xFF. + log.debug("5A A5[%d] metadata found — fetching extra chunks until silence", chunk_num) for _extra_n in range(extra_chunks_after_metadata): chunk_num += 1 @@ -667,20 +666,26 @@ class MiniMateProtocol: self._send(build_5a_frame(_BULK_CHUNK_OFFSET, params)) try: extra = self._recv_one(expected_sub=rsp_sub, timeout=10.0) - log.debug("5A A5[%d] extra chunk page_key=0x%04X data_len=%d has_footer=%s", - chunk_num, extra.page_key, len(extra.data), - b'\x0e\x08' in extra.data[-50:]) + payload = extra.data[7:] # skip 7-byte frame header + ff_ratio = payload.count(0xFF) / max(len(payload), 1) + is_silence = ff_ratio > 0.8 + log.debug( + "5A A5[%d] extra chunk page_key=0x%04X data_len=%d " + "ff_ratio=%.2f silence=%s", + chunk_num, extra.page_key, len(extra.data), + ff_ratio, is_silence, + ) if extra.page_key == 0x0000: if include_terminator: frames_data.append(extra) return frames_data - frames_data.append(extra) - # Stop as soon as we find the footer marker in the tail - # of this chunk — the body is complete. - if b'\x0e\x08' in extra.data[-50:]: - log.debug("5A A5[%d] footer marker found — stopping extra chunks", + if is_silence: + # Don't include the silence chunk — terminate here. + # The termination response will contain the footer. + log.debug("5A A5[%d] silence detected — stopping before this chunk", chunk_num) break + frames_data.append(extra) except TimeoutError: log.debug("5A extra chunk %d timed out — end of stream", _extra_n + 1) break diff --git a/sfm/server.py b/sfm/server.py index 13d9c10..21a8308 100644 --- a/sfm/server.py +++ b/sfm/server.py @@ -894,7 +894,7 @@ def device_event_blastware_file( rectime = float(info.compliance_config.record_time or 1.0) except (AttributeError, TypeError, ValueError): pass - extra_chunks = max(4, int(rectime * 4) + 4) + extra_chunks = max(4, int(rectime * 6) + 8) # generous cap; silence detection stops early log.info("blastware_file: rectime=%.1fs → extra_chunks_cap=%d", rectime, extra_chunks) events = client.get_events( full_waveform=False,