feat: updates to 0.8.0 - initial write functions
This commit is contained in:
+277
-4
@@ -30,6 +30,7 @@ from .framing import (
|
||||
S3FrameParser,
|
||||
build_bw_frame,
|
||||
build_5a_frame,
|
||||
build_bw_write_frame,
|
||||
waveform_key_params,
|
||||
token_params,
|
||||
bulk_waveform_params,
|
||||
@@ -65,6 +66,17 @@ SUB_BULK_WAVEFORM = 0x5A
|
||||
SUB_COMPLIANCE = 0x1A
|
||||
SUB_UNKNOWN_2E = 0x2E
|
||||
|
||||
# Write command SUBs (= Read SUB + 0x60, confirmed from BW captures 3-11-26)
|
||||
# Response SUB follows the standard 0xFF - Request SUB rule.
|
||||
SUB_EVENT_INDEX_WRITE = 0x68 # Write event index (0x08 + 0x60) ✅
|
||||
SUB_WAVEFORM_DATA_WRITE = 0x69 # Write waveform data (0x09 + 0x60) ✅
|
||||
SUB_COMPLIANCE_WRITE = 0x71 # Write compliance cfg (0x11 + 0x60) ✅
|
||||
SUB_WRITE_CONFIRM_A = 0x72 # Confirm A — sent after 71×3 and other writes ✅
|
||||
SUB_WRITE_CONFIRM_B = 0x73 # Confirm B — sent after 68 ✅
|
||||
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 ✅
|
||||
|
||||
# Hardcoded data lengths for the two-step read protocol.
|
||||
#
|
||||
# The S3 probe response page_key is always 0x0000 — it does NOT carry the
|
||||
@@ -95,10 +107,11 @@ DATA_LENGTHS: dict[int, int] = {
|
||||
# Confirmed from 1-2-26 BW TX capture analysis (2026-04-02).
|
||||
_BULK_CHUNK_OFFSET = 0x1004 # offset field for probe + all regular chunk requests ✅
|
||||
_BULK_TERM_OFFSET = 0x005A # offset field for termination request ✅
|
||||
_BULK_COUNTER_STEP = 0x0400 # chunk counter increment for chunks 2+ ✅
|
||||
# Chunk 1 counter is 0x1004 (NOT 1 * 0x0400 = 0x0400). Confirmed from 4-2-26 BW TX
|
||||
# capture. Chunks 2+ use n * 0x0400 (0x0800, 0x0C00, …). Device silently ignores
|
||||
# frames with wrong counter — this was the root cause of the full-waveform timeout.
|
||||
_BULK_COUNTER_STEP = 0x0400 # chunk counter increment per chunk ✅
|
||||
# Chunk counter formula: chunk_num * 0x0400 for ALL chunks including chunk 1.
|
||||
# Earlier captures showed 0x1004 for chunk 1 — that was a Blastware artifact, not a
|
||||
# protocol requirement. Confirmed 2026-04-06: 0x0400 for chunk 1 works; 0x1004
|
||||
# causes a 120-second device timeout. Formula n * 0x0400 is used for all chunks.
|
||||
|
||||
# Default timeout values (seconds).
|
||||
# MiniMate Plus is a slow device — keep these generous.
|
||||
@@ -749,6 +762,266 @@ class MiniMateProtocol:
|
||||
|
||||
return bytes(config)
|
||||
|
||||
# ── Write commands (SUBs 68–83) ───────────────────────────────────────────
|
||||
|
||||
def recv_write_ack(
|
||||
self,
|
||||
expected_sub: int,
|
||||
timeout: Optional[float] = None,
|
||||
) -> S3Frame:
|
||||
"""
|
||||
Wait for a write-ack S3 frame.
|
||||
|
||||
All write ack responses are 17-byte frames (11-byte header + no data +
|
||||
1 checksum byte) with SUB = 0xFF - request_SUB. The page_key and data
|
||||
section carry zeros. Confirmed from 3-11-26 BW capture.
|
||||
|
||||
Args:
|
||||
expected_sub: Expected response SUB byte (0xFF - write_request_SUB).
|
||||
timeout: Seconds to wait; defaults to self._recv_timeout.
|
||||
|
||||
Returns:
|
||||
The ack S3Frame.
|
||||
|
||||
Raises:
|
||||
TimeoutError: if no frame arrives in time.
|
||||
UnexpectedResponse: if the response SUB doesn't match.
|
||||
"""
|
||||
log.debug("recv_write_ack: waiting for SUB=0x%02X", expected_sub)
|
||||
ack = self._recv_one(expected_sub=expected_sub, timeout=timeout)
|
||||
log.debug(
|
||||
"recv_write_ack: received SUB=0x%02X page=0x%04X data=%d bytes",
|
||||
ack.sub, ack.page_key, len(ack.data),
|
||||
)
|
||||
return ack
|
||||
|
||||
def write_confirm(self, sub: int) -> S3Frame:
|
||||
"""
|
||||
Send a zero-data confirm frame and wait for the ack.
|
||||
|
||||
Confirm frames (SUBs 72, 73, 74, 83) carry no write data — they are
|
||||
16-byte header-only frames (offset=0, params=zeros, data=b"") with the
|
||||
DLE-aware large-frame checksum. The device acks with the complementary
|
||||
RSP_SUB.
|
||||
|
||||
Args:
|
||||
sub: Confirm SUB byte (SUB_WRITE_CONFIRM_A/B/C or SUB_TRIGGER_CONFIRM).
|
||||
|
||||
Returns:
|
||||
The ack S3Frame.
|
||||
|
||||
Raises:
|
||||
ProtocolError: on timeout or wrong response SUB.
|
||||
"""
|
||||
rsp_sub = _expected_rsp_sub(sub)
|
||||
frame = build_bw_write_frame(sub, b"")
|
||||
log.debug("write_confirm: SUB=0x%02X frame=%s", sub, frame.hex())
|
||||
self._send(frame)
|
||||
return self.recv_write_ack(expected_sub=rsp_sub)
|
||||
|
||||
def write_event_index(self, data: bytes) -> S3Frame:
|
||||
"""
|
||||
Send a SUB 68 (EVENT_INDEX_WRITE) frame and await the confirm ack (SUB 97).
|
||||
|
||||
Offset formula: data[1] + 2 — confirmed from 3-11-26 BW TX capture frame 102.
|
||||
The write payload has a 2-byte header [0x00][length] where data[1] encodes
|
||||
the length of the meaningful payload; offset = data[1] + 2.
|
||||
|
||||
Example from capture:
|
||||
data[0:4] = 00 58 09 00 (data[1]=0x58=88 → offset=0x5A=90)
|
||||
data length = 91, offset = 90
|
||||
|
||||
Write sequence fragment:
|
||||
68 (data) → device acks with SUB 0x97
|
||||
73 (confirm) → device acks with SUB 0x8C
|
||||
|
||||
Callers should call write_confirm(SUB_WRITE_CONFIRM_B) after this.
|
||||
|
||||
Args:
|
||||
data: Raw event-index payload bytes to write to the device.
|
||||
Must be at least 2 bytes. data[1] must contain the length field.
|
||||
|
||||
Returns:
|
||||
The SUB 0x97 ack frame.
|
||||
|
||||
Raises:
|
||||
ProtocolError: on timeout or wrong response SUB.
|
||||
ValueError: if data is shorter than 2 bytes.
|
||||
"""
|
||||
if len(data) < 2:
|
||||
raise ValueError(f"event index write data must be at least 2 bytes, got {len(data)}")
|
||||
rsp_sub = _expected_rsp_sub(SUB_EVENT_INDEX_WRITE) # 0xFF - 0x68 = 0x97
|
||||
offset = data[1] + 2
|
||||
frame = build_bw_write_frame(SUB_EVENT_INDEX_WRITE, data, offset=offset)
|
||||
log.debug(
|
||||
"write_event_index: %d bytes data[1]=0x%02X offset=0x%04X rsp_sub=0x%02X",
|
||||
len(data), data[1], offset, rsp_sub,
|
||||
)
|
||||
self._send(frame)
|
||||
return self.recv_write_ack(expected_sub=rsp_sub)
|
||||
|
||||
def write_waveform_data(self, data: bytes) -> S3Frame:
|
||||
"""
|
||||
Send a SUB 69 (WAVEFORM_DATA_WRITE) frame and await the confirm ack (SUB 96).
|
||||
|
||||
Offset formula: data[1] + 2 — same pattern as write_event_index().
|
||||
Confirmed from 3-11-26 BW TX capture frame 110:
|
||||
data[0:4] = 00 c8 08 00 (data[1]=0xC8=200 → offset=0xCA=202)
|
||||
data length = 204, offset = 202
|
||||
|
||||
Write sequence fragment:
|
||||
69 (data) → device acks with SUB 0x96
|
||||
74 (confirm) → device acks with SUB 0x8B
|
||||
72 (confirm) → device acks with SUB 0x8D
|
||||
|
||||
Callers should call write_confirm(SUB_WRITE_CONFIRM_C) then
|
||||
write_confirm(SUB_WRITE_CONFIRM_A) after this.
|
||||
|
||||
Args:
|
||||
data: Raw waveform-data payload bytes to write.
|
||||
Must be at least 2 bytes. data[1] must contain the length field.
|
||||
|
||||
Returns:
|
||||
The SUB 0x96 ack frame.
|
||||
|
||||
Raises:
|
||||
ProtocolError: on timeout or wrong response SUB.
|
||||
ValueError: if data is shorter than 2 bytes.
|
||||
"""
|
||||
if len(data) < 2:
|
||||
raise ValueError(f"waveform data write payload must be at least 2 bytes, got {len(data)}")
|
||||
rsp_sub = _expected_rsp_sub(SUB_WAVEFORM_DATA_WRITE) # 0xFF - 0x69 = 0x96
|
||||
offset = data[1] + 2
|
||||
frame = build_bw_write_frame(SUB_WAVEFORM_DATA_WRITE, data, offset=offset)
|
||||
log.debug(
|
||||
"write_waveform_data: %d bytes data[1]=0x%02X offset=0x%04X rsp_sub=0x%02X",
|
||||
len(data), data[1], offset, rsp_sub,
|
||||
)
|
||||
self._send(frame)
|
||||
return self.recv_write_ack(expected_sub=rsp_sub)
|
||||
|
||||
def write_compliance_config_raw(self, data: bytes) -> None:
|
||||
"""
|
||||
Send the SUB 71 (COMPLIANCE_WRITE) 3-chunk sequence and final confirm.
|
||||
|
||||
The full compliance config payload (~2128 bytes) is split into exactly 3
|
||||
chunks with hardcoded boundaries and params confirmed from the 3-11-26 BW
|
||||
TX capture (frames 104–108):
|
||||
|
||||
Chunk 1 — first 1027 bytes:
|
||||
offset=0x1004 params=bytes(10)
|
||||
device acks SUB 0x8E
|
||||
|
||||
Chunk 2 — next 1055 bytes:
|
||||
offset=0x1004 params=b'\\x00\\x00\\x00\\x00\\x10\\x04' + b'\\x00'*4
|
||||
device acks SUB 0x8E
|
||||
|
||||
Chunk 3 — remaining bytes:
|
||||
offset=0x002C params=b'\\x00\\x00\\x08' + b'\\x00'*7
|
||||
device acks SUB 0x8E
|
||||
|
||||
Confirm — SUB 72 (zero data):
|
||||
device acks SUB 0x8D
|
||||
|
||||
The total write payload should be at least 1027+1055=2082 bytes; chunk 3
|
||||
carries everything after offset 2082 (typically ~46 bytes for a 2128-byte
|
||||
config).
|
||||
|
||||
Args:
|
||||
data: Raw compliance config bytes to write. Must be at least 2082 bytes.
|
||||
|
||||
Raises:
|
||||
ValueError: if data is too short to fill chunks 1 and 2.
|
||||
ProtocolError: on timeout or wrong response SUB from any chunk.
|
||||
"""
|
||||
_CHUNK1_SIZE = 1027
|
||||
_CHUNK2_SIZE = 1055
|
||||
_CHUNK1_OFFSET = 0x1004
|
||||
_CHUNK2_OFFSET = 0x1004
|
||||
_CHUNK3_OFFSET = 0x002C
|
||||
|
||||
_CHUNK1_PARAMS = bytes(10)
|
||||
_CHUNK2_PARAMS = bytes([0x00, 0x00, 0x00, 0x10, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00])
|
||||
_CHUNK3_PARAMS = bytes([0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])
|
||||
|
||||
min_size = _CHUNK1_SIZE + _CHUNK2_SIZE
|
||||
if len(data) < min_size:
|
||||
raise ValueError(
|
||||
f"Compliance write data too short: {len(data)} bytes, "
|
||||
f"need at least {min_size} (chunk1={_CHUNK1_SIZE} + chunk2={_CHUNK2_SIZE})"
|
||||
)
|
||||
|
||||
rsp_sub = _expected_rsp_sub(SUB_COMPLIANCE_WRITE) # 0xFF - 0x71 = 0x8E
|
||||
|
||||
chunk1 = data[:_CHUNK1_SIZE]
|
||||
chunk2 = data[_CHUNK1_SIZE : _CHUNK1_SIZE + _CHUNK2_SIZE]
|
||||
chunk3 = data[_CHUNK1_SIZE + _CHUNK2_SIZE :]
|
||||
|
||||
chunks = [
|
||||
(1, chunk1, _CHUNK1_OFFSET, _CHUNK1_PARAMS),
|
||||
(2, chunk2, _CHUNK2_OFFSET, _CHUNK2_PARAMS),
|
||||
(3, chunk3, _CHUNK3_OFFSET, _CHUNK3_PARAMS),
|
||||
]
|
||||
|
||||
for chunk_num, chunk_data, chunk_offset, chunk_params in chunks:
|
||||
frame = build_bw_write_frame(
|
||||
SUB_COMPLIANCE_WRITE,
|
||||
chunk_data,
|
||||
offset=chunk_offset,
|
||||
params=chunk_params,
|
||||
)
|
||||
log.debug(
|
||||
"write_compliance_config_raw: chunk %d %d bytes "
|
||||
"offset=0x%04X params=%s",
|
||||
chunk_num, len(chunk_data), chunk_offset, chunk_params.hex(),
|
||||
)
|
||||
self._send(frame)
|
||||
self.recv_write_ack(expected_sub=rsp_sub)
|
||||
log.debug("write_compliance_config_raw: chunk %d acked", chunk_num)
|
||||
|
||||
# Final confirm (SUB 72)
|
||||
log.debug("write_compliance_config_raw: sending confirm (SUB 0x72)")
|
||||
self.write_confirm(SUB_WRITE_CONFIRM_A)
|
||||
log.debug("write_compliance_config_raw: done")
|
||||
|
||||
def write_trigger_config(self, data: bytes) -> S3Frame:
|
||||
"""
|
||||
Send a SUB 82 (TRIGGER_CONFIG_WRITE) frame and await the confirm ack (SUB 7D).
|
||||
|
||||
Offset formula: data[1] + 2 — same pattern as write_event_index().
|
||||
Confirmed from 3-11-26 BW TX capture frame 108:
|
||||
data[0:4] = 00 1a d5 00 (data[1]=0x1A=26 → offset=0x1C=28)
|
||||
data length = 29, offset = 28
|
||||
|
||||
Write sequence fragment:
|
||||
82 (data) → device acks with SUB 0x7D
|
||||
83 (confirm) → device acks with SUB 0x7C
|
||||
|
||||
Callers should call write_confirm(SUB_TRIGGER_CONFIRM) after this.
|
||||
|
||||
Args:
|
||||
data: Raw trigger-config payload bytes to write.
|
||||
Must be at least 2 bytes. data[1] must contain the length field.
|
||||
|
||||
Returns:
|
||||
The SUB 0x7D ack frame.
|
||||
|
||||
Raises:
|
||||
ProtocolError: on timeout or wrong response SUB.
|
||||
ValueError: if data is shorter than 2 bytes.
|
||||
"""
|
||||
if len(data) < 2:
|
||||
raise ValueError(f"trigger config write payload must be at least 2 bytes, got {len(data)}")
|
||||
rsp_sub = _expected_rsp_sub(SUB_TRIGGER_CONFIG_WRITE) # 0xFF - 0x82 = 0x7D
|
||||
offset = data[1] + 2
|
||||
frame = build_bw_write_frame(SUB_TRIGGER_CONFIG_WRITE, data, offset=offset)
|
||||
log.debug(
|
||||
"write_trigger_config: %d bytes data[1]=0x%02X offset=0x%04X rsp_sub=0x%02X",
|
||||
len(data), data[1], offset, rsp_sub,
|
||||
)
|
||||
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