Ship the portal FastAPI controller with presets, sequences, and live audio.
Move the app under src/, add ring-spanning patterns with shared fonts and beat-driven motion, and wire global audio settings into the web UI. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Executable
+209
@@ -0,0 +1,209 @@
|
||||
#!/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 (
|
||||
PanelMatrixBundle,
|
||||
add_panel_args,
|
||||
apply_pin_arg,
|
||||
format_panel_targets,
|
||||
panel_targets_from_args,
|
||||
push_panel_frames,
|
||||
)
|
||||
from leds.text import (
|
||||
blit_mask,
|
||||
blit_mask_gradient,
|
||||
draw_font_text_centered,
|
||||
draw_text,
|
||||
draw_text_centered,
|
||||
render_font_mask,
|
||||
)
|
||||
|
||||
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")
|
||||
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()
|
||||
|
||||
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)
|
||||
return 1
|
||||
color = (parts[0], parts[1], parts[2])
|
||||
|
||||
targets = panel_targets_from_args(args)
|
||||
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)} "
|
||||
f"brightness={args.brightness:g}"
|
||||
)
|
||||
|
||||
try:
|
||||
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:
|
||||
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:
|
||||
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:
|
||||
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
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user