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