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:
@@ -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,
|
||||
)
|
||||
|
||||
@@ -417,3 +417,22 @@ class Event:
|
||||
parts.append(f"M={pv.micl:.6f}")
|
||||
ppv = " [" + ", ".join(parts) + " in/s]"
|
||||
return f"Event#{self.index} {ts}{ppv}"
|
||||
|
||||
|
||||
# ── MonitorStatus ─────────────────────────────────────────────────────────────
|
||||
|
||||
@dataclass
|
||||
class MonitorStatus:
|
||||
"""
|
||||
Current monitoring state decoded from SUB 0x1C response.
|
||||
|
||||
Confirmed field locations from 4-8-26/2ndtry BW capture:
|
||||
battery_v : data[11 + 0x2F : 11 + 0x31] uint16 BE ÷ 100 e.g. 680 → 6.80 V
|
||||
memory_total: data[11 + 0x31 : 11 + 0x35] uint32 BE bytes e.g. 983040 → 960 KB
|
||||
memory_free : data[11 + 0x35 : 11 + 0x39] uint32 BE bytes (subset of total)
|
||||
is_monitoring: inferred from payload length — idle = 44 bytes, monitoring = 12 bytes
|
||||
"""
|
||||
is_monitoring: bool # True if unit is actively recording ✅
|
||||
battery_v: Optional[float] = None # Battery voltage in volts ✅
|
||||
memory_total: Optional[int] = None # Total flash memory in bytes ✅
|
||||
memory_free: Optional[int] = None # Free flash memory in bytes ✅
|
||||
|
||||
@@ -57,7 +57,7 @@ SUB_SERIAL_NUMBER = 0x15
|
||||
SUB_FULL_CONFIG = 0x01
|
||||
SUB_EVENT_INDEX = 0x08
|
||||
SUB_CHANNEL_CONFIG = 0x06
|
||||
SUB_TRIGGER_CONFIG = 0x1C
|
||||
SUB_MONITOR_STATUS = 0x1C # Monitoring status read (battery, memory, mode) ✅
|
||||
SUB_EVENT_HEADER = 0x1E
|
||||
SUB_EVENT_ADVANCE = 0x1F
|
||||
SUB_WAVEFORM_HEADER = 0x0A
|
||||
@@ -77,6 +77,10 @@ SUB_WRITE_CONFIRM_C = 0x74 # Confirm C — sent after 69 ✅
|
||||
SUB_TRIGGER_CONFIG_WRITE = 0x82 # Write trigger config (0x22 + 0x60) ✅
|
||||
SUB_TRIGGER_CONFIRM = 0x83 # Confirm trigger write ✅
|
||||
|
||||
# Monitoring control SUBs (confirmed from 4-8-26/2ndtry BW TX capture)
|
||||
SUB_START_MONITORING = 0x96 # Start monitoring → response 0x69 ✅
|
||||
SUB_STOP_MONITORING = 0x97 # Stop monitoring → response 0x68 ✅
|
||||
|
||||
# Hardcoded data lengths for the two-step read protocol.
|
||||
#
|
||||
# The S3 probe response page_key is always 0x0000 — it does NOT carry the
|
||||
@@ -91,7 +95,7 @@ DATA_LENGTHS: dict[int, int] = {
|
||||
SUB_SERIAL_NUMBER: 0x0A, # 10-byte serial number block ✅
|
||||
SUB_FULL_CONFIG: 0x98, # 152-byte full config block ✅
|
||||
SUB_EVENT_INDEX: 0x58, # 88-byte event index ✅
|
||||
SUB_TRIGGER_CONFIG: 0x2C, # 44-byte trigger config 🔶
|
||||
SUB_MONITOR_STATUS: 0x2C, # 44-byte monitor status block (idle) ✅
|
||||
SUB_EVENT_HEADER: 0x08, # 8-byte event header (waveform key + event data) ✅
|
||||
SUB_EVENT_ADVANCE: 0x08, # 8-byte next-key response ✅
|
||||
# SUB_WAVEFORM_HEADER (0x0A) is VARIABLE — length read from probe response
|
||||
@@ -1056,6 +1060,71 @@ class MiniMateProtocol:
|
||||
self._send(frame)
|
||||
return self.recv_write_ack(expected_sub=rsp_sub)
|
||||
|
||||
# ── Monitoring ────────────────────────────────────────────────────────────
|
||||
|
||||
def read_monitor_status(self) -> S3Frame:
|
||||
"""
|
||||
Read monitoring status (SUB 0x1C → response 0xE3).
|
||||
|
||||
Two-step read: probe (offset=0x00) then data (offset=0x2C).
|
||||
|
||||
Returns:
|
||||
S3Frame with 44 bytes of status data (idle state).
|
||||
When unit is actively monitoring the payload is shorter (12 bytes);
|
||||
callers should check frame data length to determine mode.
|
||||
|
||||
Raises:
|
||||
ProtocolError: on timeout or wrong response SUB.
|
||||
"""
|
||||
rsp_sub = _expected_rsp_sub(SUB_MONITOR_STATUS) # 0xFF - 0x1C = 0xE3
|
||||
log.debug("read_monitor_status: probe step rsp_sub=0x%02X", rsp_sub)
|
||||
probe_frame = build_bw_frame(SUB_MONITOR_STATUS, offset=0x00)
|
||||
self._send(probe_frame)
|
||||
self._recv_one(expected_sub=rsp_sub)
|
||||
|
||||
log.debug("read_monitor_status: data step offset=0x%02X", DATA_LENGTHS[SUB_MONITOR_STATUS])
|
||||
data_frame = build_bw_frame(SUB_MONITOR_STATUS, offset=DATA_LENGTHS[SUB_MONITOR_STATUS])
|
||||
self._send(data_frame)
|
||||
return self._recv_one(expected_sub=rsp_sub)
|
||||
|
||||
def start_monitoring(self) -> S3Frame:
|
||||
"""
|
||||
Send Start Monitoring command (SUB 0x96 → response 0x69).
|
||||
|
||||
Single write frame, no data payload. Confirmed from 4-8-26/2ndtry
|
||||
BW TX capture frame 92.
|
||||
|
||||
Returns:
|
||||
S3Frame ack from device.
|
||||
|
||||
Raises:
|
||||
ProtocolError: on timeout or wrong response SUB.
|
||||
"""
|
||||
rsp_sub = _expected_rsp_sub(SUB_START_MONITORING) # 0xFF - 0x96 = 0x69
|
||||
log.debug("start_monitoring: rsp_sub=0x%02X", rsp_sub)
|
||||
frame = build_bw_write_frame(SUB_START_MONITORING, b"")
|
||||
self._send(frame)
|
||||
return self.recv_write_ack(expected_sub=rsp_sub)
|
||||
|
||||
def stop_monitoring(self) -> S3Frame:
|
||||
"""
|
||||
Send Stop Monitoring command (SUB 0x97 → response 0x68).
|
||||
|
||||
Single write frame, no data payload. Confirmed from 4-8-26/2ndtry
|
||||
BW TX capture frame 305.
|
||||
|
||||
Returns:
|
||||
S3Frame ack from device.
|
||||
|
||||
Raises:
|
||||
ProtocolError: on timeout or wrong response SUB.
|
||||
"""
|
||||
rsp_sub = _expected_rsp_sub(SUB_STOP_MONITORING) # 0xFF - 0x97 = 0x68
|
||||
log.debug("stop_monitoring: rsp_sub=0x%02X", rsp_sub)
|
||||
frame = build_bw_write_frame(SUB_STOP_MONITORING, b"")
|
||||
self._send(frame)
|
||||
return self.recv_write_ack(expected_sub=rsp_sub)
|
||||
|
||||
# ── Internal helpers ──────────────────────────────────────────────────────
|
||||
|
||||
def _send(self, frame: bytes) -> None:
|
||||
|
||||
Reference in New Issue
Block a user