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

285
tests/test_sim_15693.py Normal file
View File

@@ -0,0 +1,285 @@
"""Tests for pm3py.sim.iso15693 — ISO 15693 transponder and reader."""
import asyncio
import pytest
from pm3py.sim.frame import RFFrame
from pm3py.sim.medium import SoftwareMedium
from pm3py.sim.iso15693 import Tag15693, Reader15693, State15693
def run(coro):
return asyncio.get_event_loop().run_until_complete(coro)
# ---------------------------------------------------------------------------
# Tag15693 — basic state machine
# ---------------------------------------------------------------------------
class TestTag15693States:
def test_initial_state_is_ready(self):
tag = Tag15693(uid=bytes(8))
run(tag.power_on())
assert tag.state == "READY"
def test_stay_quiet_transitions_to_quiet(self):
tag = Tag15693(uid=b"\x01" * 8)
run(tag.power_on())
# Stay Quiet (cmd=0x02) with addressed flag and UID
frame_data = bytes([0x22, 0x02]) + tag._uid[::-1] # flags + cmd + UID (LSB first)
resp = run(tag.handle_frame(RFFrame.from_bytes(frame_data)))
assert tag.state == "QUIET"
def test_quiet_tag_ignores_inventory(self):
tag = Tag15693(uid=b"\x01" * 8)
run(tag.power_on())
# Stay Quiet
frame_data = bytes([0x22, 0x02]) + tag._uid[::-1]
run(tag.handle_frame(RFFrame.from_bytes(frame_data)))
assert tag.state == "QUIET"
# Inventory should be ignored
inv = bytes([0x26, 0x01, 0x00]) # flags=0x26, cmd=0x01, mask_len=0
resp = run(tag.handle_frame(RFFrame.from_bytes(inv)))
assert resp is None
def test_reset_to_ready(self):
tag = Tag15693(uid=b"\x01" * 8)
run(tag.power_on())
# Stay Quiet
frame_data = bytes([0x22, 0x02]) + tag._uid[::-1]
run(tag.handle_frame(RFFrame.from_bytes(frame_data)))
assert tag.state == "QUIET"
# Reset to Ready (cmd=0x26)
reset = bytes([0x22, 0x26]) + tag._uid[::-1]
run(tag.handle_frame(RFFrame.from_bytes(reset)))
assert tag.state == "READY"
class TestTag15693Inventory:
def test_1slot_inventory_returns_uid(self):
uid = b"\xE0\x04\x01\x50\x5C\x6A\x1B\x03"
tag = Tag15693(uid=uid, dsfid=0x00)
run(tag.power_on())
# Inventory: flags=0x26 (high data rate + inventory), cmd=0x01, mask_len=0
inv = bytes([0x26, 0x01, 0x00])
resp = run(tag.handle_frame(RFFrame.from_bytes(inv)))
assert resp is not None
# Response: flags(1) + DSFID(1) + UID(8, LSB first)
assert len(resp.data) == 10
assert resp.data[0] == 0x00 # no error
assert resp.data[1] == 0x00 # DSFID
# UID is LSB first in response
assert resp.data[2:10] == uid[::-1]
def test_16slot_inventory_responds_in_correct_slot(self):
"""With 16-slot flag, tag should only respond in its hashed slot."""
uid = b"\xE0\x04\x01\x50\x5C\x6A\x1B\x03"
tag = Tag15693(uid=uid)
run(tag.power_on())
# 16-slot inventory: flags=0x06 (no inventory flag bit 5 clear = 16 slots)
# Actually: flag bit 5 = 0 means 16 slots, bit 5 = 1 means 1 slot
inv = bytes([0x06, 0x01, 0x00]) # flags=0x06, cmd=0x01, mask_len=0
resp = run(tag.handle_frame(RFFrame.from_bytes(inv)))
# Tag responds (slot assignment handled by medium, not tag directly)
assert resp is not None
class TestTag15693ReadWrite:
def test_read_single_block(self):
memory = bytearray(112) # 28 blocks * 4 bytes
memory[0:4] = b"\xDE\xAD\xBE\xEF"
tag = Tag15693(uid=bytes(8), memory=memory, block_size=4, num_blocks=28)
run(tag.power_on())
# Read Single Block (cmd=0x20), unaddressed, block 0
read_cmd = bytes([0x02, 0x20, 0x00]) # flags=0x02, cmd=0x20, block=0
resp = run(tag.handle_frame(RFFrame.from_bytes(read_cmd)))
assert resp is not None
assert resp.data[0] == 0x00 # flags: no error
assert resp.data[1:5] == b"\xDE\xAD\xBE\xEF"
def test_read_single_block_addressed(self):
uid = b"\x01\x02\x03\x04\x05\x06\x07\x08"
memory = bytearray(112)
memory[4:8] = b"\xCA\xFE\xBA\xBE"
tag = Tag15693(uid=uid, memory=memory, block_size=4, num_blocks=28)
run(tag.power_on())
# Addressed read: flags=0x22 (addressed), cmd=0x20, UID(8 LSB), block=1
read_cmd = bytes([0x22, 0x20]) + uid[::-1] + bytes([0x01])
resp = run(tag.handle_frame(RFFrame.from_bytes(read_cmd)))
assert resp is not None
assert resp.data[1:5] == b"\xCA\xFE\xBA\xBE"
def test_read_wrong_uid_no_response(self):
tag = Tag15693(uid=b"\x01" * 8)
run(tag.power_on())
wrong_uid = b"\xFF" * 8
read_cmd = bytes([0x22, 0x20]) + wrong_uid[::-1] + bytes([0x00])
resp = run(tag.handle_frame(RFFrame.from_bytes(read_cmd)))
assert resp is None
def test_write_single_block(self):
tag = Tag15693(uid=bytes(8), block_size=4, num_blocks=28)
run(tag.power_on())
# Write Single Block (cmd=0x21), unaddressed, block 0, data
write_cmd = bytes([0x02, 0x21, 0x00]) + b"\xAA\xBB\xCC\xDD"
resp = run(tag.handle_frame(RFFrame.from_bytes(write_cmd)))
assert resp is not None
assert resp.data[0] == 0x00 # no error
# Verify
read_cmd = bytes([0x02, 0x20, 0x00])
resp = run(tag.handle_frame(RFFrame.from_bytes(read_cmd)))
assert resp.data[1:5] == b"\xAA\xBB\xCC\xDD"
def test_read_out_of_range_block(self):
tag = Tag15693(uid=bytes(8), block_size=4, num_blocks=4)
run(tag.power_on())
read_cmd = bytes([0x02, 0x20, 0x10]) # block 16, out of range
resp = run(tag.handle_frame(RFFrame.from_bytes(read_cmd)))
assert resp is not None
assert resp.data[0] & 0x01 != 0 # error flag set
class TestTag15693SystemInfo:
def test_get_system_info(self):
uid = b"\xE0\x04\x01\x50\x5C\x6A\x1B\x03"
tag = Tag15693(uid=uid, dsfid=0x42, block_size=4, num_blocks=28)
run(tag.power_on())
# Get System Information (cmd=0x2B), unaddressed
sysinfo = bytes([0x02, 0x2B])
resp = run(tag.handle_frame(RFFrame.from_bytes(sysinfo)))
assert resp is not None
assert resp.data[0] == 0x00 # no error
# Info flags(1) + UID(8) + DSFID(1) + AFI(1) + mem_size(2) + IC_ref(1)
assert len(resp.data) >= 14
def test_custom_command_extension_point(self):
"""_handle_custom_command returns error for unrecognized commands."""
tag = Tag15693(uid=bytes(8))
run(tag.power_on())
# Custom command 0xA0 (NXP range)
custom = bytes([0x02, 0xA0, 0x04]) # flags, cmd, mfg code
resp = run(tag.handle_frame(RFFrame.from_bytes(custom)))
assert resp is not None
assert resp.data[0] & 0x01 # error flag set
assert resp.data[1] == 0x01 # not supported
# ---------------------------------------------------------------------------
# Reader15693 — inventory and block operations
# ---------------------------------------------------------------------------
class TestReader15693Inventory:
def test_inventory_single_tag(self):
medium = SoftwareMedium()
uid = b"\xE0\x04\x01\x50\x5C\x6A\x1B\x03"
run(medium.attach(Tag15693(uid=uid, dsfid=0x00)))
reader = Reader15693(medium)
tags = run(reader.inventory())
assert len(tags) == 1
assert tags[0]["uid"] == uid
def test_inventory_two_tags_collision(self):
medium = SoftwareMedium()
uid1 = b"\xE0\x04\x01\x50\x5C\x6A\x1B\x03"
uid2 = b"\xE0\x04\x01\x50\x5C\x6A\x1B\x04"
run(medium.attach(Tag15693(uid=uid1)))
run(medium.attach(Tag15693(uid=uid2)))
reader = Reader15693(medium)
tags = run(reader.inventory())
assert len(tags) == 2
found_uids = {t["uid"] for t in tags}
assert found_uids == {uid1, uid2}
def test_inventory_no_tags(self):
medium = SoftwareMedium()
reader = Reader15693(medium)
tags = run(reader.inventory())
assert len(tags) == 0
class TestReader15693ReadWrite:
def test_read_block(self):
medium = SoftwareMedium()
memory = bytearray(112)
memory[0:4] = b"\xDE\xAD\xBE\xEF"
uid = b"\x01" * 8
run(medium.attach(Tag15693(uid=uid, memory=memory, block_size=4, num_blocks=28)))
reader = Reader15693(medium)
result = run(reader.read_block(uid=uid, block=0))
assert result["success"]
assert result["data"] == b"\xDE\xAD\xBE\xEF"
def test_write_block(self):
medium = SoftwareMedium()
uid = b"\x01" * 8
run(medium.attach(Tag15693(uid=uid, block_size=4, num_blocks=28)))
reader = Reader15693(medium)
result = run(reader.write_block(uid=uid, block=0, data=b"\x11\x22\x33\x44"))
assert result["success"]
result = run(reader.read_block(uid=uid, block=0))
assert result["data"] == b"\x11\x22\x33\x44"
def test_system_info(self):
medium = SoftwareMedium()
uid = b"\xE0\x04\x01\x50\x5C\x6A\x1B\x03"
run(medium.attach(Tag15693(uid=uid, dsfid=0x42, block_size=4, num_blocks=28)))
reader = Reader15693(medium)
result = run(reader.system_info(uid=uid))
assert result["uid"] == uid
assert result["dsfid"] == 0x42
assert result["num_blocks"] == 28
assert result["block_size"] == 4
class TestReader15693DynamicInjection:
"""Test ISO 15693 anti-collision tricks via dynamic attach/detach."""
def test_detach_mid_inventory(self):
medium = SoftwareMedium()
uid1 = b"\x01" * 8
uid2 = b"\x02" * 8
run(medium.attach(Tag15693(uid=uid1)))
tid2 = run(medium.attach(Tag15693(uid=uid2)))
reader = Reader15693(medium)
# Both present
tags = run(reader.inventory())
assert len(tags) == 2
# Remove one
run(medium.detach(tid2))
tags = run(reader.inventory())
assert len(tags) == 1
assert tags[0]["uid"] == uid1
def test_attach_new_tag(self):
medium = SoftwareMedium()
uid1 = b"\x01" * 8
run(medium.attach(Tag15693(uid=uid1)))
reader = Reader15693(medium)
tags = run(reader.inventory())
assert len(tags) == 1
# Add new tag
uid2 = b"\x02" * 8
run(medium.attach(Tag15693(uid=uid2)))
tags = run(reader.inventory())
assert len(tags) == 2