diff --git a/docs/micromate_protocol_reference.md b/docs/micromate_protocol_reference.md new file mode 100644 index 0000000..ee0eb0f --- /dev/null +++ b/docs/micromate_protocol_reference.md @@ -0,0 +1,231 @@ +# 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 + +**The Micromate 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 from a Micromate. Ten Series III read +commands were then tried and **all ten answered**, every one obeying the +Series III response-SUB rule. + +This is not a new protocol. It is close enough to Series III that large parts +of `minimateplus/protocol.py` may apply more or less directly, and the work +ahead is mapping *divergence* rather than mapping from scratch. + +--- + +## 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**, at `payload[9]`: + +| 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 | `0x2C` | — | +| `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. + +--- + +## 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]`. +5. **Modem serial rate is 115200**, not 38400 (per TMI provisioning practice; + not independently verified here). + +--- + +## ⚠ Untested and unsafe-until-agreed + +Nothing below has been sent to a unit, and nothing should be without an +explicit decision: + +- **Writes** (`0x68`–`0x83`), **call-home write** (`0x7E`/`0x7F`) +- **Erase** (`0xA3` / `0xA2`) +- **Start / stop monitoring** (`0x96` / `0x97`) +- `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 +- Whether the bulk waveform stream (`5A` on Series III) exists here, and + whether it is the transport for `.IDFW` bodies we already decode +- 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**, on the bench via USB, **zero events stored** (memory free == +total). Read commands only. Every response in this document was checksum- +validated. + +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.