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:
330
leds/animations.py
Normal file
330
leds/animations.py
Normal file
@@ -0,0 +1,330 @@
|
||||
"""Animations for LedMatrix and LedStrip."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import random
|
||||
from typing import Callable, Union
|
||||
|
||||
from leds.colors import dim, heat_color, hsv_to_rgb, wheel
|
||||
from leds.matrix import LedMatrix
|
||||
from leds.strip import LedStrip
|
||||
|
||||
Surface = Union[LedMatrix, LedStrip]
|
||||
DrawFn = Callable[[Surface, int], None]
|
||||
|
||||
|
||||
def _is_matrix(surface: Surface) -> bool:
|
||||
return hasattr(surface, "width") and hasattr(surface, "height")
|
||||
|
||||
|
||||
def _size(surface: Surface) -> tuple[int, int]:
|
||||
if _is_matrix(surface):
|
||||
return surface.width, surface.height
|
||||
n = len(surface)
|
||||
return n, 1
|
||||
|
||||
|
||||
def _set_index(surface: Surface, x: int, y: int, color: tuple[int, int, int]) -> None:
|
||||
if isinstance(surface, LedStrip):
|
||||
w, _ = _size(surface)
|
||||
surface[y * w + x] = color
|
||||
else:
|
||||
surface[x, y] = color
|
||||
|
||||
|
||||
def _each_pixel(surface: Surface):
|
||||
if _is_matrix(surface):
|
||||
w, h = surface.width, surface.height
|
||||
for y in range(h):
|
||||
for x in range(w):
|
||||
yield x, y
|
||||
else:
|
||||
for i in range(len(surface)):
|
||||
yield i, 0
|
||||
|
||||
|
||||
def rainbow_scroll(surface: Surface, frame: int) -> None:
|
||||
if isinstance(surface, LedStrip):
|
||||
n = len(surface)
|
||||
for i in range(n):
|
||||
surface[i] = wheel(frame * 2 + i * 256 // max(n, 1))
|
||||
return
|
||||
|
||||
w, h = surface.width, surface.height
|
||||
strip = getattr(surface, "strip", None)
|
||||
if strip is not None and hasattr(surface, "index_at"):
|
||||
for y in range(h):
|
||||
for x in range(w):
|
||||
strip[surface.index_at(x, y)] = wheel(x * 3 + y * 8 + frame * 2)
|
||||
else:
|
||||
for y in range(h):
|
||||
for x in range(w):
|
||||
surface[x, y] = wheel(x * 3 + y * 8 + frame * 2)
|
||||
|
||||
|
||||
def plasma(surface: Surface, frame: int) -> None:
|
||||
w, h = _size(surface)
|
||||
t = frame * 0.08
|
||||
for y in range(h):
|
||||
for x in range(w):
|
||||
v = (
|
||||
math.sin(x * 0.15 + t)
|
||||
+ math.sin(y * 0.2 - t * 1.3)
|
||||
+ math.sin((x + y) * 0.1 + t * 0.7)
|
||||
) / 3.0
|
||||
_set_index(surface, x, y, hsv_to_rgb((v + 1) / 2, 1.0, 1.0))
|
||||
|
||||
|
||||
def sine_wave(surface: Surface, frame: int) -> None:
|
||||
w, h = _size(surface)
|
||||
for y in range(h):
|
||||
for x in range(w):
|
||||
wave = math.sin(x * 0.25 + frame * 0.15) * math.cos(y * 0.35 - frame * 0.1)
|
||||
_set_index(surface, x, y, dim((0, 80, 200), (wave + 1) / 2))
|
||||
|
||||
|
||||
def scanner(surface: Surface, frame: int) -> None:
|
||||
w, h = _size(surface)
|
||||
pos = frame % max(w * 2 - 2, 1)
|
||||
if pos >= w:
|
||||
pos = w * 2 - 2 - pos
|
||||
if _is_matrix(surface):
|
||||
surface.fill((0, 0, 0))
|
||||
else:
|
||||
surface.fill((0, 0, 0))
|
||||
for y in range(h):
|
||||
for dx in range(-3, 4):
|
||||
x = pos + dx
|
||||
if 0 <= x < w:
|
||||
_set_index(surface, x, y, dim((255, 0, 0), 1.0 - abs(dx) / 4.0))
|
||||
|
||||
|
||||
def comet(surface: Surface, frame: int) -> None:
|
||||
w, h = _size(surface)
|
||||
head = frame % (w + 12)
|
||||
if _is_matrix(surface):
|
||||
surface.fill((0, 0, 0))
|
||||
else:
|
||||
surface.fill((0, 0, 0))
|
||||
for y in range(h):
|
||||
for i in range(12):
|
||||
x = head - i
|
||||
if 0 <= x < w:
|
||||
_set_index(surface, x, y, dim(wheel(frame * 3 + i * 10), (12 - i) / 12))
|
||||
|
||||
|
||||
def pulse(surface: Surface, frame: int) -> None:
|
||||
level = (math.sin(frame * 0.12) + 1) / 2
|
||||
color = dim((120, 0, 180), 0.15 + level * 0.85)
|
||||
if _is_matrix(surface):
|
||||
surface.fill(color)
|
||||
else:
|
||||
surface.fill(color)
|
||||
|
||||
|
||||
def sparkle(surface: Surface, frame: int) -> None:
|
||||
for x, y in _each_pixel(surface):
|
||||
if _is_matrix(surface):
|
||||
r, g, b = surface[x, y]
|
||||
else:
|
||||
r, g, b = surface[x]
|
||||
c = (r >> 2, g >> 2, b >> 2)
|
||||
_set_index(surface, x, y, c)
|
||||
w, h = _size(surface)
|
||||
for _ in range(6):
|
||||
_set_index(surface, random.randint(0, w - 1), random.randint(0, h - 1), (220, 220, 255))
|
||||
|
||||
|
||||
def fire(surface: Surface, frame: int) -> None:
|
||||
w, h = _size(surface)
|
||||
size = w * h
|
||||
if not hasattr(fire, "_heat") or len(fire._heat) != size:
|
||||
fire._heat = [0.0] * size
|
||||
|
||||
heat: list[float] = fire._heat
|
||||
for x in range(w):
|
||||
heat[(h - 1) * w + x] = random.random() * 0.5 + 0.5
|
||||
|
||||
for y in range(h - 2, -1, -1):
|
||||
for x in range(w):
|
||||
below = (y + 1) * w + x
|
||||
left = below - 1 if x > 0 else below
|
||||
right = below + 1 if x < w - 1 else below
|
||||
decay = random.uniform(0.0, 0.18)
|
||||
heat[y * w + x] = max(0.0, (heat[left] + heat[below] + heat[right]) / 3 - decay)
|
||||
|
||||
for y in range(h):
|
||||
for x in range(w):
|
||||
_set_index(surface, x, y, heat_color(heat[y * w + x]))
|
||||
|
||||
|
||||
def rain(surface: Surface, frame: int) -> None:
|
||||
w, h = _size(surface)
|
||||
if not hasattr(rain, "_drops"):
|
||||
rain._drops = [
|
||||
{"x": random.randint(0, w - 1), "y": random.randint(-h, 0), "speed": random.randint(1, 3)}
|
||||
for _ in range(max(w // 2, 1))
|
||||
]
|
||||
|
||||
if _is_matrix(surface):
|
||||
surface.fill((0, 0, 0))
|
||||
else:
|
||||
surface.fill((0, 0, 0))
|
||||
|
||||
for drop in rain._drops:
|
||||
drop["y"] += drop["speed"]
|
||||
if drop["y"] >= h:
|
||||
drop["y"] = random.randint(-3, -1)
|
||||
drop["x"] = random.randint(0, w - 1)
|
||||
drop["speed"] = random.randint(1, 3)
|
||||
x, y = drop["x"], int(drop["y"])
|
||||
if 0 <= y < h:
|
||||
_set_index(surface, x, y, (0, 220, 80))
|
||||
if y > 0:
|
||||
_set_index(surface, x, y - 1, dim((0, 220, 80), 0.4))
|
||||
|
||||
|
||||
def stripes(surface: Surface, frame: int) -> None:
|
||||
w, h = _size(surface)
|
||||
offset = frame % max(w, 1)
|
||||
for y in range(h):
|
||||
for x in range(w):
|
||||
band = (x + offset) // 3 % 3
|
||||
if band == 0:
|
||||
_set_index(surface, x, y, (200, 0, 0))
|
||||
elif band == 1:
|
||||
_set_index(surface, x, y, (0, 200, 0))
|
||||
else:
|
||||
_set_index(surface, x, y, (0, 0, 200))
|
||||
|
||||
|
||||
def bounce(surface: Surface, frame: int) -> None:
|
||||
w, h = _size(surface)
|
||||
if _is_matrix(surface):
|
||||
surface.fill((0, 0, 0))
|
||||
else:
|
||||
surface.fill((0, 0, 0))
|
||||
t = frame * 0.2
|
||||
x = int((math.sin(t) + 1) / 2 * (w - 1))
|
||||
y = int((math.cos(t * 1.3) + 1) / 2 * (h - 1))
|
||||
for dy in range(-1, 2):
|
||||
for dx in range(-1, 2):
|
||||
px, py = x + dx, y + dy
|
||||
if 0 <= px < w and 0 <= py < h:
|
||||
_set_index(surface, px, py, wheel(frame * 5))
|
||||
|
||||
|
||||
def _iter_line(x0: int, y0: int, x1: int, y1: int):
|
||||
"""Yield pixel coordinates along a Bresenham line."""
|
||||
dx = abs(x1 - x0)
|
||||
dy = -abs(y1 - y0)
|
||||
sx = 1 if x0 < x1 else -1
|
||||
sy = 1 if y0 < y1 else -1
|
||||
err = dx + dy
|
||||
x, y = x0, y0
|
||||
while True:
|
||||
yield x, y
|
||||
if x == x1 and y == y1:
|
||||
break
|
||||
e2 = 2 * err
|
||||
if e2 >= dy:
|
||||
err += dy
|
||||
x += sx
|
||||
if e2 <= dx:
|
||||
err += dx
|
||||
y += sy
|
||||
|
||||
|
||||
def rolling(surface: Surface, frame: int) -> None:
|
||||
"""Full-height band rolling left to right across the panel."""
|
||||
w, h = _size(surface)
|
||||
tail = 10
|
||||
head = frame % (w + tail)
|
||||
if _is_matrix(surface):
|
||||
surface.fill((0, 0, 0))
|
||||
else:
|
||||
surface.fill((0, 0, 0))
|
||||
for y in range(h):
|
||||
hue_shift = y * 18 + frame * 3
|
||||
for i in range(tail):
|
||||
x = head - i
|
||||
if 0 <= x < w:
|
||||
bright = (tail - i) / tail
|
||||
_set_index(surface, x, y, dim(wheel(hue_shift + i * 8), bright))
|
||||
|
||||
|
||||
def spin_line(surface: Surface, frame: int) -> None:
|
||||
"""Line from panel center spinning like a radar sweep."""
|
||||
w, h = _size(surface)
|
||||
if _is_matrix(surface):
|
||||
surface.fill((0, 0, 0))
|
||||
else:
|
||||
surface.fill((0, 0, 0))
|
||||
|
||||
if not _is_matrix(surface):
|
||||
n = len(surface)
|
||||
pos = int((frame * 3) % max(n, 1))
|
||||
for i in range(8):
|
||||
idx = (pos - i) % n
|
||||
surface[idx] = dim(wheel(frame * 5 + i * 20), (8 - i) / 8)
|
||||
return
|
||||
|
||||
cx = (w - 1) / 2.0
|
||||
cy = (h - 1) / 2.0
|
||||
angle = frame * 0.14
|
||||
reach = max(w, h) * 1.2
|
||||
x1 = int(cx + math.cos(angle) * reach)
|
||||
y1 = int(cy + math.sin(angle) * reach)
|
||||
color = wheel(frame * 4)
|
||||
|
||||
for x, y in _iter_line(int(cx), int(cy), x1, y1):
|
||||
if 0 <= x < w and 0 <= y < h:
|
||||
_set_index(surface, x, y, color)
|
||||
|
||||
hub_x, hub_y = int(cx), int(cy)
|
||||
if 0 <= hub_x < w and 0 <= hub_y < h:
|
||||
_set_index(surface, hub_x, hub_y, (255, 255, 255))
|
||||
|
||||
|
||||
def solid(surface: Surface, frame: int) -> None:
|
||||
colors = [(255, 0, 0), (0, 255, 0), (0, 0, 255)]
|
||||
if _is_matrix(surface):
|
||||
surface.fill(colors[frame % 3])
|
||||
else:
|
||||
surface.fill(colors[frame % 3])
|
||||
|
||||
|
||||
ANIMATIONS: dict[str, DrawFn] = {
|
||||
"solid": solid,
|
||||
"rainbow": rainbow_scroll,
|
||||
"plasma": plasma,
|
||||
"wave": sine_wave,
|
||||
"scanner": scanner,
|
||||
"comet": comet,
|
||||
"pulse": pulse,
|
||||
"sparkle": sparkle,
|
||||
"fire": fire,
|
||||
"rain": rain,
|
||||
"stripes": stripes,
|
||||
"bounce": bounce,
|
||||
"rolling": rolling,
|
||||
"spin_line": spin_line,
|
||||
}
|
||||
|
||||
DEFAULT_FPS: dict[str, float] = {
|
||||
"solid": 1,
|
||||
"rainbow": 30,
|
||||
"plasma": 25,
|
||||
"wave": 25,
|
||||
"scanner": 35,
|
||||
"comet": 30,
|
||||
"pulse": 30,
|
||||
"sparkle": 40,
|
||||
"fire": 35,
|
||||
"rain": 30,
|
||||
"stripes": 25,
|
||||
"bounce": 35,
|
||||
"rolling": 30,
|
||||
"spin_line": 28,
|
||||
}
|
||||
Reference in New Issue
Block a user