# 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**, 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. --- ## 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: 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). --- ## 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. ## ⚠ 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**, 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. ⚠ 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.