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:
@@ -34,7 +34,7 @@ class PioBackend:
|
||||
frame = bytearray(self._led_count * 4)
|
||||
for i in range(self._led_count):
|
||||
base = i * 3
|
||||
g, r, b = grb_bytes[base], grb_bytes[base + 1], grb_bytes[base + 2]
|
||||
r, g, b = grb_bytes[base], grb_bytes[base + 1], grb_bytes[base + 2]
|
||||
offset = i * 4
|
||||
frame[offset] = r
|
||||
frame[offset + 1] = g
|
||||
|
||||
54
leds/backends/rpi5_ws2812.py
Normal file
54
leds/backends/rpi5_ws2812.py
Normal file
@@ -0,0 +1,54 @@
|
||||
"""WS2812 over SPI via the rpi5-ws2812 package (Pi 5, GPIO 10)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
from rpi5_ws2812.ws2812 import WS2812SpiDriver
|
||||
|
||||
from leds.backends.spi import spidev_bufsize_hint
|
||||
|
||||
|
||||
class Rpi5Ws2812Backend:
|
||||
def __init__(
|
||||
self,
|
||||
led_count: int,
|
||||
*,
|
||||
spi_bus: int = 0,
|
||||
spi_device: int = 0,
|
||||
max_speed_hz: int = 4_200_000,
|
||||
) -> None:
|
||||
hint = spidev_bufsize_hint(led_count)
|
||||
if hint:
|
||||
raise OSError(hint)
|
||||
|
||||
self._led_count = led_count
|
||||
self._driver = WS2812SpiDriver(spi_bus, spi_device, led_count)
|
||||
self._driver._device.max_speed_hz = max_speed_hz
|
||||
|
||||
@property
|
||||
def led_count(self) -> int:
|
||||
return self._led_count
|
||||
|
||||
def write_grb(self, rgb_bytes: bytes) -> None:
|
||||
"""RGB bytes passed through to SPI unchanged."""
|
||||
if len(rgb_bytes) != self._led_count * 3:
|
||||
raise ValueError(
|
||||
f"expected {self._led_count * 3} bytes, got {len(rgb_bytes)}"
|
||||
)
|
||||
buf = np.frombuffer(rgb_bytes, dtype=np.uint8).reshape(self._led_count, 3)
|
||||
self._driver.write(buf)
|
||||
|
||||
def clear(self) -> None:
|
||||
self._driver.clear()
|
||||
|
||||
def close(self) -> None:
|
||||
try:
|
||||
self.clear()
|
||||
finally:
|
||||
self._driver._device.close()
|
||||
|
||||
def __enter__(self) -> Rpi5Ws2812Backend:
|
||||
return self
|
||||
|
||||
def __exit__(self, *_exc) -> None:
|
||||
self.close()
|
||||
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from array import array
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
from spidev import SpiDev
|
||||
@@ -16,6 +17,48 @@ else:
|
||||
_LED_ZERO = 0b1100_0000
|
||||
_LED_ONE = 0b1111_1100
|
||||
_PREAMBLE = 42
|
||||
_DEFAULT_BUFSIZ = 65536
|
||||
|
||||
|
||||
def spidev_bufsize() -> int:
|
||||
try:
|
||||
return int(Path("/sys/module/spidev/parameters/bufsiz").read_text().strip())
|
||||
except (OSError, ValueError):
|
||||
return 4096
|
||||
|
||||
|
||||
def spi_frame_bytes(led_count: int) -> int:
|
||||
return _PREAMBLE + led_count * 24
|
||||
|
||||
|
||||
def leds_from_truncated_frame(bufsize: int) -> int:
|
||||
payload = max(0, bufsize - _PREAMBLE)
|
||||
return payload // 24
|
||||
|
||||
|
||||
def spidev_bufsize_hint(led_count: int) -> str:
|
||||
required = spi_frame_bytes(led_count)
|
||||
bufsize = spidev_bufsize()
|
||||
if bufsize >= required:
|
||||
return ""
|
||||
suggested = max(_DEFAULT_BUFSIZ, required + 1024)
|
||||
truncated = leds_from_truncated_frame(bufsize)
|
||||
return (
|
||||
f"spidev bufsiz is {bufsize} but {led_count} LEDs need a {required}-byte SPI frame "
|
||||
f"(only ~{truncated} LEDs update at the current limit).\n"
|
||||
f"Fix now:\n"
|
||||
f" sudo modprobe -r spidev\n"
|
||||
f" sudo modprobe spidev bufsiz={suggested}\n"
|
||||
f"Permanent:\n"
|
||||
f" pipenv run python examples/setup_spi_bufsiz.py --install"
|
||||
)
|
||||
|
||||
|
||||
def _write_spi_frame(device: SpiDev, frame: array) -> None:
|
||||
required = len(frame)
|
||||
if required > spidev_bufsize():
|
||||
raise OSError(spidev_bufsize_hint((required - _PREAMBLE) // 24))
|
||||
device.writebytes2(frame)
|
||||
|
||||
|
||||
class SpiBackend:
|
||||
@@ -39,12 +82,16 @@ class SpiBackend:
|
||||
self._device.mode = 0b00
|
||||
self._device.lsbfirst = False
|
||||
|
||||
frame_len = _PREAMBLE + led_count * 24
|
||||
frame_len = spi_frame_bytes(led_count)
|
||||
self._frame = array("B", [0] * frame_len)
|
||||
self._clear_frame = array("B", [0] * frame_len)
|
||||
for i in range(_PREAMBLE, frame_len):
|
||||
self._clear_frame[i] = _LED_ZERO
|
||||
|
||||
hint = spidev_bufsize_hint(led_count)
|
||||
if hint:
|
||||
raise OSError(hint)
|
||||
|
||||
@property
|
||||
def led_count(self) -> int:
|
||||
return self._led_count
|
||||
@@ -62,11 +109,10 @@ class SpiBackend:
|
||||
out[pos] = _LED_ONE if value & bit else _LED_ZERO
|
||||
pos += 1
|
||||
|
||||
# writebytes2 handles large frames (400+ LEDs); writebytes is capped at 4 KiB.
|
||||
self._device.writebytes2(out)
|
||||
_write_spi_frame(self._device, out)
|
||||
|
||||
def clear(self) -> None:
|
||||
self._device.writebytes2(self._clear_frame)
|
||||
_write_spi_frame(self._device, self._clear_frame)
|
||||
|
||||
def close(self) -> None:
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user