Files
portal/leds/array_config.py
Jimmy 5094c7bcee 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>
2026-07-30 14:54:51 +12:00

139 lines
3.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""LED matrix layout defaults."""
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
PANEL_WIDTH_BY_INDEX: dict[int, int] = {
0: 39,
1: 45,
2: 45,
3: 45,
4: 39,
}
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
# One continuous chain on a single data pin; rows all run left → right.
MATRIX_ROWS_PER_STRIP = None
MATRIX_SERPENTINE = "none" # use "rows" for zigzag / serpentine wiring
# Set True if DIN enters at the bottom row instead of the top.
MATRIX_FLIP_Y = False
MATRIX_BRIGHTNESS = 0.25
# 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:
return PANEL_WIDTH_BY_INDEX.get(panel_index, DEFAULT_PANEL_WIDTH)
def panel_layout(panel_index: int) -> tuple[int, int]:
return panel_width(panel_index), PANEL_HEIGHT
def panel_pixel_count(panel_index: int) -> int:
w, h = panel_layout(panel_index)
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,
width: int = MATRIX_WIDTH,
height: int = MATRIX_HEIGHT,
*,
serpentine: str = MATRIX_SERPENTINE,
flip_y: bool = MATRIX_FLIP_Y,
) -> int:
"""Map (x, y) to strip index. Default: every row left → right."""
if flip_y:
y = height - 1 - y
col = x
if serpentine == "rows" and y % 2 == 1:
col = width - 1 - x
return y * width + col