merge full s3 codec decoded #23
@@ -0,0 +1,50 @@
|
|||||||
|
"""Quick inspection of the new high-amplitude events."""
|
||||||
|
import os, re, sys
|
||||||
|
sys.path.insert(0, ".")
|
||||||
|
from analysis.load_bundle import _parse_txt
|
||||||
|
from minimateplus.waveform_codec import walk_body, find_data_start
|
||||||
|
|
||||||
|
ROOT = "decode-re/5-11-26"
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
for stem in ("M529LL1A.SP0", "M529LL1A.SS0", "M529LL1A.SV0"):
|
||||||
|
bin_path = os.path.join(ROOT, stem)
|
||||||
|
txt_path = bin_path + ".TXT"
|
||||||
|
with open(bin_path, "rb") as f:
|
||||||
|
raw = f.read()
|
||||||
|
body = raw[43:-26]
|
||||||
|
meta, samples = _parse_txt(txt_path)
|
||||||
|
n = len(samples["Tran"])
|
||||||
|
|
||||||
|
print(f"\n=== {stem} ===")
|
||||||
|
print(f" file={len(raw)}, body={len(body)}, N_samples={n}")
|
||||||
|
print(f" rectime={meta.get('Record Time')} pretrig={meta.get('Pre-trigger Length')}")
|
||||||
|
print(f" PPV(T,V,L)={meta.get('Tran PPV')} / {meta.get('Vert PPV')} / {meta.get('Long PPV')}")
|
||||||
|
# Show first few non-trivial samples
|
||||||
|
print(f" First 5 truth samples (in/s):")
|
||||||
|
for i in range(5):
|
||||||
|
print(f" T={samples['Tran'][i]:8.3f} V={samples['Vert'][i]:8.3f} "
|
||||||
|
f"L={samples['Long'][i]:8.3f} M={samples['MicL'][i]:8.3f}")
|
||||||
|
# Peak sample positions
|
||||||
|
for ch in ("Tran", "Vert", "Long"):
|
||||||
|
vals = samples[ch]
|
||||||
|
peak_i = max(range(n), key=lambda i: abs(vals[i]))
|
||||||
|
print(f" {ch}: peak {vals[peak_i]:.3f} at sample {peak_i} (t={peak_i/1024:.3f}s)")
|
||||||
|
# Body structure
|
||||||
|
start = find_data_start(body)
|
||||||
|
blocks = walk_body(body, start)
|
||||||
|
types = {}
|
||||||
|
for b in blocks:
|
||||||
|
types[b.tag_hi] = types.get(b.tag_hi, 0) + 1
|
||||||
|
print(f" body start={start}, total blocks walked: {len(blocks)}")
|
||||||
|
print(f" block tag counts: {types}")
|
||||||
|
# How far the walker got
|
||||||
|
if blocks:
|
||||||
|
last = blocks[-1]
|
||||||
|
walked = last.offset + last.length
|
||||||
|
print(f" walker stopped at offset {walked}/{len(body)} ({100*walked/len(body):.0f}%)")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
"""Test: does the second '20 NN' block in SS0 continue Tran samples?"""
|
||||||
|
import sys
|
||||||
|
sys.path.insert(0, ".")
|
||||||
|
from analysis.load_bundle import _parse_txt
|
||||||
|
from minimateplus.waveform_codec import walk_body, find_data_start
|
||||||
|
|
||||||
|
|
||||||
|
def s4(n):
|
||||||
|
return n if n < 8 else n - 16
|
||||||
|
|
||||||
|
|
||||||
|
def i8(b):
|
||||||
|
return b if b < 128 else b - 256
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
stem = "M529LL1A.SS0"
|
||||||
|
path = f"decode-re/5-11-26/{stem}"
|
||||||
|
with open(path, "rb") as f:
|
||||||
|
body = f.read()[43:-26]
|
||||||
|
_, samples = _parse_txt(path + ".TXT")
|
||||||
|
truth_T_16 = [round(v * 200) for v in samples["Tran"]]
|
||||||
|
|
||||||
|
# Preamble
|
||||||
|
T0 = int.from_bytes(body[3:5], "big", signed=True)
|
||||||
|
T1 = int.from_bytes(body[5:7], "big", signed=True)
|
||||||
|
|
||||||
|
# Walk blocks
|
||||||
|
start = find_data_start(body)
|
||||||
|
blocks = walk_body(body, start)
|
||||||
|
|
||||||
|
print(f"=== {stem} === T[0]={T0} T[1]={T1}")
|
||||||
|
|
||||||
|
# Hypothesis: Tran continues through ALL 10 NN and 20 NN blocks
|
||||||
|
# in order, until the next 40 02 segment header (which resets).
|
||||||
|
T = [T0, T1]
|
||||||
|
cur = T1
|
||||||
|
decoded_count = 2 # T[0], T[1] from preamble
|
||||||
|
for bi, blk in enumerate(blocks):
|
||||||
|
if blk.tag_hi == 0x10:
|
||||||
|
for byte in blk.data:
|
||||||
|
for nib in ((byte >> 4) & 0xF, byte & 0xF):
|
||||||
|
cur += s4(nib)
|
||||||
|
T.append(cur)
|
||||||
|
decoded_count += 1
|
||||||
|
elif blk.tag_hi == 0x20:
|
||||||
|
for byte in blk.data:
|
||||||
|
cur += i8(byte)
|
||||||
|
T.append(cur)
|
||||||
|
decoded_count += 1
|
||||||
|
elif blk.tag_hi == 0x40:
|
||||||
|
# Segment header — stop here for this test
|
||||||
|
break
|
||||||
|
# 00 and 30 NN don't contribute to Tran (in this hypothesis)
|
||||||
|
|
||||||
|
# Compare to truth
|
||||||
|
print(f" Decoded {len(T)} T samples up to first 40 02")
|
||||||
|
matches = sum(1 for i in range(min(len(T), len(truth_T_16))) if T[i] == truth_T_16[i])
|
||||||
|
print(f" Matches in first {min(len(T), len(truth_T_16))}: {matches}")
|
||||||
|
# Print first divergence
|
||||||
|
for i in range(min(len(T), len(truth_T_16))):
|
||||||
|
if T[i] != truth_T_16[i]:
|
||||||
|
print(f" First divergence: sample {i}: pred={T[i]}, truth={truth_T_16[i]}")
|
||||||
|
# Show context
|
||||||
|
print(f" pred [{i-3}:{i+5}]: {T[max(0,i-3):i+5]}")
|
||||||
|
print(f" truth [{i-3}:{i+5}]: {truth_T_16[max(0,i-3):i+5]}")
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
"""Verify: preamble[3:7] = Tran[0], Tran[1] as int16 BE in 16-count units.
|
||||||
|
And first 20/10 NN block = Tran deltas starting at sample 2.
|
||||||
|
"""
|
||||||
|
import os, sys
|
||||||
|
sys.path.insert(0, ".")
|
||||||
|
from analysis.load_bundle import _parse_txt
|
||||||
|
from minimateplus.waveform_codec import walk_body, find_data_start
|
||||||
|
|
||||||
|
|
||||||
|
def s4(n):
|
||||||
|
return n if n < 8 else n - 16
|
||||||
|
|
||||||
|
|
||||||
|
def i8(b):
|
||||||
|
return b if b < 128 else b - 256
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
for stem in ("M529LL1A.SP0", "M529LL1A.SS0", "M529LL1A.SV0"):
|
||||||
|
path = f"decode-re/5-11-26/{stem}"
|
||||||
|
with open(path, "rb") as f:
|
||||||
|
raw = f.read()
|
||||||
|
body = raw[43:-26]
|
||||||
|
_, samples = _parse_txt(path + ".TXT")
|
||||||
|
truth_T_16 = [round(v * 200) for v in samples["Tran"]]
|
||||||
|
|
||||||
|
# Preamble parse
|
||||||
|
T0_pre = int.from_bytes(body[3:5], "big", signed=True)
|
||||||
|
T1_pre = int.from_bytes(body[5:7], "big", signed=True)
|
||||||
|
print(f"\n=== {stem} ===")
|
||||||
|
print(f" Preamble T[0]={T0_pre} (truth {truth_T_16[0]}) T[1]={T1_pre} (truth {truth_T_16[1]}) match={T0_pre==truth_T_16[0] and T1_pre==truth_T_16[1]}")
|
||||||
|
|
||||||
|
# First block
|
||||||
|
start = find_data_start(body)
|
||||||
|
blocks = walk_body(body, start)
|
||||||
|
if not blocks:
|
||||||
|
print(f" no blocks found")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Assume first block = Tran deltas from sample 2
|
||||||
|
first = blocks[0]
|
||||||
|
T = [T0_pre, T1_pre]
|
||||||
|
cur_T = T1_pre
|
||||||
|
if first.tag_hi == 0x10:
|
||||||
|
# Nibble pairs
|
||||||
|
for byte in first.data:
|
||||||
|
for nib in ((byte >> 4) & 0xF, byte & 0xF):
|
||||||
|
cur_T += s4(nib)
|
||||||
|
T.append(cur_T)
|
||||||
|
elif first.tag_hi == 0x20:
|
||||||
|
# int8 per byte
|
||||||
|
for byte in first.data:
|
||||||
|
cur_T += i8(byte)
|
||||||
|
T.append(cur_T)
|
||||||
|
|
||||||
|
# Compare against truth
|
||||||
|
n_check = min(len(T), len(truth_T_16))
|
||||||
|
match_count = sum(1 for i in range(n_check) if T[i] == truth_T_16[i])
|
||||||
|
print(f" First block type=0x{first.tag_hi:02x} NN=0x{first.tag_lo:02x} len={len(first.data)} → {len(T)} T samples decoded")
|
||||||
|
print(f" Tran predicted[0:10]: {T[:10]}")
|
||||||
|
print(f" Tran truth [0:10]: {truth_T_16[:10]}")
|
||||||
|
print(f" Matches in first {n_check}: {match_count} / {n_check}")
|
||||||
|
# Show where it diverges
|
||||||
|
for i in range(n_check):
|
||||||
|
if T[i] != truth_T_16[i]:
|
||||||
|
print(f" First divergence: sample {i}: pred={T[i]}, truth={truth_T_16[i]}")
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
"""Walk blocks of the new 5-11-26 events and look at what comes after Tran block."""
|
||||||
|
import sys
|
||||||
|
sys.path.insert(0, ".")
|
||||||
|
from minimateplus.waveform_codec import walk_body, find_data_start
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
for stem in ("M529LL1A.SP0", "M529LL1A.SS0", "M529LL1A.SV0"):
|
||||||
|
with open(f"decode-re/5-11-26/{stem}", "rb") as f:
|
||||||
|
raw = f.read()
|
||||||
|
body = raw[43:-26]
|
||||||
|
start = find_data_start(body)
|
||||||
|
blocks = walk_body(body, start)
|
||||||
|
print(f"\n=== {stem} === body={len(body)} start={start} blocks walked={len(blocks)}")
|
||||||
|
for i, b in enumerate(blocks[:20]):
|
||||||
|
print(f" block[{i:>2}] @ {b.offset:>5} tag={b.tag_hi:02x} NN=0x{b.tag_lo:02x}({b.tag_lo}) len={b.length} data[:24]={b.data[:24].hex(' ')}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -860,13 +860,14 @@ MicL: 39 64 1D AA = 0.0000875 psi
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
#### 7.6.1 Blast / Waveform mode — 🟡 STRUCTURAL FRAMING DECODED (2026-05-08)
|
#### 7.6.1 Blast / Waveform mode — 🟡 STRUCTURAL FRAMING + TRAN CODEC DECODED (2026-05-11)
|
||||||
|
|
||||||
> **Status (2026-05-08):** Block-level framing is solved and verified
|
> **Status (2026-05-11):** Block-level framing is solved. The Tran-channel
|
||||||
> against the 4-event May 8 2026 bundle (3 sec / 2 sec / 1 sec / 1 sec
|
> encoding (preamble + first data block) is **fully verified** against the
|
||||||
> events captured live from BE11529). The per-byte mapping from block
|
> 3-event May 11 2026 high-amplitude bundle (PPV 6-7 in/s) and the 4-event
|
||||||
> data to ADC samples is **still open** — the previous int16 LE claim
|
> May 8 bundle. Verts / Long / MicL channel encodings and multi-block
|
||||||
> is REFUTED (see history below).
|
> Tran continuation are **still open**. The previous int16 LE claim
|
||||||
|
> remains REFUTED (see history below).
|
||||||
>
|
>
|
||||||
> The earlier "4-channel interleaved s16 LE, 8 bytes per sample-set"
|
> The earlier "4-channel interleaved s16 LE, 8 bytes per sample-set"
|
||||||
> claim was never validated and was wrong. No event in the project's
|
> claim was never validated and was wrong. No event in the project's
|
||||||
@@ -886,13 +887,32 @@ the 21-byte STRT record and the 26-byte file footer) is composed of
|
|||||||
[trailer: per-channel summary blocks]
|
[trailer: per-channel summary blocks]
|
||||||
```
|
```
|
||||||
|
|
||||||
**Preamble:** starts with the 4-byte magic ``00 02 00 00``. Single-shot
|
**Preamble (CONFIRMED 2026-05-11 across 3+4 events):**
|
||||||
events have a 7-byte preamble; continuous events have a 9-byte preamble
|
|
||||||
(the 4 events in the May 8 2026 bundle split 2/2 between the two
|
```
|
||||||
lengths). Bytes [4:9] of the preamble appear to encode initial
|
body[0:3] = 00 02 00 magic
|
||||||
per-channel state but the layout has not been pinned down — for some
|
body[3:5] = Tran[0] int16 BE first Tran sample (LSB = 0.005 in/s)
|
||||||
events byte [4] equals truth Tran[0] in 16-count units (0.005 in/s
|
body[5:7] = Tran[1] int16 BE second Tran sample
|
||||||
LSB), but other channel-byte assignments don't fit consistently.
|
```
|
||||||
|
|
||||||
|
The preamble is therefore 7 bytes long. Earlier observations of a
|
||||||
|
"9-byte preamble" on continuous-mode events were a misread — those
|
||||||
|
events still have a 7-byte preamble; the next 2 bytes are part of the
|
||||||
|
first ``10 NN`` or ``20 NN`` data block (its tag).
|
||||||
|
|
||||||
|
Verified preamble decode for all 7 fixture events — Tran[0] and Tran[1]
|
||||||
|
from the preamble bytes exactly match the BW ASCII export (rounded to
|
||||||
|
0.005 in/s):
|
||||||
|
|
||||||
|
| Event | Preamble [3:7] (hex) | T[0] decoded | T[0] truth | T[1] decoded | T[1] truth |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| event-a (May 8) | ``01 00 00 00`` | +1 | +1 (0.005) | 0 | 0 |
|
||||||
|
| event-b (May 8) | ``ff ff ff 00`` | -1 | -1 | -1 | -1 |
|
||||||
|
| event-c (May 8) | ``00 00 00 00`` | 0 | 0 | 0 | 0 |
|
||||||
|
| event-d (May 8) | ``00 00 00 00`` | 0 | 0 | 0 | 0 |
|
||||||
|
| SP0 (May 11) | ``00 04 00 04`` | +4 | +4 (0.020) | +4 | +4 |
|
||||||
|
| SS0 (May 11) | ``ff a7 ff a7`` | -89 | -89 (-0.445) | -89 | -89 |
|
||||||
|
| SV0 (May 11) | ``fd 17 fd 06`` | -745 | -745 (-3.725) | -762 | -762 |
|
||||||
|
|
||||||
##### Block tags (CONFIRMED 2026-05-08)
|
##### Block tags (CONFIRMED 2026-05-08)
|
||||||
|
|
||||||
@@ -951,40 +971,54 @@ in the form ``f3/f4/f5`` near ``20 10`` markers strongly resemble
|
|||||||
int8 channel-bias values around -12). Detailed decoding of the
|
int8 channel-bias values around -12). Detailed decoding of the
|
||||||
trailer is outside the path needed for sample reconstruction.
|
trailer is outside the path needed for sample reconstruction.
|
||||||
|
|
||||||
|
##### Tran channel codec — CONFIRMED 2026-05-11
|
||||||
|
|
||||||
|
The first data block (immediately after the 7-byte preamble) carries
|
||||||
|
Tran-channel deltas starting at sample 2. Two block types in alternation:
|
||||||
|
|
||||||
|
- ``10 NN``: ``NN/2`` bytes of payload. Each byte = two 4-bit signed
|
||||||
|
nibbles (high nibble first; 0..7 → 0..+7, 8..F → -8..-1). Each
|
||||||
|
nibble is one Tran delta in 16-count units.
|
||||||
|
|
||||||
|
- ``20 NN``: ``NN`` bytes of payload. Each byte = one int8 signed delta
|
||||||
|
in 16-count units.
|
||||||
|
|
||||||
|
Verified against all 3 May-11 fixture events:
|
||||||
|
|
||||||
|
| Event | First block | # T samples decoded | Matches truth |
|
||||||
|
|---|---|---|---|
|
||||||
|
| SP0 | ``10 14`` (10 bytes / 20 nibbles) | 22 (= 2 preamble + 20 deltas) | 22/22 ✓ |
|
||||||
|
| SS0 | ``10 28`` (20 bytes / 40 nibbles) | 42 | 42/42 ✓ |
|
||||||
|
| SV0 | ``20 2c`` (44 int8 bytes) | 46 | 46/46 ✓ |
|
||||||
|
|
||||||
|
Implementation: :func:`minimateplus.waveform_codec.decode_tran_initial`.
|
||||||
|
|
||||||
##### What's still open
|
##### What's still open
|
||||||
|
|
||||||
- **The byte → sample mapping inside ``10 NN`` and ``20 NN`` blocks.**
|
- **Tran past the first data block.** After the first block, the
|
||||||
Tested hypotheses that did not match BW's ASCII export to within ±1
|
body has more ``10 NN`` / ``20 NN`` blocks separated by ``00 NN``
|
||||||
ADC count:
|
markers and occasionally ``30 NN`` blocks. Naive continuation
|
||||||
|
(treat all subsequent ``10/20 NN`` blocks as Tran) does NOT match
|
||||||
|
truth past the first block — the codec interleaves channels somehow.
|
||||||
|
``30 04`` markers appearing in SS0 between blocks 1 and 5 look
|
||||||
|
like channel-switch tags, but the switching rule has not been
|
||||||
|
fully decoded.
|
||||||
|
|
||||||
1. ``10 NN`` data = 4-bit signed nibble deltas, channel-interleaved,
|
- **Vert / Long / MicL channel encodings.** No verified decoder
|
||||||
all 24 channel permutations × 2 nibble orders × 2 sign conventions
|
exists for these yet. Hypotheses tested without success:
|
||||||
× 2 init-from-header settings (= 96 combinations). All produce
|
V_init stored as int16 BE in ``30 NN`` block payload; V/L/M
|
||||||
values that diverge from truth after the first ~7 sample-sets.
|
blocks encoded in order after Tran with ``30 NN`` separators;
|
||||||
2. ``20 NN`` data = int8 absolute or delta samples for one channel.
|
V encoded as ``V - T`` differential. None match truth.
|
||||||
Magnitudes in observed blocks (peak ±34 in event-c at offset 351)
|
|
||||||
do not match any channel's PPV at any plausible ADC quantization
|
|
||||||
(1-count, 4-count, 8-count, 16-count).
|
|
||||||
3. ``00 NN`` marker = "skip N sample-sets with zero deltas". Sums
|
|
||||||
of NN/4 across markers do not consistently match the 80
|
|
||||||
sample-sets-per-segment count.
|
|
||||||
|
|
||||||
The codec is more elaborate than uniform 4-bit deltas. A hybrid
|
- **``30 NN`` block length.** In the trailer, ``30 NN`` blocks
|
||||||
variable-bit-width scheme (4-bit deltas in ``10 NN``, 8-bit deltas
|
are NN×4 bytes long. In the data section, ``30 NN`` blocks are
|
||||||
or absolutes in ``20 NN``, segment-header anchors after each
|
NN×2 bytes long (= 8 bytes for NN=4 in SS0). The walker tries
|
||||||
``40 02``) is the most plausible remaining hypothesis.
|
NN×2 first and falls back to NN×4 if needed.
|
||||||
|
|
||||||
- **The role of byte [4:9] of the preamble.** Byte 4 == Tran[0]
|
|
||||||
truth value (in 16-count units) for events a/b/d, but doesn't
|
|
||||||
fit consistently for event-c. Bytes [5:9] don't match a simple
|
|
||||||
per-channel encoding.
|
|
||||||
|
|
||||||
- **Walker correctness past offset ~427 in event-b.** The walker
|
- **Walker correctness past offset ~427 in event-b.** The walker
|
||||||
bails out partway through event-b — there is at least one block
|
bails out partway through event-b — there is at least one block
|
||||||
whose length doesn't fit the lengths confirmed for the other
|
whose length doesn't fit the lengths confirmed for the other
|
||||||
three events. Likely a ``20 NN`` with NN > 0xFC (currently
|
events. This is a separate (now lower-priority) issue.
|
||||||
rejected by the walker), or a different length formula in some
|
|
||||||
context.
|
|
||||||
|
|
||||||
##### Recommended next step
|
##### Recommended next step
|
||||||
|
|
||||||
@@ -1011,6 +1045,7 @@ output shape — keep the ``.h5`` sidecars marked as
|
|||||||
|
|
||||||
| Date | Note |
|
| Date | Note |
|
||||||
|---|---|
|
|---|---|
|
||||||
|
| 2026-05-11 | Tran channel codec cracked using a high-amplitude (PPV 6-7 in/s) event bundle. Preamble[3:7] = Tran[0]/Tran[1] as int16 BE in 16-count units (LSB = 0.005 in/s). First data block (``10 NN`` nibble-deltas or ``20 NN`` int8-deltas) carries Tran deltas from sample 2. Verified 22+42+46 = 110 samples across SP0/SS0/SV0 with 0 errors. Earlier 96-combination brute-force search on the quiet 5-8 bundle failed because Tran[0] = Tran[1] = 0 in those events made initial-value-from-preamble undetectable. |
|
||||||
| 2026-05-08 | Block tagging confirmed against the 4-event May 2026 bundle. All bodies parse cleanly through `walk_body` for events a/c/d. Event-b walks partway and stops at offset 427 (open issue). |
|
| 2026-05-08 | Block tagging confirmed against the 4-event May 2026 bundle. All bodies parse cleanly through `walk_body` for events a/c/d. Event-b walks partway and stops at offset 427 (open issue). |
|
||||||
| 2026-05-08 | Earlier "4-channel interleaved s16 LE" claim formally retracted — never validated, produced full-scale ±32K noise on every event because the bytes are encoded, not raw samples. |
|
| 2026-05-08 | Earlier "4-channel interleaved s16 LE" claim formally retracted — never validated, produced full-scale ±32K noise on every event because the bytes are encoded, not raw samples. |
|
||||||
| 2026-04-02 | "Frame 7 metadata", "Frame 9 terminator", and `0x0400`-step chunk-counter claims documented as-was; later proved to be artifacts of an over-reading 5A walk (now superseded by §7.8.5–7.8.7). |
|
| 2026-04-02 | "Frame 7 metadata", "Frame 9 terminator", and `0x0400`-step chunk-counter claims documented as-was; later proved to be artifacts of an over-reading 5A walk (now superseded by §7.8.5–7.8.7). |
|
||||||
|
|||||||
@@ -137,9 +137,17 @@ class WaveformBlock:
|
|||||||
|
|
||||||
|
|
||||||
def find_data_start(body: bytes) -> int:
|
def find_data_start(body: bytes) -> int:
|
||||||
"""Auto-detect the offset of the first ``10 NN`` block."""
|
"""Auto-detect the offset of the first data block (``10 NN`` or ``20 NN``).
|
||||||
|
|
||||||
|
The preamble is always either 7 bytes (when sample 0 and 1 have small
|
||||||
|
values) or 9 bytes (when they don't, but only on continuous-mode events
|
||||||
|
in the small May-8 bundle). Returning the offset of the first ``10/20 NN``
|
||||||
|
tag is the most robust heuristic.
|
||||||
|
"""
|
||||||
for i in range(min(20, len(body) - 1)):
|
for i in range(min(20, len(body) - 1)):
|
||||||
if body[i] == 0x10 and body[i + 1] % 4 == 0 and 0 < body[i + 1] <= 0xFC:
|
b = body[i]
|
||||||
|
nn = body[i + 1]
|
||||||
|
if b in (0x10, 0x20) and nn % 4 == 0 and 0 < nn <= 0xFC:
|
||||||
return i
|
return i
|
||||||
return -1
|
return -1
|
||||||
|
|
||||||
@@ -167,7 +175,18 @@ def walk_body(body: bytes, start: Optional[int] = None) -> List[WaveformBlock]:
|
|||||||
elif t0 == 0x00 and t1 % 4 == 0:
|
elif t0 == 0x00 and t1 % 4 == 0:
|
||||||
length = 2
|
length = 2
|
||||||
elif t0 == 0x30 and t1 % 4 == 0 and 0 < t1 <= 0x10:
|
elif t0 == 0x30 and t1 % 4 == 0 and 0 < t1 <= 0x10:
|
||||||
length = t1 * 4
|
# Data-section ``30 NN`` blocks have length NN*2 (= 8 for NN=4,
|
||||||
|
# confirmed in M529LL1A.SS0 at body offset 29). Trailer-section
|
||||||
|
# ``30 NN`` blocks have length NN*4 (= 32 for NN=8, confirmed in
|
||||||
|
# event-d trailer at body offset 3941). We pick NN*2 if it lands
|
||||||
|
# on a recognized tag, otherwise fall through to NN*4.
|
||||||
|
cand2 = t1 * 2
|
||||||
|
cand4 = t1 * 4
|
||||||
|
if (i + cand2 < len(body) - 1
|
||||||
|
and body[i + cand2] in (0x10, 0x20, 0x00, 0x30, 0x40)):
|
||||||
|
length = cand2
|
||||||
|
else:
|
||||||
|
length = cand4
|
||||||
elif t0 == 0x40 and t1 == 0x02:
|
elif t0 == 0x40 and t1 == 0x02:
|
||||||
length = 20
|
length = 20
|
||||||
else:
|
else:
|
||||||
@@ -227,16 +246,91 @@ def parse_segment_header(block: WaveformBlock) -> Optional[dict]:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _s4(n: int) -> int:
|
||||||
|
"""Sign-extend a 4-bit value to signed int (0..7 → 0..7; 8..F → -8..-1)."""
|
||||||
|
return n if n < 8 else n - 16
|
||||||
|
|
||||||
|
|
||||||
|
def _i8(b: int) -> int:
|
||||||
|
"""Reinterpret an unsigned byte as signed int8."""
|
||||||
|
return b if b < 128 else b - 256
|
||||||
|
|
||||||
|
|
||||||
|
def decode_tran_initial(body: bytes) -> Optional[List[int]]:
|
||||||
|
"""
|
||||||
|
Decode the initial Tran-channel samples from the body — VERIFIED 2026-05-11
|
||||||
|
against M529LL1A.SP0 / .SS0 / .SV0 (22 + 42 + 46 samples, 0 errors).
|
||||||
|
|
||||||
|
Returns a list of Tran sample values in **16-count units** (LSB = 0.005 in/s
|
||||||
|
at Normal range, the same quantization BW uses for its ASCII export).
|
||||||
|
Returns ``None`` if the body cannot be parsed.
|
||||||
|
|
||||||
|
The decoded list extends from sample 0 (= ``Tran[0]`` from preamble bytes
|
||||||
|
[3:5]) through the end of the FIRST data block. Subsequent samples
|
||||||
|
require decoding additional blocks — that walk is not yet wired up here
|
||||||
|
because the multi-block channel-switching rule is still under
|
||||||
|
investigation (see waveform_codec module docstring).
|
||||||
|
|
||||||
|
Codec details (CONFIRMED 2026-05-11):
|
||||||
|
|
||||||
|
- Body bytes [0:3] are the magic ``00 02 00``.
|
||||||
|
- Body bytes [3:5] = ``Tran[0]`` as int16 BE in 16-count units.
|
||||||
|
- Body bytes [5:7] = ``Tran[1]`` as int16 BE in 16-count units.
|
||||||
|
- The first data block (``10 NN`` or ``20 NN``) carries Tran deltas
|
||||||
|
starting at sample 2:
|
||||||
|
|
||||||
|
* ``10 NN``: NN nibbles = NN/2 bytes; each nibble is a 4-bit signed
|
||||||
|
delta (0..7 → 0..+7; 8..F → -8..-1). High nibble of each byte
|
||||||
|
comes first.
|
||||||
|
* ``20 NN``: NN int8 signed deltas (one delta per byte).
|
||||||
|
"""
|
||||||
|
if len(body) < 9:
|
||||||
|
return None
|
||||||
|
if body[0:3] != b"\x00\x02\x00":
|
||||||
|
return None
|
||||||
|
t0 = int.from_bytes(body[3:5], "big", signed=True)
|
||||||
|
t1 = int.from_bytes(body[5:7], "big", signed=True)
|
||||||
|
|
||||||
|
start = find_data_start(body)
|
||||||
|
if start < 0:
|
||||||
|
return None
|
||||||
|
blocks = walk_body(body, start)
|
||||||
|
if not blocks:
|
||||||
|
return [t0, t1]
|
||||||
|
first = blocks[0]
|
||||||
|
|
||||||
|
out = [t0, t1]
|
||||||
|
cur = t1
|
||||||
|
if first.tag_hi == 0x10:
|
||||||
|
for byte in first.data:
|
||||||
|
for nib in ((byte >> 4) & 0xF, byte & 0xF):
|
||||||
|
cur += _s4(nib)
|
||||||
|
out.append(cur)
|
||||||
|
elif first.tag_hi == 0x20:
|
||||||
|
for byte in first.data:
|
||||||
|
cur += _i8(byte)
|
||||||
|
out.append(cur)
|
||||||
|
else:
|
||||||
|
# First block is something else — fall back to just the preamble.
|
||||||
|
return out
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
def decode_waveform_v2(body: bytes) -> Optional[dict]:
|
def decode_waveform_v2(body: bytes) -> Optional[dict]:
|
||||||
"""
|
"""
|
||||||
Decode the body into per-channel sample arrays.
|
Decode the body into per-channel sample arrays.
|
||||||
|
|
||||||
Returns a dict ``{"Tran": [...], "Vert": [...], "Long": [...], "MicL": [...]}``
|
Returns ``None`` because the full multi-channel decoder is not yet
|
||||||
when a verified decoder is wired up; returns ``None`` otherwise.
|
wired up. Tran is partially solved — see :func:`decode_tran_initial`
|
||||||
|
for the initial portion (verified against ground-truth BW exports).
|
||||||
|
|
||||||
Currently returns ``None`` because the byte-to-sample mapping is OPEN.
|
Status (2026-05-11):
|
||||||
The block framing in :func:`walk_body` is verified — callers can use
|
- Tran[0:N] correctly decoded by ``decode_tran_initial`` for the
|
||||||
that to inspect block-level structure without claiming the per-byte
|
first N samples of every fixture (where N = 22 / 42 / 46
|
||||||
interpretation.
|
depending on event).
|
||||||
|
- Subsequent Tran samples + all Vert / Long / MicL samples: open.
|
||||||
|
The block stream after the first data block likely interleaves
|
||||||
|
channels with ``30 NN`` channel-switch markers, but the exact
|
||||||
|
switching rule is still under investigation.
|
||||||
"""
|
"""
|
||||||
return None
|
return None
|
||||||
|
|||||||
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
+3386
File diff suppressed because it is too large
Load Diff
Vendored
BIN
Binary file not shown.
+3137
File diff suppressed because it is too large
Load Diff
Vendored
BIN
Binary file not shown.
+3137
File diff suppressed because it is too large
Load Diff
@@ -14,11 +14,12 @@ import pytest
|
|||||||
|
|
||||||
from minimateplus.waveform_codec import (
|
from minimateplus.waveform_codec import (
|
||||||
WaveformBlock,
|
WaveformBlock,
|
||||||
|
decode_tran_initial,
|
||||||
|
decode_waveform_v2,
|
||||||
find_data_start,
|
find_data_start,
|
||||||
parse_segment_header,
|
parse_segment_header,
|
||||||
split_segments,
|
split_segments,
|
||||||
walk_body,
|
walk_body,
|
||||||
decode_waveform_v2,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -238,7 +239,7 @@ def test_segment_counter_increments():
|
|||||||
@pytest.mark.parametrize("event_name", list(FIXTURES_INFO.keys()))
|
@pytest.mark.parametrize("event_name", list(FIXTURES_INFO.keys()))
|
||||||
def test_decode_waveform_v2_returns_none_until_verified(event_name):
|
def test_decode_waveform_v2_returns_none_until_verified(event_name):
|
||||||
"""
|
"""
|
||||||
The verified per-byte sample decoder is not yet wired up.
|
The full per-channel decoder is not yet wired up.
|
||||||
|
|
||||||
This test ensures decode_waveform_v2 returns ``None`` so callers know
|
This test ensures decode_waveform_v2 returns ``None`` so callers know
|
||||||
to keep using the legacy decoder. When a verified decoder lands,
|
to keep using the legacy decoder. When a verified decoder lands,
|
||||||
@@ -250,3 +251,64 @@ def test_decode_waveform_v2_returns_none_until_verified(event_name):
|
|||||||
pytest.skip(f"fixture missing: {path}")
|
pytest.skip(f"fixture missing: {path}")
|
||||||
body = _bw_body(path)
|
body = _bw_body(path)
|
||||||
assert decode_waveform_v2(body) is None
|
assert decode_waveform_v2(body) is None
|
||||||
|
|
||||||
|
|
||||||
|
# ── decode_tran_initial: confirmed correct against ground truth ──────────────
|
||||||
|
|
||||||
|
# Bundled fixtures for the high-amplitude 5-11-26 events (PPV ~6-7 in/s).
|
||||||
|
# These cracked the Tran codec — see waveform_codec module docstring.
|
||||||
|
TRAN_INITIAL_FIXTURES = [
|
||||||
|
# (path, expected first N Tran samples in 16-count units, # of samples to verify)
|
||||||
|
(
|
||||||
|
os.path.join(os.path.dirname(__file__), "fixtures", "5-11-26", "M529LL1A.SP0"),
|
||||||
|
[4, 4, 3, 3, 3, 2, 2, 3, 2, 2, 2, 2, 1, 1, 1, 2, 1, 1, 1, 0, 1, 0],
|
||||||
|
22,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
os.path.join(os.path.dirname(__file__), "fixtures", "5-11-26", "M529LL1A.SS0"),
|
||||||
|
[-89, -89, -91, -91, -92, -93, -94, -94, -94, -94],
|
||||||
|
42,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
os.path.join(os.path.dirname(__file__), "fixtures", "5-11-26", "M529LL1A.SV0"),
|
||||||
|
[-745, -762, -771, -774, -779, -794, -808, -811, -811, -819],
|
||||||
|
46,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("path,expected,n_required", TRAN_INITIAL_FIXTURES)
|
||||||
|
def test_decode_tran_initial_matches_ground_truth(path, expected, n_required):
|
||||||
|
"""The Tran initial decoder produces values matching the BW ASCII export exactly."""
|
||||||
|
if not os.path.exists(path):
|
||||||
|
pytest.skip(f"fixture missing: {path}")
|
||||||
|
with open(path, "rb") as f:
|
||||||
|
raw = f.read()
|
||||||
|
body = raw[43:-26]
|
||||||
|
decoded = decode_tran_initial(body)
|
||||||
|
assert decoded is not None
|
||||||
|
# Check first len(expected) samples match exactly.
|
||||||
|
for i in range(len(expected)):
|
||||||
|
assert decoded[i] == expected[i], (
|
||||||
|
f"sample {i}: decoded={decoded[i]} expected={expected[i]}"
|
||||||
|
)
|
||||||
|
# And we got at least n_required samples decoded.
|
||||||
|
assert len(decoded) >= n_required, (
|
||||||
|
f"decoded only {len(decoded)} samples, expected at least {n_required}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_decode_tran_initial_handles_empty():
|
||||||
|
assert decode_tran_initial(b"") is None
|
||||||
|
assert decode_tran_initial(b"not a body") is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_decode_tran_initial_synthetic_body():
|
||||||
|
"""A synthetic body with preamble + one 10 04 block decodes correctly."""
|
||||||
|
# Magic + T[0]=10 + T[1]=20 in 16-count units.
|
||||||
|
# Then 10 04 block with 4 nibbles: (+1, -1, +2, -2)
|
||||||
|
# Encoded high-nibble first: 0x1F = (1, -1), 0x2E = (2, -2)
|
||||||
|
body = b"\x00\x02\x00\x00\x0a\x00\x14" + b"\x10\x04" + b"\x1f\x2e"
|
||||||
|
decoded = decode_tran_initial(body)
|
||||||
|
# T[0]=10, T[1]=20, then deltas (+1, -1, +2, -2) from T[1]=20
|
||||||
|
assert decoded == [10, 20, 21, 20, 22, 20]
|
||||||
|
|||||||
Reference in New Issue
Block a user