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:
285
tests/test_sim_15693.py
Normal file
285
tests/test_sim_15693.py
Normal 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
|
||||
Reference in New Issue
Block a user