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
|
||||
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
|
||||
|
||||
Nothing below has been sent to a unit, and nothing should be without an
|
||||
|
||||
Reference in New Issue
Block a user