v0.10.0 — monitor log entry support (SUB 0x0A partial records)
Add full decode pipeline for 0x2C partial records from the device's event list, representing continuous monitoring intervals where no threshold was crossed. These records appear interleaved with full triggered events in the browse walk and were previously ignored. minimateplus/models.py - Add MonitorLogEntry dataclass: key, start_time, stop_time, serial, geo_threshold_ips, raw_header, duration_seconds property minimateplus/protocol.py - read_waveform_header() now returns (data_rsp.data, length) — full payload including the record-type byte at position 0 — instead of the sliced header. Callers that need the old slice use raw_data[11:11+length] as before. minimateplus/client.py - Add _decode_0a_partial_header(): auto-detects 9-byte (sub_code=0x10) vs 10-byte (sub_code=0x03) timestamp format, handles 1-byte inter-timestamp gap, extracts serial via BE anchor and geo threshold via Geo: anchor. - Add get_monitor_log_entries(skip_keys=None): browse walk (1E → 0A → 1F), decodes partial records, skips full records and already-seen keys. minimateplus/__init__.py - Export MonitorLogEntry bridges/ach_server.py - After get_events(), call get_monitor_log_entries(skip_keys=seen_keys) and save new entries to monitor_log.json in the session directory. - Add _monitor_log_entry_to_dict() helper. - Include monitor log keys in downloaded_keys for state persistence. CLAUDE.md / CHANGELOG.md - Document 0x2C partial record layout (timestamp format, ASCII metadata region, 1-byte gap edge case) confirmed from 4-11-26 MITM capture. - Version bump to v0.10.0; update What's next. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
Ground-up Python replacement for **Blastware**, Instantel's Windows-only software for
|
||||
managing MiniMate Plus seismographs. Connects over direct RS-232 or cellular modem
|
||||
(Sierra Wireless RV50 / RV55). Current version: **v0.9.0**.
|
||||
(Sierra Wireless RV50 / RV55). Current version: **v0.10.0**.
|
||||
|
||||
---
|
||||
|
||||
@@ -25,9 +25,9 @@ CHANGELOG.md ← version history
|
||||
|
||||
---
|
||||
|
||||
## Current implementation state (v0.9.0)
|
||||
## Current implementation state (v0.10.0)
|
||||
|
||||
Full read pipeline + write pipeline + erase pipeline working end-to-end over TCP/cellular:
|
||||
Full read pipeline + write pipeline + erase pipeline + monitor log working end-to-end over TCP/cellular:
|
||||
|
||||
| Step | SUB | Status |
|
||||
|---|---|---|
|
||||
@@ -42,7 +42,8 @@ Full read pipeline + write pipeline + erase pipeline working end-to-end over TCP
|
||||
| **Bulk waveform stream (event-time metadata)** | **5A** | ✅ new v0.6.0 |
|
||||
| Event advance / next key | 1F | ✅ |
|
||||
| **Write commands (push config to device)** | **68–83** | ✅ new v0.8.0 |
|
||||
| **Erase all events** | **0xA3 → 0x1C → 0x06 → 0xA2** | ✅ **new v0.9.0** |
|
||||
| **Erase all events** | **0xA3 → 0x1C → 0x06 → 0xA2** | ✅ new v0.9.0 |
|
||||
| **Monitor log entries (partial 0x2C records)** | **0A browse** | ✅ **new v0.10.0** |
|
||||
|
||||
`get_events()` sequence per event: `1E → 0A → 0C → 5A → 1F`
|
||||
|
||||
@@ -795,8 +796,134 @@ the erase). Used for post-erase detection.
|
||||
|
||||
---
|
||||
|
||||
## Monitor log entries — SUB 0x0A partial records (confirmed 2026-04-11)
|
||||
|
||||
Confirmed from 4-11-26 MITM capture: 12 partial records (record type `0x2C`) and 7 full
|
||||
event records (record type `0x46`) across 19 total 0x0A responses.
|
||||
|
||||
### Record type detection
|
||||
|
||||
`read_waveform_header()` returns `(raw_data, length)` where `raw_data = data_rsp.data`
|
||||
(the full payload including prefix bytes). The record type is at `raw_data[0]`:
|
||||
|
||||
| Value | Type | How to process |
|
||||
|---|---|---|
|
||||
| `0x46` | Full triggered event | Normal download: 0C → 5A → 1F |
|
||||
| `0x2C` | Monitor log entry (partial) | No 0C/5A; decode inline from 0A payload |
|
||||
|
||||
Length heuristic: `length < 0x40` (64) reliably identifies partial records across all
|
||||
observed captures. Both checks (`raw_data[0] == 0x2C` and `length < 0x40`) are used.
|
||||
|
||||
### SUB 0x0A partial record (0x2C) payload layout
|
||||
|
||||
All offsets are from `raw_data` (the full `data_rsp.data` array including the 11-byte
|
||||
prefix before the actual header bytes start).
|
||||
|
||||
```
|
||||
raw_data[0] = 0x2C ← record type (partial / monitor log)
|
||||
raw_data[1:11] = prefix bytes (vary; contain key4 copy, flags, length)
|
||||
raw_data[11:] = timestamp and ASCII metadata payload
|
||||
```
|
||||
|
||||
**Timestamp auto-detection** (confirmed from 4-11-26 capture):
|
||||
|
||||
```
|
||||
raw_data[11] == 0x10 → 10-byte sub_code=0x03 format (continuous mode)
|
||||
raw_data[11] != 0x10 → 9-byte sub_code=0x10 format (single-shot mode)
|
||||
```
|
||||
|
||||
**9-byte timestamp format (sub_code=0x10):**
|
||||
|
||||
| Byte | Field |
|
||||
|---|---|
|
||||
| 0 | day |
|
||||
| 1 | `0x10` (sub_code marker) |
|
||||
| 2 | month |
|
||||
| 3–4 | year (uint16 BE) |
|
||||
| 5 | unknown (0x00) |
|
||||
| 6 | hour |
|
||||
| 7 | minute |
|
||||
| 8 | second |
|
||||
|
||||
**10-byte timestamp format (sub_code=0x03):**
|
||||
|
||||
| Byte | Field |
|
||||
|---|---|
|
||||
| 0 | `0x10` (marker) |
|
||||
| 1 | day |
|
||||
| 2 | `0x10` (marker) |
|
||||
| 3 | month |
|
||||
| 4–5 | year (uint16 BE) |
|
||||
| 6 | unknown (0x00) |
|
||||
| 7 | hour |
|
||||
| 8 | minute |
|
||||
| 9 | second |
|
||||
|
||||
**Two timestamps:** Each partial record contains two timestamps — `start_time` and
|
||||
`stop_time` — stored consecutively:
|
||||
- `ts1` (start) at `raw_data[ts_offset : ts_offset + ts_size]` where `ts_offset = 11`
|
||||
- `ts2` (stop) at `raw_data[ts1_end : ts1_end + ts_size]`
|
||||
|
||||
**Edge case — 1-byte gap between timestamps:** Occurs when ts1 and ts2 share the same
|
||||
minute:second. If `try_ts(raw_data[ts1_end:])` fails, try `try_ts(raw_data[ts1_end+1:])`.
|
||||
Confirmed in frames 121, 161, 165 of the 4-11-26 MITM capture. Frame 121 still shows 0s
|
||||
duration (both decode to 16:02:00) — the extra byte appears in all same-second cases.
|
||||
|
||||
**ASCII metadata after timestamps:**
|
||||
```
|
||||
<separator bytes> BE<serial>\x00Geo: <float> in/s ...
|
||||
```
|
||||
|
||||
- Serial: scan for `b"BE"`, read until `b"\x00"` (e.g. `"BE11529"`)
|
||||
- Geo threshold: scan for `b"Geo: "`, read float until next space (e.g. `0.254` in/s)
|
||||
|
||||
A separator of variable length (4–5 bytes of `\x00` + flags) sits between the two
|
||||
timestamps and the ASCII region. The `b"BE"` anchor scan is robust to separator length
|
||||
variation.
|
||||
|
||||
### `_decode_0a_partial_header(raw_data, index, key4)` — client.py
|
||||
|
||||
Returns a `MonitorLogEntry` or `None`. Called by `get_monitor_log_entries()` for each
|
||||
event key whose 0x0A response has `raw_data[0] == 0x2C` or `length < 0x40`.
|
||||
|
||||
### `MiniMateClient.get_monitor_log_entries(skip_keys=None)` — client.py
|
||||
|
||||
Browse-mode walk: `1E → 0A → check type → decode if partial → 1F`. No 0x0C or 5A reads
|
||||
performed. Full (0x46) records are skipped without decoding. Returns `list[MonitorLogEntry]`.
|
||||
|
||||
`skip_keys` (optional `set[str]`): keys in this set are still advanced through the walk
|
||||
(to avoid disrupting the iteration sequence), but no `MonitorLogEntry` is created for them.
|
||||
|
||||
### `MonitorLogEntry` model — models.py
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class MonitorLogEntry:
|
||||
index: int # 0-based position
|
||||
key: str # 8-hex event key
|
||||
start_time: Optional[datetime.datetime] = None
|
||||
stop_time: Optional[datetime.datetime] = None
|
||||
serial: Optional[str] = None
|
||||
geo_threshold_ips: Optional[float] = None
|
||||
raw_header: Optional[bytes] = field(default=None, repr=False)
|
||||
|
||||
@property
|
||||
def duration_seconds(self) -> Optional[float]: ...
|
||||
```
|
||||
|
||||
### ACH server integration (v0.10.0)
|
||||
|
||||
After `get_events()`, the ACH server calls `get_monitor_log_entries(skip_keys=seen_keys)`.
|
||||
New entries are saved to `monitor_log.json` in the session directory. Monitor log keys are
|
||||
included in `current_keys` for state persistence so they are not re-processed on the next
|
||||
call-home.
|
||||
|
||||
---
|
||||
|
||||
## What's next
|
||||
|
||||
- **Database** — SQLite store for events + monitor log entries; dedup by key; queryable
|
||||
- **Histograms** — decode histogram-mode A5 data (noise floor tracking)
|
||||
- Compliance config encoder — build raw write payloads from a `ComplianceConfig` object
|
||||
- Locate "Sensor Check" byte in compliance config (need capture with Disabled vs Before-monitoring)
|
||||
- Modem manager — push RV50/RV55 configs via Sierra Wireless API
|
||||
|
||||
Reference in New Issue
Block a user