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