feat(blastware_file): classify A5 frames, only write waveform frames to body
Add classify_frame() which categorises each A5 frame by content:
terminator — page_key == 0x0000
probe_or_strt — contains b"STRT"
metadata — contains compliance-config ASCII markers
(Project:, Client:, Standard Recording Setup, …)
waveform — binary-heavy (< 20% printable ASCII), i.e. raw ADC data
unknown — fallback
Update write_blastware_file() body loop: frame 0 (probe) is still
always processed; frames 1+ are only included when classify_frame
returns "waveform". Metadata frames (compliance config block with
Project:/Client:/etc.) and any stray STRT-bearing frames are skipped
with a warning/debug log. Terminator frame handling is unchanged.
Adds temporary print() diagnostics so each frame's classification is
visible in the server log to aid debugging.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -151,6 +151,7 @@ MLG CRC:
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
|
import logging
|
||||||
import struct
|
import struct
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional, Union
|
from typing import Optional, Union
|
||||||
@@ -158,6 +159,8 @@ from typing import Optional, Union
|
|||||||
from .framing import S3Frame
|
from .framing import S3Frame
|
||||||
from .models import Event, MonitorLogEntry, Timestamp
|
from .models import Event, MonitorLogEntry, Timestamp
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
# ── File header constants ─────────────────────────────────────────────────────
|
# ── File header constants ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
# Common 16-byte prefix shared by waveform files and MLG (confirmed from binary inspection).
|
# 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
|
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 ───────────────────────────────────────────────────────────
|
# ── Waveform file writer ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def write_blastware_file(
|
def write_blastware_file(
|
||||||
@@ -631,12 +679,29 @@ def write_blastware_file(
|
|||||||
all_bytes = bytearray()
|
all_bytes = bytearray()
|
||||||
|
|
||||||
for fi, frame in enumerate(body_frames):
|
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:
|
if fi == 0:
|
||||||
|
# Probe frame: always process regardless of classification.
|
||||||
|
# It holds the STRT record; probe_skip positions us past it.
|
||||||
skip = probe_skip
|
skip = probe_skip
|
||||||
elif fi == 1:
|
elif ftype == "waveform":
|
||||||
skip = 13 # 7-byte frame.data prefix + 6-byte first-chunk header
|
skip = 13 if fi == 1 else 12
|
||||||
else:
|
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))
|
all_bytes.extend(_frame_body_bytes(frame, skip))
|
||||||
|
|
||||||
# Terminator contributes its content, which ends with the 26-byte footer.
|
# Terminator contributes its content, which ends with the 26-byte footer.
|
||||||
|
|||||||
Reference in New Issue
Block a user