Compare commits
4
Commits
45007e12d8
...
701af47170
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
701af47170 | ||
|
|
02ed22f561 | ||
|
|
23cdbef737 | ||
|
|
71f19c90d1 |
@@ -197,7 +197,12 @@ Series III reads.
|
|||||||
### The probe response carries the data length
|
### The probe response carries the data length
|
||||||
|
|
||||||
Series III hardcodes `DATA_LENGTHS` per SUB. On the Micromate the **probe
|
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 |
|
| 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** |
|
| `0x2C` | call-home config | `0x7E` | `0x7C` ✗ **differs by 2** |
|
||||||
| `0x08` | event index | `0x5A` | — |
|
| `0x08` | event index | `0x5A` | — |
|
||||||
| `0x1E` | event header | `0x08` | — |
|
| `0x1E` | event header | `0x08` | — |
|
||||||
| `0x1A` | compliance config | `0x2C` | — |
|
| `0x1A` | compliance config | `0x082C` (2092) | — |
|
||||||
| `0x0A` | waveform header | `0x00` | — (no event context) |
|
| `0x0A` | waveform header | `0x00` | — (no event context) |
|
||||||
| `0xFE` | full config | `0x00` | — (see note) |
|
| `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`)
|
we know the wire protocol is not it, and the flags byte (`0xC5` vs `0x03`)
|
||||||
gives a concrete anchor to search for.
|
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
|
## ⚠ 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
|
||||||
@@ -538,8 +801,6 @@ explicit decision:
|
|||||||
Also unknown:
|
Also unknown:
|
||||||
|
|
||||||
- Whether `0x10` bytes inside request params need stuffing
|
- 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
|
- Everything about the **call-home session** — the device-initiated direction
|
||||||
has not been observed at all. Specifically: how a unit announces itself,
|
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.**
|
and **how it learns an event was accepted so it stops re-sending it.**
|
||||||
|
|||||||
Reference in New Issue
Block a user