In Thor, a schedule entry that starts monitoring forces you to attach a setup,
and sending the schedule pushes that setup too, overwriting anything with the
same name. The protocol requires none of it.
Evidence from the scheduler capture:
* The two writes are separate operations, not one transaction. The config
write ends at frame 18, Thor sends a fresh POLL preamble, and only then opens
the schedule at frame 20. Different commands, different paths:
config 0xDA -> 0x68/0x73 -> 0x82/0x83 -> 0x71/0x72
schedule 0x8D -> 0x8E
* The schedule stores a length-prefixed NAME, not a config blob. It is a
reference, and a reference does not require rewriting its referent.
* The config Thor pushed was already on the unit unchanged -- its 2,090-byte
0x71 payload is byte-identical to the previous capture's, zero differences.
Thor spent a whole block write re-sending a setup the device already had.
So SFM can, with today's protocol: enumerate setups with 0x3F/0x40, write ONLY
the schedule when the referenced setup already exists, and push a config only
when it is genuinely missing or deliberately edited. Common case drops from
524 + 2090 bytes to 524, and the write disappears entirely.
It also removes a real hazard. Because the reference is by name, and a same-name
write overwrites silently with an indistinguishable ack, Thor's pattern means
scheduling something can quietly rewrite a setup that other schedules or the
operator's own work depend on. Validating the reference instead of rewriting the
referent avoids the class of problem.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ru8Lg9HkkYvX9VWWo65SmL
1449 lines
64 KiB
Markdown
1449 lines
64 KiB
Markdown
# Micromate Protocol Reference — Thor / Micromate Series IV, live wire protocol
|
||
|
||
Sibling to [instantel_protocol_reference.md](instantel_protocol_reference.md)
|
||
(Series III, "the Rosetta Stone") and
|
||
[idf_protocol_reference.md](idf_protocol_reference.md) (Series IV *file*
|
||
format). This document covers the Series IV **live device protocol** — what
|
||
the unit says over the wire, as opposed to what it writes into a `.IDFW`.
|
||
|
||
**Status (2026-09-23): opening session.** Everything below was established in
|
||
a single bench session against one unit. Treat it as a strong start, not a
|
||
settled spec — in particular, everything here comes from **one unit, over USB,
|
||
with no events stored**.
|
||
|
||
---
|
||
|
||
## The headline
|
||
|
||
**A Micromate running the *Blastware* firmware answers Series III command
|
||
frames.**
|
||
|
||
An unmodified Series III `POLL` (`SUB 0x5B`), built by
|
||
`minimateplus.framing.build_bw_frame` with no changes at all, produced a
|
||
complete two-step probe/data cycle. Ten Series III read commands were then
|
||
tried and **all ten answered**, every one obeying the Series III response-SUB
|
||
rule.
|
||
|
||
⚠ **That qualifier is load-bearing, and it was discovered after the fact.**
|
||
Instantel ships the Micromate in two firmware lines:
|
||
|
||
| firmware | Instantel's own description |
|
||
|---|---|
|
||
| `11.0CB` | "Utilize with **Blastware**" |
|
||
| `11.0BD` | "Utilize with **THOR, Vision, Vision II**" |
|
||
|
||
The bench unit reports **`11.0CB`** — the Blastware build. So the clean
|
||
Series III behaviour above is very likely *because the unit is in Blastware
|
||
mode*, not because the Micromate natively speaks Series III. **Nothing here
|
||
should be assumed to hold on a `11.0BD` unit until tested.**
|
||
|
||
This reframes the project. The question is no longer only "what is the
|
||
Series IV protocol" but **"which firmware line do we target, and does one of
|
||
them let the existing Series III stack drive the whole fleet?"**
|
||
|
||
---
|
||
|
||
## Firmware — the variable nobody knew was a variable
|
||
|
||
This explains a production problem TMI has lived with: two ACH servers, two
|
||
machines, and units that will not cross over. It is not a misconfiguration.
|
||
**Instantel ships different firmware for different host software**, and the
|
||
wire protocol differs with it.
|
||
|
||
### Fleet audit (physical, 2026-09-22/23)
|
||
|
||
| unit | firmware | line | location |
|
||
|---|---|---|---|
|
||
| UM11719 | `11.0CB` | Blastware | Ped Bridge Loc 2 |
|
||
| UM6047 | `11.0CB` | Blastware | Brookville Loc 9 |
|
||
| UM12947 | `11.0CB` | Blastware | **bench** |
|
||
| UM14133 | `11.0CB` | Blastware | Pitt-Music Bldg Loc 1 |
|
||
| UM11402 | `11.0BD` | Thor | Ped Bridge Loc 1 |
|
||
| UM20147 | `11.0BD` | Thor | **bench** |
|
||
| UM13981 | `11.0AK` | pre-split | RKM Loc 1 |
|
||
| UM20146 | `11.0AK` | pre-split | Karns Loc 2 |
|
||
| UM12420 | `10.90GC` | pre-split | RKM Loc 2 |
|
||
|
||
**4 Blastware / 2 Thor / 3 pre-split.** The Blastware line is already the
|
||
plurality, which makes "standardise on Blastware" less disruptive than it
|
||
first appeared.
|
||
|
||
A store-derived audit (firmware is recorded in every `.sfm.json` sidecar as
|
||
`extensions.idf_report.version`) agreed with the physical audit on **7 of 9**.
|
||
The two that differed — UM6047 and UM14133 — are the most recently deployed
|
||
units, reflashed after their last stored event. Useful technique: the fleet's
|
||
firmware history is reconstructable from the store without touching a unit,
|
||
but it lags reality by one deployment.
|
||
|
||
Firmware is **not stable per-unit over time** — five of nine have been
|
||
reflashed at least once. Any fleet-wide claim needs a fresh audit.
|
||
|
||
### ⚠ Retraction: firmware line does NOT determine Thor compatibility
|
||
|
||
An earlier draft of this document suggested that UM12947's trouble with Thor
|
||
was explained by its being on the Blastware build. **That is not supported.**
|
||
|
||
Ped Bridge runs UM11402 (`11.0BD`) and UM11719 (`11.0CB`) side by side, both
|
||
deployed 2026-04-20, and **both call Thor successfully** — UM11719 has 331
|
||
Thor-collected events in the store while on `11.0CB`, through 2026-08-23.
|
||
|
||
So a Blastware-line unit does feed Thor. Whatever the CB/BD split changes, it
|
||
is not "which host software can collect from it", and UM12947's specific
|
||
problem remains unexplained.
|
||
|
||
**What is actually established:** a `11.0CB` unit answers Series III command
|
||
frames. Whether a `11.0BD` unit does is **untested** — and UM20147 (`11.0BD`)
|
||
is on the bench, which makes that a direct A/B away.
|
||
|
||
The version is readable over the wire from `SUB 0x01` as two separate ASCII
|
||
runs — `"0CB"` then `"11"` — with no single concatenated string.
|
||
|
||
### The strategic fork
|
||
|
||
- **Option A — standardise the fleet on the Blastware line.** Every unit,
|
||
MiniMate and Micromate alike, then speaks Series III, and the existing
|
||
`minimateplus/` stack drives all of it. One protocol, one call-home
|
||
receiver. Dramatically cheaper *if* it holds up.
|
||
- **Option B — reverse-engineer the Thor line (`11.0BD`) and support both.**
|
||
|
||
Option A is the shortcut, but it is unproven and carries real unknowns, all
|
||
of which are cheap to answer on the bench and expensive to discover later:
|
||
|
||
1. **What file format does a `11.0CB` unit produce?** If it emits Blastware
|
||
binaries rather than `.IDFW`/`.IDFH`, the (now exact) Series III decoder
|
||
applies and the IDF codec becomes a legacy path. Not a loss — we have
|
||
both — but it changes what the ingest pipeline sees.
|
||
2. **Does Thor still work with a `11.0CB` unit?** If not, flashing a
|
||
production unit breaks data collection until the replacement path exists.
|
||
3. **Are any Micromate-specific capabilities lost** on the Blastware line?
|
||
4. **Is the flash reversible in the field**, and what does it cost in downtime?
|
||
|
||
## How this was obtained
|
||
|
||
No Thor, no modem, no Windows machine. The Micromate exposes its protocol on
|
||
a **USB CDC-ACM virtual serial port**:
|
||
|
||
```
|
||
ID 2504:0300 Instantel Inc. MICROMATE COM PORT
|
||
driver: cdc_acm ATTRS{serial}=="V1.00"
|
||
→ /dev/ttyACM0
|
||
```
|
||
|
||
Plain CDC ACM, so no vendor driver and no proprietary USB layer — any host
|
||
that can open a serial port can talk to the unit.
|
||
|
||
**Baud is irrelevant over USB.** Identical byte-for-byte responses at 38400
|
||
and 115200; CDC-ACM ignores the line rate. TMI provisions Micromate *modem*
|
||
links at **115200** (Series III uses 38400) — that matters for the cellular
|
||
path, not for USB.
|
||
|
||
**The device never speaks first.** 20 s of passive listening on an idle open
|
||
port produced zero bytes. It is strictly request/response.
|
||
|
||
---
|
||
|
||
## Physical + framing layer
|
||
|
||
### Requests — Series III format, unmodified
|
||
|
||
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]
|
||
```
|
||
|
||
⚠ 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.
|
||
|
||
### Responses — Series III *minus the DLE prefix*
|
||
|
||
```
|
||
Series III: [DLE 0x10] [STX 0x02] … [chk] [ETX 0x03]
|
||
Micromate: [STX 0x02] … [chk] [ETX 0x03] ← no leading DLE
|
||
```
|
||
|
||
This single byte matters operationally: Blastware's parser locates frames by
|
||
scanning for `DLE+STX`, so it will **never find a frame boundary** in Micromate
|
||
traffic no matter what else is correct. That is a structural reason a
|
||
Micromate cannot call into a Blastware ACH server, independent of any baud
|
||
mismatch.
|
||
|
||
### Response payload header
|
||
|
||
```
|
||
[0] CMD 0x00 same as Series III
|
||
[1] flags 0xC5 ← Series III uses 0x10. Constant across all 10 SUBs.
|
||
[2] SUB 0xFF − request_SUB
|
||
[3] PAGE_HI
|
||
[4] PAGE_LO
|
||
[5+] data
|
||
```
|
||
|
||
### Checksum — the DLE-aware variant
|
||
|
||
```python
|
||
chk = sum(b for b in payload if b != 0x10) & 0xFF
|
||
```
|
||
|
||
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.
|
||
|
||
### The probe response carries the data length
|
||
|
||
Series III hardcodes `DATA_LENGTHS` per SUB. On the Micromate the **probe
|
||
response tells you**, as a **uint16 BE at `payload[8:10]`**:
|
||
|
||
⚠ **Corrected 2026-09-23.** An earlier draft read this as a single byte at
|
||
`payload[9]`. That is right only while the high byte is zero, and it is
|
||
catastrophically wrong for `SUB 0x1A`, whose real length is `0x082C` = **2092**
|
||
— read as a byte it gives **44**, a 47× under-read. Always read the pair.
|
||
|
||
| SUB | command | `payload[9]` | Series III constant |
|
||
|---|---|---|---|
|
||
| `0x15` | serial number | `0x0A` | `0x0A` ✓ |
|
||
| `0x01` | device info | `0x98` | `0x98` ✓ |
|
||
| `0x1C` | monitor status | `0x2C` | `0x2C` ✓ |
|
||
| `0x06` | storage range | `0x24` | `0x24` ✓ |
|
||
| `0x2C` | call-home config | `0x7E` | `0x7C` ✗ **differs by 2** |
|
||
| `0x08` | event index | `0x5A` | — |
|
||
| `0x1E` | event header | `0x08` | — |
|
||
| `0x1A` | compliance config | `0x082C` (2092) | — |
|
||
| `0x0A` | waveform header | `0x00` | — (no event context) |
|
||
| `0xFE` | full config | `0x00` | — (see note) |
|
||
|
||
Four of four known Series III lengths match exactly. **Read the length from
|
||
the probe rather than hardcoding it** — it is free, and it already caught the
|
||
call-home divergence.
|
||
|
||
---
|
||
|
||
## Confirmed commands (read-only)
|
||
|
||
All ten below answered with a correct `0xFF − SUB` response. Nothing that
|
||
writes, erases, or changes monitoring state has been sent to a unit.
|
||
|
||
| SUB | RSP | Command | Data proven |
|
||
|---|---|---|---|
|
||
| `0x5B` | `0xA4` | POLL / handshake | yes — ID block |
|
||
| `0x15` | `0xEA` | Serial number | yes — `UM12947` |
|
||
| `0x01` | `0xFE` | Device info | yes — 152 B |
|
||
| `0x2C` | `0xD3` | Call-home config | yes — 126 B |
|
||
| `0x1C` | `0xE3` | Monitor status | yes — 44 B |
|
||
| `0x06` | `0xF9` | Event storage range | yes — 36 B |
|
||
| `0x08` | `0xF7` | Event index | probe only |
|
||
| `0x1E` | `0xE1` | Event header / first key | probe only |
|
||
| `0x0A` | `0xF5` | Waveform header | probe only |
|
||
| `0x1A` | `0xE5` | Compliance config | probe only |
|
||
|
||
### Decoded so far
|
||
|
||
**`SUB 0x15` — serial.** ASCII, null-terminated: `UM12947`.
|
||
|
||
**`SUB 0x5B` / `0x01` — identification strings.**
|
||
`Instantel\0` and `MM/ISEE/S/IO` (MicroMate / ISEE standard). `0x01` also
|
||
carries eight consecutive `3f 80 00 00` float32 values (= `1.0f`) — almost
|
||
certainly per-channel calibration/scale factors, by analogy with Series III's
|
||
`geo_hardware_constant`. **Unverified.**
|
||
|
||
**`SUB 0x1C` — monitor status. Series III field offsets apply unchanged:**
|
||
|
||
| field | offset | read |
|
||
|---|---|---|
|
||
| battery × 100 | `payload[-10:-8]` uint16 BE | `0x017D` → **3.81 V** |
|
||
| memory total | `payload[-8:-4]` uint32 BE | 15,000,000 |
|
||
| memory free | `payload[-4:]` uint32 BE | 15,000,000 (empty) |
|
||
| date | `payload[18:22]` | day 23, month 9, year `0x07EA` = 2026 |
|
||
|
||
The battery reading independently corroborates: Thor's own event reports for
|
||
these units print `BatteryLevel : 3.8 volts`.
|
||
|
||
**`SUB 0x2C` — call-home config.** Contains the ASCII string `RADIO RING`.
|
||
Worth flagging: that is the *exact* string seen in the RV50 `ALEOS_SERIAL`
|
||
debug during the BE12599 incident —
|
||
`'ATQ1^MATE0^MATS0=2^M^MRADIO RING^M'`. So this block holds the modem dial /
|
||
answer strings, and it is the most directly relevant command to the
|
||
call-home-receiver goal. Field layout **not yet mapped**; Series III's map
|
||
(`raw[5]` enabled, `raw[6:46]` dial string) is a starting hypothesis only, and
|
||
the length already differs (`0x7E` vs `0x7C`).
|
||
|
||
**`SUB 0x06` — storage range.** All zeros on this unit, consistent with
|
||
`memory free == memory total`. Series III reads first/last event keys from
|
||
the final 8 bytes; untestable until the unit holds events.
|
||
|
||
---
|
||
|
||
## The event chain — walked end to end (2026-09-23, 5 events)
|
||
|
||
With 5 events on the bench unit (4 waveform + 1 histogram), the Series III
|
||
browse walk works unmodified:
|
||
|
||
```
|
||
1E (all-zero params) -> first key + size
|
||
0A (key) -> partial record, histogram only
|
||
0C (key) -> 210-byte waveform record
|
||
1F (all-zero params/browse) -> next key + size
|
||
... repeat ...
|
||
1F -> all-zero key = NULL SENTINEL, chain ends
|
||
```
|
||
|
||
The sentinel terminated correctly after exactly 5 events.
|
||
|
||
### Event keys are sequential, not addresses
|
||
|
||
```
|
||
055d4a81 055d4a82 055d4a83 055d4a84 055d4a85
|
||
```
|
||
|
||
**This is a real divergence.** Series III keys are flash-buffer *addresses*
|
||
(`01110000`, `011121F2`, …) that advance by the event's byte length, which is
|
||
why its 5A chunk walk is address-arithmetic. Micromate keys are a plain
|
||
incrementing counter. Any port of the Series III download walk must not
|
||
assume key arithmetic means anything.
|
||
|
||
### The 4 bytes after the key are the event's size
|
||
|
||
`1E`/`1F` return `[key 4B][size 4B]`. Series III uses that slot as an offset
|
||
to the next key; here it is a byte count:
|
||
|
||
| key | size | kind |
|
||
|---|---|---|
|
||
| `055d4a81` | 4,076 | histogram |
|
||
| `055d4a82` | 11,032 | waveform |
|
||
| `055d4a83` | 11,502 | waveform |
|
||
| `055d4a84` | 13,424 | waveform |
|
||
| `055d4a85` | 8,746 | waveform |
|
||
|
||
Consistent with real file sizes (corpus `.IDFH` ≈ 3.7–25 KB, `.IDFW` ≈
|
||
8.6–15.8 KB), and the histogram is unmistakably the small one. ⚠ Inferred,
|
||
not proven: the sizes sum to 48,780 while monitor status reports 57,344 bytes
|
||
used, so ~8.5 KB of overhead is unaccounted for.
|
||
|
||
### `SUB 0x0C` — waveform record, and it carries the job metadata
|
||
|
||
**Length `0xD2` = 210 bytes — identical to Series III.** Contents confirmed
|
||
across all 5 events:
|
||
|
||
- the event key, echoed
|
||
- date + time (`17 09 07 ea` → 23 Sep 2026, then `10 21` → 16:33 — matching
|
||
the actual bench recording time)
|
||
- title note `"Location"`
|
||
- **the project string** — `"Univ of Pitt-1st Yr Housing-Loc1 Ruskin"`
|
||
- serial `"UM12947"`
|
||
- channel labels `Tran` / `Vert` / `Long` / `Mic` — the same labels Series III
|
||
uses, and the same label-relative float32 layout
|
||
- per-event float32 peaks: 3.5152, 1.3720, 2.3542, 3.5152, 0.4227 in/s
|
||
across the five events (varied deliberately during recording)
|
||
|
||
**This closes the biggest open question for the call-home-receiver goal.**
|
||
The job identity strings (`project` / `client` / `operator` / `setup`) that
|
||
today arrive only via Thor's `.txt` sidecar — and which no amount of sample
|
||
decoding can reconstruct — are **available over the wire from `0x0C`**. A
|
||
direct-to-SFM event need not arrive with blank metadata.
|
||
|
||
### `SUB 0x0A` — partial record, histogram only
|
||
|
||
`0x0A` returned `len = 0x1E` (30 B) for the histogram and `len = 0x00` for all
|
||
four waveforms. The histogram payload carries **two timestamps** and the
|
||
ASCII string `"\r Vert: 0.300 in/s"` — structurally the Series III
|
||
**monitor-log partial record** (`0x2C` type), which likewise holds a start/stop
|
||
pair and a `"Geo: <float> in/s"` trigger string.
|
||
|
||
So on the Micromate the division of labour is: `0x0A` describes interval-style
|
||
records, `0x0C` describes triggered events. Series III uses `0x0A`'s
|
||
*response length* (`0x46` vs `0x2C`) to tell real events from boundaries;
|
||
that discriminator does not apply here.
|
||
|
||
### DLE stuffing in responses — confirmed present
|
||
|
||
Earlier marked untested. The `0x0C` timestamp field contains `10 10`, which
|
||
destuffs to a single `0x10` and yields a sensible clock reading. **Responses
|
||
are DLE-stuffed**, so a parser must destuff before applying field offsets.
|
||
|
||
---
|
||
|
||
## A/B: Blastware build vs Thor build (2026-09-23)
|
||
|
||
UM12947 (`11.0CB`) and UM20147 (`11.0BD`) were each put on the bench and given
|
||
the identical read-only sweep. **Both answer Series III command frames.**
|
||
|
||
| | UM12947 `11.0CB` | UM20147 `11.0BD` |
|
||
|---|---|---|
|
||
| POLL answers | ✓ | ✓ |
|
||
| All 10 read SUBs answer | ✓ | ✓ |
|
||
| `response_SUB = 0xFF − req` | ✓ | ✓ |
|
||
| DLE-aware checksum valid | ✓ | ✓ |
|
||
| Two-step probe/data read | ✓ | ✓ |
|
||
| ID string | `MM/ISEE/S/IO` | `MM/ISEE/S` |
|
||
| **flags byte** | **`0xC5`** | **`0x03`** |
|
||
| **`0x1C` length** | **`0x2C`** | **`0x30`** |
|
||
|
||
**The firmware line does not change the wire protocol.** One protocol stack
|
||
can drive the whole fleet regardless of which build a unit is on. This is the
|
||
single most consequential finding so far: "standardise the fleet on one
|
||
firmware" becomes an *optional* convenience rather than a prerequisite for
|
||
building a call-home receiver.
|
||
|
||
### The two differences that do exist
|
||
|
||
**1. The flags byte identifies the build.** Response `payload[1]` is `0xC5`
|
||
on the Blastware line and `0x03` on the Thor line, constant across all ten
|
||
SUBs on both units. That makes firmware line detectable from *any* response,
|
||
without reading device info. ⚠ Two units, one each — treat as a strong
|
||
hypothesis, not a proven encoding.
|
||
|
||
Note `0x03` is ETX, so on Thor-line units it arrives DLE-escaped as `10 03`.
|
||
A parser that fails to destuff will mis-locate every field by one byte on
|
||
exactly half your fleet.
|
||
|
||
**2. `SUB 0x1C` (monitor status) is 4 bytes longer on the Thor line** —
|
||
`0x30` vs `0x2C` — with four extra trailing bytes (`0f a0 00 00`, purpose
|
||
unknown).
|
||
|
||
⚠ **This breaks relative-to-end parsing.** Series III reads battery and
|
||
memory from the *end* of the `0x1C` block (`[-10:-8]`, `[-8:-4]`, `[-4:]`).
|
||
Those offsets are correct on `11.0CB` and wrong on `11.0BD` — applying them
|
||
blindly to UM20147 yields a battery reading of **577.92 V**. Parse forward
|
||
from the declared length instead of backward from the end.
|
||
|
||
With the offsets shifted by 4, UM20147 reads correctly: battery **3.81 V**,
|
||
memory 15,000,000 total and free (no events stored).
|
||
|
||
## Divergences from Series III (running list)
|
||
|
||
1. **No `DLE` prefix on responses** — bare `STX`.
|
||
2. **Response flags byte is `0xC5`**, not `0x10`.
|
||
3. **Call-home config is 126 bytes**, not 124.
|
||
4. **Data lengths are discoverable** from the probe response at `payload[9]`.
|
||
4b. **Event keys are a sequential counter**, not flash addresses.
|
||
4c. **`1E`/`1F` return the event size**, where Series III returns an offset.
|
||
4d. **`0x0A` vs `0x0C` split by record type**, not by the `0x46`/`0x2C`
|
||
length discriminator Series III uses.
|
||
4e. **Response `payload[1]` (flags) encodes the firmware line** — `0xC5`
|
||
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.
|
||
13. **The monitoring flag is `0x0E`**, where Series III uses `0x10` — and it
|
||
sits at `SUB 0x1C` `data[12]`. A second indicator lives at `SUB 0x49`
|
||
`data[11]` (`0x02` monitoring), which Series III has no equivalent of.
|
||
14. **Setup files are enumerated** with a `0x3F`/`0x40` first/next walk.
|
||
Series III has one config and nothing to enumerate.
|
||
15. **`SUB 0x1C` carries the device clock** (day/month/year/h/m/s at
|
||
`data[13:21]`). Nothing else read so far reports the unit's own time.
|
||
16. **There is a generic file transfer addressed by full path** — `0x94`/`0x48`
|
||
read, `0x8D`/`0x8E` write. Series III has nothing comparable; its config is
|
||
reachable only through dedicated commands.
|
||
17. **The scheduler is a separate file**, `\system\schedule\schedule.dat`,
|
||
and a schedule entry names a **setup file** to load.
|
||
|
||
---
|
||
|
||
## The firmware images are unencrypted — and they document themselves
|
||
|
||
`ref-stuff/micromate-firmware/MICROMATE(CB).BIN` and `MICROMATE(BD).BIN`,
|
||
~2.77 MB each and within 192 bytes of one another.
|
||
|
||
- **Entropy 6.08 bits/byte** — neither encrypted nor compressed. Plain code
|
||
and data.
|
||
- Header is a **big-endian vector table**, handlers at `0x4010_30xx`.
|
||
- **~16,700 extractable strings**, including the developers' own debug
|
||
`printf` format strings with function names intact.
|
||
|
||
This is a legitimate interoperability reference for hardware TMI owns, and it
|
||
short-circuits work I had scoped as "only answerable from a live modem
|
||
capture".
|
||
|
||
### The call-home state machine, verbatim
|
||
|
||
```
|
||
ACH_NOT_STARTED → ACH_IDLE → ACH_INITIALIZING → ACH_CONNECTING
|
||
→ ACH_CONNECTED → ACH_TRANSFER_DATA
|
||
→ ACH_RETRY / ACH_QUITTING (also ACH_STARTED)
|
||
```
|
||
|
||
Supporting strings:
|
||
|
||
```
|
||
ACH: Entry StartCallHome()
|
||
ACH: CallHome_task ; CheckAliveTime CANCEL ; TimeBetweenRetries = %d
|
||
ACH: CallHome_task ; !ExpectedCommunicationsDetected() CANCEL ; TimeBetweenRetries = %d
|
||
ACH: CallHomeCommectionCompleteProcessing() ACH=%s EAMWC=%s
|
||
ACH: %s() three attempts and it's over
|
||
ACH: %s() Send CMD_START_MONITOR
|
||
ACH: %s() Send CMD_STOP_MONITOR
|
||
ACH: Start Ignore request, Call Home is in progress
|
||
```
|
||
|
||
What this tells us without a single captured packet:
|
||
|
||
1. **Retry limit is three** — "three attempts and it's over".
|
||
2. **`ExpectedCommunicationsDetected()` gates the session.** If the host does
|
||
not say something the unit recognises, the call is *cancelled* and
|
||
rescheduled after `TimeBetweenRetries`. A homebrew receiver must satisfy
|
||
this check or units will retry forever — which is exactly the failure mode
|
||
seen on BE12599.
|
||
3. **The unit stops monitoring to call home and restarts afterwards**
|
||
(`Send CMD_STOP_MONITOR` / `CMD_START_MONITOR`). Relevant to any
|
||
wedged-unit rescue: the monitoring state around a call is the device's own
|
||
doing, not ours.
|
||
4. **Calls are not re-entrant** — "Call Home is in progress" is ignored.
|
||
|
||
### Internal command table
|
||
|
||
`CMD_CALLHOME`, `CMD_CALLHOME_CANCEL`, **`CMD_CALLHOME_CONNECTION_CONFIRMED`**,
|
||
`CMD_CALLHOME_CONNECTION_COMPLETE`, `CMD_CALLHOME_SET_SCHEDULE`,
|
||
`CMD_CALLHOME_CLEAR_SCHEDULE`, `CMD_STOP_CALLHOME_FILETRANSFER`,
|
||
`CMD_DUTYCYCLE_AUTOCALLHOME`, `CMD_PURGE_EVENT_FLASH`.
|
||
|
||
`CONNECTION_CONFIRMED` as a distinct state from `CONNECTION_COMPLETE` implies
|
||
a **handshake the host must complete before data flows** — the concrete shape
|
||
of `ExpectedCommunicationsDetected()`.
|
||
|
||
### Event delivery — the mechanism, probably
|
||
|
||
```
|
||
All Events Uploaded
|
||
Mark/Unmark File Delete Marked Events Marked Events were Deleted
|
||
MONITOR::MESG PURGE_EVENT_FLASH BEGIN / END
|
||
```
|
||
|
||
A **marking** mechanism exists, alongside a distinct "all uploaded" terminal
|
||
state. 🔶 **Inferred:** events are *marked* as transferred rather than
|
||
deleted on send, and purging is a separate explicit act. If so, a receiver
|
||
that fails to mark would see the same events re-offered every call — the
|
||
question that gates a safe homebrew receiver. **Not yet confirmed**; needs
|
||
either a live call-home capture or disassembly around these strings.
|
||
|
||
### A full user manual is embedded
|
||
|
||
The firmware carries its own HTML help, which documents configuration we would
|
||
otherwise have to infer:
|
||
|
||
- **Modem mode**: `Generic` (through a modem) vs `USB to PC`.
|
||
- **Modem baud**: 9600 / 19200 / 38400 / 57600 / 115200 / 230400 — "must match
|
||
the expected rate of the PC or modem". Confirms 115200 is a *setting*, not
|
||
a fixed rate.
|
||
- **Modem relay + warmup** (0–300 s), auxiliary mode, warning/alarm hold.
|
||
- **Record modes**: Waveform, Waveform Manual, Histogram, Histogram-Combo;
|
||
sample rates 1024 / 2048 / 4096.
|
||
- **A scheduler downloaded from THOR** that can start/stop monitoring, change
|
||
record mode, trigger a call home, or run a self check on a daily/weekly
|
||
schedule. Pairs with `CMD_CALLHOME_SET_SCHEDULE`.
|
||
|
||
### Still worth doing
|
||
|
||
Diffing the two images should isolate exactly what the CB/BD split changes —
|
||
we know the wire protocol is not it, and the flags byte (`0xC5` vs `0x03`)
|
||
gives a concrete anchor to search for.
|
||
|
||
## `SUB 0x5A` — bulk download. It streams the `.IDFW` file verbatim.
|
||
|
||
**The complete read path works with no Instantel software in the loop.**
|
||
|
||
### It needs no arming sequence
|
||
|
||
Series III ignores a `5A` probe unless preceded by
|
||
`1E → 0A → 1E(token 0xFE) → 0C → 1F(token 0xFE) → POLL × 3`. The Micromate
|
||
answers a **bare `5A` request** with nothing before it. That whole ritual is
|
||
gone.
|
||
|
||
### The offset word is a LENGTH, not a position
|
||
|
||
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*:
|
||
|
||
```
|
||
offset_word = 0x1000 + 2 × pages pages = ceil(event_size / 512)
|
||
```
|
||
|
||
| `offset_word` | pages | destuffed file bytes |
|
||
|---|---|---|
|
||
| `0x1002` | 1 | 518 |
|
||
| `0x1004` | 2 | 1,030 |
|
||
| `0x1006` | 3 | 1,541 |
|
||
| `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.
|
||
|
||
Params are the Series III *probe* form: `[0x00][key4][6 × 0x00]`.
|
||
|
||
### The payload is the `.IDFW` file, byte for byte
|
||
|
||
```
|
||
[18-byte frame header] [ .IDFW file ] [chk] [ETX] ← raw wire
|
||
^ destuffed offset 16
|
||
```
|
||
|
||
The file begins `00 12 01 00 00 00 "Instantel\0"` — `_THOR_PREFIX` +
|
||
`_INSTANTEL_TAG` from `micromate/idf_file.py`. The first 32 bytes are
|
||
**identical to a production `.IDFW`** pulled from the store.
|
||
|
||
⚠ Responses are DLE-stuffed. Destuff before locating the file, or the raw
|
||
byte count overshoots (11,781 raw → 11,049 destuffed for an 11,032-byte event).
|
||
|
||
### End-to-end proof
|
||
|
||
Event `055d4a82` downloaded over USB and fed straight to `read_idf_file()`:
|
||
|
||
```
|
||
serial UM12947
|
||
timestamp 2026-09-23 16:33:19
|
||
samples Tran 3072 Vert 3072 Long 3072 MicL 3072
|
||
peaks Tran 0.2433 Vert 1.3706 Long 0.2672 in/s
|
||
```
|
||
|
||
All four channels equal length, and the timestamp matches the `0x0C` record
|
||
for the same key. **Independent cross-check:** `0x0C` reports a stored peak
|
||
of **1.3720** for this event; the decoded samples give **1.3706** — two
|
||
unrelated paths agreeing to 0.1%.
|
||
|
||
**Consequence:** no new codec work is needed. The bytes off the wire are the
|
||
same bytes `thor-watcher` forwards today, so `/db/import/idf_file` ingests a
|
||
directly-downloaded event unchanged. Everything the IDF decoder already does
|
||
per-sample-exact applies.
|
||
|
||
### What a full read now looks like
|
||
|
||
```
|
||
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)
|
||
```
|
||
|
||
## Setups are FILES, not a config block
|
||
|
||
Series III has one compliance config you overwrite. Series IV keeps **named
|
||
setup files on an on-device filesystem**, with a pointer to the current one.
|
||
From the firmware:
|
||
|
||
```
|
||
csetup.MMB the current setup
|
||
factory.MMB Factory Default Setup File
|
||
callhome.MMB call-home config is a file too
|
||
"Current Setup File: " "Can Not Delete Active Setup File"
|
||
GetSelectedSetupFilePathName() CSelectSetupFiles CSaveSetupFile
|
||
```
|
||
|
||
Names are up to 20 characters and may contain spaces, hyphens, underscores.
|
||
The unit's help text describes selecting, renaming and deleting them, and the
|
||
event list records which setup file produced each event.
|
||
|
||
Filesystem primitives exist internally (`NS_ReadFile_internal`,
|
||
`NS_WriteFile_internal`, `NS_SeekFile_internal`).
|
||
|
||
> ⚠ **RETRACTED 2026-09-24.** This section originally concluded "**no generic
|
||
> file-transfer command is exposed on the wire**", reasoning from the absence of
|
||
> firmware strings. **That was wrong.** `SUB 0x94` / `0x48` / `0x8D` / `0x8E`
|
||
> are exactly that — a read/write file transfer addressed by **full filesystem
|
||
> path** — and they were caught in the open on the first capture that touched
|
||
> the scheduler. See *The scheduler, and a generic file transfer* below.
|
||
>
|
||
> The lesson is the usual one: absence of a firmware string is not absence of a
|
||
> command. The dispatch is a 68K jump table and the commands carry no strings
|
||
> of their own.
|
||
>
|
||
> The *setups* half of the original claim survives: setups are **not** pushed as
|
||
> raw `.MMB` blobs. They go through `0xDA` + the ordinary config block.
|
||
|
||
### `SUB 0x1A` reads the whole active setup — 2,092 bytes
|
||
|
||
Structurally close to Series III's ~2,126-byte compliance block, and it
|
||
carries everything a setup consists of:
|
||
|
||
- **the setup file name** — `Univ of Pitt-1st Yr. Housing-Loc1 Ruskin.MMB`
|
||
- all four title note/value pairs — `Location`, `Client`, `Company`,
|
||
`General Notes`, with their strings
|
||
- the sensor location string (`Loc 1`)
|
||
- per-channel labels *and units*: `Tran in./s.`, `Vert in./s.`,
|
||
`Long in./s.`, `Mic psi (L)`, `LMic psi (L)`, `SMic (A)`
|
||
|
||
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 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*: Thor names the target file, then
|
||
writes the ordinary config block into it. The unit acks with `0x25` before any
|
||
config bytes are sent.
|
||
|
||
Note `0xDA` takes a **bare filename** null-padded to 256 bytes. That is a
|
||
different mechanism from the path-addressed file transfer (`0x94` / `0x8D`,
|
||
which carry `\system\schedule\schedule.dat` unpadded) — setups do not go through
|
||
the file transfer, and the file transfer is not how setups are written.
|
||
|
||
### 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`.
|
||
- **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.
|
||
|
||
### ✅ `0xDA` CREATES setup files (confirmed on the device, 2026-09-24)
|
||
|
||
`TEST1.mmb` did not exist on the unit before the push. Afterwards it is
|
||
**present in the unit's setup list and selected as the active config** —
|
||
verified on the Micromate's own screen, not inferred from the ack.
|
||
|
||
So the four commands below are the complete setup-management path, and a
|
||
homebrew client needs no file-transfer primitive and no pre-existing target:
|
||
|
||
```
|
||
0x41 read the active setup's name
|
||
0x1A read its config block ─┐ read
|
||
0xDA name the target .MMB ─┘ modify
|
||
0x71 → 0x72 write the block back write (creates the file if absent)
|
||
```
|
||
|
||
That closes the last open question about whether Series IV setup management is
|
||
reachable from outside Thor. It is.
|
||
|
||
### ✅ Overwriting is protocol-identical to creating (2026-09-24)
|
||
|
||
The firmware carries strings that suggest an overwrite handshake:
|
||
|
||
```
|
||
Overwrite File MFS FILE EXISTS
|
||
Cannot be Overwritten Can Not Delete Active Setup File
|
||
```
|
||
|
||
**There is no such handshake on the wire.** A second push to `TEST1.mmb` — a
|
||
name that now existed, and which `SUB 0x41` confirmed was the *active* setup —
|
||
produced a byte-for-byte identical command sequence:
|
||
|
||
| | create | overwrite |
|
||
|---|---|---|
|
||
| SUBs, and their order | `5B 41 08 2E 1A DA 68 73 82 83 71 72` | **identical** |
|
||
| every `offset` field | — | **identical** |
|
||
| `0xDA` / `0x68` / `0x82` data | — | **0 bytes differ** |
|
||
| `0x71` data | — | 18 bytes differ = the one edited note |
|
||
| all seven write acks | 11 zero bytes | **identical, still all-zero** |
|
||
|
||
No extra command, no confirm step, no error status, and **no dialog on Thor**.
|
||
Those firmware strings belong to the on-device Save screen (the
|
||
`CSaveSetupFile` UI class), not to the protocol.
|
||
|
||
**The overwrite was verified on the unit itself** — the edited `General Notes`
|
||
string is present in the setup on the device, so the write applied, silently and
|
||
in place.
|
||
|
||
This is the case that mattered most, and it landed the right way round: the
|
||
target was the **active** setup, which is what a real remote config push would
|
||
hit. Being active bought it **no protection** — it was overwritten directly.
|
||
A writer therefore needs no exists-check and no overwrite negotiation.
|
||
|
||
⚠ That cuts both ways. A remote push to the active setup of a **monitoring**
|
||
unit changes the config it is recording with, with no prompt, no warning and no
|
||
distinguishable ack. Whatever SFM eventually exposes should gate this on the
|
||
operator, not on the protocol — the device will not stop anyone.
|
||
|
||
⚠ Two overwrite cases remain untested: a **non-active** setup file, and
|
||
`factory.MMB`, which `Cannot be Overwritten` probably guards. Neither blocks a
|
||
writer — the active setup is the one worth pushing to.
|
||
|
||
⚠ Note also that the write acks are **all-zero in every case observed**, across
|
||
a create and an overwrite. We have never seen this protocol report a *failed*
|
||
write, so **do not treat a zero ack as proof a write was applied.** Read the
|
||
config back and compare; `SUB 0x41` plus `SUB 0x1A` make that cheap.
|
||
|
||
## The scheduler, and a generic file transfer (2026-09-24)
|
||
|
||
Capture: `bridges/captures/9-24-26 - micromate2/*_read_scheduler.bin`.
|
||
25 request frames, 25 responses, every checksum valid.
|
||
|
||
Four operations in one capture, segmented by Thor's `POLL` preamble (marks are
|
||
not written into the raw `.bin` — in TCP mode seismo_lab logs them to the
|
||
on-screen log only):
|
||
|
||
| frames | operation |
|
||
|---|---|
|
||
| 0–4 | read the schedule off the unit |
|
||
| 5–18 | push a setup — the sequence already documented above, unchanged |
|
||
| 19–21 | write the schedule back |
|
||
| 22–24 | enable the scheduler |
|
||
|
||
### 🔑 `SUB 0x94` / `0x48` / `0x8D` / `0x8E` — file transfer by path
|
||
|
||
**The unit will read and write files addressed by full filesystem path.**
|
||
|
||
```
|
||
0x94 <path> → 0x6B open for READ
|
||
0x48 → 0xB7 read next page; repeat until an all-zero response
|
||
0x8D <path> → 0x72 open for WRITE
|
||
0x8E <body> → 0x71 write the file body
|
||
```
|
||
|
||
The path is plain ASCII, **unpadded**, with `offset` = its exact length:
|
||
|
||
```
|
||
5c 73 79 73 74 65 6d 5c 73 63 68 65 64 75 6c 65 5c 73 63 68 65 64 75 6c 65 2e 64 61 74
|
||
\system\schedule\schedule.dat 29 bytes, offset = 0x001D
|
||
```
|
||
|
||
`0x94` and `0x8D` sent **byte-identical** 29-byte payloads — the same path, only
|
||
the command distinguishing read from write.
|
||
|
||
The `0x48` read is paged, and the page number comes back in the response header
|
||
at `payload[3:5]`:
|
||
|
||
| call | page | payload | content |
|
||
|---|---|---|---|
|
||
| 1st | `0x0000` | 15 B | descriptor — `04 00 … 01 00 00 00 00 00 01` |
|
||
| 2nd | `0x0002` | 535 B | the file body |
|
||
| 3rd | `0x0000` | 11 B | all zeros = **end of file** |
|
||
|
||
⚠ This is the single most consequential find of the session, because it is not
|
||
specific to the scheduler. **`callhome.MMB` is a file too**, and the call-home
|
||
config is the last unsolved piece of the project. Reading it should be a
|
||
matter of pointing `0x94` at the right path. That is a *lead*, not a result —
|
||
no path other than `schedule.dat` has been tried.
|
||
|
||
### The schedule file itself
|
||
|
||
The write body is the read body minus an 11-byte response prefix (524 vs 535),
|
||
so both describe the same structure:
|
||
|
||
```
|
||
03 00 00 00 0f 03 02 [namelen] [setup-file name] …zeros… 27 03 10
|
||
```
|
||
|
||
- **`[namelen]` is a length prefix**: `0x28` = 40 for the 40-character name read
|
||
off the unit, `0x09` = 9 for `TEST1.mmb` written back. Confirmed both ways.
|
||
- The trailing `27 03 10` sits at `0x108` in the write body and `0x113` in the
|
||
read — the same offset once the 11-byte prefix is accounted for.
|
||
|
||
**A schedule entry names a setup file.** That is how the help text's "change
|
||
the record mode" works: an entry says *at this time, load this setup*. It also
|
||
means the scheduler and the setup list are coupled — deleting a setup that a
|
||
schedule references is a foot-gun worth checking before we ever expose either.
|
||
|
||
⚠ **The entry internals are NOT decoded.** One entry, one capture, no
|
||
variation to diff against. `03 00 00 00 0f 03 02` and `27 03 10` are recorded
|
||
as observed bytes, nothing more. Decoding needs schedules that differ in a
|
||
known way — two entries, or one entry at a different time of day.
|
||
|
||
### `SUB 0x47` — the scheduler enable, probably
|
||
|
||
Step 4 is two bare `0x47` frames with no data, differing only in `params[7]`:
|
||
|
||
```
|
||
req params = 00 00 00 00 00 00 00 [01] 00 00 → rsp 04 00 … 00 [01] 00 00 00 00 00 01
|
||
req params = 00 00 00 00 00 00 00 [03] 00 00 → rsp 04 00 … 00 [03] 00 00 00 00 00 01
|
||
```
|
||
|
||
The response echoes the selector and ends `01`. Its 15-byte shape is identical
|
||
to the descriptor `0x48` returns on page 0.
|
||
|
||
⚠ **Whether `0x47` sets or merely reads is genuinely undetermined.** Both calls
|
||
returned the same trailing `01`, and there is no before/after to compare — the
|
||
scheduler was enabled in this same capture, so no "disabled" reading exists.
|
||
`params[7]` is also the token position Series III uses, which is suggestive but
|
||
not evidence. **Do not implement an enable against this until a
|
||
disable-then-enable capture settles it.**
|
||
|
||
### The scheduler enable is NOT in the setup config block
|
||
|
||
A prediction made before this capture — that the `Scheduler On/Off` switch would
|
||
appear as a byte in the `0x71` block, because the unit's help text lists it in
|
||
the same edit screen as Record Mode and Sample Rate — **did not hold**.
|
||
|
||
The `0x71`, `0x68` and `0x82` payloads are **byte-identical** to the previous
|
||
capture's, all three, zero differences. The scheduler was enabled without any
|
||
of them changing.
|
||
|
||
⚠ The test is weaker than it looks: the operator re-sent the *same* config, so
|
||
a byte-identical block is also what "nothing changed" looks like. What it does
|
||
establish is that enabling the scheduler did **not** require a config write —
|
||
whatever `0x47` does, it does alone.
|
||
|
||
## Monitoring control and the setup list (2026-09-24)
|
||
|
||
Capture: `bridges/captures/9-24-26 - micromate2/*_turn_on_monitormode_*`.
|
||
Thor started monitoring, listed the unit's setups, and stopped monitoring.
|
||
**40 request frames and 40 responses, every checksum valid.**
|
||
|
||
### Thor's per-operation preamble
|
||
|
||
Thor re-runs this before *every* operation — three times in this one capture:
|
||
|
||
```
|
||
POLL (0x5B) → SERIAL (0x15) → 0x49 → POLL (0x5B) → <the operation>
|
||
```
|
||
|
||
⚠ **Whether the unit requires this is untested** — see *Thor's conventions vs
|
||
the protocol's requirements*. Thor sends it before trivial reads too, so it may
|
||
be habit rather than handshake. Do not assume it is mandatory. Note `POLL` here carries `offset = 0x0030` (its data
|
||
length), not `0xFFFF` — `POLL` is the one read Thor still addresses by length.
|
||
|
||
### `SUB 0x96` / `0x97` — start and stop monitoring ✅
|
||
|
||
Identical to Series III, including the acks:
|
||
|
||
| request | ack | effect |
|
||
|---|---|---|
|
||
| `0x96` | `0x69` | **start monitoring** |
|
||
| `0x97` | `0x68` | **stop monitoring** |
|
||
|
||
Both are bare frames — no params, no data — and both ack with the usual 16-byte
|
||
zero-data response. Thor follows each with a `SUB 0x1C` status read to confirm.
|
||
|
||
### `SUB 0x1C` — monitor status, 55-byte data
|
||
|
||
```
|
||
data[12] monitoring flag 0x0E monitoring / 0x00 idle
|
||
data[13] day
|
||
data[14] month
|
||
data[15:17] year, uint16 BE
|
||
data[17] ⚠ unidentified — 32 while monitoring, 100 when idle
|
||
data[18] hour ─┐ device clock, verified against the capture's
|
||
data[19] minute │ own wall time (19:12:25 → 19:13:34 EDT)
|
||
data[20] second ─┘
|
||
data[-8:-4] memory total, uint32 BE = 15,000,000 bytes exactly
|
||
data[-4:] memory free, uint32 BE
|
||
```
|
||
|
||
⚠ **The monitoring flag is `0x0E`, not Series III's `0x10`.** Do not reuse the
|
||
Series III constant. Only five bytes differ between the monitoring and idle
|
||
responses: the flag, `data[17]`, the clock, and the memory-free field.
|
||
|
||
Memory free dropped by exactly **4,096 bytes** across the ~70-second monitoring
|
||
session — monitoring allocates as it runs, so free memory is not a stable value
|
||
to compare against.
|
||
|
||
The device clock is free here, on a command Thor already sends. That is worth
|
||
having: nothing else read so far reports the unit's own time.
|
||
|
||
### `SUB 0x49` → `0xB6` — a cheap state check
|
||
|
||
16-byte data, in Thor's preamble before every operation:
|
||
|
||
```
|
||
05 00 00 00 00 00 00 00 00 00 00 [ST] e8 00 0b 00
|
||
↑ data[11]: 0x02 monitoring, 0x00 idle
|
||
```
|
||
|
||
**A second monitoring indicator, in a 21-byte response instead of `0x1C`'s 60.**
|
||
Different encoding from `0x1C`'s flag (`0x02` vs `0x0E`), so they are separate
|
||
fields, not the same byte read twice. For a polling client this is the cheaper
|
||
of the two, and Thor evidently treats it as the routine one.
|
||
|
||
### `SUB 0x3F` / `0x40` — walking the setup-file list ✅
|
||
|
||
The same first/next shape as Series III's `1E`/`1F` event walk, applied to
|
||
setup files:
|
||
|
||
| request | ack | meaning |
|
||
|---|---|---|
|
||
| `0x3F` | `0xC0` | **first** setup record |
|
||
| `0x40` | `0xBF` | **next** setup record; repeat until the name is empty |
|
||
|
||
Every record is 266 bytes of data and carries **nothing but the name**:
|
||
|
||
```
|
||
ff 00 00 00 00 00 00 00 00 00 00 <null-terminated name> 00 …
|
||
└──────────── 11-byte header ─────┘
|
||
```
|
||
|
||
The walk ends on a record whose name is empty — 24 records for 23 setups.
|
||
`factory.MMB` comes first, from `0x3F`.
|
||
|
||
**There is no active-setup flag in the list.** The header is byte-identical on
|
||
every record including the terminator, and the tail is all zeros. The active
|
||
setup is identified only by `SUB 0x41`, whose response uses this exact record
|
||
format. (On the unit's own screen the active setup is marked with a trailing
|
||
asterisk — that is a UI decoration, not a field.)
|
||
|
||
`TEST1.mmb`, created over the wire earlier the same day, appears in the list and
|
||
is what `0x41` reports as active — independent confirmation that a written setup
|
||
becomes a real, enumerable file.
|
||
|
||
### Thor refuses to send a setup while a unit is monitoring
|
||
|
||
Send-to-unit is **greyed out in Thor's UI** when the unit is monitoring, and
|
||
nothing is transmitted — this capture contains no `0xDA` or `0x71` at all.
|
||
|
||
That is a Thor-side policy, not a device refusal: nothing observed suggests the
|
||
Micromate would reject the write. It squares with the earlier finding that a
|
||
push to the **active** setup overwrites silently — Thor is preventing exactly
|
||
the footgun the protocol leaves open.
|
||
|
||
The *reason* behind the rule is sound and SFM should honour it — but not
|
||
necessarily by copying the greyed-out button. Stop → push → restart as a single
|
||
operation is what an operator usually wants, and `0x49` data[11] (or `0x1C`
|
||
data[12]) makes the state check cheap either way.
|
||
|
||
## Static analysis of the firmware (2026-09-23, solo session)
|
||
|
||
### Architecture
|
||
|
||
**ColdFire / 68K, big-endian, Freescale MQX RTOS** — not ARM as the vector
|
||
table first suggested. The giveaway is the function epilogue/prologue
|
||
`4E 5E 4E 75 4E 56` = `UNLK A6` / `RTS` / `LINK A6`, littered through both
|
||
images, plus an `MQX_OK` assertion string.
|
||
|
||
### CB vs BD: the same source, ~10 lines apart
|
||
|
||
A byte diff is useless — **68% of bytes differ** because the two are separately
|
||
linked builds with everything relocated. A *string-set* diff is
|
||
position-independent and tells the real story: **17,128 strings shared**, and
|
||
almost every "unique" string is the same message with a different source line
|
||
number:
|
||
|
||
```
|
||
CB: MONITOR[3268]: STATUS_BATTERY_LOW
|
||
BD: MONITOR[3258]: STATUS_BATTERY_LOW ← consistently 10 lines apart
|
||
```
|
||
|
||
The offset is exactly 10 across `STATUS_BATTERY_LOW`, `STATUS_BATTERY_CRITICAL`,
|
||
`Battery Critical Exit Monitor`, `histogram interval size of 0` and
|
||
`Offsets by channel` — so one ~10-line block differs in the monitor module and
|
||
essentially nothing else. The only functional string unique to either build is
|
||
`CITIZEN` (a receipt-printer brand) in BD.
|
||
|
||
**This corroborates the bench A/B from the other direction:** the CB/BD split
|
||
is a tiny code delta, not two protocol stacks. Whatever drives Instantel to
|
||
ship two downloads, it is not a different wire protocol.
|
||
|
||
⚠ The `SUB` dispatch is a 68K switch jump table (`CMPI.L` bounds check →
|
||
`MOVE.W (table,PC,Dn)` → `JMP (d8,PC,Xn)`). Byte-pattern hunting will not
|
||
isolate the write opcodes — that needs a real disassembler.
|
||
|
||
### Call-home config field names, from the firmware's own debug dump
|
||
|
||
```
|
||
CallHome.Enable = %s
|
||
CallHome.DialString = "%s"
|
||
CallHome.Retries = %d
|
||
CallHome.SessionTimeout = %d
|
||
CallHome.WaitForConnection = %d
|
||
CallHome.WarmupTime = %d
|
||
CallHome.PowerSave = %s
|
||
```
|
||
|
||
Seven fields, which is what the 126-byte `SUB 0x2C` block has to encode. Note
|
||
`SessionTimeout` and `PowerSave` have no Series III equivalent, and Series III's
|
||
scheduled-time fields (`time1/time2 hour/min`) are absent here — consistent
|
||
with Series IV moving scheduling into the THOR-downloaded scheduler instead.
|
||
|
||
Also present: `AT+CSQ` (signal quality), so the firmware talks AT to the modem
|
||
directly.
|
||
|
||
## All five bench events, downloaded and decoded
|
||
|
||
Read-only, over USB, no Instantel software:
|
||
|
||
| key | declared size | got | decoded |
|
||
|---|---|---|---|
|
||
| `055d4a81` | 4,076 | 4,076 | histogram, 1 interval, 16:33:16 |
|
||
| `055d4a82` | 11,032 | 11,032 | waveform, 3072 × 4 ch, 16:33:19 |
|
||
| `055d4a83` | 11,502 | 11,502 | waveform, 3072 × 4 ch, 16:33:27 |
|
||
| `055d4a84` | 13,424 | 13,424 | waveform, 3072 × 4 ch, 16:33:34 |
|
||
| `055d4a85` | 8,746 | 8,746 | waveform, 2048 × 4 ch, 16:33:36 |
|
||
|
||
Every event arrived at exactly its declared size, every channel came out equal
|
||
length, and the timestamps are sequential across the recording session.
|
||
|
||
### Record type + filename: generate it, don't detect it
|
||
|
||
`read_idf_file()` decides waveform vs histogram from the **filename suffix** —
|
||
and there is no filename when downloading over the wire.
|
||
|
||
⚠ Worth correcting a natural assumption: **Series III does not detect this from
|
||
content either.** `event_file_io.derive_record_type_from_filename()` reads the
|
||
last character of the extension (`M529LKIQ.G10H` → `H` → Histogram). Nothing
|
||
in the codebase infers record type from file content, for either family.
|
||
|
||
And there is no obvious type field to find. The first 64 bytes of a histogram
|
||
and a waveform are byte-identical; they diverge at ~`0x0947` into wholly
|
||
different structures rather than differing by a flag.
|
||
|
||
**The answer is the Series III pattern — generate the name.** Series III has
|
||
`blastware_filename()`, which builds a name from serial + timestamp + type.
|
||
Series IV needs the same thing, and its convention is far simpler:
|
||
|
||
```
|
||
<serial>_<YYYYMMDDHHMMSS>.IDF{W,H} e.g. UM12947_20260923163319.IDFW
|
||
```
|
||
|
||
versus Series III's `<letter><serial3><4-char base-36 stem><AB0T ext>`, where
|
||
the stem is base-36 of seconds-since-1985 ÷ 1296.
|
||
|
||
All three inputs are already available on a direct download:
|
||
|
||
| input | source |
|
||
|---|---|
|
||
| serial | `extract_binary_metadata()` — decoded from the IDF header |
|
||
| timestamp | `extract_binary_metadata()` — same |
|
||
| **type** | **the chain walk** — `SUB 0x0A` length `0x1E` = histogram, `0x00` = waveform |
|
||
|
||
Verified against all five bench events: the generated names match the
|
||
convention of real files in the production store byte for byte. A directly
|
||
downloaded event can therefore be filed under exactly the name Thor would have
|
||
given it, and `/db/import/idf_file` needs no change at all.
|
||
|
||
⚠ The type still comes from the *protocol*, not the payload — so a downloader
|
||
must carry it out of the chain walk. Losing it means losing the ability to
|
||
name the file correctly.
|
||
|
||
### ⚠ Unresolved: the `0x0C` peak float
|
||
|
||
The float32 extracted from `0x0C` runs 2–5% above `max(Tran, Vert, Long)` from
|
||
the decoded samples:
|
||
|
||
| key | `0x0C` float | max channel |
|
||
|---|---|---|
|
||
| `…81` | 3.5152 | 3.4419 |
|
||
| `…82` | 1.3720 | 1.3706 |
|
||
| `…83` | 2.3542 | 2.2227 |
|
||
| `…84` | 3.5152 | 3.4419 |
|
||
| `…85` | 0.4227 | 0.4198 |
|
||
|
||
It is not peak vector sum either (computed PVS runs *higher* than both). The
|
||
field may not be the peak at all — its offset was inferred from a byte marker,
|
||
not established. **Do not rely on it** until it is pinned properly.
|
||
|
||
Worth noting the histogram (`…81`) and the loudest waveform (`…84`) report
|
||
*identical* peaks to four decimals, in both measures. That is self-consistent:
|
||
the histogram's single 1-minute interval spans the whole thumping session, so
|
||
its maximum should equal the loudest event in it.
|
||
|
||
## Thor's conventions vs the protocol's requirements
|
||
|
||
**SFM is not meant to reimplement Thor.** Thor is the only available teacher of
|
||
the wire protocol, but almost nothing about *how* it sequences its work has been
|
||
shown to be required by the device. Those are two different things and this
|
||
document should not blur them.
|
||
|
||
The distinction matters because several observations above were written as
|
||
"a client should do X" when the honest statement is "Thor does X, and we have not
|
||
checked whether the unit cares."
|
||
|
||
| Thor does this | required? | what it means for SFM |
|
||
|---|---|---|
|
||
| `POLL → 0x15 → 0x49 → POLL` before **every** operation | **unknown** | Series III needed `POLL`×3 before `5A` *specifically*, so a preamble requirement is plausible — but Thor sends this before trivial reads too. **Testable:** issue one operation cold and see if it answers. |
|
||
| Reads with `offset = 0xFFFF`, skipping the probe | **no — the probe works** | We have a genuine choice, and the probe is arguably better: it reports the length instead of making us trust a fixed one. This is the one place Thor's shortcut is probably worse. |
|
||
| Sends `0x68` + `0x82` in every setup push | **unknown** | In both setup captures these carried near-zero payloads and **changed nothing**. If they are optional, a setup write is 3 frames instead of 7, with less to get wrong. **Worth testing before we build the writer.** |
|
||
| Rewrites the whole 2,090-byte config for a one-field change | **unknown** | No narrower write has been observed. Read-modify-write is safe and known; a targeted write would be nicer but is unevidenced. |
|
||
| Reads `0x41` twice in a row (scheduler capture, frames 6 and 8) | **no** | Plainly redundant. A reminder that Thor's sequence is not a minimal one. |
|
||
| Greys out send-to-unit while monitoring | **Thor policy** | The *reason* is real — a push to the active setup overwrites silently. But "the button is grey and you figure it out" is a UX choice, not the only answer. SFM could offer stop → push → restart as one operation, which is what an operator actually wants. |
|
||
| Names the target with `0xDA` before a config write | **almost certainly required** | The unit has to know which file to write. Closest thing here to a genuine protocol requirement. |
|
||
|
||
### The schedule↔config coupling is Thor's, not the protocol's
|
||
|
||
In Thor, putting "start monitoring" in a schedule forces you to attach a setup,
|
||
and sending the schedule pushes that setup at the same time — overwriting
|
||
whatever already has that name. **The protocol does not require any of this.**
|
||
|
||
The evidence, from the scheduler capture:
|
||
|
||
- **The two writes are separate operations**, not one transaction. The config
|
||
write ends at frame 18 (`0x72`), then Thor sends a fresh `POLL` preamble, and
|
||
only then opens the schedule for writing at frame 20. Different commands,
|
||
different paths, no shared state:
|
||
|
||
```
|
||
config 0xDA → 0x68/0x73 → 0x82/0x83 → 0x71/0x72
|
||
schedule 0x8D → 0x8E
|
||
```
|
||
|
||
- **The schedule stores a NAME, not a config.** A length-prefixed filename is
|
||
all an entry carries. It is a *reference*, and references do not require the
|
||
referent to be rewritten.
|
||
|
||
- **The config Thor pushed was already on the unit, unchanged.** Its 2,090-byte
|
||
`0x71` payload is **byte-identical** to the previous capture's — zero
|
||
differences. Thor spent a full block write re-sending a setup the device
|
||
already had, purely to satisfy its own coupling.
|
||
|
||
So the sequencing is a Thor UI decision, and a costly one.
|
||
|
||
**What SFM can do instead, with today's protocol and nothing new:**
|
||
|
||
1. Enumerate the unit's setups with `0x3F` / `0x40` — cheap, read-only, and it
|
||
yields the exact names a schedule may legally reference.
|
||
2. Write **only** the schedule (`0x8D` / `0x8E`) when the referenced setup is
|
||
already present.
|
||
3. Push a config only when it is actually missing, or when the operator
|
||
deliberately edited it — and say so explicitly rather than silently.
|
||
|
||
That removes the write entirely from the common case: scheduling against a setup
|
||
that already exists becomes a 524-byte schedule write instead of 524 bytes plus
|
||
a 2,090-byte config overwrite.
|
||
|
||
⚠ **And it removes a real hazard.** Because the reference is by name, and
|
||
because a same-name write overwrites silently with an indistinguishable ack,
|
||
Thor's pattern means *scheduling* something can quietly rewrite a setup that
|
||
other schedules — or the operator's own saved work — depend on. Nothing in the
|
||
protocol or the ack reports that this happened. Validating the reference
|
||
instead of rewriting the referent avoids the whole class of problem.
|
||
|
||
### What this implies for the build order
|
||
|
||
The read path is fully known and needs no Thor-shaped decisions, so a read-only
|
||
client can be written now with confidence. **The write path should not be built
|
||
by transcribing Thor's sequence** — the `0x68`/`0x82` question above decides
|
||
whether our setup write is 3 frames or 7, and it is answerable with one capture
|
||
plus one careful experiment.
|
||
|
||
Two reliability problems worth designing *against*, both observed rather than
|
||
assumed:
|
||
|
||
1. **A zero ack does not mean a write applied.** Every ack seen is 11 zero
|
||
bytes, across creates and overwrites alike, and no failing write has ever been
|
||
observed. Whatever SFM does, it should read back and compare rather than
|
||
trust the ack. `0x41` + `0x1A` makes that cheap.
|
||
2. **Nothing warns before clobbering a monitoring unit's active setup.** The
|
||
device will not stop it and the ack will not distinguish it. That guard has
|
||
to live in SFM.
|
||
|
||
## ⚠ Untested and unsafe-until-agreed
|
||
|
||
**Nothing below has been sent to a unit by us, and nothing should be without an
|
||
explicit decision.**
|
||
|
||
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`) — **the only genuinely untouched destructive path**
|
||
- **Start / stop monitoring** (`0x96` / `0x97`) — observed via Thor 2026-09-24,
|
||
acks `0x69` / `0x68`; still never originated by us
|
||
- `0x1F` (advance event pointer) — non-destructive on Series III but it does
|
||
move device state, so it is parked with the rest
|
||
|
||
Also unknown:
|
||
|
||
- 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.**
|
||
That last question gates any homebrew receiver and cannot be answered over
|
||
USB.
|
||
|
||
---
|
||
|
||
## Session provenance
|
||
|
||
Unit **UM12947**, firmware **`11.0CB`** (Blastware line), on the bench via
|
||
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.
|
||
|
||
✅ The original "empty unit" limitation is gone — `0x08`, `0x1E`, `0x0A` and
|
||
`0x06` were all exercised against 5 stored events on 2026-09-23.
|