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:
170
examples/animations.py
Executable file
170
examples/animations.py
Executable file
@@ -0,0 +1,170 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run LED matrix animations locally (SPI/PIO) or on Pico panel (UDP)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
from leds import LedMatrix, LedStrip
|
||||
from leds.animations import ANIMATIONS, DEFAULT_FPS
|
||||
from leds.array_config import (
|
||||
MATRIX_BRIGHTNESS,
|
||||
MATRIX_HEIGHT,
|
||||
MATRIX_SPI_BUS,
|
||||
MATRIX_SPI_DEVICE,
|
||||
MATRIX_WIDTH,
|
||||
)
|
||||
from leds.config import spi_strip_config
|
||||
from leds.panel_udp import (
|
||||
add_panel_network_args,
|
||||
animation_playlist,
|
||||
format_panel_targets,
|
||||
panel_targets_from_args,
|
||||
run_panel_animation_loop,
|
||||
run_panel_playlist,
|
||||
)
|
||||
|
||||
|
||||
def _flash_test(surface, brightness: float) -> None:
|
||||
for color in ((255, 0, 0), (0, 255, 0), (0, 0, 255)):
|
||||
surface.fill(color)
|
||||
surface.show()
|
||||
time.sleep(0.35)
|
||||
surface.fill((0, 0, 0))
|
||||
surface.show()
|
||||
|
||||
|
||||
def _run_local_playlist(surface, playlist: list[str], args: argparse.Namespace) -> None:
|
||||
while True:
|
||||
for name in playlist:
|
||||
draw = ANIMATIONS[name]
|
||||
fps = args.fps or DEFAULT_FPS.get(name, 30)
|
||||
delay = 1.0 / fps
|
||||
frames = max(int(args.duration * fps), 1)
|
||||
print(f" {name} ({fps:.0f} fps)")
|
||||
for frame in range(frames):
|
||||
draw(surface, frame)
|
||||
surface.show()
|
||||
time.sleep(delay)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
names = list(ANIMATIONS.keys())
|
||||
parser = argparse.ArgumentParser(description="LED matrix animations")
|
||||
parser.add_argument(
|
||||
"animation",
|
||||
nargs="?",
|
||||
choices=names + ["all"],
|
||||
default="all",
|
||||
)
|
||||
parser.add_argument("--width", type=int, default=MATRIX_WIDTH)
|
||||
parser.add_argument("--height", type=int, default=MATRIX_HEIGHT)
|
||||
parser.add_argument(
|
||||
"--spi-bus",
|
||||
type=int,
|
||||
default=int(os.environ.get("PORTAL_SPI_BUS", MATRIX_SPI_BUS)),
|
||||
)
|
||||
parser.add_argument("--spi-device", type=int, default=MATRIX_SPI_DEVICE)
|
||||
parser.add_argument("--brightness", type=float, default=MATRIX_BRIGHTNESS)
|
||||
parser.add_argument("--duration", type=float, default=12.0)
|
||||
parser.add_argument("--fps", type=float, default=None)
|
||||
parser.add_argument("--linear", action="store_true")
|
||||
parser.add_argument(
|
||||
"--serpentine",
|
||||
action="store_true",
|
||||
help="zigzag odd rows (default: all rows left → right)",
|
||||
)
|
||||
parser.add_argument("--no-test", action="store_true")
|
||||
parser.add_argument(
|
||||
"--panel",
|
||||
action="store_true",
|
||||
help="send to Pico panel(s) over UDP instead of local SPI/PIO",
|
||||
)
|
||||
add_panel_network_args(parser)
|
||||
parser.add_argument("--once", action="store_true", help="panel: play playlist once")
|
||||
args = parser.parse_args()
|
||||
|
||||
playlist = animation_playlist(args.animation)
|
||||
|
||||
if args.panel:
|
||||
targets = panel_targets_from_args(args)
|
||||
print(f"Panel animations -> {format_panel_targets(targets, args.port)}")
|
||||
print(f"Playlist: {', '.join(playlist)}")
|
||||
try:
|
||||
if len(playlist) == 1 and args.animation != "all":
|
||||
run_panel_animation_loop(
|
||||
targets,
|
||||
args.port,
|
||||
playlist[0],
|
||||
fps=args.fps,
|
||||
brightness=args.brightness,
|
||||
panel_id=args.panel_id,
|
||||
)
|
||||
else:
|
||||
run_panel_playlist(
|
||||
targets,
|
||||
args.port,
|
||||
playlist,
|
||||
duration=args.duration,
|
||||
fps_override=args.fps,
|
||||
brightness=args.brightness,
|
||||
panel_id=args.panel_id,
|
||||
loop=not args.once,
|
||||
)
|
||||
except KeyboardInterrupt:
|
||||
print("\nStopped.")
|
||||
except OSError as exc:
|
||||
print(f"Network error: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
return 0
|
||||
|
||||
count = args.width * args.height
|
||||
serpentine = "rows" if args.serpentine else "none"
|
||||
device = f"/dev/spidev{args.spi_bus}.{args.spi_device}"
|
||||
mode = f"linear {count}" if args.linear else f"{args.width}×{args.height}"
|
||||
print(f"Matrix {mode} ({count} LEDs) → {device}")
|
||||
|
||||
strip_cfg = spi_strip_config(
|
||||
count,
|
||||
spi_bus=args.spi_bus,
|
||||
spi_device=args.spi_device,
|
||||
brightness=args.brightness,
|
||||
)
|
||||
|
||||
try:
|
||||
if args.linear:
|
||||
with LedStrip(
|
||||
count,
|
||||
backend="spi",
|
||||
spi_bus=args.spi_bus,
|
||||
spi_device=args.spi_device,
|
||||
brightness=args.brightness,
|
||||
) as surface:
|
||||
if not args.no_test:
|
||||
_flash_test(surface, args.brightness)
|
||||
_run_local_playlist(surface, playlist, args)
|
||||
else:
|
||||
with LedMatrix(
|
||||
args.width,
|
||||
args.height,
|
||||
serpentine=serpentine,
|
||||
config0=strip_cfg,
|
||||
) as surface:
|
||||
if not args.no_test:
|
||||
_flash_test(surface, args.brightness)
|
||||
_run_local_playlist(surface, playlist, args)
|
||||
except KeyboardInterrupt:
|
||||
print("\nStopped.")
|
||||
return 0
|
||||
except Exception as exc:
|
||||
print(f"Error: {exc}", file=sys.stderr)
|
||||
print("Try: --spi-bus 1 or --panel --host <pico-ip>", file=sys.stderr)
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
68
examples/fix_spi1_config.py
Executable file
68
examples/fix_spi1_config.py
Executable file
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Ensure dtoverlay=spi1-3cs is under [all] for Pi 5 Model B."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
CONFIG = Path("/boot/firmware/config.txt")
|
||||
OVERLAY = "dtoverlay=spi1-3cs"
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if not CONFIG.exists():
|
||||
print(f"Error: {CONFIG} not found", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
lines = CONFIG.read_text().splitlines()
|
||||
out: list[str] = []
|
||||
in_cm5 = False
|
||||
has_overlay = False
|
||||
inserted = False
|
||||
|
||||
for line in lines:
|
||||
stripped = line.strip()
|
||||
if stripped == "[cm5]":
|
||||
in_cm5 = True
|
||||
elif stripped.startswith("[") and stripped.endswith("]"):
|
||||
if in_cm5 and not inserted and not has_overlay:
|
||||
out.append(OVERLAY)
|
||||
inserted = True
|
||||
in_cm5 = False
|
||||
|
||||
if stripped == "dtparam=spi1=on":
|
||||
continue
|
||||
if stripped == OVERLAY:
|
||||
has_overlay = True
|
||||
if in_cm5:
|
||||
continue
|
||||
out.append(line)
|
||||
continue
|
||||
out.append(line)
|
||||
|
||||
if not has_overlay and not inserted:
|
||||
if out and out[-1].strip():
|
||||
out.append("")
|
||||
out.append(OVERLAY)
|
||||
|
||||
new_text = "\n".join(out) + "\n"
|
||||
if new_text == CONFIG.read_text():
|
||||
print("config.txt already correct.")
|
||||
else:
|
||||
CONFIG.write_text(new_text)
|
||||
print("Updated /boot/firmware/config.txt:")
|
||||
print(f" - removed dtparam=spi1=on (ignored on Pi 5)")
|
||||
print(f" - moved {OVERLAY} out of [cm5] into [all]")
|
||||
print("Reboot for the change to take effect on boot.")
|
||||
|
||||
print(
|
||||
"\nWiring:\n"
|
||||
" strip 0 DIN → GPIO 10 (SPI0 MOSI) /dev/spidev0.0\n"
|
||||
" strip 1 DIN → GPIO 20 (SPI1 MOSI) /dev/spidev1.0"
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
99
examples/led_demo.py
Executable file
99
examples/led_demo.py
Executable file
@@ -0,0 +1,99 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Two strips showing different patterns at the same time."""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
import time
|
||||
|
||||
from leds import LedStrips, board_family, board_model, default_dual_strip_configs, pi5_setup_hint
|
||||
|
||||
|
||||
def _describe(cfg) -> str:
|
||||
if cfg.backend == "pio":
|
||||
return f"GPIO {cfg.pin} → {cfg.device}"
|
||||
if cfg.backend == "ws281x":
|
||||
return f"GPIO {cfg.pin} (channel {cfg.channel})"
|
||||
return f"/dev/spidev{cfg.spi_bus}.{cfg.spi_device}"
|
||||
|
||||
|
||||
def _wheel(pos: int) -> tuple[int, int, int]:
|
||||
pos = pos % 256
|
||||
if pos < 85:
|
||||
return (255 - pos * 3, pos * 3, 0)
|
||||
if pos < 170:
|
||||
pos -= 85
|
||||
return (0, 255 - pos * 3, pos * 3)
|
||||
pos -= 170
|
||||
return (pos * 3, 0, 255 - pos * 3)
|
||||
|
||||
|
||||
def chase(strip, color: tuple[int, int, int], step: int) -> None:
|
||||
count = len(strip)
|
||||
strip.fill((0, 0, 0))
|
||||
strip[step % count] = color
|
||||
|
||||
|
||||
def rainbow(strip, offset: int) -> None:
|
||||
for i in range(len(strip)):
|
||||
strip[i] = _wheel(offset + i * 256 // max(len(strip), 1))
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="WS2812 dual-strip demo")
|
||||
parser.add_argument(
|
||||
"-n",
|
||||
"--count",
|
||||
type=int,
|
||||
nargs=2,
|
||||
default=[200, 200],
|
||||
metavar=("STRIP0", "STRIP1"),
|
||||
help="LED count per strip (default: 10 10)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--brightness",
|
||||
type=float,
|
||||
default=0.25,
|
||||
help="brightness 0.0-1.0 (default: 0.25)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
print(f"Board: {board_model()}")
|
||||
cfg0, cfg1 = default_dual_strip_configs(
|
||||
args.count[0], args.count[1], brightness=args.brightness
|
||||
)
|
||||
print(f"Strip 0: {cfg0.backend} → {_describe(cfg0)} (red chase)")
|
||||
print(f"Strip 1: {cfg1.backend} → {_describe(cfg1)} (rainbow)")
|
||||
|
||||
step = 0
|
||||
try:
|
||||
with LedStrips(args.count[0], args.count[1], brightness=args.brightness) as strips:
|
||||
while True:
|
||||
# strip 0: red dot chasing along the strip
|
||||
chase(strips[0], (255, 0, 0), step)
|
||||
|
||||
# strip 1: rainbow scroll — completely independent
|
||||
rainbow(strips[1], step * 4)
|
||||
|
||||
strips.show()
|
||||
step += 1
|
||||
time.sleep(0.05)
|
||||
except KeyboardInterrupt:
|
||||
print("\nStopped.")
|
||||
return 0
|
||||
except FileNotFoundError as exc:
|
||||
print(f"Error: {exc}", file=sys.stderr)
|
||||
if board_family() == "pi5":
|
||||
print("\n" + pi5_setup_hint(args.count[0], args.count[1]), file=sys.stderr)
|
||||
return 1
|
||||
except OSError as exc:
|
||||
print(f"Error: {exc}", file=sys.stderr)
|
||||
if board_family() == "pi5" and "No such file" in str(exc):
|
||||
print("\n" + pi5_setup_hint(args.count[0], args.count[1]), file=sys.stderr)
|
||||
return 1
|
||||
except Exception as exc:
|
||||
print(f"Error: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
79
examples/matrix_demo.py
Executable file
79
examples/matrix_demo.py
Executable file
@@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Rainbow scroll on the LED matrix (local SPI or Pico panel)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
from leds import ANIMATIONS, LedMatrix
|
||||
from leds.array_config import MATRIX_BRIGHTNESS, MATRIX_HEIGHT, MATRIX_SPI_BUS, MATRIX_SPI_DEVICE, MATRIX_WIDTH
|
||||
from leds.config import spi_strip_config
|
||||
from leds.panel_udp import (
|
||||
add_panel_network_args,
|
||||
format_panel_targets,
|
||||
panel_targets_from_args,
|
||||
run_panel_animation_loop,
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="LED matrix rainbow demo")
|
||||
parser.add_argument("--brightness", type=float, default=MATRIX_BRIGHTNESS)
|
||||
parser.add_argument(
|
||||
"--spi-bus",
|
||||
type=int,
|
||||
default=int(os.environ.get("PORTAL_SPI_BUS", MATRIX_SPI_BUS)),
|
||||
)
|
||||
parser.add_argument("--spi-device", type=int, default=MATRIX_SPI_DEVICE)
|
||||
parser.add_argument("--panel", action="store_true", help="send to Pico panel(s) over UDP")
|
||||
add_panel_network_args(parser)
|
||||
args = parser.parse_args()
|
||||
|
||||
print(f"Matrix: {MATRIX_WIDTH}×{MATRIX_HEIGHT} ({MATRIX_WIDTH * MATRIX_HEIGHT} LEDs)")
|
||||
|
||||
if args.panel:
|
||||
targets = panel_targets_from_args(args)
|
||||
try:
|
||||
run_panel_animation_loop(
|
||||
targets,
|
||||
args.port,
|
||||
"rainbow",
|
||||
brightness=args.brightness,
|
||||
)
|
||||
except KeyboardInterrupt:
|
||||
print("\nStopped.")
|
||||
except OSError as exc:
|
||||
print(f"Network error: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
return 0
|
||||
|
||||
draw = ANIMATIONS["rainbow"]
|
||||
cfg = spi_strip_config(
|
||||
MATRIX_WIDTH * MATRIX_HEIGHT,
|
||||
spi_bus=args.spi_bus,
|
||||
spi_device=args.spi_device,
|
||||
brightness=args.brightness,
|
||||
)
|
||||
|
||||
try:
|
||||
with LedMatrix(config0=cfg) as matrix:
|
||||
frame = 0
|
||||
while True:
|
||||
draw(matrix, frame)
|
||||
matrix.show()
|
||||
frame += 1
|
||||
time.sleep(1 / 30)
|
||||
except KeyboardInterrupt:
|
||||
print("\nStopped.")
|
||||
return 0
|
||||
except Exception as exc:
|
||||
print(f"Error: {exc}", file=sys.stderr)
|
||||
print("Try: --panel --host <pico-ip>", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
71
examples/panel_animations.py
Executable file
71
examples/panel_animations.py
Executable file
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run LED matrix animations on Pico panel(s) over UDP."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
from leds.panel_udp import (
|
||||
add_panel_args,
|
||||
animation_playlist,
|
||||
format_panel_targets,
|
||||
panel_targets_from_args,
|
||||
run_panel_animation_loop,
|
||||
run_panel_playlist,
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
from leds.animations import ANIMATIONS
|
||||
|
||||
names = list(ANIMATIONS.keys())
|
||||
parser = argparse.ArgumentParser(description="Animations for Pico panel(s) over UDP")
|
||||
parser.add_argument(
|
||||
"animation",
|
||||
nargs="?",
|
||||
choices=names + ["all"],
|
||||
default="all",
|
||||
)
|
||||
add_panel_args(parser)
|
||||
parser.add_argument("--duration", type=float, default=12.0)
|
||||
parser.add_argument("--fps", type=float, default=None)
|
||||
parser.add_argument("--once", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
targets = panel_targets_from_args(args)
|
||||
playlist = animation_playlist(args.animation)
|
||||
print(f"Panel animations -> {format_panel_targets(targets, args.port)}")
|
||||
print(f"Playlist: {', '.join(playlist)}")
|
||||
|
||||
try:
|
||||
if len(playlist) == 1 and args.animation != "all":
|
||||
run_panel_animation_loop(
|
||||
targets,
|
||||
args.port,
|
||||
playlist[0],
|
||||
fps=args.fps,
|
||||
brightness=args.brightness,
|
||||
panel_id=args.panel_id,
|
||||
)
|
||||
else:
|
||||
run_panel_playlist(
|
||||
targets,
|
||||
args.port,
|
||||
playlist,
|
||||
duration=args.duration,
|
||||
fps_override=args.fps,
|
||||
brightness=args.brightness,
|
||||
panel_id=args.panel_id,
|
||||
loop=not args.once,
|
||||
)
|
||||
except KeyboardInterrupt:
|
||||
print("\nStopped.")
|
||||
except OSError as exc:
|
||||
print(f"Network error: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
54
examples/panel_color_test.py
Executable file
54
examples/panel_color_test.py
Executable file
@@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Full-panel R / G / B flash for Pico panel(s)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import socket
|
||||
import sys
|
||||
import time
|
||||
|
||||
from leds.panel_udp import (
|
||||
add_panel_args,
|
||||
format_panel_targets,
|
||||
panel_targets_from_args,
|
||||
send_solid_to_targets,
|
||||
)
|
||||
|
||||
COLORS = (
|
||||
("RED", (255, 0, 0)),
|
||||
("GREEN", (0, 255, 0)),
|
||||
("BLUE", (0, 0, 255)),
|
||||
("WHITE", (255, 255, 255)),
|
||||
("OFF", (0, 0, 0)),
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="R/G/B color test for Pico panel(s)")
|
||||
add_panel_args(parser)
|
||||
parser.add_argument("--pause", type=float, default=1.5)
|
||||
args = parser.parse_args()
|
||||
|
||||
targets = panel_targets_from_args(args)
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
print(f"Color test -> {format_panel_targets(targets, args.port)}\n")
|
||||
|
||||
try:
|
||||
for label, color in COLORS:
|
||||
print(f" {label}")
|
||||
send_solid_to_targets(sock, targets, args.port, color, args.brightness, args.panel_id)
|
||||
time.sleep(args.pause)
|
||||
except KeyboardInterrupt:
|
||||
print("\nStopped.")
|
||||
except OSError as exc:
|
||||
print(f"Network error: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
finally:
|
||||
send_solid_to_targets(sock, targets, args.port, (0, 0, 0), 1.0, args.panel_id)
|
||||
sock.close()
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
44
examples/panel_rgb_cycle.py
Executable file
44
examples/panel_rgb_cycle.py
Executable file
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Cycle full-panel red / green / blue on Pico panel(s)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
import time
|
||||
|
||||
from leds.panel_udp import PanelClient, add_panel_args, format_panel_targets, panel_targets_from_args
|
||||
|
||||
COLORS = (
|
||||
("RED", (255, 0, 0)),
|
||||
("GREEN", (0, 255, 0)),
|
||||
("BLUE", (0, 0, 255)),
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="RGB cycle on Pico panel(s)")
|
||||
add_panel_args(parser)
|
||||
parser.add_argument("--pause", type=float, default=1.0)
|
||||
args = parser.parse_args()
|
||||
|
||||
targets = panel_targets_from_args(args)
|
||||
print(f"RGB cycle -> {format_panel_targets(targets, args.port)} (Ctrl+C to stop)")
|
||||
|
||||
try:
|
||||
with PanelClient.from_args(args) as client:
|
||||
while True:
|
||||
for label, color in COLORS:
|
||||
print(f" {label}")
|
||||
client.fill(color)
|
||||
time.sleep(args.pause)
|
||||
except KeyboardInterrupt:
|
||||
print("\nStopped.")
|
||||
except OSError as exc:
|
||||
print(f"Network error: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
361
examples/panel_sync_test.py
Normal file
361
examples/panel_sync_test.py
Normal file
@@ -0,0 +1,361 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Positioning and sync test for multiple Pico panels.
|
||||
|
||||
Runs a sequence of patterns on all panels (or one via --panel-index):
|
||||
identify - panel index digit on each board (check physical mapping)
|
||||
corners - red/green/blue/yellow corners (check orientation)
|
||||
crosshair - center row + column
|
||||
columns - sweep the same column index on every panel together
|
||||
rows - sweep rows together
|
||||
flash - simultaneous white flashes (check sync)
|
||||
pulse - in-sync brightness pulse on all panels
|
||||
width - sweep columns near right edge (find 38 vs 39 vs 40)
|
||||
all - full sequence (default)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import math
|
||||
import socket
|
||||
import sys
|
||||
import time
|
||||
|
||||
from leds.panel_udp import (
|
||||
HeadlessMatrix,
|
||||
PanelTarget,
|
||||
add_panel_args,
|
||||
format_panel_targets,
|
||||
panel_targets_from_args,
|
||||
send_frame,
|
||||
)
|
||||
from leds.text import draw_text_centered
|
||||
|
||||
PANEL_COLORS = (
|
||||
(255, 80, 80),
|
||||
(80, 255, 80),
|
||||
(80, 160, 255),
|
||||
(255, 200, 80),
|
||||
(200, 80, 255),
|
||||
)
|
||||
|
||||
CORNER_COLORS = {
|
||||
"top-left": (255, 0, 0),
|
||||
"top-right": (0, 255, 0),
|
||||
"bottom-left": (0, 0, 255),
|
||||
"bottom-right": (255, 255, 0),
|
||||
}
|
||||
|
||||
|
||||
def _matrices(targets: list[PanelTarget], brightness: float) -> list[tuple[PanelTarget, HeadlessMatrix]]:
|
||||
return [(t, HeadlessMatrix(t.width, t.height, brightness=brightness)) for t in targets]
|
||||
|
||||
|
||||
def _push(
|
||||
sock: socket.socket,
|
||||
targets: list[PanelTarget],
|
||||
matrices: list[tuple[PanelTarget, HeadlessMatrix]],
|
||||
port: int,
|
||||
panel_id: int | None,
|
||||
) -> None:
|
||||
"""Send all panel frames in a tight burst for best sync."""
|
||||
for target, matrix in matrices:
|
||||
send_frame(sock, target.host, port, matrix.rgb_bytes(), panel_id)
|
||||
|
||||
|
||||
def _clear_all(
|
||||
sock: socket.socket,
|
||||
targets: list[PanelTarget],
|
||||
port: int,
|
||||
brightness: float,
|
||||
panel_id: int | None,
|
||||
) -> None:
|
||||
matrices = _matrices(targets, brightness)
|
||||
for _, matrix in matrices:
|
||||
matrix.clear()
|
||||
_push(sock, targets, matrices, port, panel_id)
|
||||
|
||||
|
||||
def test_identify(
|
||||
sock: socket.socket,
|
||||
targets: list[PanelTarget],
|
||||
port: int,
|
||||
brightness: float,
|
||||
panel_id: int | None,
|
||||
pause: float,
|
||||
) -> None:
|
||||
print("Test: identify — each panel shows its index (0–4)")
|
||||
matrices = _matrices(targets, brightness)
|
||||
for target, matrix in matrices:
|
||||
matrix.clear()
|
||||
color = PANEL_COLORS[target.index % len(PANEL_COLORS)]
|
||||
draw_text_centered(matrix, str(target.index), color)
|
||||
print(f" panel {target.index} -> {target.host} ({target.width}×{target.height})")
|
||||
_push(sock, targets, matrices, port, panel_id)
|
||||
time.sleep(pause)
|
||||
|
||||
|
||||
def test_corners(
|
||||
sock: socket.socket,
|
||||
targets: list[PanelTarget],
|
||||
port: int,
|
||||
brightness: float,
|
||||
panel_id: int | None,
|
||||
pause: float,
|
||||
) -> None:
|
||||
print("Test: corners — TL=red TR=green BL=blue BR=yellow (all panels)")
|
||||
matrices = _matrices(targets, brightness)
|
||||
for target, matrix in matrices:
|
||||
w, h = target.width, target.height
|
||||
matrix.clear()
|
||||
points = {
|
||||
"top-left": (0, 0),
|
||||
"top-right": (w - 1, 0),
|
||||
"bottom-left": (0, h - 1),
|
||||
"bottom-right": (w - 1, h - 1),
|
||||
}
|
||||
for name, (x, y) in points.items():
|
||||
matrix[x, y] = CORNER_COLORS[name]
|
||||
_push(sock, targets, matrices, port, panel_id)
|
||||
time.sleep(pause)
|
||||
|
||||
|
||||
def test_crosshair(
|
||||
sock: socket.socket,
|
||||
targets: list[PanelTarget],
|
||||
port: int,
|
||||
brightness: float,
|
||||
panel_id: int | None,
|
||||
pause: float,
|
||||
) -> None:
|
||||
print("Test: crosshair — center row + column (white)")
|
||||
matrices = _matrices(targets, brightness)
|
||||
for target, matrix in matrices:
|
||||
w, h = target.width, target.height
|
||||
matrix.clear()
|
||||
mid_y = h // 2
|
||||
mid_x = w // 2
|
||||
for x in range(w):
|
||||
matrix[x, mid_y] = (255, 255, 255)
|
||||
for y in range(h):
|
||||
matrix[mid_x, y] = (200, 200, 200)
|
||||
_push(sock, targets, matrices, port, panel_id)
|
||||
time.sleep(pause)
|
||||
|
||||
|
||||
def test_columns(
|
||||
sock: socket.socket,
|
||||
targets: list[PanelTarget],
|
||||
port: int,
|
||||
brightness: float,
|
||||
panel_id: int | None,
|
||||
pause: float,
|
||||
) -> None:
|
||||
max_width = max(t.width for t in targets)
|
||||
print(f"Test: column sweep — x=0..{max_width - 1} on all panels together")
|
||||
for x in range(max_width):
|
||||
matrices = _matrices(targets, brightness)
|
||||
for target, matrix in matrices:
|
||||
matrix.clear()
|
||||
if x >= target.width:
|
||||
continue
|
||||
hue = int(255 * x / max(max_width - 1, 1))
|
||||
for y in range(target.height):
|
||||
matrix[x, y] = (hue, 80, 255 - hue)
|
||||
_push(sock, targets, matrices, port, panel_id)
|
||||
time.sleep(pause)
|
||||
_clear_all(sock, targets, port, brightness, panel_id)
|
||||
|
||||
|
||||
def test_rows(
|
||||
sock: socket.socket,
|
||||
targets: list[PanelTarget],
|
||||
port: int,
|
||||
brightness: float,
|
||||
panel_id: int | None,
|
||||
pause: float,
|
||||
) -> None:
|
||||
height = targets[0].height if targets else 9
|
||||
print(f"Test: row sweep — y=0..{height - 1} on all panels together")
|
||||
for y in range(height):
|
||||
matrices = _matrices(targets, brightness)
|
||||
for target, matrix in matrices:
|
||||
matrix.clear()
|
||||
for x in range(target.width):
|
||||
matrix[x, y] = (255, 255, 255)
|
||||
_push(sock, targets, matrices, port, panel_id)
|
||||
time.sleep(pause)
|
||||
_clear_all(sock, targets, port, brightness, panel_id)
|
||||
|
||||
|
||||
def test_flash(
|
||||
sock: socket.socket,
|
||||
targets: list[PanelTarget],
|
||||
port: int,
|
||||
brightness: float,
|
||||
panel_id: int | None,
|
||||
pause: float,
|
||||
count: int,
|
||||
) -> None:
|
||||
print(f"Test: sync flash — {count} white flashes on all panels")
|
||||
for n in range(count):
|
||||
matrices = _matrices(targets, brightness)
|
||||
for _, matrix in matrices:
|
||||
matrix.fill((255, 255, 255))
|
||||
_push(sock, targets, matrices, port, panel_id)
|
||||
time.sleep(pause)
|
||||
_clear_all(sock, targets, port, brightness, panel_id)
|
||||
time.sleep(pause / 2)
|
||||
print(f" flash {n + 1}/{count}")
|
||||
|
||||
|
||||
def test_pulse(
|
||||
sock: socket.socket,
|
||||
targets: list[PanelTarget],
|
||||
port: int,
|
||||
brightness: float,
|
||||
panel_id: int | None,
|
||||
seconds: float,
|
||||
fps: float,
|
||||
) -> None:
|
||||
print(f"Test: sync pulse — {seconds:.0f}s solid red fade (all panels)")
|
||||
frames = max(int(seconds * fps), 1)
|
||||
for frame in range(frames):
|
||||
t = frame / frames
|
||||
level = 0.15 + 0.85 * (0.5 - 0.5 * math.cos(t * 6.28318))
|
||||
color = (int(255 * level), 0, 0)
|
||||
matrices = _matrices(targets, min(brightness * level, 1.0))
|
||||
for _, matrix in matrices:
|
||||
matrix.fill(color)
|
||||
_push(sock, targets, matrices, port, panel_id)
|
||||
time.sleep(1.0 / fps)
|
||||
_clear_all(sock, targets, port, brightness, panel_id)
|
||||
|
||||
|
||||
def test_chain_ends(
|
||||
sock: socket.socket,
|
||||
targets: list[PanelTarget],
|
||||
port: int,
|
||||
brightness: float,
|
||||
panel_id: int | None,
|
||||
pause: float,
|
||||
) -> None:
|
||||
print("Test: chain ends — LED 0=red, last LED=green (per panel)")
|
||||
matrices = _matrices(targets, brightness)
|
||||
for target, matrix in matrices:
|
||||
matrix.clear()
|
||||
matrix[0, 0] = (255, 0, 0)
|
||||
last = target.pixels - 1
|
||||
x = last % target.width
|
||||
y = last // target.width
|
||||
matrix[x, y] = (0, 255, 0)
|
||||
print(f" panel {target.index}: index 0 and {last} ({x},{y})")
|
||||
_push(sock, targets, matrices, port, panel_id)
|
||||
time.sleep(pause)
|
||||
|
||||
|
||||
def test_width(
|
||||
sock: socket.socket,
|
||||
targets: list[PanelTarget],
|
||||
port: int,
|
||||
brightness: float,
|
||||
panel_id: int | None,
|
||||
pause: float,
|
||||
col_start: int,
|
||||
col_end: int,
|
||||
) -> None:
|
||||
probe_w = col_end + 1
|
||||
probe_leds = probe_w * targets[0].height if targets else probe_w * 9
|
||||
print("Test: width — one full column at a time (right-edge probe)")
|
||||
print(f" Probing columns {col_start}–{col_end} (sending {probe_w}×9 = {probe_leds} LEDs)")
|
||||
print(" Flash firmware with at least NUM_LEDS={} first, e.g.:".format(probe_leds))
|
||||
print(f" make deploy PANEL_ID=<n> NUM_LEDS={probe_leds}")
|
||||
print(" The last column that lights a real LED → width = column + 1")
|
||||
print(" (column 37 lit → width 38; column 38 lit → width 39)\n")
|
||||
for x in range(col_start, col_end + 1):
|
||||
burst: list[tuple[PanelTarget, HeadlessMatrix]] = []
|
||||
for target in targets:
|
||||
matrix = HeadlessMatrix(probe_w, target.height, brightness=brightness)
|
||||
matrix.clear()
|
||||
for y in range(target.height):
|
||||
matrix[x, y] = (255, 255, 255)
|
||||
probe_target = PanelTarget(target.index, target.host, probe_w, target.height)
|
||||
burst.append((probe_target, matrix))
|
||||
for probe_target, matrix in burst:
|
||||
send_frame(sock, probe_target.host, port, matrix.rgb_bytes(), panel_id)
|
||||
print(f" column {x} → width {x + 1} if this is the last real column")
|
||||
time.sleep(pause)
|
||||
_clear_all(sock, targets, port, brightness, panel_id)
|
||||
|
||||
|
||||
TESTS = {
|
||||
"identify": test_identify,
|
||||
"corners": test_corners,
|
||||
"crosshair": test_crosshair,
|
||||
"columns": test_columns,
|
||||
"rows": test_rows,
|
||||
"flash": test_flash,
|
||||
"pulse": test_pulse,
|
||||
"ends": test_chain_ends,
|
||||
"width": test_width,
|
||||
}
|
||||
|
||||
ALL_ORDER = ("identify", "corners", "ends", "crosshair", "columns", "rows", "flash", "pulse")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Multi-panel positioning and sync test")
|
||||
add_panel_args(parser)
|
||||
parser.add_argument(
|
||||
"--test",
|
||||
choices=[*TESTS.keys(), "all"],
|
||||
default="all",
|
||||
help="which test to run (default: full sequence)",
|
||||
)
|
||||
parser.add_argument("--pause", type=float, default=1.2, help="hold time per static pattern")
|
||||
parser.add_argument("--column-pause", type=float, default=0.12, help="delay per column in sweep")
|
||||
parser.add_argument("--row-pause", type=float, default=0.25, help="delay per row in sweep")
|
||||
parser.add_argument("--flashes", type=int, default=3, help="count for sync flash test")
|
||||
parser.add_argument("--pulse-seconds", type=float, default=4.0)
|
||||
parser.add_argument("--fps", type=float, default=30.0)
|
||||
parser.add_argument("--col-start", type=int, default=32, help="width test: first column")
|
||||
parser.add_argument("--col-end", type=int, default=42, help="width test: last column")
|
||||
args = parser.parse_args()
|
||||
|
||||
targets = panel_targets_from_args(args)
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
print(f"Panel sync test -> {format_panel_targets(targets, args.port)}")
|
||||
print("Ctrl+C to stop\n")
|
||||
|
||||
tests = ALL_ORDER if args.test == "all" else (args.test,)
|
||||
|
||||
try:
|
||||
for name in tests:
|
||||
fn = TESTS[name]
|
||||
if name == "flash":
|
||||
fn(sock, targets, args.port, args.brightness, args.panel_id, args.pause, args.flashes)
|
||||
elif name == "pulse":
|
||||
fn(sock, targets, args.port, args.brightness, args.panel_id, args.pulse_seconds, args.fps)
|
||||
elif name == "columns":
|
||||
fn(sock, targets, args.port, args.brightness, args.panel_id, args.column_pause)
|
||||
elif name == "rows":
|
||||
fn(sock, targets, args.port, args.brightness, args.panel_id, args.row_pause)
|
||||
elif name == "width":
|
||||
fn(sock, targets, args.port, args.brightness, args.panel_id, args.pause, args.col_start, args.col_end)
|
||||
else:
|
||||
fn(sock, targets, args.port, args.brightness, args.panel_id, args.pause)
|
||||
print()
|
||||
print("Done.")
|
||||
except KeyboardInterrupt:
|
||||
print("\nStopped.")
|
||||
except OSError as exc:
|
||||
print(f"Network error: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
finally:
|
||||
_clear_all(sock, targets, args.port, args.brightness, args.panel_id)
|
||||
sock.close()
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
145
examples/panel_test.py
Executable file
145
examples/panel_test.py
Executable file
@@ -0,0 +1,145 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Step-by-step drive test for Pico panel firmware over UDP."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
import time
|
||||
|
||||
from leds.array_config import MATRIX_HEIGHT, MATRIX_PIXELS, MATRIX_WIDTH, matrix_pixel_index
|
||||
from leds.panel_udp import PanelClient, add_panel_args, format_panel_targets, panel_targets_from_args
|
||||
|
||||
|
||||
def test_flash(client: PanelClient, pause: float) -> None:
|
||||
print("Test: RGB flash (red, green, blue, off)")
|
||||
for name, color in (("red", (255, 0, 0)), ("green", (0, 255, 0)), ("blue", (0, 0, 255))):
|
||||
print(f" {name}")
|
||||
client.fill(color)
|
||||
time.sleep(pause)
|
||||
client.clear()
|
||||
time.sleep(pause / 2)
|
||||
|
||||
|
||||
def test_corners(client: PanelClient, pause: float) -> None:
|
||||
target = client.targets[0]
|
||||
width, height = target.width, target.height
|
||||
print("Test: matrix corners (left → right rows)")
|
||||
corners = [
|
||||
("top-left", matrix_pixel_index(0, 0, width, height), (255, 0, 0)),
|
||||
("top-right", matrix_pixel_index(width - 1, 0, width, height), (0, 255, 0)),
|
||||
("bottom-left", matrix_pixel_index(0, height - 1, width, height), (0, 0, 255)),
|
||||
("bottom-right", matrix_pixel_index(width - 1, height - 1, width, height), (255, 255, 0)),
|
||||
]
|
||||
for label, index, color in corners:
|
||||
if index >= client.pixel_count:
|
||||
print(f" skip {label} (index {index})")
|
||||
continue
|
||||
print(f" {label} -> LED {index}")
|
||||
client.set_indices([index], color)
|
||||
time.sleep(pause)
|
||||
client.clear()
|
||||
time.sleep(pause / 2)
|
||||
|
||||
|
||||
def test_rows(client: PanelClient, pause: float) -> None:
|
||||
target = client.targets[0]
|
||||
width, height = target.width, target.height
|
||||
print("Test: one row at a time (white)")
|
||||
width = min(width, client.pixel_count)
|
||||
rows = min(height, (client.pixel_count + width - 1) // width)
|
||||
for y in range(rows):
|
||||
indices = [matrix_pixel_index(x, y, width, height) for x in range(width)]
|
||||
indices = [i for i in indices if i < client.pixel_count]
|
||||
print(f" row {y} ({len(indices)} LEDs)")
|
||||
client.set_indices(indices, (255, 255, 255))
|
||||
time.sleep(pause)
|
||||
client.clear()
|
||||
time.sleep(pause / 2)
|
||||
|
||||
|
||||
def test_chase(client: PanelClient, pause: float, steps: int | None) -> None:
|
||||
count = steps if steps is not None else min(client.pixel_count, 60)
|
||||
print(f"Test: chase ({count} steps, cyan)")
|
||||
for i in range(count):
|
||||
client.set_indices([i % client.pixel_count], (0, 255, 255))
|
||||
time.sleep(pause)
|
||||
client.clear()
|
||||
time.sleep(pause / 2)
|
||||
|
||||
|
||||
def test_first_last(client: PanelClient, pause: float) -> None:
|
||||
print("Test: first and last LED in chain")
|
||||
last = client.pixel_count - 1
|
||||
print(f" LED 0 = red, LED {last} = green")
|
||||
pixels = [(0, 0, 0)] * client.pixel_count
|
||||
pixels[0] = (255, 0, 0)
|
||||
pixels[last] = (0, 255, 0)
|
||||
client.send_pixels(pixels)
|
||||
time.sleep(pause)
|
||||
client.clear()
|
||||
time.sleep(pause / 2)
|
||||
|
||||
|
||||
TESTS = {
|
||||
"flash": lambda c, p, _s: test_flash(c, p),
|
||||
"first-last": lambda c, p, _s: test_first_last(c, p),
|
||||
"corners": lambda c, p, _s: test_corners(c, p),
|
||||
"rows": lambda c, p, _s: test_rows(c, p),
|
||||
"chase": test_chase,
|
||||
}
|
||||
|
||||
|
||||
def run_all(client: PanelClient, pause: float, chase_steps: int | None) -> None:
|
||||
test_flash(client, pause)
|
||||
test_first_last(client, pause)
|
||||
if client.pixel_count >= client.targets[0].width:
|
||||
test_corners(client, pause)
|
||||
test_rows(client, max(pause * 0.6, 0.15))
|
||||
test_chase(client, max(pause * 0.25, 0.05), chase_steps)
|
||||
client.clear()
|
||||
print("Done.")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Drive-test Pico panel LEDs over UDP")
|
||||
add_panel_args(parser)
|
||||
parser.add_argument("--pause", type=float, default=0.8)
|
||||
parser.add_argument("--test", choices=[*TESTS.keys(), "all"], default="all")
|
||||
parser.add_argument("--chase-steps", type=int, default=None)
|
||||
parser.add_argument("--color", default=None, metavar="R,G,B")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.count is not None and args.count < 1:
|
||||
print("Error: --count must be at least 1", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
targets = panel_targets_from_args(args)
|
||||
print(f"Panel drive test -> {format_panel_targets(targets, args.port)}")
|
||||
|
||||
try:
|
||||
with PanelClient.from_args(args) as client:
|
||||
if args.color:
|
||||
parts = [int(p.strip()) for p in args.color.split(",")]
|
||||
if len(parts) != 3:
|
||||
raise SystemExit("--color must be R,G,B")
|
||||
client.fill((parts[0], parts[1], parts[2]))
|
||||
print(f"Sent solid color {tuple(parts)}")
|
||||
return 0
|
||||
|
||||
if args.test == "all":
|
||||
run_all(client, args.pause, args.chase_steps)
|
||||
else:
|
||||
TESTS[args.test](client, args.pause, args.chase_steps)
|
||||
client.clear()
|
||||
print("Done.")
|
||||
except KeyboardInterrupt:
|
||||
print("\nStopped.")
|
||||
except OSError as exc:
|
||||
print(f"Network error: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
101
examples/panel_text.py
Executable file
101
examples/panel_text.py
Executable file
@@ -0,0 +1,101 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Display text on Pico panel(s) over UDP."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
from leds.panel_udp import (
|
||||
HeadlessMatrix,
|
||||
add_panel_args,
|
||||
format_panel_targets,
|
||||
panel_targets_from_args,
|
||||
send_frame,
|
||||
)
|
||||
from leds.text import draw_text, draw_text_centered
|
||||
|
||||
DEFAULT_NAME = "JIMMY"
|
||||
|
||||
|
||||
def _default_name() -> str:
|
||||
try:
|
||||
out = subprocess.run(
|
||||
["git", "config", "user.name"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
name = out.stdout.strip()
|
||||
if name:
|
||||
return name.upper()
|
||||
except OSError:
|
||||
pass
|
||||
return DEFAULT_NAME
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Display text on LED panel(s)")
|
||||
parser.add_argument("text", nargs="?", default=None)
|
||||
add_panel_args(parser)
|
||||
parser.add_argument("--color", default="255,200,80", metavar="R,G,B")
|
||||
parser.add_argument("--hold", type=float, default=0.0, help="0 = show until Ctrl+C")
|
||||
parser.add_argument("--scroll", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
text = (args.text or _default_name()).upper()
|
||||
parts = [int(p.strip()) for p in args.color.split(",")]
|
||||
if len(parts) != 3:
|
||||
print("Error: --color must be R,G,B", file=sys.stderr)
|
||||
return 1
|
||||
color = (parts[0], parts[1], parts[2])
|
||||
|
||||
targets = panel_targets_from_args(args)
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
print(f"Displaying '{text}' on {format_panel_targets(targets, args.port)}")
|
||||
|
||||
try:
|
||||
matrices = [
|
||||
(target, HeadlessMatrix(target.width, target.height, brightness=args.brightness))
|
||||
for target in targets
|
||||
]
|
||||
if args.scroll:
|
||||
start = time.monotonic()
|
||||
offsets = {target.index: matrix.width for target, matrix in matrices}
|
||||
while True:
|
||||
for target, matrix in matrices:
|
||||
matrix.clear()
|
||||
draw_text(matrix, text, offsets[target.index], 1, color)
|
||||
send_frame(sock, target.host, args.port, matrix.rgb_bytes(), args.panel_id)
|
||||
offsets[target.index] -= 1
|
||||
if offsets[target.index] < -len(text) * 6:
|
||||
offsets[target.index] = matrix.width
|
||||
time.sleep(0.08)
|
||||
if args.hold > 0 and time.monotonic() - start >= args.hold:
|
||||
break
|
||||
else:
|
||||
for target, matrix in matrices:
|
||||
matrix.clear()
|
||||
draw_text_centered(matrix, text, color)
|
||||
send_frame(sock, target.host, args.port, matrix.rgb_bytes(), args.panel_id)
|
||||
if args.hold <= 0:
|
||||
print("Showing (Ctrl+C to clear)")
|
||||
while True:
|
||||
time.sleep(1)
|
||||
else:
|
||||
time.sleep(args.hold)
|
||||
except KeyboardInterrupt:
|
||||
print("\nClearing.")
|
||||
finally:
|
||||
for target, matrix in matrices:
|
||||
matrix.clear()
|
||||
send_frame(sock, target.host, args.port, matrix.rgb_bytes(), args.panel_id)
|
||||
sock.close()
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
48
examples/panel_udp_send.py
Executable file
48
examples/panel_udp_send.py
Executable file
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Send one animation to Pico panel(s) over UDP."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
from leds.animations import ANIMATIONS, DEFAULT_FPS
|
||||
from leds.panel_udp import (
|
||||
add_panel_args,
|
||||
format_panel_targets,
|
||||
panel_targets_from_args,
|
||||
run_panel_animation_loop,
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="UDP animation sender for Pico panel(s)")
|
||||
add_panel_args(parser)
|
||||
parser.add_argument("--animation", default="rainbow", choices=sorted(ANIMATIONS))
|
||||
parser.add_argument("--fps", type=float, default=None)
|
||||
parser.add_argument("--brightness", type=float, default=0.25)
|
||||
args = parser.parse_args()
|
||||
|
||||
targets = panel_targets_from_args(args)
|
||||
fps = args.fps or DEFAULT_FPS.get(args.animation, 30)
|
||||
print(f"Sending {args.animation} to {format_panel_targets(targets, args.port)} @ {fps:.0f} fps")
|
||||
|
||||
try:
|
||||
run_panel_animation_loop(
|
||||
targets,
|
||||
args.port,
|
||||
args.animation,
|
||||
fps=args.fps,
|
||||
brightness=args.brightness,
|
||||
panel_id=args.panel_id,
|
||||
)
|
||||
except KeyboardInterrupt:
|
||||
print("\nStopped.")
|
||||
except OSError as exc:
|
||||
print(f"Network error: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
69
examples/setup_pi5_leds.py
Executable file
69
examples/setup_pi5_leds.py
Executable file
@@ -0,0 +1,69 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Print or install Pi 5 ws2812-pio overlay lines for two strips."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
CONFIG = Path("/boot/firmware/config.txt")
|
||||
MARKER = "# portal ws2812 strips"
|
||||
|
||||
|
||||
def overlay_lines(count0: int, count1: int, gpio0: int, gpio1: int) -> list[str]:
|
||||
return [
|
||||
MARKER,
|
||||
f"dtoverlay=ws2812-pio,gpio={gpio0},num_leds={count0}",
|
||||
f"dtoverlay=ws2812-pio,gpio={gpio1},num_leds={count1}",
|
||||
]
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Configure Pi 5 ws2812-pio overlays for two LED strips"
|
||||
)
|
||||
parser.add_argument("-n", type=int, nargs=2, default=[10, 10], metavar=("STRIP0", "STRIP1"))
|
||||
parser.add_argument("--gpio0", type=int, default=18, help="strip 0 data pin (default 18)")
|
||||
parser.add_argument("--gpio1", type=int, default=13, help="strip 1 data pin (default 13)")
|
||||
parser.add_argument(
|
||||
"--install",
|
||||
action="store_true",
|
||||
help="append overlay lines to /boot/firmware/config.txt (requires sudo)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
lines = overlay_lines(args.n[0], args.n[1], args.gpio0, args.gpio1)
|
||||
|
||||
print("Add these lines to /boot/firmware/config.txt under [all], then reboot:\n")
|
||||
for line in lines:
|
||||
print(line)
|
||||
print(
|
||||
f"\nWiring: strip 0 DIN → GPIO {args.gpio0}, strip 1 DIN → GPIO {args.gpio1}, "
|
||||
"GND → Pi GND, 5V from external supply for long strips."
|
||||
)
|
||||
print("\nAfter reboot you should see /dev/leds0 and /dev/leds1.")
|
||||
|
||||
if not args.install:
|
||||
return 0
|
||||
|
||||
if not CONFIG.exists():
|
||||
print(f"Error: {CONFIG} not found", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
text = CONFIG.read_text()
|
||||
if MARKER in text:
|
||||
print("\nOverlay lines already present in config.txt.")
|
||||
return 0
|
||||
|
||||
with CONFIG.open("a") as f:
|
||||
f.write("\n")
|
||||
for line in lines:
|
||||
f.write(line + "\n")
|
||||
|
||||
print(f"\nAppended to {CONFIG}. Reboot to apply.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user