From dda56835720ecb7f1e78a9c29a4a3983c4407523 Mon Sep 17 00:00:00 2001 From: Brian Harrison Date: Wed, 8 Apr 2026 18:29:51 -0400 Subject: [PATCH] fix: improve monitoring functionality with session-reset signal and payload adjustments --- CLAUDE.md | 30 +++++++++++++++++++++++------- minimateplus/client.py | 25 ++++++++++++++++--------- minimateplus/framing.py | 8 ++++++++ minimateplus/protocol.py | 12 ++++++++++++ 4 files changed, 59 insertions(+), 16 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 20aa160..da501b2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -582,17 +582,33 @@ All confirmed from 4-8-26/2ndtry BW TX/S3 capture (clean start → 30s monitor Standard two-step read (probe at offset 0x00, data at offset 0x2C). Response SUB = 0xFF − 0x1C = **0xE3** (standard formula — no exception). -Payload length indicates mode: -- **44 bytes (0x2C)** — unit is **idle**: full status block with battery + memory -- **12 bytes** — unit is **monitoring**: abbreviated block, no battery/memory fields +**Payload length is ~46–49 bytes in BOTH idle and monitoring states** — length alone +is NOT a reliable mode indicator. Earlier note claiming "12 bytes when monitoring" +was wrong (confirmed 2026-04-08 from 4-8-26/mid-monitor captures). -**Field offsets (idle payload, relative to the 11-byte section header start):** +**Monitoring flag (confirmed from 4-8-26/2ndtry byte-by-byte comparison):** +- `section[1] == 0x00` → unit is **idle** +- `section[1] == 0x10` → unit is **monitoring** (flips exactly at start/stop transitions) + +Battery and memory fields are present in **both** states. + +**Field offsets (relative to `data[11:]` = section, confirmed from 2ndtry IDLE frames):** | Offset | Field | Type | Notes | |---|---|---|---| -| `[0x2F:0x31]` | battery voltage × 100 | uint16 BE | `0x02A8` = 680 → 6.80 V | -| `[0x31:0x35]` | memory total (bytes) | uint32 BE | e.g. 983040 = 960 KB | -| `[0x35:0x39]` | memory free (bytes) | uint32 BE | | +| `[36:38]` | battery voltage × 100 | uint16 BE | `0x02A8` = 680 → 6.80 V | +| `[38:42]` | memory total (bytes) | uint32 BE | e.g. 983026 ≈ 960 KB | +| `[42:46]` | memory free (bytes) | uint32 BE | decreases as events are stored | + +### SESSION_RESET signal (`41 03`) — required for monitoring units + +Confirmed from 4-8-26 BW TX captures: Blastware sends a 2-byte `41 03` (ACK + ETX, +no STX) immediately before the first POLL probe AND between the probe and data frames. +This signal is **required to wake units that are actively monitoring** — without it +the unit does not respond to POLL over TCP. Harmless for idle units. + +`SESSION_RESET = bytes([0x41, 0x03])` is defined in `framing.py` and sent by +`protocol.startup()` before and between POLL frames. ### SUB 0x96 — Start monitoring diff --git a/minimateplus/client.py b/minimateplus/client.py index bc33bde..f7d50c6 100644 --- a/minimateplus/client.py +++ b/minimateplus/client.py @@ -1767,25 +1767,32 @@ def _decode_monitor_status(data: bytes) -> MonitorStatus: # The data section starts at offset 11 (after the S3 section header). section = data[11:] if len(data) > 11 else data - # Log the raw payload at WARNING level so we can see it in the server logs - # and confirm the field offsets and is_monitoring detection are correct. - log.warning( + log.debug( "_decode_monitor_status: total data=%d bytes section=%d bytes hex=%s", len(data), len(section), section.hex(), ) - # Mode: idle payload is 44 bytes; monitoring is shorter (12 bytes observed) - is_monitoring = len(section) < 20 + # Monitoring flag confirmed from 2ndtry 4-8-26 capture: + # section[1] == 0x00 → idle + # section[1] == 0x10 → monitoring (flips exactly at start/stop transitions) + # Payload length is ~46-49 bytes in BOTH states — length alone is unreliable. + is_monitoring = len(section) > 1 and section[1] == 0x10 battery_v = None memory_total = None memory_free = None - if not is_monitoring and len(section) >= 0x39: - batt_raw = struct.unpack(">H", section[0x2F:0x31])[0] + # Battery and memory fields confirmed from 2ndtry IDLE S3 frames: + # section[36:38] battery × 100 uint16 BE 0x02A8 = 680 → 6.80 V + # section[38:42] memory_total uint32 BE bytes = total flash + # section[42:46] memory_free uint32 BE bytes = free flash + # These fields appear in BOTH idle and monitoring payloads (battery/memory + # are available regardless of mode). + if len(section) >= 46: + batt_raw = struct.unpack(">H", section[36:38])[0] battery_v = batt_raw / 100.0 - memory_total = struct.unpack(">I", section[0x31:0x35])[0] - memory_free = struct.unpack(">I", section[0x35:0x39])[0] + memory_total = struct.unpack(">I", section[38:42])[0] + memory_free = struct.unpack(">I", section[42:46])[0] return MonitorStatus( is_monitoring=is_monitoring, diff --git a/minimateplus/framing.py b/minimateplus/framing.py index 7ca6e75..31c1fba 100644 --- a/minimateplus/framing.py +++ b/minimateplus/framing.py @@ -438,6 +438,14 @@ def bulk_waveform_term_params(key4: bytes, counter: int) -> bytes: POLL_PROBE = build_bw_frame(0x5B, 0x00) # length-probe POLL (offset = 0) POLL_DATA = build_bw_frame(0x5B, 0x30) # data-request POLL (offset = 0x30) +# Session-reset signal (ACK + ETX, no STX/payload). +# Confirmed from 4-8-26 BW TX captures: Blastware sends this 2-byte sequence +# immediately before the first POLL probe, and again between the POLL probe +# and the POLL data request. Required to wake a unit that is actively +# monitoring — without it the unit does not respond to POLL over TCP. +# Harmless for idle units (they respond to POLL regardless). +SESSION_RESET = bytes([0x41, 0x03]) + # ── S3 response dataclass ───────────────────────────────────────────────────── diff --git a/minimateplus/protocol.py b/minimateplus/protocol.py index 36a6b7e..04b77d8 100644 --- a/minimateplus/protocol.py +++ b/minimateplus/protocol.py @@ -37,6 +37,7 @@ from .framing import ( bulk_waveform_term_params, POLL_PROBE, POLL_DATA, + SESSION_RESET, ) from .transport import BaseTransport @@ -190,6 +191,13 @@ class MiniMateProtocol: log.debug("startup: draining boot string") self._drain_boot_string() + # Send session-reset signal (ACK+ETX) before the first POLL probe. + # Confirmed from 4-8-26 BW TX captures: Blastware always sends this + # 2-byte signal at session start. Required to wake units that are + # actively monitoring — without it they don't respond to POLL over TCP. + log.debug("startup: session reset signal") + self._send(SESSION_RESET) + log.debug("startup: POLL probe") self._send(POLL_PROBE) probe_rsp = self._recv_one( @@ -200,6 +208,10 @@ class MiniMateProtocol: "startup: POLL probe response page_key=0x%04X", probe_rsp.page_key ) + # Send another session-reset between probe and data (matches BW behavior). + log.debug("startup: session reset signal (inter-frame)") + self._send(SESSION_RESET) + log.debug("startup: POLL data request") self._send(POLL_DATA) data_rsp = self._recv_one(