fix: update timestamp decoding for Waveform and Continuous records in models and client

This commit is contained in:
Brian Harrison
2026-04-04 00:09:55 -04:00
parent 2286d2ccf8
commit 1c570b083a
3 changed files with 77 additions and 12 deletions

View File

@@ -268,7 +268,7 @@ Anchor: `b'\x01\x2c\x00\x00\xbe\x80\x00\x00\x00\x00'`, search `cfg[0:150]`
| Offset | Field | Type | | Offset | Field | Type |
|---|---|---| |---|---|---|
| 0 | day | uint8 | | 0 | day | uint8 |
| 1 | sub_code | uint8 (`0x10` = Waveform, `0x03` = MonitorLog) | | 1 | sub_code | uint8 (`0x10` = Waveform single-shot, `0x03` = Waveform continuous) |
| 2 | month | uint8 | | 2 | month | uint8 |
| 34 | year | uint16 BE | | 34 | year | uint16 BE |
| 5 | unknown | uint8 (always 0) | | 5 | unknown | uint8 (always 0) |

View File

@@ -625,14 +625,18 @@ def _decode_waveform_record_into(data: bytes, event: Event) -> None:
# ── Timestamp ───────────────────────────────────────────────────────────── # ── Timestamp ─────────────────────────────────────────────────────────────
# 9-byte format for sub_code=0x10 Waveform records: # 9-byte format for sub_code=0x10 Waveform records:
# [day][sub_code][month][year:2 BE][unknown][hour][min][sec] # [day][sub_code][month][year:2 BE][unknown][hour][min][sec]
# MonitorLog (sub_code=0x03) records have a different byte layout — applying # sub_code=0x10 and sub_code=0x03 have different timestamp byte layouts.
# the waveform layout produces garbage (year=1031, month=16). Leave timestamp # Both confirmed against Blastware event reports (BE11529, 2026-04-01 and 2026-04-03).
# None for non-Waveform records until the correct layout is confirmed.
if event.record_type == "Waveform": if event.record_type == "Waveform":
try: try:
event.timestamp = Timestamp.from_waveform_record(data) event.timestamp = Timestamp.from_waveform_record(data)
except Exception as exc: except Exception as exc:
log.warning("waveform record timestamp decode failed: %s", exc) log.warning("waveform record timestamp decode failed: %s", exc)
elif event.record_type == "Waveform (Continuous)":
try:
event.timestamp = Timestamp.from_continuous_record(data)
except Exception as exc:
log.warning("continuous record timestamp decode failed: %s", exc)
# ── Peak values (per-channel PPV + Peak Vector Sum) ─────────────────────── # ── Peak values (per-channel PPV + Peak Vector Sum) ───────────────────────
try: try:
@@ -936,11 +940,11 @@ def _extract_record_type(data: bytes) -> Optional[str]:
if code == 0x10: if code == 0x10:
return "Waveform" return "Waveform"
if code == 0x03: if code == 0x03:
# Monitor log record — Instantel's periodic time-weighted average record. # Continuous mode waveform record (confirmed by user — NOT a monitor log).
# Appears in BW's event list but NOT on the physical device display. # The byte layout differs from 0x10 single-shot records: the timestamp
# The byte layout differs from 0x10 waveform records: the timestamp fields # fields decode as garbage under the 0x10 waveform layout.
# decode as garbage under the waveform layout and should not be trusted. # TODO: confirm correct timestamp layout for 0x03 records from a known-time event.
return "MonitorLog" return "Waveform (Continuous)"
log.warning("_extract_record_type: unknown sub_code=0x%02X", code) log.warning("_extract_record_type: unknown sub_code=0x%02X", code)
return f"Unknown(0x{code:02X})" return f"Unknown(0x{code:02X})"

View File

@@ -98,14 +98,17 @@ class Timestamp:
Wire layout (✅ CONFIRMED 2026-04-01 against Blastware event report): Wire layout (✅ CONFIRMED 2026-04-01 against Blastware event report):
byte[0]: day (uint8) byte[0]: day (uint8)
byte[1]: sub_code / mode flag (0x10 = Waveform mode) 🔶 byte[1]: sub_code / mode flag (0x10 = Waveform single-shot)
byte[2]: month (uint8) byte[2]: month (uint8)
bytes[34]: year (big-endian uint16) bytes[34]: year (big-endian uint16)
byte[5]: unknown (0x00 in all observed samples) byte[5]: unknown (0x00 in all observed samples)
byte[6]: hour (uint8) byte[6]: hour (uint8)
byte[7]: minute (uint8) byte[7]: minute (uint8)
byte[8]: second (uint8) byte[8]: second (uint8)
Used for sub_code=0x10 records only. For sub_code=0x03 (continuous
mode) use from_continuous_record() — the layout is shifted by 1 byte.
Args: Args:
data: at least 9 bytes; only the first 9 are consumed. data: at least 9 bytes; only the first 9 are consumed.
@@ -120,7 +123,7 @@ class Timestamp:
f"Waveform record timestamp requires at least 9 bytes, got {len(data)}" f"Waveform record timestamp requires at least 9 bytes, got {len(data)}"
) )
day = data[0] day = data[0]
sub_code = data[1] # 0x10 = Waveform; histogram code not yet confirmed sub_code = data[1] # 0x10 = Waveform single-shot
month = data[2] month = data[2]
year = struct.unpack_from(">H", data, 3)[0] year = struct.unpack_from(">H", data, 3)[0]
unknown_byte = data[5] unknown_byte = data[5]
@@ -139,6 +142,64 @@ class Timestamp:
second=second, second=second,
) )
@classmethod
def from_continuous_record(cls, data: bytes) -> "Timestamp":
"""
Decode a 10-byte timestamp from the first bytes of a sub_code=0x03
(Waveform Continuous) 210-byte record.
Wire layout (✅ CONFIRMED 2026-04-03 against Blastware event report,
event recorded at 15:20:17 April 3 2026, raw: 10 03 10 04 07 ea 00 0f 14 11):
byte[0]: unknown_a (0x10 observed — meaning TBD)
byte[1]: day (uint8)
byte[2]: unknown_b (0x10 observed — meaning TBD)
bytes[3]: month (uint8)
bytes[45]: year (big-endian uint16)
byte[6]: unknown (0x00 in all observed samples)
byte[7]: hour (uint8)
byte[8]: minute (uint8)
byte[9]: second (uint8)
This is the sub_code=0x10 layout shifted forward by 1 byte, with two
extra unknown bytes at [0] and [2]. The sub_code (0x03) itself is at
byte[1] in the raw record, which also encodes the day — but the day
value (3 = April 3rd) happens to differ from the sub_code (0x03) only
in semantics; the byte is shared.
Args:
data: at least 10 bytes; only the first 10 are consumed.
Returns:
Decoded Timestamp with hour/minute/second populated.
Raises:
ValueError: if data is fewer than 10 bytes.
"""
if len(data) < 10:
raise ValueError(
f"Continuous record timestamp requires at least 10 bytes, got {len(data)}"
)
unknown_a = data[0] # 0x10 observed; meaning unknown
day = data[1] # doubles as the sub_code byte (0x03) — day=3 on Apr 3
unknown_b = data[2] # 0x10 observed; meaning unknown
month = data[3]
year = struct.unpack_from(">H", data, 4)[0]
unknown_byte = data[6]
hour = data[7]
minute = data[8]
second = data[9]
return cls(
raw=bytes(data[:10]),
flag=unknown_a,
year=year,
unknown_byte=unknown_byte,
month=month,
day=day,
hour=hour,
minute=minute,
second=second,
)
@property @property
def clock_set(self) -> bool: def clock_set(self) -> bool:
"""False when year == 1995 (factory default / battery-lost state).""" """False when year == 1995 (factory default / battery-lost state)."""