"""Host-side tests for firmware protocol parsers. These tests verify that firmware/protocol.py correctly decodes packets produced by lightsync/protocol/drgb.py and lightsync/protocol/animation_cmd.py. Run with: python -m pytest tests/test_firmware_protocol.py -v """ import sys import os import pytest # Add firmware/ to path so we can import protocol.py sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'firmware')) from protocol import classify_packet, parse_drgb, parse_drgbw, parse_dnrgb, parse_animation_cmd # Import the server-side encoders for round-trip testing from lightsync.protocol.drgb import encode_drgb, encode_drgbw, encode_dnrgb_packets from lightsync.protocol.animation_cmd import encode_animation_cmd class TestClassifyPacket: def test_drgb(self): assert classify_packet(bytes([0x02, 0x00])) == 'drgb' def test_drgbw(self): assert classify_packet(bytes([0x03, 0x00])) == 'drgbw' def test_dnrgb(self): assert classify_packet(bytes([0x04, 0x00])) == 'dnrgb' def test_anim_cmd(self): assert classify_packet(bytes([0xAC, 0x00])) == 'anim_cmd' def test_unknown(self): assert classify_packet(bytes([0xFF])) is None def test_empty(self): assert classify_packet(b'') is None class TestDRGBRoundTrip: def test_simple_pixels(self): pixels = [(255, 0, 0), (0, 255, 0), (0, 0, 255)] packet = encode_drgb(pixels) decoded = parse_drgb(packet) assert decoded == pixels def test_300_leds(self): pixels = [(i % 256, (i * 2) % 256, (i * 3) % 256) for i in range(300)] packet = encode_drgb(pixels) decoded = parse_drgb(packet) assert len(decoded) == 300 assert decoded[0] == pixels[0] assert decoded[299] == pixels[299] class TestDRGBWRoundTrip: def test_simple_pixels(self): pixels = [(255, 0, 0, 128), (0, 255, 0, 64)] packet = encode_drgbw(pixels) decoded = parse_drgbw(packet) assert decoded == pixels def test_300_leds(self): pixels = [(i % 256, (i * 2) % 256, (i * 3) % 256, (i * 4) % 256) for i in range(300)] packet = encode_drgbw(pixels) decoded = parse_drgbw(packet) assert len(decoded) == 300 assert decoded[0] == pixels[0] class TestDNRGBRoundTrip: def test_single_packet(self): pixels = [(255, 0, 0)] * 100 packets = encode_dnrgb_packets(pixels) assert len(packets) == 1 start_idx, decoded = parse_dnrgb(packets[0]) assert start_idx == 0 assert len(decoded) == 100 def test_multi_packet(self): pixels = [(i % 256, 0, 0) for i in range(600)] packets = encode_dnrgb_packets(pixels) assert len(packets) == 2 start0, px0 = parse_dnrgb(packets[0]) start1, px1 = parse_dnrgb(packets[1]) assert start0 == 0 assert start1 == 489 assert len(px0) == 489 assert len(px1) == 111 class TestAnimationCmdRoundTrip: @pytest.mark.parametrize("anim_name", [ 'solid_color', 'chase', 'pulse', 'rainbow', 'strobe', 'color_wipe', 'fire' ]) def test_all_animations(self, anim_name): params = {'color': [255, 128, 0], 'speed': 0.7, 'white': 32, 'bg_color': [10, 20, 30], 'reverse': True, 'density': 42} packet = encode_animation_cmd(anim_name, params) decoded = parse_animation_cmd(packet) assert decoded is not None assert decoded['animation'] == anim_name assert decoded['color'] == (255, 128, 0) assert decoded['bg_color'] == (10, 20, 30) assert decoded['white'] == 32 assert decoded['reverse'] is True assert abs(decoded['speed'] - 0.7) < 0.001 def test_invalid_marker(self): assert parse_animation_cmd(bytes(16)) is None def test_too_short(self): assert parse_animation_cmd(bytes([0xAC, 0x01])) is None