Compare commits
25
Commits
v0.26.0
...
91b9b4578c
+156
-1
@@ -4,7 +4,162 @@ All notable changes to seismo-relay are documented here.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## [Unreleased]
|
## v0.29.0 — 2026-09-04
|
||||||
|
|
||||||
|
First release to reach prod since **v0.27.0**, so it ships **both** the
|
||||||
|
`false_trigger_reason` column below *and* the v0.28.0 offset (DC-baseline)
|
||||||
|
detector: v0.28.0 was version-bumped in-tree (`TOOL_VERSION`, CHANGELOG) but
|
||||||
|
never tagged or deployed, so 0.29.0 is the first build to carry either to prod.
|
||||||
|
Pairs with Terra-View ≥ 0.24.0. The `false_trigger_reason` column auto-migrates
|
||||||
|
on startup; the offset detector still needs the shape backfill on the prod store
|
||||||
|
(`scripts/backfill_event_shape.py`) to populate `shape_offset*` on existing rows.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **`events.false_trigger_reason` — optional FT cause.** A nullable `TEXT`
|
||||||
|
column recording *why* an event is a false trigger (e.g. `"offset"`), as a
|
||||||
|
subtype of the FT flag: setting a reason via the sidecar review PATCH implies
|
||||||
|
`false_trigger=1`, and the reason is cleared whenever FT ends up 0
|
||||||
|
(confirm-real, clear-FT, `set_false_trigger(false)`). `propagate_review_to_twins`
|
||||||
|
carries the reason to the histogram/waveform twin alongside the flag.
|
||||||
|
Auto-migrated (`_SCHEMA` + `_migrate` ADD COLUMN — not the Migration-1
|
||||||
|
rebuild); exposed via `/db/events`. Terra-View surfaces it as a manual
|
||||||
|
"Flag as offset" action + an `FT · offset` badge.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **BlastMate serials — the family prefix is read from the file, not guessed.**
|
||||||
|
The Blastware filename encodes only the serial *number* (`L895…` → 10895);
|
||||||
|
the two-letter prefix is not in it. `waveform_store` synthesised `"BE"`, so
|
||||||
|
an imported **BlastMate** (serials `BA…`) was filed under a MiniMate Plus
|
||||||
|
serial that does not exist — silently, and Terra-View read it straight
|
||||||
|
through. `save_imported_bw` now resolves serial as hint → file body →
|
||||||
|
filename guess, via a new `_serial_from_bw_bytes` that accepts a candidate
|
||||||
|
only when its numeric part matches the filename. `client._decode_0a_partial_header`
|
||||||
|
likewise matched a literal `b"BE"` in monitor-log partial records; on a
|
||||||
|
BlastMate that returned −1 and skipped the whole block, losing the **geo
|
||||||
|
threshold** along with the serial. It now matches any two-letter prefix and
|
||||||
|
requires the NUL terminator — stricter than the search it replaces.
|
||||||
|
|
||||||
|
BlastMate is the MiniMate Plus's larger Series III sibling and its files are
|
||||||
|
byte-compatible: all 1,493 in the DL2 archive decode through the existing
|
||||||
|
codec at 100%, same four channels. **The serial string was the only thing
|
||||||
|
blocking BlastMate support in SFM.** Four archive units were affected —
|
||||||
|
BA9229, BA10060, BA10895, BA15957.
|
||||||
|
|
||||||
|
**No backfill and no `TOOL_VERSION` bump**: this changes which serial an
|
||||||
|
*import* is filed under, not any decoded value, so existing sidecars and
|
||||||
|
`.h5` files are untouched. **No migration either** — prod holds no BlastMate
|
||||||
|
events (the archive's BA units last recorded 2018-10 through 2023-11; the
|
||||||
|
prod backfill reaches back only to ~May 2025).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v0.28.0 — 2026-09-02
|
||||||
|
|
||||||
|
**Offset (DC-baseline) false-trigger detector.** Productionizes the validated
|
||||||
|
pre-trigger detector: a geophone event whose baseline sits off zero and stays
|
||||||
|
flat across the record (sensor bumped / settled / drifted) is now flagged and
|
||||||
|
surfaced in Terra-View as an `offset` false-trigger reason — catching offsets the
|
||||||
|
crest/near-peak spike rule misses (an offset is low-crest and flat).
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- `shape_metrics.offset_from_samples` / `offset_from_h5`: per geophone channel,
|
||||||
|
`|median(pre-trigger)| ≥ 0.025 in/s` AND `pre/mid/end spread ≤ 0.02` → offset;
|
||||||
|
the consistency test rejects transients (a real event moves one third). Reads
|
||||||
|
the `.h5` samples + the `pretrig_samples` attr, range-aware via the in/s float
|
||||||
|
samples. Constants `OFFSET_FLOOR` / `OFFSET_MAX_SPREAD` are tunable.
|
||||||
|
- `events.shape_offset` / `shape_offset_axis` / `shape_offset_pre` /
|
||||||
|
`shape_offset_spread` columns (auto-migrated: `_SCHEMA` + the `_migrate`
|
||||||
|
ADD COLUMN loop), computed at all three ingest paths and by
|
||||||
|
`backfill_event_shape.py`, exposed via `/db/events`.
|
||||||
|
|
||||||
|
Requires the shape/offset backfill on the prod store to populate existing events:
|
||||||
|
`python scripts/backfill_event_shape.py --db-path … --store-root …`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v0.27.0 — 2026-08-28
|
||||||
|
|
||||||
|
**Per-sample decoder verification at scale, plus the offset investigation.**
|
||||||
|
The series-3 codec is now verified sample-by-sample against **14,338** preserved
|
||||||
|
Blastware ASCII exports — 1,249 waveform and 13,089 histogram, spanning 45 units
|
||||||
|
and files back to 2018. That is 11x the ground truth the production store
|
||||||
|
carried, and it found one real codec bug (below).
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **Sub-minute histograms with a partial final block decoded to nothing**
|
||||||
|
(`histogram_codec.detect_multi_interval_stride`). The stride search confirmed
|
||||||
|
itself on a third block header whenever the body was long enough to hold one —
|
||||||
|
but a body can exceed two strides and still contain only two real blocks, because
|
||||||
|
a *partial* final block leaves trailing padding. BE18193 `T193L0XM.CI0H` (51
|
||||||
|
intervals at 2 s = one full 30-interval block plus a 21-interval remainder, in a
|
||||||
|
2787-byte body) therefore had its correct stride of 612 discarded and produced an
|
||||||
|
empty decode. A missing third header now means end-of-stream rather than
|
||||||
|
disqualification; the block-counter check, which is what actually prevents the
|
||||||
|
false positives that once mis-dispatched 9,082 files, is unchanged.
|
||||||
|
|
||||||
|
Found by decoding the full DL2 archive against its preserved Blastware ASCII
|
||||||
|
exports. Across **63,535 unique** histogram binaries the fix recovers **4 files** —
|
||||||
|
`K440HJCN.3C0H` and `K557IF1U.8K0H` (stride 252), `T191HVNP.0S0H` (92) and
|
||||||
|
`T193L0XM.CI0H` (612) — with **zero** files regressed. Verification over all
|
||||||
|
14,340 archive pairs goes 14,337 → 14,338 exact, the only remainder being two
|
||||||
|
series-4 IDF files that belong to a different codec.
|
||||||
|
|
||||||
|
(The DL2 export keeps a byte-identical `Sent/` mirror of its root, so a naive
|
||||||
|
walk double-counts every binary — 127,035 paths are 63,535 distinct files. The
|
||||||
|
ASCII exports are *not* mirrored, so the 14,340 pair count is already distinct.)
|
||||||
|
|
||||||
|
**No prod backfill is required for this.** Verified after the fact: all four
|
||||||
|
recovered files are archive-only — none exists in the production store or the
|
||||||
|
events DB — and re-running stride detection over the production store's
|
||||||
|
**10,215** histogram binaries shows **0 files whose decode changes**. The fix
|
||||||
|
matters for future ingests of sub-minute histograms with a partial final block,
|
||||||
|
not for anything already stored.
|
||||||
|
|
||||||
|
(`TOOL_VERSION` moves with the release, so whenever a backfill *is* next run for
|
||||||
|
some other reason it will regenerate the whole store rather than skipping. That
|
||||||
|
is harmless — the output is byte-identical for every currently-stored file — but
|
||||||
|
it means the run takes its full ~2 hours on the NAS.)
|
||||||
|
|
||||||
|
- **Histogram/waveform twin matching is now interval-based** (`find_twins`). A real
|
||||||
|
trigger is recorded twice — as a triggered waveform (stamped at the trigger instant)
|
||||||
|
and inside the scheduled histogram whose interval contains it (stamped at the 7am/7pm
|
||||||
|
interval start) — so the two twins can be **hours apart**. The old ±5-minute window
|
||||||
|
silently missed them, which broke review propagation (flagging one twin didn't flag its
|
||||||
|
twin). Twins are now matched by same serial + identical `peak_vector_sum` + opposite
|
||||||
|
record type + the waveform falling within the histogram's interval (bounded by the next
|
||||||
|
same-serial histogram). `window_seconds` is retained but ignored. Fixes terra-view #102
|
||||||
|
sub-task 2.
|
||||||
|
|
||||||
|
- **`/health` reported a hard-coded `0.1.0`** instead of the real service version.
|
||||||
|
`sfm/server.py` now derives its version from `minimateplus.event_file_io.TOOL_VERSION`,
|
||||||
|
making that constant the single source of truth for the service version and the
|
||||||
|
sidecar stamp alike — one place to bump at release.
|
||||||
|
|
||||||
|
- **`CLAUDE.md` had 793 NUL bytes appended** after its last line, which made `grep`
|
||||||
|
treat the file as binary and silently skip it. Present since at least v0.21.0.
|
||||||
|
Stripped.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **`docs/offset_investigation.md`** — a dated journal of the "offset" hardware
|
||||||
|
fault: base rate, detector design, per-unit case files, ruled-out hypotheses
|
||||||
|
(each kept with the evidence that killed it), and Instantel's own autozero
|
||||||
|
procedure with its 2027–2069 acceptance window.
|
||||||
|
- **`scratch/verify_against_ascii.py`** — decodes a corpus of BW binaries and
|
||||||
|
diffs every sample against the paired `_ASCII.TXT`. Includes a saturation
|
||||||
|
carve-out: BW clamps clipped events to the range maximum and writes `OORANGE`,
|
||||||
|
while the decoder faithfully reports counts past nominal full scale.
|
||||||
|
- **`scratch/offset_scan3.py`** — offset detector. Measures the resting floor in
|
||||||
|
the *pre-trigger* window (definitionally quiet) and requires it to hold across
|
||||||
|
pre / middle / end. Result: **5 of 45 units (11%)**, stable across a 2x
|
||||||
|
threshold range. Supersedes `offset_scan.py` and `offset_scan2.py`, both kept
|
||||||
|
as the reasoning trail.
|
||||||
|
|
||||||
|
### Verified
|
||||||
|
- **19,244 healthy channel-events sit at a pre-trigger floor of exactly 0.000
|
||||||
|
(62.7%), 94.5% within ±1 quantisation unit, median +0.0000.** No systematic
|
||||||
|
zero-point bias in the decoder — an independent confirmation of the
|
||||||
|
32000-count geo full scale, arrived at from a different direction than the
|
||||||
|
ASCII sample comparisons.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -2,21 +2,28 @@
|
|||||||
|
|
||||||
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.29.0**.
|
||||||
|
|
||||||
|
Stack-level context — which repo owns what, and how the three project versions
|
||||||
|
pair — lives in `../terra-view/docs/tmi-stack.md`, which is also loaded as
|
||||||
|
`~/CLAUDE.md`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Where things stand (updated 2026-08-27)
|
## Where things stand (updated 2026-08-28)
|
||||||
|
|
||||||
Read this first when picking the project back up.
|
Read this first when picking the project back up.
|
||||||
|
|
||||||
- **Series-3 decode is correct and verified.** All 11,603 series-3 binaries in
|
- **Series-3 decode is verified per-sample at scale (v0.27.0).** The full DL2
|
||||||
the prod snapshot pass every check (channel lengths, peaks vs the device's
|
archive decodes **14,338 / 14,338** paired files exactly against their
|
||||||
own reported PPV, nothing above full scale, length vs declared record time).
|
preserved Blastware ASCII exports — 1,249 waveform + 13,089 histogram, 45
|
||||||
Ground truth: 1,211/1,211 histograms exact per-interval and 75/75 waveform
|
units, files back to 2018. That is 11x the ground truth the prod store
|
||||||
sample counts exact against preserved Blastware ASCII exports.
|
carried, and it supersedes the old "per-sample on 11%, peak-only on 89%"
|
||||||
⚠ That is per-sample proof on 11% of files and peak-only consistency on the
|
caveat. Harness: `scratch/verify_against_ascii.py` (note its saturation
|
||||||
other 89% — see `docs/instantel_protocol_reference.md` §7.6.1.
|
carve-out — BW clamps clipped events, the decoder reports true counts).
|
||||||
|
Independent corroboration of the 32000-count scale: 19,244 healthy
|
||||||
|
channel-events sit at a pre-trigger floor of exactly 0.000 (62.7%), 94.5%
|
||||||
|
within ±1 quantisation unit, median +0.0000 — no zero-point bias.
|
||||||
- **Series-4 (Thor / Micromate) is NOT verified.** UM-series sits at ~48%
|
- **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.
|
against device peaks with a ~1.7% systematic bias and a near-zero tail.
|
||||||
Thor IDFW is pinned to `decode_waveform_legacy` deliberately.
|
Thor IDFW is pinned to `decode_waveform_legacy` deliberately.
|
||||||
@@ -24,11 +31,24 @@ Read this first when picking the project back up.
|
|||||||
(= 10.0/1.25) units discrepancy; `scripts/backfill_sidecars.py --force` also
|
(= 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
|
inserts DB rows for store files that have none (one-time per store) and the
|
||||||
dry-run does not report that count.
|
dry-run does not report that count.
|
||||||
- **After any codec change, regenerate the store** — `backfill_sidecars.py
|
- **After any codec change, regenerate the store** — `backfill_sidecars.py`
|
||||||
--force` then `backfill_event_shape.py`, DB backup first. Stored `.h5` files
|
then `backfill_event_shape.py`, DB backup first. Stored `.h5` files do not
|
||||||
do not update themselves.
|
update themselves. No `--force` needed as long as `TOOL_VERSION` was bumped
|
||||||
- **Parked:** the "offset" hardware-fault investigation (Appendix E of the
|
(it gates regeneration). ⚠ On the office NAS this takes **~2 hours**
|
||||||
protocol reference) pending the multi-year BW archive.
|
(~1.5 files/sec vs 85/sec on the dev box — gzip-4 in `sfm/event_hdf5.py`
|
||||||
|
against a Synology CPU). Budget it up front.
|
||||||
|
**v0.27.0 does NOT owe prod a backfill** — verified: the partial-final-block
|
||||||
|
fix changes 0 of the 10,215 histograms in the prod store (the 4 recovered
|
||||||
|
files are archive-only and were never ingested).
|
||||||
|
- **The "offset" hardware fault has its own journal** --
|
||||||
|
`docs/offset_investigation.md`. **5 of 45 units (11%)**, and the fault is
|
||||||
|
**persistent** — it stays until the geophone is serviced. Detect it with
|
||||||
|
`scratch/offset_scan3.py`: the resting floor in the **pre-trigger** window,
|
||||||
|
required to hold across pre/middle/end. Never score only the dominant-peak
|
||||||
|
axis and never use the mean — both produce false recoveries (see the
|
||||||
|
retraction banner in the journal). Instantel's autozero procedure and its
|
||||||
|
2027-2069 acceptance window are recorded there too. Best open lead is
|
||||||
|
`SUB 0x0E` (unimplemented), which may carry those very 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
|
||||||
@@ -1803,4 +1823,4 @@ body) because writing a dial string may require DLE escaping for embedded contro
|
|||||||
|
|
||||||
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`). | |||||||