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

View File

@@ -0,0 +1,160 @@
"""Tests for access control I/O: credentials, Wiegand, OSDP."""
import asyncio
import pytest
from pm3py.sim.access_control.credential import (
Credential, encode_wiegand, decode_wiegand, from_uid,
)
from pm3py.sim.access_control.wiegand import (
WiegandOutput, WiegandInput,
)
from pm3py.sim.access_control.osdp import (
OSDPFrame, OSDPChannel, osdp_crc16,
)
def run(coro):
return asyncio.get_event_loop().run_until_complete(coro)
# ---------------------------------------------------------------------------
# Credential model
# ---------------------------------------------------------------------------
class TestCredential:
def test_construction(self):
c = Credential(facility_code=42, card_number=12345, format="H10301")
assert c.facility_code == 42
assert c.card_number == 12345
assert c.format == "H10301"
def test_equality(self):
c1 = Credential(42, 12345, "H10301")
c2 = Credential(42, 12345, "H10301")
assert c1 == c2
def test_from_uid_derives_credential(self):
"""Derive a credential from a tag UID."""
cred = from_uid(b"\x01\x02\x03\x04", format="H10301")
assert cred.facility_code >= 0
assert cred.card_number >= 0
assert cred.format == "H10301"
class TestWiegandEncoding:
def test_encode_26bit(self):
bits = encode_wiegand(Credential(42, 12345, "H10301"))
assert len(bits) == 26
def test_decode_26bit(self):
cred = Credential(42, 12345, "H10301")
bits = encode_wiegand(cred)
decoded = decode_wiegand(bits, "H10301")
assert decoded.facility_code == 42
assert decoded.card_number == 12345
def test_encode_37bit(self):
bits = encode_wiegand(Credential(1000, 50000, "H10304"))
assert len(bits) == 37
def test_decode_37bit_roundtrip(self):
cred = Credential(1000, 50000, "H10304")
bits = encode_wiegand(cred)
decoded = decode_wiegand(bits, "H10304")
assert decoded == cred
def test_26bit_parity(self):
bits = encode_wiegand(Credential(42, 12345, "H10301"))
assert sum(bits[:13]) % 2 == 0 # even parity first half
assert sum(bits[13:]) % 2 == 1 # odd parity second half
def test_encode_35bit_corp1000(self):
bits = encode_wiegand(Credential(100, 5000, "C1000_35"))
assert len(bits) == 35
# ---------------------------------------------------------------------------
# Wiegand I/O (software mode)
# ---------------------------------------------------------------------------
class TestWiegandOutput:
def test_software_mode_encode(self):
"""WiegandOutput in software mode stores bit stream."""
out = WiegandOutput()
cred = Credential(42, 12345, "H10301")
out.send(cred)
assert len(out.last_bits) == 26
def test_software_mode_timing(self):
"""Verify pulse timing parameters exist."""
out = WiegandOutput(pulse_width_us=50, interval_us=2000)
assert out.pulse_width_us == 50
assert out.interval_us == 2000
class TestWiegandInput:
def test_software_mode_decode(self):
"""WiegandInput in software mode decodes bit stream."""
inp = WiegandInput()
cred = Credential(42, 12345, "H10301")
bits = encode_wiegand(cred)
decoded = inp.decode(bits, "H10301")
assert decoded.facility_code == 42
assert decoded.card_number == 12345
# ---------------------------------------------------------------------------
# OSDP protocol
# ---------------------------------------------------------------------------
class TestOSDPFrame:
def test_frame_construction(self):
frame = OSDPFrame(address=0, command=0x60, data=b"")
raw = frame.encode()
assert raw[0] == 0x53 # SOM
assert len(raw) >= 7 # SOM + ADDR + LEN(2) + CMD + CRC(2)
def test_frame_parse_roundtrip(self):
frame = OSDPFrame(address=1, command=0x61, data=b"\x01\x02")
raw = frame.encode()
parsed = OSDPFrame.decode(raw)
assert parsed.address == 1
assert parsed.command == 0x61
assert parsed.data == b"\x01\x02"
def test_crc16(self):
"""OSDP uses CRC-16/AUG-CCITT."""
crc = osdp_crc16(b"\x53\x00\x08\x00\x60")
assert isinstance(crc, int)
assert 0 <= crc <= 0xFFFF
class TestOSDPChannel:
def test_software_loopback(self):
"""Channel in software mode: controller polls, reader replies."""
channel = OSDPChannel() # software loopback
# Controller sends POLL
poll = OSDPFrame(address=0, command=0x60, data=b"")
channel.send(poll)
# Reader responds with ACK
ack = OSDPFrame(address=0, command=0x40, data=b"")
channel.send(ack)
# Both frames in buffer
assert len(channel.buffer) == 2
def test_card_present_notification(self):
"""Simulate card present event via OSDP."""
channel = OSDPChannel()
cred = Credential(42, 12345, "H10301")
bits = encode_wiegand(cred)
# Reader sends RAW (card data) — cmd 0x73
raw_data = bytes(bits) # simplified
frame = OSDPFrame(address=0, command=0x73, data=raw_data)
channel.send(frame)
last = channel.buffer[-1]
assert last.command == 0x73