feat: updates to 0.8.0 - initial write functions
This commit is contained in:
@@ -194,6 +194,109 @@ def build_bw_frame(sub: int, offset: int = 0, params: bytes = bytes(10)) -> byte
|
||||
return wire
|
||||
|
||||
|
||||
def build_bw_write_frame(
|
||||
sub: int,
|
||||
data: bytes,
|
||||
*,
|
||||
offset: int = 0,
|
||||
params: bytes = bytes(10),
|
||||
) -> bytes:
|
||||
"""
|
||||
Build a BW→S3 write-command frame.
|
||||
|
||||
Write frames extend the standard 16-byte read header with a variable-length
|
||||
data payload. They use a different checksum formula from read frames.
|
||||
|
||||
**CRITICAL: Write frames use minimal DLE stuffing.**
|
||||
|
||||
Unlike read frames (build_bw_frame), write frames do NOT apply full DLE
|
||||
stuffing to the payload. Only the BW_CMD byte (0x10) at position [0] is
|
||||
doubled to 0x10 0x10 on the wire. All other bytes — flags, sub, offset,
|
||||
params, data, and checksum — are written RAW with no stuffing, even if they
|
||||
contain 0x10 bytes (e.g. offset_hi=0x10 for compliance chunks, or 0x10
|
||||
bytes in the write data payload).
|
||||
|
||||
Confirmed from 3-11-26 BW TX capture (frames 102–112): all 11 write frames
|
||||
match the rule "double BW_CMD only; everything else raw." ✅ 2026-04-07.
|
||||
|
||||
Wire layout:
|
||||
[41] ACK
|
||||
[02] STX
|
||||
[10 10] BW_CMD doubled (the ONLY DLE stuffing applied)
|
||||
[00] flags
|
||||
[sub] write command byte (0x68–0x83)
|
||||
[00] always zero
|
||||
[hi][lo] offset as uint16 BE (raw; NOT stuffed even if hi=0x10)
|
||||
[params] 10 bytes (raw)
|
||||
[data] variable-length write payload (raw; NOT stuffed)
|
||||
[chk] checksum byte (raw; NOT stuffed even if 0x10)
|
||||
[03] ETX
|
||||
|
||||
De-stuffed payload (for checksum computation):
|
||||
[0] BW_CMD 0x10
|
||||
[1] flags 0x00
|
||||
[2] SUB write command byte
|
||||
[3] 0x00 always zero
|
||||
[4] offset_hi
|
||||
[5] offset_lo
|
||||
[6:16] params 10 bytes
|
||||
[16:] data write payload
|
||||
[-1] chk
|
||||
|
||||
**Checksum formula (confirmed 2026-03-12 from 3-11-26 BW TX capture):**
|
||||
chk = (sum(b for b in payload[2:] if b != 0x10) + 0x10) % 256
|
||||
where payload = destuffed content BEFORE appending chk.
|
||||
This skips all 0x10 bytes in payload[2:] (sub onwards), including any
|
||||
0x10 bytes in the offset, params, data, and the checksum byte itself.
|
||||
|
||||
The offset field [4:6] meaning per write SUB:
|
||||
- SUBs 68, 69, 82 (single-chunk writes): offset = data[1] + 2, where
|
||||
data[1] is an embedded length field in the write payload.
|
||||
Confirmed from capture: 68→0x5A (data[1]=0x58+2), 82→0x1C
|
||||
(data[1]=0x1A+2), 69→0xCA (data[1]=0xC8+2).
|
||||
- SUB 71 (multi-chunk compliance): 0x1004 for full chunks, 0x002C
|
||||
for the final partial chunk.
|
||||
- Confirm frames (72, 73, 74, 83): offset=0, no data.
|
||||
|
||||
Args:
|
||||
sub: Write command SUB byte.
|
||||
data: Write payload (variable length; empty for confirm frames).
|
||||
offset: 16-bit value placed at [4:6]. See per-SUB notes above.
|
||||
params: 10 bytes placed at [6:16]. All-zero for most writes; compliance
|
||||
chunk writes use chunk-specific values.
|
||||
|
||||
Returns:
|
||||
Complete frame bytes ready to write to the transport.
|
||||
"""
|
||||
if len(params) != 10:
|
||||
raise ValueError(f"params must be exactly 10 bytes, got {len(params)}")
|
||||
if offset > 0xFFFF:
|
||||
raise ValueError(f"offset must fit in uint16, got {offset:#06x}")
|
||||
|
||||
offset_hi = (offset >> 8) & 0xFF
|
||||
offset_lo = offset & 0xFF
|
||||
|
||||
# Destuffed payload (used only for checksum; not sent directly)
|
||||
payload_no_chk = bytes([BW_CMD, 0x00, sub, 0x00, offset_hi, offset_lo]) + params + data
|
||||
|
||||
# Large-frame checksum: sum payload[2:] skipping all 0x10 bytes, add 0x10.
|
||||
# Applied to the destuffed representation — confirms correctly against
|
||||
# all 11 write frames in the 3-11-26/170151 BW TX capture. ✅
|
||||
chk = (sum(b for b in payload_no_chk[2:] if b != 0x10) + 0x10) & 0xFF
|
||||
|
||||
# Wire construction: only BW_CMD is doubled; everything else is raw.
|
||||
# Do NOT use dle_stuff() here — that would incorrectly double 0x10 bytes
|
||||
# in the offset, params, and data sections.
|
||||
wire = (
|
||||
bytes([ACK, STX]) # Frame prefix (not part of payload)
|
||||
+ bytes([BW_CMD, BW_CMD]) # BW_CMD doubled (only DLE stuffing applied)
|
||||
+ payload_no_chk[1:] # flags, sub, offset, params, data — RAW
|
||||
+ bytes([chk]) # checksum — RAW
|
||||
+ bytes([ETX]) # Frame terminator
|
||||
)
|
||||
return wire
|
||||
|
||||
|
||||
def waveform_key_params(key4: bytes) -> bytes:
|
||||
"""
|
||||
Build the 10-byte params block that carries a 4-byte waveform key.
|
||||
|
||||
Reference in New Issue
Block a user