Add multi-panel Pico UDP firmware and Python LED control.
Pico panels get static IPs on 10.1.1.10–14 with per-panel LED counts, Makefile deploy targets, and Python examples for animations, sync tests, and direct Pi SPI control. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
168
leds/matrix.py
Normal file
168
leds/matrix.py
Normal file
@@ -0,0 +1,168 @@
|
||||
"""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,
|
||||
)
|
||||
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 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()
|
||||
Reference in New Issue
Block a user