diff --git a/CLAUDE.md b/CLAUDE.md index a93fa71..882ae66 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -268,7 +268,7 @@ Anchor: `b'\x01\x2c\x00\x00\xbe\x80\x00\x00\x00\x00'`, search `cfg[0:150]` | Offset | Field | Type | |---|---|---| | 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 | | 3–4 | year | uint16 BE | | 5 | unknown | uint8 (always 0) | diff --git a/minimateplus/client.py b/minimateplus/client.py index a13217a..9c1389a 100644 --- a/minimateplus/client.py +++ b/minimateplus/client.py @@ -625,14 +625,18 @@ def _decode_waveform_record_into(data: bytes, event: Event) -> None: # ── Timestamp ───────────────────────────────────────────────────────────── # 9-byte format for sub_code=0x10 Waveform records: # [day][sub_code][month][year:2 BE][unknown][hour][min][sec] - # MonitorLog (sub_code=0x03) records have a different byte layout — applying - # the waveform layout produces garbage (year=1031, month=16). Leave timestamp - # None for non-Waveform records until the correct layout is confirmed. + # sub_code=0x10 and sub_code=0x03 have different timestamp byte layouts. + # Both confirmed against Blastware event reports (BE11529, 2026-04-01 and 2026-04-03). if event.record_type == "Waveform": try: event.timestamp = Timestamp.from_waveform_record(data) except Exception as 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) ─────────────────────── try: @@ -936,11 +940,11 @@ def _extract_record_type(data: bytes) -> Optional[str]: if code == 0x10: return "Waveform" if code == 0x03: - # Monitor log record — Instantel's periodic time-weighted average record. - # Appears in BW's event list but NOT on the physical device display. - # The byte layout differs from 0x10 waveform records: the timestamp fields - # decode as garbage under the waveform layout and should not be trusted. - return "MonitorLog" + # Continuous mode waveform record (confirmed by user — NOT a monitor log). + # The byte layout differs from 0x10 single-shot records: the timestamp + # fields decode as garbage under the 0x10 waveform layout. + # TODO: confirm correct timestamp layout for 0x03 records from a known-time event. + return "Waveform (Continuous)" log.warning("_extract_record_type: unknown sub_code=0x%02X", code) return f"Unknown(0x{code:02X})" diff --git a/minimateplus/models.py b/minimateplus/models.py index bff0388..a6dcbec 100644 --- a/minimateplus/models.py +++ b/minimateplus/models.py @@ -98,14 +98,17 @@ class Timestamp: Wire layout (✅ CONFIRMED 2026-04-01 against Blastware event report): 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) 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[7]: minute (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: 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)}" ) 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] year = struct.unpack_from(">H", data, 3)[0] unknown_byte = data[5] @@ -139,6 +142,64 @@ class Timestamp: 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 def clock_set(self) -> bool: """False when year == 1995 (factory default / battery-lost state)."""