From ab2c11e9a90c7d401744589901cb5891e1ca55e1 Mon Sep 17 00:00:00 2001 From: Brian Harrison Date: Thu, 23 Apr 2026 20:30:07 -0400 Subject: [PATCH] fix(protocol): refine extra chunk fetching logic for accurate termination response --- minimateplus/protocol.py | 32 ++++++++++---------------------- sfm/server.py | 22 +++++++++------------- 2 files changed, 19 insertions(+), 35 deletions(-) diff --git a/minimateplus/protocol.py b/minimateplus/protocol.py index effaf0f..9f48bc5 100644 --- a/minimateplus/protocol.py +++ b/minimateplus/protocol.py @@ -652,13 +652,14 @@ 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 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) + # Download extra_chunks_after_metadata more chunks past the + # metadata. The caller calculates this from record_time and + # sample_rate so we download exactly the right amount of ADC + # data — no more, no less — before terminating. + # The device returns the footer in the termination response only + # after the right amount of data has been consumed. + log.debug("5A A5[%d] metadata found — fetching %d more chunk(s)", + chunk_num, extra_chunks_after_metadata) for _extra_n in range(extra_chunks_after_metadata): chunk_num += 1 counter = chunk_num * _BULK_COUNTER_STEP @@ -666,25 +667,12 @@ class MiniMateProtocol: self._send(build_5a_frame(_BULK_CHUNK_OFFSET, params)) try: extra = self._recv_one(expected_sub=rsp_sub, timeout=10.0) - 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, - ) + log.debug("5A A5[%d] extra chunk page_key=0x%04X data_len=%d", + chunk_num, extra.page_key, len(extra.data)) if extra.page_key == 0x0000: if include_terminator: frames_data.append(extra) return frames_data - 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) diff --git a/sfm/server.py b/sfm/server.py index 21a8308..5d57c49 100644 --- a/sfm/server.py +++ b/sfm/server.py @@ -885,21 +885,17 @@ def device_event_blastware_file( def _do(): with _build_client(port, baud, host, tcp_port, timeout=120.0) as client: info = client.connect() - # Download extra chunks after "Project:" until the footer marker - # 0x0e 0x08 is detected in a chunk tail. The cap prevents - # accidentally downloading into post-event silence. - # Cap = rectime * 4 + 4 covers up to ~10 sec events safely. - rectime = 1.0 - try: - rectime = float(info.compliance_config.record_time or 1.0) - except (AttributeError, TypeError, ValueError): - pass - 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) + # Use full_waveform=True (stop_after_metadata=False) so the device + # streams until it naturally signals end-of-stream (1-raw-byte signal). + # BW does the same — the ACH download and manual pull both let the device + # determine when to stop. The termination response at that point contains + # the correct 0e 08 footer with monitoring timestamps. + # For Continuous/Single-Shot events, end-of-stream comes after the real + # ADC data (not after 35+ silence chunks as in Histogram+Continuous mode). + # max_chunks=32 is a safety cap; natural end-of-stream stops much earlier. events = client.get_events( - full_waveform=False, + full_waveform=True, stop_after_index=index, - extra_chunks_after_metadata=extra_chunks, ) matching = [ev for ev in events if ev.index == index] return matching[0] if matching else None, info