Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
701af47170 | ||
|
|
02ed22f561 | ||
|
|
23cdbef737 | ||
|
|
71f19c90d1 |
@@ -197,7 +197,12 @@ 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]`:
|
||||
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 |
|
||||
|---|---|---|---|
|
||||
@@ -208,7 +213,7 @@ response tells you**, at `payload[9]`:
|
||||
| `0x2C` | call-home config | `0x7E` | `0x7C` ✗ **differs by 2** |
|
||||
| `0x08` | event index | `0x5A` | — |
|
||||
| `0x1E` | event header | `0x08` | — |
|
||||
| `0x1A` | compliance config | `0x2C` | — |
|
||||
| `0x1A` | compliance config | `0x082C` (2092) | — |
|
||||
| `0x0A` | waveform header | `0x00` | — (no event context) |
|
||||
| `0xFE` | full config | `0x00` | — (see note) |
|
||||
|
||||
@@ -524,6 +529,264 @@ 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`), but **no generic
|
||||
file-transfer command is exposed on the wire** — the only file-transfer string
|
||||
is `CMD_STOP_CALLHOME_FILETRANSFER`. So setups are unlikely to be pushed as
|
||||
raw `.MMB` blobs over the protocol.
|
||||
|
||||
### `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
|
||||
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.
|
||||
|
||||
### 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.
|
||||
|
||||
## ⚠ Untested and unsafe-until-agreed
|
||||
|
||||
Nothing below has been sent to a unit, and nothing should be without an
|
||||
@@ -538,8 +801,6 @@ explicit decision:
|
||||
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.**
|
||||
|
||||
Reference in New Issue
Block a user