feat: add erase-all protocol and browse helpers to protocol/client layer
protocol.py: - SUB_ERASE_ALL_BEGIN = 0xA3, SUB_ERASE_ALL_CONFIRM = 0xA2 (confirmed 4-11-26 MITM) - SUB_CHANNEL_CONFIG (0x06) data length = 0x24 (36 bytes) in DATA_LENGTHS - begin_erase_all() — single frame, token=0xFE, response 0x5C - confirm_erase_all() — single frame, token=0xFE, response 0x5D - read_event_storage_range() — two-step read (probe+data), token=0xFE Response last 8 bytes = first/last stored event key; both 0x01110000 after erase client.py: - list_event_keys() — browse-mode 1E→0A→1F walk, no waveform download; returns list of hex key strings; used as fast pre-check before get_events() - get_events(skip_waveform_for_keys=set()) — for already-seen keys: only 0A+1F(browse), skips 1E-arm/0C/POLL×3/5A entirely - delete_all_events() — orchestrates the confirmed erase sequence: 0xA3 → 0x1C → 0x06 → 0xA2; logs first/last key from storage range response Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -57,7 +57,7 @@ SUB_POLL = 0x5B
|
||||
SUB_SERIAL_NUMBER = 0x15
|
||||
SUB_FULL_CONFIG = 0x01
|
||||
SUB_EVENT_INDEX = 0x08
|
||||
SUB_CHANNEL_CONFIG = 0x06
|
||||
SUB_CHANNEL_CONFIG = 0x06 # Event storage range read (first/last key) ✅
|
||||
SUB_MONITOR_STATUS = 0x1C # Monitoring status read (battery, memory, mode) ✅
|
||||
SUB_EVENT_HEADER = 0x1E
|
||||
SUB_EVENT_ADVANCE = 0x1F
|
||||
@@ -82,6 +82,12 @@ SUB_TRIGGER_CONFIRM = 0x83 # Confirm trigger write ✅
|
||||
SUB_START_MONITORING = 0x96 # Start monitoring → response 0x69 ✅
|
||||
SUB_STOP_MONITORING = 0x97 # Stop monitoring → response 0x68 ✅
|
||||
|
||||
# Erase-all SUBs (confirmed from 4-11-26 MITM capture)
|
||||
# Both use token=0xFE at params[7] and return minimal 11-byte acks.
|
||||
# Standard response formula applies: 0xFF - SUB.
|
||||
SUB_ERASE_ALL_BEGIN = 0xA3 # Begin erase all events → response 0x5C ✅
|
||||
SUB_ERASE_ALL_CONFIRM = 0xA2 # Confirm erase all events → response 0x5D ✅
|
||||
|
||||
# Hardcoded data lengths for the two-step read protocol.
|
||||
#
|
||||
# The S3 probe response page_key is always 0x0000 — it does NOT carry the
|
||||
@@ -96,6 +102,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_CHANNEL_CONFIG: 0x24, # 36-byte event storage range (first/last key) ✅
|
||||
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 ✅
|
||||
@@ -1137,6 +1144,78 @@ class MiniMateProtocol:
|
||||
self._send(frame)
|
||||
return self.recv_write_ack(expected_sub=rsp_sub)
|
||||
|
||||
def read_event_storage_range(self) -> S3Frame:
|
||||
"""
|
||||
Read event storage range (SUB 0x06 → response 0xF9).
|
||||
|
||||
Two-step read: probe (offset=0x00) then data (offset=0x24 = 36 bytes).
|
||||
Uses token=0xFE at params[7] — same as the erase sequence.
|
||||
|
||||
The 36-byte response ends with two 4-byte event keys (first and last
|
||||
stored event key). After a successful erase, both keys are 0x01110000
|
||||
(device-empty sentinel). Confirmed from 4-11-26 MITM capture.
|
||||
|
||||
Returns:
|
||||
S3Frame with 36 bytes of storage range data.
|
||||
|
||||
Raises:
|
||||
ProtocolError: on timeout or wrong response SUB.
|
||||
"""
|
||||
rsp_sub = _expected_rsp_sub(SUB_CHANNEL_CONFIG) # 0xFF - 0x06 = 0xF9
|
||||
params = token_params(0xFE)
|
||||
log.debug("read_event_storage_range: probe step rsp_sub=0x%02X", rsp_sub)
|
||||
self._send(build_bw_frame(SUB_CHANNEL_CONFIG, offset=0x00, params=params))
|
||||
self._recv_one(expected_sub=rsp_sub)
|
||||
|
||||
log.debug(
|
||||
"read_event_storage_range: data step offset=0x%02X",
|
||||
DATA_LENGTHS[SUB_CHANNEL_CONFIG],
|
||||
)
|
||||
self._send(build_bw_frame(SUB_CHANNEL_CONFIG,
|
||||
offset=DATA_LENGTHS[SUB_CHANNEL_CONFIG],
|
||||
params=params))
|
||||
return self._recv_one(expected_sub=rsp_sub)
|
||||
|
||||
def begin_erase_all(self) -> S3Frame:
|
||||
"""
|
||||
Send Begin-Erase-All command (SUB 0xA3 → response 0x5C).
|
||||
|
||||
Single frame with token=0xFE at params[7]. The device acknowledges with
|
||||
a minimal ack and begins the erase process. Follow up with
|
||||
read_monitor_status() + read_event_storage_range() + confirm_erase_all()
|
||||
to complete the sequence. Confirmed from 4-11-26 MITM capture.
|
||||
|
||||
Returns:
|
||||
S3Frame ack from device (SUB 0x5C).
|
||||
|
||||
Raises:
|
||||
ProtocolError: on timeout or wrong response SUB.
|
||||
"""
|
||||
rsp_sub = _expected_rsp_sub(SUB_ERASE_ALL_BEGIN) # 0xFF - 0xA3 = 0x5C
|
||||
log.debug("begin_erase_all: rsp_sub=0x%02X", rsp_sub)
|
||||
self._send(build_bw_frame(SUB_ERASE_ALL_BEGIN, params=token_params(0xFE)))
|
||||
return self._recv_one(expected_sub=rsp_sub)
|
||||
|
||||
def confirm_erase_all(self) -> S3Frame:
|
||||
"""
|
||||
Send Confirm-Erase-All command (SUB 0xA2 → response 0x5D).
|
||||
|
||||
Single frame with token=0xFE at params[7]. Must be preceded by
|
||||
begin_erase_all() + read_monitor_status() + read_event_storage_range().
|
||||
After this call the device memory is cleared. Confirmed from 4-11-26
|
||||
MITM capture.
|
||||
|
||||
Returns:
|
||||
S3Frame ack from device (SUB 0x5D).
|
||||
|
||||
Raises:
|
||||
ProtocolError: on timeout or wrong response SUB.
|
||||
"""
|
||||
rsp_sub = _expected_rsp_sub(SUB_ERASE_ALL_CONFIRM) # 0xFF - 0xA2 = 0x5D
|
||||
log.debug("confirm_erase_all: rsp_sub=0x%02X", rsp_sub)
|
||||
self._send(build_bw_frame(SUB_ERASE_ALL_CONFIRM, params=token_params(0xFE)))
|
||||
return self._recv_one(expected_sub=rsp_sub)
|
||||
|
||||
# ── Internal helpers ──────────────────────────────────────────────────────
|
||||
|
||||
def _send(self, frame: bytes) -> None:
|
||||
|
||||
Reference in New Issue
Block a user