"""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.transponders.lf.base import TagLF, ReaderLF, Modulation from pm3py.transponders.lf.em.em4100 import EM4100Tag, EM4100Reader from pm3py.transponders.lf.hid.hid import HIDProxTag, HIDReader from pm3py.transponders.lf.atmel.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