fix(transport): skip async debug frames — the real cause of garbage LF reads

Debug-heavy firmware emits DEBUG_PRINT_STRING/INTEGERS/BYTES (0x0100-0x0102) frames
asynchronously, independent of any command. pm3py's read_response returned the FIRST
frame it saw, so a debug line landing mid-command was misparsed as the reply — observed
on hardware as garbage LF sample counts (e.g. 1593903105), a config-SET that "timed
out", and desynced BigBuf downloads (reading firmware memory / the 0xDEADBEEF stack
canary). The C client routes debug frames to its logger and keeps reading for the real
reply; pm3py did not.

read_response and _read_any_response now loop past DEBUG_PRINT_* (and malformed) frames
up to a bound, returning the actual command reply — for both NG replies and the OLD-frame
bulk-download stream. This is what makes LF read/download reliable on this device.

Tests: single + multiple interleaved debug frames are skipped to reach the real reply.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
michael
2026-07-14 17:17:33 -07:00
parent f09946c4a3
commit 938a571155
2 changed files with 80 additions and 22 deletions

View File

@@ -151,3 +151,36 @@ def test_download_bulk_drains_stale_input_first():
t.send_frame = tracking_send
_run(t.download_bulk(Cmd.DOWNLOAD_BIGBUF, 0, 100, Cmd.DOWNLOADED_BIGBUF, Cmd.ACK))
assert calls[0] == "drain" and calls[1] == "send" # drained BEFORE the request went out
def _resp_frame(cmd, data=b"", status=0):
# build a no-CRC NG response frame the way the device sends one
length_ng = len(data) | 0x8000
pre = struct.pack("<IHbbH", RESP_PREAMBLE_MAGIC, length_ng, status, 0, cmd)
return pre + data + struct.pack("<H", RESP_POSTAMBLE_NOCRC)
def test_read_response_skips_async_debug_frames():
# a debug-print frame interleaved before the real reply must be skipped, not misread as data
from unittest.mock import AsyncMock as _AM
t = PM3Transport("/dev/null")
t._reader = object() # truthy so read_response proceeds
debug = _resp_frame(int(Cmd.DEBUG_PRINT_STRING), b"LF Sampling config q=95")
reply = _resp_frame(int(Cmd.LF_ACQ_RAW_ADC), struct.pack("<I", 12288))
t._read_response_frame = _AM(side_effect=[debug, reply])
resp = _run(t.read_response())
assert resp.cmd == int(Cmd.LF_ACQ_RAW_ADC)
assert struct.unpack_from("<I", resp.data, 0)[0] == 12288 # the real sample count, not debug bytes
def test_read_response_skips_multiple_debug_then_reply():
from unittest.mock import AsyncMock as _AM
t = PM3Transport("/dev/null")
t._reader = object()
frames = [_resp_frame(int(Cmd.DEBUG_PRINT_STRING), b"a"),
_resp_frame(int(Cmd.DEBUG_PRINT_INTEGERS), struct.pack("<I", 999)),
_resp_frame(int(Cmd.DEBUG_PRINT_BYTES), b"\x01\x02"),
_resp_frame(int(Cmd.PING), b"pong")]
t._read_response_frame = _AM(side_effect=frames)
resp = _run(t.read_response())
assert resp.cmd == int(Cmd.PING) and resp.data == b"pong"