feat: migrate sim framework from worktree to master

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>
This commit is contained in:
michael
2026-03-18 20:04:55 -07:00
parent 7fc2470b01
commit 668170457e
68 changed files with 16951 additions and 1 deletions

218
tests/test_sim_lf.py Normal file
View File

@@ -0,0 +1,218 @@
"""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