feat(micromate): framing layer -- and three spec rules the bytes refuted

Step 1 of docs/micromate_client_spec.md: micromate/framing.py plus 31
offline tests.  Every rule was checked against the captures BEFORE being
written, which is the only reason this commit is not a bug.

Three things the spec asserted are wrong, all of which fail silently:

1. Requests are NOT plain Series III frames.  A Micromate escapes four
   byte values -- 0x02 0x03 0x04 0x10 -- where Series III escapes one.
   minimateplus.build_bw_frame reproduces 161 of Thor's 218 captured read
   frames; build_request() reproduces 218/218.  The 57 it missed include
   EVERY 0x5A download (offset 0x0400 puts a literal 0x04 in offset_hi)
   and the scheduler enable.  An unescaped 0x03/0x04 terminates the frame,
   so the unit never answers -- indistinguishable from a dead unit, and
   event download would have hit it on the first request ever sent.

2. The checksum is plain SUM8 of the destuffed payload, not the DLE-aware
   variant.  251/251 both directions.  The DLE-aware form is correct
   paired with Series III destuffing, which leaves an escaped byte as two
   bytes; after uniform destuffing it subtracts the correction twice and
   disagrees with the wire on 55 of 251 responses.

   scratch/mm_frame_parse.py shipped with exactly that pairing and looked
   clean only because it accepts either rule -- so it labelled those 55
   "SUM8" and never flagged one bad.  "Zero bad checksums" was true and
   carried no information.  A tool that tries N candidate rules cannot
   falsify any of them.  Fixed to validate against SUM8 alone.

3. SUB 0x5A is a 1024-byte chunk loop, not one request per event.  Thor's
   form, verified on all six bench events (4,076 -> 13,424 B): chunks =
   ceil(size/1024), offset = min(1024, size - 1024*i) as a byte count,
   params[2:4] = the byte offset, response data = offset + 11.
   sum(offsets) == size exactly, every time.

   This does not retract the earlier single-request observation -- that
   used offset_hi = 0x10, which in Series III is the bulk-stream marker,
   so it is plausibly a distinct streaming mode returning several frames.
   Those captures never landed in the repo, so it cannot be re-derived.
   Implement Thor's form; the other is worth one bench test.

Also: a 0x10 inside request params needs no special handling (settled --
Thor sends it, the wire doubles it), so the planned NotImplementedError
guard is gone.  declared_length -> probe_length, because it is only
meaningful in a probe reply and Thor never probes.

Synthesised test frames are marked and each says what it stands in for.
The flags=0x03 case is the only coverage of the Thor firmware line -- it
wants a real 11.0BD capture next time UM20147 is on a bench.

No writes.  Read-path framing only; nothing here can originate a command.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ru8Lg9HkkYvX9VWWo65SmL
This commit is contained in:
2026-09-27 05:24:24 -04:00
co-authored by Claude Opus 5
parent a7631a8179
commit 5fe99568a2
5 changed files with 1006 additions and 72 deletions
+114 -44
View File
@@ -57,32 +57,37 @@ branches until neither case is readable.
---
## `micromate/framing.py`
## `micromate/framing.py` — ✅ BUILT 2026-09-27
Implemented, with `tests/test_micromate_framing.py` (31 tests, all passing).
**Two things in this section as originally drafted were wrong**, and both were
caught by measuring against the captures before writing code rather than after.
They are left in place below, struck through, because both are the kind of
mistake that would be made again.
### Requests
Series IV accepts Series III request frames unmodified. The simplest correct
implementation re-exports the builder rather than duplicating it:
~~Series IV accepts Series III request frames unmodified. The simplest correct
implementation re-exports the builder rather than duplicating it:~~
```python
from minimateplus.framing import build_bw_frame # requests are identical
from minimateplus.framing import build_bw_frame # ✗ WRONG — 161/218
```
⚠ One open question, flagged in the protocol reference and **not** settled:
whether `0x10` bytes inside request *params* need stuffing. No probe we sent
carried one. Until it is settled, assert on it rather than guessing:
⚠ **`build_bw_frame` reproduces only 161 of Thor's 218 captured read frames.**
The payload layout is identical; the stuffing is not. A Micromate escapes
**four** byte values — `0x02`, `0x03`, `0x04`, `0x10` — where Series III
escapes one. The frames this breaks are **every `SUB 0x5A` download**
(`offset = 0x0400` → a literal `0x04` in `offset_hi`) and the scheduler enable.
An unescaped `0x03`/`0x04` terminates the frame early, so the unit just does not
answer.
```python
def build_request(sub: int, offset: int = 0, params: bytes = bytes(10)) -> bytes:
if 0x10 in params:
raise NotImplementedError(
"params containing 0x10 — stuffing rule unconfirmed; see "
"micromate_protocol_reference.md, 'Untested and unsafe-until-agreed'"
)
return build_bw_frame(sub, offset, params)
```
`build_request()` with the correct escape set reproduces **218/218**.
That turns an unknown into a loud failure instead of a corrupt frame.
✅ **Settled: `0x10` inside params needs no special handling.** The planned
`NotImplementedError` guard is unnecessary — Thor sends
`params = 00 00 10 00 …` on `SUB 0x5A` in five captured frames and the wire
carries an ordinary doubled `10 10`. One rule covers the whole payload.
### Responses — where Series III's parser cannot follow
@@ -104,11 +109,22 @@ validate.
```python
def checksum(payload: bytes) -> int:
return sum(b for b in payload if b != 0x10) & 0xFF
return sum(payload) & 0xFF # payload already de-stuffed
```
The DLE-aware variant, same as Series III's `5A` and write frames — not the plain
SUM8 of ordinary Series III reads.
~~The DLE-aware variant, same as Series III's `5A` and write frames.~~
⚠ **Plain SUM8, not the DLE-aware variant** — 251/251 both directions. The
DLE-aware form is the right answer paired with *Series III* de-stuffing, which
leaves an escaped byte in the payload as two bytes. De-stuffing `10 XX → XX`
already removes the `0x10`, so excluding it again subtracts the correction
twice, and the result disagrees with the wire on **55 of 251** captured
responses — every frame holding a literal `0x10`.
`scratch/mm_frame_parse.py` shipped with exactly that pairing. It looked clean
only because it accepts a frame matching *either* rule, so it labelled those 55
`SUM8` and never flagged one bad. "Zero bad checksums" was true and carried no
information. Fixed there too.
### ⚠ The SUB byte can be escaped
@@ -129,16 +145,27 @@ class MicromateFrame:
data: bytes # payload[5:], checksum stripped
checksum_valid: bool
@property
def request_sub(self) -> int: # 0xFF - sub
@property
def page_key(self) -> int: # uint16 BE at payload[3:5]
@property
def firmware_line(self) -> str: # "blastware" | "thor" | "unknown"
@property
def declared_length(self) -> int: # uint16 BE at data[3:5] (= payload[8:10])
def probe_length(self) -> int | None: # uint16 BE at data[3:5] (= payload[8:10])
```
⚠ **`declared_length` is a uint16 BE.** Read as a single byte it under-reads
⚠ **`probe_length` is a uint16 BE.** Read as a single byte it under-reads
`SUB 0x1A` by 47x — 44 against a true 2092. This is the single most expensive
mistake available in this protocol and it has already been made once.
Renamed from `declared_length`, because it is **only meaningful in the reply to
an `offset = 0` probe** — and Thor never probes. Across all 251 captured
responses the field reads 0 or a page count, never a length, precisely because
that session is single-step reads throughout. `page_key` is the field that
carries meaning there. The only genuine probe reply we hold is the POLL one
preserved in `scratch/fake_unit.py`.
`MicromateFrameParser` mirrors `S3FrameParser`: `feed(bytes) -> list[frame]`,
accumulates in `.frames`, `reset()`, and keeps the `bytes_fed` counter (it is
what distinguishes "no bytes at all" from "bytes but no complete frame" on a
@@ -183,21 +210,41 @@ unit yields a battery voltage of **577.92 V**.
⚠ **Test the monitoring flag for non-zero**, never against a constant. It has
read both `0x0E` and `0x0C` while monitoring.
### `0x5A` — simpler than Series III, deliberately
### `0x5A` — a bounded chunk loop, and much simpler than Series III
No arming ritual, no chunk loop, no `STRT` end-offset parsing, no `TERM` frame.
One request returns the whole event:
⚠ **Corrected 2026-09-27.** This section said "no chunk loop — one request
returns the whole event", with `offset_word = 0x1000 + 2 * ceil(size / 512)`.
That describes our own 2026-09-23 probes, which set `offset_hi = 0x10`. **Thor
chunks**, and Thor's form is the one verified from bytes on disk:
```python
offset_word = 0x1000 + 2 * ceil(size / 512) # size from the chain walk
n = ceil(size / 1024) # size from the chain walk
for i in range(n):
offset = min(1024, size - 1024 * i) # a BYTE COUNT
params = key4 + bytes(6) if i == 0 else bytes(2) + pack(">H", 1024*i) + bytes(6)
file_bytes += response.data[11:] # response data is exactly offset + 11
```
The payload **is** the `.IDFW` file, byte for byte — so it feeds
Verified on all six bench events (4,076 → 13,424 B): `sum(offsets) == size`
exactly, with the predicted chunk count and final offset every time.
Still no arming ritual for `0x5A` itself, no `STRT` end-offset parsing and no
`TERM` frame — the simplification the original claim celebrated is real, it just
is not single-shot. (`SUB 0x93` arms the *chain walk*, before `1E`/`1F`, not
the download.)
The concatenated payload **is** the `.IDFW` file, byte for byte — so it feeds
`micromate.idf_file.read_idf_file()` and `/db/import/idf_file` unchanged.
⚠ Do not port the Series III `5A` walk. Its address arithmetic caused a 5x
over-read and a `> 64 KB` page-boundary bug that is *still open* on the Series
III side. None of that applies here.
III side. None of that applies here: the chunk index is a byte offset into the
file, bounded by a size the device told us, and it cannot run past the event.
⚠ **`assert sum(len(chunk) - 11 for chunk in chunks) == size`.** A silently
short event is the failure mode this project has been bitten by repeatedly on
the Series III side, and here the check is free because the size is known up
front.
---
@@ -242,22 +289,34 @@ tests/test_micromate_framing.py
⚠ `bridges/captures/` and `tests/fixtures/` are both gitignored, so tests must
not depend on files being present. **Embed the frames as hex constants** — they
are 16–68 bytes each, and a handful covers every case:
are 19–138 bytes each. ✅ Done; what actually landed:
| case | why |
|---|---|
| POLL probe reply, 19 B | shortest valid frame |
| POLL data reply, 68 B | contains a literal `0x10` — only the DLE-aware checksum matches |
| `0x1A` response, 2108 B | exercises `declared_length` as a true uint16 (`0x082C`) |
| a Thor-line reply, `flags = 0x03` | `0x03` is ETX; proves destuffing before framing |
| a frame whose SUB is `0x02` | arrives as `10 02`; proves destuff-then-index |
| a truncated frame | parser must return nothing, not a bad frame |
| a corrupted checksum | `checksum_valid == False`, frame still returned |
| case | source | why |
|---|---|---|
| POLL probe reply, 19 B | captured (via `fake_unit.py`) | shortest valid frame; the only real probe reply we hold |
| `0x5A` chunk, 138 B | captured | holds literal `0x10` **and** literal `0x41` — the checksum case, and proves ACK is not escaped |
| `0x49` state reply, 25 B | captured | a literal `0x02`, escaped |
| `0x48` file reply, 24 B | captured | escaped `0x04` in `data[0]` — one byte late without destuffing |
| 8 Thor request frames | captured | byte-for-byte against `build_request()`, incl. both `0x5A` forms and the scheduler enable |
| `probe_length = 0x082C` | **synthesised** | no probe reply for `0x1A` exists on disk — the 9-24-26 session never probes |
| Thor-line reply, `flags = 0x03` | **synthesised** | no 11.0BD capture is in the repo; built by flipping one byte of the real POLL reply |
| escaped checksum byte | **synthesised** | the shortest real one is 1,070 B, too long to embed for one assertion |
| truncated / corrupt / split-across-feeds | derived | parser must return nothing, flag rather than swallow, and survive any split point |
Then a round-trip assertion: feed a whole captured session through the parser and
assert the frame count and every SUB, against `scratch/mm_frame_parse.py`'s
output — which is already known good, having parsed 24, 38 and 40-frame sessions
with zero bad checksums.
⚠ Synthesised frames are marked `SYNTH_`-style in the test and each says what it
stands in for and why no capture was available. Do not let that set grow
quietly: the `flags = 0x03` case in particular is the **only** coverage of half
the fleet, and it deserves a real 11.0BD capture the next time UM20147 is on a
bench.
Two corpus-backed tests run when the captures happen to be on the box and skip
cleanly otherwise: **251 response frames parse with zero bad checksums**, and
**`build_request()` reproduces 218/218 read frames**. The second is the test
that would have caught the escape-set error, so it is worth the skip marker.
⚠ Do **not** assert against `scratch/mm_frame_parse.py`'s output as the original
plan proposed. That script accepts either checksum rule and is wrong about
which one is right — using it as an oracle would have pinned the bug.
**Live, second:** against the bench unit on mint-mac via `mm_link.py`.
`connect()`, `list_setups()` (should return the 23 known names), `list_events()`,
@@ -268,7 +327,7 @@ then `download_event()` and assert the bytes decode and match a
## Order of work
1. `framing.py` + its tests — offline, verifiable immediately
1. ✅ `framing.py` + its tests — **done 2026-09-27**, 31 tests, offline
2. `protocol.py` — reads only, one method per row of the table above
3. `client.py` — `connect()`, `get_state()`, `list_setups()`
4. the event chain and `download_event()`
@@ -276,11 +335,22 @@ then `download_event()` and assert the bytes decode and match a
Steps 1–2 need no hardware at all.
**Worth carrying forward from step 1:** every rule got checked against the
captures *before* being written, and two of the three the spec asserted turned
out wrong — the escape set (26% of frames) and the checksum (22%). Both fail
silently. The captures are on disk and a re-stuff-and-compare loop takes about
two minutes per rule, so do that for `protocol.py`'s per-command offsets too
rather than trusting the table above.
---
## Open questions to settle while implementing
- **Request param stuffing** — raise `NotImplementedError` rather than guess.
- ~~**Request param stuffing**~~ — ✅ settled 2026-09-27; no special handling.
- **Is the single-request `0x5A` form real?** Our 2026-09-23 probes set
`offset_hi = 0x10` and appeared to get a whole 11 KB event back, where Thor
chunks at 1024 B. Plausibly a distinct streaming mode that returns several
frames. One bench test settles it; implement Thor's form regardless.
- **Is THOR's preamble required?** Try one command cold and find out; it is a
two-minute test with the bench unit and it removes a ritual if unnecessary.
- **`Event` model fit** — Series IV carries fields Series III lacks (setup file
+158 -18
View File
@@ -150,12 +150,38 @@ Every frame in this session was produced by `build_bw_frame(sub, offset)` with
no Series IV changes, and the device accepted all of them:
```
[ACK 0x41] [STX 0x02] [10 10] [flags 00] [SUB] [00] [00] [offset] [params×10] [chk] [ETX 0x03]
[ACK 0x41] [STX 0x02] [10 10] [flags 00] [SUB] [00] [offset_hi] [offset_lo] [params×10] [chk] [ETX 0x03]
```
⚠ Only the doubled `BW_CMD` (`10 10`) form has been exercised. Whether other
literal `0x10` bytes inside params require stuffing is **untested** — none of
the probes sent carried one.
The *payload layout* is identical to Series III. The *stuffing* is not.
> #### ⚠ Correction, 2026-09-27 — `build_bw_frame()` is NOT sufficient
>
> This section used to say requests were Series III frames "unmodified", and
> the client spec accordingly planned to re-export
> `minimateplus.framing.build_bw_frame`. Measured against Thor's own frames,
> that builder reproduces **161 of 218** captured read frames. It escapes only
> `0x10`; a Micromate escapes four bytes (see *The escape set* below).
>
> The 57 it gets wrong are not obscure:
>
> | frame | why it breaks |
> |---|---|
> | **every `SUB 0x5A` download** | `offset = 0x0400` puts a literal `0x04` in `offset_hi`, which must go out as `10 04` |
> | `SUB 0x47` scheduler enable | `params[7] = 0x03` → `10 03` |
>
> An unescaped `0x03` or `0x04` *is* a frame terminator, so the unit sees a
> short frame and does not answer — indistinguishable from a dead unit, and
> event download would have hit it on the first request ever sent.
>
> `micromate/framing.py:build_request()` reproduces **218/218**. Pinned per
> frame in `tests/test_micromate_framing.py`.
✅ **Settled 2026-09-27: a `0x10` inside params needs no special handling.**
Previously flagged untested. Thor sends `SUB 0x5A` with
`params = 00 00 10 00 …` in five captured frames and the wire carries the
ordinary doubled `10 10`. There is no Series-III-style partial-stuffing
carve-out here — one rule covers the whole payload including the checksum.
### Responses — Series III *minus the DLE prefix*
@@ -181,18 +207,75 @@ mismatch.
[5+] data
```
### Checksum — the DLE-aware variant
### Checksum — plain SUM8 of the de-stuffed payload
```python
chk = sum(b for b in payload if b != 0x10) & 0xFF
chk = sum(payload) & 0xFF # payload already de-stuffed
```
Confirmed on every frame captured. The `POLL` probe response contains no
`0x10` and so cannot distinguish plain SUM8 from the DLE-aware form; the
`POLL` **data** response contains a `0x10` at payload offset 42, and only the
DLE-aware rule matches there. This is the same checksum Series III uses for
its `5A` bulk-stream and write frames — not the plain SUM8 of ordinary
Series III reads.
**251/251 responses and 251/251 requests**, across every capture in
`bridges/captures/9-24-26 - micromate2/`.
> #### ⚠ Correction, 2026-09-27 — "the DLE-aware variant"
>
> This section previously specified
> `chk = sum(b for b in payload if b != 0x10) & 0xFF`, and that is wrong **as
> paired with the uniform de-stuffing rule below.**
>
> The earlier claim was not a misreading; it was a correct rule attached to the
> wrong convention. The DLE-aware form belongs with *Series III* de-stuffing,
> which leaves an escaped byte in the payload as **two** bytes — there,
> skipping the `0x10` is the necessary correction. De-stuffing `10 XX → XX`
> already removes it, so excluding `0x10` as well **subtracts the correction
> twice**.
>
> Cost of the pairing, measured: it disagrees with the wire on **55 of 251**
> response frames — every frame whose payload holds a literal `0x10`. A clean
> example, from the `0x5A` chunk at offset `0x0070` of
> `raw_s3_…_Download_events_then_delete_1_event.bin`: the wire says `0xC1`,
> plain SUM8 says `0xC1`, the DLE-aware form says `0x91`.
>
> **Why nobody noticed:** `scratch/mm_frame_parse.py` accepts a frame matching
> *either* rule and labels which one hit. It reported those 55 frames as
> `SUM8` and never flagged one bad, so "zero bad checksums across 24, 38 and
> 40-frame sessions" was true and told us nothing about which rule was right.
> A tool that tries every candidate cannot falsify any of them — if it is going
> to stay permissive, it has to *report the split*, not just the pass.
>
> Pinned by `tests/test_micromate_framing.py`.
### The escape set — exactly four bytes
A Micromate escapes `0x02`, `0x03`, `0x04` and `0x10`, each prefixed with a
`DLE`, **and nothing else** — in both directions.
Established by re-stuffing every captured frame and comparing to the wire:
| candidate escape set | responses reproduced | requests reproduced |
|---|---|---|
| `{0x10}` — the Series III rule | 130/251 | 177/251 |
| `{0x10, 0x03}` | 135/251 | 184/251 |
| `{0x10, 0x02, 0x03}` | 178/251 | 192/251 |
| **`{0x10, 0x02, 0x03, 0x04}`** | **251/251** | **251/251** |
| `{0x10, 0x02, 0x03, 0x04, 0x41}` | 196/251 | — |
Corroborated independently by the byte that follows a wire `DLE`: across all
251 responses it is only ever `0x02` (933×), `0x03` (510×), `0x04` (655×) or
`0x10` (1179×). Nothing else ever appears there.
The last row matters because ACK looks like it ought to be escaped and is not —
a literal `0x41` in the data goes out bare.
**The checksum byte is escaped too.** Three captured responses have a checksum
of `0x02`/`0x03`/`0x04` and all three arrive as `10 XX` immediately before the
terminating `ETX`. No captured *request* happened to land on one, so Thor's
behaviour there is unobserved — but a device and its host share one framing
routine, and the alternative is a frame the far end truncates, so escape it.
This supersedes nothing: the de-stuffing rule `10 XX → XX` stays exactly as
documented. Knowing only four values are ever escaped is what makes that
uniform rule *exact* rather than merely convenient — and it is what an
**encoder** needs, which the write path will.
### The probe response carries the data length
@@ -573,8 +656,63 @@ Series III ignores a `5A` probe unless preceded by
answers a **bare `5A` request** with nothing before it. That whole ritual is
gone.
### ⚠ Correction, 2026-09-27 — THOR uses a 1024-byte chunk loop
The section below ("One request returns the entire event; there is no chunk
loop") describes **our own probes**, and it is not how THOR downloads an event.
Read from the wire, THOR's sequence per event is:
```
0x93 (arm) → 0x1E / 0x1F → key + size
0x0C → the 210-byte record
0x5A × n → n = ceil(size / 1024)
```
and the `0x5A` frames are a plain bounded chunk walk:
| | |
|---|---|
| chunk `i` offset | `min(1024, size − 1024·i)` — a **byte count** |
| chunk 0 params | `[key4][6 × 0x00]` — the key means "from the start" |
| chunk `i>0` params | `[00 00][uint16 BE of 1024·i][6 × 0x00]` |
| response data | exactly `offset + 11` bytes; the file bytes are `data[11:]` |
| response `page_key` | `offset // 256` — a page count, not an address |
**Verified on all six bench events**, sizes 4,076 → 13,424 bytes:
`sum(offsets) == size` **exactly** in every case, with the predicted chunk
count and predicted final offset. No `STRT` parsing, no `TERM` frame, no
over-read — that part of the original claim holds, and it is still far simpler
than the Series III walk.
```
key size(1E) chunks sum(offsets) last offset
055d4a81 4076 4 4076 0x03ec
055d4a82 11032 11 11032 0x0318
055d4a83 11502 12 11502 0x00ee
055d4a84 13424 14 13424 0x0070
055d4a85 8746 9 8746 0x022a
055d4a86 6092 6 6092 0x03cc
```
**These are probably two different modes, not a contradiction.** THOR's
`offset_hi` is the chunk length (`0x04`, `0x03`, `0x00` …). Our single-request
probes set `offset_hi = 0x10` — which in Series III is precisely the
bulk-stream marker `build_5a_frame()` writes raw. So `0x10XX` plausibly means
"stream until done" and returns **several** frames, which the parser of the day
concatenated into the 11,049 bytes recorded below. That reconciles both
observations, but it is a hypothesis: those 2026-09-23 captures never landed in
the repo, so it cannot be re-derived from bytes on disk.
**Implement THOR's chunked form.** It is verified byte-exact across six events
and five distinct sizes, and it is what the firmware runs every day. The
single-request form is worth one bench test as an optimisation — `offset_hi =
0x10` and count the frames — but not worth depending on first.
### The offset word is a LENGTH, not a position
⚠ Superseded as the implementation path by the correction above; the *reading*
of the field is right and is what makes THOR's chunk walk make sense.
This is the key divergence. Series III walks chunks by absolute flash
address, stepping `0x0200` per request. On the Micromate the offset word
requests *how much to send*:
@@ -591,10 +729,11 @@ offset_word = 0x1000 + 2 × pages pages = ceil(event_size / 512)
| `0x102C` | 22 | **11,033 — the whole event** |
`event_size` comes from the chain walk (the 4 bytes after the key in
`1E`/`1F`). **One request returns the entire event**; there is no chunk loop,
no `STRT` end-offset parsing, and no `TERM` frame. Over-requesting is safe —
`0x1030` (24 pages) returned exactly the same bytes as `0x102C`, so the device
caps at the real size.
`1E`/`1F`). **One request returns the entire event** ⚠ *— true of our
`offset_hi = 0x10` probes; see the 2026-09-27 correction above. THOR chunks* —
there is no `STRT` end-offset parsing and no `TERM` frame in either form.
Over-requesting is safe — `0x1030` (24 pages) returned exactly the same bytes
as `0x102C`, so the device caps at the real size.
Params are the Series III *probe* form: `[0x00][key4][6 × 0x00]`.
@@ -636,10 +775,11 @@ per-sample-exact applies.
### What a full read now looks like
```
0x93 → arm (THOR sends this before every 1E/1F)
1E → first key + size
0C(key) → project/client/operator, timestamp, peaks
5A(key, 0x1000+2×ceil(size/512)) → the whole .IDFW
1F → next key + size (until null sentinel)
5A × ceil(size/1024) → the .IDFW, 1024 bytes at a time
0x93 → 1F → next key + size (until null sentinel)
```
## Setups are FILES, not a config block