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>
This commit is contained in:
@@ -23,6 +23,7 @@ from leds.panel_udp import (
|
||||
animation_playlist,
|
||||
format_panel_targets,
|
||||
panel_targets_from_args,
|
||||
parse_pins_arg,
|
||||
run_panel_animation_loop,
|
||||
run_panel_playlist,
|
||||
)
|
||||
@@ -102,6 +103,9 @@ def main() -> int:
|
||||
fps=args.fps,
|
||||
brightness=args.brightness,
|
||||
panel_id=args.panel_id,
|
||||
sync_send=args.sync_send,
|
||||
pin=args.pin,
|
||||
pins=parse_pins_arg(args),
|
||||
)
|
||||
else:
|
||||
run_panel_playlist(
|
||||
@@ -113,6 +117,9 @@ def main() -> int:
|
||||
brightness=args.brightness,
|
||||
panel_id=args.panel_id,
|
||||
loop=not args.once,
|
||||
sync_send=args.sync_send,
|
||||
pin=args.pin,
|
||||
pins=parse_pins_arg(args),
|
||||
)
|
||||
except KeyboardInterrupt:
|
||||
print("\nStopped.")
|
||||
@@ -138,7 +145,7 @@ def main() -> int:
|
||||
if args.linear:
|
||||
with LedStrip(
|
||||
count,
|
||||
backend="spi",
|
||||
backend=strip_cfg.backend,
|
||||
spi_bus=args.spi_bus,
|
||||
spi_device=args.spi_device,
|
||||
brightness=args.brightness,
|
||||
|
||||
@@ -15,6 +15,7 @@ from leds.panel_udp import (
|
||||
add_panel_network_args,
|
||||
format_panel_targets,
|
||||
panel_targets_from_args,
|
||||
parse_pins_arg,
|
||||
run_panel_animation_loop,
|
||||
)
|
||||
|
||||
@@ -36,12 +37,16 @@ def main() -> int:
|
||||
|
||||
if args.panel:
|
||||
targets = panel_targets_from_args(args)
|
||||
print(f"Panel -> {format_panel_targets(targets, args.port)}")
|
||||
try:
|
||||
run_panel_animation_loop(
|
||||
targets,
|
||||
args.port,
|
||||
"rainbow",
|
||||
brightness=args.brightness,
|
||||
panel_id=args.panel_id,
|
||||
pin=args.pin,
|
||||
pins=parse_pins_arg(args),
|
||||
)
|
||||
except KeyboardInterrupt:
|
||||
print("\nStopped.")
|
||||
|
||||
@@ -11,6 +11,7 @@ from leds.panel_udp import (
|
||||
animation_playlist,
|
||||
format_panel_targets,
|
||||
panel_targets_from_args,
|
||||
parse_pins_arg,
|
||||
run_panel_animation_loop,
|
||||
run_panel_playlist,
|
||||
)
|
||||
@@ -47,6 +48,9 @@ def main() -> int:
|
||||
fps=args.fps,
|
||||
brightness=args.brightness,
|
||||
panel_id=args.panel_id,
|
||||
sync_send=args.sync_send,
|
||||
pin=args.pin,
|
||||
pins=parse_pins_arg(args),
|
||||
)
|
||||
else:
|
||||
run_panel_playlist(
|
||||
@@ -58,6 +62,9 @@ def main() -> int:
|
||||
brightness=args.brightness,
|
||||
panel_id=args.panel_id,
|
||||
loop=not args.once,
|
||||
sync_send=args.sync_send,
|
||||
pin=args.pin,
|
||||
pins=parse_pins_arg(args),
|
||||
)
|
||||
except KeyboardInterrupt:
|
||||
print("\nStopped.")
|
||||
|
||||
@@ -8,19 +8,21 @@ import socket
|
||||
import sys
|
||||
import time
|
||||
|
||||
from leds.colors import BLUE, GREEN, OFF, RED, WHITE
|
||||
from leds.panel_udp import (
|
||||
add_panel_args,
|
||||
apply_pin_arg,
|
||||
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)),
|
||||
("RED", RED),
|
||||
("GREEN", GREEN),
|
||||
("BLUE", BLUE),
|
||||
("WHITE", WHITE),
|
||||
("OFF", OFF),
|
||||
)
|
||||
|
||||
|
||||
@@ -31,7 +33,9 @@ def main() -> int:
|
||||
args = parser.parse_args()
|
||||
|
||||
targets = panel_targets_from_args(args)
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
has_udp = any(not t.local for t in targets)
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) if has_udp else None
|
||||
targets = apply_pin_arg(args, sock, targets)
|
||||
print(f"Color test -> {format_panel_targets(targets, args.port)}\n")
|
||||
|
||||
try:
|
||||
@@ -46,7 +50,8 @@ def main() -> int:
|
||||
return 1
|
||||
finally:
|
||||
send_solid_to_targets(sock, targets, args.port, (0, 0, 0), 1.0, args.panel_id)
|
||||
sock.close()
|
||||
if sock is not None:
|
||||
sock.close()
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
@@ -7,14 +7,9 @@ import argparse
|
||||
import sys
|
||||
import time
|
||||
|
||||
from leds.colors import RGB_CYCLE
|
||||
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)")
|
||||
@@ -28,7 +23,7 @@ def main() -> int:
|
||||
try:
|
||||
with PanelClient.from_args(args) as client:
|
||||
while True:
|
||||
for label, color in COLORS:
|
||||
for label, color in RGB_CYCLE:
|
||||
print(f" {label}")
|
||||
client.fill(color)
|
||||
time.sleep(args.pause)
|
||||
|
||||
9
examples/panel_spi_bridge.py
Normal file
9
examples/panel_spi_bridge.py
Normal file
@@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run a UDP-to-SPI bridge so a Pi-connected panel looks like a Pico adapter."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from leds.panel_bridge import main
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -1,8 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Positioning and sync test for multiple Pico panels.
|
||||
"""Positioning and sync test for the hexagonal portal panels.
|
||||
|
||||
Runs a sequence of patterns on all panels (or one via --panel-index):
|
||||
identify - panel index digit on each board (check physical mapping)
|
||||
identify - panel index digit on each face (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
|
||||
@@ -23,12 +23,15 @@ import time
|
||||
|
||||
from leds.panel_udp import (
|
||||
HeadlessMatrix,
|
||||
PanelMatrixBundle,
|
||||
PanelTarget,
|
||||
add_panel_args,
|
||||
apply_pin_arg,
|
||||
format_panel_targets,
|
||||
panel_targets_from_args,
|
||||
send_frame,
|
||||
push_panel_frames,
|
||||
)
|
||||
from leds.array_config import panel_label
|
||||
from leds.text import draw_text_centered
|
||||
|
||||
PANEL_COLORS = (
|
||||
@@ -47,37 +50,35 @@ CORNER_COLORS = {
|
||||
}
|
||||
|
||||
|
||||
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 _matrices(targets: list[PanelTarget], brightness: float) -> PanelMatrixBundle:
|
||||
return PanelMatrixBundle(targets, brightness)
|
||||
|
||||
|
||||
def _push(
|
||||
sock: socket.socket,
|
||||
targets: list[PanelTarget],
|
||||
matrices: list[tuple[PanelTarget, HeadlessMatrix]],
|
||||
sock: socket.socket | None,
|
||||
bundle: PanelMatrixBundle,
|
||||
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)
|
||||
push_panel_frames(sock, bundle.matrices, port, panel_id)
|
||||
|
||||
|
||||
def _clear_all(
|
||||
sock: socket.socket,
|
||||
sock: socket.socket | None,
|
||||
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)
|
||||
with PanelMatrixBundle(targets, brightness) as bundle:
|
||||
for _, matrix in bundle.matrices:
|
||||
matrix.clear()
|
||||
_push(sock, bundle, port, panel_id)
|
||||
|
||||
|
||||
def test_identify(
|
||||
sock: socket.socket,
|
||||
sock: socket.socket | None,
|
||||
targets: list[PanelTarget],
|
||||
port: int,
|
||||
brightness: float,
|
||||
@@ -85,18 +86,18 @@ def test_identify(
|
||||
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)
|
||||
with _matrices(targets, brightness) as bundle:
|
||||
for target, matrix in bundle.matrices:
|
||||
matrix.clear()
|
||||
color = PANEL_COLORS[target.index % len(PANEL_COLORS)]
|
||||
draw_text_centered(matrix, str(target.index), color)
|
||||
print(f" {panel_label(target.index)} -> {target.label()}")
|
||||
_push(sock, bundle, port, panel_id)
|
||||
time.sleep(pause)
|
||||
|
||||
|
||||
def test_corners(
|
||||
sock: socket.socket,
|
||||
sock: socket.socket | None,
|
||||
targets: list[PanelTarget],
|
||||
port: int,
|
||||
brightness: float,
|
||||
@@ -104,24 +105,24 @@ def test_corners(
|
||||
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)
|
||||
with _matrices(targets, brightness) as bundle:
|
||||
for target, matrix in bundle.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, bundle, port, panel_id)
|
||||
time.sleep(pause)
|
||||
|
||||
|
||||
def test_crosshair(
|
||||
sock: socket.socket,
|
||||
sock: socket.socket | None,
|
||||
targets: list[PanelTarget],
|
||||
port: int,
|
||||
brightness: float,
|
||||
@@ -129,22 +130,22 @@ def test_crosshair(
|
||||
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)
|
||||
with _matrices(targets, brightness) as bundle:
|
||||
for target, matrix in bundle.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, bundle, port, panel_id)
|
||||
time.sleep(pause)
|
||||
|
||||
|
||||
def test_columns(
|
||||
sock: socket.socket,
|
||||
sock: socket.socket | None,
|
||||
targets: list[PanelTarget],
|
||||
port: int,
|
||||
brightness: float,
|
||||
@@ -153,22 +154,22 @@ def test_columns(
|
||||
) -> 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)
|
||||
with _matrices(targets, brightness) as bundle:
|
||||
for x in range(max_width):
|
||||
for target, matrix in bundle.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, bundle, port, panel_id)
|
||||
time.sleep(pause)
|
||||
_clear_all(sock, targets, port, brightness, panel_id)
|
||||
|
||||
|
||||
def test_rows(
|
||||
sock: socket.socket,
|
||||
sock: socket.socket | None,
|
||||
targets: list[PanelTarget],
|
||||
port: int,
|
||||
brightness: float,
|
||||
@@ -177,19 +178,19 @@ def test_rows(
|
||||
) -> 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)
|
||||
with _matrices(targets, brightness) as bundle:
|
||||
for y in range(height):
|
||||
for target, matrix in bundle.matrices:
|
||||
matrix.clear()
|
||||
for x in range(target.width):
|
||||
matrix[x, y] = (255, 255, 255)
|
||||
_push(sock, bundle, port, panel_id)
|
||||
time.sleep(pause)
|
||||
_clear_all(sock, targets, port, brightness, panel_id)
|
||||
|
||||
|
||||
def test_flash(
|
||||
sock: socket.socket,
|
||||
sock: socket.socket | None,
|
||||
targets: list[PanelTarget],
|
||||
port: int,
|
||||
brightness: float,
|
||||
@@ -199,18 +200,18 @@ def test_flash(
|
||||
) -> 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)
|
||||
with _matrices(targets, brightness) as bundle:
|
||||
for _, matrix in bundle.matrices:
|
||||
matrix.fill((255, 255, 255))
|
||||
_push(sock, bundle, 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,
|
||||
sock: socket.socket | None,
|
||||
targets: list[PanelTarget],
|
||||
port: int,
|
||||
brightness: float,
|
||||
@@ -220,20 +221,20 @@ def test_pulse(
|
||||
) -> 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)
|
||||
with _matrices(targets, brightness) as bundle:
|
||||
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)
|
||||
for _, matrix in bundle.matrices:
|
||||
matrix.fill(color)
|
||||
_push(sock, bundle, port, panel_id)
|
||||
time.sleep(1.0 / fps)
|
||||
_clear_all(sock, targets, port, brightness, panel_id)
|
||||
|
||||
|
||||
def test_chain_ends(
|
||||
sock: socket.socket,
|
||||
sock: socket.socket | None,
|
||||
targets: list[PanelTarget],
|
||||
port: int,
|
||||
brightness: float,
|
||||
@@ -241,21 +242,21 @@ def test_chain_ends(
|
||||
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)
|
||||
with _matrices(targets, brightness) as bundle:
|
||||
for target, matrix in bundle.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, bundle, port, panel_id)
|
||||
time.sleep(pause)
|
||||
|
||||
|
||||
def test_width(
|
||||
sock: socket.socket,
|
||||
sock: socket.socket | None,
|
||||
targets: list[PanelTarget],
|
||||
port: int,
|
||||
brightness: float,
|
||||
@@ -264,25 +265,26 @@ def test_width(
|
||||
col_start: int,
|
||||
col_end: int,
|
||||
) -> None:
|
||||
udp_targets = [t for t in targets if not t.local]
|
||||
if not udp_targets:
|
||||
print("Test: width — skipped (no UDP Pico panels in target list)")
|
||||
return
|
||||
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)")
|
||||
probe_leds = probe_w * udp_targets[0].height
|
||||
print("Test: width — one full column at a time (right-edge probe, UDP panels only)")
|
||||
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:
|
||||
for target in udp_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)
|
||||
push_panel_frames(sock, burst, port, 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)
|
||||
@@ -323,7 +325,9 @@ def main() -> int:
|
||||
args = parser.parse_args()
|
||||
|
||||
targets = panel_targets_from_args(args)
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
has_udp = any(not t.local for t in targets)
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) if has_udp else None
|
||||
targets = apply_pin_arg(args, sock, targets)
|
||||
print(f"Panel sync test -> {format_panel_targets(targets, args.port)}")
|
||||
print("Ctrl+C to stop\n")
|
||||
|
||||
@@ -353,7 +357,8 @@ def main() -> int:
|
||||
return 1
|
||||
finally:
|
||||
_clear_all(sock, targets, args.port, args.brightness, args.panel_id)
|
||||
sock.close()
|
||||
if sock is not None:
|
||||
sock.close()
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
@@ -10,13 +10,21 @@ import sys
|
||||
import time
|
||||
|
||||
from leds.panel_udp import (
|
||||
HeadlessMatrix,
|
||||
PanelMatrixBundle,
|
||||
add_panel_args,
|
||||
apply_pin_arg,
|
||||
format_panel_targets,
|
||||
panel_targets_from_args,
|
||||
send_frame,
|
||||
push_panel_frames,
|
||||
)
|
||||
from leds.text import (
|
||||
blit_mask,
|
||||
blit_mask_gradient,
|
||||
draw_font_text_centered,
|
||||
draw_text,
|
||||
draw_text_centered,
|
||||
render_font_mask,
|
||||
)
|
||||
from leds.text import draw_text, draw_text_centered
|
||||
|
||||
DEFAULT_NAME = "JIMMY"
|
||||
|
||||
@@ -44,9 +52,40 @@ def main() -> int:
|
||||
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")
|
||||
parser.add_argument(
|
||||
"--cursive",
|
||||
action="store_true",
|
||||
help="Italic script style (keeps letter case; auto-scrolls if too wide)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--font-size",
|
||||
type=int,
|
||||
default=None,
|
||||
help="Font point size (default: panel height + 3)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--letter-spacing",
|
||||
type=int,
|
||||
default=0,
|
||||
help="Pixels between cursive letters (default: 0)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--speed",
|
||||
type=float,
|
||||
default=None,
|
||||
help="Scroll delay in seconds (default: 0.15 cursive, 0.08 bitmap)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--rainbow",
|
||||
action="store_true",
|
||||
help="Rainbow gradient across cursive text",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
text = (args.text or _default_name()).upper()
|
||||
if args.cursive:
|
||||
text = args.text or "Allyssia"
|
||||
else:
|
||||
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)
|
||||
@@ -54,46 +93,114 @@ def main() -> int:
|
||||
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)}")
|
||||
has_udp = any(not t.local for t in targets)
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) if has_udp else None
|
||||
targets = apply_pin_arg(args, sock, targets)
|
||||
style = "cursive" if args.cursive else "bitmap"
|
||||
|
||||
cursive_masks: dict[int, tuple[int, list[tuple[int, int]]]] = {}
|
||||
if args.cursive:
|
||||
try:
|
||||
for target in targets:
|
||||
width, _, ons = render_font_mask(
|
||||
text,
|
||||
height=target.height,
|
||||
size=args.font_size,
|
||||
letter_spacing=args.letter_spacing,
|
||||
)
|
||||
cursive_masks[target.index] = (width, ons)
|
||||
except (ImportError, FileNotFoundError, OSError) as exc:
|
||||
print(f"Error: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
# Auto-scroll when the rendered text is wider than any target panel.
|
||||
do_scroll = args.scroll
|
||||
if args.cursive and not do_scroll:
|
||||
if any(
|
||||
cursive_masks[t.index][0] > t.width for t in targets if t.index in cursive_masks
|
||||
):
|
||||
do_scroll = True
|
||||
|
||||
scroll_delay = args.speed
|
||||
if scroll_delay is None:
|
||||
scroll_delay = 0.15 if args.cursive else 0.08
|
||||
|
||||
print(
|
||||
f"Displaying '{text}' ({style}"
|
||||
f"{', scrolling' if do_scroll else ''})"
|
||||
f" 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)")
|
||||
with PanelMatrixBundle(targets, args.brightness) as bundle:
|
||||
if do_scroll:
|
||||
start = time.monotonic()
|
||||
offsets = {
|
||||
target.index: matrix.width for target, matrix in bundle.matrices
|
||||
}
|
||||
while True:
|
||||
time.sleep(1)
|
||||
phase = (time.monotonic() - start) * 0.55
|
||||
for target, matrix in bundle.matrices:
|
||||
matrix.clear()
|
||||
if args.cursive:
|
||||
tw, ons = cursive_masks[target.index]
|
||||
if args.rainbow:
|
||||
blit_mask_gradient(
|
||||
matrix,
|
||||
ons,
|
||||
offsets[target.index],
|
||||
0,
|
||||
width=tw,
|
||||
phase=phase,
|
||||
cycles=2.5,
|
||||
)
|
||||
else:
|
||||
blit_mask(matrix, ons, offsets[target.index], 0, color)
|
||||
wrap = tw
|
||||
else:
|
||||
draw_text(matrix, text, offsets[target.index], 1, color)
|
||||
wrap = len(text) * 6
|
||||
offsets[target.index] -= 1
|
||||
if offsets[target.index] < -wrap:
|
||||
offsets[target.index] = matrix.width
|
||||
push_panel_frames(sock, bundle.matrices, args.port, args.panel_id)
|
||||
time.sleep(scroll_delay)
|
||||
if args.hold > 0 and time.monotonic() - start >= args.hold:
|
||||
break
|
||||
else:
|
||||
time.sleep(args.hold)
|
||||
for target, matrix in bundle.matrices:
|
||||
matrix.clear()
|
||||
if args.cursive:
|
||||
if args.rainbow:
|
||||
tw, ons = cursive_masks[target.index]
|
||||
x = max(0, (matrix.width - tw) // 2)
|
||||
blit_mask_gradient(matrix, ons, x, 0, width=tw, phase=0.0)
|
||||
else:
|
||||
draw_font_text_centered(
|
||||
matrix,
|
||||
text,
|
||||
color,
|
||||
size=args.font_size,
|
||||
letter_spacing=args.letter_spacing,
|
||||
)
|
||||
else:
|
||||
draw_text_centered(matrix, text, color)
|
||||
push_panel_frames(sock, bundle.matrices, args.port, 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()
|
||||
with PanelMatrixBundle(targets, args.brightness) as bundle:
|
||||
for _, matrix in bundle.matrices:
|
||||
matrix.clear()
|
||||
push_panel_frames(sock, bundle.matrices, args.port, args.panel_id)
|
||||
if sock is not None:
|
||||
sock.close()
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ from leds.panel_udp import (
|
||||
add_panel_args,
|
||||
format_panel_targets,
|
||||
panel_targets_from_args,
|
||||
parse_pins_arg,
|
||||
run_panel_animation_loop,
|
||||
)
|
||||
|
||||
@@ -35,6 +36,8 @@ def main() -> int:
|
||||
fps=args.fps,
|
||||
brightness=args.brightness,
|
||||
panel_id=args.panel_id,
|
||||
pin=args.pin,
|
||||
pins=parse_pins_arg(args),
|
||||
)
|
||||
except KeyboardInterrupt:
|
||||
print("\nStopped.")
|
||||
|
||||
67
examples/portal_simulator.py
Normal file
67
examples/portal_simulator.py
Normal file
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Web portal simulator — FastAPI + live reload."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Portal web simulator (FastAPI)")
|
||||
parser.add_argument("--host", default="127.0.0.1")
|
||||
parser.add_argument("--port", type=int, default=8765)
|
||||
parser.add_argument(
|
||||
"--reload",
|
||||
action=argparse.BooleanOptionalAction,
|
||||
default=True,
|
||||
help="reload Python on code changes (default: on)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--browser-reload",
|
||||
action=argparse.BooleanOptionalAction,
|
||||
default=True,
|
||||
help="reload browser when web/ files change (default: on)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
web_dir = Path(__file__).resolve().parent.parent / "web"
|
||||
if not web_dir.is_dir():
|
||||
print(f"Missing web directory: {web_dir}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
try:
|
||||
import uvicorn
|
||||
except ImportError:
|
||||
print(
|
||||
"FastAPI simulator requires: pipenv install fastapi 'uvicorn[standard]'",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
|
||||
os.environ["PORTAL_DEV_RELOAD"] = "1" if args.browser_reload else "0"
|
||||
|
||||
leds_dir = Path(__file__).resolve().parent.parent / "leds"
|
||||
reload_dirs = [str(web_dir), str(leds_dir)] if args.reload else None
|
||||
|
||||
print(f"Portal simulator → http://{args.host}:{args.port}/")
|
||||
if args.reload:
|
||||
print("Python reload: on")
|
||||
if args.browser_reload:
|
||||
print("Browser reload: on (watches web/)")
|
||||
print("Ctrl+C to stop")
|
||||
|
||||
uvicorn.run(
|
||||
"leds.portal_web:app",
|
||||
host=args.host,
|
||||
port=args.port,
|
||||
reload=args.reload,
|
||||
reload_dirs=reload_dirs,
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
69
examples/setup_spi_bufsiz.py
Normal file
69
examples/setup_spi_bufsiz.py
Normal file
@@ -0,0 +1,69 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Raise spidev bufsiz so full WS2812 SPI frames fit (405 LEDs ≈ 10 KiB)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from leds.backends.spi import _DEFAULT_BUFSIZ, spidev_bufsize
|
||||
|
||||
MODPROBE_D = Path("/etc/modprobe.d/spidev-bufsiz.conf")
|
||||
MARKER = "# portal spidev bufsiz"
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Configure spidev bufsiz for long WS2812 SPI strips"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--bufsiz",
|
||||
type=int,
|
||||
default=_DEFAULT_BUFSIZ,
|
||||
help=f"Kernel SPI buffer size (default {_DEFAULT_BUFSIZ})",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--install",
|
||||
action="store_true",
|
||||
help="Write /etc/modprobe.d/spidev-bufsiz.conf and reload spidev (sudo)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
current = spidev_bufsize()
|
||||
print(f"spidev bufsiz: {current}")
|
||||
print(f"405 LEDs need ~9762 bytes per frame.\n")
|
||||
|
||||
line = f"options spidev bufsiz={args.bufsiz}"
|
||||
print("Add to /etc/modprobe.d/spidev-bufsiz.conf:\n")
|
||||
print(f"{MARKER}")
|
||||
print(line)
|
||||
print("\nThen reload:\n sudo modprobe -r spidev && sudo modprobe spidev")
|
||||
|
||||
if not args.install:
|
||||
return 0
|
||||
|
||||
try:
|
||||
existing = MODPROBE_D.read_text() if MODPROBE_D.exists() else ""
|
||||
if MARKER in existing:
|
||||
print(f"\n{MODPROBE_D} already configured.")
|
||||
else:
|
||||
MODPROBE_D.write_text(f"{MARKER}\n{line}\n")
|
||||
print(f"\nWrote {MODPROBE_D}.")
|
||||
|
||||
subprocess.run(["modprobe", "-r", "spidev"], check=False)
|
||||
subprocess.run(["modprobe", "spidev", f"bufsiz={args.bufsiz}"], check=True)
|
||||
print(f"Reloaded spidev (bufsiz={spidev_bufsize()}).")
|
||||
except OSError as exc:
|
||||
print(f"Error: {exc}", file=sys.stderr)
|
||||
print("Re-run with sudo.", file=sys.stderr)
|
||||
return 1
|
||||
except subprocess.CalledProcessError as exc:
|
||||
print(f"modprobe failed: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
153
examples/spi_panel_test.py
Normal file
153
examples/spi_panel_test.py
Normal file
@@ -0,0 +1,153 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Step-by-step drive test for a Pi-connected panel over SPI."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
from leds.array_config import (
|
||||
MATRIX_BRIGHTNESS,
|
||||
MATRIX_SPI_BUS,
|
||||
MATRIX_SPI_DEVICE,
|
||||
matrix_pixel_index,
|
||||
panel_layout,
|
||||
panel_pixel_count,
|
||||
)
|
||||
from leds.backends.spi import spidev_bufsize_hint
|
||||
from leds.config import panel_strip_config
|
||||
from leds.matrix import LedMatrix
|
||||
|
||||
|
||||
def _flash(matrix: LedMatrix, pause: float) -> None:
|
||||
print("Test: RGB flash")
|
||||
for name, color in (("red", (255, 0, 0)), ("green", (0, 255, 0)), ("blue", (0, 0, 255))):
|
||||
print(f" {name}")
|
||||
matrix.fill(color)
|
||||
matrix.show()
|
||||
time.sleep(pause)
|
||||
matrix.clear()
|
||||
matrix.show()
|
||||
|
||||
|
||||
def _first_last(matrix: LedMatrix, pixels: int, pause: float) -> None:
|
||||
print(f"Test: LED 0 red, LED {pixels - 1} green")
|
||||
width = matrix.width
|
||||
matrix.fill((0, 0, 0))
|
||||
matrix.set_pixel(0, 0, (255, 0, 0))
|
||||
last = pixels - 1
|
||||
matrix.set_pixel(last % width, last // width, (0, 255, 0))
|
||||
matrix.show()
|
||||
time.sleep(pause)
|
||||
matrix.clear()
|
||||
matrix.show()
|
||||
|
||||
|
||||
def _rows(matrix: LedMatrix, width: int, height: int, pause: float) -> None:
|
||||
print("Test: one row at a time (white)")
|
||||
for y in range(height):
|
||||
matrix.fill((0, 0, 0))
|
||||
for x in range(width):
|
||||
matrix[x, y] = (255, 255, 255)
|
||||
print(f" row {y}")
|
||||
matrix.show()
|
||||
time.sleep(pause)
|
||||
matrix.clear()
|
||||
matrix.show()
|
||||
|
||||
|
||||
def _corners(matrix: LedMatrix, width: int, height: int, pause: float) -> None:
|
||||
print("Test: corners")
|
||||
corners = (
|
||||
("top-left", 0, 0, (255, 0, 0)),
|
||||
("top-right", width - 1, 0, (0, 255, 0)),
|
||||
("bottom-left", 0, height - 1, (0, 0, 255)),
|
||||
("bottom-right", width - 1, height - 1, (255, 255, 0)),
|
||||
)
|
||||
for label, x, y, color in corners:
|
||||
matrix.fill((0, 0, 0))
|
||||
matrix[x, y] = color
|
||||
idx = matrix_pixel_index(x, y, width, height)
|
||||
print(f" {label} ({x},{y}) -> LED {idx}")
|
||||
matrix.show()
|
||||
time.sleep(pause)
|
||||
matrix.clear()
|
||||
matrix.show()
|
||||
|
||||
|
||||
def _chase(matrix: LedMatrix, pixels: int, pause: float, steps: int) -> None:
|
||||
print(f"Test: chase ({steps} steps)")
|
||||
width = matrix.width
|
||||
for i in range(steps):
|
||||
matrix.fill((0, 0, 0))
|
||||
idx = i % pixels
|
||||
matrix[idx % width, idx // width] = (0, 255, 255)
|
||||
matrix.show()
|
||||
time.sleep(pause)
|
||||
matrix.clear()
|
||||
matrix.show()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="SPI drive test for Pi-connected panel")
|
||||
parser.add_argument("--panel-index", type=int, default=2, choices=range(5), metavar="N")
|
||||
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("--spi-mhz", type=float, default=4.2)
|
||||
parser.add_argument("--wire-order", default=None)
|
||||
parser.add_argument("--pause", type=float, default=0.8)
|
||||
parser.add_argument(
|
||||
"--test",
|
||||
choices=("all", "flash", "first-last", "rows", "corners", "chase"),
|
||||
default="all",
|
||||
)
|
||||
parser.add_argument("--chase-steps", type=int, default=None)
|
||||
args = parser.parse_args()
|
||||
|
||||
width, height = panel_layout(args.panel_index)
|
||||
pixels = panel_pixel_count(args.panel_index)
|
||||
hint = spidev_bufsize_hint(pixels)
|
||||
if hint:
|
||||
print(f"Warning: {hint}\n", file=sys.stderr)
|
||||
|
||||
cfg = panel_strip_config(
|
||||
args.panel_index,
|
||||
pixels,
|
||||
spi_max_speed_hz=max(1, int(args.spi_mhz * 1_000_000)),
|
||||
wire_order=args.wire_order,
|
||||
brightness=args.brightness,
|
||||
)
|
||||
chase_steps = args.chase_steps if args.chase_steps is not None else pixels
|
||||
|
||||
print(f"SPI panel test: panel {args.panel_index}, {width}×{height} ({pixels} LEDs)")
|
||||
|
||||
try:
|
||||
with LedMatrix(width, height, config0=cfg) as matrix:
|
||||
if args.test in ("all", "flash"):
|
||||
_flash(matrix, args.pause)
|
||||
if args.test in ("all", "first-last"):
|
||||
_first_last(matrix, pixels, args.pause)
|
||||
if args.test in ("all", "corners"):
|
||||
_corners(matrix, width, height, args.pause)
|
||||
if args.test in ("all", "rows"):
|
||||
_rows(matrix, width, height, max(args.pause * 0.6, 0.15))
|
||||
if args.test in ("all", "chase"):
|
||||
_chase(matrix, pixels, max(args.pause * 0.25, 0.05), chase_steps)
|
||||
except KeyboardInterrupt:
|
||||
print("\nStopped.")
|
||||
except OSError as exc:
|
||||
print(f"SPI error: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
print("Done.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
114
examples/spi_rgb_test.py
Normal file
114
examples/spi_rgb_test.py
Normal file
@@ -0,0 +1,114 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Full-panel red / green / blue over SPI (GPIO 10 on Pi 5)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
from leds.array_config import (
|
||||
MATRIX_BRIGHTNESS,
|
||||
MATRIX_SPI_BUS,
|
||||
MATRIX_SPI_DEVICE,
|
||||
panel_layout,
|
||||
panel_pixel_count,
|
||||
)
|
||||
from leds.colors import BLUE, GREEN, OFF, RED, WHITE
|
||||
from leds.config import panel_strip_config, spi_strip_config
|
||||
from leds.matrix import LedMatrix
|
||||
|
||||
COLORS = (
|
||||
("RED", RED),
|
||||
("GREEN", GREEN),
|
||||
("BLUE", BLUE),
|
||||
("WHITE", WHITE),
|
||||
("OFF", OFF),
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="R/G/B color test on a WS2812 strip via SPI"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--panel-index",
|
||||
type=int,
|
||||
default=2,
|
||||
choices=range(5),
|
||||
metavar="N",
|
||||
help="Panel layout for width×height (default: 2 = top / Pi)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--count",
|
||||
type=int,
|
||||
default=None,
|
||||
help="LED count override (default: panel width × 9)",
|
||||
)
|
||||
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(
|
||||
"--spi-mhz",
|
||||
type=float,
|
||||
default=4.2,
|
||||
help="SPI clock in MHz (default: 4.2)",
|
||||
)
|
||||
parser.add_argument("--pause", type=float, default=1.5)
|
||||
args = parser.parse_args()
|
||||
|
||||
width, height = panel_layout(args.panel_index)
|
||||
pixels = args.count if args.count is not None else panel_pixel_count(args.panel_index)
|
||||
spi_hz = max(1, int(args.spi_mhz * 1_000_000))
|
||||
|
||||
if args.count is not None:
|
||||
if pixels % width != 0:
|
||||
parser.error(f"--count {pixels} is not a multiple of width {width}")
|
||||
height = pixels // width
|
||||
cfg = spi_strip_config(
|
||||
pixels,
|
||||
spi_bus=args.spi_bus,
|
||||
spi_device=args.spi_device,
|
||||
spi_max_speed_hz=spi_hz,
|
||||
brightness=args.brightness,
|
||||
passthrough=True,
|
||||
swap_rg=True,
|
||||
)
|
||||
else:
|
||||
cfg = panel_strip_config(
|
||||
args.panel_index,
|
||||
pixels,
|
||||
spi_max_speed_hz=spi_hz,
|
||||
brightness=args.brightness,
|
||||
passthrough=True,
|
||||
swap_rg=True,
|
||||
)
|
||||
|
||||
print(
|
||||
f"SPI RGB test: {width}×{height} ({pixels} LEDs), "
|
||||
f"bus {args.spi_bus}, {args.spi_mhz:g} MHz, passthrough RGB"
|
||||
)
|
||||
|
||||
try:
|
||||
with LedMatrix(width, height, config0=cfg) as matrix:
|
||||
for label, color in COLORS:
|
||||
print(f" {label}")
|
||||
matrix.fill(color)
|
||||
matrix.show()
|
||||
time.sleep(args.pause)
|
||||
except KeyboardInterrupt:
|
||||
print("\nStopped.")
|
||||
except OSError as exc:
|
||||
print(f"SPI error: {exc}", file=sys.stderr)
|
||||
print("Enable SPI and wire DIN → GPIO 10 (/dev/spidev0.0).", file=sys.stderr)
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user