Add portal web simulator, SPI bridges, and Pico firmware updates.
Bring the five-panel hex portal online with a browser 3D/schematic preview, Pi SPI backends, and renamed multi-panel Pico UDP firmware. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -5,13 +5,15 @@ from __future__ import annotations
|
||||
from typing import Literal, Sequence, Tuple, Union
|
||||
|
||||
from leds.backends.pio import PioBackend
|
||||
from leds.backends.rpi5_ws2812 import Rpi5Ws2812Backend
|
||||
from leds.backends.spi import SpiBackend
|
||||
from leds.backends.ws281x import Ws281xBackend
|
||||
from leds.config import pi5_setup_hint
|
||||
from leds.detect import board_family, board_model, pio_available, supports_ws281x
|
||||
from leds.wire import normalize_wire_order, rgb_bytes_to_wire, swap_rg_bytes
|
||||
|
||||
Color = Union[Tuple[int, int, int], Sequence[int]]
|
||||
BackendName = Literal["auto", "pio", "spi", "ws281x"]
|
||||
BackendName = Literal["auto", "pio", "spi", "rpi5_ws2812", "ws281x"]
|
||||
|
||||
|
||||
def _clamp(value: int) -> int:
|
||||
@@ -41,10 +43,12 @@ def _resolve_backend(name: BackendName) -> str:
|
||||
)
|
||||
if name == "pio" and board_family() == "pi5" and not pio_available():
|
||||
raise RuntimeError(pi5_setup_hint(10, 10))
|
||||
if name == "rpi5_ws2812" and board_family() != "pi5":
|
||||
raise RuntimeError("rpi5_ws2812 backend requires Raspberry Pi 5")
|
||||
return name
|
||||
|
||||
if board_family() == "pi5":
|
||||
return "pio" if pio_available() else "spi"
|
||||
return "rpi5_ws2812"
|
||||
return "ws281x"
|
||||
|
||||
|
||||
@@ -53,7 +57,8 @@ class LedStrip:
|
||||
Drive a WS2812 / NeoPixel strip on Pi 5, 4, or Zero.
|
||||
|
||||
Default backends:
|
||||
- Pi 5: ws2812-pio on GPIO 18/13 (/dev/leds0, /dev/leds1)
|
||||
- Pi 5: rpi5-ws2812 SPI on GPIO 10 (/dev/spidev0.0)
|
||||
- Pi 5 (pio): ws2812-pio on GPIO 18/13 (/dev/leds0, /dev/leds1) when backend='pio'
|
||||
- Pi 4 / Zero: PWM on GPIO 18/13 via rpi_ws281x
|
||||
"""
|
||||
|
||||
@@ -68,20 +73,35 @@ class LedStrip:
|
||||
brightness: float = 1.0,
|
||||
spi_bus: int = 0,
|
||||
spi_device: int = 0,
|
||||
spi_max_speed_hz: int | None = None,
|
||||
wire_order: str | None = None,
|
||||
passthrough: bool = False,
|
||||
swap_rg: bool = True,
|
||||
) -> None:
|
||||
if count < 1:
|
||||
raise ValueError("count must be at least 1")
|
||||
|
||||
self._count = count
|
||||
self._brightness = max(0.0, min(1.0, brightness))
|
||||
self._passthrough = passthrough
|
||||
self._swap_rg = swap_rg
|
||||
self._wire_order = normalize_wire_order(wire_order)
|
||||
self._pixels: list[Tuple[int, int, int]] = [(0, 0, 0)] * count
|
||||
|
||||
resolved = _resolve_backend(backend)
|
||||
if resolved == "pio":
|
||||
self._backend = PioBackend(count, device=device)
|
||||
elif resolved == "spi":
|
||||
spi_kwargs: dict[str, int] = {}
|
||||
if spi_max_speed_hz is not None:
|
||||
spi_kwargs["max_speed_hz"] = spi_max_speed_hz
|
||||
self._backend = SpiBackend(
|
||||
count, spi_bus=spi_bus, spi_device=spi_device
|
||||
count, spi_bus=spi_bus, spi_device=spi_device, **spi_kwargs
|
||||
)
|
||||
elif resolved == "rpi5_ws2812":
|
||||
hz = spi_max_speed_hz if spi_max_speed_hz is not None else 4_200_000
|
||||
self._backend = Rpi5Ws2812Backend(
|
||||
count, spi_bus=spi_bus, spi_device=spi_device, max_speed_hz=hz
|
||||
)
|
||||
else:
|
||||
self._backend = Ws281xBackend(count, pin=pin, channel=channel)
|
||||
@@ -121,14 +141,43 @@ class LedStrip:
|
||||
self[index] = color
|
||||
|
||||
def show(self) -> None:
|
||||
grb = bytearray(self._count * 3)
|
||||
self._write_frame(self._logical_frame_bytes())
|
||||
|
||||
def show_rgb_bytes(self, rgb: bytes) -> None:
|
||||
"""Push pre-dimmed RGB frame to the strip (skips pixel buffer)."""
|
||||
if len(rgb) != self._count * 3:
|
||||
raise ValueError(f"expected {self._count * 3} bytes, got {len(rgb)}")
|
||||
scale = self._brightness
|
||||
if scale >= 1.0:
|
||||
logical = rgb
|
||||
else:
|
||||
dimmed = bytearray(len(rgb))
|
||||
for i in range(0, len(rgb), 3):
|
||||
dimmed[i] = int(rgb[i] * scale)
|
||||
dimmed[i + 1] = int(rgb[i + 1] * scale)
|
||||
dimmed[i + 2] = int(rgb[i + 2] * scale)
|
||||
logical = bytes(dimmed)
|
||||
self._write_frame(logical)
|
||||
|
||||
def _write_frame(self, logical_rgb: bytes) -> None:
|
||||
frame = swap_rg_bytes(logical_rgb) if self._swap_rg else logical_rgb
|
||||
if self._passthrough:
|
||||
self._backend.write_grb(frame)
|
||||
return
|
||||
self._backend.write_grb(self._encode_frame_bytes(frame))
|
||||
|
||||
def _logical_frame_bytes(self) -> bytes:
|
||||
logical = bytearray(self._count * 3)
|
||||
for i, (r, g, b) in enumerate(self._pixels):
|
||||
r, g, b = _apply_brightness((r, g, b), self._brightness)
|
||||
base = i * 3
|
||||
grb[base] = g
|
||||
grb[base + 1] = r
|
||||
grb[base + 2] = b
|
||||
self._backend.write_grb(bytes(grb))
|
||||
logical[base] = r
|
||||
logical[base + 1] = g
|
||||
logical[base + 2] = b
|
||||
return bytes(logical)
|
||||
|
||||
def _encode_frame_bytes(self, logical_rgb: bytes) -> bytes:
|
||||
return rgb_bytes_to_wire(logical_rgb, self._wire_order)
|
||||
|
||||
def clear(self) -> None:
|
||||
self.fill((0, 0, 0))
|
||||
|
||||
Reference in New Issue
Block a user