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

38
firmware/boot.py Normal file
View File

@@ -0,0 +1,38 @@
import network
import time
import json
def load_config():
try:
with open('config.json') as f:
return json.load(f)
except OSError:
print('[boot] config.json not found, using defaults')
return {
'ssid': 'MyWiFi', 'password': 'secret',
'led_count': 300, 'strip_type': 'sk6812',
'led_pin': 5, 'spi_id': 1, 'udp_port': 21324
}
def connect_wifi(ssid, password, timeout=15):
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
if wlan.isconnected():
print('[boot] Already connected:', wlan.ifconfig()[0])
return wlan
print('[boot] Connecting to', ssid, '...')
wlan.connect(ssid, password)
start = time.time()
while not wlan.isconnected():
if time.time() - start > timeout:
print('[boot] WiFi timeout after', timeout, 'seconds')
return wlan
time.sleep(0.5)
print('[boot] Connected:', wlan.ifconfig()[0])
return wlan
config = load_config()
wlan = connect_wifi(config['ssid'], config['password'])

11
firmware/config.json Normal file
View File

@@ -0,0 +1,11 @@
{
"ssid": "MyWiFi",
"password": "secret",
"led_count": 300,
"strip_type": "sk6812",
"led_pin": 5,
"spi_id": 1,
"spi_clk_pin": 18,
"spi_dat_pin": 23,
"udp_port": 21324
}

87
firmware/led_driver.py Normal file
View File

@@ -0,0 +1,87 @@
class NeoPixelStrip:
"""SK6812 RGBW driver using built-in neopixel module."""
def __init__(self, pin_num, led_count):
import neopixel
from machine import Pin
self.np = neopixel.NeoPixel(Pin(pin_num), led_count, bpp=4, timing=1)
self.led_count = led_count
def write_rgb(self, pixels):
"""Write list of (R,G,B) tuples. W channel set to 0."""
for i, (r, g, b) in enumerate(pixels):
if i >= self.led_count:
break
self.np[i] = (r, g, b, 0)
self.np.write()
def write_rgbw(self, pixels):
"""Write list of (R,G,B,W) tuples."""
for i, (r, g, b, w) in enumerate(pixels):
if i >= self.led_count:
break
self.np[i] = (r, g, b, w)
self.np.write()
def fill(self, r, g, b, w=0):
"""Fill all pixels with one color."""
self.np.fill((r, g, b, w))
self.np.write()
def clear(self):
self.fill(0, 0, 0, 0)
class WS2801Strip:
"""WS2801 RGB driver using SPI — no external library needed."""
def __init__(self, spi_id, clk_pin, dat_pin, led_count):
from machine import SPI, Pin
self.spi = SPI(spi_id, baudrate=1000000, polarity=0, phase=0,
sck=Pin(clk_pin), mosi=Pin(dat_pin))
self.led_count = led_count
self._buf = bytearray(led_count * 3)
def write_rgb(self, pixels):
"""Write list of (R,G,B) tuples."""
for i, (r, g, b) in enumerate(pixels):
if i >= self.led_count:
break
off = i * 3
self._buf[off] = r
self._buf[off + 1] = g
self._buf[off + 2] = b
self.spi.write(self._buf)
def write_rgbw(self, pixels):
"""Write RGBW tuples — W channel is discarded (WS2801 is RGB only)."""
for i, (r, g, b, _w) in enumerate(pixels):
if i >= self.led_count:
break
off = i * 3
self._buf[off] = r
self._buf[off + 1] = g
self._buf[off + 2] = b
self.spi.write(self._buf)
def fill(self, r, g, b, w=0):
for i in range(self.led_count):
off = i * 3
self._buf[off] = r
self._buf[off + 1] = g
self._buf[off + 2] = b
self.spi.write(self._buf)
def clear(self):
self.fill(0, 0, 0)
def create_strip(config):
"""Factory: create the right strip driver from config dict."""
if config['strip_type'] == 'sk6812':
return NeoPixelStrip(config['led_pin'], config['led_count'])
elif config['strip_type'] == 'ws2801':
return WS2801Strip(config['spi_id'], config.get('spi_clk_pin', 18),
config.get('spi_dat_pin', 23), config['led_count'])
else:
raise ValueError('Unknown strip_type: ' + config['strip_type'])