fix: update timestamp decoding for Waveform and Continuous records in models and client
This commit is contained in:
@@ -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 |
|
||||||
| 3–4 | year | uint16 BE |
|
| 3–4 | year | uint16 BE |
|
||||||
| 5 | unknown | uint8 (always 0) |
|
| 5 | unknown | uint8 (always 0) |
|
||||||
|
|||||||
@@ -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})"
|
||||||
|
|
||||||
|
|||||||
@@ -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[3–4]: year (big-endian uint16)
|
bytes[3–4]: 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[4–5]: 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)."""
|
||||||
|
|||||||
Reference in New Issue
Block a user