diff --git a/CLAUDE.md b/CLAUDE.md index 32dd6d1..6101185 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -118,9 +118,11 @@ S3→BW (response): Both differences confirmed by reproducing Blastware's exact wire bytes from the 1-2-26 BW TX capture. All 10 frames verified. -### SUB 5A — chunk counter is monotonic (CORRECTED 2026-04-06) +### SUB 5A — chunk counter formula (FINAL CORRECTION 2026-04-24) -**Chunk counters are `chunk_num * 0x0400` for ALL chunks including chunk 1.** +**Chunk counter = `key4[2:4] + (chunk_num - 1) * 0x0400` for ALL chunks.** + +where `key4[2:4] = (key4[2] << 8) | key4[3]` is the event's circular-buffer base offset. The 4-2-26 BW TX capture showed `counter=0x1004` for chunk 1 of event key `01110000`, which led to `_CHUNK1_COUNTER = 0x1004` being hardcoded as a special case. This was a Blastware @@ -130,9 +132,14 @@ immediately and streams all frames correctly. The 4-3-26 capture confirms the pattern for a second event (key `0111245a`): chunk 1 = `0x245A`, chunk 2 = `0x285A`, chunk 3 = `0x2C5A` (each +0x0400). Blastware's -true formula is `key4[2:4] + n * 0x0400` — but since `key4[2:4]` of the first event is -`0x0000`, `n * 0x0400` produces the right result. The device does not strictly validate the -counter and streams data for any valid 5A request; using `chunk_num * 0x0400` is correct. +true formula is `key4[2:4] + (chunk_num - 1) * 0x0400`. + +**2026-04-24 CORRECTION — `n * 0x0400` is WRONG for non-first events.** For event key +`01110000`, `key4[2:4] == 0x0000` so the old `chunk_num * 0x0400` formula was accidentally +correct. For keys with `key4[2:4] != 0` (e.g. key `01111884`, offset `0x1884`), the old +formula sends counters pointing into the wrong buffer region — the device returns data from +a completely different stored event and `b"Project:"` never appears in the stream. +Use `key4[2:4] + (chunk_num - 1) * 0x0400` exclusively. ### SUB 5A — params are 11 bytes for chunk frames, 10 for termination diff --git a/docs/instantel_protocol_reference.md b/docs/instantel_protocol_reference.md index cd0812e..327ed13 100644 --- a/docs/instantel_protocol_reference.md +++ b/docs/instantel_protocol_reference.md @@ -1231,21 +1231,23 @@ Two critical differences from `build_bw_frame`: | Frame | offset_word | counter | params | Purpose | |---|---|---|---|---| | Probe | `0x1004` | `0x0000` | 10 bytes (`bulk_waveform_params(0)`) | Initiate transfer | -| Chunk 1 | `0x1004` | `0x0400` | 11 bytes | First data chunk | -| Chunk 2 | `0x1004` | `0x0800` | 11 bytes | Second chunk | -| Chunk N | `0x1004` | `N * 0x0400` | 11 bytes | Nth chunk | +| Chunk 1 | `0x1004` | `key4[2:4]` | 11 bytes | First data chunk | +| Chunk 2 | `0x1004` | `key4[2:4] + 0x0400` | 11 bytes | Second chunk | +| Chunk N | `0x1004` | `key4[2:4] + (N-1) * 0x0400` | 11 bytes | Nth chunk | | … | … | … | … | … | -| Termination | `0x005A` | `last + 0x0400` | 10 bytes | End transfer | +| Termination | `0x005A` | `key4[2:4] + N * 0x0400` | 10 bytes | End transfer | -> ⚠️ **2026-04-06 CORRECTED — chunk counter is monotonic for ALL chunks.** -> The 4-2-26 BW TX capture showed counter=0x1004 for chunk 1, which was hardcoded as a -> special case. This was a Blastware artifact. Empirically confirmed: counter=0x0400 for -> chunk 1 works correctly; counter=0x1004 causes the device to time out. The device does -> NOT strictly validate the counter value — it streams data for any valid 5A request for -> the given key. Use `chunk_num * 0x0400` (monotonic) for all chunks. -> BW's true internal formula is `key4[2:4] + n * 0x0400`. For event 1 (key `01110000`) -> this equals `n * 0x0400` since `key4[2:4] = 0x0000`. The monotonic formula is correct -> for all keys encountered on this device. +> ⚠️ **2026-04-06 CORRECTED — chunk counter is `key4[2:4] + (N-1) * 0x0400`.** +> The 4-2-26 BW TX capture showed counter=0x1004 for chunk 1 of key `01110000`, leading to +> an interim "monotonic n * 0x0400" formula. This was accidentally correct because +> `key4[2:4] == 0x0000` for that event. +> +> **2026-04-24 FINAL CORRECTION:** The counter is an absolute circular-buffer address. +> BW's true formula is `key4[2:4] + (chunk_num - 1) * 0x0400` where `key4[2:4]` is the +> event's storage base offset (`(key4[2]<<8) | key4[3]`). For keys where +> `key4[2:4] != 0x0000` (e.g. key `01111884`), using `n * 0x0400` sends requests into the +> wrong buffer region — the device returns data from a completely different event and +> `b"Project:"` never appears in the stream. Confirmed correct 2026-04-24. The `stop_after_metadata=True` flag causes the loop to stop as soon as `b"Project:"` is found in the accumulated A5 frame data, typically after 4–9 chunks. A termination frame diff --git a/minimateplus/blastware_file.py b/minimateplus/blastware_file.py index 53128e2..dabcce9 100644 --- a/minimateplus/blastware_file.py +++ b/minimateplus/blastware_file.py @@ -527,7 +527,7 @@ def classify_frame(frame: S3Frame) -> str: Returns one of: "terminator" — page_key == 0x0000 - "probe_or_strt" — data contains b"STRT" (the initial probe response) + "probe_or_strt" — data contains b"STRT\xff\xfe" (the initial probe response) "metadata" — data contains ASCII compliance-config markers "waveform" — predominantly binary (< 20 % printable ASCII) "unknown" — none of the above criteria matched @@ -539,7 +539,7 @@ def classify_frame(frame: S3Frame) -> str: if frame.page_key == 0x0000: return "terminator" data = bytes(frame.data) - if b"STRT" in data: + if b"STRT\xff\xfe" in data: return "probe_or_strt" if any(m in data for m in _METADATA_FRAME_MARKERS): return "metadata" @@ -677,6 +677,7 @@ def write_blastware_file( term_frame = None all_bytes = bytearray() + seen_metadata = False for fi, frame in enumerate(body_frames): ftype = classify_frame(frame) @@ -686,11 +687,20 @@ def write_blastware_file( # Probe frame: always process regardless of classification. # It holds the STRT record; probe_skip positions us past it. skip = probe_skip - elif ftype == "waveform": + elif seen_metadata: + # Drop all frames that come after the compliance/metadata block. + # (e.g. extra waveform chunks fetched after stop_after_metadata) + log.debug( + "write_blastware_file: frame %d after metadata — skipping", fi + ) + continue + elif ftype in ("waveform", "metadata"): skip = 13 if fi == 1 else 12 + if ftype == "metadata": + seen_metadata = True else: - # Skip metadata, probe_or_strt, and unknown frames. - if b"STRT" in bytes(frame.data): + # Skip probe_or_strt and unknown frames. + if b"STRT\xff\xfe" in bytes(frame.data): log.warning( "write_blastware_file: frame %d (%s) contains STRT — skipping", fi, ftype, diff --git a/minimateplus/protocol.py b/minimateplus/protocol.py index 5a2537f..f599170 100644 --- a/minimateplus/protocol.py +++ b/minimateplus/protocol.py @@ -126,10 +126,12 @@ DATA_LENGTHS: dict[int, int] = { _BULK_CHUNK_OFFSET = 0x1004 # offset field for probe + all regular chunk requests ✅ _BULK_TERM_OFFSET = 0x005A # offset field for termination request ✅ _BULK_COUNTER_STEP = 0x0400 # chunk counter increment per chunk ✅ -# Chunk counter formula: chunk_num * 0x0400 for ALL chunks including chunk 1. -# Earlier captures showed 0x1004 for chunk 1 — that was a Blastware artifact, not a -# protocol requirement. Confirmed 2026-04-06: 0x0400 for chunk 1 works; 0x1004 -# causes a 120-second device timeout. Formula n * 0x0400 is used for all chunks. +# Chunk counter formula: key4[2:4] + (chunk_num - 1) * 0x0400 +# where key4[2:4] is the event's circular-buffer base offset ((key4[2]<<8)|key4[3]). +# Earlier captures showed 0x1004 for chunk 1 of key 01110000 — that was a Blastware +# artifact. For keys where key4[2:4] != 0x0000 (e.g. key 01111884) the old +# "n * 0x0400" formula sends counters from the wrong buffer region and the device +# returns data from a different event. Confirmed correct 2026-04-24. # Default timeout values (seconds). # MiniMate Plus is a slow device — keep these generous.