Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e1a6fd5386 | ||
|
|
6b875e161b | ||
|
|
f5c81f2cab |
@@ -1,28 +0,0 @@
|
|||||||
.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,142 +2,12 @@
|
|||||||
|
|
||||||
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.26.0**.
|
(Sierra Wireless RV50 / RV55). Current version: **v0.12.3**.
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 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
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -147,8 +17,6 @@ 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)
|
||||||
@@ -159,7 +27,7 @@ CHANGELOG.md ← version history
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Current implementation state (v0.14.3)
|
## Current implementation state (v0.12.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:
|
||||||
|
|
||||||
@@ -173,15 +41,14 @@ 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 + 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. |
|
| **Bulk waveform stream (event-time metadata)** | **5A** | ✅ new v0.6.0 |
|
||||||
| 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 → 1E(arm token=0xFE) → 0C → 1F(arm) → POLL×3 → 5A → 1F(browse)`
|
`get_events()` sequence per event: `1E → 0A → 0C → 5A → 1F`
|
||||||
(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`
|
||||||
|
|
||||||
@@ -189,269 +56,6 @@ 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
|
||||||
@@ -511,203 +115,32 @@ 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.
|
||||||
|
|
||||||
3. **Params region uses partial DLE stuffing (CONFIRMED 2026-05-05).** The device's
|
Both differences confirmed by reproducing Blastware's exact wire bytes from the 1-2-26
|
||||||
de-stuffing rule for bytes inside the params region is:
|
BW TX capture. All 10 frames verified.
|
||||||
|
|
||||||
- `10 10` → de-stuffs to `10`
|
### SUB 5A — chunk counter formula (FINAL CORRECTION 2026-04-26)
|
||||||
- `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`)
|
|
||||||
|
|
||||||
Therefore any `0x10` byte in the *logical* params that is followed by a byte NOT in
|
**Chunk counter = `max(key4[2:4], 0x0400) + (chunk_num - 1) * 0x0400` for ALL chunks.**
|
||||||
`{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`.
|
|
||||||
|
|
||||||
`0x10` bytes in `offset_hi` (body[5]) are still written RAW — only the params region
|
where `key4[2:4] = (key4[2] << 8) | key4[3]` is the event's circular-buffer base offset.
|
||||||
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.
|
|
||||||
|
|
||||||
Both differences (1) and (2) confirmed by reproducing Blastware's exact wire bytes from
|
The `max(..., 0x0400)` guard is critical for events at the start of the circular buffer
|
||||||
the 1-2-26 BW TX capture (10 frames). Difference (3) confirmed against the 5-1-26
|
(key4[2:4] == 0x0000, e.g. key `01110000`). Without it, chunk 1 gets counter=0x0000, which
|
||||||
"bwcap3sec" capture (17 frames, all match byte-for-byte after fix).
|
is the same address as the probe frame — the device re-returns the STRT record data instead
|
||||||
|
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`).
|
||||||
|
|
||||||
### SUB 5A — chunk counter formula (REWRITTEN 2026-05-01 — see 5-1-26 captures)
|
The 4-3-26 capture confirms the pattern for a second event (key `0111245a`, key4[2:4]=0x245a):
|
||||||
|
chunk 1 = `0x245A`, chunk 2 = `0x285A`, chunk 3 = `0x2C5A` (each +0x0400).
|
||||||
> ⚠️ **Everything that came before this rewrite was WRONG in important ways.** The previous
|
`max(0x245a, 0x0400) = 0x245a` → formula works correctly for non-zero base offset too.
|
||||||
> 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: `chunk_num * 0x0400` (worked for key 01110000 only).
|
- 2026-04-06: Corrected to `chunk_num * 0x0400` (worked for key 01110000 only).
|
||||||
- 2026-04-24: `key4[2:4] + (chunk_num-1) * 0x0400` (fixed non-zero offsets, broke key 01110000).
|
- 2026-04-24: Corrected to `key4[2:4] + (chunk_num-1) * 0x0400` (fixed non-zero offsets,
|
||||||
- 2026-04-26: `max(key4[2:4], 0x0400) + (chunk_num-1) * 0x0400` (broken — over-read past event end).
|
but accidentally broke key 01110000 — counter=0x0000 sends probe address again).
|
||||||
- 2026-05-01: Increments are 0x0200 not 0x0400; absolute addresses inside event range; bounded
|
- 2026-04-26: Final formula: `max(key4[2:4], 0x0400) + (chunk_num-1) * 0x0400`.
|
||||||
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
|
||||||
|
|
||||||
@@ -715,11 +148,10 @@ dump the structural layout if more session-global fields need to be extracted.
|
|||||||
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 source (FINALIZED 2026-05-05)
|
### SUB 5A — event-time metadata lives in A5 frame 7
|
||||||
|
|
||||||
The metadata strings come from the two fixed metadata pages at counter `0x1002` and
|
The bulk stream sends 9+ A5 response frames. Frame 7 (0-indexed) contains the compliance
|
||||||
`0x1004` (see "SUB 5A — fixed metadata pages 0x1002 and 0x1004" above). These pages
|
setup as it existed when the event was recorded:
|
||||||
are GLOBAL session metadata — read once per Blastware/SFM session, not per event.
|
|
||||||
|
|
||||||
```
|
```
|
||||||
"Project:" → project description
|
"Project:" → project description
|
||||||
@@ -729,71 +161,44 @@ are GLOBAL session metadata — read once per Blastware/SFM session, not per eve
|
|||||||
"Extended Notes"→ notes
|
"Extended Notes"→ notes
|
||||||
```
|
```
|
||||||
|
|
||||||
**IMPORTANT — these strings are session-start config, NOT per-event:**
|
**IMPORTANT — 5A "Project:" is session-start config, NOT per-event (confirmed 2026-04-05):**
|
||||||
Project / Client / User Name / Seis Loc reflect the compliance setup from when the
|
The "Project:" string in the A5 frame 7 payload reflects the compliance setup from when
|
||||||
*monitoring session first started*, not the individual event's per-event metadata. The
|
the *monitoring session first started*, not the individual event's project name. The per-
|
||||||
authoritative per-event project name is stored in the 210-byte 0C waveform record.
|
event project name is correctly stored in the 210-byte 0C waveform record and must be
|
||||||
`_decode_a5_metadata_into` therefore only sets `project` from the 5A metadata pages
|
used as the authoritative source. `_decode_a5_metadata_into` therefore only sets
|
||||||
when 0C didn't already supply one.
|
`project` from 5A 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 — the metadata pages are the sole source for those fields and they are set
|
record — 5A remains the sole source for those fields and they are set unconditionally.
|
||||||
unconditionally.
|
|
||||||
|
|
||||||
#### Deprecated knobs (do not re-introduce)
|
`stop_after_metadata=True` (default) stops the 5A loop as soon as `b"Project:"` appears,
|
||||||
|
then sends the termination frame.
|
||||||
|
|
||||||
The `read_bulk_waveform_stream()` function still accepts these legacy kwargs for
|
### SUB 5A — end-of-stream signal (confirmed 2026-04-06)
|
||||||
backward compatibility, but they are **no-ops** under the v0.14.0+ walk:
|
|
||||||
|
|
||||||
- `stop_after_metadata=True` — used to scan the chunk stream for `b"Project:"` and stop
|
After streaming all waveform chunks, the device sends exactly **1 raw byte** in response to
|
||||||
one chunk later as a workaround for the missing end_offset bound. Obsolete: the loop
|
the next chunk request, then goes silent. This is the natural end-of-stream indicator — NOT
|
||||||
is now deterministically bounded by `end_offset` parsed from the STRT record at
|
a complete A5 frame. `S3FrameParser.bytes_fed` will be 1; no frame is assembled.
|
||||||
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.
|
|
||||||
|
|
||||||
If you find code or docs referencing "A5 frame 7" as the source of metadata strings,
|
Handling: on `TimeoutError`, if `bytes_fed > 0` AND frames were already collected, treat as
|
||||||
that's an old-walk artifact (the broken `0x0400`-step formula occasionally caught the
|
graceful end-of-stream, break the loop, and proceed to the termination frame. If `bytes_fed
|
||||||
0x1002 metadata page at sample-chunk fi=7). Update to reference the dedicated metadata
|
== 0` with no prior frames, it is a genuine transport failure — re-raise.
|
||||||
pages instead.
|
|
||||||
|
|
||||||
### SUB 5A — end-of-stream (FINALIZED 2026-05-01)
|
**Chunk recv timeout must be 10 s, not the default 120 s.** Chunks arrive within ~1 s each.
|
||||||
|
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.
|
||||||
|
|
||||||
Under the v0.14.0+ STRT-bounded walk the stream ends cleanly:
|
**Typical chunk count (BE11529, 1024 sps):** A 9,306-sample event produces 35 chunks before
|
||||||
|
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. Removed.
|
9-frame original blast capture where frame 9 was assumed to be a terminator. For current
|
||||||
TERM detection in the file builder uses `frame.page_key != 0x0010` (sample marker),
|
35-frame streams, fi==9 is live waveform data (~133 sample-sets were being dropped).
|
||||||
not frame index — see `blastware_file.py`.
|
Removed. Terminator detection is via `page_key == 0x0000` in `read_bulk_waveform_stream`,
|
||||||
|
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)
|
||||||
|
|
||||||
@@ -898,55 +303,6 @@ 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:
|
||||||
@@ -991,6 +347,36 @@ 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 |
|
||||||
@@ -1171,8 +557,6 @@ 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. |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -1436,7 +820,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 5A metadata pages at counter 0x1002 / 0x1004 — see "SUB 5A — fixed metadata pages" section)
|
- Project, Client, User Name, Seis Loc (ASCII strings) ✅ (sourced from A5 frame 7 via 5A)
|
||||||
- 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
|
||||||
@@ -1748,11 +1132,9 @@ 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 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>`
|
- **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>`
|
||||||
|
|
||||||
**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).
|
||||||
|
|
||||||
@@ -1788,22 +1170,17 @@ 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-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-8-26/` | Monitor status read, start/stop monitoring, SESSION_RESET signal, sensor check |
|
||||||
| **`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-3-26-multi_event/` | Browse-mode S3 capture with 2+ events (1E/0A/1F iteration 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. |
|
| `4-2-26/` | Download-mode BW TX capture (5A bulk stream, POLL×3 requirement confirmed) |
|
||||||
| `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. |
|
| `3-31-26/` | Single-event download (148 BW / 147 S3 frames) |
|
||||||
|
| `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`).
|
inside write frame data (the naive parser terminates early at the escaped `0x03`). | ||||||