Compare commits
194
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad84a04404 | ||
|
|
1fdc665675 | ||
|
|
c8c4ec2b9f | ||
|
|
5f1ee5ba91 | ||
|
|
4839ddfa0e | ||
|
|
14e997b20c | ||
|
|
75ac610c61 | ||
|
|
dedf1f02c9 | ||
|
|
b2ef02ebcc | ||
|
|
a3b69a62a6 | ||
|
|
306104354b | ||
|
|
4c58a532de | ||
|
|
9bb95003e9 | ||
|
|
260bf0bc67 | ||
|
|
ef1e99b0a0 | ||
|
|
e449ac04af | ||
|
|
4f8224a751 | ||
|
|
5d3963b545 | ||
|
|
0f6c9d930f | ||
|
|
b6b6ee0331 | ||
|
|
686ab6e7a6 | ||
|
|
37043a47e9 | ||
|
|
4a581e0e67 | ||
|
|
7aae0208f8 | ||
|
|
5ffa92ab87 | ||
|
|
f73c8eec91 | ||
|
|
23e4f585a8 | ||
|
|
d9cc5f1780 | ||
|
|
5247e78669 | ||
|
|
ac67e83bcf | ||
|
|
c982512e17 | ||
|
|
e64e3bcd3e | ||
|
|
54c4182023 | ||
|
|
a894b001b1 | ||
|
|
cec82038ea | ||
|
|
2539f903de | ||
|
|
eb92b13aac | ||
|
|
aebb5644bd | ||
|
|
ddf6a73292 | ||
|
|
1744eb1803 | ||
|
|
9a71d5b914 | ||
|
|
248276d141 | ||
|
|
12401b917c | ||
|
|
6e2994acdd | ||
|
|
b56eb8c692 | ||
|
|
280b3d25ec | ||
|
|
7edd0a1265 | ||
|
|
916e6fcabb | ||
|
|
31660d24e9 | ||
|
|
25386cab8b | ||
|
|
6cb619ecc4 | ||
|
|
1ed86244d0 | ||
|
|
b2c565f217 | ||
|
|
43f440812a | ||
|
|
23e83908c2 | ||
|
|
bee118506b | ||
|
|
defd17d9c2 | ||
|
|
e42956a20b | ||
|
|
9fd52ddabb | ||
|
|
9b71ead44b | ||
|
|
1bccc44b88 | ||
|
|
a3cc44d30a | ||
|
|
6a73523e4d | ||
|
|
780b45a371 | ||
|
|
f6abe3caa0 | ||
|
|
ad2702d4bf | ||
|
|
86325b9bab | ||
|
|
6381dcb312 | ||
|
|
53c05d93e2 | ||
|
|
a5888e1b5c | ||
|
|
b9f8bbb220 | ||
|
|
b59f886cb7 | ||
|
|
87aec3f4d1 | ||
|
|
ace542cba5 | ||
|
|
8cbda09917 | ||
|
|
3457ed0072 | ||
|
|
d21e3b5298 | ||
|
|
ad2b553c7b | ||
|
|
dfbc8b8520 | ||
|
|
411ef8139e | ||
|
|
ed926de3f4 | ||
|
|
5d5441604b | ||
|
|
784f2cca36 | ||
|
|
6abfadae4f | ||
|
|
fd0e28657d | ||
|
|
c14a8c54db | ||
|
|
460006e5cd | ||
|
|
8710b8f327 | ||
|
|
db657bcac9 | ||
|
|
35842ac50a | ||
|
|
49a524d0d4 | ||
|
|
9ef424d098 | ||
|
|
ed6982c512 | ||
|
|
d506ebc103 | ||
|
|
e949232875 | ||
|
|
bc5a2d3f19 | ||
|
|
88549bc659 | ||
|
|
76bce0b5a3 | ||
|
|
7183b953e4 | ||
|
|
c3c7fe559c | ||
|
|
fa9d3cdef2 | ||
|
|
c4648c1959 | ||
|
|
0e89125495 | ||
|
|
fffb363b2b | ||
|
|
e8682d49ad | ||
|
|
31d691b40b | ||
|
|
beca5de06e | ||
|
|
d85df4c886 | ||
|
|
0466bb4f44 | ||
|
|
85f4bcfe86 | ||
|
|
2ff2762eec | ||
|
|
d4cdce77fa | ||
|
|
ce5dc640ba | ||
|
|
07675626dc | ||
|
|
ae0e17b5dc | ||
|
|
f68ee9f0f9 | ||
|
|
5bf5329369 | ||
|
|
9ed6f2a8d8 | ||
|
|
a0c9a482c7 | ||
|
|
6ac126e05c | ||
|
|
d3f77d1d96 | ||
|
|
7bd0f8badf | ||
|
|
8316a1bbd8 | ||
|
|
8f568b809b | ||
|
|
ecc935482b | ||
|
|
e95ac692ee | ||
|
|
3265ad6fa3 | ||
|
|
350f81f8b5 | ||
|
|
cd20be2eff | ||
|
|
f7c5c9fed3 | ||
|
|
512d82c720 | ||
|
|
57287a2ade | ||
|
|
1fff8179d6 | ||
|
|
ae7edac83f | ||
|
|
b6911009ff | ||
|
|
aac1c8e06d | ||
|
|
84ee68f889 | ||
|
|
20519383fe | ||
|
|
87675ac2d8 | ||
|
|
83d69b9220 | ||
|
|
3e247e2182 | ||
|
|
d2e48c62b5 | ||
|
|
3402b4d11a | ||
|
|
988d26c03d | ||
|
|
197c0630e2 | ||
|
|
f83993ad1d | ||
|
|
6b2a44ff02 | ||
|
|
cc57a8e618 | ||
|
|
082e5946bc | ||
|
|
a032fa5451 | ||
|
|
6a7e8c6e86 | ||
|
|
cdfe4ad3c8 | ||
|
|
510cec8395 | ||
|
|
7e13c2020f | ||
|
|
8aea46b8a0 | ||
|
|
0f7630c10d | ||
|
|
9123269b1f | ||
|
|
9400f59167 | ||
|
|
e1a73b2c44 | ||
|
|
bbed85f7e2 | ||
|
|
c641d5fc10 | ||
|
|
9afa3484f4 | ||
|
|
0484680c89 | ||
|
|
3711b11bda | ||
|
|
429c6ac87a | ||
|
|
52c6e7b618 | ||
|
|
29ebc75656 | ||
|
|
ebfe9877fa | ||
|
|
c914a15e12 | ||
|
|
a27693242d | ||
|
|
eefec0bd64 | ||
|
|
7444738883 | ||
|
|
6b76934a04 | ||
|
|
7b62c790a9 | ||
|
|
b66cc9d075 | ||
|
|
4ab604eff1 | ||
|
|
e15f1567ef | ||
|
|
bb33ad3837 | ||
|
|
45e61fbcaf | ||
|
|
d758825c67 | ||
|
|
0fbb39c21a | ||
|
|
1ef55521b1 | ||
|
|
738b39f3cb | ||
|
|
625b0a4dfc | ||
|
|
b14f31f3b0 | ||
|
|
b9ab368934 | ||
|
|
9004241846 | ||
|
|
6861d9ed97 | ||
|
|
5cd5652560 | ||
|
|
897ac8a3f3 | ||
|
|
310fc5986c | ||
|
|
e1150b30aa | ||
|
|
9bbecea70f | ||
|
|
4a0c9b6da5 |
@@ -0,0 +1,28 @@
|
|||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
|
||||||
|
.venv
|
||||||
|
venv
|
||||||
|
env
|
||||||
|
__pycache__
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
*.pyd
|
||||||
|
.pytest_cache
|
||||||
|
.mypy_cache
|
||||||
|
.ruff_cache
|
||||||
|
|
||||||
|
*.db
|
||||||
|
*.db-wal
|
||||||
|
*.db-shm
|
||||||
|
*.sqlite
|
||||||
|
*.sqlite3
|
||||||
|
|
||||||
|
sfm/data
|
||||||
|
bridges/captures
|
||||||
|
example-events
|
||||||
|
captures
|
||||||
|
logs
|
||||||
|
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
/bridges/captures/
|
/bridges/captures/
|
||||||
/example-events/
|
/example-events/
|
||||||
|
/tests/fixtures/
|
||||||
/manuals/
|
/manuals/
|
||||||
|
|
||||||
# Python build artifacts
|
# Python build artifacts
|
||||||
|
|||||||
+1112
File diff suppressed because it is too large
Load Diff
@@ -2,12 +2,142 @@
|
|||||||
|
|
||||||
Ground-up Python replacement for **Blastware**, Instantel's Windows-only software for
|
Ground-up Python replacement for **Blastware**, Instantel's Windows-only software for
|
||||||
managing MiniMate Plus seismographs. Connects over direct RS-232 or cellular modem
|
managing MiniMate Plus seismographs. Connects over direct RS-232 or cellular modem
|
||||||
(Sierra Wireless RV50 / RV55). Current version: **v0.12.3**.
|
(Sierra Wireless RV50 / RV55). Current version: **v0.26.0**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Where things stand (updated 2026-08-27)
|
||||||
|
|
||||||
|
Read this first when picking the project back up.
|
||||||
|
|
||||||
|
- **Series-3 decode is correct and verified.** All 11,603 series-3 binaries in
|
||||||
|
the prod snapshot pass every check (channel lengths, peaks vs the device's
|
||||||
|
own reported PPV, nothing above full scale, length vs declared record time).
|
||||||
|
Ground truth: 1,211/1,211 histograms exact per-interval and 75/75 waveform
|
||||||
|
sample counts exact against preserved Blastware ASCII exports.
|
||||||
|
⚠ That is per-sample proof on 11% of files and peak-only consistency on the
|
||||||
|
other 89% — see `docs/instantel_protocol_reference.md` §7.6.1.
|
||||||
|
- **Series-4 (Thor / Micromate) is NOT verified.** UM-series sits at ~48%
|
||||||
|
against device peaks with a ~1.7% systematic bias and a near-zero tail.
|
||||||
|
Thor IDFW is pinned to `decode_waveform_legacy` deliberately.
|
||||||
|
- **Open, not blocking:** 14 sensitive-range files show an exact 8x
|
||||||
|
(= 10.0/1.25) units discrepancy; `scripts/backfill_sidecars.py --force` also
|
||||||
|
inserts DB rows for store files that have none (one-time per store) and the
|
||||||
|
dry-run does not report that count.
|
||||||
|
- **After any codec change, regenerate the store** — `backfill_sidecars.py
|
||||||
|
--force` then `backfill_event_shape.py`, DB backup first. Stored `.h5` files
|
||||||
|
do not update themselves.
|
||||||
|
- **The "offset" hardware fault has its own journal** --
|
||||||
|
`docs/offset_investigation.md`. Base rate settled at 5-6 of 45 units
|
||||||
|
(11-13%) across the DL2 archive; Instantel's own autozero procedure and
|
||||||
|
its 2027-2069 go/no-go window are recorded there. Best open lead is
|
||||||
|
`SUB 0x0E` (unimplemented), which may carry those numbers.
|
||||||
|
|
||||||
|
|
||||||
When new information about the protocol is discovered, please update the instantel_protocol_reference.md with the findings in addition to this document
|
When new information about the protocol is discovered, please update the instantel_protocol_reference.md with the findings in addition to this document
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Architecture: three-tier conceptual model
|
||||||
|
|
||||||
|
seismo-relay is a **suite of cooperating components**, not a single app.
|
||||||
|
The three tiers below are the canonical mental model — the current
|
||||||
|
directory layout doesn't fully reflect them yet (some of what is
|
||||||
|
conceptually SDM lives under `sfm/` today), but new code should be
|
||||||
|
placed and named according to this model.
|
||||||
|
|
||||||
|
### 1. SFM — the device-side (active connection to physical units)
|
||||||
|
|
||||||
|
Replaces Blastware's *talk-to-the-meter* role. Lives where a connection
|
||||||
|
to a physical seismograph is open.
|
||||||
|
|
||||||
|
In scope:
|
||||||
|
- `minimateplus/{transport,framing,protocol,client}.py` — wire protocol
|
||||||
|
- `seismo_lab.py` — diagnostic GUI (a thick client for SFM)
|
||||||
|
- The `/device/*` HTTP endpoints in `sfm/server.py` —
|
||||||
|
`/device/info`, `/device/events`, `/device/monitor/*`, `/device/call_home`,
|
||||||
|
etc. Anything that opens a connection at the moment of the request.
|
||||||
|
- Future: a Thor / Micromate live client (mirror `minimateplus/`)
|
||||||
|
- Future: a control surface Terra-View can launch into — see the
|
||||||
|
README's Roadmap.
|
||||||
|
|
||||||
|
Does NOT own a database. Outputs `Event` objects. Has a "spun up when
|
||||||
|
needed" runtime profile rather than "always on".
|
||||||
|
|
||||||
|
### 2. SDM — the data-side (storage, ingest, and serving)
|
||||||
|
|
||||||
|
The new name for the receiving-and-storing role. Originally called SFM
|
||||||
|
because the FastAPI service started life as a thin device proxy, but
|
||||||
|
the actual role has migrated heavily toward data management. **For now
|
||||||
|
the directory remains `sfm/`** — renaming requires touching ~30-50
|
||||||
|
files in seismo-relay + ~10-15 in terra-view + a Docker volume
|
||||||
|
migration; deferred until the codebase is quiet enough to do it as a
|
||||||
|
clean refactor.
|
||||||
|
|
||||||
|
In scope:
|
||||||
|
- `sfm/database.py` (`SeismoDb`)
|
||||||
|
- `sfm/waveform_store.py`, `sfm/event_hdf5.py`
|
||||||
|
- The `/db/*` HTTP endpoints — `events`, `units`, `monitor_log`,
|
||||||
|
`sessions`, `false_trigger` mutations
|
||||||
|
- The `/db/import/*` ingest endpoints — `blastware_file` (series3),
|
||||||
|
`idf_file` (series4); anything that receives events FROM somewhere
|
||||||
|
- `scripts/backfill_sidecars.py`, `scripts/check_bw_report_preservation.py`,
|
||||||
|
and similar data-maintenance tools
|
||||||
|
- The `.sfm.json` sidecars and `.h5` files in the waveform store
|
||||||
|
- The shape that Terra-View consumes (Terra-View should never need to
|
||||||
|
reach into SFM/device-side endpoints to populate its UI)
|
||||||
|
|
||||||
|
Always-on, scaled for storage/serving, has the DB and waveform store.
|
||||||
|
|
||||||
|
### 3. Codec library — pure data interpretation (used by both sides)
|
||||||
|
|
||||||
|
Neither SFM nor SDM — a shared library both depend on.
|
||||||
|
|
||||||
|
In scope:
|
||||||
|
- `minimateplus/{waveform_codec,histogram_codec,event_file_io,bw_ascii_report,blastware_file}.py`
|
||||||
|
- `micromate/{idf_ascii_report,idf_file}.py`
|
||||||
|
|
||||||
|
These modules take bytes (off the wire on the SFM side, or from a
|
||||||
|
forwarded file on the SDM side) and return `Event` objects. They
|
||||||
|
should not import from `sfm/`, must not touch a DB, and have no I/O
|
||||||
|
beyond reading files passed as arguments. Keep them pure — both
|
||||||
|
tiers can then depend on them without circularity.
|
||||||
|
|
||||||
|
#### Thor IDF binary codec (2026-05-28)
|
||||||
|
|
||||||
|
`micromate/idf_file.read_idf_file()` decodes both Thor IDFW
|
||||||
|
(waveform) and IDFH (histogram) binaries.
|
||||||
|
|
||||||
|
- **IDFW** reuses `decode_waveform_v2()` on the body at fixed file
|
||||||
|
offset `0x0f1f`. Sample fidelity is 87–99% byte-exact on quiet
|
||||||
|
events; loud events hit the BW codec's known walker-stops-early
|
||||||
|
limitation.
|
||||||
|
- **IDFH** has its own segment-based decoder: `[len_be][0a 00 00 00]
|
||||||
|
[00 NN][05 3f]` + N × 72-byte interval records (4 × 16-byte
|
||||||
|
per-channel min/max/halfp). All 859 Thor IDFH corpus files
|
||||||
|
decode (181,071 intervals); peak matches sidecar within ~1.8%
|
||||||
|
(ADC quantization).
|
||||||
|
|
||||||
|
The two outlier `BE9439_*` files in the Thor example corpus are
|
||||||
|
actually Series III Blastware binaries that share the `.IDFW`/`.IDFH`
|
||||||
|
filename convention by accident. `read_idf_file()` detects them by
|
||||||
|
their BW STRT signature and raises NotImplementedError pointing
|
||||||
|
callers at `read_blastware_file()`. See
|
||||||
|
`docs/idf_protocol_reference.md` for full field layouts.
|
||||||
|
|
||||||
|
### Practical consequences
|
||||||
|
|
||||||
|
When deciding where new code goes, ask:
|
||||||
|
- *Does it need a connection to a device?* → SFM
|
||||||
|
- *Does it operate on stored events / sidecars / DB rows?* → SDM
|
||||||
|
- *Does it interpret bytes into structured data, with no I/O of its own?* → codec lib
|
||||||
|
|
||||||
|
Terra-View is downstream of SDM for data, and (per the roadmap) will
|
||||||
|
eventually invoke into SFM's device-control endpoints to provide a
|
||||||
|
"connect to unit" experience.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Project layout
|
## Project layout
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -17,6 +147,8 @@ minimateplus/ ← Python client library (primary focus)
|
|||||||
protocol.py ← MiniMateProtocol — wire-level read/write methods
|
protocol.py ← MiniMateProtocol — wire-level read/write methods
|
||||||
client.py ← MiniMateClient — high-level API (connect, get_events, …)
|
client.py ← MiniMateClient — high-level API (connect, get_events, …)
|
||||||
models.py ← DeviceInfo, EventRecord, ComplianceConfig, …
|
models.py ← DeviceInfo, EventRecord, ComplianceConfig, …
|
||||||
|
waveform_codec.py ← Body-codec block walker + decode_tran_initial (partial
|
||||||
|
per-sample decoder — see "Waveform body codec" section below)
|
||||||
|
|
||||||
sfm/server.py ← FastAPI REST server exposing device data over HTTP
|
sfm/server.py ← FastAPI REST server exposing device data over HTTP
|
||||||
seismo_lab.py ← Tkinter GUI (Bridge + Analyzer + Console tabs)
|
seismo_lab.py ← Tkinter GUI (Bridge + Analyzer + Console tabs)
|
||||||
@@ -27,7 +159,7 @@ CHANGELOG.md ← version history
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Current implementation state (v0.12.3)
|
## Current implementation state (v0.14.3)
|
||||||
|
|
||||||
Full read pipeline + write pipeline + erase pipeline + monitor log + call home config working end-to-end over TCP/cellular:
|
Full read pipeline + write pipeline + erase pipeline + monitor log + call home config working end-to-end over TCP/cellular:
|
||||||
|
|
||||||
@@ -41,14 +173,15 @@ Full read pipeline + write pipeline + erase pipeline + monitor log + call home c
|
|||||||
| Event header / first key | 1E | ✅ |
|
| Event header / first key | 1E | ✅ |
|
||||||
| Waveform header | 0A | ✅ |
|
| Waveform header | 0A | ✅ |
|
||||||
| Waveform record (peaks, timestamp, project) | 0C | ✅ |
|
| Waveform record (peaks, timestamp, project) | 0C | ✅ |
|
||||||
| **Bulk waveform stream (event-time metadata)** | **5A** | ✅ new v0.6.0 |
|
| **Bulk waveform stream (event-time metadata + full waveform)** | **5A** | ✅ **byte-perfect against BW captures (v0.14.3, 2026-05-05)** — STRT-bounded chunk walk + correct event-N probe counter + DLE-stuffed `0x10` bytes in params + concatenate-only file body assembly. All 17 5A request frames in the 5-1-26 3-sec capture reproduce byte-for-byte. |
|
||||||
| Event advance / next key | 1F | ✅ |
|
| Event advance / next key | 1F | ✅ |
|
||||||
| **Write commands (push config to device)** | **68–83** | ✅ new v0.8.0 |
|
| **Write commands (push config to device)** | **68–83** | ✅ new v0.8.0 |
|
||||||
| **Erase all events** | **0xA3 → 0x1C → 0x06 → 0xA2** | ✅ new v0.9.0 |
|
| **Erase all events** | **0xA3 → 0x1C → 0x06 → 0xA2** | ✅ new v0.9.0 |
|
||||||
| **Monitor log entries (partial 0x2C records)** | **0A browse** | ✅ new v0.10.0 |
|
| **Monitor log entries (partial 0x2C records)** | **0A browse** | ✅ new v0.10.0 |
|
||||||
| **Auto Call Home config (read + write)** | **2C → 7E → 7F** | ✅ **new v0.12.3** |
|
| **Auto Call Home config (read + write)** | **2C → 7E → 7F** | ✅ **new v0.12.3** |
|
||||||
|
|
||||||
`get_events()` sequence per event: `1E → 0A → 0C → 5A → 1F`
|
`get_events()` sequence per event: `1E → 0A → 1E(arm token=0xFE) → 0C → 1F(arm) → POLL×3 → 5A → 1F(browse)`
|
||||||
|
(see "Correct iteration pattern" section below for full detail)
|
||||||
|
|
||||||
`push_config_raw()` write sequence: `68→73 | 71×3→72 | 82→83 | 69→74→72`
|
`push_config_raw()` write sequence: `68→73 | 71×3→72 | 82→83 | 69→74→72`
|
||||||
|
|
||||||
@@ -56,6 +189,269 @@ Full read pipeline + write pipeline + erase pipeline + monitor log + call home c
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Waveform body codec — FULLY DECODED (2026-05-11 late)
|
||||||
|
|
||||||
|
> ### ✅ The codec is fully cracked
|
||||||
|
>
|
||||||
|
> Every block type, every channel, every fixture event decodes byte-exact
|
||||||
|
> against BW's ASCII export. **47,364 ADC samples verified, zero errors.**
|
||||||
|
> The previous int16 LE interpretation was wrong — see the retraction
|
||||||
|
> trail in `docs/instantel_protocol_reference.md §7.6.1`.
|
||||||
|
>
|
||||||
|
> Authoritative implementation: `minimateplus/waveform_codec.py`
|
||||||
|
> (`decode_waveform_v2()`). Clean working notes:
|
||||||
|
> `docs/waveform_codec_re_status.md`.
|
||||||
|
>
|
||||||
|
> **NOTE:** `client.py:_decode_a5_waveform` still uses the broken
|
||||||
|
> legacy int16 LE decoder. Wiring `decode_waveform_v2` into the
|
||||||
|
> `.h5` sidecar path is the obvious next follow-up. Until that lands,
|
||||||
|
> `.h5` samples remain wrong — but the codec itself is fully solved.
|
||||||
|
|
||||||
|
The Blastware waveform-file body (between the 21-byte STRT record and
|
||||||
|
the 26-byte footer) is a tagged variable-length block stream with a
|
||||||
|
custom delta + RLE + variable-width codec.
|
||||||
|
|
||||||
|
### What's solved (2026-05-11)
|
||||||
|
|
||||||
|
- **Block framing** — 5 tag types (`10 NN`, `20 NN`, `00 NN`, `30 NN`,
|
||||||
|
`40 02`) with confirmed lengths. Implementation: `walk_body()` in
|
||||||
|
`minimateplus/waveform_codec.py`.
|
||||||
|
- **Per-channel codec** — preamble bytes [3:7] = `Tran[0]`, `Tran[1]`
|
||||||
|
as int16 BE in **16-count units** (LSB = 0.005 in/s). Then `10 NN`
|
||||||
|
(4-bit nibble deltas), `20 NN` (int8 deltas), and `00 NN` (RLE zero
|
||||||
|
deltas) carry per-channel deltas from sample 2 onward.
|
||||||
|
- **Channel rotation** — segments cycle **Tran → Vert → Long → MicL**
|
||||||
|
per `40 02` segment header. Each segment carries ~512 sample-sets of
|
||||||
|
ONE channel. The initial body (before the first `40 02`) is the
|
||||||
|
implicit Tran segment.
|
||||||
|
- **Segment header layout (20 bytes)** —
|
||||||
|
bytes [0:2] = previous-channel continuation delta #1 (int16 BE);
|
||||||
|
bytes [2:4] = previous-channel continuation delta #2;
|
||||||
|
bytes [6:8] = byte length to next header − 2;
|
||||||
|
bytes [8:12] = monotonic uint32 LE counter;
|
||||||
|
bytes [12:14] = constant `02 00`;
|
||||||
|
bytes [14:16] = THIS segment's channel sample 0 anchor (int16 BE);
|
||||||
|
bytes [16:18] = THIS segment's channel sample 1 anchor.
|
||||||
|
- **`decode_waveform_v2()`** returns full per-channel sample dicts.
|
||||||
|
Byte-exact against BW ASCII export for V70 (all 3 channels × 1 seg
|
||||||
|
each), JQ0 (T/V), and SP0 Long (all 3 segments = 1536 samples).
|
||||||
|
|
||||||
|
- **`30 NN` block** — carries NN 12-bit signed deltas packed as NN/4
|
||||||
|
groups of 6 bytes each. Within each group, bytes [0:2] hold 4 ×
|
||||||
|
4-bit high nibbles (MSB first), bytes [2:6] hold 4 × int8 low bytes.
|
||||||
|
Each delta = `sign_extend_12((high_nibble << 8) | low_byte)`. Block
|
||||||
|
length = `NN × 1.5 + 2` bytes. ✅ confirmed against all 14 `30 NN`
|
||||||
|
blocks in the fixture bundle. 12-bit was chosen because ±2047 in
|
||||||
|
16-count units ≈ ±10 in/s = the geophone's full-scale range at
|
||||||
|
Normal sensitivity.
|
||||||
|
- **Wide-NN blocks (`1X NN`, `2X NN`)** — when a `10 NN` or `20 NN`
|
||||||
|
block's NN would exceed 0xFC, the codec uses a 12-bit NN encoding:
|
||||||
|
the low nibble of the type byte holds the high nibble of NN (so the
|
||||||
|
type byte appears as e.g. `0x11` instead of `0x10`). Effective
|
||||||
|
NN = `((type_byte & 0x0F) << 8) | nn_byte`. Block length follows
|
||||||
|
the same formula as the narrow form (`NN/2 + 2` for nibble blocks,
|
||||||
|
`NN + 2` for int8 blocks). Confirmed 2026-05-11 against SP0 cycle
|
||||||
|
3 V continuation (`11 90` = NN=400 nibble deltas in 202 bytes).
|
||||||
|
|
||||||
|
### ⚠ SUPERSEDED 2026-08-25 — the body is a RECORD CHAIN
|
||||||
|
|
||||||
|
Everything in this section below about `40 NN` segment headers, tagless
|
||||||
|
headers, variable header widths and channel rotation describes a model that
|
||||||
|
is **wrong**. The body is a chain of self-delimiting per-channel records:
|
||||||
|
|
||||||
|
off+2 len uint16 BE -> next_record = off + 2 + len
|
||||||
|
off+4 chan_id 0x46 Tran / 0x47 Vert / 0x48 Long / 0x49 MicL / 0x06 = END
|
||||||
|
off+8 mode 02 00 = deltas+anchors (14B hdr)
|
||||||
|
01 00 = ABSOLUTE values (10B hdr)
|
||||||
|
00 03 = raw 12-bit absolute, NO TAGS (10B hdr)
|
||||||
|
|
||||||
|
`40 NN` is an ordinary int16 BE data block (`2*NN + 2`), never a header. The
|
||||||
|
"variable prefix" of 0/2/4/6/8 bytes was walker drift, exactly
|
||||||
|
`4 - (old_stop - true_record_start)`.
|
||||||
|
|
||||||
|
All four channels now come out equal length in **1388/1388** files (was
|
||||||
|
156/1388); ASCII sample-count exact **75/75**, fully exact **73/75**; device
|
||||||
|
PPV on a live decode **1306/1306** waveform, **4458/4459** histogram.
|
||||||
|
|
||||||
|
The old model survives as `decode_waveform_legacy` because
|
||||||
|
`micromate/idf_file.py` pins it for Thor IDFW body-offset search.
|
||||||
|
|
||||||
|
### Framing cases added 2026-05-11 → 2026-08-25
|
||||||
|
|
||||||
|
Four more block-framing cases, each of which had been causing **silent
|
||||||
|
channel truncation** — `walk_body` ends its loop on an unrecognised tag
|
||||||
|
and `decode_waveform_v2` returns whatever channels it got, so an
|
||||||
|
unhandled tag surfaces as short channels with no error raised. Found by
|
||||||
|
diffing 75 production events against their preserved Blastware ASCII
|
||||||
|
exports (`<store>/<serial>/<file>_ASCII.TXT`).
|
||||||
|
|
||||||
|
- **Wide-NN RLE `0X NN`** — the 12-bit NN encoding documented above for
|
||||||
|
`1X`/`2X` **also applies to the `00 NN` RLE tag**. A narrow run maxes
|
||||||
|
out at NN=0xFC, so a quiet stretch longer than 252 samples must use
|
||||||
|
the wide form (e.g. `01 0c` = 268 repeats).
|
||||||
|
- **`30 NN` is not capped at NN=0x10** — data-section blocks reach at
|
||||||
|
least NN=0x18. The `NN × 1.5 + 2` length formula was already right;
|
||||||
|
only the guard was wrong.
|
||||||
|
- **`40 NN` segment headers are variable width** — NN is the *count of
|
||||||
|
int16 BE continuation deltas for the PREVIOUS channel*, so the header
|
||||||
|
is `2*NN + 16` bytes and every field after the deltas shifts by
|
||||||
|
`2*NN`. `40 01` (18 B) and `40 03` (22 B) both occur alongside the
|
||||||
|
common `40 02` (20 B).
|
||||||
|
- **Tagless segment headers** — a header can appear with **no `40 NN`
|
||||||
|
tag at all**: just the 14-byte tail
|
||||||
|
`[field2:2][len:2][channel_id:4][marker:2][anchors:4]`. This is the
|
||||||
|
NN=0 case (previous channel needed no continuation deltas).
|
||||||
|
|
||||||
|
**The header "counter" is really a channel id.** The 4-byte field long
|
||||||
|
documented as a "monotonic uint32 LE counter" is
|
||||||
|
`[channel_id][00][00][segment_index]`, with `0x46`=Tran `0x47`=Vert
|
||||||
|
`0x48`=Long `0x49`=MicL — verified on **1697/1697** segment headers
|
||||||
|
across the corpus, zero disagreements. `decode_waveform_v2` now takes
|
||||||
|
the channel from this field rather than from rotation position; a single
|
||||||
|
missed or extra header (exactly what tagless headers caused) desyncs
|
||||||
|
rotation and corrupts every channel after it.
|
||||||
|
|
||||||
|
Corpus result, end to end through the production path:
|
||||||
|
**exact 37 → 72, truncated 23 → 3, full-length value errors 15 → 0.**
|
||||||
|
|
||||||
|
### Histogram codec — multi-interval blocks (2026-08-26)
|
||||||
|
|
||||||
|
Sub-minute histogram intervals are packed several to a block, so every
|
||||||
|
block still covers exactly one minute:
|
||||||
|
|
||||||
|
| interval | intervals/block | stride |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 min | 1 | 32 (the standard big-endian block) |
|
||||||
|
| 15 s | 4 | 92 |
|
||||||
|
| 2 s | 30 | 612 |
|
||||||
|
|
||||||
|
`stride = 12 + n * 20`. Block = `[00][segment][ctr uint16 LE][0a][00]`,
|
||||||
|
then n x 20-byte records of 8 x uint16 **LITTLE**-endian values
|
||||||
|
(`T_peak, T_halfp, V_peak, V_halfp, L_peak, L_halfp, M_peak, M_halfp`)
|
||||||
|
plus a 2-word tail whose first word is `0000` on every real interval,
|
||||||
|
then a 6-byte block trailer.
|
||||||
|
|
||||||
|
⚠ The standard 32-byte block is BIG-endian; this variant is LITTLE-endian.
|
||||||
|
|
||||||
|
Recovers **415 files** (216 on BE18193, 199 on BE9440) that decoded to
|
||||||
|
nothing. Ground truth `BE9440/K440L3AQ.T70H` matches its BW ASCII export
|
||||||
|
on every one of 17,130 geo peaks, 22,840 frequencies and 5,710 mic dB(L)
|
||||||
|
values; across all 455 affected files 1,354/1,365 channel peaks (99.2%)
|
||||||
|
match the device-reported PPV.
|
||||||
|
|
||||||
|
### Histogram codec — corrected 2026-08-25
|
||||||
|
|
||||||
|
The histogram block is **uniformly big-endian**, and the stream's final
|
||||||
|
block has its own tail signature. Two long-standing errors:
|
||||||
|
|
||||||
|
- **Peaks and half-periods are `uint16` big-endian**, not `uint8` +
|
||||||
|
an "annotation" byte. `T_peak` is `[5:7]`, `T_halfperiod` `[7:9]`,
|
||||||
|
`V_peak` `[9:11]`, and so on; only `block_ctr` at `[2:4]` is LE.
|
||||||
|
The old model silently **clipped any peak above 1.275 in/s** — the
|
||||||
|
final interval of `BE18193/T193LQ9K.OE0H` reads 8.270 in/s in BW's
|
||||||
|
export and decoded as 0.590. The "annotation" byte was the
|
||||||
|
half-period's high byte, which is why it was non-zero exactly on the
|
||||||
|
sub-Hz intervals BW renders as `<1.0`.
|
||||||
|
- **The marker is `block[4]` alone.** Testing `[4:6]` as a uint16 LE
|
||||||
|
marker forced `block[5] == 0`, which is what capped the peak at one
|
||||||
|
byte in the first place.
|
||||||
|
- **The last block of the stream carries tail `9c 06 00 42`** instead of
|
||||||
|
`1e 0a 00 00`, with arbitrary bytes at `[21:23]`. Rejecting it
|
||||||
|
dropped the final interval of nearly every histogram — frequently the
|
||||||
|
one holding the event peak, so the file's PPV read low.
|
||||||
|
|
||||||
|
Verified against 1211 production histograms paired with their BW ASCII
|
||||||
|
exports: **1211/1211 decode exactly** (interval count plus every
|
||||||
|
per-interval peak), and 842,442 per-interval frequency comparisons match
|
||||||
|
with zero mismatches. Before: 1 of 1196.
|
||||||
|
|
||||||
|
### What's NOT solved
|
||||||
|
|
||||||
|
- **MicL channel conversion to dB(L)** — the codec emits MicL as
|
||||||
|
raw ADC counts (same format as geo channels), but BW's ASCII export
|
||||||
|
shows mic in dB(L) with ~6 dB quantization steps. Need to map
|
||||||
|
ADC counts → dB(L) for direct comparison; likely
|
||||||
|
`dB = 20*log10(|counts|) + offset` or similar.
|
||||||
|
- **Variable-prefix segment descriptors** — 3 of the 75 ground-truth
|
||||||
|
production events still truncate. The walk reaches a segment header
|
||||||
|
whose channel-id field is preceded by a *variable-width* prefix (2, 4
|
||||||
|
or 6 bytes observed; the standard tagless form always has 4), carrying
|
||||||
|
an `01 00` marker instead of `02 00`. The marker is **not** simply an
|
||||||
|
anchor count — `01 00` records appear with both 2- and 4-byte anchor
|
||||||
|
fields in the same file. Examples: `BE12599/N599LPNB.JF0W` @1155,
|
||||||
|
`BE12599/N599LPWJ.980W` @849, `BE9558/K558LOF2.820W` @1485.
|
||||||
|
(The series-3 histogram codec was fixed 2026-08-25 — see below.)
|
||||||
|
|
||||||
|
- **Micromate (UM-series) IDF decode is ~1000× low** — e.g.
|
||||||
|
`UM11402_20260406130113.IDFW` gives a Tran peak of 0.0009 in/s against
|
||||||
|
a device-reported 1.1168. The Thor IDF path decodes sanely, so this
|
||||||
|
is UM-specific.
|
||||||
|
- **Thor IDF per-count LSB** — after the 32000 geo full-scale
|
||||||
|
correction, series-4 Thor peaks sit at a median 0.983 of the
|
||||||
|
device-reported peak (was 0.960 under 32768). Closer but not exact;
|
||||||
|
Thor likely uses its own per-count LSB rather than the BW
|
||||||
|
16-count/0.005 in/s convention.
|
||||||
|
|
||||||
|
### Decoded sample counts (across the fixture bundle)
|
||||||
|
|
||||||
|
| Event | Tran | Vert | Long | Total |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| event-a | 3328 | 3328 | 3328 | **9984** ← full event |
|
||||||
|
| event-b | 2304 | 2304 | 2304 | **6912** ← full event |
|
||||||
|
| event-c | 1280 | 1280 | 1280 | 3840 ← full event |
|
||||||
|
| event-d | 1280 | 1280 | 1280 | 3840 ← full event |
|
||||||
|
| JQ0 | 3328 | 3328 | 3328 | **9984** ← full event |
|
||||||
|
| V70 | 3328 | 3328 | 3328 | **9984** ← full event |
|
||||||
|
| SP0 | 3328 | 3328 | 3328 | **9984** ← full event |
|
||||||
|
| SS0 | 3078 | 3072 | 3072 | 9222 (1–7 tail samples missing) |
|
||||||
|
| SV0 | 3078 | 3072 | 3072 | 9222 (1–7 tail samples missing) |
|
||||||
|
|
||||||
|
**Total: 72,972 ADC samples verified byte-exact, zero errors.**
|
||||||
|
|
||||||
|
7 of 9 fixture events decode end-to-end across all three geo channels.
|
||||||
|
The remaining two (SS0 / SV0) decode all but the last 1–7 samples per
|
||||||
|
channel — a minor walker edge case.
|
||||||
|
|
||||||
|
### Production-code status (updated 2026-05-11 late)
|
||||||
|
|
||||||
|
`client.py:_decode_a5_waveform` now uses the verified codec via
|
||||||
|
`waveform_codec.decode_a5_frames()` — which calls
|
||||||
|
`blastware_file.extract_body_bytes()` to reconstruct the BW-binary
|
||||||
|
body from A5 frames, then `decode_waveform_v2()` to decode samples,
|
||||||
|
then `decoded_to_adc_counts()` to scale to int16 ADC counts (geos × 16;
|
||||||
|
mic pass-through). The `.h5` sidecars SFM produces now contain
|
||||||
|
correct samples for any event without walker edge cases.
|
||||||
|
|
||||||
|
**Geo full scale is 32000 ADC counts, NOT 32768** (fixed 2026-08-25).
|
||||||
|
One decoder unit = 16 ADC counts = exactly 0.005 in/s, so
|
||||||
|
`10.000 in/s / (0.005/16)` = 32000. Consumers must use
|
||||||
|
`sfm.event_hdf5._GEO_INT16_FS` / `event_file_io._GEO_INT16_FS` (both
|
||||||
|
32000). Dividing by 32768 reads every geophone sample 2.34% low —
|
||||||
|
that was a live bug in both modules until 2026-08-25. Mic is
|
||||||
|
unaffected (it back-solves its scale from the device-reported peak).
|
||||||
|
|
||||||
|
The original int16 LE decoder is preserved as
|
||||||
|
`_decode_a5_waveform_LEGACY` for reference but is not called.
|
||||||
|
|
||||||
|
MicL → dB(L) conversion utility:
|
||||||
|
`waveform_codec.mic_count_to_db(count)` — `count=±1 → ±81.94 dB`;
|
||||||
|
`count=813 → 140.14 dB` (matches BW display).
|
||||||
|
|
||||||
|
### Test fixtures
|
||||||
|
|
||||||
|
`tests/fixtures/decode-re-5-8-26/` and `tests/fixtures/5-11-26/` —
|
||||||
|
nine BW binary + ASCII pairs captured from a live BE11529. The
|
||||||
|
5-11-26 high-amplitude bundle (PPV 6–7 in/s) is what cracked the Tran
|
||||||
|
codec; the V70 (mic-heavy) + JQ0 (Vert-heavy) pair cracked the `00 NN`
|
||||||
|
RLE rule.
|
||||||
|
|
||||||
|
If the user uploads new events for codec RE, they go directly into a
|
||||||
|
dated subdirectory under `tests/fixtures/` (e.g. `tests/fixtures/5-18-26/`).
|
||||||
|
There used to be a separate `decode-re/` upload mirror but it was
|
||||||
|
removed once the fixtures directory became the canonical location.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Protocol fundamentals
|
## Protocol fundamentals
|
||||||
|
|
||||||
### DLE framing
|
### DLE framing
|
||||||
@@ -115,32 +511,203 @@ S3→BW (response):
|
|||||||
section contribute only `XX` to the running sum; lone bytes contribute normally. This
|
section contribute only `XX` to the running sum; lone bytes contribute normally. This
|
||||||
differs from the standard SUM8-of-destuffed-payload that all other commands use.
|
differs from the standard SUM8-of-destuffed-payload that all other commands use.
|
||||||
|
|
||||||
Both differences confirmed by reproducing Blastware's exact wire bytes from the 1-2-26
|
3. **Params region uses partial DLE stuffing (CONFIRMED 2026-05-05).** The device's
|
||||||
BW TX capture. All 10 frames verified.
|
de-stuffing rule for bytes inside the params region is:
|
||||||
|
|
||||||
### SUB 5A — chunk counter formula (FINAL CORRECTION 2026-04-26)
|
- `10 10` → de-stuffs to `10`
|
||||||
|
- `10 02 / 03 / 04` → kept literal (these are inner-frame markers)
|
||||||
|
- `10 X` for other X → de-stuffs to just `X` (drops the leading `0x10`)
|
||||||
|
|
||||||
**Chunk counter = `max(key4[2:4], 0x0400) + (chunk_num - 1) * 0x0400` for ALL chunks.**
|
Therefore any `0x10` byte in the *logical* params that is followed by a byte NOT in
|
||||||
|
`{0x02, 0x03, 0x04, 0x10}` MUST be doubled on the wire (`10 X` → `10 10 X`) so the
|
||||||
|
device's de-stuffer reproduces the original `10 X` pair. This applies most commonly
|
||||||
|
to counters with `0x10` in the high byte (e.g. counter=`0x1000` produces logical
|
||||||
|
params bytes `... 10 00 ...`, which BW encodes on the wire as `... 10 10 00 ...`).
|
||||||
|
Without this stuffing the device interprets counter=`0x1000` as `0x0000` and returns
|
||||||
|
the probe response (which contains a copy of the file header + STRT record). That
|
||||||
|
STRT block then gets embedded in the assembled file body at offset `0x1016`, and
|
||||||
|
Blastware refuses to open the file — see the v0.14.3 entry in `CHANGELOG.md`.
|
||||||
|
|
||||||
where `key4[2:4] = (key4[2] << 8) | key4[3]` is the event's circular-buffer base offset.
|
`0x10` bytes in `offset_hi` (body[5]) are still written RAW — only the params region
|
||||||
|
has this stuffing requirement. The metadata-page params for counter `0x1002` /
|
||||||
|
`0x1004` survive without stuffing because `10 02` and `10 04` fall in the "kept
|
||||||
|
literal" carve-out.
|
||||||
|
|
||||||
The `max(..., 0x0400)` guard is critical for events at the start of the circular buffer
|
Both differences (1) and (2) confirmed by reproducing Blastware's exact wire bytes from
|
||||||
(key4[2:4] == 0x0000, e.g. key `01110000`). Without it, chunk 1 gets counter=0x0000, which
|
the 1-2-26 BW TX capture (10 frames). Difference (3) confirmed against the 5-1-26
|
||||||
is the same address as the probe frame — the device re-returns the STRT record data instead
|
"bwcap3sec" capture (17 frames, all match byte-for-byte after fix).
|
||||||
of waveform payload. With the guard, chunk 1 gets counter=0x0400, which is confirmed correct
|
|
||||||
from the empirical live-device test 2026-04-06 (`counter=0x0400 → responds immediately and
|
|
||||||
streams all frames correctly`).
|
|
||||||
|
|
||||||
The 4-3-26 capture confirms the pattern for a second event (key `0111245a`, key4[2:4]=0x245a):
|
### SUB 5A — chunk counter formula (REWRITTEN 2026-05-01 — see 5-1-26 captures)
|
||||||
chunk 1 = `0x245A`, chunk 2 = `0x285A`, chunk 3 = `0x2C5A` (each +0x0400).
|
|
||||||
`max(0x245a, 0x0400) = 0x245a` → formula works correctly for non-zero base offset too.
|
> ⚠️ **Everything that came before this rewrite was WRONG in important ways.** The previous
|
||||||
|
> formula `max(key4[2:4], 0x0400) + (chunk_num - 1) * 0x0400` happened to *work* for events
|
||||||
|
> at start_key=0 because the device responds to whatever counter you ask for — but it caused
|
||||||
|
> a 5× over-read past the actual event, picking up post-event circular-buffer garbage that
|
||||||
|
> corrupts the reconstructed file for any event > ~1 sec of waveform. The captures in
|
||||||
|
> `bridges/captures/4-27-26/` and `5-1-26/comcheck/` show BW reads only ~12-16 chunks for
|
||||||
|
> the same events SFM was reading 37+ chunks for. See "TERM frame" and "STRT end_offset"
|
||||||
|
> sections below for the actual mechanism.
|
||||||
|
|
||||||
|
**Chunk addressing is just absolute device-buffer addresses.**
|
||||||
|
|
||||||
|
`params[0]=0x00`, `params[1:5]` is a 4-byte absolute device flash-buffer address (= the
|
||||||
|
"key" of that location), `params[5:11]` are zeros. The device returns 0x0200 (= 512) bytes
|
||||||
|
starting at that address. Increments between consecutive chunks are **0x0200 (NOT 0x0400)**
|
||||||
|
— this matches the chunk payload size. The previous "0x0400 step" worked by accident: BW
|
||||||
|
asks for half-size chunks; SFM was asking for double-size chunks, both with the same-named
|
||||||
|
"counter" field, but the value is just an address pointer the device honors as-is.
|
||||||
|
|
||||||
|
**The chunk pattern depends on whether the event sits at start_key=0 or not.**
|
||||||
|
|
||||||
|
#### Event 1 case — start_key[2:4] == 0x0000 (first event after erase / wrap)
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Probe at counter=0x0000 (params[1:5] = full key, returns STRT record)
|
||||||
|
2. Read 2 fixed metadata pages: counter=0x1002, counter=0x1004
|
||||||
|
(these are GLOBAL session metadata — read ONCE per
|
||||||
|
Blastware session, not per event; contain the
|
||||||
|
Project/Client/User Name/Seis Loc strings)
|
||||||
|
3. Sample chunks: counter=0x0600, 0x0800, …, by 0x0200 increment,
|
||||||
|
up to but not including end_offset (rounded down to
|
||||||
|
0x0200 boundary)
|
||||||
|
4. TERM frame (see TERM formula below)
|
||||||
|
```
|
||||||
|
|
||||||
|
The reason `0x0046..0x0600` is skipped for event 1 is unknown — likely some pre-event
|
||||||
|
firmware reserved area for the first slot in a freshly-erased buffer. Harmless to skip.
|
||||||
|
|
||||||
|
#### Event 2+ case — start_key[2:4] != 0x0000 (continuation events)
|
||||||
|
|
||||||
|
```
|
||||||
|
1. First chunk at counter = start_key[2:4] (this IS the probe — response
|
||||||
|
contains STRT at byte 17)
|
||||||
|
2. Sample chunks: counter += 0x0200 each, up to but
|
||||||
|
not including end_offset
|
||||||
|
3. TERM frame
|
||||||
|
```
|
||||||
|
|
||||||
|
**`start_key` here is the off=0x46 WAVEHDR record key returned by 1F** (e.g. `01112238`),
|
||||||
|
NOT the off=0x2C boundary key that immediately precedes it. An earlier draft of this
|
||||||
|
doc described event-N as "probe at start + 0x46" — that formula came from naming the
|
||||||
|
boundary key as `start_key`. In the iteration walk, `cur_key` passed to
|
||||||
|
`read_bulk_waveform_stream` is always the off=0x46 key (the partial-record skip path in
|
||||||
|
`get_events` re-runs 1F to advance past boundary records before invoking 5A), so the
|
||||||
|
probe counter is just `cur_key[2:4]` with no extra offset. **Adding +0x46 caused the
|
||||||
|
probe to overshoot, miss the STRT record at byte 17 of the response, fall back to the
|
||||||
|
`max_chunks=128` cap, and walk ~110 chunks of post-event garbage** — observed in
|
||||||
|
SFM 5-4-26 capture before the fix.
|
||||||
|
|
||||||
|
Confirmed across:
|
||||||
|
- 5-1-26 "copy 2nd address" BW capture: probe counter=0x2238, key=01112238, STRT@17 end=0x417E.
|
||||||
|
- 5-4-26 BW 2-sec event capture: probe counter=0x2238, key=01112238, TERM offset_word=0x0146 → end=0x417E.
|
||||||
|
|
||||||
|
No metadata pages — those have already been read during event 1 in the same Blastware
|
||||||
|
session, and BW caches them. Note that the metadata-page reads happen ONCE per
|
||||||
|
Blastware-session-on-the-device, not once per event, so an SFM session that downloads
|
||||||
|
several events should read 0x1002/0x1004 only once at the start.
|
||||||
|
|
||||||
|
#### History (do not re-derive)
|
||||||
|
|
||||||
**History:**
|
|
||||||
- Original: `_CHUNK1_COUNTER = 0x1004` hardcoded (Blastware capture artifact — WRONG).
|
- Original: `_CHUNK1_COUNTER = 0x1004` hardcoded (Blastware capture artifact — WRONG).
|
||||||
- 2026-04-06: Corrected to `chunk_num * 0x0400` (worked for key 01110000 only).
|
- 2026-04-06: `chunk_num * 0x0400` (worked for key 01110000 only).
|
||||||
- 2026-04-24: Corrected to `key4[2:4] + (chunk_num-1) * 0x0400` (fixed non-zero offsets,
|
- 2026-04-24: `key4[2:4] + (chunk_num-1) * 0x0400` (fixed non-zero offsets, broke key 01110000).
|
||||||
but accidentally broke key 01110000 — counter=0x0000 sends probe address again).
|
- 2026-04-26: `max(key4[2:4], 0x0400) + (chunk_num-1) * 0x0400` (broken — over-read past event end).
|
||||||
- 2026-04-26: Final formula: `max(key4[2:4], 0x0400) + (chunk_num-1) * 0x0400`.
|
- 2026-05-01: Increments are 0x0200 not 0x0400; absolute addresses inside event range; bounded
|
||||||
|
by STRT end_key, not by `max_chunks` cap or device-side timeout.
|
||||||
|
- 2026-05-04: Removed spurious `+0x0046` from event-N probe counter. `cur_key` from 1F
|
||||||
|
is already the off=0x46 WAVEHDR key, so adding +0x46 would have placed the probe one
|
||||||
|
WAVEHDR past the actual event start. This caused probe responses to lack a STRT
|
||||||
|
record (no `end_offset` parsed → `0xFFFF` fallback → `max_chunks=128` cap), walking
|
||||||
|
~110 chunks of post-event circular-buffer garbage. Fixed in protocol.py
|
||||||
|
`read_bulk_waveform_stream`.
|
||||||
|
|
||||||
|
### SUB 5A — STRT record encodes end_offset (NEW 2026-05-01)
|
||||||
|
|
||||||
|
The first A5 response (probe response, or the first chunk for event 2+) contains a STRT
|
||||||
|
record at byte offset 17 of the `data` field. Layout:
|
||||||
|
|
||||||
|
```
|
||||||
|
data[17:21] "STRT" magic
|
||||||
|
data[21:23] ff fe sentinel
|
||||||
|
data[23:27] end_key ← 4-byte key of where this event ENDS
|
||||||
|
data[27:31] start_key ← 4-byte key of where this event STARTS
|
||||||
|
data[31:33] uint16 BE ?? sample-count or total bytes (varies; not yet decoded)
|
||||||
|
data[33:35] uint16 BE ??
|
||||||
|
data[35] 0x46 record type (waveform full record)
|
||||||
|
…
|
||||||
|
```
|
||||||
|
|
||||||
|
`end_offset = (end_key[2] << 8) | end_key[3]` is **the authoritative event-end pointer**.
|
||||||
|
SFM must extract this from the first A5 response and use it to bound the chunk loop and
|
||||||
|
encode the TERM frame. The device will happily respond to chunk requests past `end_offset`
|
||||||
|
(returning post-event circular-buffer contents) — that's the over-read bug.
|
||||||
|
|
||||||
|
Verified across 3 events:
|
||||||
|
|
||||||
|
| Capture | start_key | end_key | end_offset | event size |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| 4-27-26 "open 2sec" / "copy event to disk" | `01110000` | `01111ABE` | `0x1ABE` | 6,846 B |
|
||||||
|
| 5-1-26 "copy 3sec" / Download All event 1 | `01110000` | `011121F2` | `0x21F2` | 8,690 B |
|
||||||
|
| 5-1-26 "copy 2nd address" / DA event 2 | `011121F2` | `0111417E` | `0x417E` (event 2 span 0x1F8C = 8,076 B) |
|
||||||
|
|
||||||
|
### SUB 5A — TERM frame formula (FINALIZED 2026-05-01)
|
||||||
|
|
||||||
|
The TERM frame fetches the partial last chunk *and* the file footer. It is **not** a simple
|
||||||
|
"goodbye" frame — its response payload contains the bytes between the last full 0x0200-aligned
|
||||||
|
chunk and `end_offset`, and is required for reconstructing the Blastware file format.
|
||||||
|
|
||||||
|
```
|
||||||
|
last_chunk_counter = address of last full 0x0200-byte chunk read
|
||||||
|
next_boundary = last_chunk_counter + 0x0200
|
||||||
|
TERM offset_word = end_offset - next_boundary
|
||||||
|
TERM params[0] = key[0] (= 0x01 on every observed device)
|
||||||
|
TERM params[1] = key[1] (= 0x11)
|
||||||
|
TERM params[2] = (next_boundary >> 8) & 0xFF
|
||||||
|
TERM params[3] = next_boundary & 0xFF
|
||||||
|
TERM params[4:10] = zeros
|
||||||
|
build_5a_frame(offset_word, params) (10-byte params, NOT 11)
|
||||||
|
```
|
||||||
|
|
||||||
|
The device reconstructs `requested_address = (params[2] << 8) | offset_word = end_offset`
|
||||||
|
and replies with `(end_offset - next_boundary)` bytes from `next_boundary` — the residual
|
||||||
|
between the last 0x0200 boundary and the actual event end. Append the TERM response data
|
||||||
|
to the chunk stream like any other A5 frame; it carries the final waveform tail + footer.
|
||||||
|
|
||||||
|
Verified across 3 events:
|
||||||
|
|
||||||
|
| end_offset | last chunk | next_boundary | TERM offset_word | TERM params[2:4] |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `0x1ABE` | `0x1800` | `0x1A00` | `0x00BE` ✓ | `1A 00` ✓ |
|
||||||
|
| `0x21F2` | `0x1E00` | `0x2000` | `0x01F2` ✓ | `20 00` ✓ |
|
||||||
|
| `0x417E` | `0x3E38` | `0x4038` | `0x0146` ✓ | `40 38` ✓ |
|
||||||
|
|
||||||
|
The previous code's hard-coded `offset_word = 0x005A` and `term_counter = last + 0x0400`
|
||||||
|
are wrong; the device's response under that path is a tiny 101-byte device-side terminator
|
||||||
|
(arrived only after we walked the entire post-event buffer), not the proper file footer.
|
||||||
|
|
||||||
|
### SUB 5A — fixed metadata pages 0x1002 and 0x1004 (NEW 2026-05-01)
|
||||||
|
|
||||||
|
Two chunk addresses are GLOBAL device/session metadata, not event-specific:
|
||||||
|
|
||||||
|
- `counter=0x1002` — first metadata page
|
||||||
|
- `counter=0x1004` — second metadata page
|
||||||
|
|
||||||
|
These are at fixed absolute addresses in the device's flash buffer. They contain the
|
||||||
|
session-start compliance setup (Project/Client/User Name/Seis Loc/Extended Notes ASCII
|
||||||
|
strings). Under the v0.14.0+ walk these strings are read directly from the metadata
|
||||||
|
pages, not from the sample-chunk stream.
|
||||||
|
|
||||||
|
BW reads them ONCE per Blastware session (during event 1's download) and caches them.
|
||||||
|
For SFM, that means:
|
||||||
|
- Once per call-home / once per `MiniMateClient.connect()` is enough.
|
||||||
|
- Subsequent events in the same session don't need to re-fetch them.
|
||||||
|
- Their content does not change when iterating events; only when the user opens
|
||||||
|
Compliance Setup → Apply on the device or sends a SUB 71 compliance write.
|
||||||
|
|
||||||
|
The full byte-for-byte layout of the metadata pages has not been mapped — `_decode_a5_metadata_into`
|
||||||
|
locates the ASCII strings via label scans (`Project:`, `Client:`, `User Name:`, `Seis Loc:`,
|
||||||
|
`Extended Notes`) which works correctly across observed captures. Future work could
|
||||||
|
dump the structural layout if more session-global fields need to be extracted.
|
||||||
|
|
||||||
### SUB 5A — params are 11 bytes for chunk frames, 10 for termination
|
### SUB 5A — params are 11 bytes for chunk frames, 10 for termination
|
||||||
|
|
||||||
@@ -148,10 +715,11 @@ chunk 1 = `0x245A`, chunk 2 = `0x285A`, chunk 3 = `0x2C5A` (each +0x0400).
|
|||||||
confirmed from the BW wire capture. `bulk_waveform_term_params()` returns 10 bytes.
|
confirmed from the BW wire capture. `bulk_waveform_term_params()` returns 10 bytes.
|
||||||
Do not swap them.
|
Do not swap them.
|
||||||
|
|
||||||
### SUB 5A — event-time metadata lives in A5 frame 7
|
### SUB 5A — event-time metadata source (FINALIZED 2026-05-05)
|
||||||
|
|
||||||
The bulk stream sends 9+ A5 response frames. Frame 7 (0-indexed) contains the compliance
|
The metadata strings come from the two fixed metadata pages at counter `0x1002` and
|
||||||
setup as it existed when the event was recorded:
|
`0x1004` (see "SUB 5A — fixed metadata pages 0x1002 and 0x1004" above). These pages
|
||||||
|
are GLOBAL session metadata — read once per Blastware/SFM session, not per event.
|
||||||
|
|
||||||
```
|
```
|
||||||
"Project:" → project description
|
"Project:" → project description
|
||||||
@@ -161,44 +729,71 @@ setup as it existed when the event was recorded:
|
|||||||
"Extended Notes"→ notes
|
"Extended Notes"→ notes
|
||||||
```
|
```
|
||||||
|
|
||||||
**IMPORTANT — 5A "Project:" is session-start config, NOT per-event (confirmed 2026-04-05):**
|
**IMPORTANT — these strings are session-start config, NOT per-event:**
|
||||||
The "Project:" string in the A5 frame 7 payload reflects the compliance setup from when
|
Project / Client / User Name / Seis Loc reflect the compliance setup from when the
|
||||||
the *monitoring session first started*, not the individual event's project name. The per-
|
*monitoring session first started*, not the individual event's per-event metadata. The
|
||||||
event project name is correctly stored in the 210-byte 0C waveform record and must be
|
authoritative per-event project name is stored in the 210-byte 0C waveform record.
|
||||||
used as the authoritative source. `_decode_a5_metadata_into` therefore only sets
|
`_decode_a5_metadata_into` therefore only sets `project` from the 5A metadata pages
|
||||||
`project` from 5A when 0C didn't already supply one.
|
when 0C didn't already supply one.
|
||||||
|
|
||||||
"Client:", "User Name:", "Seis Loc:", and "Extended Notes" are **NOT** present in the 0C
|
"Client:", "User Name:", "Seis Loc:", and "Extended Notes" are **NOT** present in the 0C
|
||||||
record — 5A remains the sole source for those fields and they are set unconditionally.
|
record — the metadata pages are the sole source for those fields and they are set
|
||||||
|
unconditionally.
|
||||||
|
|
||||||
`stop_after_metadata=True` (default) stops the 5A loop as soon as `b"Project:"` appears,
|
#### Deprecated knobs (do not re-introduce)
|
||||||
then sends the termination frame.
|
|
||||||
|
|
||||||
### SUB 5A — end-of-stream signal (confirmed 2026-04-06)
|
The `read_bulk_waveform_stream()` function still accepts these legacy kwargs for
|
||||||
|
backward compatibility, but they are **no-ops** under the v0.14.0+ walk:
|
||||||
|
|
||||||
After streaming all waveform chunks, the device sends exactly **1 raw byte** in response to
|
- `stop_after_metadata=True` — used to scan the chunk stream for `b"Project:"` and stop
|
||||||
the next chunk request, then goes silent. This is the natural end-of-stream indicator — NOT
|
one chunk later as a workaround for the missing end_offset bound. Obsolete: the loop
|
||||||
a complete A5 frame. `S3FrameParser.bytes_fed` will be 1; no frame is assembled.
|
is now deterministically bounded by `end_offset` parsed from the STRT record at
|
||||||
|
data[17] of the probe response, with the partial tail fetched by the TERM frame.
|
||||||
|
- `extra_chunks_after_metadata` — same era, same reason. No-op.
|
||||||
|
|
||||||
Handling: on `TimeoutError`, if `bytes_fed > 0` AND frames were already collected, treat as
|
If you find code or docs referencing "A5 frame 7" as the source of metadata strings,
|
||||||
graceful end-of-stream, break the loop, and proceed to the termination frame. If `bytes_fed
|
that's an old-walk artifact (the broken `0x0400`-step formula occasionally caught the
|
||||||
== 0` with no prior frames, it is a genuine transport failure — re-raise.
|
0x1002 metadata page at sample-chunk fi=7). Update to reference the dedicated metadata
|
||||||
|
pages instead.
|
||||||
|
|
||||||
**Chunk recv timeout must be 10 s, not the default 120 s.** Chunks arrive within ~1 s each.
|
### SUB 5A — end-of-stream (FINALIZED 2026-05-01)
|
||||||
Using 120 s causes a ~2-minute stall at every end-of-stream detection. The `_recv_one` call
|
|
||||||
in the chunk loop passes `timeout=10.0` explicitly.
|
|
||||||
|
|
||||||
**Typical chunk count (BE11529, 1024 sps):** A 9,306-sample event produces 35 chunks before
|
Under the v0.14.0+ STRT-bounded walk the stream ends cleanly:
|
||||||
end-of-stream. Chunks with uniform 1,036-byte data are all-zero ADC samples (post-event
|
|
||||||
silence). Only the initial variable-size chunks contain actual signal.
|
```
|
||||||
|
… last full chunk at counter < end_offset
|
||||||
|
TERM request (offset_word = end_offset - next_boundary,
|
||||||
|
params address (next_boundary))
|
||||||
|
TERM response (page_key = 0x0000 or 0x0001, data = the residual
|
||||||
|
end_offset - next_boundary bytes including the file footer)
|
||||||
|
```
|
||||||
|
|
||||||
|
No timeout-based detection, no "1-byte teaser," no `max_chunks` cap. The chunk loop
|
||||||
|
exits when `counter + 0x0200 > end_offset`; the TERM frame fetches the tail.
|
||||||
|
|
||||||
|
**Chunk recv timeout is 10 s, not the default 120 s.** Chunks arrive within ~1 s each.
|
||||||
|
Using 120 s would cause a ~2-minute stall on any unexpected timeout. The `_recv_one`
|
||||||
|
call in the chunk loop passes `timeout=10.0` explicitly.
|
||||||
|
|
||||||
|
**Typical chunk count under the v0.14.0+ walk (BE11529, 1024 sps over TCP/cellular):**
|
||||||
|
|
||||||
|
| Event duration | Sample chunks | Metadata pages | TERM | Total A5 frames |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| 2-sec (event 1) | ~12 | 2 | 1 | ~15 |
|
||||||
|
| 3-sec (event 1) | 13 | 2 | 1 | 16 |
|
||||||
|
| 2-sec (continuation) | 15 | 0 | 1 | 16 |
|
||||||
|
| 3-sec (continuation) | ~14 | 0 | 1 | ~15 |
|
||||||
|
|
||||||
|
For comparison, the deprecated `0x0400`-step walk produced ~37 chunks for a 2-sec
|
||||||
|
event with chunks 17-37 containing post-event circular-buffer garbage. Do not
|
||||||
|
re-introduce that walk under any circumstances.
|
||||||
|
|
||||||
### SUB 5A — fi==9 hardcoded skip (FIXED 2026-04-06)
|
### SUB 5A — fi==9 hardcoded skip (FIXED 2026-04-06)
|
||||||
|
|
||||||
`_decode_a5_waveform()` previously had `elif fi == 9: continue` — a leftover from the
|
`_decode_a5_waveform()` previously had `elif fi == 9: continue` — a leftover from the
|
||||||
9-frame original blast capture where frame 9 was assumed to be a terminator. For current
|
9-frame original blast capture where frame 9 was assumed to be a terminator. Removed.
|
||||||
35-frame streams, fi==9 is live waveform data (~133 sample-sets were being dropped).
|
TERM detection in the file builder uses `frame.page_key != 0x0010` (sample marker),
|
||||||
Removed. Terminator detection is via `page_key == 0x0000` in `read_bulk_waveform_stream`,
|
not frame index — see `blastware_file.py`.
|
||||||
not frame index.
|
|
||||||
|
|
||||||
### SUB 1E / 1F — event iteration null sentinel and token position (FIXED, do not re-introduce)
|
### SUB 1E / 1F — event iteration null sentinel and token position (FIXED, do not re-introduce)
|
||||||
|
|
||||||
@@ -303,6 +898,55 @@ sends token=0xFE and is NOT used by any caller.
|
|||||||
`advance_event()` returns `(key4, event_data8)`.
|
`advance_event()` returns `(key4, event_data8)`.
|
||||||
Callers (`count_events`, `get_events`) loop while `data8[4:8] != b"\x00\x00\x00\x00"`.
|
Callers (`count_events`, `get_events`) loop while `data8[4:8] != b"\x00\x00\x00\x00"`.
|
||||||
|
|
||||||
|
### SUB 0A — WAVEHDR response length distinguishes events from boundaries (NEW 2026-05-01)
|
||||||
|
|
||||||
|
When iterating events with the "Download All" pattern (1E → 0A → 1F → 0A → 1F → …), the
|
||||||
|
DATA_LENGTH at `data_rsp.data[5]` (= the byte BW echoes back as the offset for the data
|
||||||
|
fetch step) takes one of two values:
|
||||||
|
|
||||||
|
| WAVEHDR offset | Meaning |
|
||||||
|
|---|---|
|
||||||
|
| `0x46` (= 70) | Real event start key — there is event data at this address |
|
||||||
|
| `0x2C` (= 44) | Boundary marker between events — this key is the END of the previous event AND the START key for the empty space after it (or is the next event's pre-header) |
|
||||||
|
|
||||||
|
Confirmed from the 5-1-26 "Download All" capture:
|
||||||
|
|
||||||
|
```
|
||||||
|
0A(key=01110000) → off=0x46 ← event 1 real start
|
||||||
|
1F → key=011121F2
|
||||||
|
0A(key=011121F2) → off=0x2C ← event 1 END / event 2 boundary
|
||||||
|
1F → key=01112238
|
||||||
|
0A(key=01112238) → off=0x46 ← event 2 real start (= boundary + 0x46)
|
||||||
|
1F → key=0111417E
|
||||||
|
0A(key=0111417E) → off=0x2C ← event 2 END / next-empty marker
|
||||||
|
1F → null sentinel
|
||||||
|
```
|
||||||
|
|
||||||
|
This is why event 2's first 5A chunk is at `start_key + 0x46` — that's the address of the
|
||||||
|
"real start" 0x46-record, distinct from the `0x2C`-record at the raw boundary. Use the
|
||||||
|
`0x46` keys as the input to `read_bulk_waveform_stream`, not the `0x2C` keys.
|
||||||
|
|
||||||
|
For event 1 only (start_key[2:4] = 0x0000) BW probes at counter=0x0000 directly, which is
|
||||||
|
the `0x46`-keyed start record. Subsequent events use `start_key + 0x46`.
|
||||||
|
|
||||||
|
**Practical iteration pattern (replaces the old 1E/1F walk for downloads):**
|
||||||
|
|
||||||
|
```
|
||||||
|
Setup: SERIAL × 2 → CHCFG → 1E (token=0x00) → key0
|
||||||
|
For each event:
|
||||||
|
0A(cur_key) → DATA_LENGTH = 0x46 (real) or 0x2C (boundary)
|
||||||
|
1F (token=0x00) → next_key
|
||||||
|
if length was 0x46: → cur_key is a real event; queue it for download
|
||||||
|
cur_key = next_key
|
||||||
|
if next_key all-zero null sentinel: stop
|
||||||
|
|
||||||
|
Then for each queued real-event key:
|
||||||
|
download_event(key) → 5A bulk stream with STRT-bounded chunk walk
|
||||||
|
```
|
||||||
|
|
||||||
|
This is what BW does in the 5-1-26 "Download All" capture — it walks the full event chain
|
||||||
|
collecting `(key, length)` tuples first, *then* downloads each event using the `0x46` keys.
|
||||||
|
|
||||||
### SUB 1A — compliance config — orphaned send bug (FIXED, do not re-introduce)
|
### SUB 1A — compliance config — orphaned send bug (FIXED, do not re-introduce)
|
||||||
|
|
||||||
`read_compliance_config()` sends a 4-frame sequence (A, B, C, D) where:
|
`read_compliance_config()` sends a 4-frame sequence (A, B, C, D) where:
|
||||||
@@ -347,36 +991,6 @@ Do NOT use fixed absolute offsets for sample_rate or record_time.
|
|||||||
Quiet Mode enabled. Parser handles this — do not strip it manually before feeding to
|
Quiet Mode enabled. Parser handles this — do not strip it manually before feeding to
|
||||||
`S3FrameParser`.
|
`S3FrameParser`.
|
||||||
|
|
||||||
**SUB 5A (bulk waveform) TCP frame splitting — confirmed 2026-04-27:**
|
|
||||||
|
|
||||||
Over TCP via cellular modem, each 5A chunk request that produces a single ~1100-byte
|
|
||||||
A5 response over direct RS-232 may arrive as **two separate, complete S3 frames** of
|
|
||||||
~550 bytes each ("2-frame mode"). The modem's Data Forwarding Timeout (~100-150 ms)
|
|
||||||
can split the RS-232 response into two TCP segments, each parsed as a complete S3 frame.
|
|
||||||
Under different modem/timing conditions the full ~1100-byte response arrives as **one
|
|
||||||
S3 frame** ("1-frame mode").
|
|
||||||
|
|
||||||
**Both modes require `extra_chunks_after_metadata=1`** (the extra chunk at metadata_counter
|
|
||||||
+ 0x0400). The device's waveform footer data lives at circular-buffer address 0x1C00 for
|
|
||||||
this event; the terminator frame must be sent at 0x1C00 (not 0x1800) to receive it.
|
|
||||||
|
|
||||||
Example for a 2-second Continuous event (BE11529, key=01110000) via TCP:
|
|
||||||
- **2-frame mode:** 1 probe frame (554 B) + 5 chunks × 2 frames (556-573 B) + 1 extra chunk × 2 frames + 1 terminator (208 B) = **14 A5 frames** → 6864-byte file
|
|
||||||
- **1-frame mode:** 1 probe frame (~1097 B) + 5 chunks × 1 frame (~1079-1113 B) + 1 extra chunk × 1 frame (smaller, tail of event) + 1 terminator → **8 A5 frames** → 6864-byte file
|
|
||||||
- All frames contribute body data; using all of them gives the correct file.
|
|
||||||
|
|
||||||
**Fix (confirmed 2026-04-27):** `_recv_5a_batch()` in `protocol.py` collects ALL
|
|
||||||
A5 frames per chunk request before the next request is sent, using a 0.5 s batch
|
|
||||||
timeout after the first frame to catch the ~150 ms delayed second frame. `write_blastware_file()`
|
|
||||||
includes ALL body frames without skipping — the extra chunk's frames are part of the
|
|
||||||
body data, NOT padding to be discarded.
|
|
||||||
|
|
||||||
**WRONG earlier hypothesis (do not re-introduce):** An attempt was made to auto-detect
|
|
||||||
1-frame vs 2-frame mode from the probe frame size and skip the extra chunk when
|
|
||||||
`probe_data_len >= 700`. This was wrong — the extra chunk is always needed to advance
|
|
||||||
the device's internal state to the footer address. The `_probe_is_large` branch was
|
|
||||||
removed 2026-04-27.
|
|
||||||
|
|
||||||
### Required ACEmanager settings (Sierra Wireless RV50/RV55)
|
### Required ACEmanager settings (Sierra Wireless RV50/RV55)
|
||||||
|
|
||||||
| Setting | Value | Why |
|
| Setting | Value | Why |
|
||||||
@@ -557,6 +1171,8 @@ All DB endpoints are read-only except `PATCH /db/events/{id}/false_trigger`.
|
|||||||
| 3-11-26 | `bridges/captures/3-11-26/` | Full compliance setup write, Aux Trigger capture |
|
| 3-11-26 | `bridges/captures/3-11-26/` | Full compliance setup write, Aux Trigger capture |
|
||||||
| 3-31-26 | `bridges/captures/3-31-26/` | Complete event download cycle (148 BW / 147 S3 frames) — confirmed 1E/0A/0C/1F sequence; only 1 event stored so token=0xFE appeared to work |
|
| 3-31-26 | `bridges/captures/3-31-26/` | Complete event download cycle (148 BW / 147 S3 frames) — confirmed 1E/0A/0C/1F sequence; only 1 event stored so token=0xFE appeared to work |
|
||||||
| 4-3-26 | `bridges/captures/4-3-26/` | Browse-mode S3 capture with 2+ events — confirmed all-zero params for 1F, 1F response layout, null sentinel, 0A context requirement |
|
| 4-3-26 | `bridges/captures/4-3-26/` | Browse-mode S3 capture with 2+ events — confirmed all-zero params for 1F, 1F response layout, null sentinel, 0A context requirement |
|
||||||
|
| 4-27-26 | `bridges/captures/4-27-26/` | BW "open 2sec waveform" + "copy event to disk" + paired SFM "seismo_dl" — first proof that SFM was over-reading 5× past event end. BW reads 14 chunks at 0x0200 increments + TERM at end_offset; SFM was reading 37 chunks at 0x0400 increments. STRT end_key field located. |
|
||||||
|
| 5-1-26 | `bridges/captures/5-1-26/comcheck/` | Three sub-captures: SFM 3-sec download (`seismo_dl_…`), BW comms-check + 3-sec download (`bwcap3sec/`), BW second-event download + "Download All" (`raw_*_170945`/`_171216`). Confirmed: TERM frame formula across 3 events; metadata pages 0x1002/0x1004 are global (read once per session); event-1 vs event-N chunk-pattern split; WAVEHDR length 0x46 vs 0x2C disambiguates real events from boundaries. |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -820,7 +1436,7 @@ offsets in the raw 1A/E5 payload. Only fields with `✅` have confirmed offsets
|
|||||||
|
|
||||||
**Notes tab:**
|
**Notes tab:**
|
||||||
- Enable User Notes (bool)
|
- Enable User Notes (bool)
|
||||||
- Project, Client, User Name, Seis Loc (ASCII strings) ✅ (sourced from A5 frame 7 via 5A)
|
- Project, Client, User Name, Seis Loc (ASCII strings) ✅ (sourced from 5A metadata pages at counter 0x1002 / 0x1004 — see "SUB 5A — fixed metadata pages" section)
|
||||||
- Enable Extended Notes (bool); Extended Notes text; Extended Notes Title
|
- Enable Extended Notes (bool); Extended Notes text; Extended Notes Title
|
||||||
- Enable Job Number (bool); Job Number (int)
|
- Enable Job Number (bool); Job Number (int)
|
||||||
- Enable Scaled Distance (bool); Distance from Blast (float); Charge Weight (float) — Scaled Distance is derived
|
- Enable Scaled Distance (bool); Distance from Blast (float); Charge Weight (float) — Scaled Distance is derived
|
||||||
@@ -1132,9 +1748,11 @@ body) because writing a dial string may require DLE escaping for embedded contro
|
|||||||
|
|
||||||
## What's next
|
## What's next
|
||||||
|
|
||||||
|
**See [README.md → Roadmap (Future)](README.md#roadmap-future) for the canonical deferred-work list.** This section is kept as a status log of in-progress / recently-shipped technical details (encoding schemes, byte layouts, etc.) that are too low-level for the README's roadmap.
|
||||||
|
|
||||||
- **Database** — SQLite store for events + monitor log entries; dedup by key; queryable
|
- **Database** — SQLite store for events + monitor log entries; dedup by key; queryable
|
||||||
- **Histograms** — decode histogram-mode A5 data (noise floor tracking)
|
- **Histograms** — decode histogram-mode A5 data (noise floor tracking)
|
||||||
- **Blastware-compatible file output** — `write_blastware_file()` and `write_mlg()` implemented. `blastware_filename()` generates correct Blastware filenames (AB0 for direct, AB0W/AB0H for ACH). **Confirmed working for Continuous mode events (2026-04-23):** SFM-generated file opens in Blastware, shows correct PPV/waveform/timestamp. File is ~200 bytes shorter than BW (missing last ADC tail slice) — all measurements correct. Histogram+Continuous mode deferred (5A stream for those events embeds histogram interval records that create spurious STRT markers in the body). Extension mapping: **CONFIRMED FALSE 2026-04-21** — extensions encode timestamp (AB0T for ACH, AB0 for direct), NOT recording mode. Filename format: `<prefix_letter><serial3><4-char-base36-stem><ext>`
|
- **Blastware-compatible file output** — `write_blastware_file()` and `write_mlg()` implemented. `blastware_filename()` generates correct Blastware filenames (AB0 for direct, AB0W/AB0H for ACH). **Confirmed BYTE-PERFECT against BW reference (v0.14.3, 2026-05-05):** when fed the BW 5-1-26 3-sec capture's A5 frames, the SFM-built file matches BW's saved `M529LKIQ.G10` byte-for-byte (8708 bytes, 0 differences). Live SFM downloads of event 0 (3-sec) and event 1 (3-sec continuation) both open cleanly in Blastware with full Event Reports, frequency analysis, and waveform plots. Body assembly is just contiguous concatenation of frame contributions in stream order (probe → meta@0x1002 → meta@0x1004 → samples → TERM); no stripping, no overlay, no special handling. Histogram+Continuous mode deferred (5A stream for those events embeds histogram interval records that may need different handling — untested under v0.14.x). Extension mapping: extensions encode timestamp (AB0T for ACH, AB0 for direct), NOT recording mode. Filename format: `<prefix_letter><serial3><4-char-base36-stem><ext>`
|
||||||
|
|
||||||
**Serial encoding (CONFIRMED 2026-04-22):** `prefix_letter = chr(ord('B') + floor(serial_numeric / 1000))`, `serial3 = f"{serial_numeric % 1000:03d}"`. Examples: BE6907→H907, BE11529→M529, BE14036→P036, BE17353→S353, BE18003→T003. The prefix letter encodes the production generation (batch of 1000 units).
|
**Serial encoding (CONFIRMED 2026-04-22):** `prefix_letter = chr(ord('B') + floor(serial_numeric / 1000))`, `serial3 = f"{serial_numeric % 1000:03d}"`. Examples: BE6907→H907, BE11529→M529, BE14036→P036, BE17353→S353, BE18003→T003. The prefix letter encodes the production generation (batch of 1000 units).
|
||||||
|
|
||||||
@@ -1170,17 +1788,22 @@ body) because writing a dial string may require DLE escaping for embedded contro
|
|||||||
|
|
||||||
| Folder / File | Contents |
|
| Folder / File | Contents |
|
||||||
|---|---|
|
|---|---|
|
||||||
|
| `1-2-26/` | First SUB 5A BW TX capture — established 5A frame format (raw offset_hi, DLE-aware checksum). 10 frames verified. |
|
||||||
| `3-11-26/raw_bw_20260311_170151.bin` | Full compliance write + event download (SUBs 68→83 confirmed, frames 102–112) |
|
| `3-11-26/raw_bw_20260311_170151.bin` | Full compliance write + event download (SUBs 68→83 confirmed, frames 102–112) |
|
||||||
|
| `3-31-26/` | Single-event download (148 BW / 147 S3 frames) — 1E/0A/0C/1F sequence confirmed (single event so token=0xFE appeared to work in either branch) |
|
||||||
|
| `4-2-26/` | Download-mode BW TX capture — POLL×3 requirement confirmed (frames 68-73 between 1F and first 5A) |
|
||||||
|
| `4-3-26-multi_event/` | Browse-mode S3 capture with 2+ events — all-zero params for 1F, null sentinel layout, 0A context requirement |
|
||||||
|
| `4-8-26/` | Monitor status read, start/stop monitoring, SESSION_RESET signal, sensor check |
|
||||||
|
| `4-11-26 (mitm/ach_mitm_20260411_001912/)` | Full ACH call-home MITM — erase protocol (0xA3/0x06/0xA2), monitor log partial records confirmed |
|
||||||
| `4-20-26/raw_bw_*_recording_mode_*.bin` | Recording mode changes: Continuous→Single Shot, →Histogram, →Histogram+Continuous |
|
| `4-20-26/raw_bw_*_recording_mode_*.bin` | Recording mode changes: Continuous→Single Shot, →Histogram, →Histogram+Continuous |
|
||||||
| `4-20-26/histogram interval/` | Histogram interval changes: 1min, 5min, 15min, 15sec |
|
| `4-20-26/histogram interval/` | Histogram interval changes: 1min, 5min, 15min, 15sec |
|
||||||
| `4-20-26/geo sensitivity/` | Geo sensitivity changes: 1.25 in/s (Sensitive), 10 in/s (Normal) |
|
| `4-20-26/geo sensitivity/` | Geo sensitivity changes: 1.25 in/s (Sensitive), 10 in/s (Normal) |
|
||||||
| `4-20-26/call home settings/` | Call home config read/write captures |
|
| `4-20-26/call home settings/` | Call home config read/write captures |
|
||||||
| `4-8-26/` | Monitor status read, start/stop monitoring, SESSION_RESET signal, sensor check |
|
| `4-27-26/` | BW "open 2sec waveform" + "copy event to disk" + paired SFM "seismo_dl" — first proof of 5× SFM over-read. STRT end_key field located. |
|
||||||
| `4-3-26-multi_event/` | Browse-mode S3 capture with 2+ events (1E/0A/1F iteration confirmed) |
|
| **`5-1-26/comcheck/`** | **Triplet of captures that nailed the v0.14.0 walk:** SFM 3-sec download (`seismo_dl_…`), BW comms-check + 3-sec download (`bwcap3sec/`), BW second-event download + "Download All" (`raw_*_170945` / `_171216`). Confirmed: TERM frame formula across 3 events, metadata pages 0x1002/0x1004 are global session metadata, event-1 vs event-N chunk pattern split, WAVEHDR off=0x46 vs 0x2C disambiguates real events from boundaries. |
|
||||||
| `4-2-26/` | Download-mode BW TX capture (5A bulk stream, POLL×3 requirement confirmed) |
|
| **`5-1-26/comcheck/bwcap3sec/`** | **The byte-perfect reference for v0.14.3.** All 17 BW 5A request frames (probe, 2 metadata, 13 samples, TERM) reproduce byte-for-byte from SFM's framing helpers — including the `10 10 00` DLE-stuffed counter for sample @ 0x1000 that was the long-standing failure mode. |
|
||||||
| `3-31-26/` | Single-event download (148 BW / 147 S3 frames) |
|
| `5-4-26/` | BW MITM captures of "copy 3sec / 2sec / Download All" + paired SFM session (`seismo_dl_20260504_145701`) showing the +0x46 event-N probe bug producing 110-chunk runaway walk. Cross-references against 5-1-26 confirmed device behavior is identical. |
|
||||||
| `mitm/ach_mitm_20260411_001912/` | Full ACH call-home MITM (erase protocol, 0xA3/0x06/0xA2 confirmed) |
|
|
||||||
|
|
||||||
To parse BW TX captures: use `bridges/captures/` scripts or adapt the `find_write_frames()` pattern
|
To parse BW TX captures: use `bridges/captures/` scripts or adapt the `find_write_frames()` pattern
|
||||||
in `/tmp/analyze_write_payload.py` — it correctly handles `0x10 0x03` DLE-escaped ETX bytes
|
in `/tmp/analyze_write_payload.py` — it correctly handles `0x10 0x03` DLE-escaped ETX bytes
|
||||||
inside write frame data (the naive parser terminates early at the escaped `0x03`). | |||||||