d3f77d1d96
Decoded the structural framing of the Blastware waveform body — the bytes between the 21-byte STRT record and the 26-byte file footer. The body is a sequence of tagged variable-length blocks, NOT raw int16 LE. Five tag types (10/20/00/30/40 NN) and their lengths are now confirmed against the 4-event May 2026 fixture bundle. Body splits cleanly into ~16 segments (for a 1280-sample event) separated by 40 02 segment headers carrying a monotonically incrementing uint32 LE counter at bytes [8:12]. What's done: - minimateplus/waveform_codec.py — block walker, segment splitter, segment header parser. decode_waveform_v2 is a stub returning None until the byte-to-sample mapping is solved; client.py is unchanged. - tests/test_waveform_codec.py — 31 tests covering block detection, lengths, contiguous-walk, segment splitting, segment-header parsing, and counter monotonicity. All pass. - tests/fixtures/decode-re-5-8-26/ — bundled fixtures (4 events, BW binary + Blastware ASCII export each). - docs/instantel_protocol_reference.md §7.6.1 — replaced retraction box with the verified structural decoding plus an explicit list of what's still open. What's still open: the per-byte mapping inside 10 NN / 20 NN blocks. 96 channel-permutation × nibble-order × sign-convention combinations were brute-force tested; none match BW's ASCII export to within ±1 ADC count. The codec is more elaborate than uniform 4-bit deltas — likely a hybrid variable-bit-width scheme with segment-anchor resync points. Next recommended step: capture an event with a known calibration tone to pin down magnitude scaling. Walker also bails out partway through event-b (open issue documented in both the module and the protocol reference).
68 lines
2.3 KiB
Python
68 lines
2.3 KiB
Python
"""Try various nibble-level channel interleavings to find which one matches truth."""
|
|
import sys
|
|
sys.path.insert(0, ".")
|
|
from analysis.load_bundle import load_bundle
|
|
|
|
|
|
def s4(n):
|
|
return n if n < 8 else n - 16
|
|
|
|
|
|
def run_decoder(body, layout, skip, n_channels=4):
|
|
"""layout: function nibble_index -> channel_index. Returns list-of-lists per channel."""
|
|
out = [[] for _ in range(n_channels)]
|
|
cur = [0] * n_channels
|
|
nibbles = []
|
|
for byte in body[skip:]:
|
|
nibbles.append((byte >> 4) & 0xF)
|
|
nibbles.append(byte & 0xF)
|
|
for i, n in enumerate(nibbles):
|
|
ch = layout(i)
|
|
cur[ch] += s4(n)
|
|
out[ch].append(cur[ch])
|
|
return out
|
|
|
|
|
|
def cmp(pred, truth, n=24):
|
|
n = min(n, len(pred), len(truth))
|
|
return [(pred[i], truth[i]) for i in range(n)]
|
|
|
|
|
|
def main():
|
|
b = load_bundle("event-c")
|
|
truth_T = [round(v * 200) for v in b.samples["Tran"]]
|
|
truth_V = [round(v * 200) for v in b.samples["Vert"]]
|
|
truth_L = [round(v * 200) for v in b.samples["Long"]]
|
|
print(f"T truth[0:10]: {truth_T[:10]}")
|
|
print(f"V truth[0:10]: {truth_V[:10]}")
|
|
print(f"L truth[0:10]: {truth_L[:10]}")
|
|
|
|
# Try several nibble->channel layouts (4 channels)
|
|
layouts = {
|
|
"interleaved TVLM (0,1,2,3,0,1,2,3,...)": lambda i: i % 4,
|
|
"interleaved VLMT": lambda i: (i + 3) % 4,
|
|
"interleaved LMTV": lambda i: (i + 2) % 4,
|
|
"interleaved MTVL": lambda i: (i + 1) % 4,
|
|
"byte-based TV LM TV LM (high T low V byte0; high L low M byte1)": lambda i: i % 4,
|
|
# "chunks of 8 nibbles per channel": each channel gets 8 nibbles in a row
|
|
"chunks-8 TVLM": lambda i: (i // 8) % 4,
|
|
"chunks-16 TVLM": lambda i: (i // 16) % 4,
|
|
# planar (full channel sequential)
|
|
"planar T(0..N) V(N..2N) L(2N..3N) M(3N..4N)": None, # special
|
|
}
|
|
|
|
for label, layout_fn in layouts.items():
|
|
if layout_fn is None:
|
|
continue
|
|
for skip in (0, 4, 7, 8, 9, 11, 14):
|
|
out = run_decoder(b.body, layout_fn, skip)
|
|
# Check first 8 cumulative on each channel
|
|
print(f" skip={skip:2} {label}")
|
|
print(f" T_cum[0:10]: {out[0][:10]}")
|
|
print(f" V_cum[0:10]: {out[1][:10]}")
|
|
print(f" L_cum[0:10]: {out[2][:10]}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|