Files
portal/leds/matrix.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

179 lines
5.9 KiB
Python
Raw Permalink 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.
"""2D WS2812 matrix mapped onto one or two strips."""
from __future__ import annotations
from typing import Literal, Sequence, Tuple
from leds.array_config import (
MATRIX_BRIGHTNESS,
MATRIX_HEIGHT,
MATRIX_ROWS_PER_STRIP,
MATRIX_SERPENTINE,
MATRIX_WIDTH,
)
from leds.config import StripConfig, default_dual_strip_configs
from leds.strip import Color, LedStrip, _normalize_color
from leds.strips import LedStrips
Serpentine = Literal["rows", "none"]
class LedMatrix:
"""
45×9 matrix on one WS2812 data line (405 LEDs).
Any size works: LedMatrix(width=50, height=12) → 600 LEDs, etc.
"""
def __init__(
self,
width: int = MATRIX_WIDTH,
height: int = MATRIX_HEIGHT,
*,
rows_per_strip: Sequence[int] | None = MATRIX_ROWS_PER_STRIP,
serpentine: Serpentine = MATRIX_SERPENTINE,
brightness: float = MATRIX_BRIGHTNESS,
config0: StripConfig | None = None,
config1: StripConfig | None = None,
) -> None:
if width < 1 or height < 1:
raise ValueError("width and height must be positive")
self.width = width
self.height = height
self.serpentine = serpentine
self._strip: LedStrip | None = None
self._strips: LedStrips | None = None
if rows_per_strip is None:
self._rows_per_strip = (height,)
count0 = width * height
if config0 is None:
from leds.config import default_strip_config
config0 = default_strip_config(count0, brightness=brightness)
self._strip = LedStrip(
count0,
backend=config0.backend,
pin=config0.pin,
channel=config0.channel,
device=config0.device,
brightness=config0.brightness,
spi_bus=config0.spi_bus,
spi_device=config0.spi_device,
spi_max_speed_hz=config0.spi_max_speed_hz,
wire_order=config0.wire_order,
passthrough=config0.passthrough,
swap_rg=config0.swap_rg,
)
else:
total_rows = sum(rows_per_strip)
if total_rows != height:
raise ValueError(
f"rows_per_strip sums to {total_rows}, expected {height}"
)
counts = [width * r for r in rows_per_strip]
self._rows_per_strip = tuple(rows_per_strip)
if config0 is None or config1 is None:
defaults = default_dual_strip_configs(
counts[0], counts[1], brightness=brightness
)
config0 = config0 or defaults[0]
config1 = config1 or defaults[1]
self._strips = LedStrips(
counts[0],
counts[1],
config0=config0,
config1=config1,
brightness=brightness,
)
def _locate(self, x: int, y: int) -> Tuple[int, int]:
if not (0 <= x < self.width and 0 <= y < self.height):
raise IndexError(f"({x}, {y}) out of range for {self.width}×{self.height}")
row_offset = 0
for strip_id, num_rows in enumerate(self._rows_per_strip):
if y < row_offset + num_rows:
local_y = y - row_offset
col = x
if self.serpentine == "rows" and y % 2 == 1:
col = self.width - 1 - x
return strip_id, local_y * self.width + col
row_offset += num_rows
raise IndexError(f"({x}, {y}) out of range")
def index_at(self, x: int, y: int) -> int:
"""Strip index for (x, y) on a single-strip matrix."""
strip_id, index = self._locate(x, y)
if strip_id != 0 or self._strip is None:
raise RuntimeError("index_at requires a single-strip matrix")
return index
@property
def strip(self) -> LedStrip | None:
return self._strip
def __getitem__(self, pos: Tuple[int, int]) -> Tuple[int, int, int]:
x, y = pos
strip_id, index = self._locate(x, y)
if self._strip is not None:
return self._strip[index]
assert self._strips is not None
return self._strips[strip_id][index]
def __setitem__(self, pos: Tuple[int, int], color: Color) -> None:
x, y = pos
strip_id, index = self._locate(x, y)
rgb = _normalize_color(color)
if self._strip is not None:
self._strip[index] = rgb
else:
assert self._strips is not None
self._strips[strip_id][index] = rgb
def set_pixel(self, x: int, y: int, color: Color) -> None:
self[x, y] = color
def fill(self, color: Color) -> None:
rgb = _normalize_color(color)
if self._strip is not None:
self._strip.fill(rgb)
else:
assert self._strips is not None
self._strips[0].fill(rgb)
self._strips[1].fill(rgb)
def show(self) -> None:
if self._strip is not None:
self._strip.show()
else:
assert self._strips is not None
self._strips.show()
def show_rgb_bytes(self, rgb: bytes) -> None:
"""Push a linear RGB frame (row-major) straight to the strip."""
if self._strip is None:
raise RuntimeError("show_rgb_bytes requires a single-strip matrix")
self._strip.show_rgb_bytes(rgb)
def clear(self) -> None:
if self._strip is not None:
self._strip.clear()
else:
assert self._strips is not None
self._strips.clear()
def close(self) -> None:
if self._strip is not None:
self._strip.close()
elif self._strips is not None:
self._strips.close()
def __enter__(self) -> LedMatrix:
return self
def __exit__(self, *_exc) -> None:
self.close()