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