v0.19.0 - minimate compatability + family separation #22
@@ -516,6 +516,7 @@ class AchSession:
|
|||||||
serial=serial or self.peer,
|
serial=serial or self.peer,
|
||||||
session_id=None,
|
session_id=None,
|
||||||
waveform_records=waveform_records,
|
waveform_records=waveform_records,
|
||||||
|
device_family="series3",
|
||||||
)
|
)
|
||||||
_ml_ins, _ml_skip = self.db.insert_monitor_log(
|
_ml_ins, _ml_skip = self.db.insert_monitor_log(
|
||||||
new_monitor_entries, session_id=None
|
new_monitor_entries, session_id=None
|
||||||
|
|||||||
@@ -326,6 +326,7 @@ def main(argv=None) -> int:
|
|||||||
}}
|
}}
|
||||||
if ev._waveform_key else None
|
if ev._waveform_key else None
|
||||||
),
|
),
|
||||||
|
device_family="series3",
|
||||||
)
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
log.warning("DB upsert failed for %s: %s", path.name, exc)
|
log.warning("DB upsert failed for %s: %s", path.name, exc)
|
||||||
|
|||||||
+56
-3
@@ -85,6 +85,7 @@ CREATE TABLE IF NOT EXISTS events (
|
|||||||
blastware_filesize INTEGER, -- bytes; NULL if no event file saved
|
blastware_filesize INTEGER, -- bytes; NULL if no event file saved
|
||||||
a5_pickle_filename TEXT, -- "<filename>.a5.pkl" sidecar
|
a5_pickle_filename TEXT, -- "<filename>.a5.pkl" sidecar
|
||||||
sidecar_filename TEXT, -- "<filename>.sfm.json" review/metadata sidecar
|
sidecar_filename TEXT, -- "<filename>.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')),
|
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
||||||
UNIQUE(serial, timestamp)
|
UNIQUE(serial, timestamp)
|
||||||
);
|
);
|
||||||
@@ -198,11 +199,53 @@ class SeismoDb:
|
|||||||
("blastware_filesize", "INTEGER"),
|
("blastware_filesize", "INTEGER"),
|
||||||
("a5_pickle_filename", "TEXT"),
|
("a5_pickle_filename", "TEXT"),
|
||||||
("sidecar_filename", "TEXT"),
|
("sidecar_filename", "TEXT"),
|
||||||
|
("device_family", "TEXT"),
|
||||||
):
|
):
|
||||||
if col not in existing_cols:
|
if col not in existing_cols:
|
||||||
log.info("_migrate: events ADD COLUMN %s %s", col, ddl)
|
log.info("_migrate: events ADD COLUMN %s %s", col, ddl)
|
||||||
conn.execute(f"ALTER TABLE events ADD COLUMN {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` / `.<base36>` 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
|
# Migration 2: change monitor_log UNIQUE from (serial, waveform_key) to
|
||||||
# (serial, start_time) — same reasoning as events.
|
# (serial, start_time) — same reasoning as events.
|
||||||
row = conn.execute(
|
row = conn.execute(
|
||||||
@@ -302,6 +345,7 @@ class SeismoDb:
|
|||||||
serial: str,
|
serial: str,
|
||||||
session_id: Optional[str] = None,
|
session_id: Optional[str] = None,
|
||||||
waveform_records: Optional[dict[str, dict]] = None,
|
waveform_records: Optional[dict[str, dict]] = None,
|
||||||
|
device_family: Optional[str] = None,
|
||||||
) -> tuple[int, int]:
|
) -> tuple[int, int]:
|
||||||
"""
|
"""
|
||||||
Insert triggered events. Silently skips duplicates (serial+timestamp).
|
Insert triggered events. Silently skips duplicates (serial+timestamp).
|
||||||
@@ -316,6 +360,11 @@ class SeismoDb:
|
|||||||
(dedup hit), the matching waveform record is upserted onto the
|
(dedup hit), the matching waveform record is upserted onto the
|
||||||
existing row so a re-download via the live endpoint refreshes the
|
existing row so a re-download via the live endpoint refreshes the
|
||||||
file metadata.
|
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
|
inserted = skipped = 0
|
||||||
wave_recs = waveform_records or {}
|
wave_recs = waveform_records or {}
|
||||||
@@ -349,8 +398,9 @@ class SeismoDb:
|
|||||||
project, client, operator, sensor_location,
|
project, client, operator, sensor_location,
|
||||||
sample_rate, record_type,
|
sample_rate, record_type,
|
||||||
blastware_filename, blastware_filesize,
|
blastware_filename, blastware_filesize,
|
||||||
a5_pickle_filename, sidecar_filename)
|
a5_pickle_filename, sidecar_filename,
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
device_family)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
""",
|
""",
|
||||||
(
|
(
|
||||||
self._new_id(), serial, key, session_id, ts,
|
self._new_id(), serial, key, session_id, ts,
|
||||||
@@ -369,6 +419,7 @@ class SeismoDb:
|
|||||||
rec.get("filesize"),
|
rec.get("filesize"),
|
||||||
rec.get("a5_pickle_filename"),
|
rec.get("a5_pickle_filename"),
|
||||||
rec.get("sidecar_filename"),
|
rec.get("sidecar_filename"),
|
||||||
|
device_family,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
inserted += 1
|
inserted += 1
|
||||||
@@ -409,7 +460,8 @@ class SeismoDb:
|
|||||||
blastware_filename = ?,
|
blastware_filename = ?,
|
||||||
blastware_filesize = ?,
|
blastware_filesize = ?,
|
||||||
a5_pickle_filename = ?,
|
a5_pickle_filename = ?,
|
||||||
sidecar_filename = ?
|
sidecar_filename = ?,
|
||||||
|
device_family = COALESCE(?, device_family)
|
||||||
WHERE serial = ? AND timestamp = ?
|
WHERE serial = ? AND timestamp = ?
|
||||||
""",
|
""",
|
||||||
(
|
(
|
||||||
@@ -428,6 +480,7 @@ class SeismoDb:
|
|||||||
rec.get("filesize") if rec else None,
|
rec.get("filesize") if rec else None,
|
||||||
rec.get("a5_pickle_filename") if rec else None,
|
rec.get("a5_pickle_filename") if rec else None,
|
||||||
rec.get("sidecar_filename") if rec else None,
|
rec.get("sidecar_filename") if rec else None,
|
||||||
|
device_family,
|
||||||
serial,
|
serial,
|
||||||
ts,
|
ts,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -166,6 +166,7 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
{ev._waveform_key.hex(): rec}
|
{ev._waveform_key.hex(): rec}
|
||||||
if ev._waveform_key else None
|
if ev._waveform_key else None
|
||||||
),
|
),
|
||||||
|
device_family="series3",
|
||||||
)
|
)
|
||||||
tag = "OK " if ins else ("SKIP" if sk else "OK ")
|
tag = "OK " if ins else ("SKIP" if sk else "OK ")
|
||||||
print(f" [{tag}] {path.name} → {rec['filename']} "
|
print(f" [{tag}] {path.name} → {rec['filename']} "
|
||||||
|
|||||||
@@ -918,6 +918,7 @@ def device_event_blastware_file(
|
|||||||
[ev],
|
[ev],
|
||||||
serial=serial,
|
serial=serial,
|
||||||
waveform_records={ev._waveform_key.hex(): rec},
|
waveform_records={ev._waveform_key.hex(): rec},
|
||||||
|
device_family="series3",
|
||||||
)
|
)
|
||||||
log.info(
|
log.info(
|
||||||
"blastware_file: persisted to store (%s, %d bytes)",
|
"blastware_file: persisted to store (%s, %d bytes)",
|
||||||
@@ -2434,6 +2435,7 @@ async def db_import_blastware_file(
|
|||||||
ev._waveform_key.hex(): rec
|
ev._waveform_key.hex(): rec
|
||||||
if ev._waveform_key else None
|
if ev._waveform_key else None
|
||||||
} if ev._waveform_key else None,
|
} if ev._waveform_key else None,
|
||||||
|
device_family="series3",
|
||||||
)
|
)
|
||||||
results.append({
|
results.append({
|
||||||
"filename": filename,
|
"filename": filename,
|
||||||
@@ -2558,6 +2560,7 @@ async def db_import_idf_file(
|
|||||||
waveform_records={
|
waveform_records={
|
||||||
ev._waveform_key.hex(): rec
|
ev._waveform_key.hex(): rec
|
||||||
} if ev._waveform_key else None,
|
} if ev._waveform_key else None,
|
||||||
|
device_family="series4",
|
||||||
)
|
)
|
||||||
results.append({
|
results.append({
|
||||||
"filename": filename,
|
"filename": filename,
|
||||||
|
|||||||
+14
-11
@@ -2392,11 +2392,9 @@ async function loadHistory() {
|
|||||||
<td class="td-dim">${(() => {
|
<td class="td-dim">${(() => {
|
||||||
const m = ev.mic_ppv == null ? null : Number(ev.mic_ppv);
|
const m = ev.mic_ppv == null ? null : Number(ev.mic_ppv);
|
||||||
if (m == null || !isFinite(m) || m <= 0) return '—';
|
if (m == null || !isFinite(m) || m <= 0) return '—';
|
||||||
// BW (.AB0*/.N00) stores mic_ppv as psi → convert to dBL.
|
// Series III (MiniMate Plus / BW) stores mic_ppv as psi → convert.
|
||||||
// Thor IDF (.IDFH/.IDFW) already stores dBL → display directly.
|
// Series IV (Micromate / Thor) already stores dB(L) → display direct.
|
||||||
const fn = (ev.blastware_filename || '').toUpperCase();
|
if (ev.device_family === 'series4') return m.toFixed(1) + ' dBL';
|
||||||
const isThor = fn.endsWith('.IDFH') || fn.endsWith('.IDFW');
|
|
||||||
if (isThor) return m.toFixed(1) + ' dBL';
|
|
||||||
return (20 * Math.log10(m / DBL_REF)).toFixed(1) + ' dBL';
|
return (20 * Math.log10(m / DBL_REF)).toFixed(1) + ' dBL';
|
||||||
})()}</td>
|
})()}</td>
|
||||||
<td class="td-text">${ev.project ?? '—'}</td>
|
<td class="td-text">${ev.project ?? '—'}</td>
|
||||||
@@ -2464,13 +2462,18 @@ function _renderSidecar(data) {
|
|||||||
const n = Number(v);
|
const n = Number(v);
|
||||||
return isFinite(n) ? n.toFixed(5) + ' in/s' : String(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 => {
|
const fmtMic = v => {
|
||||||
if (v == null) return '—';
|
if (v == null) return '—';
|
||||||
const n = Number(v);
|
const n = Number(v);
|
||||||
if (!isFinite(n) || n <= 0) return '—';
|
if (!isFinite(n) || n <= 0) return '—';
|
||||||
// Source-aware: Thor IDF imports store mic as dB(L); BW imports store
|
// Series IV (Micromate / Thor) stores mic as dB(L); Series III (BW)
|
||||||
// it as psi. `src` is in the outer scope from _renderSidecar().
|
// stores it as psi and we render both for cross-reference.
|
||||||
if ((src.kind || '') === 'idf-import') return `${n.toFixed(1)} dBL`;
|
if (family === 'series4') return `${n.toFixed(1)} dBL`;
|
||||||
const dbl = 20 * Math.log10(n / DBL_REF);
|
const dbl = 20 * Math.log10(n / DBL_REF);
|
||||||
return `${dbl.toFixed(1)} dBL (${n.toExponential(2)} psi)`;
|
return `${dbl.toFixed(1)} dBL (${n.toExponential(2)} psi)`;
|
||||||
};
|
};
|
||||||
@@ -2767,9 +2770,9 @@ document.getElementById('api-base').value = window.location.origin;
|
|||||||
<div class="sc-section">
|
<div class="sc-section">
|
||||||
<h4>Source / files</h4>
|
<h4>Source / files</h4>
|
||||||
<dl class="sc-grid">
|
<dl class="sc-grid">
|
||||||
<dt>BW filename</dt> <dd id="sc-f-bw">—</dd>
|
<dt id="sc-l-bw">Event file</dt> <dd id="sc-f-bw">—</dd>
|
||||||
<dt>BW filesize</dt> <dd id="sc-f-bwsize">—</dd>
|
<dt id="sc-l-bwsize">File size</dt> <dd id="sc-f-bwsize">—</dd>
|
||||||
<dt>BW sha256</dt> <dd id="sc-f-sha">—</dd>
|
<dt id="sc-l-sha">File sha256</dt> <dd id="sc-f-sha">—</dd>
|
||||||
<dt>Source kind</dt> <dd id="sc-f-src">—</dd>
|
<dt>Source kind</dt> <dd id="sc-f-src">—</dd>
|
||||||
<dt>Captured at</dt> <dd id="sc-f-cap">—</dd>
|
<dt>Captured at</dt> <dd id="sc-f-cap">—</dd>
|
||||||
</dl>
|
</dl>
|
||||||
|
|||||||
Reference in New Issue
Block a user