diff --git a/tests/test_firmware_frames.py b/tests/test_firmware_frames.py new file mode 100644 index 0000000..b9a1ad8 --- /dev/null +++ b/tests/test_firmware_frames.py @@ -0,0 +1,204 @@ +"""Host-side tests for firmware frame mode. + +Tests DRGB/DRGBW/DNRGB parsing and DNRGB multi-packet reassembly. +Run with: python -m pytest tests/test_firmware_frames.py -v +""" +import sys +import os +import pytest + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'firmware')) + +from protocol import parse_drgb, parse_drgbw, parse_dnrgb, FrameAssembler +from lightsync.protocol.drgb import encode_drgb, encode_drgbw, encode_dnrgb_packets + + +class TestDRGBFrames: + def test_single_pixel(self): + packet = encode_drgb([(255, 0, 0)]) + pixels = parse_drgb(packet) + assert pixels == [(255, 0, 0)] + + def test_300_leds(self): + src = [(i % 256, (i * 7) % 256, (i * 13) % 256) for i in range(300)] + packet = encode_drgb(src) + pixels = parse_drgb(packet) + assert len(pixels) == 300 + assert pixels[0] == src[0] + assert pixels[149] == src[149] + assert pixels[299] == src[299] + + def test_all_black(self): + src = [(0, 0, 0)] * 100 + packet = encode_drgb(src) + pixels = parse_drgb(packet) + assert all(p == (0, 0, 0) for p in pixels) + + def test_all_white(self): + src = [(255, 255, 255)] * 50 + packet = encode_drgb(src) + pixels = parse_drgb(packet) + assert all(p == (255, 255, 255) for p in pixels) + + +class TestDRGBWFrames: + def test_single_pixel(self): + packet = encode_drgbw([(255, 0, 0, 128)]) + pixels = parse_drgbw(packet) + assert pixels == [(255, 0, 0, 128)] + + def test_300_leds_rgbw(self): + src = [(i % 256, (i * 3) % 256, (i * 5) % 256, (i * 7) % 256) for i in range(300)] + packet = encode_drgbw(src) + pixels = parse_drgbw(packet) + assert len(pixels) == 300 + assert pixels[0] == src[0] + assert pixels[299] == src[299] + + def test_w_channel_preserved(self): + """Verify the W channel survives encode/decode round-trip.""" + src = [(100, 100, 100, 200)] + packet = encode_drgbw(src) + pixels = parse_drgbw(packet) + assert pixels[0][3] == 200 + + +class TestDNRGBFrames: + def test_single_packet_small(self): + src = [(255, 0, 0)] * 100 + packets = encode_dnrgb_packets(src) + assert len(packets) == 1 + start_idx, pixels = parse_dnrgb(packets[0]) + assert start_idx == 0 + assert len(pixels) == 100 + assert pixels[0] == (255, 0, 0) + + def test_multi_packet_600_leds(self): + """600 LEDs = 2 DNRGB packets (489 + 111).""" + src = [(i % 256, 0, 0) for i in range(600)] + packets = encode_dnrgb_packets(src) + assert len(packets) == 2 + + s0, px0 = parse_dnrgb(packets[0]) + s1, px1 = parse_dnrgb(packets[1]) + + assert s0 == 0 + assert s1 == 489 + assert len(px0) == 489 + assert len(px1) == 111 + + # Verify pixel values survive round-trip + assert px0[0] == src[0] + assert px0[488] == src[488] + assert px1[0] == src[489] + assert px1[110] == src[599] + + def test_start_index_offset(self): + """Packets with non-zero start_index.""" + src = [(0, 255, 0)] * 50 + packets = encode_dnrgb_packets(src, start_index=100) + assert len(packets) == 1 + start_idx, pixels = parse_dnrgb(packets[0]) + assert start_idx == 100 + assert len(pixels) == 50 + + +class TestFrameAssembler: + def test_single_chunk_completes(self): + asm = FrameAssembler(100) + pixels = [(i, 0, 0) for i in range(100)] + result = asm.feed(0, pixels, 1000) + assert result is not None + assert len(result) == 100 + assert result[0] == (0, 0, 0) + assert result[99] == (99, 0, 0) + + def test_two_chunks(self): + asm = FrameAssembler(200) + chunk1 = [(i, 0, 0) for i in range(100)] + chunk2 = [(100 + i, 0, 0) for i in range(100)] + + result1 = asm.feed(0, chunk1, 1000) + assert result1 is None # not complete yet + + result2 = asm.feed(100, chunk2, 1010) + assert result2 is not None + assert len(result2) == 200 + assert result2[0] == (0, 0, 0) + assert result2[100] == (100, 0, 0) + assert result2[199] == (199, 0, 0) + + def test_timeout_clears_buffer(self): + asm = FrameAssembler(200) + chunk1 = [(i, 0, 0) for i in range(100)] + + asm.feed(0, chunk1, 1000) + assert len(asm._buf) == 100 + + # Feed again after timeout (300ms > 200ms timeout) + chunk2 = [(50 + i, 0, 0) for i in range(50)] + asm.feed(0, chunk2, 1300) + # Buffer should have been cleared, only chunk2 remains + assert len(asm._buf) == 50 + + def test_reset(self): + asm = FrameAssembler(100) + asm.feed(0, [(0, 0, 0)] * 50, 1000) + asm.reset() + assert len(asm._buf) == 0 + + def test_full_round_trip_with_encoder(self): + """Multi-packet DNRGB: encode 600 LEDs -> feed both packets -> get complete frame.""" + asm = FrameAssembler(600) + src = [(i % 256, (i * 3) % 256, (i * 7) % 256) for i in range(600)] + packets = encode_dnrgb_packets(src) + assert len(packets) == 2 + + s0, px0 = parse_dnrgb(packets[0]) + result = asm.feed(s0, px0, 1000) + assert result is None + + s1, px1 = parse_dnrgb(packets[1]) + result = asm.feed(s1, px1, 1005) + assert result is not None + assert len(result) == 600 + assert result[0] == src[0] + assert result[599] == src[599] + + +class TestFrameModeIntegration: + """Test that SK6812 (RGBW) and WS2801 (RGB) frame paths work correctly.""" + + def test_drgb_for_ws2801(self): + """WS2801 is RGB — DRGB packets contain 3 bytes per pixel.""" + src = [(255, 128, 64)] * 300 + packet = encode_drgb(src) + pixels = parse_drgb(packet) + # All pixels should be RGB tuples + assert all(len(p) == 3 for p in pixels) + assert pixels[0] == (255, 128, 64) + + def test_drgbw_for_sk6812(self): + """SK6812 is RGBW — DRGBW packets contain 4 bytes per pixel.""" + src = [(255, 128, 64, 200)] * 300 + packet = encode_drgbw(src) + pixels = parse_drgbw(packet) + # All pixels should be RGBW tuples + assert all(len(p) == 4 for p in pixels) + assert pixels[0] == (255, 128, 64, 200) + + def test_drgb_pixel_count_matches(self): + """300 LED DRGB: 2 + 300*3 = 902 bytes, fits in one packet.""" + src = [(0, 0, 0)] * 300 + packet = encode_drgb(src) + assert len(packet) == 902 + pixels = parse_drgb(packet) + assert len(pixels) == 300 + + def test_drgbw_pixel_count_matches(self): + """300 LED DRGBW: 2 + 300*4 = 1202 bytes, fits in one packet.""" + src = [(0, 0, 0, 0)] * 300 + packet = encode_drgbw(src) + assert len(packet) == 1202 + pixels = parse_drgbw(packet) + assert len(pixels) == 300