"""Tests for pm3py.sim LF transponders and readers — TagLF, EM4100, HID, T5577.""" import asyncio import pytest from pm3py.sim.frame import RFFrame from pm3py.sim.medium import SoftwareMedium from pm3py.sim.lf_base import TagLF, ReaderLF, Modulation from pm3py.sim.em4100 import EM4100Tag, EM4100Reader from pm3py.sim.hid import HIDProxTag, HIDReader from pm3py.sim.t5577 import T5577Tag, T5577Reader def run(coro): return asyncio.get_event_loop().run_until_complete(coro) # --------------------------------------------------------------------------- # TagLF — base LF transponder # --------------------------------------------------------------------------- class TestTagLFBase: def test_modulation_types_exist(self): assert Modulation.ASK is not None assert Modulation.FSK is not None assert Modulation.PSK is not None assert Modulation.NRZ is not None def test_taglf_responds_to_field(self): """Any LF tag responds when RF field is present (energized).""" tag = EM4100Tag(tag_id=0x1A2B3C4D5E) run(tag.power_on()) # LF tags continuously transmit when energized # A "read" frame from the reader is just an empty energize command resp = run(tag.handle_frame(RFFrame.from_bytes(b"\x00"))) assert resp is not None # --------------------------------------------------------------------------- # EM4100 — read-only 64-bit Manchester ASK tag # --------------------------------------------------------------------------- class TestEM4100Tag: def test_construction(self): tag = EM4100Tag(tag_id=0x1A2B3C4D5E) assert tag.tag_id == 0x1A2B3C4D5E def test_encodes_64bit_data(self): """EM4100 data: 9-bit header + 10 rows of (4 data + 1 parity) + 4 col parity + stop.""" tag = EM4100Tag(tag_id=0x1A2B3C4D5E) data = tag.encoded_data assert len(data) == 64 # 64 bits total def test_header_is_9_ones(self): tag = EM4100Tag(tag_id=0x0000000000) data = tag.encoded_data assert all(b == 1 for b in data[:9]) def test_row_parity(self): """Each 4-bit row has even parity appended.""" tag = EM4100Tag(tag_id=0x1A2B3C4D5E) data = tag.encoded_data # 10 rows starting at bit 9, each 5 bits (4 data + 1 parity) for row in range(10): start = 9 + row * 5 row_bits = data[start:start + 5] assert sum(row_bits) % 2 == 0, f"Row {row} parity error" def test_column_parity(self): """4 column parity bits after the data rows.""" tag = EM4100Tag(tag_id=0x1A2B3C4D5E) data = tag.encoded_data # Column parity at bits 59-62 for col in range(4): col_sum = 0 for row in range(10): col_sum += data[9 + row * 5 + col] col_sum += data[59 + col] # parity bit itself assert col_sum % 2 == 0, f"Column {col} parity error" def test_stop_bit(self): tag = EM4100Tag(tag_id=0x0000000000) data = tag.encoded_data assert data[63] == 0 # stop bit def test_responds_to_read(self): medium = SoftwareMedium() tag = EM4100Tag(tag_id=0x1A2B3C4D5E) run(medium.attach(tag)) reader = EM4100Reader(medium) result = run(reader.read_id()) assert result is not None assert result["tag_id"] == 0x1A2B3C4D5E def test_decode_roundtrip(self): """Encode then decode should return original ID.""" original_id = 0xDEADBEEF01 tag = EM4100Tag(tag_id=original_id) decoded = EM4100Tag.decode_data(tag.encoded_data) assert decoded == original_id # --------------------------------------------------------------------------- # HID ProxCard — FSK modulated, Wiegand credential # --------------------------------------------------------------------------- class TestHIDProxTag: def test_construction_26bit(self): tag = HIDProxTag(facility_code=42, card_number=12345, format="H10301") assert tag.facility_code == 42 assert tag.card_number == 12345 assert tag.format == "H10301" def test_26bit_wiegand_encoding(self): """H10301: 1 even parity + 8 FC + 16 CN + 1 odd parity = 26 bits.""" tag = HIDProxTag(facility_code=42, card_number=12345, format="H10301") wiegand = tag.wiegand_bits assert len(wiegand) == 26 def test_26bit_parity(self): """First 13 bits: even parity. Last 13 bits: odd parity.""" tag = HIDProxTag(facility_code=42, card_number=12345, format="H10301") wiegand = tag.wiegand_bits assert sum(wiegand[:13]) % 2 == 0 # even parity on first half assert sum(wiegand[13:]) % 2 == 1 # odd parity on second half def test_37bit_format(self): tag = HIDProxTag(facility_code=1000, card_number=50000, format="H10304") wiegand = tag.wiegand_bits assert len(wiegand) == 37 def test_reader_reads_credential(self): medium = SoftwareMedium() tag = HIDProxTag(facility_code=42, card_number=12345) run(medium.attach(tag)) reader = HIDReader(medium) result = run(reader.read_credential()) assert result is not None assert result["facility_code"] == 42 assert result["card_number"] == 12345 class TestHIDProxDecoding: def test_decode_26bit_roundtrip(self): tag = HIDProxTag(facility_code=100, card_number=9999, format="H10301") decoded = HIDProxTag.decode_wiegand(tag.wiegand_bits, "H10301") assert decoded["facility_code"] == 100 assert decoded["card_number"] == 9999 # --------------------------------------------------------------------------- # T5577 — programmable LF emulator # --------------------------------------------------------------------------- class TestT5577Tag: def test_construction(self): tag = T5577Tag() assert len(tag.blocks) == 8 assert all(b == 0 for b in tag.blocks) def test_read_block(self): tag = T5577Tag() tag.blocks[1] = 0xDEADBEEF run(tag.power_on()) resp = run(tag.handle_frame(RFFrame.from_bytes(b"\x01\x01"))) # read block 1 assert resp is not None def test_write_block(self): tag = T5577Tag() run(tag.power_on()) # Write block 1 = 0xCAFEBABE import struct write_data = b"\x02\x01" + struct.pack(">I", 0xCAFEBABE) resp = run(tag.handle_frame(RFFrame.from_bytes(write_data))) assert tag.blocks[1] == 0xCAFEBABE def test_password_protection(self): tag = T5577Tag(password=0x12345678) run(tag.power_on()) # Read without password should fail resp = run(tag.handle_frame(RFFrame.from_bytes(b"\x01\x01"))) assert resp is None # Read with correct password import struct pwd_read = b"\x03\x01" + struct.pack(">I", 0x12345678) resp = run(tag.handle_frame(RFFrame.from_bytes(pwd_read))) assert resp is not None def test_configure_as_em4100(self): """T5577 can be configured to emulate EM4100.""" tag = T5577Tag.preset("em4100", tag_id=0x1A2B3C4D5E) assert tag.blocks[0] != 0 # config block set # Block 0 should have ASK/Manchester config def test_reader_read_block(self): medium = SoftwareMedium() tag = T5577Tag() tag.blocks[1] = 0xDEADBEEF run(medium.attach(tag)) reader = T5577Reader(medium) result = run(reader.read_block(1)) assert result["data"] == 0xDEADBEEF def test_reader_write_block(self): medium = SoftwareMedium() tag = T5577Tag() run(medium.attach(tag)) reader = T5577Reader(medium) run(reader.write_block(1, 0xCAFEBABE)) result = run(reader.read_block(1)) assert result["data"] == 0xCAFEBABE