big refactor of waveform protocol.

This commit is contained in:
2026-05-03 01:20:21 -04:00
parent d758825c67
commit 45e61fbcaf
7 changed files with 409 additions and 278 deletions
+47 -54
View File
@@ -672,11 +672,13 @@ def write_blastware_file(
# Do NOT use a5_frames[-1] — if _a5_frames contains stray frames from a
# subsequent event (a known get_events side-effect), the last frame will
# not be the terminator and the footer will be mis-identified.
# TERM detection (v0.14.0):
# Legacy walk: TERM has page_key == 0x0000.
# BW-exact walk: TERM has page_key != 0x0010 (e.g. 0x0001).
# The TERM is always the LAST frame in the list (include_terminator=True).
term_idx: Optional[int] = None
for _i, _f in enumerate(a5_frames):
if _f.page_key == 0x0000:
term_idx = _i
break
if a5_frames and a5_frames[-1].page_key != 0x0010:
term_idx = len(a5_frames) - 1
if term_idx is not None:
body_frames = a5_frames[:term_idx]
@@ -685,64 +687,33 @@ 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.
# Frame contribution loop (v0.14.0 BW-exact walk).
# Frame structure:
# Event 1: [probe] [meta@0x1002] [meta@0x1004] [samples ...] [TERM-not-in-body]
# Event N: [probe@start+0x46] [samples ...] [TERM-not-in-body]
#
# 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
# Skip values per frame (confirmed from byte-diff vs BW-saved file
# M529LKIQ.G10):
# probe (fi=0): probe_skip (depends on STRT position)
# meta@0x1002 (fi=1): 13 (6-byte inner header)
# meta@0x1004 (fi=2): 13 (6-byte inner header)
# sample chunks (fi=3+): 12 (5-byte inner header)
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,
log.debug(
"write_blastware_file: %d body_frames last_fi=%d",
len(body_frames), last_fi,
)
all_bytes = bytearray()
for fi, frame in enumerate(body_frames):
# 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.
# It holds the STRT record; probe_skip positions us past it.
skip = probe_skip
elif fi in (1, 2):
skip = 13 # metadata pages
else:
# ALL subsequent frames are included unconditionally — no filtering on
# frame type. In the A5 stream, frame 0 is always the probe response;
# frames 1+ are always data (waveform chunks, compliance config, or
# compliance continuation). Classification is for logging only.
#
# DO NOT gate on classify_frame() here:
# - "probe_or_strt" at fi>0 is always a false positive — ADC binary
# data can coincidentally contain b"STRT\xff\xfe" (confirmed from
# live capture: frames 1 and 5 matched on event key=01110000).
# - "metadata" frames must be included (compliance config body).
# - The compliance block spans 2 frames; skipping either produces a
# truncated file that Blastware rejects.
skip = 13 if fi == 1 else 12
skip = 12 # sample chunks
contribution = _frame_body_bytes(frame, skip)
log.warning("write_blastware_file: fi=%d skip=%d raw_data=%d contribution=%d",
@@ -769,11 +740,33 @@ def write_blastware_file(
bytes(all_bytes[-28:]).hex() if len(all_bytes) >= 28 else bytes(all_bytes).hex(),
)
if len(all_bytes) >= 26:
# Find the first valid 0e 08 footer marker (v0.14.0). The device's
# TERM response contains the real Blastware footer; older walks
# accidentally fetched data past the footer. Validate by checking the
# year field (uint16 BE at offset+4) is in 2015..2050.
footer_pos = -1
pos = 0
while True:
pos = bytes(all_bytes).find(b"\x0e\x08", pos)
if pos < 0 or pos + 26 > len(all_bytes):
break
yr = (all_bytes[pos + 4] << 8) | all_bytes[pos + 5]
if 2015 <= yr <= 2050:
footer_pos = pos
break
pos += 1
if footer_pos >= 0:
body = bytes(all_bytes[:footer_pos])
footer = bytes(all_bytes[footer_pos:footer_pos + 26])
log.warning(
"write_blastware_file: real 0e 08 footer at all_bytes[%d]; "
"truncating %d post-footer bytes",
footer_pos, len(all_bytes) - footer_pos - 26,
)
elif len(all_bytes) >= 26:
body = bytes(all_bytes[:-26])
footer = bytes(all_bytes[-26:])
else:
# Fallback: no terminator or very short stream → build footer from event metadata
body = bytes(all_bytes)
start_dt = _ts_from_model(event.timestamp)
stop_dt: Optional[datetime.datetime] = None
@@ -784,7 +777,7 @@ def write_blastware_file(
+ _encode_ts_be(start_dt)
+ _encode_ts_be(stop_dt)
+ b"\x00\x01\x00\x02\x00\x00"
+ b"\x00\x00" # CRC placeholder
+ b"\x00\x00"
)
# ── Write file ───────────────────────────────────────────────────────────