feat: decode waveform record timestamp, record type, and Peak Vector Sum

Confirmed 2026-04-01 against Blastware event report for BE11529 thump
event ("00:28:12 April 1, 2026", PVS 3.906 in/s).

models.py:
- Timestamp.from_waveform_record(): decode 9-byte format from 0C record
  bytes[0-8]: [day][sub_code][month][year:2BE][?][hour][min][sec]
- Timestamp: add hour/minute/second optional fields; __str__ includes
  time when available
- PeakValues: add peak_vector_sum field (confirmed fixed offset 87)

client.py:
- _decode_waveform_record_into: add timestamp decode from bytes[0:9]
- _extract_record_type: decode byte[1] (sub_code), not ASCII string
  search; 0x10 → "Waveform", histogram TBD
- _extract_peak_floats: add PVS from offset 87 (IEEE 754 BE float32)
  = √(T²+V²+L²) at max instantaneous vector moment

sfm/server.py:
- _serialise_timestamp: add hour/minute/second/day fields to JSON
- _serialise_peak_values: add peak_vector_sum to JSON

docs: update §7.7.5 and §8 with confirmed 9-byte timestamp layout,
PVS field, and byte[1] record type encoding; update command table;
close resolved open questions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Brian Harrison
2026-04-01 00:53:34 -04:00
parent f74992f4e5
commit 4944974f6e
4 changed files with 257 additions and 73 deletions

View File

@@ -394,25 +394,30 @@ def _decode_waveform_record_into(data: bytes, event: Event) -> None:
The *data* argument is the raw record bytes returned by
MiniMateProtocol.read_waveform_record() — i.e. data_rsp.data[11:11+0xD2].
Extracts:
- record_type: "Histogram" or "Waveform" (string search) 🔶
- peak_values: label-based float32 lookup (confirmed ✅)
- project_info: "Project:", "Client:", etc. string search ✅
Timestamp in the waveform record:
7-byte format: [0x09][year:2 BE][0x00][hour][minute][second]
Month and day come from a separate source (not yet fully mapped ❓).
For now we leave event.timestamp as None.
Extracts (all ✅ confirmed 2026-04-01 against Blastware event report):
- timestamp: 9-byte format at bytes [0:9]
- record_type: sub_code at byte[1] (0x10 = "Waveform")
- peak_values: label-based float32 at label+6 for Tran/Vert/Long/MicL
- peak_vector_sum: IEEE 754 BE float at offset 87
- project_info: "Project:", "Client:", etc. string search
Modifies event in-place.
"""
# ── Timestamp ─────────────────────────────────────────────────────────────
# 9-byte format: [day][sub_code][month][year:2 BE][unknown][hour][min][sec]
try:
event.timestamp = Timestamp.from_waveform_record(data)
except Exception as exc:
log.warning("waveform record timestamp decode failed: %s", exc)
# ── Record type ───────────────────────────────────────────────────────────
# Decoded from byte[1] (sub_code), not from ASCII string search
try:
event.record_type = _extract_record_type(data)
except Exception as exc:
log.warning("waveform record type decode failed: %s", exc)
# ── Peak values ───────────────────────────────────────────────────────────
# ── Peak values (per-channel PPV + Peak Vector Sum) ───────────────────────
try:
peak_values = _extract_peak_floats(data)
if peak_values:
@@ -431,14 +436,24 @@ def _decode_waveform_record_into(data: bytes, event: Event) -> None:
def _extract_record_type(data: bytes) -> Optional[str]:
"""
Search the waveform record for a record-type indicator string.
Decode the recording mode from byte[1] of the 210-byte waveform record.
Confirmed types from 3-31-26 capture: "Histogram", "Waveform".
Returns the first match, or None if neither is found.
Byte[1] is the sub-record code that immediately follows the day byte in the
9-byte timestamp header at the start of each waveform record:
[day:1] [sub_code:1] [month:1] [year:2 BE] ...
Confirmed codes (✅ 2026-04-01):
0x10 → "Waveform" (continuous / single-shot mode)
Histogram mode code is not yet confirmed — a histogram event must be
captured with debug=true to identify it. Returns None for unknown codes.
"""
for rtype in (b"Histogram", b"Waveform"):
if data.find(rtype) >= 0:
return rtype.decode()
if len(data) < 2:
return None
code = data[1]
if code == 0x10:
return "Waveform"
# TODO: add histogram sub_code once a histogram event is captured with debug=true
return None
@@ -486,11 +501,23 @@ def _extract_peak_floats(data: bytes) -> Optional[PeakValues]:
if not vals:
return None
# ── Peak Vector Sum — fixed offset 87 (✅ confirmed 2026-04-01) ───────────
# = √(Tran² + Vert² + Long²) at the sample instant of maximum combined geo
# motion, NOT the vector sum of the three per-channel peak values (which may
# occur at different times). Matches Blastware "Peak Vector Sum" exactly.
pvs: Optional[float] = None
if len(data) > 91:
try:
pvs = struct.unpack_from(">f", data, 87)[0]
except struct.error:
pass
return PeakValues(
tran=vals.get("tran"),
vert=vals.get("vert"),
long=vals.get("long_"),
micl=vals.get("micl"),
peak_vector_sum=pvs,
)