Compare commits
2
Commits
dev
..
2eb1d25028
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2eb1d25028 | ||
|
|
cc821f9ee3 |
+1
-1053
File diff suppressed because it is too large
Load Diff
@@ -2,152 +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.31.0**.
|
(Sierra Wireless RV50 / RV55). Current version: **v0.20.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-28)
|
|
||||||
|
|
||||||
Read this first when picking the project back up.
|
|
||||||
|
|
||||||
- **Series-3 decode is verified per-sample at scale (v0.27.0).** The full DL2
|
|
||||||
archive decodes **14,338 / 14,338** paired files exactly against their
|
|
||||||
preserved Blastware ASCII exports — 1,249 waveform + 13,089 histogram, 45
|
|
||||||
units, files back to 2018. That is 11x the ground truth the prod store
|
|
||||||
carried, and it supersedes the old "per-sample on 11%, peak-only on 89%"
|
|
||||||
caveat. Harness: `scratch/verify_against_ascii.py` (note its saturation
|
|
||||||
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 now verified per-sample (2026-09-10).**
|
|
||||||
**1,057,536 / 1,057,536** geo samples across all 153 genuine Thor waveform
|
|
||||||
files reproduce Thor's own CSV export exactly; IDFH peaks are within 2% on
|
|
||||||
858/858 (median -0.004%). The ground truth was in the corpus all along —
|
|
||||||
Thor writes `CSV/<name>.IDFW.csv` beside each binary with a **per-sample**
|
|
||||||
four-column block. Harness: `scratch/verify_thor_against_csv.py`.
|
|
||||||
Four bugs, all fixed: geo LSB was `0.0003` (display rounding of the real
|
|
||||||
`0.000310308`, so every sample read **3.3% low**); the IDFH segment
|
|
||||||
validator required a zero counter high byte, **capping every histogram at
|
|
||||||
250 intervals**; record mode `00 00` (raw int16) was unhandled, silently
|
|
||||||
dropping each channel's first 512 samples; and the body-offset search
|
|
||||||
matched `00 02 00` *inside* record headers, decoding a rotation-shifted
|
|
||||||
body. IDFW is no longer pinned to `decode_waveform_legacy`.
|
|
||||||
Series-3 re-verified unchanged at 14,338/14,338 after the shared-codec
|
|
||||||
change.
|
|
||||||
- **Mic-disabled (3-channel) units are a distinct shape (2026-09-10).**
|
|
||||||
Verified on a second corpus (`~/thor-csv-req`, UM11402/UM12947/UM20147):
|
|
||||||
**139/139** waveforms per-sample exact, **877/877** histograms within 2%.
|
|
||||||
Two structural differences: the shorter header puts the waveform record
|
|
||||||
chain head at `0x0dba` (below the old `_BODY_SCAN_FLOOR` of `0x0E00`, so it
|
|
||||||
was invisible and Vert came up exactly 512 short), and the histogram
|
|
||||||
interval record is **56 bytes, not 72** — `16 × n_channels + 8`, derived per
|
|
||||||
segment from the cumulative interval counter, never assumed.
|
|
||||||
- **`40 NN` blocks are not capped at NN=8 (2026-09-11).** `data_block_len()`
|
|
||||||
rejected `NN > 0x08`, a guard with no evidence behind it — the corpora
|
|
||||||
available when it was written only used NN ∈ {1,2,3,4,8}. Loud UM12947
|
|
||||||
events use NN up to 196, and since the walker stops at the first
|
|
||||||
unrecognised tag rather than raising, this surfaced as silently short
|
|
||||||
channels. Verified on 167 UM12947 waveforms: length mismatches 22 → 0,
|
|
||||||
1,476,242/1,476,249 samples exact.
|
|
||||||
- **Production IDFW is now 575/575** — zero truncations, zero decode
|
|
||||||
failures, median PPV error −0.0007% across 8 units (was 41 truncated + 1
|
|
||||||
failing, −3.3%). Across all three ground-truth corpora: **459 files,
|
|
||||||
3,807,158/3,807,165 samples exact**; the 7 stragglers differ by one
|
|
||||||
4th-decimal tick and are Thor's own rounding — no single linear LSB can
|
|
||||||
reproduce every printed value (the constraints are infeasible by 7e-5
|
|
||||||
relative), so do NOT retune `_GEO_LSB_IPS`.
|
|
||||||
- **⚠ KNOWN BUG — the 5A walk breaks once a unit's buffer crosses 64 KB.**
|
|
||||||
`parse_strt_end_offset()` returns only `(end_key[2] << 8) | end_key[3]`,
|
|
||||||
discarding the key's page byte. An event starting at `0x0111F2A2` and ending
|
|
||||||
at `0x0112_1010` therefore reads `end_offset = 0x1010` — *behind* its own
|
|
||||||
start. The chunk loop then exits before fetching anything and TERM computes
|
|
||||||
a negative `offset_word`, which `struct.pack(">H", ...)` rejects: the
|
|
||||||
`/device/events` walk 500s. Reproduced on BE12599 (2026-09-19), which had
|
|
||||||
78 KB stored and had rolled into page `0x12`.
|
|
||||||
**Why it hid so long:** every 5A capture the walk was verified against came
|
|
||||||
from a freshly-erased BE11529 — all three confirmed TERM examples in
|
|
||||||
`framing.py` (`0x1ABE`, `0x21F2`, `0x417E`) sit inside page `0x11`. Prod is
|
|
||||||
unaffected: it ingests complete files via BW ACH, never this walk.
|
|
||||||
**Fixing it has two layers** — the arithmetic (`if end < start: end +=
|
|
||||||
0x10000`) stops the crash and bounds the loop correctly; carrying the page
|
|
||||||
byte through the chunk requests (`params[1]` 0x11 -> 0x12, counter rolling
|
|
||||||
over) needs a BW capture of a spanning event first. Do not ship layer one
|
|
||||||
alone without a loud truncation warning — a silently short event is the
|
|
||||||
failure mode this codec has been bitten by repeatedly.
|
|
||||||
- **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`
|
|
||||||
then `backfill_event_shape.py`, DB backup first. Stored `.h5` files do not
|
|
||||||
update themselves. No `--force` needed as long as `TOOL_VERSION` was bumped
|
|
||||||
(it gates regeneration). ⚠ On the office NAS this takes **~2 hours**
|
|
||||||
(~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
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Changelog & release convention
|
|
||||||
|
|
||||||
**Feature branches do NOT touch `CHANGELOG.md`. Write the entry on `dev`, as
|
|
||||||
part of finishing the merge, under `## Unreleased`. Cut the version on `dev` in a
|
|
||||||
dedicated release commit when you are ready to ship to `main`.**
|
|
||||||
|
|
||||||
- **The changelog is written on `dev`, never on a feature branch.** With
|
|
||||||
several branches in flight they all edit the same few lines at the top of
|
|
||||||
the file and conflict every time. Writing it once, after the merge, also
|
|
||||||
lets it describe what actually *landed* — including anything that changed
|
|
||||||
during conflict resolution.
|
|
||||||
- ⚠ **The merge is not finished until `## Unreleased` is updated.** Same sitting,
|
|
||||||
not "later" — that is the one failure mode of writing it after the fact.
|
|
||||||
Reconstruct from the branch's own commit messages:
|
|
||||||
`git log --oneline dev..<branch>` before you merge, or
|
|
||||||
`git log --oneline <merge-base>..<branch>` after.
|
|
||||||
- **No preamble under `## Unreleased`** — just the `### Added` / `### Changed` /
|
|
||||||
`### Fixed` lists. The themed opening paragraph gets written at release
|
|
||||||
time, when the whole release is visible and can be named honestly. A theme
|
|
||||||
written when the first item landed is stale by the third.
|
|
||||||
- ⚠ **State the operational consequence** on any entry touching the codec, the
|
|
||||||
waveform store, or the DB — **including when it is "none."** "requires
|
|
||||||
`backfill_sidecars.py` + `backfill_event_shape.py`, ~2 h on the NAS",
|
|
||||||
"`TOOL_VERSION` bumped", "no schema change, no migration". Silence is
|
|
||||||
ambiguous; "none" is information. This repo's changelog is how future-you
|
|
||||||
learns whether a deploy costs two hours.
|
|
||||||
- **Releases are cut on judgement, not on a schedule or a merge.** `Unreleased`
|
|
||||||
is the staging area for whatever is going into the next release; when enough
|
|
||||||
has accumulated to be worth shipping, it gets a number and a date. Nothing
|
|
||||||
about a merge to `dev` triggers a release.
|
|
||||||
- **Cutting a release** is its own `chore(release): vX.Y.Z — <theme>` commit on
|
|
||||||
`dev`, renaming `## Unreleased` → `## vX.Y.Z — YYYY-MM-DD` and touching:
|
|
||||||
`CHANGELOG.md`, `pyproject.toml`, the version line in `CLAUDE.md` and
|
|
||||||
`README.md`, and `minimateplus/event_file_io.py` (`TOOL_VERSION`) **when the
|
|
||||||
codec changed** — that constant gates `.h5` regeneration.
|
|
||||||
- **`main` carries only released versions.** No `## Unreleased` section there;
|
|
||||||
it lands via the `dev` → `main` PR. `main` lagging `dev` by a version is
|
|
||||||
normal.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Architecture: three-tier conceptual model
|
## Architecture: three-tier conceptual model
|
||||||
|
|
||||||
seismo-relay is a **suite of cooperating components**, not a single app.
|
seismo-relay is a **suite of cooperating components**, not a single app.
|
||||||
@@ -213,42 +73,6 @@ 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
|
beyond reading files passed as arguments. Keep them pure — both
|
||||||
tiers can then depend on them without circularity.
|
tiers can then depend on them without circularity.
|
||||||
|
|
||||||
#### Thor IDF binary codec (updated 2026-09-10)
|
|
||||||
|
|
||||||
`micromate/idf_file.read_idf_file()` decodes both Thor IDFW
|
|
||||||
(waveform) and IDFH (histogram) binaries. **Verified per-sample
|
|
||||||
against Thor's own CSV exports** — see
|
|
||||||
`scratch/verify_thor_against_csv.py`.
|
|
||||||
|
|
||||||
- **IDFW** uses the series-3 record-chain `decode_waveform_v2()`. The
|
|
||||||
body offset is **not** fixed: it is `<chain-head record> + 7`, found
|
|
||||||
by `_find_waveform_body_offset()` anchoring on record headers. All
|
|
||||||
**153/153** genuine Thor waveform files decode per-sample exact
|
|
||||||
(1,057,536/1,057,536 samples).
|
|
||||||
- **IDFH** segment header is `[len_be][0a 00 00 00][counter_be][05 3f]`,
|
|
||||||
where `counter` is a **uint16 cumulative interval index** — it must
|
|
||||||
not be constrained to a zero high byte (that capped histograms at 250
|
|
||||||
intervals). Intervals whose `min > max` on all channels are unwritten
|
|
||||||
slots carrying a ±full-scale seed and are skipped. 858/858 files land
|
|
||||||
within 2% of Thor's PPV (median -0.004%).
|
|
||||||
- **Geo LSB is `0.000310308` in/s per count** (full scale 10.0 in/s =
|
|
||||||
32226.05 counts). Series-3's 32000-count scale does NOT apply.
|
|
||||||
- **Record modes** are `02 00` deltas (14 B header), `01 00` absolute,
|
|
||||||
`00 03` raw 12-bit, and `00 00` **raw int16** (all 10 B headers).
|
|
||||||
`01 00` and `00 00` are also valid as the implicit segment-0 preamble.
|
|
||||||
|
|
||||||
⚠ **Thor's histogram PPV has a 0.0050 in/s display floor.** 41.4% of
|
|
||||||
prod IDFH sidecars report a component PPV exceeding their own vector
|
|
||||||
sum — impossible. On quiet files our decode is *more* accurate than
|
|
||||||
the reference; do not "fix" the decoder to match it.
|
|
||||||
|
|
||||||
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
|
### Practical consequences
|
||||||
|
|
||||||
When deciding where new code goes, ask:
|
When deciding where new code goes, ask:
|
||||||
@@ -377,118 +201,6 @@ custom delta + RLE + variable-width codec.
|
|||||||
`NN + 2` for int8 blocks). Confirmed 2026-05-11 against SP0 cycle
|
`NN + 2` for int8 blocks). Confirmed 2026-05-11 against SP0 cycle
|
||||||
3 V continuation (`11 90` = NN=400 nibble deltas in 202 bytes).
|
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
|
### What's NOT solved
|
||||||
|
|
||||||
- **MicL channel conversion to dB(L)** — the codec emits MicL as
|
- **MicL channel conversion to dB(L)** — the codec emits MicL as
|
||||||
@@ -496,29 +208,9 @@ with zero mismatches. Before: 1 of 1196.
|
|||||||
shows mic in dB(L) with ~6 dB quantization steps. Need to map
|
shows mic in dB(L) with ~6 dB quantization steps. Need to map
|
||||||
ADC counts → dB(L) for direct comparison; likely
|
ADC counts → dB(L) for direct comparison; likely
|
||||||
`dB = 20*log10(|counts|) + offset` or similar.
|
`dB = 20*log10(|counts|) + offset` or similar.
|
||||||
- **Variable-prefix segment descriptors** — 3 of the 75 ground-truth
|
- **Walker edge cases** — SP0/SS0/SV0 don't walk the full event due
|
||||||
production events still truncate. The walk reaches a segment header
|
to block-length quirks past the first few segments. Every sample
|
||||||
whose channel-id field is preceded by a *variable-width* prefix (2, 4
|
reached is correct; the walker just needs robustness improvements.
|
||||||
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**~~ — FIXED 2026-09-10.
|
|
||||||
`UM11402_20260406130113.IDFW` now decodes Tran 1.1168 / Vert 4.3220 /
|
|
||||||
Long 0.9135, matching the device report exactly. Root cause was the
|
|
||||||
body-offset search landing inside a record header plus the unhandled
|
|
||||||
`00 00` record mode, not anything UM-specific.
|
|
||||||
- ~~**Thor IDF per-count LSB**~~ — RESOLVED 2026-09-10. The 0.983 ratio was
|
|
||||||
exactly `0.0003 / 0.000310308`. Thor's geo LSB is **0.000310308 in/s per
|
|
||||||
count** (full scale 10.0 in/s = 32226.05 counts), pinned to ±6e-11 by
|
|
||||||
intersecting 991,415 rounding constraints from Thor's own exports and
|
|
||||||
corroborated by the ±full-scale seed (`±32226`) left in unwritten IDFH
|
|
||||||
interval slots. Series-3's 32000-count scale does **not** carry over.
|
|
||||||
Note `10.0/32226` is very slightly wrong — see
|
|
||||||
`docs/idf_protocol_reference.md`.
|
|
||||||
|
|
||||||
### Decoded sample counts (across the fixture bundle)
|
### Decoded sample counts (across the fixture bundle)
|
||||||
|
|
||||||
@@ -550,14 +242,6 @@ then `decoded_to_adc_counts()` to scale to int16 ADC counts (geos × 16;
|
|||||||
mic pass-through). The `.h5` sidecars SFM produces now contain
|
mic pass-through). The `.h5` sidecars SFM produces now contain
|
||||||
correct samples for any event without walker edge cases.
|
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
|
The original int16 LE decoder is preserved as
|
||||||
`_decode_a5_waveform_LEGACY` for reference but is not called.
|
`_decode_a5_waveform_LEGACY` for reference but is not called.
|
||||||
|
|
||||||
@@ -1934,4 +1618,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`).
|
inside write frame data (the naive parser terminates early at the escaped `0x03`). | ||||||