A debug printf in the scheduler names the record's fields outright: SCHEDULER : _ReadRecord(%d) -> %s (Action=%u, 1/2h=%u, Day=%u, Setup="%s") [WDAY=%d] They fit the captured record, and the name-length byte anchors the alignment -- it reads 40 for the 40-character name pulled off the unit and 9 for TEST1.mmb written back, same position, both directions: [0] Action = 3 [1:4] zero (padding, or Action is a uint32) [4] 1/2h = 15 -> 30-minute resolution, 48 slots/day [5] Day = 3 [6] unidentified (WDAY?) [7] name length -- 0x28=40 read, 0x09=9 written <-- confirms the layout [8:] setup name [264] trailer 27 03 10 Slot 15 would be 07:30 counted from midnight; flagged unconfirmed because the schedule's actual time was not recorded with the capture. Six duty-cycle actions, not the five previously recorded -- there is also DUTYCYCLE_START_MONITOR_WITH_SETUP_STOP_COMPLETE. THOR exposes four. Two start variants it never offers, one needing no setup file. Strengthened the setup-less-action finding and ruled out an alternative explanation I had not considered: the Micromate has a separate Timer Mode (MODE_TIMER, Monitor Once Only, under Special Setup), so START_MONITOR could have belonged to that path. It does not -- it is a case in _PSA(), the scheduler's own dispatcher for _ReadRecord's Action field, and `_PSA() send ->> CMD_DUTYCYCLE_ START_MONITOR` shows it is live code sending a real message, not a dead case. Also: SysPref.bMonitorScheduler places the scheduler enable in system preferences, which confirms from the other side why the 0x71 block was byte-identical when the scheduler was switched on -- the flag was never going to be in the compliance config. It also suggests 0x47 is a SysPref get/set rather than anything scheduler-specific, which would explain its params[7] selector and its response shape matching 0x48's page-0 descriptor. Still a hypothesis. Names the one capture that would settle the rest: a schedule with TWO entries at different times with different actions. That yields the record stride, the action code values, and the time encoding at once. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ru8Lg9HkkYvX9VWWo65SmL
75 KiB
Micromate Protocol Reference — Thor / Micromate Series IV, live wire protocol
Sibling to instantel_protocol_reference.md
(Series III, "the Rosetta Stone") and
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:
- What file format does a
11.0CBunit 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. - Does Thor still work with a
11.0CBunit? If not, flashing a production unit breaks data collection until the replacement path exists. - Are any Micromate-specific capabilities lost on the Blastware line?
- 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
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, then10 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)
- No
DLEprefix on responses — bareSTX. - Response flags byte is
0xC5, not0x10. - Call-home config is 126 bytes, not 124.
- Data lengths are discoverable from the probe response at
payload[9]. 4b. Event keys are a sequential counter, not flash addresses. 4c.1E/1Freturn the event size, where Series III returns an offset. 4d.0x0Avs0x0Csplit by record type, not by the0x46/0x2Clength discriminator Series III uses. 4e. Responsepayload[1](flags) encodes the firmware line —0xC5Blastware,0x03Thor — where Series III has a constant0x10. - Modem serial rate is 115200, not 38400 (per TMI provisioning practice; not independently verified here).
- Reads are single-step — Thor asks for
offset = 0xFFFFand gets the whole block; Series III always probes first. (2026-09-24) - Setup writes are preceded by
SUB 0xDA, which names the target.MMBfile. Series III has no equivalent — it has one config, not named files. - The compliance block is written in ONE
0x71frame, not Series III's three chunks. 0x69/0x74(waveform data write) are absent from a setup push.- Six channel blocks (
Tran/Vert/Long/Mic/LMic/SMic) on a 48-byte stride, against Series III's four. - The notes block is four fixed-width
[label:22][value:42]entries on a 64-byte stride, with different labels — not Series III'slabel: valuescan targets. - The geophone scale factor is
3.10308, exactly half Series III's6.206053, with an ADC of 10,000 counts per volt. - The monitoring flag is
0x0E, where Series III uses0x10— and it sits atSUB 0x1Cdata[12]. A second indicator lives atSUB 0x49data[11](0x02monitoring), which Series III has no equivalent of. - Setup files are enumerated with a
0x3F/0x40first/next walk. Series III has one config and nothing to enumerate. SUB 0x1Ccarries the device clock (day/month/year/h/m/s atdata[13:21]). Nothing else read so far reports the unit's own time.- There is a generic file transfer addressed by full path —
0x94/0x48read,0x8D/0x8Ewrite. Series III has nothing comparable; its config is reachable only through dedicated commands. - 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
printfformat 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:
- Retry limit is three — "three attempts and it's over".
ExpectedCommunicationsDetected()gates the session. If the host does not say something the unit recognises, the call is cancelled and rescheduled afterTimeBetweenRetries. A homebrew receiver must satisfy this check or units will retry forever — which is exactly the failure mode seen on BE12599.- 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. - 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) vsUSB 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/0x8Eare 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
.MMBblobs. They go through0xDA+ 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 0xDAis new and comes first — see below.0x71is 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
0x68and0x82actually 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 into0x68; unverified here. - The three header bytes at data
0x0000/0x0009/0x0013. - Scheduler / call-home writes.
callhome.MMBis a file too, so it may go through the same0xDA+ 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 forTEST1.mmbwritten back. Confirmed both ways.- The trailing
27 03 10sits at0x108in the write body and0x113in 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.
What the THOR manual settles about the ACH session (2026-09-24)
Source: manuals/723U0201 THOR Operator Manual Rev 08.pdf §6.2, §3.6.
⚠ This is vendor documentation, not observed bytes. It tells us the shape of
the session and the vocabulary; it does not give opcodes. Treated as a strong
prior, not as confirmed protocol.
🔑 A unit does NOT learn that an event was accepted — the server decides
This document previously listed, as the thing gating any homebrew receiver:
how a unit announces itself, and how it learns an event was accepted so it stops re-sending it.
That was the wrong question. There is no acknowledgement mechanism to discover, because the unit is not tracking what has been collected. Per §6.2.2.2 the ACH session is a list of server-chosen actions, and two of them are independent:
| ACH action | what it does |
|---|---|
| Copy events | downloads events — "Only applies to events not previously downloaded" |
| Copy monitor log | downloads the monitor log |
| Delete events and Logs from Unit | explicitly deletes them from the unit |
| Set Date/Time | unit synchronises its clock from the computer |
| Send Events / Schedule to Vision | Instantel cloud, not relevant to us |
"Not previously downloaded" is computer-side bookkeeping. The unit keeps its events until a server tells it to erase them, and the manual's own warning proves the two are decoupled:
⚠ "If you enable 'Delete Events and Logs from Unit' but disable 'Copy Events'. The events and logs will be deleted without being uploaded."
A server that never issues the delete simply re-reads the same events forever.
This is exactly the model our Series III ACH server already implements —
ach_state.json with downloaded_keys / max_downloaded_key, and erase as a
separate deliberate step. No new mechanism is needed for Series IV.
So a homebrew receiver needs: accept the connection, identify the unit, walk the
events (already solved — 0x08/0x1E/0x0A/0x5A), keep our own high-water
mark, and optionally erase. The erase opcodes are the only genuinely missing
piece, and they remain on the unsafe list.
The session is server-driven
Which matches the firmware's state machine and its
CMD_EXIT_CALL_HOME_DELETE_EVENTS_START_MONITORING /
Call Home Deleting Events strings: the unit dials in, then waits to be told
what to do, and returns to monitoring when the server is finished.
Scheduled ACH and event-triggered ACH behave differently (§3.6.1):
- Event-triggered (ACH enabled on the unit, an event occurred) — "Records events + transfer data, no stopping to monitor." Governed by Monitoring While Calling Home; with MWCH enabled "the monitor log will not be copied, events will not be deleted, and time will not synchronize".
- Scheduled — "Stop monitoring + transfer data (or delete events, or synchronize)." Monitoring stops and in-progress events complete first.
⚠ Worth noting for SFM: with MWCH on, an event-triggered session cannot delete or sync. A receiver that relies on erase to avoid re-reading will silently never erase on those units. Our high-water mark must be the primary mechanism, with erase as an optimisation — which is how Series III already does it.
Session Time Out is unit-side only
§6.2 is explicit that THOR configures ACH on both ends "with one exception; the
Session Time Out. This must be configured directly on the unit." That matches
the firmware's CallHome.SessionTimeout field, and means it lives in
callhome.MMB rather than anywhere THOR reaches — one more reason to read that
file with 0x94.
Unit identification is by serial number, with wildcards
§6.2.2.2: filters match against the serial number the unit presents, *
wildcards allowed, best-match wins — UM* all Micromates, MP* all Minimate
Pros, BE/BC Minimate Plus variants.
So the unit announces its serial early enough for the server to route on it.
SUB 0x15 returns the serial and is in Thor's standard preamble, which is
consistent, though the inbound direction has still never been observed.
⚠ The manual's worked example contradicts its own table — it says "To filter Micromate units type MP"* and "To filter Minimate Pro units type UM"*, which is backwards. The table is right. Noted so nobody copies the error.
Thor requires Idle for configuration
§6.2.1 step 3: "The Monitoring Mode must be 'Idle'." Independent confirmation that the greyed-out send observed on the bench is deliberate Thor policy, applied to ACH setup as well as compliance setup — and still not evidence the device refuses.
The schedule's actions, from the UI side
§3.6 lists exactly four actions THOR exposes: Start monitoring, Stop
monitoring, Self-check, Auto Call Home. The firmware has five — it also has
a setup-less DUTYCYCLE_START_MONITOR.
§3.6.2 step 8 confirms the coupling is Thor's own requirement: "Select the
appropriate Action and the Unit Setup. (A Unit Setup must exist…)" — while §3.6.2
step 1 notes "The unit will execute any actions in a schedule using its current
settings." The two statements sit a paragraph apart and pull in opposite
directions, which is consistent with START_MONITOR existing and THOR never
emitting it.
Also from §3.6: schedules are Day or Week ("Select Day… Select Week"), have a
Repeat Daily / Repeat Weekly flag, carry a Name, Description and Unit Type,
and "Saving a schedule will only store it on the computer, it must still be sent
to the unit and enabled" — which is why the capture shows 0x8E (send) and
0x47 (enable) as separate steps.
Those are the fields to look for when the schedule entry is finally decoded:
action, setup name, time, day-or-week, day selection, repeat. The one
captured entry (03 00 00 00 0f 03 02 …) has seven bytes before the name, which
is the right order of magnitude for that field list.
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 freshPOLLpreamble, 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
0x71payload 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:
- Enumerate the unit's setups with
0x3F/0x40— cheap, read-only, and it yields the exact names a schedule may legally reference. - Write only the schedule (
0x8D/0x8E) when the referenced setup is already present. - 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.
The schedule record format, from the firmware's own debug printf
SCHEDULER : _ReadRecord(%d) -> %s (Action=%u, 1/2h=%u, Day=%u, Setup="%s") [WDAY=%d]
That names the fields outright, and they fit the captured record:
| offset | field | read capture | write capture |
|---|---|---|---|
[0] |
Action | 03 |
03 |
[1:4] |
zero — padding, or Action is a uint32 |
00 00 00 |
00 00 00 |
[4] |
1/2h — half-hour slot | 0f = 15 |
0f = 15 |
[5] |
Day | 03 |
03 |
[6] |
unidentified (WDAY?) |
02 |
02 |
[7] |
name length | 0x28 = 40 |
0x09 = 9 |
[8:] |
Setup name | 40-char name | TEST1.mmb |
[264] |
trailer | 27 03 10 |
27 03 10 |
The length byte is what anchors this — it reads 40 for the 40-character name
and 9 for TEST1.mmb, in the same position, in both directions. The layout is
not a guess.
1/2h implies 30-minute resolution, 48 slots per day. Slot 15 would be
07:30 if counted from midnight — plausible but ⚠ unconfirmed, since the
schedule's actual time was not recorded alongside the capture. One question to
the operator settles it.
⚠ Still unknown, and all answerable with one schedule containing two entries:
- the record stride and how many records the file holds (one record was
captured;
27 03 10at[264]may be a trailer or a second record's head) - whether
Actionis one byte or a uint32 - the action code values —
03is presumablyDUTYCYCLE_START_MONITOR_WITH_SETUPbut the enum's base is unknown [6], and howDayandWDAYdivide the work
Six duty-cycle actions, not five
The full _PSA() dispatch — the scheduler's own action processor, reading records
via _ReadRecord:
DUTYCYCLE_START_MONITOR ← no setup
DUTYCYCLE_START_MONITOR_WITH_SETUP
DUTYCYCLE_START_MONITOR_WITH_SETUP_STOP_COMPLETE ← a third start variant
DUTYCYCLE_STOP_MONITOR
DUTYCYCLE_CALLHOME
DUTYCYCLE_SELF_CHECK
THOR exposes four. Two start variants it never offers, one of which needs no setup file at all.
_PSA() send ->> CMD_DUTYCYCLE_START_MONITOR shows the setup-less action is live
code that the scheduler genuinely dispatches, not a dead case — it sends a real
message. And since every one of these cases sits in the function that consumes
_ReadRecord's Action field, a schedule record can carry it.
An alternative explanation was checked and ruled out: the Micromate does have a
separate Timer Mode (MODE_TIMER, MODE_MONITOR_TIMER, Monitor Once Only,
under Special Setup), so START_MONITOR could have belonged to that path
instead. It does not — it is in _PSA(), the scheduler's dispatcher.
The scheduler enable lives in SysPref, not in the setup
SysPref.bMonitorScheduler = %s
_Task() CMD_SET_SCHEDULE - MonitorScheduler ENABLED
_Task() CMD_SET_SCHEDULE - ! MonitorScheduler NOT Enabled
This confirms the failed prediction above from the other side: the
Scheduler On/Off switch is a system preference, which is why the 0x71
compliance block was byte-identical when the scheduler was turned on. It was
never going to be in there.
It also suggests 0x47 — the two bare frames at the end of the capture — is a
SysPref get/set rather than anything scheduler-specific, which would explain its
params[7] selector and its 15-byte response shape being shared with 0x48's
page-0 descriptor. ⚠ Still a hypothesis; the disable/enable capture settles it.
The schedule has a setup-less start action — Thor just never uses it
The coupling above looked like it might be a workaround for the unit crashing on a missing setup. The firmware says otherwise: there are two distinct start-monitoring actions, and only one of them involves a setup file.
_PSA() case DUTYCYCLE_START_MONITOR
_PSA() case DUTYCYCLE_START_MONITOR_WITH_SETUP
The full duty-cycle action set, from the scheduler task's own dispatch strings:
| action | notes |
|---|---|
DUTYCYCLE_START_MONITOR |
no setup involved — uses whatever config is loaded |
DUTYCYCLE_START_MONITOR_WITH_SETUP |
the variant Thor always emits |
DUTYCYCLE_STOP_MONITOR |
|
DUTYCYCLE_CALLHOME |
send ==> CMD_SCHEDULE_CALL_HOME |
DUTYCYCLE_SELF_CHECK |
call DailySelfCheck() |
DUTYCYCLE_ON / OFF / NEXT |
scheduler state, not per-entry actions |
So the schedule↔config coupling is not a crash workaround — Thor picks the more
demanding of two available actions, every time. SFM can emit
START_MONITOR and skip the config entirely.
On whether a missing setup would break the unit: the firmware suggests
graceful degradation rather than a crash — Setup File Not Found exists, and so
does Invalid parameters reset to factory default - please review setup, which
is a deliberate fallback. ⚠ Untested, and not worth relying on until it is.
⚠ Hypothesis, not a finding: the schedule entry's leading byte may be the
action code — the one captured entry reads 03 00 00 00 0f 03 02 [namelen] [name], and 03 would fit START_MONITOR_WITH_SETUP in an enum of the above.
Unverified; there is one entry and nothing to diff against.
The capture that would settle it (more valuable than the 0x47
disable/enable test): build a schedule whose entry is stop monitoring or
call home — an action with no setup attached. That would (a) confirm or kill
the action-code hypothesis, since a setup-less action should change the leading
byte and drop the name, and (b) prove from the other direction that a schedule
can be sent with no config push at all.
Note also DUTYCYCLE_CALLHOME → CMD_SCHEDULE_CALL_HOME: a scheduled
call-home is a way to make a unit dial out on demand, which is the one
remaining lever on the unsolved call-home direction.
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:
- 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+0x1Amakes that cheap. - 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, acks0x69/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
0x10bytes inside request params need stuffing. (Write-frame data stuffing is now settled —10 XX→XX; see The write path.) -
The call-home session's bytes — the device-initiated direction has still not been observed at all, and cannot be over USB.
✅ The second half of this item is resolved (2026-09-24, from the THOR manual): "how it learns an event was accepted so it stops re-sending it" was the wrong question — it does not learn. Collection state is server-side bookkeeping and erase is a separate, optional, server-issued action. See What the THOR manual settles about the ACH session. What remains unknown is the framing of the inbound session and the erase opcodes, not any acknowledgement mechanism.
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.