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,341 @@
"""Tests for ICODE SLIX (SL2S2002) transponder model."""
import asyncio
import struct
import pytest
from pm3py.sim.frame import RFFrame
from pm3py.sim.nxp_icode import (
CMD_SET_EAS, CMD_RESET_EAS, CMD_LOCK_EAS, CMD_EAS_ALARM,
CMD_PASSWORD_PROTECT_EAS_AFI, CMD_WRITE_EAS_ID,
CMD_INVENTORY_READ, CMD_FAST_INVENTORY_READ,
)
def run(coro):
return asyncio.get_event_loop().run_until_complete(coro)
# ---------------------------------------------------------------------------
# Helper: GET_RANDOM + XOR password
# ---------------------------------------------------------------------------
def _xor_password(tag, pwd_value: int) -> bytes:
"""Get random from tag, return XOR'd password bytes."""
resp = run(tag.handle_frame(RFFrame.from_bytes(bytes([0x02, 0xB2, 0x04]))))
r = resp.data[1:3]
pwd = struct.pack("<I", pwd_value)
return bytes([pwd[0] ^ r[0], pwd[1] ^ r[1], pwd[2] ^ r[0], pwd[3] ^ r[1]])
def _authenticate(tag, pwd_id: int, pwd_value: int):
"""GET_RANDOM + SET_PASSWORD for a given password ID."""
xored = _xor_password(tag, pwd_value)
cmd = bytes([0x02, 0xB3, 0x04, pwd_id]) + xored
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp.data[0] == 0x00, f"auth failed for pwd_id {pwd_id:#x}"
PWD_EAS_AFI = 0x10
# ---------------------------------------------------------------------------
# Basic SLIX properties
# ---------------------------------------------------------------------------
class TestIcodeSlixBasics:
def test_import(self):
"""IcodeSlixTag can be imported."""
from pm3py.sim.icode_slix import IcodeSlixTag
def test_inherits_nxp_icode(self):
"""SLIX inherits from NxpIcodeTag."""
from pm3py.sim.icode_slix import IcodeSlixTag
from pm3py.sim.nxp_icode import NxpIcodeTag
assert issubclass(IcodeSlixTag, NxpIcodeTag)
def test_default_28_blocks(self):
"""SLIX defaults to 28 blocks × 4 bytes."""
from pm3py.sim.icode_slix import IcodeSlixTag
tag = IcodeSlixTag(uid=b"\xE0\x04\x01\x10" + bytes(4))
assert tag._num_blocks == 28
assert tag._block_size == 4
def test_responds_to_inventory(self):
from pm3py.sim.icode_slix import IcodeSlixTag
tag = IcodeSlixTag(uid=b"\xE0\x04\x01\x10" + bytes(4))
run(tag.power_on())
inv = bytes([0x26, 0x01, 0x00])
resp = run(tag.handle_frame(RFFrame.from_bytes(inv)))
assert resp is not None
def test_read_write_block(self):
from pm3py.sim.icode_slix import IcodeSlixTag
tag = IcodeSlixTag(uid=b"\xE0\x04\x01\x10" + bytes(4))
run(tag.power_on())
# Write block 5
cmd = bytes([0x02, 0x21, 0x05, 0xAA, 0xBB, 0xCC, 0xDD])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp.data[0] == 0x00
# Read it back
cmd = bytes([0x02, 0x20, 0x05])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp.data[1:5] == bytes([0xAA, 0xBB, 0xCC, 0xDD])
# ---------------------------------------------------------------------------
# Single password (EAS/AFI only)
# ---------------------------------------------------------------------------
class TestIcodeSlixPassword:
def test_set_password_eas_afi(self):
"""SET_PASSWORD with correct EAS/AFI password succeeds."""
from pm3py.sim.icode_slix import IcodeSlixTag
tag = IcodeSlixTag(uid=b"\xE0\x04\x01\x10" + bytes(4),
eas_afi_password=0xAABBCCDD)
run(tag.power_on())
_authenticate(tag, PWD_EAS_AFI, 0xAABBCCDD)
def test_set_password_wrong(self):
"""SET_PASSWORD with wrong password fails."""
from pm3py.sim.icode_slix import IcodeSlixTag
tag = IcodeSlixTag(uid=b"\xE0\x04\x01\x10" + bytes(4),
eas_afi_password=0xAABBCCDD)
run(tag.power_on())
xored = _xor_password(tag, 0x00000000)
cmd = bytes([0x02, 0xB3, 0x04, PWD_EAS_AFI]) + xored
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp.data[0] & 0x01
def test_write_password(self):
"""WRITE_PASSWORD changes the EAS/AFI password."""
from pm3py.sim.icode_slix import IcodeSlixTag
tag = IcodeSlixTag(uid=b"\xE0\x04\x01\x10" + bytes(4),
eas_afi_password=0xAABBCCDD)
run(tag.power_on())
_authenticate(tag, PWD_EAS_AFI, 0xAABBCCDD)
new_xored = _xor_password(tag, 0x11223344)
cmd = bytes([0x02, 0xB4, 0x04, PWD_EAS_AFI]) + new_xored
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp.data[0] == 0x00
# New password works
_authenticate(tag, PWD_EAS_AFI, 0x11223344)
def test_lock_password(self):
"""LOCK_PASSWORD prevents WRITE_PASSWORD."""
from pm3py.sim.icode_slix import IcodeSlixTag
tag = IcodeSlixTag(uid=b"\xE0\x04\x01\x10" + bytes(4),
eas_afi_password=0xAABBCCDD)
run(tag.power_on())
_authenticate(tag, PWD_EAS_AFI, 0xAABBCCDD)
cmd = bytes([0x02, 0xB5, 0x04, PWD_EAS_AFI])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp.data[0] == 0x00
# WRITE_PASSWORD now fails
_authenticate(tag, PWD_EAS_AFI, 0xAABBCCDD)
new_xored = _xor_password(tag, 0x11223344)
cmd = bytes([0x02, 0xB4, 0x04, PWD_EAS_AFI]) + new_xored
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp.data[0] & 0x01
def test_no_privacy_password(self):
"""SLIX has no privacy — only EAS/AFI password accepted."""
from pm3py.sim.icode_slix import IcodeSlixTag
tag = IcodeSlixTag(uid=b"\xE0\x04\x01\x10" + bytes(4),
eas_afi_password=0xAABBCCDD)
run(tag.power_on())
# Try to auth with privacy pwd_id — should fail
xored = _xor_password(tag, 0xAABBCCDD)
cmd = bytes([0x02, 0xB3, 0x04, 0x04]) + xored # pwd_id=0x04 (privacy)
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp.data[0] & 0x01
# ---------------------------------------------------------------------------
# EAS subsystem
# ---------------------------------------------------------------------------
class TestIcodeSlixEas:
def test_set_eas(self):
from pm3py.sim.icode_slix import IcodeSlixTag
tag = IcodeSlixTag(uid=b"\xE0\x04\x01\x10" + bytes(4))
run(tag.power_on())
cmd = bytes([0x02, CMD_SET_EAS, 0x04])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp.data[0] == 0x00
assert tag.eas_enabled
def test_reset_eas(self):
from pm3py.sim.icode_slix import IcodeSlixTag
tag = IcodeSlixTag(uid=b"\xE0\x04\x01\x10" + bytes(4))
run(tag.power_on())
tag.set_eas(True)
cmd = bytes([0x02, CMD_RESET_EAS, 0x04])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp.data[0] == 0x00
assert not tag.eas_enabled
def test_eas_alarm(self):
from pm3py.sim.icode_slix import IcodeSlixTag
tag = IcodeSlixTag(uid=b"\xE0\x04\x01\x10" + bytes(4))
run(tag.power_on())
tag.set_eas(True)
cmd = bytes([0x02, CMD_EAS_ALARM, 0x04])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp is not None
assert resp.data[0] == 0x00
assert len(resp.data) >= 33
def test_eas_alarm_silent_when_disabled(self):
from pm3py.sim.icode_slix import IcodeSlixTag
tag = IcodeSlixTag(uid=b"\xE0\x04\x01\x10" + bytes(4))
run(tag.power_on())
cmd = bytes([0x02, CMD_EAS_ALARM, 0x04])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp is None
def test_lock_eas(self):
from pm3py.sim.icode_slix import IcodeSlixTag
tag = IcodeSlixTag(uid=b"\xE0\x04\x01\x10" + bytes(4))
run(tag.power_on())
tag.set_eas(True)
cmd = bytes([0x02, CMD_LOCK_EAS, 0x04])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp.data[0] == 0x00
# Can't change now
cmd = bytes([0x02, CMD_RESET_EAS, 0x04])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp.data[0] & 0x01
def test_password_protect_eas_afi(self):
from pm3py.sim.icode_slix import IcodeSlixTag
tag = IcodeSlixTag(uid=b"\xE0\x04\x01\x10" + bytes(4),
eas_afi_password=0xAABBCCDD)
run(tag.power_on())
_authenticate(tag, PWD_EAS_AFI, 0xAABBCCDD)
cmd = bytes([0x02, CMD_PASSWORD_PROTECT_EAS_AFI, 0x04])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp.data[0] == 0x00
assert tag._eas_password_protected
def test_write_eas_id(self):
"""WRITE EAS ID is SLIX2-only (EAS Selective per AN11809)."""
from pm3py.sim.icode_slix2 import IcodeSlix2Tag
tag = IcodeSlix2Tag(uid=b"\xE0\x04\x02" + bytes(5),
eas_afi_password=0xAABBCCDD)
run(tag.power_on())
_authenticate(tag, PWD_EAS_AFI, 0xAABBCCDD)
cmd = bytes([0x02, CMD_WRITE_EAS_ID, 0x04, 0x34, 0x12])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp.data[0] == 0x00
assert tag._eas_id == 0x1234
def test_slix_rejects_write_eas_id(self):
"""SLIX (SL2S2002) does not support WRITE EAS ID (0xA7)."""
from pm3py.sim.icode_slix import IcodeSlixTag
tag = IcodeSlixTag(uid=b"\xE0\x04\x01\x10" + bytes(4))
run(tag.power_on())
cmd = bytes([0x02, CMD_WRITE_EAS_ID, 0x04, 0x34, 0x12])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp is None
# ---------------------------------------------------------------------------
# INVENTORY READ
# ---------------------------------------------------------------------------
class TestIcodeSlixInventoryRead:
def test_inventory_read(self):
from pm3py.sim.icode_slix import IcodeSlixTag
tag = IcodeSlixTag(uid=b"\xE0\x04\x01\x10\x03\x04\x05\x06")
run(tag.power_on())
cmd = bytes([0x06, CMD_INVENTORY_READ, 0x04, 0x00, 0x00, 0x00])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp is not None
assert resp.data[0] == 0x00
def test_fast_inventory_read(self):
from pm3py.sim.icode_slix import IcodeSlixTag
tag = IcodeSlixTag(uid=b"\xE0\x04\x01\x10\x03\x04\x05\x06")
run(tag.power_on())
cmd = bytes([0x06, CMD_FAST_INVENTORY_READ, 0x04, 0x00, 0x00, 0x00])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp is not None
assert resp.data[0] == 0x00
# ---------------------------------------------------------------------------
# SLIX2 still works after refactoring
# ---------------------------------------------------------------------------
class TestSlix2StillWorks:
"""Verify SLIX2 inherits from SLIX and retains all features."""
def test_slix2_inherits_slix(self):
from pm3py.sim.icode_slix import IcodeSlixTag
from pm3py.sim.icode_slix2 import IcodeSlix2Tag
assert issubclass(IcodeSlix2Tag, IcodeSlixTag)
def test_slix2_privacy_still_works(self):
from pm3py.sim.icode_slix2 import IcodeSlix2Tag
tag = IcodeSlix2Tag(uid=b"\xE0\x04\x02" + bytes(5),
privacy_password=0xDEADBEEF)
run(tag.power_on())
tag.enter_privacy_mode()
assert tag.privacy_mode
def test_slix2_destroy_still_works(self):
from pm3py.sim.icode_slix2 import IcodeSlix2Tag
tag = IcodeSlix2Tag(uid=b"\xE0\x04\x02" + bytes(5),
destroy_password=0xDEAD1234)
run(tag.power_on())
xored = _xor_password(tag, 0xDEAD1234)
cmd = bytes([0x02, 0xB9, 0x04]) + xored
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp.data[0] == 0x00
# Dead
inv = bytes([0x26, 0x01, 0x00])
assert run(tag.handle_frame(RFFrame.from_bytes(inv))) is None
def test_slix2_eas_inherited_from_slix(self):
"""SLIX2 EAS works via inheritance from SLIX."""
from pm3py.sim.icode_slix2 import IcodeSlix2Tag
tag = IcodeSlix2Tag(uid=b"\xE0\x04\x02" + bytes(5))
run(tag.power_on())
cmd = bytes([0x02, CMD_SET_EAS, 0x04])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp.data[0] == 0x00
assert tag.eas_enabled
# ---------------------------------------------------------------------------
# SLIX should NOT have SLIX2-only features
# ---------------------------------------------------------------------------
class TestSlixLacksPrivacy:
def test_no_enable_privacy(self):
"""SLIX does not respond to ENABLE PRIVACY (0xBA)."""
from pm3py.sim.icode_slix import IcodeSlixTag
tag = IcodeSlixTag(uid=b"\xE0\x04\x01\x10" + bytes(4))
run(tag.power_on())
cmd = bytes([0x02, 0xBA, 0x04, 0x00, 0x00, 0x00, 0x00])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
# Should be None (unhandled) or error
assert resp is None or resp.data[0] & 0x01
def test_no_destroy(self):
"""SLIX does not respond to DESTROY (0xB9)."""
from pm3py.sim.icode_slix import IcodeSlixTag
tag = IcodeSlixTag(uid=b"\xE0\x04\x01\x10" + bytes(4))
run(tag.power_on())
cmd = bytes([0x02, 0xB9, 0x04, 0x00, 0x00, 0x00, 0x00])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp is None or resp.data[0] & 0x01
def test_no_protect_page(self):
"""SLIX does not respond to PROTECT PAGE (0xB6)."""
from pm3py.sim.icode_slix import IcodeSlixTag
tag = IcodeSlixTag(uid=b"\xE0\x04\x01\x10" + bytes(4))
run(tag.power_on())
cmd = bytes([0x02, 0xB6, 0x04, 0x08, 0x01])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp is None or resp.data[0] & 0x01