feat: add high-water mark state tracking to ach_server + fix monitoring flag

ach_server.py:
- Add ach_state.json per-unit state tracking (keyed by serial number)
- count_events() before any download; skip session if no new events since last call-home
- Download only events beyond the previous high-water mark (all_events[last_count:])
- --max-events N safety cap for first-run units with many stored events
- state_path and max_events wired through AchSession constructor and serve()

client.py (_decode_monitor_status):
- Revert monitoring flag to section[1] == 0x10 (was incorrectly changed to section[6])
- Fix battery/memory offsets to section[-10:-8], [-8:-4], [-4:] (no trailing checksum byte)
- Both confirmed by full byte diff of all 144 0xE3 data frames in 4-8-26/2ndtry capture

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-09 14:38:44 -04:00
committed by serversdown
parent cf7d838bf4
commit 0358acb51d
4 changed files with 210 additions and 107 deletions
+19 -15
View File
@@ -582,28 +582,32 @@ All confirmed from 4-8-26/2ndtry BW TX/S3 capture (clean start → 30s monitor
Standard two-step read (probe at offset 0x00, data at offset 0x2C).
Response SUB = 0xFF 0x1C = **0xE3** (standard formula — no exception).
**Payload length is ~4649 bytes in BOTH idle and monitoring states** — length alone
is NOT a reliable mode indicator. Earlier note claiming "12 bytes when monitoring"
was wrong (confirmed 2026-04-08 from 4-8-26/mid-monitor captures).
**Payload length is 4647 bytes IDLE, 4849 bytes MONITORING** — not a reliable sole
indicator due to 1-byte jitter overlap at the boundary.
**Monitoring flag (CORRECTED 2026-04-08 full byte diff of 2ndtry capture):**
- `section[6] == 0x00` → unit is **idle**
- `section[6] == 0x10` → unit is **monitoring**
**Monitoring flag (CONFIRMED 2026-04-09 — byte diff of all 144 data frames, 2ndtry capture):**
- `section[1] == 0x00` → unit is **idle**
- `section[1] == 0x10` → unit is **monitoring**
Earlier note claiming `section[1]` was the flag was WRONG — section[1] is always 0x00 in both states. The correction was found by diffing all 0xE3 data frames across the start/stop transitions: `section[6]` is the only byte that flips cleanly at frame #36 (start) and #132 (stop) within the 2ndtry 0xE3 frame sequence.
This is `data[12]` (= `frame.data[12]`). The flag is 0x00 in all 36 IDLE_BEFORE frames,
0x10 in all 98 MONITORING frames, and 0x00 in all 10 IDLE_AFTER frames — 100% accurate.
Battery and memory fields are present in **both** states, but the payload grows by **3 bytes** when monitoring is active (section goes from ~52 to ~55 bytes), shifting subsequent fields by +3.
**HISTORY OF THIS FIELD (do not re-derive):** The original implementation used `section[1]`.
A re-analysis in the prior session incorrectly concluded `section[1]` is always 0x00 and
"corrected" the flag to `section[6]`, which has non-binary values (0xea idle, 0x07 monitoring)
and is device-specific. The 2026-04-09 re-analysis confirms `section[1]` was right.
**Field offsets (relative to `data[11:]` = section):**
**IMPORTANT — `frame.data` has checksum already stripped** by `S3FrameParser._finalise()`
(`raw_payload = body[:-1]`; `data = raw_payload[5:]`). There is NO trailing checksum byte in
`section`. All relative-from-end offsets must account for this.
Battery and memory are at **relative offsets from the end** — the payload can vary by ±13 bytes due to counter jitter and monitoring-mode expansion, but these 10 bytes are always anchored at the end:
Battery and memory fields are present in **both** states:
| Offset (relative to end) | Field | Type | Notes |
|---|---|---|---|
| `section[-11:-9]` | battery voltage × 100 | uint16 BE | `0x02A8` = 680 → 6.80 V |
| `section[-9:-5]` | memory total (bytes) | uint32 BE | e.g. 983026 ≈ 960 KB |
| `section[-5:-1]` | memory free (bytes) | uint32 BE | decreases as events are stored |
| `section[-1]` | frame checksum | — | last byte, skip |
| `section[-10:-8]` | battery voltage × 100 | uint16 BE | `0x02A8` = 680 → 6.80 V |
| `section[-8:-4]` | memory total (bytes) | uint32 BE | e.g. 983026 ≈ 960 KB |
| `section[-4:]` | memory free (bytes) | uint32 BE | decreases as events are stored |
### SESSION_RESET signal (`41 03`) — required for monitoring units
@@ -657,7 +661,7 @@ Key findings:
**SFM behavior after `POST /device/monitor/start`:** `_pollMonitorConfirm()` polls
`/device/monitor/status` every 5 s for up to 60 s, updating the badge on each poll.
Status will show MONITORING once `section[6]` flips to `0x10`.
Status will show MONITORING once `section[1]` flips to `0x10`.
### SUBs known from sensor-check capture (4-8-26) — NOT YET IMPLEMENTED