fix(sfm): read the serial family prefix from the file, enabling BlastMates
The BW filename encodes only the serial NUMBER — `<letter><3 digits>`, so
`L895…` is 10895 and nothing more. The two-letter family prefix is not in it:
"BE" is a MiniMate Plus, "BA" a BlastMate. Both are Series III and their
files are byte-identical in every way that matters — all 1,493 BlastMate
binaries in the DL2 archive decode through the existing codec at 100%, same
four channels — so the serial string was the only thing standing between SFM
and BlastMate support.
Two sites synthesised the prefix and got it wrong:
- waveform_store `_serial_from_bw_filename` returned f"BE{num}" on import, so
a BlastMate event was filed under a unit that does not exist, silently, and
Terra-View read it straight through. Split into
`_serial_number_from_bw_filename` (the number, which the filename really
does carry) and a new `_serial_from_bw_bytes` that reads the serial out of
the body and accepts it only when its numeric part agrees with the
filename. save_imported_bw now prefers hint -> body -> filename guess.
Verified against real archive bytes for BA9229, BA10060, BA10895, BA15957
and BE9558/BE11529/BE18003.
- client `_decode_0a_partial_header` searched for a literal b"BE" in the
monitor-log partial record. On a BlastMate that returns -1 and skips the
whole block, so the geo threshold went missing along with the serial. Now
matches any two-letter prefix, and requires the NUL terminator — stricter
than the bare two-byte search it replaces.
Nothing to migrate: no BlastMate events are in prod. The archive's BA units
last recorded 2018-10 (BA9229, BA15957), 2023-08 (BA10895) and 2023-11
(BA10060), and the prod backfill only reaches back to ~May 2025.
21 tests. Suite: 309 passed, same 16 pre-existing failures as at HEAD
(15 missing ASCII fixtures + one peak_values assertion, all untouched here).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HgTe8CamXAHcAmaQ6QNcog
This commit is contained in:
+61
-10
@@ -32,6 +32,7 @@ from __future__ import annotations
|
||||
import datetime
|
||||
import logging
|
||||
import pickle
|
||||
import re
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from typing import Optional, Union
|
||||
@@ -379,8 +380,16 @@ class WaveformStore:
|
||||
|
||||
# Resolve serial. blastware_filename derives a 4-char prefix from
|
||||
# the numeric serial (e.g. BE11529 → M529); we go the other way
|
||||
# via the source filename if a hint wasn't given.
|
||||
serial = serial_hint or _serial_from_bw_filename(source_path.name) or "UNKNOWN"
|
||||
# if a hint wasn't given. The filename carries only the NUMBER,
|
||||
# so read the family prefix out of the body first — a BlastMate
|
||||
# ("BA") filed as "BE" is a unit that does not exist. The
|
||||
# filename-only decoder stays as the last resort.
|
||||
serial = (
|
||||
serial_hint
|
||||
or _serial_from_bw_bytes(bw_bytes, source_path.name)
|
||||
or _serial_from_bw_filename(source_path.name)
|
||||
or "UNKNOWN"
|
||||
)
|
||||
|
||||
# Use the source filename verbatim — it already encodes timestamp
|
||||
# + record type per BW's AB0T scheme, and we want to preserve it
|
||||
@@ -840,20 +849,24 @@ class WaveformStore:
|
||||
|
||||
# ── helpers ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
def _serial_from_bw_filename(name: str) -> Optional[str]:
|
||||
def _serial_number_from_bw_filename(name: str) -> Optional[int]:
|
||||
"""
|
||||
Reverse of `blastware_filename`'s serial-prefix encoding.
|
||||
Reverse of `blastware_filename`'s serial-prefix encoding — the NUMBER only.
|
||||
|
||||
BW filename format (V10.72): `<P><serial3><stem4>.<ext>`
|
||||
where P = chr(ord('B') + floor(serial // 1000))
|
||||
and serial3 = f"{serial % 1000:03d}".
|
||||
|
||||
Examples (from CLAUDE.md verification archive):
|
||||
P036... → BE14036 H907... → BE6907
|
||||
M529... → BE11529 T003... → BE18003
|
||||
P036... → 14036 H907... → 6907
|
||||
M529... → 11529 T003... → 18003
|
||||
L895... → 10895
|
||||
|
||||
Returns the inferred BE-prefix serial (e.g. "BE11529") or None when
|
||||
the filename doesn't match the expected pattern.
|
||||
⚠ The filename encodes **only the number**. The two-letter family
|
||||
prefix is NOT in it — "BE" is a MiniMate Plus, "BA" a BlastMate — so
|
||||
the prefix has to come from the file body (`_serial_from_bw_bytes`)
|
||||
or from an explicit hint. Returns None when the filename doesn't
|
||||
match the expected pattern.
|
||||
"""
|
||||
if not name:
|
||||
return None
|
||||
@@ -866,5 +879,43 @@ def _serial_from_bw_filename(name: str) -> Optional[str]:
|
||||
if prefix_letter < "B":
|
||||
return None
|
||||
thousands = ord(prefix_letter) - ord("B")
|
||||
serial_num = thousands * 1000 + int(base[1:4])
|
||||
return f"BE{serial_num}"
|
||||
return thousands * 1000 + int(base[1:4])
|
||||
|
||||
|
||||
_BW_SERIAL_RE = re.compile(rb"[A-Z]{2}\d{3,6}")
|
||||
|
||||
|
||||
def _serial_from_bw_bytes(data: bytes, name: str) -> Optional[str]:
|
||||
"""
|
||||
Read the real serial — prefix included — out of a BW file body.
|
||||
|
||||
The body carries the serial as a plain ASCII string ("BE9558",
|
||||
"BA10895"). We accept a candidate only when its numeric part matches
|
||||
the number the filename encodes, which keeps a stray byte sequence in
|
||||
the sample stream from being mistaken for a serial.
|
||||
|
||||
Returns None when the filename number can't be derived or no
|
||||
candidate in the body agrees with it — the caller then falls back.
|
||||
"""
|
||||
num = _serial_number_from_bw_filename(name)
|
||||
if num is None or not data:
|
||||
return None
|
||||
for match in _BW_SERIAL_RE.findall(data):
|
||||
candidate = match.decode("ascii", errors="replace")
|
||||
if candidate[2:].lstrip("0") == str(num):
|
||||
return candidate
|
||||
return None
|
||||
|
||||
|
||||
def _serial_from_bw_filename(name: str) -> Optional[str]:
|
||||
"""
|
||||
Best-effort serial from the filename alone.
|
||||
|
||||
⚠ The family prefix is a **guess** — the filename does not carry it.
|
||||
"BE" is right for every MiniMate Plus but wrong for a BlastMate, whose
|
||||
serials start "BA". Prefer `_serial_from_bw_bytes` whenever the file
|
||||
body is at hand; this exists for callers that only have a name
|
||||
(log lines, dry-run output).
|
||||
"""
|
||||
num = _serial_number_from_bw_filename(name)
|
||||
return None if num is None else f"BE{num}"
|
||||
|
||||
Reference in New Issue
Block a user