feat(07-01): add firmware scaffold — boot.py, config.json, led_driver.py, protocol tests

- firmware/boot.py: WiFi connection on startup (MicroPython boot sequence)
- firmware/config.json: default device config with all 9 fields (ssid, password, led_count, strip_type, led_pin, spi_id, spi_clk_pin, spi_dat_pin, udp_port)
- firmware/led_driver.py: NeoPixelStrip (SK6812 RGBW) and WS2801Strip (RGB SPI) with unified interface (write_rgb, write_rgbw, fill, clear)
- tests/test_firmware_protocol.py: 21 host-side tests for round-trip protocol parsing (DRGB, DRGBW, DNRGB, animation_cmd)
This commit is contained in:
Claude
2026-04-07 15:42:17 +00:00
parent 2a22eeff75
commit 62bc61259f
4 changed files with 250 additions and 0 deletions

View File

@@ -0,0 +1,114 @@
"""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