"""Tests for UART v2 protocol serialization in switch_pico_uart.""" import struct import pytest from switch_pico_bridge.switch_pico_uart import ( SwitchReport, IMUSample, SwitchDpad, PicoUART, UART_HEADER, UART_PROTOCOL_VERSION, RUMBLE_HEADER, RUMBLE_TYPE_DECODED, ACCEL_LSB_PER_G, GYRO_LSB_PER_RAD_S, MS2_PER_G, compute_checksum, ) class BufferedSerial: def __init__(self, data: bytes = b""): self._data = bytearray(data) @property def in_waiting(self) -> int: return len(self._data) def read(self, size: int) -> bytes: data = bytes(self._data[:size]) del self._data[:size] return data def feed(self, data: bytes) -> None: self._data.extend(data) def make_rumble_frame(low: int, high: int) -> bytes: frame = bytes([RUMBLE_HEADER, RUMBLE_TYPE_DECODED, low, high]) return frame + bytes([compute_checksum(frame)]) def make_uart(data: bytes = b"") -> tuple[PicoUART, BufferedSerial]: uart = object.__new__(PicoUART) serial_port = BufferedSerial(data) uart.serial = serial_port uart._buffer = bytearray() return uart, serial_port def test_v2_frame_with_imu_samples(): """V2 frame with 3 IMU samples should be 48 bytes with correct layout.""" r = SwitchReport( buttons=0, imu_samples=[ IMUSample(100, -200, 4096, 50, -50, 0), IMUSample(101, -201, 4097, 51, -51, 1), IMUSample(102, -202, 4098, 52, -52, 2), ], ) data = r.to_bytes() assert len(data) == 48, f"Expected 48 bytes, got {len(data)}" assert data[0] == UART_HEADER # 0xAA assert data[1] == UART_PROTOCOL_VERSION # 0x02 assert data[2] == 44 # payload_len assert data[10] == 3 # imu_count # Verify checksum assert data[-1] == compute_checksum(data[:-1]) # Verify first sample accel_x (int16 LE at byte 11) ax0 = struct.unpack_from("3 IMU samples should cap at 3.""" samples = [IMUSample(i, 0, 0, 0, 0, 0) for i in range(5)] r = SwitchReport(imu_samples=samples) data = r.to_bytes() assert len(data) == 48 # 3 samples, not 5 assert data[10] == 3 assert data[2] == 44 # payload_len for 3 samples def test_decoded_rumble_frame_survives_fragmented_input(): frame = make_rumble_frame(64, 192) uart, serial_port = make_uart(frame[:3]) assert uart.read_rumble() is None serial_port.feed(frame[3:]) assert uart.read_rumble() == pytest.approx((64 / 255.0, 192 / 255.0)) def test_decoded_rumble_frame_resynchronizes_after_garbage(): uart, _ = make_uart(b"\x00\xffnot-a-frame" + make_rumble_frame(12, 34)) assert uart.read_rumble() == pytest.approx((12 / 255.0, 34 / 255.0)) def test_decoded_rumble_frame_rejects_bad_checksum(): corrupted = bytearray(make_rumble_frame(25, 50)) corrupted[-1] ^= 0x01 uart, _ = make_uart(bytes(corrupted) + make_rumble_frame(75, 100)) assert uart.read_rumble() == pytest.approx((75 / 255.0, 100 / 255.0)) def test_decoded_rumble_zero_and_full_magnitudes(): uart, _ = make_uart(make_rumble_frame(0, 0) + make_rumble_frame(255, 255)) assert uart.read_rumble() == (0.0, 0.0) assert uart.read_rumble() == (1.0, 1.0)