feat: Add monitoring functionality to MiniMate protocol and web interface

- Introduced new SUBs for monitoring status, start, and stop commands in protocol.py.
- Implemented read_monitor_status, start_monitoring, and stop_monitoring methods in MiniMateProtocol class.
- Added new API endpoints for monitoring status retrieval and control in server.py.
- Enhanced the web application with a monitoring panel, including battery and memory status display.
- Created a new Python script to parse SUB 0x1C response frames for monitoring status.
- Documented the monitoring status response format and field locations in markdown and text files.
This commit is contained in:
2026-04-08 14:34:42 -04:00
parent 8545daac04
commit a41e7a9e1a
9 changed files with 1121 additions and 10 deletions
+101
View File
@@ -37,6 +37,7 @@ from .models import (
ComplianceConfig,
DeviceInfo,
Event,
MonitorStatus,
PeakValues,
ProjectInfo,
Timestamp,
@@ -49,6 +50,7 @@ from .protocol import (
SUB_WRITE_CONFIRM_B,
SUB_WRITE_CONFIRM_C,
SUB_TRIGGER_CONFIRM,
SUB_MONITOR_STATUS,
)
from .transport import SerialTransport, BaseTransport
@@ -725,6 +727,66 @@ class MiniMateClient:
notes=notes,
)
# ── Monitoring ────────────────────────────────────────────────────────────
def get_monitor_status(self) -> MonitorStatus:
"""
Read the current monitoring state, battery voltage, and memory usage.
Wraps protocol.read_monitor_status() and decodes the raw payload into
a MonitorStatus object.
The device payload length indicates mode:
- 44 bytes (0x2C): unit is idle (full status block present)
- 12 bytes : unit is actively monitoring (abbreviated block)
Confirmed field offsets (relative to data[11], the start of the S3
data section after the 11-byte frame header):
[0x2F:0x31] battery voltage × 100 uint16 BE e.g. 0x02A8 = 680 → 6.80 V
[0x31:0x35] memory total (bytes) uint32 BE e.g. 0x000F0000 = 983040 bytes
[0x35:0x39] memory free (bytes) uint32 BE
Returns:
MonitorStatus with is_monitoring, battery_v, memory_total, memory_free.
Raises:
RuntimeError: if not connected.
ProtocolError: on timeout or wrong response SUB.
"""
proto = self._require_proto()
frame = proto.read_monitor_status()
return _decode_monitor_status(frame.data)
def start_monitoring(self) -> None:
"""
Command the device to begin monitoring (recording triggered events).
Sends SUB 0x96; device responds with a 17-byte zero-data ack (SUB 0x69).
Confirmed from 4-8-26/2ndtry BW TX capture frame 92.
Raises:
RuntimeError: if not connected.
ProtocolError: on timeout or wrong response.
"""
proto = self._require_proto()
proto.start_monitoring()
log.info("start_monitoring: device is now monitoring")
def stop_monitoring(self) -> None:
"""
Command the device to stop monitoring.
Sends SUB 0x97; device responds with a 17-byte zero-data ack (SUB 0x68).
Confirmed from 4-8-26/2ndtry BW TX capture frame 305.
Raises:
RuntimeError: if not connected.
ProtocolError: on timeout or wrong response.
"""
proto = self._require_proto()
proto.stop_monitoring()
log.info("stop_monitoring: device stopped monitoring")
# ── Internal helpers ──────────────────────────────────────────────────────
def _require_proto(self) -> MiniMateProtocol:
@@ -1666,3 +1728,42 @@ def _find_first_string(data: bytes, start: int, end: int, min_len: int) -> Optio
i += 1
return None
def _decode_monitor_status(data: bytes) -> MonitorStatus:
"""
Decode SUB 0x1C response payload into a MonitorStatus object.
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
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 data section starts at offset 11 (after the S3 section header).
section = data[11:] if len(data) > 11 else data
# Mode: idle payload is 44 bytes; monitoring is shorter (12 bytes observed)
is_monitoring = len(section) < 20
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_v = batt_raw / 100.0
memory_total = struct.unpack(">I", section[0x31:0x35])[0]
memory_free = struct.unpack(">I", section[0x35:0x39])[0]
return MonitorStatus(
is_monitoring=is_monitoring,
battery_v=battery_v,
memory_total=memory_total,
memory_free=memory_free,
)