fix: correct monitoring flag and battery/memory offsets in _decode_monitor_status

section[6] is the monitoring flag (was wrongly section[1] — section[1] is always
0x00 in both states). Battery and memory fields use relative-from-end offsets
(section[-11:-9], section[-9:-5], section[-5:-1]) instead of absolute positions,
which broke when the payload grew by 3 bytes in monitoring mode.

Confirmed from full byte diff of 142 0xE3 frames in 4-8-26/2ndtry capture.
SFM start_monitoring now polls /device/monitor/status every 5s for up to 60s
instead of a fixed 25s delay (unit runs ~40s on-device sensor check before
confirming monitoring state).

Also corrects stale 1C→6E response anomaly claim in protocol reference — no
exceptions to the 0xFF−SUB rule are known.
This commit is contained in:
2026-04-08 23:41:11 -04:00
parent dda5683572
commit 990cb8850e
4 changed files with 345 additions and 56 deletions
+35 -22
View File
@@ -1755,14 +1755,22 @@ def _decode_monitor_status(data: bytes) -> MonitorStatus:
data is the raw S3 frame .data attribute (includes the 11-byte section
header, so field offsets below are relative to data[11]).
Payload length indicates mode:
44 bytes (0x2C): idle — full status block with battery + memory fields
12 bytes : actively monitoring — abbreviated, no battery/memory
Monitoring flag (confirmed 4-8-26/2ndtry, full byte diff analysis):
section[6] == 0x00 → idle
section[6] == 0x10 → monitoring
Field offsets (idle mode, confirmed 4-8-26/2ndtry):
data[11 + 0x2F : 11 + 0x31] battery × 100 uint16 BE
data[11 + 0x31 : 11 + 0x35] memory_total uint32 BE bytes
data[11 + 0x35 : 11 + 0x39] memory_free uint32 BE bytes
The payload size varies (5255+ bytes) but the battery/memory block is
always the last 10 bytes before the trailing checksum byte:
section[-11:-9] battery × 100 uint16 BE (0x02A8 = 6.80 V)
section[-9 :-5] memory_total uint32 BE bytes
section[-5 :-1] memory_free uint32 BE bytes
section[-1] checksum (not data)
Values confirmed from 4-8-26/2ndtry capture (BE11529):
battery 0x02A8 = 680 → 6.80 V
mem_total 0x000EFFF2 = 983026 bytes ≈ 960 KB
mem_free 0x000E9E52 = 958034 bytes ≈ 935 KB
"""
# The data section starts at offset 11 (after the S3 section header).
section = data[11:] if len(data) > 11 else data
@@ -1772,27 +1780,32 @@ def _decode_monitor_status(data: bytes) -> MonitorStatus:
len(data), len(section), section.hex(),
)
# 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
# Monitoring flag: section[6] (CORRECTED 2026-04-08 — was wrongly section[1]).
# Byte diff of 2ndtry BW-S3 captures confirms section[6] flips 0x00↔0x10
# exactly at the start/stop monitoring transitions (0xE3 frame #36 / #132).
is_monitoring = len(section) > 6 and section[6] == 0x10
battery_v = None
memory_total = None
memory_free = None
# 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 and memory offsets are RELATIVE TO THE END of the section.
# The payload length varies (5255+ bytes) depending on monitoring state and
# internal counters, but the battery/memory block is always the last 10 bytes
# before the checksum (section[-1]).
#
# section[-11:-9] battery × 100 uint16 BE 0x02A8 = 6.80 V
# section[-9 :-5] memory_total uint32 BE ≈ 960 KB on BE11529
# section[-5 :-1] memory_free uint32 BE decreases as events fill
# section[-1] frame checksum (not data)
#
# Confirmed stable across IDLE (52b), MONITORING (55b), and counter-jitter
# IDLE variants (53b) from 4-8-26/2ndtry full capture analysis.
if len(section) >= 11:
batt_raw = struct.unpack(">H", section[-11:-9])[0]
battery_v = batt_raw / 100.0
memory_total = struct.unpack(">I", section[38:42])[0]
memory_free = struct.unpack(">I", section[42:46])[0]
memory_total = struct.unpack(">I", section[-9:-5])[0]
memory_free = struct.unpack(">I", section[-5:-1])[0]
return MonitorStatus(
is_monitoring=is_monitoring,