feat(sim): ISO14443-A live sim trace consumer (Y1-Y3 + T1)
Y1: Cmd.HF_ISO14443A_SIM_TRACE (0x0339).
Y2: generalize SimSession._trace_reader dispatch to a {cmd: protocol} map so
14a (proto 1) and 15693 (proto 0) share one reader; the ADC field-strength
branch is now guarded to 15693.
Y3: rewrite start_14a as a synchronous trace path mirroring start_15693 (raw
pyserial + the _trace_reader thread). Sends the correct HF_ISO14443A_SIMULATE
NG payload (same layout as hf.iso14a.sim()), sets FLAG_SIM_TRACE when
streaming, and takes trace/on_frame/quiet/tagtype/exit_after kwargs. Loading
tag memory into emulator RAM stays a separate (hardware) step; the trace
streams every reader<->tag frame regardless. The WTX _relay_loop is retained
for the future 14a-4 path.
T1: mocked-transport unit tests for the 14a trace path (tests/test_sim_trace_14a.py).
Requires the bumped firmware submodule. 996 tests pass.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
117
tests/test_sim_trace_14a.py
Normal file
117
tests/test_sim_trace_14a.py
Normal file
@@ -0,0 +1,117 @@
|
||||
"""ISO 14443-A sim live trace: firmware CMD_HF_ISO14443A_SIM_TRACE -> entries.
|
||||
|
||||
Mirrors the SimSession portion of test_trace_scripting.py but for the 14a sim
|
||||
trace path (protocol tag 1, decode_14443a). Uses synthetic response frames fed
|
||||
through SimSession._trace_reader with a mocked serial port — no hardware.
|
||||
"""
|
||||
import struct
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from pm3py.core.protocol import RESP_PREAMBLE_MAGIC
|
||||
from pm3py.core.transport import RESP_POSTAMBLE_NOCRC
|
||||
from pm3py.sim.sim_session import SimSession, CMD_HF_ISO14443A_SIM_TRACE
|
||||
from pm3py.trace.decode_iso14a import decode_14443a
|
||||
from pm3py.trace.format import TraceFormatter
|
||||
|
||||
REQA = bytes.fromhex("26") # reader -> tag, short frame
|
||||
ATQA = bytes.fromhex("0400") # tag -> reader, no CRC
|
||||
|
||||
|
||||
def _fmt():
|
||||
return TraceFormatter(mode="sim", decoder=decode_14443a, crc_len=2, is_tty=False)
|
||||
|
||||
|
||||
def _trace_frame(direction, payload):
|
||||
"""Build one CMD_HF_ISO14443A_SIM_TRACE response frame (USB, no CRC)."""
|
||||
data = bytes([direction]) + payload
|
||||
header = struct.pack("<IHbbH", RESP_PREAMBLE_MAGIC, len(data) | 0x8000,
|
||||
0, 0, CMD_HF_ISO14443A_SIM_TRACE)
|
||||
return header + data + struct.pack("<H", RESP_POSTAMBLE_NOCRC)
|
||||
|
||||
|
||||
def _run_sim_reader(session, buf):
|
||||
"""Drive _trace_reader once over `buf`, then stop it."""
|
||||
ser = MagicMock()
|
||||
ser.in_waiting = len(buf)
|
||||
n = 0
|
||||
|
||||
def side(_):
|
||||
nonlocal n
|
||||
n += 1
|
||||
if n == 1:
|
||||
return buf
|
||||
session._active = False
|
||||
return b""
|
||||
|
||||
ser.read.side_effect = side
|
||||
session._trace_reader(ser)
|
||||
|
||||
|
||||
def _session():
|
||||
s = SimSession()
|
||||
s._formatter = _fmt()
|
||||
s._active = True
|
||||
return s
|
||||
|
||||
|
||||
def test_14a_reader_frame_decodes():
|
||||
s = _session()
|
||||
frames = []
|
||||
s._on_frame = frames.append
|
||||
s._print_frames = False
|
||||
|
||||
_run_sim_reader(s, _trace_frame(0, REQA))
|
||||
|
||||
assert len(frames) == 1 and len(s.entries) == 1
|
||||
f = frames[0]
|
||||
assert f["protocol"] == 1 # ISO 14443-A
|
||||
assert f["direction"] == 0
|
||||
assert f["data"] == REQA and f["data_hex"] == REQA.hex()
|
||||
assert f["decoded"] == "REQA"
|
||||
assert f["crc_fail"] is False
|
||||
|
||||
|
||||
def test_14a_tag_frame_decodes():
|
||||
s = _session()
|
||||
_run_sim_reader(s, _trace_frame(1, ATQA))
|
||||
|
||||
assert len(s.entries) == 1
|
||||
f = s.entries[0]
|
||||
assert f["protocol"] == 1
|
||||
assert f["direction"] == 1
|
||||
assert f["decoded"] == "ATQA 04 00"
|
||||
|
||||
|
||||
def test_14a_crc_fail_bit():
|
||||
s = _session()
|
||||
# High bit of the dir byte flags a bad-CRC frame (dir 0 | 0x80).
|
||||
_run_sim_reader(s, _trace_frame(0x80, REQA))
|
||||
|
||||
assert len(s.entries) == 1
|
||||
f = s.entries[0]
|
||||
assert f["direction"] == 0
|
||||
assert f["crc_fail"] is True
|
||||
assert "BAD CRC" in f["decoded"]
|
||||
|
||||
|
||||
def test_14a_quiet_suppresses_console(capsys):
|
||||
s = _session()
|
||||
frames = []
|
||||
s._on_frame = frames.append
|
||||
s._print_frames = False # quiet
|
||||
|
||||
_run_sim_reader(s, _trace_frame(0, REQA))
|
||||
|
||||
assert len(frames) == 1
|
||||
assert capsys.readouterr().out == ""
|
||||
|
||||
|
||||
def test_14a_entries_accumulate_and_clear():
|
||||
s = _session()
|
||||
buf = _trace_frame(0, REQA) + _trace_frame(1, ATQA)
|
||||
_run_sim_reader(s, buf)
|
||||
|
||||
assert len(s.entries) == 2
|
||||
assert [e["direction"] for e in s.entries] == [0, 1]
|
||||
s.clear()
|
||||
assert s.entries == []
|
||||
Reference in New Issue
Block a user