Migrates all 43 sim modules and 23 test files (~7.8k LOC, 686 tests) from .worktrees/sim-framework/ into pm3py/sim/. Import fixes: - 4 files: ..protocol/..transport → ..core.protocol/..core.transport - trace_fmt.py: pm3py.hf_15 → pm3py.trace.ndef - test_sim_pm3medium.py: flat imports → core.* - test_sim_trace_fmt.py: flat imports → core.* - Added sim Cmd entries to core/protocol.py (EML_SETMEM, SIM_TABLE_*) 751 tests passing (686 sim + 65 core). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
212 lines
6.5 KiB
Python
212 lines
6.5 KiB
Python
"""Tests for DualInterfaceSession coordinator."""
|
|
import time
|
|
import pytest
|
|
from pm3py.sim.dual_session import DualInterfaceSession
|
|
from pm3py.sim.mcu_bridge import McuBridge
|
|
from pm3py.sim.mcu_protocol import MsgType, build_frame, cobs_decode, parse_frame
|
|
from pm3py.sim.ntag5_link import Ntag5LinkTag
|
|
|
|
|
|
class MockSimSession:
|
|
"""Mock SimSession for testing — no real PM3 hardware."""
|
|
def __init__(self):
|
|
self.on_field_strength = None
|
|
self._started = False
|
|
self._stopped = False
|
|
self._tag = None
|
|
self._trace = False
|
|
|
|
def start_15693(self, tag, trace=False):
|
|
self._started = True
|
|
self._tag = tag
|
|
self._trace = trace
|
|
|
|
def stop(self):
|
|
self._stopped = True
|
|
|
|
|
|
class MockSerial:
|
|
"""Mock serial port for McuBridge."""
|
|
def __init__(self):
|
|
self._rx_buf = bytearray()
|
|
self._tx_buf = bytearray()
|
|
self.is_open = True
|
|
self.timeout = 0.1
|
|
|
|
def read(self, size=1):
|
|
if not self._rx_buf:
|
|
time.sleep(0.01)
|
|
return b""
|
|
data = bytes(self._rx_buf[:size])
|
|
self._rx_buf = self._rx_buf[size:]
|
|
return data
|
|
|
|
def write(self, data):
|
|
self._tx_buf.extend(data)
|
|
return len(data)
|
|
|
|
def inject(self, data: bytes):
|
|
self._rx_buf.extend(data)
|
|
|
|
def get_sent(self) -> bytes:
|
|
data = bytes(self._tx_buf)
|
|
self._tx_buf.clear()
|
|
return data
|
|
|
|
def close(self):
|
|
self.is_open = False
|
|
|
|
|
|
def _decode_sent(mock_serial) -> list[tuple[int, bytes]]:
|
|
"""Decode all COBS-framed messages sent to mock serial."""
|
|
raw = mock_serial.get_sent()
|
|
messages = []
|
|
while raw:
|
|
zero_idx = raw.find(b"\x00")
|
|
if zero_idx == -1:
|
|
break
|
|
frame = raw[:zero_idx]
|
|
raw = raw[zero_idx + 1:]
|
|
if frame:
|
|
decoded = cobs_decode(frame)
|
|
messages.append(parse_frame(decoded))
|
|
return messages
|
|
|
|
|
|
class TestDualInterfaceSession:
|
|
def _make_session(self):
|
|
"""Create a DualInterfaceSession with mocks."""
|
|
mock_serial = MockSerial()
|
|
sim = MockSimSession()
|
|
mcu = McuBridge(port=mock_serial)
|
|
tag = Ntag5LinkTag()
|
|
tag.sram_enable = True
|
|
tag.arbiter_mode = 2 # PASSTHROUGH
|
|
session = DualInterfaceSession(sim, mcu, tag)
|
|
return session, sim, mcu, mock_serial, tag
|
|
|
|
def test_start_wires_callbacks(self):
|
|
session, sim, mcu, mock_serial, tag = self._make_session()
|
|
session.start()
|
|
time.sleep(0.05)
|
|
|
|
assert sim._started
|
|
assert mcu.on_i2c_write is not None
|
|
assert mcu.on_i2c_read_req is not None
|
|
assert tag.ed_pin_callback is not None
|
|
assert sim.on_field_strength is not None
|
|
|
|
session.stop()
|
|
|
|
def test_start_pushes_initial_sram(self):
|
|
session, sim, mcu, mock_serial, tag = self._make_session()
|
|
tag._sram[0:4] = b"\xDE\xAD\xBE\xEF"
|
|
session.start()
|
|
time.sleep(0.05)
|
|
session.stop()
|
|
|
|
msgs = _decode_sent(mock_serial)
|
|
# Should have WRITE_SRAM with full 256-byte SRAM
|
|
sram_msgs = [m for m in msgs if m[0] == MsgType.WRITE_SRAM]
|
|
assert len(sram_msgs) == 1
|
|
assert sram_msgs[0][1][0] == 0x00 # offset 0
|
|
assert sram_msgs[0][1][1:5] == b"\xDE\xAD\xBE\xEF"
|
|
|
|
def test_i2c_write_updates_tag_sram(self):
|
|
session, sim, mcu, mock_serial, tag = self._make_session()
|
|
tag.config_pt_transfer_dir = False # I2C→NFC
|
|
session.start()
|
|
time.sleep(0.05)
|
|
|
|
# Simulate MCU pushing an I2C write event
|
|
mock_serial.inject(build_frame(MsgType.I2C_WRITE,
|
|
bytes([0x20, 0x00, 0xCA, 0xFE])))
|
|
time.sleep(0.15)
|
|
session.stop()
|
|
|
|
# Tag SRAM should have the data
|
|
assert tag._sram[0:2] == b"\xCA\xFE"
|
|
|
|
def test_i2c_read_req_responds(self):
|
|
session, sim, mcu, mock_serial, tag = self._make_session()
|
|
# Preload SRAM
|
|
tag._sram[0:4] = b"\x01\x02\x03\x04"
|
|
session.start()
|
|
time.sleep(0.05)
|
|
|
|
# Clear initial WRITE_SRAM from get_sent
|
|
mock_serial.get_sent()
|
|
|
|
# Simulate MCU requesting I2C read
|
|
mock_serial.inject(build_frame(MsgType.I2C_READ_REQ,
|
|
bytes([0x20, 0x00, 0x04])))
|
|
time.sleep(0.15)
|
|
session.stop()
|
|
|
|
msgs = _decode_sent(mock_serial)
|
|
resp_msgs = [m for m in msgs if m[0] == MsgType.I2C_READ_RESPONSE]
|
|
assert len(resp_msgs) == 1
|
|
assert resp_msgs[0][1] == b"\x01\x02\x03\x04"
|
|
|
|
def test_ed_pin_relayed_to_mcu(self):
|
|
session, sim, mcu, mock_serial, tag = self._make_session()
|
|
session.start()
|
|
time.sleep(0.05)
|
|
mock_serial.get_sent() # clear initial WRITE_SRAM
|
|
|
|
# Trigger ED on tag
|
|
tag.assert_ed()
|
|
time.sleep(0.05)
|
|
session.stop()
|
|
|
|
msgs = _decode_sent(mock_serial)
|
|
ed_msgs = [m for m in msgs if m[0] == MsgType.SET_ED]
|
|
assert len(ed_msgs) == 1
|
|
assert ed_msgs[0][1] == bytes([0x01])
|
|
|
|
def test_field_strength_above_threshold_activates_eh(self):
|
|
session, sim, mcu, mock_serial, tag = self._make_session()
|
|
session.eh_threshold = 500
|
|
session.start()
|
|
time.sleep(0.05)
|
|
mock_serial.get_sent() # clear initial
|
|
|
|
# Simulate field strength above threshold
|
|
sim.on_field_strength(600)
|
|
time.sleep(0.05)
|
|
session.stop()
|
|
|
|
msgs = _decode_sent(mock_serial)
|
|
eh_msgs = [m for m in msgs if m[0] == MsgType.SET_EH]
|
|
assert len(eh_msgs) == 1
|
|
assert eh_msgs[0][1] == bytes([0x01])
|
|
assert tag.nfc_field_ok
|
|
|
|
def test_field_strength_below_threshold_deactivates_eh(self):
|
|
session, sim, mcu, mock_serial, tag = self._make_session()
|
|
session.eh_threshold = 500
|
|
session._eh_active = True # pretend already active
|
|
session.start()
|
|
time.sleep(0.05)
|
|
mock_serial.get_sent()
|
|
|
|
sim.on_field_strength(300)
|
|
time.sleep(0.05)
|
|
session.stop()
|
|
|
|
msgs = _decode_sent(mock_serial)
|
|
eh_msgs = [m for m in msgs if m[0] == MsgType.SET_EH]
|
|
assert len(eh_msgs) == 1
|
|
assert eh_msgs[0][1] == bytes([0x00])
|
|
|
|
def test_stop_unhooks_callbacks(self):
|
|
session, sim, mcu, mock_serial, tag = self._make_session()
|
|
session.start()
|
|
time.sleep(0.05)
|
|
session.stop()
|
|
|
|
assert mcu.on_i2c_write is None
|
|
assert mcu.on_i2c_read_req is None
|
|
assert tag.ed_pin_callback is None
|
|
assert sim.on_field_strength is None
|