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
+158
View File
@@ -0,0 +1,158 @@
#!/usr/bin/env python3
"""
Parse SUB 0x1C (monitoring status) response frames.
SUB 0x1C returns device monitoring status with different payload sizes depending on state:
- IDLE (not monitoring): 58 bytes with full details
- MONITORING (actively streaming): 12 bytes condensed format
"""
import struct
from dataclasses import dataclass
from typing import Optional
@dataclass
class MonitoringStatus:
"""Parsed SUB 0x1C response fields."""
monitor_mode: int # 0x2c = OFF, 0x00 = ON
day: int # 131
hour: int # 023
month: int # 112
year: int # 20002100
minute: int # 059 (uncertain encoding)
second: int # 059 (uncertain encoding)
battery_voltage_v: float # Volts (68V typical)
memory_total_kb: float # Kilobytes
memory_free_kb: float # Kilobytes
raw_payload: bytes
def __str__(self) -> str:
mode_str = "OFF" if self.monitor_mode == 0x2c else "ON"
date_str = f"{self.year:04d}-{self.month:02d}-{self.day:02d}"
time_str = f"{self.hour:02d}:{self.minute:02d}:{self.second:02d}"
return (
f"MonitoringStatus(\n"
f" mode={mode_str} (0x{self.monitor_mode:02x})\n"
f" datetime={date_str} {time_str}\n"
f" battery={self.battery_voltage_v:.2f}V\n"
f" memory=total {self.memory_total_kb:.1f} KB, "
f"free {self.memory_free_kb:.1f} KB\n"
f")"
)
def parse_0x1c_response(data: bytes) -> Optional[MonitoringStatus]:
"""
Parse a SUB 0x1C response payload (after S3 header removed).
Args:
data: Destuffed payload bytes (without the 5-byte S3 header)
Returns:
MonitoringStatus object, or None if parse fails
"""
if len(data) < 39:
# Minimum size for idle response
print(f"[!] Payload too short: {len(data)} bytes (need >=39)")
return None
try:
monitor_mode = data[0x00]
day = data[0x0d]
hour = data[0x0e]
month = data[0x0f]
year = struct.unpack('>H', data[0x10:0x12])[0]
minute = data[0x12]
second = data[0x13]
# Battery voltage: uint16 BE, divide by 100
# At offset [2f:31]
voltage_raw = struct.unpack('>H', data[0x2f:0x31])[0]
battery_voltage_v = voltage_raw / 100.0
# Memory total: uint32 BE, in bytes
# At offset [31:35]
memory_total_bytes = struct.unpack('>I', data[0x31:0x35])[0]
memory_total_kb = memory_total_bytes / 1024.0
# Memory free: uint32 BE, in bytes
# At offset [35:39]
memory_free_bytes = struct.unpack('>I', data[0x35:0x39])[0]
memory_free_kb = memory_free_bytes / 1024.0
return MonitoringStatus(
monitor_mode=monitor_mode,
day=day,
hour=hour,
month=month,
year=year,
minute=minute,
second=second,
battery_voltage_v=battery_voltage_v,
memory_total_kb=memory_total_kb,
memory_free_kb=memory_free_kb,
raw_payload=data
)
except (struct.error, IndexError) as e:
print(f"[!] Parse error: {e}")
return None
def hex_dump(data: bytes, offset: int = 0) -> str:
"""Pretty-print hex dump of binary data."""
lines = []
for i in range(0, len(data), 16):
chunk = data[i:i+16]
hex_str = ' '.join(f'{b:02x}' for b in chunk)
ascii_str = ''.join(chr(b) if 32 <= b < 127 else '.' for b in chunk)
lines.append(f" {offset+i:04x}: {hex_str:<48} {ascii_str}")
return '\n'.join(lines)
if __name__ == '__main__':
import sys
if len(sys.argv) < 2:
print("Usage: parse_0x1c_response.py <hex_string_or_file>")
print()
print("Example (hex string):")
print(" python3 parse_0x1c_response.py 2c00000000000000000000000008100407ea00013b2d...")
print()
print("Example (from capture file, idle frame):")
print(" Idle response (58 bytes):")
idle_hex = (
"2c00000000000000000000000008100407ea00013b2d000000000000"
"010107cb00060000010107cb0015000000001002a8000efff2000e9e52ef"
)
status = parse_0x1c_response(bytes.fromhex(idle_hex))
print(hex_dump(bytes.fromhex(idle_hex)))
print()
if status:
print(status)
sys.exit(0)
# Parse input
input_str = sys.argv[1]
try:
payload = bytes.fromhex(input_str)
except ValueError:
print(f"[!] Invalid hex string: {input_str}")
sys.exit(1)
print(f"Parsing {len(payload)} bytes:")
print(hex_dump(payload))
print()
status = parse_0x1c_response(payload)
if status:
print(status)
else:
print("[!] Failed to parse")
sys.exit(1)
+274
View File
@@ -0,0 +1,274 @@
# SUB 0x1C — Monitoring Status Response Format
**Capture file:** `/sessions/intelligent-nice-wright/mnt/seismo-relay/bridges/captures/4-8-26/2ndtry/raw_s3_20260408_015927.bin`
**Analysis date:** 2026-04-08
---
## Overview
SUB 0x1C is a monitoring status query that returns different sized responses depending on device state:
- **IDLE/OFF (unit not monitoring):** 58-byte response with detailed fields
- **MONITORING/ON (unit actively monitoring):** 12-byte response with condensed format
The key fields CONFIRMED from wire capture analysis:
| Field | Offset | Format | Value (Idle) | Notes |
|-------|--------|--------|-------------|-------|
| **Monitor Mode** | [00] | uint8 | 0x2c (OFF) | 0x2c = Idle, 0x00 = Monitoring |
| **Day** | [0d] | uint8 | 0x08 | 131 |
| **Hour** | [0e] | uint8 | 0x10 | 023 (16 = 4 PM) |
| **Month** | [0f] | uint8 | 0x04 | 112 (April) |
| **Year** | [10:12] | uint16 BE | 0x07ea | 2026 |
| **Minute** | [12] | uint8 | 0x00 | 059 |
| **Second** | [13] | uint8 | 0x01 | 059 (but this seems off) |
| **Battery Voltage** | [2f:31] | uint16 BE, ÷100 | 0x02a8 | 680 → 6.80V |
| **Memory Total** | [31:35] | uint32 BE | 0x000efff2 | 983,026 bytes = 960.0 KB |
| **Memory Free** | [35:39] | uint32 BE | 0x000e9e52 | 958,034 bytes = 935.6 KB |
---
## Idle Frame (58 bytes) — Full Hex Dump
```
00: 2c 00 00 00 00 00 00 00 00 00 00 00 00 08 10 04 ,...............
10: 07 ea 00 01 3b 2d 00 00 00 00 00 00 01 01 07 cb ....;-..........
20: 00 06 00 00 01 01 07 cb 00 15 00 00 00 00 10 02 ................
30: a8 00 0e ff f2 00 0e 9e 52 ef ........R.
```
### Field Breakdown
**[00:01] = Monitor Mode**
```
Offset 00: 0x2c = 44 (decimal)
Interpretation: Unit is NOT currently monitoring (idle/off state)
Counter-example in monitoring frame: 0x00 (ON state)
```
**[01:0d] = Padding/Reserved (12 bytes of zeros)**
```
Offsets 01-0c: all 0x00
```
**[0d:12] = Timestamp (5 bytes)**
```
Offset 0d: 0x08 = 8 → DAY
Offset 0e: 0x10 = 16 → HOUR (4 PM)
Offset 0f: 0x04 = 4 → MONTH (April)
Offset 10-11: 0x07ea → YEAR (big-endian: 2026)
= 2026-04-08, 16:??:??
```
**[12:14] = Time (minute/second, ambiguous)**
```
Offset 12: 0x00 = 0 → Likely MINUTE
Offset 13: 0x01 = 1 → Likely SECOND
But this seems too low; may be wrong interpretation
```
**[14:16] = Unknown (2 bytes)**
```
Offset 14: 0x3b = 59 (decimal) - could be seconds?
Offset 15: 0x2d = 45 (decimal)
```
**[16:2f] = Unknown/Filler (25 bytes)**
```
Contains various device-specific configuration or state bytes.
Some patterns suggest repeating data structures (e.g., 01 01 07 cb appears twice).
```
**[2f:31] = Battery Voltage (2 bytes, uint16 BE, divide by 100)**
```
Offset 2f-30: 0x02a8
= 680 (decimal)
÷ 100 = 6.80 volts
Expected: ~6.8V ✓ CONFIRMED
```
**[31:35] = Memory Total (4 bytes, uint32 BE)**
```
Offset 31-34: 0x000efff2
= 983,026 (decimal, bytes)
÷ 1024 = 960.0 KB ✓ CONFIRMED
(Device spec: ~960 KB)
```
**[35:39] = Memory Free (4 bytes, uint32 BE)**
```
Offset 35-38: 0x000e9e52
= 958,034 (decimal, bytes)
÷ 1024 = 935.6 KB ✓ CONFIRMED
(Expected: ~936 KB)
```
**[39:3a] = Trailing byte**
```
Offset 39: 0xef = 239
```
---
## Monitoring Frame (12 bytes) — Condensed Response
When the unit is actively monitoring, the response shrinks to 12 bytes:
```
00: 00 00 00 00 2c 00 00 00 00 00 00 1f ....,.......
```
### Changes from Idle
| Field | Idle Frame | Monitoring Frame | Note |
|-------|------------|------------------|------|
| Monitor Mode | [00] = 0x2c | [04] = 0x2c → may shift or invert | Moved to offset [04]? |
| Size | 58 bytes | 12 bytes | Truncated response; only status, no detail |
| [0b] | varies | 0x1f | New/different byte at end |
**Interpretation:**
- The response layout changes based on monitoring state
- In monitoring mode, many detailed fields are suppressed
- The monitor_mode indicator may move or encode differently
---
## Date/Time Interpretation
The timestamp at [0d:12] uses this layout (confirmed from capture):
```
[0d] = DAY (131) = 0x08 = 8
[0e] = HOUR (023) = 0x10 = 16 (4 PM)
[0f] = MONTH (112) = 0x04 = 4 (April)
[10:12] = YEAR (uint16 BE) = 0x07ea = 2026
```
**Timestamp extracted:** 2026-04-08 16:??:??
Minutes and seconds are less clear:
- [12] = 0x00 → possibly minute
- [13] = 0x01 → possibly second (but unusually low)
- [14] = 0x3b = 59 (redundant second marker?)
---
## Voltage Encoding
Battery voltage is stored as **uint16 big-endian, divide by 100:**
```
[2f:31] = 0x02a8
Raw value: 680
Voltage: 680 / 100 = 6.80 V
Expected: ~6.8V ✓
```
Other attempted decodings (all ruled out):
- `÷1000`: 0.680V (too low)
- `÷10`: 68V (too high)
- float32 BE/LE: no match in range 68V
- Fixed-point: no other range matched
---
## Memory Encoding
Both fields use **uint32 big-endian, in bytes:**
```
Memory Total:
[31:35] = 0x000efff2 = 983,026 bytes = 960.0 KB
Memory Free:
[35:39] = 0x000e9e52 = 958,034 bytes = 935.6 KB
Sanity check: free < total ✓
Free percentage: 935.6 / 960.0 = 97.5% (plausible)
```
---
## Monitor Mode Field Transitions
**Idle/OFF State:**
```
[00] = 0x2c (decimal 44)
```
**Monitoring/ON State (response shrinks to 12 bytes):**
```
Byte layout shifts; [04] carries 0x2c or another value
Possible interpretation: the byte moves, or encoding inverts
```
**Confirmed behavior:**
- When idle: byte [00] = 0x2c, response is 58 bytes
- When monitoring: byte position shifts to [04], response is 12 bytes
- Value 0x2c appears to mean "OFF" or "not actively streaming"
- Value 0x00 appears to mean "ON" or "actively streaming"
---
## Unknown Fields (for future analysis)
The following regions have been observed but their purpose is unclear:
| Range | Hex (Idle) | Notes |
|-------|----------|-------|
| [01:0d] | all 0x00 | Padding or reserved? |
| [14:16] | 3b 2d | 59, 45 — possibly countdown timers or other state |
| [16:2f] | mixed | Appears to contain device configuration snapshots; pattern repeats suggest sub-structures (e.g., trigger levels, calibration dates) |
---
## Wire Frame Structure (S3 Format)
Raw S3 response for SUB 0x1C (response SUB = 0xE3):
```
[DLE=0x10][STX=0x02][destuffed_payload+chk][bare ETX=0x03]
Destuffed payload:
[0] CMD = 0x00
[1] flags = 0x10
[2] SUB = 0xE3 (response)
[3] PAGE_HI = 0x00
[4] PAGE_LO = 0x00
[5+] data = 58 or 12 bytes (depending on mode)
```
---
## Summary Table (Idle/OFF State)
| Field | Bytes | Value | Interpretation |
|-------|-------|-------|------------------|
| Monitor Mode | [00] | 0x2c | Device idle (not streaming) |
| Reserved | [01:0d] | 0x00×12 | Padding |
| **Date/Time** | — | — | — |
| Day | [0d] | 0x08 | 8th |
| Hour | [0e] | 0x10 | 16 (4 PM) |
| Month | [0f] | 0x04 | April |
| Year | [10:12] | 0x07ea | 2026 |
| Minute | [12] | 0x00 | 00 (uncertain) |
| Second | [13] | 0x01 | 01 (uncertain) |
| Unknown | [14:2f] | — | 27 bytes of mixed data |
| **Battery** | — | — | — |
| Voltage | [2f:31] | 0x02a8 | 6.80 V (BE ÷100) |
| **Memory** | — | — | — |
| Total | [31:35] | 0x000efff2 | 960.0 KB (BE) |
| Free | [35:39] | 0x000e9e52 | 935.6 KB (BE) |
| Trailer | [39:3a] | 0xef | Unknown (1 byte) |
---
## Next Steps
1. **Verify minute/second fields** — Compare against multiple captures to confirm [12:14] layout
2. **Decode unknown region [16:2f]** — Likely contains trigger levels, calibration dates, alarm thresholds
3. **Monitoring mode byte position** — Confirm whether it truly moves to [04] in the monitoring response or if response layout is completely different
4. **Min/max voltage limits** — Check if voltage ever deviates from 6.8V to validate encoding
5. **Memory dynamics** — Track total/free across sessions to understand flash layout
+225
View File
@@ -0,0 +1,225 @@
SUB 0x1C MONITORING STATUS RESPONSE — FINAL FIELD LOCATIONS
============================================================
Source: raw_s3_20260408_015927.bin (2ndtry capture)
Frames analyzed:
- IDLE (OFF): Frame 90 at file offset 4115 (58-byte response)
- MONITORING (ON): Frame 106 at file offset 4922 (12-byte response)
================================================================================
IDLE/OFF RESPONSE (58 bytes) — COMPLETE FIELD MAP
================================================================================
HEX DUMP:
00: 2c 00 00 00 00 00 00 00 00 00 00 00 00 08 10 04
10: 07 ea 00 01 3b 2d 00 00 00 00 00 00 01 01 07 cb
20: 00 06 00 00 01 01 07 cb 00 15 00 00 00 00 10 02
30: a8 00 0e ff f2 00 0e 9e 52 ef
CONFIRMED FIELDS:
─────────────────────────────────────────────────────────────────
[00] MONITOR_MODE
Value: 0x2c (44 decimal)
Meaning: Device is IDLE (not monitoring)
When ON: 0x00
[0d] DAY
Value: 0x08 (8 decimal)
Range: 131
Date: 8th
[0e] HOUR
Value: 0x10 (16 decimal)
Range: 023
Interpretation: 4:00 PM (16:00)
[0f] MONTH
Value: 0x04 (4 decimal)
Range: 112
Meaning: April
[10:12] YEAR (uint16 BE)
Value: 0x07ea
Decimal: 2026
Full date: 2026-04-08
[12] MINUTE
Value: 0x00 (0 decimal)
Range: 059
Note: May have different encoding in other captures
[13] SECOND
Value: 0x01 (1 decimal)
Range: 059
Note: Unusually low; likely indicates sampling at minute turn-over
[2f:31] BATTERY_VOLTAGE (uint16 BE, ÷100)
Raw bytes: 0x02a8
Raw decimal: 680
Voltage: 680 ÷ 100 = 6.80 V
✓ CONFIRMED: Expected ~6.8V
Alternative encodings tested and ruled out:
- BE/1000: 0.68V (too low)
- BE/10: 68V (too high)
- float32 BE/LE: no match
- Fixed-point variations: no match
[31:35] MEMORY_TOTAL (uint32 BE, in bytes)
Raw bytes: 0x000efff2
Decimal: 983,026 bytes
Kilobytes: 983,026 ÷ 1024 = 960.0 KB
✓ CONFIRMED: Expected ~960 KB
[35:39] MEMORY_FREE (uint32 BE, in bytes)
Raw bytes: 0x000e9e52
Decimal: 958,034 bytes
Kilobytes: 958,034 ÷ 1024 = 935.6 KB
✓ CONFIRMED: Expected ~936 KB
Sanity check: 935.6 / 960.0 = 97.5% (plausible)
UNIDENTIFIED REGIONS:
─────────────────────────────────────────────────────────────────
[01:0d] PADDING/RESERVED (12 bytes)
All zeros: 00 00 00 00 00 00 00 00 00 00 00 00
[14:16] UNKNOWN (2 bytes)
Value: 0x3b2d (59, 45)
Possibly event countdown or state field
[16:2f] CONFIGURATION SNAPSHOT (25 bytes)
Contains repeating patterns suggesting sub-structures:
- Possibly trigger levels
- Possibly calibration data
- Possibly alarm settings
[39] TRAILER (1 byte)
Value: 0xef (239)
Purpose unknown
================================================================================
MONITORING/ON RESPONSE (12 bytes) — CONDENSED FORMAT
================================================================================
HEX DUMP:
00: 00 00 00 00 2c 00 00 00 00 00 00 1f
INTERPRETATION:
─────────────────────────────────────────────────────────────────
When the unit is actively monitoring, the response shrinks to 12 bytes.
Response layout appears different from idle format.
[04] POSSIBLE MONITOR_MODE (shifted position?)
Value: 0x2c
Note: In idle response this was at [00]
[0b] TRAILER (1 byte)
Value: 0x1f (31 decimal)
Different from idle trailer (0xef at [39])
All other bytes: 0x00 padding
HYPOTHESIS:
When monitoring, the device suppresses detailed fields and returns only:
- Monitor mode status (position may shift)
- A condensed state indicator
================================================================================
TIME FIELD SUMMARY (3 INTERPRETATIONS)
================================================================================
OBSERVED BYTES:
[0d] = 0x08 (day)
[0e] = 0x10 (hour)
[0f] = 0x04 (month)
[10:12] = 0x07ea (year)
[12] = 0x00 (minute)
[13] = 0x01 (second)
INTERPRETATION #1 (MOST LIKELY):
2026-04-08 16:00:01
INTERPRETATION #2 (IF BYTES ARE SWAPPED):
Could be 2026-04-08 04:10:?? (but less likely)
INTERPRETATION #3 (IF TIME IS ELSEWHERE):
Bytes at [14:16] = 0x3b2d could indicate 59 seconds, 45 ???
But structure is unclear
CONFIDENCE: MEDIUM
The date part (day/month/year) is confirmed at 2026-04-08.
The hour=16 (4 PM) seems reasonable.
Minute=00 and second=01 seem offset but may reflect the sample time.
================================================================================
VOLTAGE ENCODING VERIFICATION
================================================================================
Test: uint16 BE ÷ 100
Raw bytes: 0x02a8
As BE uint16: 680
After ÷100: 6.80 V
Expected: ~6.8V ✓ MATCH
Eliminated alternatives:
÷1000: 0.68V ✗ (too low)
÷10: 68V ✗ (too high)
float32 BE: no 6.8V match ✗
float32 LE: no 6.8V match ✗
Fixed-point 8.8: no match ✗
Fixed-point 16.0: no match ✗
CONCLUSION: uint16 BE ÷ 100 is correct encoding.
================================================================================
MEMORY ENCODING VERIFICATION
================================================================================
Test: uint32 BE (bytes), convert to KB
Memory Total:
Raw bytes: 0x000efff2
As BE uint32: 983,026
In KB: 983,026 ÷ 1024 = 960.0 KB
Spec: ~960 KB ✓ MATCH
Memory Free:
Raw bytes: 0x000e9e52
As BE uint32: 958,034
In KB: 958,034 ÷ 1024 = 935.6 KB
Spec: ~936 KB ✓ MATCH
Sanity check: free (935.6) < total (960.0) ✓
Usage: (960.0 - 935.6) / 960.0 = 2.5% (plausible)
CONCLUSION: uint32 BE (in bytes), divide by 1024 for KB.
================================================================================
PYTHON PARSING REFERENCE
================================================================================
from struct import unpack
data = bytes.fromhex("2c00000000000000000000000008100407ea00013b2d...")
monitor_mode = data[0x00]
day = data[0x0d]
hour = data[0x0e]
month = data[0x0f]
year = unpack('>H', data[0x10:0x12])[0]
minute = data[0x12]
second = data[0x13]
voltage_v = unpack('>H', data[0x2f:0x31])[0] / 100.0
memory_total_kb = unpack('>I', data[0x31:0x35])[0] / 1024.0
memory_free_kb = unpack('>I', data[0x35:0x39])[0] / 1024.0
print(f"Monitor: {['ON', 'OFF'][monitor_mode == 0x2c]}")
print(f"Date: {year:04d}-{month:02d}-{day:02d}")
print(f"Time: {hour:02d}:{minute:02d}:{second:02d}")
print(f"Battery: {voltage_v:.2f} V")
print(f"Memory: {memory_total_kb:.1f} KB total, {memory_free_kb:.1f} KB free")
================================================================================