codec-re: 00 NN is RLE; full Tran segment-0 decode (4 of 5 events)
User uploaded a Vert-heavy event (JQ0) and a Mic-heavy event (V70). Those two were exactly what was needed to crack the next piece: - 00 NN block = run-length-encoded zero deltas in the current channel. Append NN copies of the current cumulative value (no change). - find_data_start now recognizes 00 NN as a valid first tag (some events begin with a leading 00 NN RLE block). - decode_tran_initial now decodes the FULL segment 0 (not just the first data block). Results across 5 fixture events: - M529LL1A.SP0 (loud-all-channels) : 510 / 510 ✓ - M529LL1L.JQ0 (Vert-heavy) : 510 / 510 ✓ - M529LL1L.V70 (Mic-heavy) : 510 / 510 ✓ - M529LL1A.SV0 (loud-from-start) : 58 / 58 ✓ - M529LL1A.SS0 (loud-from-start) : 42 / 502 (stops at first 30 04) The 30 04 block (only seen in loud-from-start events) hasn't been decoded yet — likely a channel-switch marker for the high-amplitude regime. Also discovered: segment header (40 02) payload bytes [0:2] = T_delta at first sample of new segment, [6:8] = byte length to next segment. Multi-segment Tran decoding still diverges after sample 512 because the per-segment channel ordering after the header is unknown. Tests: 40 pass (up from 36). Files: - minimateplus/waveform_codec.py: find_data_start fix, RLE handling, full segment-0 decode in decode_tran_initial - tests/test_waveform_codec.py: synthetic RLE test, full segment 0 tests for JQ0 and V70 - tests/fixtures/5-11-26/: M529LL1L.JQ0, M529LL1L.V70 + TXT exports - docs/instantel_protocol_reference.md §7.6.1: RLE + segment-header docs
This commit is contained in:
@@ -137,13 +137,20 @@ class WaveformBlock:
|
||||
|
||||
|
||||
def find_data_start(body: bytes) -> int:
|
||||
"""Auto-detect the offset of the first data block (``10 NN`` or ``20 NN``).
|
||||
"""Auto-detect the offset of the first data block.
|
||||
|
||||
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.
|
||||
The body starts with a 7-byte preamble (magic ``00 02 00`` + two int16 BE
|
||||
Tran anchors). After that, the data section starts with a tag — usually
|
||||
``10 NN`` or ``20 NN``, but quiet events may begin with a ``00 NN`` RLE
|
||||
marker. We return the offset of the first recognized tag.
|
||||
"""
|
||||
# Try fixed offset 7 first (canonical preamble length).
|
||||
if len(body) >= 9:
|
||||
b, nn = body[7], body[8]
|
||||
if (b in (0x00, 0x10, 0x20, 0x30) and nn % 4 == 0 and 0 < nn <= 0xFC) \
|
||||
or (b == 0x40 and nn == 0x02):
|
||||
return 7
|
||||
# Fall back to scanning the first 20 bytes.
|
||||
for i in range(min(20, len(body) - 1)):
|
||||
b = body[i]
|
||||
nn = body[i + 1]
|
||||
@@ -258,61 +265,70 @@ def _i8(b: int) -> int:
|
||||
|
||||
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).
|
||||
Decode the initial Tran-channel samples — VERIFIED 2026-05-11.
|
||||
|
||||
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.
|
||||
Returns Tran samples 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).
|
||||
The decoded list extends from sample 0 through the end of segment 0
|
||||
(= just before the first ``40 02`` segment header; ~510 sample-sets
|
||||
for the events tested). Multi-segment decoding requires continuing
|
||||
past the segment header — that's done by :func:`decode_tran_full`
|
||||
when the per-segment rules are pinned down for all signal types.
|
||||
|
||||
Codec details (CONFIRMED 2026-05-11):
|
||||
Codec for segment 0 (CONFIRMED 2026-05-11 against 7 fixture events):
|
||||
|
||||
- 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:
|
||||
- Data blocks (``10 NN`` or ``20 NN``) carry 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.
|
||||
* ``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).
|
||||
|
||||
- ``00 NN`` blocks are run-length-encoded zero deltas: append NN
|
||||
copies of the current cumulative Tran value (no change).
|
||||
|
||||
- ``30 NN`` blocks have not yet been decoded for content — they
|
||||
appear in segment 0 of loud-from-start events (SS0, SV0) and
|
||||
seem to signal a transition or special-case interpretation.
|
||||
The walker steps over them but their data is ignored.
|
||||
|
||||
The walk stops at the first ``40 02`` segment header.
|
||||
"""
|
||||
if len(body) < 9:
|
||||
return None
|
||||
if body[0:3] != b"\x00\x02\x00":
|
||||
if len(body) < 7 or 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)
|
||||
for blk in walk_body(body, start):
|
||||
if blk.tag_hi == 0x40:
|
||||
# Segment boundary — stop. Multi-segment decode is decode_tran_full.
|
||||
break
|
||||
if blk.tag_hi == 0x10:
|
||||
for byte in blk.data:
|
||||
for nib in ((byte >> 4) & 0xF, byte & 0xF):
|
||||
cur += _s4(nib)
|
||||
out.append(cur)
|
||||
elif blk.tag_hi == 0x20:
|
||||
for byte in blk.data:
|
||||
cur += _i8(byte)
|
||||
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
|
||||
elif blk.tag_hi == 0x00:
|
||||
# RLE zero deltas: append NN copies of current Tran value.
|
||||
for _ in range(blk.tag_lo):
|
||||
out.append(cur)
|
||||
# 30 NN: unknown content; skip.
|
||||
return out
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user