feat: add thor/micromate compatibility v0.18.0

This commit is contained in:
2026-05-19 04:32:43 +00:00
parent 512d82c720
commit cd20be2eff
7 changed files with 839 additions and 2 deletions
+117
View File
@@ -2472,6 +2472,123 @@ def _serial_from_event(ev) -> Optional[str]:
return None
# ── /db/import/idf_file — ingest Thor (Series IV) IDF event files ────────────
@app.post("/db/import/idf_file")
async def db_import_idf_file(
files: list[UploadFile] = File(...),
serial: Optional[str] = Query(None, description="Optional serial-number hint (e.g. UM11719); falls back to the IDF filename's literal prefix when omitted"),
) -> dict:
"""
Multipart upload of one or more Thor (Micromate Series IV) IDF event
file binaries (`.IDFH` histogram, `.IDFW` waveform), typically
forwarded by `thor-watcher`'s SFM forwarder.
For each file:
1. Pair the binary with its `<binary>.txt` ASCII report when one
is present in the same upload.
2. Parse the report via `sfm.idf_ascii_report.parse_idf_report`
and copy the binary into the persistent store via
`WaveformStore.save_imported_idf`, writing a `.sfm.json`
sidecar with `source.kind = "idf-import"`.
3. Upsert a row into `events` (dedup'd on serial+timestamp).
**Paired Thor TXT reports.** Thor's TXT exporter writes a
per-event ASCII report next to each binary as `<binary>.txt`
(e.g. `UM11719_20231219163444.IDFW` + `UM11719_20231219163444.IDFW.txt`).
The thor-watcher forwarder ships both files in a single multipart
upload. If the report is present, its decoded fields (Tran/Vert/Long
PPV, ZC Freq, Peak Vector Sum, Mic PSPL, calibration, sensor
self-check results, project strings) land in the sidecar's
`extensions.idf_report` block and the SFM `events` row's
device-authoritative columns.
Pairing is by exact filename match (case-insensitive): a binary
named `foo.IDFW` is paired with a report named `foo.IDFW.txt` or
`foo.IDFW.TXT`.
Response includes per-file outcomes so the watcher can see which
landed cleanly and which failed (e.g. malformed file, unknown
serial, etc.).
"""
store = _get_store()
db = _get_db()
results: list[dict] = []
binaries: list[tuple[str, bytes]] = []
reports: dict[str, bytes] = {} # keyed by lower-cased binary filename
for upload in files:
name = upload.filename or ""
try:
content = await upload.read()
except Exception as exc:
results.append({
"filename": name or "<unnamed>", "status": "error",
"detail": f"read failed: {exc}",
})
continue
if name.lower().endswith(".txt"):
# Thor convention: <binary>.txt — strip the trailing ".txt"
# to recover the binary's filename.
stripped = name[:-4]
reports[stripped.lower()] = content
else:
binaries.append((name, content))
for filename, content in binaries:
report_bytes = reports.get(filename.lower())
try:
ev, rec = store.save_imported_idf(
content,
source_path=Path(filename or "imported.idf"),
serial_hint=serial,
idf_report_text=report_bytes,
)
resolved_serial = (
serial
or rec.get("serial")
or "UNKNOWN"
)
inserted, skipped = db.insert_events(
[ev],
serial=resolved_serial,
waveform_records={
ev._waveform_key.hex(): rec
} if ev._waveform_key else None,
)
results.append({
"filename": filename,
"status": "ok",
"stored_filename": rec["filename"],
"filesize": rec["filesize"],
"sha256": rec["sha256"],
"serial": resolved_serial,
"report_attached": report_bytes is not None,
"inserted": inserted,
"skipped": skipped,
})
except Exception as exc:
log.error("idf import failed for %s: %s", filename, exc, exc_info=True)
results.append({
"filename": filename, "status": "error",
"detail": str(exc),
})
# Surface unmatched .txt uploads so the daemon can detect mis-pairings.
used_report_keys = {fn.lower() for fn, _ in binaries}
for stem in reports.keys() - used_report_keys:
results.append({
"filename": stem + ".txt",
"status": "warning",
"detail": "Thor TXT report supplied but no matching binary in this upload",
})
return {"count": len(results), "results": results}
@app.get("/db/units/{serial}/waveforms.zip")
def db_unit_waveforms_zip(
serial: str,