feat: add thor/micromate compatibility v0.18.0
This commit is contained in:
+117
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user