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).
94 lines
3.3 KiB
Python
94 lines
3.3 KiB
Python
"""Brute-force test channel permutations / nibble orders on event-d (simplest signal)."""
|
|
import sys
|
|
import itertools
|
|
sys.path.insert(0, ".")
|
|
from analysis.load_bundle import load_bundle
|
|
from minimateplus.waveform_codec import walk_body
|
|
|
|
|
|
def s4(n):
|
|
return n if n < 8 else n - 16
|
|
|
|
|
|
def decode(body, channel_perm, nibble_order, sign_mode, init_from_header):
|
|
"""Try one decoder configuration on event-d. Returns first 8 cumulative samples per channel."""
|
|
blocks = walk_body(body)
|
|
# Initial values from bytes [4:7] if init_from_header else 0
|
|
if init_from_header:
|
|
init = [body[4] if body[4] < 128 else body[4] - 256,
|
|
body[5] if body[5] < 128 else body[5] - 256,
|
|
body[6] if body[6] < 128 else body[6] - 256,
|
|
0]
|
|
else:
|
|
init = [0, 0, 0, 0]
|
|
cur = list(init)
|
|
out = [[init[0]], [init[1]], [init[2]], [init[3]]] # sample 0 = init
|
|
nibble_idx = 0 # within delta stream; channel = channel_perm[nibble_idx % 4]
|
|
|
|
# Walk only the 10 NN data blocks
|
|
for blk in blocks:
|
|
if blk.tag_hi != 0x10:
|
|
continue
|
|
for byte in blk.data:
|
|
if nibble_order == 'high_first':
|
|
nib1, nib2 = (byte >> 4) & 0xF, byte & 0xF
|
|
else:
|
|
nib1, nib2 = byte & 0xF, (byte >> 4) & 0xF
|
|
for nib in (nib1, nib2):
|
|
if sign_mode == 'signed':
|
|
delta = s4(nib)
|
|
else:
|
|
delta = nib
|
|
ch = channel_perm[nibble_idx % 4]
|
|
cur[ch] += delta
|
|
if (nibble_idx + 1) % 4 == 0:
|
|
out[0].append(cur[0])
|
|
out[1].append(cur[1])
|
|
out[2].append(cur[2])
|
|
out[3].append(cur[3])
|
|
nibble_idx += 1
|
|
if len(out[0]) >= 16:
|
|
return out
|
|
return out
|
|
|
|
|
|
def best_match(pred, truth, n=10):
|
|
"""Sum of squared differences in first n samples."""
|
|
n = min(n, len(pred), len(truth))
|
|
return sum((pred[i] - truth[i])**2 for i in range(n))
|
|
|
|
|
|
def main():
|
|
b = load_bundle("event-d")
|
|
# truth in 16-count units
|
|
tr = {ch: [round(v * 200) for v in b.samples[ch]] for ch in ("Tran", "Vert", "Long")}
|
|
|
|
print("Truth event-d first 10 samples:")
|
|
for ch in ("Tran", "Vert", "Long"):
|
|
print(f" {ch}: {tr[ch][:10]}")
|
|
|
|
# Test 96 combinations
|
|
best = []
|
|
for perm in itertools.permutations([0, 1, 2, 3]):
|
|
for nibble_order in ('high_first', 'low_first'):
|
|
for sign in ('signed', 'unsigned'):
|
|
for init_h in (False, True):
|
|
decoded = decode(b.body, perm, nibble_order, sign, init_h)
|
|
# Score as TVL channel-sum
|
|
score = sum(
|
|
best_match(decoded[i], tr[ch], n=10)
|
|
for i, ch in enumerate(("Tran", "Vert", "Long"))
|
|
if i < 3
|
|
)
|
|
label = f"perm={perm} nib={nibble_order[:1]} sign={sign[:3]} init={init_h}"
|
|
best.append((score, label, decoded))
|
|
|
|
best.sort(key=lambda x: x[0])
|
|
print(f"\nTop 10 configurations:")
|
|
for s, lbl, dec in best[:10]:
|
|
print(f" score={s:>5} {lbl} T={dec[0][:8]} V={dec[1][:8]} L={dec[2][:8]}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|