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:
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user