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:
@@ -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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user