diff --git a/bridges/ach_server.py b/bridges/ach_server.py index c048d4c..0bfd8af 100644 --- a/bridges/ach_server.py +++ b/bridges/ach_server.py @@ -516,6 +516,7 @@ class AchSession: serial=serial or self.peer, session_id=None, waveform_records=waveform_records, + device_family="series3", ) _ml_ins, _ml_skip = self.db.insert_monitor_log( new_monitor_entries, session_id=None diff --git a/scripts/backfill_sidecars.py b/scripts/backfill_sidecars.py index 6b7cb82..b937e8c 100644 --- a/scripts/backfill_sidecars.py +++ b/scripts/backfill_sidecars.py @@ -326,6 +326,7 @@ def main(argv=None) -> int: }} if ev._waveform_key else None ), + device_family="series3", ) except Exception as exc: log.warning("DB upsert failed for %s: %s", path.name, exc) diff --git a/sfm/database.py b/sfm/database.py index 8228d6c..1792f20 100644 --- a/sfm/database.py +++ b/sfm/database.py @@ -85,6 +85,7 @@ CREATE TABLE IF NOT EXISTS events ( blastware_filesize INTEGER, -- bytes; NULL if no event file saved a5_pickle_filename TEXT, -- ".a5.pkl" sidecar sidecar_filename TEXT, -- ".sfm.json" review/metadata sidecar + device_family TEXT, -- "series3" (MiniMate Plus / BW) | "series4" (Micromate / Thor) — drives per-family UI rendering (units, labels) created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), UNIQUE(serial, timestamp) ); @@ -198,11 +199,53 @@ class SeismoDb: ("blastware_filesize", "INTEGER"), ("a5_pickle_filename", "TEXT"), ("sidecar_filename", "TEXT"), + ("device_family", "TEXT"), ): if col not in existing_cols: log.info("_migrate: events ADD COLUMN %s %s", col, ddl) conn.execute(f"ALTER TABLE events ADD COLUMN {col} {ddl}") + # Migration 1c: backfill device_family for existing rows by sniffing + # the device-native binary filename's extension. Thor (Micromate + # Series IV) writes `.IDFH` / `.IDFW`; MiniMate Plus (Series III) + # writes `.AB0*` / `.N00` / `.` Blastware extensions. We do + # this here rather than from sidecars so the migration is fully + # self-contained (doesn't need the waveform-store root) and runs at + # DB-init time. Only fills NULL device_family so re-runs are no-ops. + rebackfill = conn.execute( + "SELECT COUNT(*) FROM events WHERE device_family IS NULL" + ).fetchone() + if rebackfill and rebackfill[0] > 0: + log.info("_migrate: backfilling device_family for %d events", rebackfill[0]) + # Series IV (Thor IDF) — extension is exactly .IDFH or .IDFW + conn.execute( + """ + UPDATE events + SET device_family = 'series4' + WHERE device_family IS NULL + AND ( + UPPER(blastware_filename) LIKE '%.IDFH' + OR UPPER(blastware_filename) LIKE '%.IDFW' + ) + """ + ) + # Everything else with a filename → Series III (Blastware family) + conn.execute( + """ + UPDATE events + SET device_family = 'series3' + WHERE device_family IS NULL + AND blastware_filename IS NOT NULL + """ + ) + # Rows with no filename (e.g. older monitor_log-derived events) + # stay NULL — UI handles NULL as "unknown family". + remaining = conn.execute( + "SELECT COUNT(*) FROM events WHERE device_family IS NULL" + ).fetchone()[0] + log.info("_migrate: device_family backfill complete (remaining NULL=%d)", + remaining) + # Migration 2: change monitor_log UNIQUE from (serial, waveform_key) to # (serial, start_time) — same reasoning as events. row = conn.execute( @@ -302,6 +345,7 @@ class SeismoDb: serial: str, session_id: Optional[str] = None, waveform_records: Optional[dict[str, dict]] = None, + device_family: Optional[str] = None, ) -> tuple[int, int]: """ Insert triggered events. Silently skips duplicates (serial+timestamp). @@ -316,6 +360,11 @@ class SeismoDb: (dedup hit), the matching waveform record is upserted onto the existing row so a re-download via the live endpoint refreshes the file metadata. + + ``device_family`` (optional): "series3" (MiniMate Plus / Blastware) or + "series4" (Micromate / Thor). Drives per-family UI rendering — most + importantly the mic-unit convention (psi vs dB(L)). Set on every + insert and overwritten on every UPSERT so the latest writer wins. """ inserted = skipped = 0 wave_recs = waveform_records or {} @@ -349,8 +398,9 @@ class SeismoDb: project, client, operator, sensor_location, sample_rate, record_type, blastware_filename, blastware_filesize, - a5_pickle_filename, sidecar_filename) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + a5_pickle_filename, sidecar_filename, + device_family) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( self._new_id(), serial, key, session_id, ts, @@ -369,6 +419,7 @@ class SeismoDb: rec.get("filesize"), rec.get("a5_pickle_filename"), rec.get("sidecar_filename"), + device_family, ), ) inserted += 1 @@ -409,7 +460,8 @@ class SeismoDb: blastware_filename = ?, blastware_filesize = ?, a5_pickle_filename = ?, - sidecar_filename = ? + sidecar_filename = ?, + device_family = COALESCE(?, device_family) WHERE serial = ? AND timestamp = ? """, ( @@ -428,6 +480,7 @@ class SeismoDb: rec.get("filesize") if rec else None, rec.get("a5_pickle_filename") if rec else None, rec.get("sidecar_filename") if rec else None, + device_family, serial, ts, ), diff --git a/sfm/import_bw.py b/sfm/import_bw.py index f49097b..5d31bdd 100644 --- a/sfm/import_bw.py +++ b/sfm/import_bw.py @@ -166,6 +166,7 @@ def main(argv: list[str] | None = None) -> int: {ev._waveform_key.hex(): rec} if ev._waveform_key else None ), + device_family="series3", ) tag = "OK " if ins else ("SKIP" if sk else "OK ") print(f" [{tag}] {path.name} → {rec['filename']} " diff --git a/sfm/server.py b/sfm/server.py index 2378ab9..078fb96 100644 --- a/sfm/server.py +++ b/sfm/server.py @@ -918,6 +918,7 @@ def device_event_blastware_file( [ev], serial=serial, waveform_records={ev._waveform_key.hex(): rec}, + device_family="series3", ) log.info( "blastware_file: persisted to store (%s, %d bytes)", @@ -2434,6 +2435,7 @@ async def db_import_blastware_file( ev._waveform_key.hex(): rec if ev._waveform_key else None } if ev._waveform_key else None, + device_family="series3", ) results.append({ "filename": filename, @@ -2558,6 +2560,7 @@ async def db_import_idf_file( waveform_records={ ev._waveform_key.hex(): rec } if ev._waveform_key else None, + device_family="series4", ) results.append({ "filename": filename, diff --git a/sfm/sfm_webapp.html b/sfm/sfm_webapp.html index 019f900..576ae94 100644 --- a/sfm/sfm_webapp.html +++ b/sfm/sfm_webapp.html @@ -2392,11 +2392,9 @@ async function loadHistory() { ${(() => { const m = ev.mic_ppv == null ? null : Number(ev.mic_ppv); if (m == null || !isFinite(m) || m <= 0) return '—'; - // BW (.AB0*/.N00) stores mic_ppv as psi → convert to dBL. - // Thor IDF (.IDFH/.IDFW) already stores dBL → display directly. - const fn = (ev.blastware_filename || '').toUpperCase(); - const isThor = fn.endsWith('.IDFH') || fn.endsWith('.IDFW'); - if (isThor) return m.toFixed(1) + ' dBL'; + // Series III (MiniMate Plus / BW) stores mic_ppv as psi → convert. + // Series IV (Micromate / Thor) already stores dB(L) → display direct. + if (ev.device_family === 'series4') return m.toFixed(1) + ' dBL'; return (20 * Math.log10(m / DBL_REF)).toFixed(1) + ' dBL'; })()} ${ev.project ?? '—'} @@ -2464,13 +2462,18 @@ function _renderSidecar(data) { const n = Number(v); return isFinite(n) ? n.toFixed(5) + ' in/s' : String(v); }; + // Map sidecar source.kind → device family (Series IV ingest path is + // "idf-import"; everything else is Series III today). The events-list + // table uses ev.device_family from the DB row, but sidecars don't carry + // that column — source.kind is the equivalent signal here. + const family = ((src.kind || '') === 'idf-import') ? 'series4' : 'series3'; const fmtMic = v => { if (v == null) return '—'; const n = Number(v); if (!isFinite(n) || n <= 0) return '—'; - // Source-aware: Thor IDF imports store mic as dB(L); BW imports store - // it as psi. `src` is in the outer scope from _renderSidecar(). - if ((src.kind || '') === 'idf-import') return `${n.toFixed(1)} dBL`; + // Series IV (Micromate / Thor) stores mic as dB(L); Series III (BW) + // stores it as psi and we render both for cross-reference. + if (family === 'series4') return `${n.toFixed(1)} dBL`; const dbl = 20 * Math.log10(n / DBL_REF); return `${dbl.toFixed(1)} dBL (${n.toExponential(2)} psi)`; }; @@ -2767,9 +2770,9 @@ document.getElementById('api-base').value = window.location.origin;

Source / files

-
BW filename
-
BW filesize
-
BW sha256
+
Event file
+
File size
+
File sha256
Source kind
Captured at