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,11 +3,45 @@
from __future__ import annotations
import os
from typing import Literal
MATRIX_WIDTH = 45
MATRIX_HEIGHT = 9
MATRIX_PIXELS = MATRIX_WIDTH * MATRIX_HEIGHT # 405
# --- Hexagonal portal -------------------------------------------------------
# Bottom face = floor (no LEDs). Panels 04 numbered clockwise from bottom-left.
# Panel 2 is the top face. All five panels are Pico UDP adapters.
#
# Viewed from outside:
#
# [2] TOP
# [1] [3]
# top-left top-right
#
# [0] [4]
# bottom-left bottom-right
#
# Floor (no LEDs) is below.
TOP_PANEL_INDEX = 2
PORTAL_PANEL_ORDER_CLOCKWISE: tuple[int, ...] = (0, 1, 2, 3, 4)
SIDE_PANEL_ORDER: tuple[int, ...] = (0, 1, 3, 4)
PANEL_POSITION_BY_INDEX: dict[int, str] = {
0: "bottom-left",
1: "top-left",
2: "top",
3: "top-right",
4: "bottom-right",
}
PortalFace = Literal["top", "side", "floor"]
PANEL_FACE_BY_INDEX: dict[int, PortalFace] = {
2: "top",
0: "side",
1: "side",
3: "side",
4: "side",
}
# Per-panel width (height is always 9 rows). Edit to match your hardware.
# Panel 4 may still be 38 or 39 — run: panel_sync_test.py --test width --panel-index 4
PANEL_HEIGHT = 9
@@ -21,6 +55,24 @@ PANEL_WIDTH_BY_INDEX: dict[int, int] = {
DEFAULT_PANEL_WIDTH = 45
PANEL_COUNT = 5
# Panels driven in-process by the animation script (SPI/PIO on this Pi).
LOCAL_PANEL_INDICES: frozenset[int] = frozenset()
# Optional: Pi UDP→SPI bridge panels (examples/panel_spi_bridge.py). Empty = all Pico.
PI_BRIDGE_PANEL_INDICES: frozenset[int] = frozenset()
# Optional: two panels on one Pico (GP27 + GP28).
# Example — panels 0 and 1 on 10.1.1.10, strips 0 and 1:
# PANEL_HOST_BY_INDEX = {0: "10.1.1.10", 1: "10.1.1.10"}
# PANEL_STRIP_BY_INDEX = {0: 0, 1: 1}
# PANEL_WS2812_PIN_BY_INDEX = {0: 28, 1: 27}
PANEL_HOST_BY_INDEX: dict[int, str] = {}
PANEL_STRIP_BY_INDEX: dict[int, int] = {}
PANEL_WS2812_PIN_BY_INDEX: dict[int, int] = {}
# Optional SPI bus override per panel index (Pi 5: 0 = GPIO 10, 1 = GPIO 20).
PANEL_SPI_BUS_BY_INDEX: dict[int, int] = {}
# SPI data line: bus 0 = GPIO 10 (MOSI), bus 1 = GPIO 20 (SPI1 MOSI)
MATRIX_SPI_BUS = 0
MATRIX_SPI_DEVICE = 0
@@ -34,8 +86,9 @@ MATRIX_FLIP_Y = False
MATRIX_BRIGHTNESS = 0.25
# LED wire order for direct SPI on Pi (not used for Pico UDP — firmware packs GRB).
WIRE_ORDER = os.environ.get("PORTAL_WIRE_ORDER", "grb").lower()
# Optional wire reorder for direct SPI tests only (set PORTAL_WIRE_ORDER env).
# UDP/bridge/firmware pass logical RGB through unchanged (default rgb = no swap).
WIRE_ORDER = os.environ.get("PORTAL_WIRE_ORDER", "rgb").lower()
def panel_width(panel_index: int) -> int:
@@ -51,6 +104,22 @@ def panel_pixel_count(panel_index: int) -> int:
return w * h
def panel_face(panel_index: int) -> PortalFace | None:
return PANEL_FACE_BY_INDEX.get(panel_index)
def panel_label(panel_index: int) -> str:
face = panel_face(panel_index)
pos = PANEL_POSITION_BY_INDEX.get(panel_index)
if face == "top":
return f"panel {panel_index} (top / Pi)"
if pos:
return f"panel {panel_index} ({pos})"
if face == "side":
return f"panel {panel_index} (side wall)"
return f"panel {panel_index}"
def matrix_pixel_index(
x: int,
y: int,