feat: add thor/micromate compatibility v0.18.0
This commit is contained in:
@@ -413,6 +413,179 @@ class WaveformStore:
|
||||
"serial": serial,
|
||||
}
|
||||
|
||||
def save_imported_idf(
|
||||
self,
|
||||
idf_bytes: bytes,
|
||||
source_path: Path,
|
||||
*,
|
||||
serial_hint: Optional[str] = None,
|
||||
idf_report_text: Optional[Union[str, bytes]] = None,
|
||||
) -> tuple[Optional["Event"], dict]:
|
||||
"""
|
||||
Ingest a Thor (Micromate Series IV) IDF event file (`.IDFW` or
|
||||
`.IDFH`) produced by Thor's TXT exporter.
|
||||
|
||||
Thor binaries are stored as opaque bytes — seismo-relay doesn't
|
||||
decode the proprietary IDF binary format. Device-authoritative
|
||||
metadata comes from the paired `.IDFW.txt` / `.IDFH.txt` sidecar
|
||||
when supplied; we parse that text and surface its fields onto
|
||||
the returned Event so the SFM database row has real PPV/project
|
||||
values instead of NULLs.
|
||||
|
||||
Workflow:
|
||||
1. Parse the paired TXT report (when supplied) via
|
||||
`sfm.idf_ascii_report.parse_idf_report`.
|
||||
2. Build a minimal `Event` populated from the report fields
|
||||
(timestamp, peaks, project info, sample_rate, record_type).
|
||||
3. Resolve serial from filename prefix or `serial_hint`.
|
||||
4. Copy bytes verbatim into <root>/<serial>/<filename>.
|
||||
5. Write the `.sfm.json` sidecar with source.kind = "idf-import".
|
||||
|
||||
Returns (event, record_dict) so the endpoint can both insert
|
||||
into SeismoDb and surface the parsed event.
|
||||
"""
|
||||
from sfm.idf_ascii_report import (
|
||||
parse_idf_report,
|
||||
parse_event_filename,
|
||||
serial_from_filename as _idf_serial_from_filename,
|
||||
)
|
||||
from minimateplus.models import (
|
||||
Event, PeakValues, ProjectInfo, Timestamp,
|
||||
)
|
||||
|
||||
# Parse the .txt sidecar (best-effort; non-fatal on failure).
|
||||
report: dict = {}
|
||||
if idf_report_text is not None:
|
||||
try:
|
||||
report = parse_idf_report(idf_report_text)
|
||||
except Exception as exc:
|
||||
log.warning(
|
||||
"save_imported_idf: report parse failed: %s — continuing without it",
|
||||
exc,
|
||||
)
|
||||
|
||||
# Resolve serial: prefer the explicit hint, fall back to filename prefix.
|
||||
serial = (
|
||||
serial_hint
|
||||
or report.get("serial_number")
|
||||
or _idf_serial_from_filename(source_path.name)
|
||||
or "UNKNOWN"
|
||||
)
|
||||
|
||||
# Resolve event timestamp + kind from the filename (always present).
|
||||
parsed_name = parse_event_filename(source_path.name)
|
||||
kind = "Waveform"
|
||||
ts_dt: Optional[datetime.datetime] = None
|
||||
if parsed_name is not None:
|
||||
_, ts_dt, kind_token = parsed_name
|
||||
kind = "Histogram" if kind_token == "IDFH" else "Waveform"
|
||||
# Report's event_datetime is the device-authoritative value; prefer it.
|
||||
if "event_datetime" in report:
|
||||
try:
|
||||
ts_dt = datetime.datetime.fromisoformat(report["event_datetime"])
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
|
||||
ts_obj: Optional[Timestamp] = None
|
||||
if ts_dt is not None:
|
||||
ts_obj = Timestamp(
|
||||
raw=bytes(9),
|
||||
flag=0,
|
||||
year=ts_dt.year,
|
||||
unknown_byte=0,
|
||||
month=ts_dt.month,
|
||||
day=ts_dt.day,
|
||||
hour=ts_dt.hour,
|
||||
minute=ts_dt.minute,
|
||||
second=ts_dt.second,
|
||||
)
|
||||
|
||||
# Build PeakValues from the report (fields are None when absent).
|
||||
pv = PeakValues(
|
||||
tran=report.get("tran_ppv"),
|
||||
vert=report.get("vert_ppv"),
|
||||
long=report.get("long_ppv"),
|
||||
micl=report.get("mic_ppv"),
|
||||
peak_vector_sum=report.get("peak_vector_sum"),
|
||||
)
|
||||
|
||||
# Build ProjectInfo. See idf_ascii_report — Thor's title strings
|
||||
# carry project / client / company / notes in TitleString1..4.
|
||||
pi = ProjectInfo(
|
||||
setup_name=report.get("setup"),
|
||||
project=report.get("project"),
|
||||
client=report.get("client"),
|
||||
operator=report.get("operator"),
|
||||
sensor_location=None, # Thor folds location into TitleString1 = project
|
||||
notes=report.get("notes"),
|
||||
)
|
||||
|
||||
# Filesystem write.
|
||||
filename = source_path.name
|
||||
bw_path = self._serial_dir(serial) / filename
|
||||
bw_path.write_bytes(idf_bytes)
|
||||
filesize = bw_path.stat().st_size
|
||||
sha256 = event_file_io.file_sha256(bw_path)
|
||||
|
||||
# _waveform_key dedups (serial, timestamp) rows in the events
|
||||
# table. Use the binary's sha256 (first 16 bytes) as a stable
|
||||
# surrogate — every distinct binary maps to a distinct row.
|
||||
waveform_key = bytes.fromhex(sha256)[:16]
|
||||
|
||||
ev = Event(
|
||||
index=0,
|
||||
timestamp=ts_obj,
|
||||
sample_rate=report.get("sample_rate"),
|
||||
peak_values=pv,
|
||||
project_info=pi,
|
||||
record_type=kind,
|
||||
rectime_seconds=report.get("record_time_sec"),
|
||||
)
|
||||
ev._waveform_key = waveform_key
|
||||
|
||||
# Write the sidecar. Source kind "idf-import" was added to the
|
||||
# allow-list in event_file_io.event_to_sidecar_dict for this.
|
||||
sidecar_path = self.sidecar_path_for(serial, filename)
|
||||
existing_review = None
|
||||
if sidecar_path.exists():
|
||||
try:
|
||||
existing_review = event_file_io.read_sidecar(sidecar_path).get("review")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
sidecar = event_file_io.event_to_sidecar_dict(
|
||||
ev,
|
||||
serial=serial,
|
||||
blastware_filename=filename,
|
||||
blastware_filesize=filesize,
|
||||
blastware_sha256=sha256,
|
||||
source_kind="idf-import",
|
||||
a5_pickle_filename=None,
|
||||
review=existing_review,
|
||||
)
|
||||
# Stash the full parsed IDF report under extensions so downstream
|
||||
# consumers can recover the rich derived fields that don't fit
|
||||
# the BW-shaped event model (Peak Acceleration / Displacement,
|
||||
# Time of Peak, sensor self-check, calibration, firmware).
|
||||
if report:
|
||||
sidecar["extensions"]["idf_report"] = report
|
||||
event_file_io.write_sidecar(sidecar_path, sidecar)
|
||||
|
||||
log.info(
|
||||
"WaveformStore.save_imported_idf serial=%s filename=%s filesize=%d "
|
||||
"report_attached=%s",
|
||||
serial, filename, filesize, bool(report),
|
||||
)
|
||||
return ev, {
|
||||
"filename": filename,
|
||||
"filesize": filesize,
|
||||
"sha256": sha256,
|
||||
"a5_pickle_filename": None,
|
||||
"hdf5_filename": None,
|
||||
"sidecar_filename": sidecar_path.name,
|
||||
"serial": serial,
|
||||
}
|
||||
|
||||
def load_a5(self, serial: str, filename: str) -> Optional[list[S3Frame]]:
|
||||
"""
|
||||
Re-hydrate the pickled A5 frame stream for a stored event.
|
||||
|
||||
Reference in New Issue
Block a user