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