feat: migrate sim framework from worktree to master

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>
This commit is contained in:
michael
2026-03-18 20:04:55 -07:00
parent 7fc2470b01
commit 668170457e
68 changed files with 16951 additions and 1 deletions

View File

@@ -0,0 +1,211 @@
"""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