Deletes 24 shim files from sim/. All test imports now point directly to canonical transponders/ and trace/ paths. sim/ contains only infrastructure (15 files): frame, memory, transponder ABC, reader ABC, medium, sim_session, table_compiler, replay, relay, fuzzer, pm3medium, mcu_bridge, mcu_protocol, dual_session, __init__. 751 tests passing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
286 lines
10 KiB
Python
286 lines
10 KiB
Python
"""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.transponders.hf.iso15693.base 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
|