big refactor of waveform protocol.
This commit is contained in:
@@ -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 ───────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user