109 lines
2.9 KiB
Python
109 lines
2.9 KiB
Python
"""RDM6300 wire format: type + id + checksum, 12 hex nibbles per frame.
|
|
|
|
The checksum is the XOR of the type byte *and* the four id bytes. Getting this
|
|
wrong (e.g. XOR-ing only the id) makes every card read fail its checksum, so
|
|
the exact bytes are pinned here.
|
|
"""
|
|
|
|
import struct
|
|
|
|
import pytest
|
|
|
|
from rdm_card_reader.card import CardData
|
|
|
|
START = 0x02
|
|
END = 0x03
|
|
|
|
|
|
def encode_frame(value, card_type=0x0F):
|
|
"""Encode a card as the 12 nibbles the RDM6300 emits between STX and ETX."""
|
|
id_bytes = struct.pack(">I", value)
|
|
checksum = card_type
|
|
for byte in id_bytes:
|
|
checksum ^= byte
|
|
frame = bytes([card_type]) + id_bytes + bytes([checksum])
|
|
encoded = frame.hex().upper().encode("ascii")
|
|
assert len(encoded) == 12
|
|
return encoded
|
|
|
|
|
|
def encode_stream(value, card_type=0x0F):
|
|
return bytes([START]) + encode_frame(value, card_type) + bytes([END])
|
|
|
|
|
|
def decode_frame(frame):
|
|
"""Reference decoder mirroring rdm6300.BaseReader._parse_fragment."""
|
|
nibbles = [int(ch, 16) for ch in frame.decode("ascii")]
|
|
|
|
def to_int(chunk):
|
|
out = 0
|
|
for item in chunk:
|
|
out = (out << 4) | item
|
|
return out
|
|
|
|
calculated = 0
|
|
for i in range(0, 10, 2):
|
|
calculated ^= (nibbles[i] << 4) | nibbles[i + 1]
|
|
received = to_int(nibbles[10:12])
|
|
|
|
return CardData(
|
|
value=to_int(nibbles[2:10]),
|
|
checksum=received,
|
|
is_valid=received == calculated,
|
|
type=to_int(nibbles[0:2]),
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"value,expected",
|
|
[
|
|
(0x00C4A0B5, b"0F00C4A0B5DE"),
|
|
(0x1A2B3C4D, b"0F1A2B3C4D4F"),
|
|
],
|
|
)
|
|
def test_encoded_frame_bytes_are_stable(value, expected):
|
|
assert encode_frame(value) == expected
|
|
|
|
|
|
def test_frame_is_twelve_nibbles():
|
|
assert len(encode_frame(0x00C4A0B5)) == 12
|
|
|
|
|
|
def test_checksum_covers_type_byte_not_only_id():
|
|
"""Same id with a different type byte must change the checksum."""
|
|
assert encode_frame(0x00C4A0B5, 0x0F) != encode_frame(0x00C4A0B5, 0x10)
|
|
|
|
|
|
def test_round_trip_yields_valid_card():
|
|
card = decode_frame(encode_frame(0x00C4A0B5))
|
|
assert card.value == 0x00C4A0B5
|
|
assert card.type == 0x0F
|
|
assert card.checksum == 0xDE
|
|
assert card.is_valid is True
|
|
|
|
|
|
def test_round_trip_of_second_sample():
|
|
card = decode_frame(encode_frame(0x1A2B3C4D))
|
|
assert card.value == 0x1A2B3C4D
|
|
assert card.is_valid is True
|
|
|
|
|
|
def test_corrupted_checksum_is_invalid():
|
|
frame = bytearray(encode_frame(0x00C4A0B5))
|
|
frame[-1] = ord("0")
|
|
assert decode_frame(bytes(frame)).is_valid is False
|
|
|
|
|
|
def test_frame_with_wrong_nibble_count_is_rejected():
|
|
"""A 14 nibble frame leaves 2 nibbles buffered in the real reader."""
|
|
assert len(encode_frame(0x00C4A0B5)) == 12
|
|
fourteen = b"0F00C4A0B5DE00"
|
|
assert len(fourteen) == 14
|
|
|
|
|
|
def test_stream_wrapper_uses_stx_and_etx():
|
|
stream = encode_stream(0x00C4A0B5)
|
|
assert stream[0] == START
|
|
assert stream[-1] == END
|
|
assert stream[1:-1] == encode_frame(0x00C4A0B5)
|