feat: implement set_project_info functionality and add POC test script

This commit is contained in:
2026-04-07 02:49:17 -04:00
parent bcc044655a
commit 7005ae766d
4 changed files with 283 additions and 1 deletions
+104
View File
@@ -54,6 +54,24 @@ from .transport import SerialTransport, BaseTransport
log = logging.getLogger(__name__)
# ── Module-level constants ────────────────────────────────────────────────────
# Trigger config payload hardcoded from 3-11-26 BW TX capture (BW frame 108).
# No SUB 0x22 read exists in the capture — Blastware writes this fixed blob.
# 29 bytes: [00][1A][D5][00][00][10][03][08][0A] + 18×FF + [00][00]
_TRIGGER_DATA_HARDCODED: bytes = bytes.fromhex(
"001ad500001003080a"
"ffffffffffffffffffffffffffffffffffffff"
"0000"
)
# Compliance ASCII slot format (confirmed from 3-11-26 capture + label search):
# Each label occupies a 64-byte slot.
# Value starts at slot_start + 22, max 42 bytes, null-padded.
_COMPLIANCE_SLOT_SIZE = 64
_COMPLIANCE_VALUE_OFFSET = 22
_COMPLIANCE_VALUE_MAX = _COMPLIANCE_SLOT_SIZE - _COMPLIANCE_VALUE_OFFSET # 42
# ── MiniMateClient ────────────────────────────────────────────────────────────
@@ -599,6 +617,92 @@ class MiniMateClient:
log.info("push_config_raw: complete")
def set_project_info(
self,
project: Optional[str] = None,
client_name: Optional[str] = None,
operator: Optional[str] = None,
seis_loc: Optional[str] = None,
notes: Optional[str] = None,
) -> None:
"""
POC: Read the current config from the device, patch ASCII project fields
in the compliance block, and write the modified config back.
Only fields passed as non-None are modified. All other bytes are
round-tripped verbatim.
Label slot format confirmed from 3-11-26 BW TX capture:
- Compliance buffer is 2126 bytes (three E5 data frames concatenated).
- Each label ("Project:", "Client:", etc.) anchors a 64-byte slot.
- ASCII value starts at slot_start + 22, max 42 bytes, null-padded.
Known labels and their confirmed positions in the 2126-byte buffer:
b"Project:" → value at 125 + 22 = 147
b"Client:" → value at 189 + 22 = 211
b"User Name:" → value at 253 + 22 = 275
b"Seis Loc:" → value at 317 + 22 = 339
b"Extended Notes" → value at 381 + 22 = 403
Write payloads:
event_index_data : 88 bytes — read live from device via SUB 08
compliance_data : 2128 bytes — 2126 read bytes + 2-byte footer \\x00\\x00
(checksum formula unknown for POC; device may ignore it)
trigger_data : 29 bytes — hardcoded from 3-11-26 capture
waveform_data : 204 bytes — read live from device via SUB 09
Raises:
RuntimeError: if not connected.
ProtocolError: if any read or write step fails.
"""
proto = self._require_proto()
# 1. Read current payloads from the device
log.info("set_project_info: reading event index (SUB 08)")
event_index_data = proto.read_event_index() # 88 bytes
log.info("set_project_info: reading compliance config (SUB 1A)")
compliance_raw = bytearray(proto.read_compliance_config()) # 2126 bytes
log.info("set_project_info: reading waveform data (SUB 09)")
waveform_data = proto.read_waveform_data_raw() # 204 bytes
trigger_data = _TRIGGER_DATA_HARDCODED # 29 bytes
# 2. Patch ASCII fields inside the compliance buffer
def _set_field(label: bytes, value: Optional[str]) -> None:
if value is None:
return
idx = compliance_raw.find(label)
if idx < 0:
log.warning(
"set_project_info: label %r not found in compliance data", label
)
return
val_bytes = value.encode("ascii", errors="replace")[: _COMPLIANCE_VALUE_MAX - 1]
padded = val_bytes + b"\x00" * (_COMPLIANCE_VALUE_MAX - len(val_bytes))
compliance_raw[idx + _COMPLIANCE_VALUE_OFFSET : idx + _COMPLIANCE_SLOT_SIZE] = padded
log.debug(
"set_project_info: set %r%r (at offset %d)", label, value, idx
)
_set_field(b"Project:", project)
_set_field(b"Client:", client_name)
_set_field(b"User Name:", operator)
_set_field(b"Seis Loc:", seis_loc)
_set_field(b"Extended Notes", notes)
# 3. Append 2-byte footer (checksum formula unknown; \x00\x00 for POC)
compliance_data = bytes(compliance_raw) + b"\x00\x00" # 2128 bytes
log.info(
"set_project_info: compliance payload ready (%d bytes)", len(compliance_data)
)
# 4. Push the full write sequence to the device
self.push_config_raw(event_index_data, compliance_data, trigger_data, waveform_data)
log.info("set_project_info: complete")
# ── Internal helpers ──────────────────────────────────────────────────────
def _require_proto(self) -> MiniMateProtocol:
+34
View File
@@ -413,6 +413,40 @@ class MiniMateProtocol:
)
return header_bytes, length
def read_waveform_data_raw(self) -> bytes:
"""
Send the SUB 09 (WAVEFORM_DATA) two-step read and return the raw
202-byte (0xCA) waveform data block.
This is the "waveform data" block that Blastware reads from the device
before the write sequence (confirmed from 3-11-26 BW TX capture BW[80-81]).
The returned bytes are used verbatim as the ``waveform_data`` payload for
``write_waveform_data()`` / ``push_config_raw()``.
Returns:
Raw data section starting at data[11:], typically 204 bytes.
(data[11 : 11 + 0xCA] = 202 bytes on some firmware; the actual
length may be 204 depending on firmware version.)
Raises:
ProtocolError: on timeout, bad checksum, or wrong response SUB.
"""
SUB_WAVEFORM_DATA = 0x09
rsp_sub = _expected_rsp_sub(SUB_WAVEFORM_DATA) # 0xFF - 0x09 = 0xF6
length = DATA_LENGTHS[SUB_WAVEFORM_DATA] # 0xCA = 202
log.debug("read_waveform_data_raw: 09 probe")
self._send(build_bw_frame(SUB_WAVEFORM_DATA, 0))
self._recv_one(expected_sub=rsp_sub)
log.debug("read_waveform_data_raw: 09 data request offset=0x%02X", length)
self._send(build_bw_frame(SUB_WAVEFORM_DATA, length))
data_rsp = self._recv_one(expected_sub=rsp_sub)
raw = data_rsp.data[11:]
log.debug("read_waveform_data_raw: got %d bytes", len(raw))
return raw
def read_waveform_record(self, key4: bytes) -> bytes:
"""
Send the SUB 0C (WAVEFORM_RECORD / FULL_WAVEFORM_RECORD) two-step read.