fix(transport): handle CMD_WTX wait-extension frames, don't misread them as replies
The firmware sends an async CMD_WTX ("wait N ms") control frame on FPGA bitstream
reload (fpgaloader.c: send_wtx(FPGA_LOAD_WAIT_TIME=1500)). The first HF command after
connect triggers the HF-bitstream load, so a WTX frame lands before the real reply.
Our read loops skipped only DEBUG_PRINT_* frames, so they returned the WTX frame as
the command reply -- leaving the real reply pending and permanently desyncing every
later read (each command then got the *previous* command's response).
This was the root cause of the intermittent "15693 garbage/shift" reads. ping (no
bitstream load) and well-coupled 14a were unaffected, which is why it looked
RF/coupling-specific -- it never was, and it is not a firmware bug: the firmware
behaves correctly and the C client already handles WTX (comms.c). pm3py just was not
a spec-compliant client.
Mirror the C client: on CMD_WTX, add the wtx value to the wait and keep reading.
Applied to both read_response and _read_any_response.
Verified on hardware: an ICODE SLIX GET_SYSTEM_INFO/READ stress went from 100% anomaly
(permanent shift) to 0% (187/187 correct). Adds a regression test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -186,6 +186,21 @@ def test_read_response_skips_multiple_debug_then_reply():
|
||||
assert resp.cmd == int(Cmd.PING) and resp.data == b"pong"
|
||||
|
||||
|
||||
def test_read_response_skips_wtx_frame_not_returns_it():
|
||||
# a CMD_WTX "wait N ms" control frame (firmware sends it on FPGA bitstream reload) must be
|
||||
# skipped like a debug frame — returning it as the reply is what permanently desynced every
|
||||
# subsequent 15693 read (each command then grabbed the previous command's response)
|
||||
from unittest.mock import AsyncMock as _AM
|
||||
t = PM3Transport("/dev/null")
|
||||
t._reader = object()
|
||||
wtx = _resp_frame(int(Cmd.WTX), struct.pack("<H", 1500)) # FPGA_LOAD_WAIT_TIME
|
||||
reply = _resp_frame(int(Cmd.HF_ISO15693_COMMAND), b"\x00\x0f\x6b\xc0")
|
||||
t._read_response_frame = _AM(side_effect=[wtx, reply])
|
||||
resp = _run(t.read_response())
|
||||
assert resp.cmd == int(Cmd.HF_ISO15693_COMMAND)
|
||||
assert resp.data == b"\x00\x0f\x6b\xc0" # the real tag reply, not the WTX frame
|
||||
|
||||
|
||||
def test_read_timeout_sets_resync_next_send_drains():
|
||||
# a read timeout flags a resync; the next send drains the late response before writing
|
||||
import asyncio as _a
|
||||
|
||||
Reference in New Issue
Block a user