Migrates all 43 sim modules and 23 test files (~7.8k LOC, 686 tests) from .worktrees/sim-framework/ into pm3py/sim/. Import fixes: - 4 files: ..protocol/..transport → ..core.protocol/..core.transport - trace_fmt.py: pm3py.hf_15 → pm3py.trace.ndef - test_sim_pm3medium.py: flat imports → core.* - test_sim_trace_fmt.py: flat imports → core.* - Added sim Cmd entries to core/protocol.py (EML_SETMEM, SIM_TABLE_*) 751 tests passing (686 sim + 65 core). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
219 lines
7.7 KiB
Python
219 lines
7.7 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.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
|