test(07-03): add firmware frame mode tests — 19 passing

- TestDRGBFrames: single pixel, 300 LEDs, all black/white
- TestDRGBWFrames: single pixel, 300 LEDs RGBW, W channel preserved
- TestDNRGBFrames: single packet, multi-packet 600 LEDs, start_index offset
- TestFrameAssembler: single chunk, two chunks, timeout, reset, round-trip
- TestFrameModeIntegration: DRGB/DRGBW pixel counts, strip type routing
This commit is contained in:
Claude
2026-04-07 15:42:37 +00:00
parent 62bc61259f
commit ec97600763

View File

@@ -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