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).
52 lines
1.7 KiB
Python
52 lines
1.7 KiB
Python
"""Walk chunks; auto-detect preamble length by finding first 10 NN."""
|
|
import sys
|
|
sys.path.insert(0, ".")
|
|
from analysis.load_bundle import load_bundle
|
|
|
|
|
|
def walk_chunks(body, start, max_NN=0x80):
|
|
chunks = []
|
|
i = start
|
|
while i + 1 < len(body):
|
|
if body[i] != 0x10:
|
|
break
|
|
NN = body[i + 1]
|
|
if NN == 0 or NN > max_NN or NN % 4 != 0:
|
|
break
|
|
chunk_len = NN // 2 + 4
|
|
if i + chunk_len > len(body):
|
|
break
|
|
data = bytes(body[i + 2 : i + 2 + NN // 2])
|
|
trailer = bytes(body[i + 2 + NN // 2 : i + chunk_len])
|
|
chunks.append((i, NN, data, trailer))
|
|
i += chunk_len
|
|
return chunks, i
|
|
|
|
|
|
def find_first_chunk_start(body):
|
|
"""Locate first byte that begins a `10 NN` chunk (NN ∈ multiples of 4, 4..0x7C)."""
|
|
for i in range(20):
|
|
if body[i] == 0x10 and body[i + 1] % 4 == 0 and 0 < body[i + 1] <= 0x7C:
|
|
return i
|
|
return -1
|
|
|
|
|
|
def main():
|
|
for name in ("event-c", "event-d", "event-a", "event-b"):
|
|
b = load_bundle(name)
|
|
body = b.body
|
|
start = find_first_chunk_start(body)
|
|
chunks, end = walk_chunks(body, start)
|
|
print(f"\n=== {name} === body={len(body)} N_samples={len(b.samples['Tran'])} start={start}")
|
|
print(f" chunks parsed: {len(chunks)}, walk ended at {end}")
|
|
if chunks:
|
|
print(f" first 5 chunks: {[(c[0], c[1], c[3].hex()) for c in chunks[:5]]}")
|
|
print(f" last 5 chunks: {[(c[0], c[1], c[3].hex()) for c in chunks[-5:]]}")
|
|
print(f" bytes around end of walk: {body[end-4:end+12].hex(' ')}")
|
|
else:
|
|
print(f" bytes at start: {body[start:start+16].hex(' ')}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|