#!/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)}" ) 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())