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>
139 lines
3.9 KiB
Python
139 lines
3.9 KiB
Python
"""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 0–4 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
|