docs(series4): firmware static analysis, and all five bench events decoded
Solo session while the bench was unattended. Read-only throughout.
Architecture: ColdFire/68K, big-endian, Freescale MQX RTOS -- not ARM as the
vector table first suggested. The tell is 4E 5E 4E 75 4E 56 (UNLK A6 / RTS /
LINK A6) throughout both images, plus an MQX_OK assertion.
CB vs BD: a byte diff is useless (68% of bytes differ -- separately linked
builds, everything relocated). A string-set diff is position-independent and
shows 17,128 strings shared, with almost every "unique" string being the same
message at a different source line:
CB: MONITOR[3268]: STATUS_BATTERY_LOW
BD: MONITOR[3258]: STATUS_BATTERY_LOW
Consistently 10 lines apart across five different MONITOR messages, 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 printer brand) in
BD. This corroborates the bench A/B from the other direction: the split is a
tiny code delta, not two protocol stacks.
The SUB dispatch is a 68K switch jump table, so byte-pattern hunting will not
isolate the write opcodes -- that needs a disassembler.
Call-home config field names recovered from the firmware's own debug dump:
Enable, DialString, Retries, SessionTimeout, WaitForConnection, WarmupTime,
PowerSave -- seven fields for the 126-byte SUB 0x2C block. SessionTimeout and
PowerSave have no Series III equivalent, and Series III's scheduled-time fields
are absent, consistent with scheduling moving into the THOR-downloaded
scheduler. AT+CSQ is present, so the firmware speaks AT to the modem directly.
All five bench events downloaded and decoded over USB: each arrived at exactly
its declared size, every channel equal length, timestamps sequential.
Two gaps recorded:
- No content-based record-type discriminator. read_idf_file() dispatches on the
.IDFH/.IDFW filename suffix, which does not exist over the wire, and the
first 64 bytes of a histogram and a waveform are byte-identical. The protocol
supplies one instead: SUB 0x0A returns 0x1E for a histogram and 0x00 for a
waveform, so the type must be carried from the chain walk.
- The 0x0C peak float runs 2-5% above max(Tran,Vert,Long) and is not the vector
sum either. Its offset was inferred from a byte marker rather than
established, so it may not be the peak at all. Marked do-not-rely-on.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ru8Lg9HkkYvX9VWWo65SmL
This commit is contained in:
@@ -652,6 +652,112 @@ 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
|
round-tripped: read the active config, modify, write it back. The write half
|
||||||
is not yet attempted.
|
is not yet attempted.
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
### ⚠ Gap: no content-based record-type discriminator
|
||||||
|
|
||||||
|
`read_idf_file()` decides waveform vs histogram from the **filename suffix**
|
||||||
|
(`.IDFH` / `.IDFW`) — and there is no filename when downloading over the wire.
|
||||||
|
The first 64 bytes of a histogram and a waveform are **byte-identical**, so the
|
||||||
|
header does not obviously carry the type either.
|
||||||
|
|
||||||
|
The protocol does supply one: **`SUB 0x0A` returns length `0x1E` for a
|
||||||
|
histogram and `0x00` for a waveform** (see the event-chain section). Any
|
||||||
|
direct-download implementation must carry the type from the chain walk rather
|
||||||
|
than inferring it from the payload, or find the type field inside the IDF
|
||||||
|
header.
|
||||||
|
|
||||||
|
### ⚠ 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.
|
||||||
|
|
||||||
## ⚠ Untested and unsafe-until-agreed
|
## ⚠ Untested and unsafe-until-agreed
|
||||||
|
|
||||||
Nothing below has been sent to a unit, and nothing should be without an
|
Nothing below has been sent to a unit, and nothing should be without an
|
||||||
|
|||||||
Reference in New Issue
Block a user