feat: add pipeline diagnostic script, viz/midi log recipes, reset-mcu recipe
Validate and Test / validate-patches (push) Failing after 13s

- scripts/diagnostics/test-pipeline.py: full end-to-end test
- just diag-full: run pipeline diagnostic
- just viz-log/midi-log: watch service logs live
- just viz-stats/midi-stats: check counters
- just reset-mcu: reset MCU + reopen serial via router RPC
- just test-chord: send test MIDI chord locally
- just fix-perms: restore +x bits on all scripts
This commit is contained in:
2026-06-24 15:02:39 -07:00
parent b4d19f680a
commit c0af9c6b6f
2 changed files with 233 additions and 0 deletions
+68
View File
@@ -338,6 +338,74 @@ kconfig:
@ssh {{SSH_OPTS}} "{{BOARD_USER}}@{{BOARD_HOST}}" "\ @ssh {{SSH_OPTS}} "{{BOARD_USER}}@{{BOARD_HOST}}" "\
zgrep -E 'CONFIG_USB_(GADGET|CONFIGFS|F_UAC1|F_MIDI|DWC3|F_ACC|F_SERIAL)' /proc/config.gz 2>/dev/null | awk -F= '{printf \" %-45s %s\\n\", \$1, \$2}'" zgrep -E 'CONFIG_USB_(GADGET|CONFIGFS|F_UAC1|F_MIDI|DWC3|F_ACC|F_SERIAL)' /proc/config.gz 2>/dev/null | awk -F= '{printf \" %-45s %s\\n\", \$1, \$2}'"
# Full pipeline diagnostic: test MIDI → viz → router → MCU → matrix
diag-full:
python3 scripts/diagnostics/test-pipeline.py
# Watch viz real-time log
viz-log:
@ssh {{SSH_OPTS}} "{{BOARD_USER}}@{{BOARD_HOST}}" "journalctl -u led-matrix-viz -f -n 20"
# Watch midi-bridge real-time log
midi-log:
@ssh {{SSH_OPTS}} "{{BOARD_USER}}@{{BOARD_HOST}}" "journalctl -u midi-bridge -f -n 20"
# Reset MCU via router RPC and reopen serial
reset-mcu:
@ssh {{SSH_OPTS}} "{{BOARD_USER}}@{{BOARD_HOST}}" "\
python3 -c '
import socket, msgpack, time
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
s.settimeout(5)
s.connect(\"/var/run/arduino-router.sock\")
p = msgpack.Packer()
req = p.pack([0, 1, \"mon/reset\", []])
s.sendall(req); s.recv(4096)
req = p.pack([0, 2, \"\$/serial/close\", [\"/dev/ttyHS1\"]])
s.sendall(req); s.recv(4096)
req = p.pack([0, 3, \"\$/serial/open\", [\"/dev/ttyHS1\"]])
s.sendall(req); s.recv(4096)
s.close()
print(\"MCU reset, serial reopened. Wait 20s for Bridge.begin()...\")
'"
# Check viz stats live
viz-stats:
@ssh {{SSH_OPTS}} "{{BOARD_USER}}@{{BOARD_HOST}}" "cat /tmp/viz-stats.json"
# Check midi-bridge stats live
midi-stats:
@ssh {{SSH_OPTS}} "{{BOARD_USER}}@{{BOARD_HOST}}" "cat /tmp/midi-stats.json 2>/dev/null || echo '(no stats - bridge may be down)'"
# Show all service status
svc-status:
@ssh {{SSH_OPTS}} "{{BOARD_USER}}@{{BOARD_HOST}}" "\
for s in arduino-router midi-bridge pd-synth-onboard led-matrix-viz prometheus-exporter; do \
echo \" \$s: \$(systemctl is-active \$s 2>/dev/null)\"; \
done"
# Send test MIDI chord to viz (local test, no MIDI controller needed)
test-chord:
@ssh {{SSH_OPTS}} "{{BOARD_USER}}@{{BOARD_HOST}}" "\
python3 -c '
import socket, time
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
for note in [60, 64, 67, 72]:
s.sendto(b\"midi 144 %d 100;\" % note, (\"127.0.0.1\", 8082))
time.sleep(0.15)
time.sleep(0.5)
for note in [60, 64, 67, 72]:
s.sendto(b\"midi 128 %d 0;\" % note, (\"127.0.0.1\", 8082))
time.sleep(0.1)
print(\"Sent C-E-G-C chord\")
'"
# Fix permissions on all scripts (run after clone/checkout)
fix-perms:
@ssh {{SSH_OPTS}} "{{BOARD_USER}}@{{BOARD_HOST}}" "\
chmod +x {{REPO_DIR}}/scripts/*.py {{REPO_DIR}}/scripts/*.sh 2>/dev/null; \
echo 'Permissions fixed'"
# Compile and flash the MCU sketch (LED matrix bridge) via SWD # Compile and flash the MCU sketch (LED matrix bridge) via SWD
flash-sketch: flash-sketch:
@ssh {{SSH_OPTS}} "{{BOARD_USER}}@{{BOARD_HOST}}" "\ @ssh {{SSH_OPTS}} "{{BOARD_USER}}@{{BOARD_HOST}}" "\
+165
View File
@@ -0,0 +1,165 @@
#!/usr/bin/env python3
"""Full pipeline diagnostic: test MIDI → viz → router → MCU → LED matrix.
Usage:
python3 scripts/diagnostics/test-pipeline.py [--host HOST]
Tests:
1. Router Unix socket presence and connectivity
2. MCU RPC: get_status, clear, draw_frame
3. Viz UDP reception and frame sending
4. Mid-bridge service health
5. Full end-to-end: send MIDI → verify viz stats → verify MCU state
6. Router buffer drain test: send 100 rapid frames, verify MCU stays responsive
"""
import socket, msgpack, time, json, os, sys, subprocess
HOST = sys.argv[1] if len(sys.argv) > 1 else "192.168.9.167"
ROUTER_SOCK = "/var/run/arduino-router.sock"
VIZ_PORT = 8082
passed = 0
failed = 0
def check(name, ok, detail=""):
global passed, failed
if ok:
passed += 1
print(f"{name}")
else:
failed += 1
print(f"{name}")
if detail:
print(f" {detail}")
def ssh(cmd):
return subprocess.run(
["ssh", "-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null",
"-i", os.path.expanduser("~/.ssh/unoq_deploy_key"),
f"arduino@{HOST}", cmd],
capture_output=True, text=True, timeout=15
)
def run_python(code):
r = ssh(f"python3 -c {sh_quote(code)}")
return r.stdout, r.stderr, r.returncode
def sh_quote(s):
return f"'{s.replace(chr(39), chr(39)+chr(92)+chr(39)+chr(39))}'"
# ── Test 1: Router connectivity ──
print("\n=== Router ===")
r = ssh("test -S /var/run/arduino-router.sock && echo 'exists'")
check("Router socket exists", "exists" in r.stdout)
r = ssh("python3 -c 'import socket; s=socket.socket(socket.AF_UNIX,socket.SOCK_STREAM); s.settimeout(3); s.connect(\"/var/run/arduino-router.sock\"); print(\"ok\"); s.close()'")
check("Router connectable", "ok" in r.stdout, r.stderr)
# ── Test 2: MCU RPC ──
print("\n=== MCU RPC ===")
out, err, rc = run_python("""
import socket, msgpack
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
s.settimeout(5)
s.connect('/var/run/arduino-router.sock')
p = msgpack.Packer()
req = p.pack([0, 1, 'get_status', ['']])
s.sendall(req)
d = s.recv(4096)
obj = msgpack.unpackb(d)
print(f"status={obj[3]}")
req = p.pack([0, 2, 'clear', ['']])
s.sendall(req)
d = s.recv(4096)
obj = msgpack.unpackb(d)
print(f"clear={obj[3]}")
req = p.pack([0, 3, 'draw_frame', ['-1','-1','-1','-1']])
s.sendall(req)
d = s.recv(4096)
obj = msgpack.unpackb(d)
print(f"draw_frame={obj[3]}")
s.close()
""")
check("MCU get_status", "status=" in out, err)
check("MCU clear", "clear=" in out and "'OK'" in out, err)
check("MCU draw_frame", "draw_frame=" in out and "'OK'" in out, err)
# ── Test 3: Router buffer drain ──
print("\n=== Router buffer drain ===")
out, err, rc = run_python("""
import socket, msgpack, time
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
s.settimeout(5)
s.connect('/var/run/arduino-router.sock')
p = msgpack.Packer()
# Send 100 draw_frame REQUESTs and drain each response
for i in range(100):
req = p.pack([0, i, 'draw_frame', ['-1','-1','-1','-1']])
s.sendall(req)
s.recv(4096)
# Verify MCU still responsive
req = p.pack([0, 200, 'get_status', ['']])
s.sendall(req)
d = s.recv(4096)
obj = msgpack.unpackb(d)
print(f"ok={obj[3]}")
s.close()
""")
check("MCU responsive after 100 frames", "ok=" in out and "frame" in out, err)
# ── Test 4: Viz UDP reception ──
print("\n=== Viz pipeline ===")
out, err, rc = run_python("""
import socket, time, json
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.sendto(b'midi 144 60 100;', ('127.0.0.1', 8082))
s.sendto(b'midi 144 64 100;', ('127.0.0.1', 8082))
s.sendto(b'midi 144 67 100;', ('127.0.0.1', 8082))
time.sleep(3)
with open('/tmp/viz-stats.json') as f:
d = json.load(f)
print(f"sent={d['frames_sent']} dropped={d['frames_dropped']} active={d['active_notes']}")
""")
check("Viz received MIDI", "active" in out, err)
check("Viz zero drops", "dropped=0" in out, err)
# ── Test 5: Viz router drain ──
print("\n=== Viz + router coexistence ===")
out, err, rc = run_python("""
import socket, msgpack, time, json
# Send MIDI via viz
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
for note in range(60, 80):
s.sendto(b'midi 144 %d 100;' % note, ('127.0.0.1', 8082))
time.sleep(0.05)
for note in range(60, 80):
s.sendto(b'midi 128 %d 0;' % note, ('127.0.0.1', 8082))
time.sleep(0.02)
time.sleep(2)
# Check viz stats
with open('/tmp/viz-stats.json') as f:
d = json.load(f)
print(f"viz_sent={d['frames_sent']} viz_dropped={d['frames_dropped']}")
# MCU still responsive?
s2 = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
s2.settimeout(5)
s2.connect('/var/run/arduino-router.sock')
p = msgpack.Packer()
req = p.pack([0, 1, 'get_status', ['']])
s2.sendall(req)
d2 = s2.recv(4096)
obj = msgpack.unpackb(d2)
print(f"mcu_status={obj[3]}")
s2.close()
""")
check("Viz zero drops after burst", "viz_dropped=0" in out, err)
check("MCU responsive after viz burst", "mcu_status=" in out and "'frame'" in out, err)
# ── Summary ──
print(f"\n{'='*40}")
print(f" {passed} passed, {failed} failed")
print(f"{'='*40}")
sys.exit(0 if failed == 0 else 1)