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:
204
tests/test_firmware_frames.py
Normal file
204
tests/test_firmware_frames.py
Normal 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
|
||||
Reference in New Issue
Block a user