protocol: accumulate multiple E5 frames for chunked compliance config
BE11529 sends the compliance config (SUB 1A / E5 response) as a stream of small frames (~44 bytes each) rather than one large frame like BE18189. Changes: - read_compliance_config(): loop calling _recv_one() until 0x082A bytes accumulated or 2s inter-frame gap; first frame strips 11-byte echo header, subsequent frames logged in full for structure analysis - _recv_one(): add reset_parser=False option to preserve parser state and _pending_frames buffer between consecutive reads from one device response; also stash any extra frames parsed in a single TCP chunk so they are not lost Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -135,6 +135,9 @@ class MiniMateProtocol:
|
|||||||
self._transport = transport
|
self._transport = transport
|
||||||
self._recv_timeout = recv_timeout
|
self._recv_timeout = recv_timeout
|
||||||
self._parser = S3FrameParser()
|
self._parser = S3FrameParser()
|
||||||
|
# Extra frames buffered by _recv_one that arrived alongside the target frame.
|
||||||
|
# Used when reset_parser=False so we don't discard already-parsed frames.
|
||||||
|
self._pending_frames: list[S3Frame] = []
|
||||||
|
|
||||||
# ── Public API ────────────────────────────────────────────────────────────
|
# ── Public API ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -397,32 +400,28 @@ class MiniMateProtocol:
|
|||||||
|
|
||||||
def read_compliance_config(self) -> bytes:
|
def read_compliance_config(self) -> bytes:
|
||||||
"""
|
"""
|
||||||
Send the SUB 1A (COMPLIANCE_CONFIG) two-step read.
|
Send the SUB 1A (COMPLIANCE_CONFIG) multi-step read and accumulate
|
||||||
|
all E5 response frames into a single config byte string.
|
||||||
|
|
||||||
Returns the full 2090-byte compliance config block (E5 response)
|
BE18189 sends the full config in one large E5 frame (~4245 cfg bytes).
|
||||||
containing:
|
BE11529 appears to chunk the response — each E5 frame carries ~44 bytes
|
||||||
- Trigger and alarm levels per channel (IEEE 754 BE floats)
|
of cfg data. This method loops until the expected 0x082A (2090) bytes
|
||||||
- Record time (float at offset +0x28)
|
are accumulated or the inter-frame gap exceeds _INTER_FRAME_TIMEOUT.
|
||||||
- Project strings (Project, Client, User Name, Seis Loc, Extended Notes)
|
|
||||||
- Channel labels and unit strings
|
Frame structure (confirmed from raw BW captures 3-11-26):
|
||||||
- Per-channel max range values
|
Probe (Frame A): byte[5]=0x00, params[7]=0x64
|
||||||
|
Data req (Frame D): byte[5]=0x2A, params[2]=0x08, params[7]=0x64
|
||||||
|
|
||||||
|
0x082A split: byte[5]=0x2A (offset low), params[2]=0x08 (length high)
|
||||||
|
params[7]=0x64 required in both probe and data-request.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
2090-byte compliance config data (data[11:11+0x082A]).
|
Accumulated compliance config bytes. First frame: data[11:] (skips
|
||||||
|
11-byte echo header). Subsequent frames: structure logged and
|
||||||
|
accumulated from data[11:] as well — adjust offset if structure differs.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
ProtocolError: on timeout, bad checksum, or wrong response SUB.
|
ProtocolError: if the very first E5 frame is not received (hard timeout).
|
||||||
|
|
||||||
Confirmed from raw BW captures (3-11-26):
|
|
||||||
Probe (Frame A): [5]=0x00, params[7]=0x64
|
|
||||||
Data req (Frame D): [5]=0x2A, params[2]=0x08, params[7]=0x64
|
|
||||||
|
|
||||||
The 16-bit length 0x082A is split across two fields:
|
|
||||||
byte[5] = 0x2A (offset/length low byte, as in all two-step reads)
|
|
||||||
params[2] = 0x08 (length high byte, packed into params rather than byte[4])
|
|
||||||
|
|
||||||
params[7] = 0x64 = 100 is required in both probe and data request.
|
|
||||||
Purpose unknown — possibly max channel count or backlight timeout.
|
|
||||||
"""
|
"""
|
||||||
rsp_sub = _expected_rsp_sub(SUB_COMPLIANCE)
|
rsp_sub = _expected_rsp_sub(SUB_COMPLIANCE)
|
||||||
|
|
||||||
@@ -433,26 +432,81 @@ class MiniMateProtocol:
|
|||||||
self._recv_one(expected_sub=rsp_sub)
|
self._recv_one(expected_sub=rsp_sub)
|
||||||
|
|
||||||
# Data request — 0x082A encoded as: byte[5]=0x2A, params[2]=0x08, params[7]=0x64
|
# Data request — 0x082A encoded as: byte[5]=0x2A, params[2]=0x08, params[7]=0x64
|
||||||
# NOT as a uint16 split across bytes[4:5] — confirmed from BW capture 3-11-26.
|
|
||||||
_DATA_PARAMS = bytes([0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00])
|
_DATA_PARAMS = bytes([0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00])
|
||||||
log.debug("read_compliance_config: 1A data request offset=0x2A params[2]=0x08")
|
log.debug("read_compliance_config: 1A data request offset=0x2A params[2]=0x08")
|
||||||
self._send(build_bw_frame(SUB_COMPLIANCE, 0x2A, _DATA_PARAMS))
|
self._send(build_bw_frame(SUB_COMPLIANCE, 0x2A, _DATA_PARAMS))
|
||||||
data_rsp = self._recv_one(expected_sub=rsp_sub)
|
|
||||||
|
|
||||||
# Slice off the 11-byte echo header; take ALL remaining data (not 0x082A limit —
|
# ── Accumulate E5 frames ──────────────────────────────────────────────
|
||||||
# actual E5 payload is ~4245 bytes, varying by firmware/config).
|
# BE18189 sends one large frame; BE11529 may chunk into many small ones.
|
||||||
config = data_rsp.data[11:]
|
# Loop until target bytes received or inter-frame gap expires.
|
||||||
log.warning(
|
_TARGET = 0x082A # 2090 bytes — known total from BW captures
|
||||||
"read_compliance_config: E5 response total=%d data=%d cfg=%d bytes",
|
_INTER_FRAME_TIMEOUT = 2.0 # seconds; give up if no new frame within this
|
||||||
len(data_rsp.data) + 5, len(data_rsp.data), len(config),
|
config = bytearray()
|
||||||
|
frame_count = 0
|
||||||
|
|
||||||
|
while len(config) < _TARGET:
|
||||||
|
timeout = self._recv_timeout if frame_count == 0 else _INTER_FRAME_TIMEOUT
|
||||||
|
# reset_parser=True only for the first frame; subsequent frames must not
|
||||||
|
# discard bytes already buffered from the device's chunked response stream.
|
||||||
|
try:
|
||||||
|
data_rsp = self._recv_one(
|
||||||
|
expected_sub=rsp_sub,
|
||||||
|
timeout=timeout,
|
||||||
|
reset_parser=(frame_count == 0),
|
||||||
)
|
)
|
||||||
|
except TimeoutError:
|
||||||
|
if frame_count == 0:
|
||||||
|
raise # hard fail — device never responded at all
|
||||||
|
# Inter-frame gap expired — device finished sending
|
||||||
|
log.warning(
|
||||||
|
"read_compliance_config: inter-frame timeout after %d frames "
|
||||||
|
"(%d / %d cfg bytes received)",
|
||||||
|
frame_count, len(config), _TARGET,
|
||||||
|
)
|
||||||
|
break
|
||||||
|
|
||||||
|
frame_count += 1
|
||||||
|
|
||||||
|
if frame_count == 1:
|
||||||
|
# First frame: strip 11-byte echo header (length + key echo + padding)
|
||||||
|
chunk = data_rsp.data[11:]
|
||||||
|
log.warning(
|
||||||
|
"read_compliance_config: frame %d page=0x%04X "
|
||||||
|
"data=%d cfg_chunk=%d (header stripped)",
|
||||||
|
frame_count, data_rsp.page_key, len(data_rsp.data), len(chunk),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Subsequent frames: log raw prefix to determine structure,
|
||||||
|
# then strip the same 11-byte header (assumption — verify from logs).
|
||||||
|
raw = data_rsp.data
|
||||||
|
log.warning(
|
||||||
|
"read_compliance_config: frame %d page=0x%04X data=%d "
|
||||||
|
"raw[0:16]=%s",
|
||||||
|
frame_count, data_rsp.page_key, len(raw),
|
||||||
|
raw[:16].hex() if len(raw) >= 16 else raw.hex(),
|
||||||
|
)
|
||||||
|
# Strip header if frame is long enough; otherwise accumulate all
|
||||||
|
chunk = raw[11:] if len(raw) > 11 else raw
|
||||||
|
|
||||||
|
config.extend(chunk)
|
||||||
|
log.warning(
|
||||||
|
"read_compliance_config: running total=%d / %d",
|
||||||
|
len(config), _TARGET,
|
||||||
|
)
|
||||||
|
|
||||||
|
log.warning(
|
||||||
|
"read_compliance_config: done — %d frame(s), %d cfg bytes total",
|
||||||
|
frame_count, len(config),
|
||||||
|
)
|
||||||
|
|
||||||
# Hex dump first 128 bytes for field mapping
|
# Hex dump first 128 bytes for field mapping
|
||||||
for row in range(0, min(len(config), 128), 16):
|
for row in range(0, min(len(config), 128), 16):
|
||||||
chunk = config[row:row+16]
|
row_bytes = bytes(config[row:row + 16])
|
||||||
hex_part = ' '.join(f'{b:02x}' for b in chunk)
|
hex_part = ' '.join(f'{b:02x}' for b in row_bytes)
|
||||||
asc_part = ''.join(chr(b) if 32 <= b < 127 else '.' for b in chunk)
|
asc_part = ''.join(chr(b) if 32 <= b < 127 else '.' for b in row_bytes)
|
||||||
log.warning(" cfg[%04x]: %-48s %s", row, hex_part, asc_part)
|
log.warning(" cfg[%04x]: %-48s %s", row, hex_part, asc_part)
|
||||||
return config
|
|
||||||
|
return bytes(config)
|
||||||
|
|
||||||
# ── Internal helpers ──────────────────────────────────────────────────────
|
# ── Internal helpers ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -465,6 +519,7 @@ class MiniMateProtocol:
|
|||||||
self,
|
self,
|
||||||
expected_sub: Optional[int] = None,
|
expected_sub: Optional[int] = None,
|
||||||
timeout: Optional[float] = None,
|
timeout: Optional[float] = None,
|
||||||
|
reset_parser: bool = True,
|
||||||
) -> S3Frame:
|
) -> S3Frame:
|
||||||
"""
|
"""
|
||||||
Read bytes from the transport until one complete S3 frame is parsed.
|
Read bytes from the transport until one complete S3 frame is parsed.
|
||||||
@@ -476,6 +531,10 @@ class MiniMateProtocol:
|
|||||||
expected_sub: If provided, raises UnexpectedResponse if the
|
expected_sub: If provided, raises UnexpectedResponse if the
|
||||||
received frame's SUB doesn't match.
|
received frame's SUB doesn't match.
|
||||||
timeout: Seconds to wait. Defaults to self._recv_timeout.
|
timeout: Seconds to wait. Defaults to self._recv_timeout.
|
||||||
|
reset_parser: If True (default), reset the parser before reading.
|
||||||
|
Pass False when accumulating multiple frames from a
|
||||||
|
single device response (e.g. chunked E5 replies) so
|
||||||
|
that bytes already buffered between frames are not lost.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The first complete S3Frame received.
|
The first complete S3Frame received.
|
||||||
@@ -486,7 +545,16 @@ class MiniMateProtocol:
|
|||||||
UnexpectedResponse: if expected_sub is set and doesn't match.
|
UnexpectedResponse: if expected_sub is set and doesn't match.
|
||||||
"""
|
"""
|
||||||
deadline = time.monotonic() + (timeout or self._recv_timeout)
|
deadline = time.monotonic() + (timeout or self._recv_timeout)
|
||||||
|
if reset_parser:
|
||||||
self._parser.reset()
|
self._parser.reset()
|
||||||
|
self._pending_frames.clear()
|
||||||
|
|
||||||
|
# If a prior read() parsed more frames than it returned (e.g. two frames
|
||||||
|
# arrived in one TCP chunk), return the buffered one immediately.
|
||||||
|
if self._pending_frames:
|
||||||
|
frame = self._pending_frames.pop(0)
|
||||||
|
self._validate_frame(frame, expected_sub)
|
||||||
|
return frame
|
||||||
|
|
||||||
while time.monotonic() < deadline:
|
while time.monotonic() < deadline:
|
||||||
chunk = self._transport.read(256)
|
chunk = self._transport.read(256)
|
||||||
@@ -494,6 +562,8 @@ class MiniMateProtocol:
|
|||||||
log.debug("RX %d bytes: %s", len(chunk), chunk.hex())
|
log.debug("RX %d bytes: %s", len(chunk), chunk.hex())
|
||||||
frames = self._parser.feed(chunk)
|
frames = self._parser.feed(chunk)
|
||||||
if frames:
|
if frames:
|
||||||
|
# Stash any extras so subsequent calls with reset_parser=False see them
|
||||||
|
self._pending_frames.extend(frames[1:])
|
||||||
frame = frames[0]
|
frame = frames[0]
|
||||||
self._validate_frame(frame, expected_sub)
|
self._validate_frame(frame, expected_sub)
|
||||||
return frame
|
return frame
|
||||||
|
|||||||
Reference in New Issue
Block a user