diff --git a/minimateplus/blastware_file.py b/minimateplus/blastware_file.py index c9bbe49..53128e2 100644 --- a/minimateplus/blastware_file.py +++ b/minimateplus/blastware_file.py @@ -151,6 +151,7 @@ MLG CRC: from __future__ import annotations import datetime +import logging import struct from pathlib import Path from typing import Optional, Union @@ -158,6 +159,8 @@ from typing import Optional, Union from .framing import S3Frame from .models import Event, MonitorLogEntry, Timestamp +log = logging.getLogger(__name__) + # ── File header constants ───────────────────────────────────────────────────── # Common 16-byte prefix shared by waveform files and MLG (confirmed from binary inspection). @@ -502,6 +505,51 @@ def blastware_filename(event: Event, serial: str, ach: bool = False) -> str: return prefix + stem + ext +# ── A5 frame classifier ─────────────────────────────────────────────────────────── + +# ASCII markers that identify a compliance-config / metadata frame. +# These strings appear in the A5 bulk stream as part of the device's +# compliance setup payload. They should NEVER appear in raw ADC waveform +# frames (which are binary-heavy, < 20 % printable ASCII). +_METADATA_FRAME_MARKERS = ( + b"Project:", + b"Client:", + b"Standard Recording Setup", + b"Extended Notes", + b"User Name:", + b"Seis Loc:", +) + + +def classify_frame(frame: S3Frame) -> str: + """ + Classify an A5 bulk waveform stream frame by its content. + + Returns one of: + "terminator" — page_key == 0x0000 + "probe_or_strt" — data contains b"STRT" (the initial probe response) + "metadata" — data contains ASCII compliance-config markers + "waveform" — predominantly binary (< 20 % printable ASCII) + "unknown" — none of the above criteria matched + + Used by write_blastware_file() to filter non-waveform frames out of + the reconstructed body so that metadata blocks (Project:, Client:, …) + and spurious STRT records do not corrupt the output file. + """ + if frame.page_key == 0x0000: + return "terminator" + data = bytes(frame.data) + if b"STRT" in data: + return "probe_or_strt" + if any(m in data for m in _METADATA_FRAME_MARKERS): + return "metadata" + if len(data) > 0: + printable = sum(1 for b in data if 32 <= b < 127) + if printable / len(data) < 0.20: + return "waveform" + return "unknown" + + # ── Waveform file writer ─────────────────────────────────────────────────────────── def write_blastware_file( @@ -631,12 +679,29 @@ def write_blastware_file( all_bytes = bytearray() for fi, frame in enumerate(body_frames): + ftype = classify_frame(frame) + print(f"Frame {fi}: type={ftype}, page_key={frame.page_key:04x}, len={len(frame.data)}") + if fi == 0: + # Probe frame: always process regardless of classification. + # It holds the STRT record; probe_skip positions us past it. skip = probe_skip - elif fi == 1: - skip = 13 # 7-byte frame.data prefix + 6-byte first-chunk header + elif ftype == "waveform": + skip = 13 if fi == 1 else 12 else: - skip = 12 # 7-byte frame.data prefix + 5-byte chunk header + # Skip metadata, probe_or_strt, and unknown frames. + if b"STRT" in bytes(frame.data): + log.warning( + "write_blastware_file: frame %d (%s) contains STRT — skipping", + fi, ftype, + ) + else: + log.debug( + "write_blastware_file: frame %d classified as %s — skipping", + fi, ftype, + ) + continue + all_bytes.extend(_frame_body_bytes(frame, skip)) # Terminator contributes its content, which ends with the 26-byte footer.