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:
2026-07-30 14:54:51 +12:00
parent d5cab2efdf
commit 5094c7bcee
78 changed files with 62251 additions and 832 deletions

View File

@@ -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: