Files
pm3py/tests/test_sim_lf.py
michael c1ab980036 refactor: remove all backward-compat shims from sim/
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>
2026-03-18 20:56:40 -07:00

219 lines
7.8 KiB
Python

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