docs(series4): the setup-write path, observed end to end
Thor pushed a setup named TEST1.mmb to UM12947 while seismo_lab's TCP bridge
recorded both directions. We still have not originated a write frame -- the
wire format is now known, our encoder is not written.
Topology worth reusing: socat shares /dev/ttyACM0 on TCP from mint-mac,
seismo_lab relays Thor to it. Thor is pointed at 127.0.0.1 as if the unit were
a field modem. No modem, no SIM, production Thor box untouched.
The sequence is Series III's, plus one command:
Thor: 5B | 41 | 08 | 2E | 1A | DA | 68->73 | 82->83 | 71->72
unit: A4 | BE | F7 | D1 | E5 | 25 | 97 8C | 7D 7C | 8E 8D
All 12 device responses checksum-validate and every write is acked. Every
write response SUB matches the Series III table exactly.
New:
* SUB 0xDA names the target .MMB file -- 256 bytes, filename null-padded,
nothing else. This is why no generic file-transfer command exists: Thor
names the file, then writes the ordinary config block into it.
* SUB 0x41 reads the active setup's filename; SUB 0x2E reads trigger config.
* Reads are single-step -- Thor asks offset=0xFFFF and skips the probe.
* 0x71 writes the whole 2090-byte block in ONE frame, not Series III's three
chunks. 0x69/0x74 are absent.
Write-frame destuffing is `10 XX` -> `XX` uniformly, including `10 03`. Chosen
by checksum, not assumption: of four candidate rules, only this one makes all
four data-carrying write frames validate. 0x71's data holds 4 literal 0x03
bytes escaped as `10 03`, so escaping is mandatory for any writer.
The write body IS the read body -- 0x71 and the 0xE5 response align at a fixed
11-byte shift with 1902/2090 bytes equal (91.0%). Setups are read-modify-write.
The 12 differing regions are fully mapped: setup name, four 64-byte
[label:22][value:42] note entries, sensor location, and the three geo trigger
levels (0.3 -> 0.5 in/s) on a 48-byte channel stride.
Independent confirmation of the geo LSB: each channel block carries float32BE
3.10308 at label+24. 3.10308/10000 = 0.000310308 = _GEO_LSB_IPS to 8 figures,
and 10.0/3.10308*10000 = 32226.046 = the 32226.05 full scale. That value was
derived statistically from 991,415 rounding constraints in v0.30.0; the unit
reports it directly. It is exactly half Series III's 6.206053, so the ADC runs
10,000 counts per volt. Do NOT retune _GEO_LSB_IPS -- this corroborates it.
The `offset` field is NOT a single length formula: two frames are len, two are
len+2, and Series III's data[1]+2 reproduces neither. Recorded as observed
constants the device accepted; pinning the rule needs a capture with
differently-sized payloads. This doc has been wrong once by inferring a length
field -- not inferring this one.
Also adds scratch/mm_frame_parse.py, because S3FrameParser cannot see Micromate
responses at all (it scans for DLE+STX; Micromate responses start at a bare
STX). That is why the first pass at this capture looked like 12 unanswered
requests. 24/24 frames parse with 0 bad checksums.
Stale claims corrected: the "write half is not yet attempted" note, the
"empty unit" limitation (5 events since 2026-09-23), and the unsafe-until-agreed
list, which now distinguishes observed from exercised.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ru8Lg9HkkYvX9VWWo65SmL
This commit is contained in:
@@ -428,6 +428,20 @@ memory 15,000,000 total and free (no events stored).
|
||||
Blastware, `0x03` Thor — where Series III has a constant `0x10`.
|
||||
5. **Modem serial rate is 115200**, not 38400 (per TMI provisioning practice;
|
||||
not independently verified here).
|
||||
6. **Reads are single-step** — Thor asks for `offset = 0xFFFF` and gets the
|
||||
whole block; Series III always probes first. (2026-09-24)
|
||||
7. **Setup writes are preceded by `SUB 0xDA`**, which names the target `.MMB`
|
||||
file. Series III has no equivalent — it has one config, not named files.
|
||||
8. **The compliance block is written in ONE `0x71` frame**, not Series III's
|
||||
three chunks.
|
||||
9. **`0x69` / `0x74` (waveform data write) are absent** from a setup push.
|
||||
10. **Six channel blocks** (`Tran`/`Vert`/`Long`/`Mic`/`LMic`/`SMic`) on a
|
||||
48-byte stride, against Series III's four.
|
||||
11. **The notes block is four fixed-width `[label:22][value:42]` entries** on a
|
||||
64-byte stride, with different labels — not Series III's `label: value`
|
||||
scan targets.
|
||||
12. **The geophone scale factor is `3.10308`**, exactly half Series III's
|
||||
`6.206053`, with an ADC of 10,000 counts per volt.
|
||||
|
||||
---
|
||||
|
||||
@@ -649,8 +663,192 @@ Note `LMic` / `SMic` — linear and sound-level microphone variants that
|
||||
Series III does not have.
|
||||
|
||||
This is the **read half of setup management**, and it means a setup can be
|
||||
round-tripped: read the active config, modify, write it back. The write half
|
||||
is not yet attempted.
|
||||
round-tripped: read the active config, modify, write it back. ✅ **The write
|
||||
half was observed on 2026-09-24** — see *The write path* below, which confirms
|
||||
the round-trip: the written block is the read block, 91% byte-identical.
|
||||
|
||||
## The write path — observed end to end (2026-09-24)
|
||||
|
||||
⚠ **We have still never sent a write command to a unit.** Thor did every write
|
||||
below; seismo_lab's TCP bridge sat between Thor and the unit and recorded both
|
||||
directions. Capture: `bridges/captures/be12599-diag/9-24-26 - micromate2/`
|
||||
(`raw_bw_*` = Thor, `raw_s3_*` = unit), UM12947 on the USB **PC** port, relayed
|
||||
to TCP by `socat` on mint-mac. Operation: push a setup named `TEST1.mmb`.
|
||||
|
||||
**All 12 device responses checksum-validate and every write is acked.**
|
||||
|
||||
### The sequence
|
||||
|
||||
```
|
||||
Thor: 5B │ 41 │ 08 │ 2E │ 1A │ DA │ 68 → 73 │ 82 → 83 │ 71 → 72
|
||||
unit: A4 │ BE │ F7 │ D1 │ E5 │ 25 │ 97 8C │ 7D 7C │ 8E 8D
|
||||
└──────── reads ────────┘ └──────────── writes ────────────┘
|
||||
```
|
||||
|
||||
Series III, for comparison: `68→73 │ 71×3→72 │ 82→83 │ 69→74→72`.
|
||||
|
||||
Same write SUBs, and **every response SUB matches the Series III table
|
||||
exactly** (`68`→`97`, `73`→`8C`, `82`→`7D`, `83`→`7C`, `71`→`8E`, `72`→`8D`).
|
||||
Three differences:
|
||||
|
||||
- **`SUB 0xDA` is new** and comes first — see below.
|
||||
- **`0x71` is a single write, not three chunks.** Series III splits the
|
||||
compliance block into 1027 + 1055 + remainder; the Micromate takes all
|
||||
2,090 bytes in one frame.
|
||||
- **`0x69` / `0x74` (waveform data write) do not appear at all.**
|
||||
|
||||
Every write ack is a 16-byte zero-data frame, same shape as Series III's.
|
||||
|
||||
### Three new read commands
|
||||
|
||||
| SUB | rsp | payload | what it carries |
|
||||
|---|---|---|---|
|
||||
| `0x41` | `0xBE` | 271 B | **the active setup's file name**, e.g. `TEST1.mmb` |
|
||||
| `0x2E` | `0xD1` | 44 B | trigger-config block; mirrors what `0x82` writes |
|
||||
| `0xDA` | `0x25` | 272 B (write) | **declares the target setup file** — see below |
|
||||
|
||||
### `SUB 0xDA` — name the setup file you are about to write
|
||||
|
||||
Data is exactly **256 bytes: the file name, null-padded, nothing else.**
|
||||
|
||||
```
|
||||
54 45 53 54 31 2e 6d 6d 62 00 00 … "TEST1.mmb" + 247 × 0x00
|
||||
```
|
||||
|
||||
This is the missing link in *setups are files*: there is no generic
|
||||
file-transfer command because there does not need to be one. Thor names the
|
||||
target file, then writes the ordinary config block into it. The unit acks with
|
||||
`0x25` before any config bytes are sent.
|
||||
|
||||
### Reads are single-step — no probe
|
||||
|
||||
Series III sends every read twice (probe at `offset=0x00` to learn the length,
|
||||
then a data step). Thor **skips the probe** and asks for
|
||||
**`offset = 0xFFFF`**, getting the whole block in one response:
|
||||
|
||||
```
|
||||
41 02 10 10 00 1a 00 ff ff 00 … SUB 0x1A, offset 0xFFFF
|
||||
→ 2,103-byte response
|
||||
```
|
||||
|
||||
`POLL` is the exception — it uses `offset = 0x0030` (48), its data length.
|
||||
|
||||
This does not contradict the probe response documented above; the probe still
|
||||
works and still reports its length at `payload[8:10]`. Thor simply does not
|
||||
need it.
|
||||
|
||||
### Write-frame destuffing — `10 XX` → `XX`, uniformly
|
||||
|
||||
Only the leading `BW_CMD` is doubled (`10 10`); after that **every `10 XX` pair
|
||||
on the wire destuffs to `XX`**, including `10 03`.
|
||||
|
||||
This was settled by checksum, not by assumption. Four candidate rules were
|
||||
tested against all four data-carrying write frames; **only this one makes all
|
||||
four checksums validate**:
|
||||
|
||||
| rule | `0xDA` | `0x68` | `0x82` | `0x71` |
|
||||
|---|---|---|---|---|
|
||||
| **`10 XX` → `XX`** | ok | **ok** | ok | **ok** |
|
||||
| only `10 03` → `03` | ok | BAD | ok | BAD |
|
||||
| `10 10`→`10`, `10 03`→`03` | ok | BAD | ok | BAD |
|
||||
| nothing collapses | ok | BAD | ok | BAD |
|
||||
|
||||
`0x71`'s data contains **4 literal `0x03` bytes**, escaped as `10 03` on the
|
||||
wire. A writer that does not escape `0x03` will emit a frame the device
|
||||
terminates early — this is the same defensive ETX escaping Blastware does, and
|
||||
it is mandatory, not optional.
|
||||
|
||||
Checksum is plain SUM8 of the destuffed payload; the DLE-aware form gives the
|
||||
same answer once destuffing is correct, so it does not discriminate.
|
||||
|
||||
### The `offset` field is a per-command constant
|
||||
|
||||
| SUB | data bytes | `offset` | note |
|
||||
|---|---|---|---|
|
||||
| `0xDA` | 256 | `0x0100` = 256 | = the name-field size |
|
||||
| `0x68` | 88 | `0x005A` = 90 | **identical to Series III's documented value** |
|
||||
| `0x82` | 28 | `0x001C` = 28 | **identical to Series III's documented value** |
|
||||
| `0x71` | 2090 | `0x082C` = 2092 | = the length `SUB 0x1A` reports |
|
||||
|
||||
⚠ **There is no single length formula** — two are `len`, two are `len + 2`, and
|
||||
Series III's `data[1] + 2` rule does not reproduce either. Treat these as
|
||||
observed constants the device accepted. Pinning the actual rule needs a second
|
||||
capture whose payloads differ in size. (This document has already been wrong
|
||||
once by inferring a length field; do not infer this one.)
|
||||
|
||||
### The write body **is** the read body
|
||||
|
||||
The `0x71` write payload and the `0xE5` read response align at a fixed
|
||||
**11-byte shift** with **1902/2090 bytes equal (91.0%)** — the remaining 9% is
|
||||
exactly what was edited.
|
||||
|
||||
**So a setup is read-modify-write**, the same shape as Series III, and a write
|
||||
client does not need to synthesise a config block from scratch.
|
||||
|
||||
### Field map, from the read/write diff
|
||||
|
||||
Twelve differing regions, all accounted for. Offsets are into the `0x71`
|
||||
**data** section (= the read response's data + 11).
|
||||
|
||||
| offset | size | field | this capture |
|
||||
|---|---|---|---|
|
||||
| `0x0000` | 3 | block length echo | `08 2a` (=2090) → zeroed on write |
|
||||
| `0x0009` | 1 | unidentified | `3c` → `02` |
|
||||
| `0x0013` | 1 | unidentified | `00` → `40` |
|
||||
| `0x002A` | 44 | **setup file name** | → `TEST1.mmb` |
|
||||
| `0x0090` | 42 | note 1 value — `Location` | → `Test Location 1 - 1234 electric boogaloo` |
|
||||
| `0x00D0` | 42 | note 2 value — `Client` | → `TMI` |
|
||||
| `0x0110` | 42 | note 3 value — `Company` | → `ServersDownLabs` |
|
||||
| `0x0150` | 42 | note 4 value — `General Notes` | → `Hopefully this works!` |
|
||||
| `0x0428` | 16 | sensor location | → `Test Config Send` |
|
||||
| `0x06CA` | 4 | **Tran trigger level** float32 BE | `0.3` → `0.5` in/s |
|
||||
| `0x06FA` | 4 | **Vert trigger level** | `0.3` → `0.5` in/s |
|
||||
| `0x072A` | 4 | **Long trigger level** | `0.3` → `0.5` in/s |
|
||||
|
||||
**Notes block: four entries on a 64-byte stride**, each `[label: 22][value: 42]`,
|
||||
labels at `0x008A + 64n`. The labels are `Location`, `Client`, `Company`,
|
||||
`General Notes` — **not** Series III's `Project:` / `Client:` / `User Name:` /
|
||||
`Seis Loc:`, and they are fixed-width fields, not `label: value` pairs.
|
||||
|
||||
**Channel blocks: six, 48 bytes each**, from `0x06BC` — `Tran`, `Vert`, `Long`,
|
||||
`Mic`, `LMic`, `SMic`. Trigger level sits at **label + 30**.
|
||||
|
||||
### The geophone scale factor is in the config block — and it confirms our LSB
|
||||
|
||||
Each geo channel block carries a float32 BE at **label + 24**:
|
||||
|
||||
```
|
||||
40 46 98 dd = 3.10308003…
|
||||
```
|
||||
|
||||
Which closes a loop from the file-decode work:
|
||||
|
||||
```
|
||||
3.10308003… / 10000 = 0.000310308 = _GEO_LSB_IPS, to 8 figures
|
||||
10.0 / 3.10308003… × 10000 = 32226.046 = the 32226.05 full scale
|
||||
```
|
||||
|
||||
`_GEO_LSB_IPS` was derived statistically — intersecting 991,415 rounding
|
||||
constraints from Thor's own CSV exports (see `idf_protocol_reference.md`).
|
||||
**The unit reports the constant directly**, and it agrees. That upgrades the
|
||||
value from a fit to a reading, and explains the odd full-scale count: the
|
||||
Micromate's ADC is **10,000 counts per volt**, and 3.10308 in/s per volt is
|
||||
**exactly half** Series III's documented `6.206053` (ratio 1.99997).
|
||||
|
||||
Do **not** retune `_GEO_LSB_IPS` — this is corroboration, not a correction.
|
||||
|
||||
### Still unknown on the write path
|
||||
|
||||
- **What `0x68` and `0x82` actually contain.** Both were written with
|
||||
near-zero payloads here and nothing in them changed, so no field is located.
|
||||
Series III maps backlight/power-save/LCD-cycle into `0x68`; unverified here.
|
||||
- **The three header bytes** at data `0x0000`/`0x0009`/`0x0013`.
|
||||
- **Whether a *new* setup file can be created**, or only an existing one
|
||||
overwritten. `0xDA` named a file that did not previously exist and the unit
|
||||
accepted it — but we did not confirm on the unit's screen that `TEST1` now
|
||||
exists as a selectable setup. Worth checking on the device.
|
||||
- **Scheduler / call-home writes.** `callhome.MMB` is a file too, so it may go
|
||||
through the same `0xDA` + block-write shape with a different target name.
|
||||
|
||||
## Static analysis of the firmware (2026-09-23, solo session)
|
||||
|
||||
@@ -789,10 +987,17 @@ its maximum should equal the loudest event in it.
|
||||
|
||||
## ⚠ Untested and unsafe-until-agreed
|
||||
|
||||
Nothing below has been sent to a unit, and nothing should be without an
|
||||
explicit decision:
|
||||
**Nothing below has been sent to a unit by us, and nothing should be without an
|
||||
explicit decision.**
|
||||
|
||||
- **Writes** (`0x68`–`0x83`), **call-home write** (`0x7E`/`0x7F`)
|
||||
Note the distinction introduced on 2026-09-24: the setup-write sequence
|
||||
(`0xDA`, `0x68`/`0x73`, `0x82`/`0x83`, `0x71`/`0x72`) has now been **observed**,
|
||||
because Thor performed it while we recorded. Observed is not the same as
|
||||
exercised — **we have still never originated a write frame.** The wire format
|
||||
is known; our encoder is unwritten and unproven.
|
||||
|
||||
- **Writes** (`0x68`–`0x83`) — format now known, never sent by us
|
||||
- **Call-home write** (`0x7E` / `0x7F`) — not observed at all
|
||||
- **Erase** (`0xA3` / `0xA2`)
|
||||
- **Start / stop monitoring** (`0x96` / `0x97`)
|
||||
- `0x1F` (advance event pointer) — non-destructive on Series III but it does
|
||||
@@ -800,7 +1005,8 @@ explicit decision:
|
||||
|
||||
Also unknown:
|
||||
|
||||
- Whether `0x10` bytes inside request params need stuffing
|
||||
- Whether `0x10` bytes inside **request params** need stuffing. (Write-frame
|
||||
**data** stuffing *is* now settled — `10 XX` → `XX`; see *The write path*.)
|
||||
- Everything about the **call-home session** — the device-initiated direction
|
||||
has not been observed at all. Specifically: how a unit announces itself,
|
||||
and **how it learns an event was accepted so it stops re-sending it.**
|
||||
@@ -812,14 +1018,25 @@ Also unknown:
|
||||
## Session provenance
|
||||
|
||||
Unit **UM12947**, firmware **`11.0CB`** (Blastware line), on the bench via
|
||||
USB, **zero events stored** (memory free == total). Read commands only.
|
||||
Every response in this document was checksum-validated.
|
||||
USB. Every response in this document was checksum-validated.
|
||||
|
||||
Three sittings, all on the same unit:
|
||||
|
||||
| date | state | what was exercised |
|
||||
|---|---|---|
|
||||
| 2026-09-23 | zero events stored | read commands, direct from Python |
|
||||
| 2026-09-23 | 5 events (4 waveform, 1 histogram) | the event chain + `0x5A` |
|
||||
| 2026-09-24 | 5 events | **Thor pushing a setup**, via a recording relay |
|
||||
|
||||
The 2026-09-24 sitting used a different topology worth noting, because it is
|
||||
reusable: `socat` on mint-mac shares `/dev/ttyACM0` on TCP, seismo_lab's TCP
|
||||
bridge relays Thor to it and records both directions. Thor is configured with
|
||||
the unit at `127.0.0.1:<bridge port>` exactly as if it were a field modem. No
|
||||
modem, no SIM, and **nothing on the production Thor box is touched.**
|
||||
|
||||
⚠ Firmware is the single biggest caveat on this document. Every finding here
|
||||
is from one unit on the Blastware build. A `11.0BD` unit has not been
|
||||
touched.
|
||||
|
||||
An empty unit is a real limitation: `0x08`, `0x1E`, `0x0A` and `0x06` all have
|
||||
event-dependent payloads that could not be exercised. Recording a couple of
|
||||
events on the bench unit would unlock the entire event-walk half of the
|
||||
protocol.
|
||||
✅ The original "empty unit" limitation is gone — `0x08`, `0x1E`, `0x0A` and
|
||||
`0x06` were all exercised against 5 stored events on 2026-09-23.
|
||||
|
||||
Reference in New Issue
Block a user