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:
michael
2026-07-16 12:15:06 -07:00
parent 6ab8582308
commit c675f6b278
2 changed files with 41 additions and 2 deletions

View File

@@ -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