From a7585cb5e0ef48b3261a9605540f8629350c5139 Mon Sep 17 00:00:00 2001 From: Brian Harrison Date: Sun, 26 Apr 2026 16:32:32 -0400 Subject: [PATCH] fix(blastware_file, server): implement logic to skip extra chunks after metadata for accurate file writing --- minimateplus/blastware_file.py | 38 ++++++++++++++++++++++++++++++++-- sfm/server.py | 22 ++++++++++++-------- 2 files changed, 49 insertions(+), 11 deletions(-) diff --git a/minimateplus/blastware_file.py b/minimateplus/blastware_file.py index 265d0ca..fedf229 100644 --- a/minimateplus/blastware_file.py +++ b/minimateplus/blastware_file.py @@ -685,11 +685,45 @@ def write_blastware_file( body_frames = a5_frames term_frame = None + # ── Identify first metadata frame and skip "extra chunks" ─────────────── + # When extra_chunks_after_metadata=1 in read_bulk_waveform_stream(), the + # frame list is: [probe, data..., metadata, extra_chunk, terminator]. + # The extra_chunk is downloaded to prime the TCP terminator response — its + # ADC data is NOT part of the Blastware file body. Skip it. + # + # Rule: any frame at index strictly between first_metadata_fi and last_fi + # (the final frame) is an extra chunk and must be excluded. + # + # If no metadata frame exists (e.g. full_waveform download), first_metadata_fi + # is None and no frames are skipped — all frames contribute normally. + first_metadata_fi: Optional[int] = None + for _fi_scan, _frame_scan in enumerate(body_frames): + if _fi_scan > 0 and any(m in bytes(_frame_scan.data) for m in _METADATA_FRAME_MARKERS): + first_metadata_fi = _fi_scan + break + last_fi = len(body_frames) - 1 + + log.warning( + "write_blastware_file: %d body_frames first_metadata_fi=%s last_fi=%d", + len(body_frames), + str(first_metadata_fi) if first_metadata_fi is not None else "None", + last_fi, + ) + all_bytes = bytearray() for fi, frame in enumerate(body_frames): - ftype = classify_frame(frame) - print(f"Frame {fi}: type={ftype}, page_key={frame.page_key:04x}, len={len(frame.data)}") + # Skip "extra chunk" frames: frames after the first metadata frame but + # before the last frame (terminator). These prime the TCP terminator but + # their ADC data must NOT appear in the Blastware file body. + if (first_metadata_fi is not None + and fi > first_metadata_fi + and fi < last_fi): + log.warning( + "write_blastware_file: fi=%d SKIP (extra chunk after metadata fi=%d last_fi=%d)", + fi, first_metadata_fi, last_fi, + ) + continue if fi == 0: # Probe frame: always process regardless of classification. diff --git a/sfm/server.py b/sfm/server.py index 6f96f00..3fc4bb2 100644 --- a/sfm/server.py +++ b/sfm/server.py @@ -885,14 +885,18 @@ def device_event_blastware_file( def _do(): with _build_client(port, baud, host, tcp_port, timeout=120.0) as client: info = client.connect() - # Use stop_after_metadata=True (full_waveform=False) with 0 extra - # chunks after "Project:". Confirmed from 4-26-26 BW RS-232 capture - # of "copy event to file" on a 2-sec Continuous event (key=01110000): - # BW sends the termination frame IMMEDIATELY after the chunk that - # contains "Project:" — no extra chunk is downloaded first. - # extra_chunks_after_metadata=1 was WRONG: it downloaded one additional - # chunk (counter = last_data_counter + 0x0400) adding ~1053 spurious - # bytes to the body, causing Blastware to reject the file. + # Use stop_after_metadata=True (full_waveform=False) with 1 extra + # chunk after "Project:". The extra chunk is required to prime the + # device over TCP: termination at term_counter=metadata_counter+0x0400 + # returns only ~90 bytes (no useful footer) over TCP/cellular, but + # termination at metadata_counter+0x0800 (one chunk later) returns + # the full 737-byte frame containing the footer. + # + # Confirmed from 4-26-26 BW RS-232 capture: BW terminates at 0x1800 + # without an extra chunk (works on RS-232 but not TCP). + # write_blastware_file() automatically skips the extra chunk's + # contribution — only the probe+ADC+metadata+terminator bytes appear + # in the output file. # # full_waveform=True (natural end-of-stream) downloads ALL chunks # including post-event silence (35+ chunks for a 9-sec event at @@ -900,7 +904,7 @@ def device_event_blastware_file( events = client.get_events( full_waveform=False, stop_after_index=index, - extra_chunks_after_metadata=0, + extra_chunks_after_metadata=1, ) matching = [ev for ev in events if ev.index == index] return matching[0] if matching else None, info