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).
100 lines
3.2 KiB
Python
100 lines
3.2 KiB
Python
"""
|
|
Decoder v1: nibble-pair signed deltas in 10 NN blocks, 4-channel round-robin.
|
|
"""
|
|
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 walk_blocks(body, start):
|
|
i = start
|
|
blocks = []
|
|
while i + 1 < len(body):
|
|
t0, t1 = body[i], body[i + 1]
|
|
if t0 == 0x10 and t1 % 4 == 0 and 0 < t1 <= 0xFC:
|
|
length = t1 // 2 + 2
|
|
data = bytes(body[i + 2 : i + length])
|
|
blocks.append(("10", t1, data))
|
|
i += length
|
|
elif t0 == 0x20 and t1 % 4 == 0 and 0 < t1 <= 0xFC:
|
|
length = t1 + 2
|
|
data = bytes(body[i + 2 : i + length])
|
|
blocks.append(("20", t1, data))
|
|
i += length
|
|
elif t0 == 0x00 and t1 % 4 == 0:
|
|
blocks.append(("00", t1, b""))
|
|
i += 2
|
|
elif t0 == 0x30 and t1 % 4 == 0 and 0 < t1 <= 0x10:
|
|
length = t1 * 4
|
|
data = bytes(body[i + 2 : i + length])
|
|
blocks.append(("30", t1, data))
|
|
i += length
|
|
elif t0 == 0x40 and t1 == 0x02:
|
|
length = 20
|
|
data = bytes(body[i + 2 : i + length])
|
|
blocks.append(("40", t1, data))
|
|
i += length
|
|
else:
|
|
blocks.append(("??", t0, bytes(body[i:i+8])))
|
|
break
|
|
return blocks
|
|
|
|
|
|
def decode_v1(body, start, n_samples):
|
|
"""Decode by accumulating nibble-pair deltas from all 10 NN blocks."""
|
|
blocks = walk_blocks(body, start)
|
|
# 4 channels: T, V, L, M
|
|
cur = [0, 0, 0, 0]
|
|
out = [[], [], [], []]
|
|
sample_index = 0 # how many sample-sets emitted
|
|
|
|
for typ, NN, data in blocks:
|
|
if typ == "10":
|
|
# 2 nibbles per byte, round-robin TVLM
|
|
for byte in data:
|
|
for nib in ((byte >> 4) & 0xF, byte & 0xF):
|
|
ch = sample_index % 4
|
|
cur[ch] += s4(nib)
|
|
out[ch].append(cur[ch])
|
|
sample_index = (sample_index + 1) // 4 * 4 + (sample_index + 1) % 4 # ?
|
|
sample_index += 1
|
|
# We emit per-nibble, but the structure is unclear
|
|
elif typ == "20":
|
|
# int8 absolute or delta?
|
|
for byte in data:
|
|
v = byte if byte < 128 else byte - 256
|
|
ch = sample_index % 4
|
|
cur[ch] = v # treat as absolute
|
|
out[ch].append(cur[ch])
|
|
sample_index += 1
|
|
return out
|
|
|
|
|
|
def main():
|
|
b = load_bundle("event-c")
|
|
body = b.body
|
|
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"]]
|
|
|
|
# Find start
|
|
for s in range(15):
|
|
if body[s] == 0x10 and body[s+1] % 4 == 0 and 0 < body[s+1] <= 0xFC:
|
|
start = s
|
|
break
|
|
|
|
blocks = walk_blocks(body, start)
|
|
# Print block-by-block what's in each
|
|
print(f"Total blocks: {len(blocks)}")
|
|
bytes_processed = 0
|
|
for typ, NN, data in blocks[:30]:
|
|
print(f" type={typ} NN=0x{NN:02x} data_len={len(data)} data_hex={data[:32].hex(' ')}{'...' if len(data) > 32 else ''}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|