fix: update STRT record parsing to reflect confirmed offsets and derive total/pretrig_samples from compliance config

This commit is contained in:
2026-04-14 18:32:16 -04:00
parent dbb9febe2c
commit c5a7914032
2 changed files with 105 additions and 99 deletions
+84 -93
View File
@@ -1338,15 +1338,22 @@ def _decode_a5_waveform(
── Frame structure ──────────────────────────────────────────────────────────
A5[0] (probe response):
db[7:] = [11-byte header] [21-byte STRT record] [6-byte preamble] [waveform ...]
db[7:] = [11-byte header] [21-byte STRT record] [waveform ...]
STRT: b'STRT' at offset 11, total 21 bytes
+8 uint16 BE: total_samples (expected full-record sample-sets)
+16 uint16 BE: pretrig_samples (pre-trigger sample count)
+18 uint8: rectime_seconds (record duration)
Preamble: 6 bytes after the STRT record (confirmed from 4-2-26 blast capture):
bytes 21-22: 0x00 0x00 (null padding)
bytes 23-26: 0xFF × 4 (sync sentinel / alignment marker)
Waveform starts at strt_pos + 27 within db[7:].
+4..5 0xFF 0xFE (single-shot) or 0xFF 0xFD (continuous)
+6..9 next_event_key4 (device pre-allocates next slot)
+10..13 prev_event_key4
+14..15 UNKNOWN (not total_samples)
+16..17 UNKNOWN (not pretrig_samples)
+18 mode byte: 0x46 single-shot, 0x0E continuous
+19..20 0x00 0x00
Waveform starts immediately at strt_pos + 21 (no preamble).
NOTE: The original 4-2-26 blast capture appeared to show a 6-byte
"preamble" (00 00 ff ff ff ff) after the STRT record, but this was
actually the first ~0.75 sample-sets of quiet pre-trigger ADC data
misread as padding. Confirmed 2026-04-14: bytes 21+ are raw waveform.
total_samples and pretrig_samples are NOT stored in the STRT record;
they are derived from the compliance config (the correct permanent source).
A5[1..N] (chunk responses):
db[7:] = [8-byte per-frame header] [waveform bytes ...]
@@ -1380,91 +1387,48 @@ def _decode_a5_waveform(
# STRT record layout (21 bytes, offsets relative to b'STRT'):
# +0..3 magic b'STRT'
# +4..5 0xFF 0xFE (flags)
# +6..9 key4 (4-byte event key)
# +10..13 prev_key4
# +14..15 uint16 BE total_samples (full-record expected sample-set count)
# +16..17 uint16 BE pretrig_samples
# +18 uint8 rectime_seconds
#
# NOTE: strt[8:10] is the LOWER 2 bytes of key4, NOT total_samples.
# Confirmed from raw_rx capture (4-9-26): strt[14:16] = total_samples. ✅
# Extract 32 bytes so we can see past the first 21 — the true total_samples
# and pretrig_samples offsets have not yet been confirmed and may be > +20.
strt = w0[strt_pos : strt_pos + 32]
# +4..5 0xFF 0xFE (single-shot) or 0xFF 0xFD (continuous)
# +6..9 next_event_key4 (NOT current key — device pre-allocates next)
# +10..13 prev_event_key4
# +14..15 UNKNOWN — confirmed NOT total_samples (confirmed 2026-04-14)
# +16..17 UNKNOWN — confirmed NOT pretrig_samples (confirmed 2026-04-14)
# +18 mode byte: 0x46='F' single-shot, 0x0E continuous — NOT rectime
# +19..20 0x00 0x00
# Bytes 21+ are raw ADC waveform samples — no preamble.
# total_samples / pretrig_samples are NOT stored in STRT at all.
# The compliance config fallback is the correct permanent source.
strt = w0[strt_pos : strt_pos + 21]
if len(strt) < 21:
log.warning("_decode_a5_waveform: STRT record truncated (%dB)", len(strt))
return
log.info(
"_decode_a5_waveform: STRT raw[0:32]: %s",
strt[:32].hex(' '),
"_decode_a5_waveform: STRT raw[0:21]: %s",
strt.hex(' '),
)
total_samples = struct.unpack_from(">H", strt, 14)[0]
pretrig_samples = struct.unpack_from(">H", strt, 16)[0]
# ── STRT sanity checks ───────────────────────────────────────────────────
# Check 1: pretrig must be strictly less than total_samples.
# Check 2 (cross-check): if compliance_config is available, the STRT-derived
# post-trigger record time must be within 2× of the configured record_time.
# A 3-second record at 1024 sps = 3072 post-trig samples; if STRT gives
# e.g. 21 s, something is wrong with the byte offsets.
#
# Both failures trigger the same fallback: derive pretrig from the compliance
# monitoring standard (0.25 s) and total from compliance record_time.
_strt_invalid = False
# STRT bytes +14..17 are unknown fields — confirmed NOT total/pretrig_samples
# (2026-04-14). total_samples and pretrig_samples are derived from the
# compliance config, which is the correct permanent source.
_strt_invalid = True
_sample_rate_default = 1024
if pretrig_samples >= total_samples:
log.warning(
"_decode_a5_waveform: STRT check1 FAIL — pretrig_samples=%d >= "
"total_samples=%d. Raw strt[0:21]: %s "
"Will derive pretrig from compliance config.",
pretrig_samples, total_samples, strt[0:21].hex(' '),
)
pretrig_samples = 0
total_samples = 0
_strt_invalid = True
elif compliance_config is not None and compliance_config.record_time:
cc_rt = compliance_config.record_time
cc_sr = compliance_config.sample_rate or _sample_rate_default
strt_post_trig = (total_samples - pretrig_samples) / cc_sr
# Allow up to 2× tolerance to account for zero-padding beyond the window.
# Anything more than that is a layout error, not zero-padding.
if strt_post_trig > cc_rt * 2.0 or strt_post_trig < cc_rt * 0.5:
log.warning(
"_decode_a5_waveform: STRT check2 FAIL — STRT-derived rectime "
"%.1fs is implausible vs compliance record_time=%.1fs (ratio=%.1f×). "
"Raw strt[0:21]: %s Falling back to compliance-config values.",
strt_post_trig, cc_rt, strt_post_trig / cc_rt,
strt[0:21].hex(' '),
)
pretrig_samples = 0
total_samples = 0
_strt_invalid = True
# strt[18] is a record-mode/type byte, NOT rectime in seconds.
# Confirmed from analysis of 4-9-26 ACH capture (15 distinct events):
# flags=0xFFFE (single-shot) → strt[18]=0x46 ('F') for all events regardless of duration
# flags=0xFFFD (continuous) → strt[18]=0x0E for all events regardless of duration
# The actual post-trigger record time must be derived from total_samples and pretrig_samples.
rectime_seconds = int(round(
max(0, total_samples - pretrig_samples) / _sample_rate_default
))
event.total_samples = total_samples
event.pretrig_samples = pretrig_samples
event.rectime_seconds = rectime_seconds
total_samples = 0
pretrig_samples = 0
rectime_seconds = 0
log.info(
"_decode_a5_waveform: STRT total_samples=%d pretrig=%d "
"strt[18]=0x%02X (mode byte) computed_rectime=%ds "
"raw strt[0:21]: %s",
total_samples, pretrig_samples, strt[18], rectime_seconds,
strt[0:21].hex(' '),
"_decode_a5_waveform: STRT flags=0x%04X next_key=%s prev_key=%s "
"mode=0x%02X → using compliance-config for total/pretrig",
struct.unpack_from(">H", strt, 4)[0],
strt[6:10].hex(),
strt[10:14].hex(),
strt[18],
)
event.total_samples = 0 # will be overwritten by compliance-config fallback below
event.pretrig_samples = 0
event.rectime_seconds = 0
# ── 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.
@@ -1474,13 +1438,21 @@ def _decode_a5_waveform(
for fi, db in enumerate(frames_data):
w = db[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]: waveform begins immediately after the 21-byte STRT record.
# Confirmed 2026-04-14: there is NO preamble after STRT — bytes 21+
# are raw ADC sample data. The earlier sp+27 skip was eating 6 bytes
# of real waveform, misaligning the channel decode for all subsequent
# frames.
if fi == 0:
sp = w.find(b"STRT")
if sp < 0:
continue
wave = w[sp + 27 :]
wave = w[sp + 21 :]
log.info(
"_decode_a5_waveform: A5[0] waveform starts at sp+21; "
"first 24 wave bytes: %s",
wave[:24].hex(' '),
)
# Frame 7 carries event-time metadata strings ("Project:", "Client:", …)
# and no waveform ADC data.
@@ -1556,11 +1528,29 @@ def _decode_a5_waveform(
running_offset += len(wave)
n_decoded_total = len(tran)
log.debug(
"_decode_a5_waveform: decoded %d alignment-corrected sample-sets "
"(skipped %d due to frame boundary misalignment)",
len(tran), n_sets - len(tran),
n_decoded_total, n_sets - n_decoded_total,
)
# Log first 16 and last 8 samples for every channel — essential for
# validating decoder output and diagnosing flatline / misalignment issues.
_N = min(16, n_decoded_total)
_L = max(0, n_decoded_total - 8)
log.info(
"_decode_a5_waveform: first %d samples — "
"Tran=%s Vert=%s Long=%s Mic=%s",
_N,
tran[:_N], vert[:_N], long_[:_N], mic[:_N],
)
if n_decoded_total > 16:
log.info(
"_decode_a5_waveform: last 8 samples (idx %d%d) — "
"Tran=%s Vert=%s Long=%s Mic=%s",
_L, n_decoded_total - 1,
tran[_L:], vert[_L:], long_[_L:], mic[_L:],
)
event.raw_samples = {
"Tran": tran,
@@ -1569,10 +1559,10 @@ def _decode_a5_waveform(
"Mic": mic,
}
# ── Correct STRT-invalid pretrig/total from decoded count + compliance config ──
# When the STRT layout was suspect (pretrig >= total_samples), both values were
# zeroed out earlier. Now that we know the actual decoded sample count, use it
# together with the compliance config record_time to derive a meaningful pretrig.
# ── Derive pretrig/total from compliance config (always — STRT doesn't store them) ──
# total_samples and pretrig_samples are not stored in the STRT record (confirmed
# 2026-04-14). They are derived from compliance config record_time × sample_rate.
# _strt_invalid is always True; this block always runs.
#
# Formula:
# post_trig = record_time (s) × sample_rate (sps)
@@ -1604,11 +1594,12 @@ def _decode_a5_waveform(
event.pretrig_samples = derived_pretrig
event.rectime_seconds = int(round(cc_rt))
log.info(
"_decode_a5_waveform: STRT was invalid — using pretrig=%d "
"(%.2fs default) from compliance standard; record_time=%.1fs "
"sr=%d sps decoded=%d samples display_total=%d",
"_decode_a5_waveform: pretrig=%d (%.2fs compliance default) "
"post_trig=%d total=%d record_time=%.1fs "
"sr=%d sps decoded=%d samples",
derived_pretrig, _PRETRIG_SECONDS_DEFAULT,
cc_rt, cc_sr, n_decoded, event.total_samples,
post_trig_samples, event.total_samples,
cc_rt, cc_sr, n_decoded,
)
else:
event.total_samples = n_decoded