feat: add waveform store handling #16
+78
-33
@@ -1572,59 +1572,106 @@ def _decode_a5_waveform(
|
||||
# STRT byte layout (21 bytes; verified against M529LIY6 reference files
|
||||
# and re-confirmed against live BE11529 captures, 2026-05-08):
|
||||
# [0:4] b'STRT'
|
||||
# [4:6] 0xff 0xfe fixed
|
||||
# [6:10] end_key (4-byte device flash address where event ends)
|
||||
# [10:14] start_key (4-byte device flash address where event starts)
|
||||
# [14:18] device-specific (4 bytes; semantics not pinned)
|
||||
# [18] 0x46 record-type marker (= 70 in decimal — NOT rectime!)
|
||||
# [4:6] 0xff 0xfe sentinel
|
||||
# [6:10] end_key 4-byte BE flash address where event ends
|
||||
# [10:14] start_key 4-byte BE flash address where event starts
|
||||
# [14:18] device-specific (semantics not pinned; values vary across events
|
||||
# and don't hold authoritative total_samples / pretrig)
|
||||
# [18] 0x46 record-type marker (NOT rectime)
|
||||
# [19] device-specific
|
||||
# [20] rectime (uint8 seconds, user-set Record Time)
|
||||
# [20] sometimes rectime, sometimes 0 — not reliable
|
||||
#
|
||||
# The earlier reading of `rectime_seconds = strt[18]` always returned
|
||||
# 70 for a real waveform event because it was reading the 0x46 marker.
|
||||
# Caller should prefer compliance_config.record_time when available
|
||||
# (that's the authoritative user-set value) and fall back to this.
|
||||
total_samples = struct.unpack_from(">H", strt, 8)[0]
|
||||
pretrig_samples = struct.unpack_from(">H", strt, 16)[0]
|
||||
rectime_seconds = strt[20]
|
||||
# AUTHORITATIVE values must come from compliance_config (sample_rate,
|
||||
# record_time) and from end_offset - start_offset arithmetic (event size).
|
||||
# Earlier code claimed STRT[8:10]=total_samples and STRT[16:18]=pretrig;
|
||||
# those positions actually overlap end_key low-word and dev-specific bytes
|
||||
# respectively. We surface the address-derived event size so consumers
|
||||
# can sanity-check chunk-loop bounds, but `total_samples` per channel must
|
||||
# be derived externally (sample_rate × record_time, or computed from the
|
||||
# decoded sample count below).
|
||||
end_key = strt[6:10]
|
||||
start_key = strt[10:14]
|
||||
end_offset_in_strt = (end_key[2] << 8) | end_key[3]
|
||||
start_offset_in_strt = (start_key[2] << 8) | start_key[3]
|
||||
is_event_1 = (start_offset_in_strt == 0x0000)
|
||||
|
||||
event.total_samples = total_samples
|
||||
event.pretrig_samples = pretrig_samples
|
||||
event.rectime_seconds = rectime_seconds
|
||||
# Don't trust STRT for these — leave them as None so the caller can
|
||||
# backfill from compliance_config (the authoritative source).
|
||||
event.total_samples = None
|
||||
event.pretrig_samples = None
|
||||
event.rectime_seconds = None
|
||||
|
||||
log.debug(
|
||||
"_decode_a5_waveform: STRT total_samples=%d pretrig=%d rectime=%ds "
|
||||
"(strt[18]=0x%02X record-type marker, strt[20]=0x%02X rectime)",
|
||||
total_samples, pretrig_samples, rectime_seconds, strt[18], strt[20],
|
||||
"_decode_a5_waveform: STRT start_key=%s end_key=%s "
|
||||
"start_off=0x%04X end_off=0x%04X is_event_1=%s "
|
||||
"dev-specific[14:18]=%s strt[20]=0x%02X",
|
||||
start_key.hex(), end_key.hex(),
|
||||
start_offset_in_strt, end_offset_in_strt, is_event_1,
|
||||
strt[14:18].hex(), strt[20],
|
||||
)
|
||||
|
||||
# ── Collect per-frame waveform bytes with global offset tracking ─────────
|
||||
# global_offset is the cumulative byte count across all frames, used to
|
||||
# compute the channel alignment at each frame boundary.
|
||||
#
|
||||
# Frame layout under the v0.14.0+ walk:
|
||||
# frames_data[0] = probe response (page_addr 0x0000;
|
||||
# contains STRT + post-STRT data)
|
||||
# frames_data[1..2] = (event 1 only) metadata pages
|
||||
# page_addr = 0x1002 / 0x1004
|
||||
# frames_data[mid] = sample chunks at flash addresses
|
||||
# 0x0600, 0x0800, … (page_addr in
|
||||
# {0x0600..0x1FFE})
|
||||
# frames_data[last] = TERM response (page_key=0x0000)
|
||||
#
|
||||
# We identify metadata pages by their PAGE ADDRESS at db.data[4:6] (the
|
||||
# 2-byte counter the device echoes back), NOT by content scan. An earlier
|
||||
# needle-based detection (b"Project:", b"Client:", etc.) was the wrong
|
||||
# layer of abstraction:
|
||||
# • The actual metadata pages 0x1002 / 0x1004 do NOT contain ASCII
|
||||
# project strings on this firmware (S338.17 / BE11529).
|
||||
# • The strings physically live at flash address 0x1600 — which falls
|
||||
# inside the sample-chunk address range. Skipping that frame would
|
||||
# drop a real sample chunk.
|
||||
# BW handles the "samples region happens to contain string bytes" case
|
||||
# by just rendering the bytes verbatim; we do the same.
|
||||
_METADATA_PAGES = (b"\x10\x02", b"\x10\x04")
|
||||
|
||||
chunks: list[tuple[int, bytes]] = [] # (frame_idx, waveform_bytes)
|
||||
global_offset = 0
|
||||
|
||||
for fi, db in enumerate(frames_data):
|
||||
page_addr = db.data[4:6] if len(db.data) >= 6 else b""
|
||||
w = db.data[7:] # frame.data[7:]
|
||||
|
||||
# A5[0]: waveform begins after the 21-byte STRT record and 6-byte preamble.
|
||||
# Layout: STRT(21B) + null-pad(2B) + 0xFF sentinel(4B) = 27 bytes total.
|
||||
# A5[0]: probe response. Two cases:
|
||||
# - Event 1 (start_offset_in_strt == 0x0000): the bytes after STRT
|
||||
# are the device's *pre-event reserved area* (flash 0x0046 to
|
||||
# 0x0600), NOT samples. We must skip them; samples begin at
|
||||
# the first dedicated chunk frame at counter=0x0600.
|
||||
# - Event N (continuation, start_offset != 0x0000): the bytes after
|
||||
# the STRT record ARE the first slice of real samples for the
|
||||
# event (BW's chunk loop addresses the probe as a sample chunk).
|
||||
if fi == 0:
|
||||
sp = w.find(b"STRT")
|
||||
if sp < 0:
|
||||
continue
|
||||
if is_event_1:
|
||||
# No usable samples in the probe — pre-event reserved bytes.
|
||||
continue
|
||||
# Layout: STRT(21B) + null-pad(2B) + 0xFF sentinel(4B) = 27 bytes total.
|
||||
wave = w[sp + 27 :]
|
||||
|
||||
# Frame 7 carries event-time metadata strings ("Project:", "Client:", …)
|
||||
# and no waveform ADC data.
|
||||
elif fi == 7:
|
||||
# Skip the dedicated metadata pages (event 1 only): page_addr 0x1002 / 0x1004.
|
||||
elif page_addr in _METADATA_PAGES:
|
||||
log.debug(
|
||||
"_decode_a5_waveform: skipping metadata page fi=%d page_addr=%s",
|
||||
fi, page_addr.hex(),
|
||||
)
|
||||
continue
|
||||
|
||||
# Terminator frames have page_key=0x0000 and are excluded upstream
|
||||
# (read_bulk_waveform_stream returns early on page_key==0).
|
||||
# No hardcoded frame-index skip here — all non-metadata frames are data.
|
||||
# Sample chunk (or TERM): strip the 8-byte per-frame header.
|
||||
else:
|
||||
# Strip the 8-byte per-frame header (ctr + 6 zero bytes)
|
||||
if len(w) < 8:
|
||||
continue
|
||||
wave = w[8:]
|
||||
@@ -1638,10 +1685,8 @@ def _decode_a5_waveform(
|
||||
total_bytes = global_offset
|
||||
n_sets = total_bytes // 8
|
||||
log.debug(
|
||||
"_decode_a5_waveform: %d chunks, %dB total → %d complete sample-sets "
|
||||
"(%d of %d expected; %.0f%%)",
|
||||
len(chunks), total_bytes, n_sets, n_sets, total_samples,
|
||||
100.0 * n_sets / total_samples if total_samples else 0,
|
||||
"_decode_a5_waveform: %d chunks, %dB total → %d complete sample-sets",
|
||||
len(chunks), total_bytes, n_sets,
|
||||
)
|
||||
|
||||
if n_sets == 0:
|
||||
@@ -1699,7 +1744,7 @@ def _decode_a5_waveform(
|
||||
"Tran": tran,
|
||||
"Vert": vert,
|
||||
"Long": long_,
|
||||
"Mic": mic,
|
||||
"MicL": mic,
|
||||
}
|
||||
|
||||
|
||||
|
||||
+1
-1
@@ -690,7 +690,7 @@ def device_event_waveform(
|
||||
if the device is not storing all frames yet, or the capture was partial)
|
||||
- **sample_rate**: samples per second (from compliance config)
|
||||
- **channels**: dict of channel name → list of signed int16 ADC counts
|
||||
(keys: "Tran", "Vert", "Long", "Mic")
|
||||
(keys: "Tran", "Vert", "Long", "MicL")
|
||||
|
||||
**Caching**: full waveforms are cached permanently after the first download —
|
||||
they are immutable once recorded on the device. Subsequent requests for the
|
||||
|
||||
Reference in New Issue
Block a user