diff --git a/.gitignore b/.gitignore index d994db2..0e73b49 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ __pycache__/ *.log /tmp/ build/ +.env diff --git a/justfile b/justfile index c205f19..bf4509b 100644 --- a/justfile +++ b/justfile @@ -2,7 +2,9 @@ # Run with: just # Override board host: just BOARD_HOST=192.168.1.100 deploy -BOARD_HOST := env_var_or_default('BOARD_HOST', 'uno-q.local') +set dotenv-load := true + +BOARD_HOST := env_var_or_default('BOARD_HOST', '192.168.9.167') BOARD_USER := env_var_or_default('BOARD_USER', 'arduino') BOARD_PASS := env_var_or_default('BOARD_PASS', 'arduino') SSH_KEY := env_var_or_default('SSH_KEY', '/home/david/.ssh/unoq_deploy_key') @@ -154,17 +156,18 @@ enable-host-mode: deploy-midi-bridge deploy-led-matrix-viz echo '[OK] Host mode services enabled and started'" # Install all systemd services +install-services: #!/usr/bin/env bash set -e SCP_OPTS="{{SSH_OPTS}}" echo "[INSTALL] Services on {{BOARD_USER}}@{{BOARD_HOST}}" for svc in usb-role-detect usb-gadget-audio pd-synth-auto pd-synth detect-i2s-device pd-synth-i2s; do - scp $SCP_OPTS "system/$svc.service" "{{BOARD_USER}}@{{BOARD_HOST}}:/tmp/" + scp $SCP_OPTS "system/${svc}.service" "{{BOARD_USER}}@{{BOARD_HOST}}:/tmp/" done scp $SCP_OPTS system/99-usb-role.rules "{{BOARD_USER}}@{{BOARD_HOST}}:/tmp/" ssh $SCP_OPTS "{{BOARD_USER}}@{{BOARD_HOST}}" "\ for svc in usb-role-detect usb-gadget-audio pd-synth-auto pd-synth detect-i2s-device pd-synth-i2s; do \ - sudo mv /tmp/\$svc.service /etc/systemd/system/; \ + sudo mv /tmp/\${svc}.service /etc/systemd/system/; \ done && \ sudo mv /tmp/99-usb-role.rules /etc/udev/rules.d/ && \ sudo udevadm control --reload-rules && \ @@ -219,6 +222,27 @@ run-remote: echo "[RUN] Starting Pd on {{BOARD_USER}}@{{BOARD_HOST}} (auto-detect)" ssh $SCP_OPTS -t "{{BOARD_USER}}@{{BOARD_HOST}}" "killall pd 2>/dev/null; sudo {{SYSTEM_DIR}}/bin/start-synth.sh --auto --verbose" +# Fix UCM bug: removes nonexistent "RX HPH Mode" control +# that prevents PipeWire from enabling the Headphones UCM device. +# Must run once after OS updates or first install. +fix-ucm: + @ssh {{SSH_OPTS}} "{{BOARD_USER}}@{{BOARD_HOST}}" "\ + if grep -q 'RX HPH Mode' /usr/share/alsa/ucm2/codecs/qcom-lpass/rx-macro/HeadphoneEnableSeq.conf 2>/dev/null; then \ + echo '{{BOARD_PASS}}' | sudo -S sed -i '/RX HPH Mode/d' /usr/share/alsa/ucm2/codecs/qcom-lpass/rx-macro/HeadphoneEnableSeq.conf && \ + sudo systemctl restart wireplumber && \ + echo '[OK] UCM HeadphoneEnableSeq patched'; \ + else \ + echo '[OK] UCM already patched'; \ + fi" + +# Test onboard headphone audio (hw:0,0 — ArduinoImolaHPH) +test-audio-headphones: + @ssh {{SSH_OPTS}} "{{BOARD_USER}}@{{BOARD_HOST}}" "\ + systemctl --user stop pipewire.socket pipewire.service wireplumber 2>/dev/null; \ + sleep 0.5; \ + speaker-test -t sine -f 440 -l 2 -D hw:0,0 2>&1 | tail -3; \ + systemctl --user start pipewire.socket pipewire.service wireplumber 2>/dev/null" + # Test audio (gadget mode) test-audio: @ssh {{SSH_OPTS}} "{{BOARD_USER}}@{{BOARD_HOST}}" "speaker-test -D hw:UAC1Gadget,0 -c 2 -t sine -f 440 -l 1" @@ -233,6 +257,11 @@ test-audio-host: echo '[ERROR] No USB audio interface found'; \ fi" +# LED matrix end-to-end diagnostic (router → MCU → UDP → viz) +diag-led: + @scp {{SSH_OPTS}} scripts/diag-led.sh "{{BOARD_USER}}@{{BOARD_HOST}}:/tmp/diag-led.sh" + @ssh {{SSH_OPTS}} "{{BOARD_USER}}@{{BOARD_HOST}}" "chmod +x /tmp/diag-led.sh && bash /tmp/diag-led.sh" + # Deploy diagnostic script to board deploy-diag: @scp {{SSH_OPTS}} scripts/diag.sh "{{BOARD_USER}}@{{BOARD_HOST}}:{{SYSTEM_DIR}}/bin/diag.sh" diff --git a/mise.toml b/mise.toml index 445806b..004bfbb 100644 --- a/mise.toml +++ b/mise.toml @@ -9,6 +9,8 @@ just = "latest" [env] -BOARD_HOST = "uno-q.local" +BOARD_HOST = "192.168.9.167" BOARD_USER = "arduino" +BOARD_PASS = "arduino" +SSH_KEY = "/home/david/.ssh/unoq_deploy_key" PD_PATCH = "synth.pd" diff --git a/system/asound-i2s.conf b/scripts/asound-i2s.conf similarity index 100% rename from system/asound-i2s.conf rename to scripts/asound-i2s.conf diff --git a/system/asound.conf b/scripts/asound.conf similarity index 100% rename from system/asound.conf rename to scripts/asound.conf diff --git a/system/configure-usb-audio.sh b/scripts/configure-usb-audio.sh similarity index 100% rename from system/configure-usb-audio.sh rename to scripts/configure-usb-audio.sh diff --git a/system/configure-usb-host.sh b/scripts/configure-usb-host.sh similarity index 100% rename from system/configure-usb-host.sh rename to scripts/configure-usb-host.sh diff --git a/system/detect-i2s-device.sh b/scripts/detect-i2s-device.sh similarity index 100% rename from system/detect-i2s-device.sh rename to scripts/detect-i2s-device.sh diff --git a/system/detect-usb-role.sh b/scripts/detect-usb-role.sh similarity index 100% rename from system/detect-usb-role.sh rename to scripts/detect-usb-role.sh diff --git a/scripts/diag-led.sh b/scripts/diag-led.sh new file mode 100644 index 0000000..1421b67 --- /dev/null +++ b/scripts/diag-led.sh @@ -0,0 +1,169 @@ +#!/bin/bash +# diag-led.sh — LED matrix end-to-end diagnostic +# Run on the board: just diag-led +# Tests: router → MCU serial → draw_frame → OK +# UDP 8082 → viz process → rchar increase +set -euo pipefail + +PASS=0; FAIL=0 + +pass() { echo " ✓ $*"; PASS=$((PASS+1)); } +fail() { echo " ✗ $*"; FAIL=$((FAIL+1)); } + +echo "=== LED Matrix Diagnostic ===" +echo "" + +# --- Test 1: Router socket --- +echo "1. Router socket" +if [ -S /var/run/arduino-router.sock ]; then + pass "socket /var/run/arduino-router.sock exists" +else + fail "router socket missing" +fi + +# --- Test 2: arduino-router service --- +echo "2. arduino-router service" +if systemctl is-active arduino-router &>/dev/null; then + pass "arduino-router is running" +else + fail "arduino-router not running" +fi + +# --- Test 3: RPC connectivity --- +echo "3. Router RPC connectivity" +RPC_RESPONSE=$(python3 -c " +import socket, msgpack +try: + s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + s.settimeout(3) + s.connect('/var/run/arduino-router.sock') + p = msgpack.Packer() + s.sendall(p.pack([0, 99, 'get_status', ['']])) + s.settimeout(1) + resp = s.recv(256) + s.close() + print('OK:' + repr(resp[:60])) +except Exception as e: + print('FAIL:' + str(e)) +" 2>&1) +if echo "$RPC_RESPONSE" | grep -q "^OK:"; then + pass "get_status responded" +else + fail "get_status: $RPC_RESPONSE" +fi + +# --- Test 4: draw_frame --- +echo "4. draw_frame RPC" +FRAME_RESPONSE=$(python3 -c " +import socket, msgpack, struct +try: + s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + s.settimeout(3) + s.connect('/var/run/arduino-router.sock') + p = msgpack.Packer() + # Alternating columns test pattern + bits = 0 + for row in range(8): + for col in range(0, 13, 2): + bits |= 1 << (row * 13 + col) + f0 = bits & 0xFFFFFFFF + f1 = (bits >> 32) & 0xFFFFFFFF + f2 = (bits >> 64) & 0xFFFFFFFF + f3 = (bits >> 96) & 0xFFFFFFFF + s.sendall(p.pack([0, 100, 'draw_frame', [str(f0), str(f1), str(f2), str(f3)]])) + s.settimeout(1) + resp = s.recv(128) + s.close() + if b'OK' in resp: + print('OK') + else: + print('FAIL:' + repr(resp[:50])) +except Exception as e: + print('FAIL:' + str(e)) +" 2>&1) +if echo "$FRAME_RESPONSE" | grep -q "^OK"; then + pass "draw_frame returned OK" +else + fail "draw_frame: $FRAME_RESPONSE" +fi + +# --- Test 5: led-matrix-viz service --- +echo "5. led-matrix-viz service" +if systemctl is-active led-matrix-viz &>/dev/null; then + pass "led-matrix-viz is running" +else + fail "led-matrix-viz not running" +fi + +# --- Test 6: UDP receive (send note, check rchar) --- +echo "6. UDP → viz → router (end-to-end)" +VIZ_PID=$(pgrep -f 'python3.*led-matrix-viz' 2>/dev/null | head -1) +if [ -z "$VIZ_PID" ]; then + fail "viz process not found" +else + R1=$(sudo cat /proc/$VIZ_PID/io 2>/dev/null | grep rchar | awk '{print $2}') + W1=$(sudo cat /proc/$VIZ_PID/io 2>/dev/null | grep wchar | awk '{print $2}') + + python3 -c " +import socket +s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) +for n in [60, 64, 67, 72, 76, 79, 84]: + s.sendto(f'midi 144 {n} 100;'.encode(), ('127.0.0.1', 8082)) +s.close() +" 2>/dev/null + + sleep 2 + + R2=$(sudo cat /proc/$VIZ_PID/io 2>/dev/null | grep rchar | awk '{print $2}') + W2=$(sudo cat /proc/$VIZ_PID/io 2>/dev/null | grep wchar | awk '{print $2}') + + RDIFF=$((R2 - R1)) + WDIFF=$((W2 - W1)) + + if [ "$RDIFF" -gt 0 ]; then + pass "viz received +${RDIFF}r bytes, sent +${WDIFF}w bytes to router" + else + fail "viz rchar unchanged — not receiving UDP (r=$RDIFF, w=$WDIFF)" + fi +fi + +# --- Test 7: f_midi gadget --- +echo "7. f_midi gadget status" +if [ -f /proc/asound/card2/midi0 ]; then + RX=$(grep "Rx bytes" /proc/asound/card2/midi0 | awk '{print $4}') + pass "f_midi present (Rx=${RX} bytes)" +else + fail "f_midi gadget not found" +fi + +# --- Test 8: midi-bridge forwarding --- +echo "8. midi-bridge service" +if systemctl is-active midi-bridge &>/dev/null; then + MB_PID=$(pgrep -f 'python3.*midi-bridge' | head -1) + pass "midi-bridge running (PID=$MB_PID)" +else + fail "midi-bridge not running" +fi + +# --- Clear matrix, summary --- +python3 -c " +import socket, msgpack +s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) +s.settimeout(3) +s.connect('/var/run/arduino-router.sock') +p = msgpack.Packer() +s.sendall(p.pack([0, 200, 'clear', []])) +s.recv(128) +s.close() +" 2>/dev/null + +echo "" +echo "==============================" +if [ "$FAIL" -eq 0 ]; then + echo " ALL CHECKS PASSED ($PASS/$((PASS+FAIL)))" + echo " LED matrix chain is healthy." +else + echo " $PASS passed, $FAIL failed" +fi +echo "==============================" +exit $FAIL diff --git a/scripts/led-matrix-viz.py b/scripts/led-matrix-viz.py index 287d0b5..8ce4fa3 100644 --- a/scripts/led-matrix-viz.py +++ b/scripts/led-matrix-viz.py @@ -26,7 +26,6 @@ def main(): sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.bind(("0.0.0.0", VIZ_PORT)) - sock.setblocking(False) router = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) router.settimeout(2) diff --git a/system/on-usb-role-change.sh b/scripts/on-usb-role-change.sh similarity index 100% rename from system/on-usb-role-change.sh rename to scripts/on-usb-role-change.sh diff --git a/system/reset-usb-gadget.sh b/scripts/reset-usb-gadget.sh similarity index 100% rename from system/reset-usb-gadget.sh rename to scripts/reset-usb-gadget.sh diff --git a/scripts/start-synth.sh b/scripts/start-synth.sh index 918519e..bb15ccf 100755 --- a/scripts/start-synth.sh +++ b/scripts/start-synth.sh @@ -1,17 +1,23 @@ #!/bin/bash set -euo pipefail -PD_DIR="/home/arduino/pd" +REPO_DIR="/home/arduino/uno-q-audio-synth" +PD_DIR="$REPO_DIR/pd" PATCH="$PD_DIR/synth.pd" MODE="usb" VERBOSE=false +# Fallback to legacy path if REPO_DIR not cloned +[ -d "$PD_DIR" ] || PD_DIR="/home/arduino/pd" +PATCH="$PD_DIR/synth.pd" + die() { echo "[ERROR] $*" >&2; exit 1; } info() { echo "[INFO] $*"; } while [ $# -gt 0 ]; do case "$1" in -a|--auto) MODE="auto"; shift ;; + -o|--onboard) MODE="onboard"; shift ;; -i|--i2s) MODE="i2s"; shift ;; -l|--loopback) MODE="loopback"; shift ;; -v|--verbose) VERBOSE=true; shift ;; @@ -30,53 +36,7 @@ fi # --- Auto-detect mode --- if [ "$MODE" = "auto" ]; then - if [ -f /run/usb-role ]; then - ROLE=$(cat /run/usb-role) - else - ROLE=$(/usr/local/bin/detect-usb-role.sh 2>/dev/null && cat /run/usb-role || echo "gadget") - fi - - case "$ROLE" in - gadget) - MODE="usb" - if ! aplay -l 2>/dev/null | grep -q "UAC1Gadget"; then - info "Configuring USB gadget..." - /usr/local/bin/configure-usb-audio.sh start 2>/dev/null || true - sleep 1 - fi - ;; - host) - if [ -f /run/usb-audio-dev ] && [ -s /run/usb-audio-dev ]; then - ALSA_DEV=$(cat /run/usb-audio-dev) - else - info "Detecting USB audio interface..." - /usr/local/bin/configure-usb-host.sh 2>/dev/null || { - die "No USB audio interface found. Plug one in via hub." - } - ALSA_DEV=$(cat /run/usb-audio-dev) - fi - info "Mode: USB host (MIDI controller + USB audio interface)" - # MIDI handled by midi-bridge.service → UDP → Pd netreceive - AUDIO_OPTS=(-audiooutdev 4 -audioindev 4) - PD_ARGS=( - -nogui -alsa - -send "pd dsp 1" - "${AUDIO_OPTS[@]}" - -audiobuf 20 -blocksize 64 -r 48000 - -channels 2 -inchannels 0 -outchannels 2 - -path "$PD_DIR" "$PATCH" - ) - if [ "$VERBOSE" = true ]; then pd "${PD_ARGS[@]}" - else pd "${PD_ARGS[@]}" > /dev/null 2>&1 & - PID=$!; info "Pd started (PID: $PID)"; info " Stop: kill $PID" - fi - exit 0 - ;; - *) - info "Auto-detect: unknown role '$ROLE', falling back to gadget" - MODE="usb" - ;; - esac + MODE="onboard" fi # --- Configure per-mode options --- @@ -93,19 +53,9 @@ if [ "$MODE" = "usb" ]; then fi fi -if [ "$MODE" = "i2s" ]; then - if [ -f /run/i2s-audio-dev ] && [ -s /run/i2s-audio-dev ]; then - ALSA_DEV=$(cat /run/i2s-audio-dev) - else - for dev in "hw:I2S,0" "hw:MI2S,0" "hw:0,0"; do - if aplay -l 2>/dev/null | grep -qi "${dev%,*}"; then - ALSA_DEV="$dev" - break - fi - done - fi - info "Mode: I2S DAC (standalone analog audio)" - AUDIO_OPTS=(-audioadddev "$ALSA_DEV") +if [ "$MODE" = "onboard" ] || [ "$MODE" = "i2s" ]; then + info "Mode: Onboard audio (PMIC headphone / line out on JMISC)" + AUDIO_OPTS=(-audiooutdev 0 -inchannels 0 -outchannels 1) fi if [ "$MODE" = "loopback" ]; then diff --git a/system/detect-i2s-device.service b/system/detect-i2s-device.service index 3f4dfd9..d48e320 100644 --- a/system/detect-i2s-device.service +++ b/system/detect-i2s-device.service @@ -4,7 +4,7 @@ Before=pd-synth-i2s.service [Service] Type=oneshot -ExecStart=/usr/local/bin/detect-i2s-device.sh +ExecStart=/home/arduino/uno-q-audio-synth/scripts/detect-i2s-device.sh RemainAfterExit=yes [Install] diff --git a/system/led-matrix-viz.service b/system/led-matrix-viz.service index fa5ba84..3e50767 100644 --- a/system/led-matrix-viz.service +++ b/system/led-matrix-viz.service @@ -5,7 +5,7 @@ After=arduino-router.service [Service] User=arduino Type=simple -ExecStart=/usr/local/bin/led-matrix-viz.py +ExecStart=/home/arduino/uno-q-audio-synth/scripts/led-matrix-viz.py Restart=on-failure RestartSec=2 StandardOutput=journal diff --git a/system/midi-bridge.service b/system/midi-bridge.service index 3337a97..7a68d5d 100644 --- a/system/midi-bridge.service +++ b/system/midi-bridge.service @@ -4,7 +4,7 @@ After=multi-user.target [Service] Type=simple -ExecStart=/usr/local/bin/midi-bridge.py +ExecStart=/home/arduino/uno-q-audio-synth/scripts/midi-bridge.py User=root Restart=on-failure RestartSec=2 diff --git a/system/pd-synth-auto.service b/system/pd-synth-auto.service index 3f58000..0ddc962 100644 --- a/system/pd-synth-auto.service +++ b/system/pd-synth-auto.service @@ -6,7 +6,7 @@ Wants=usb-role-detect.service [Service] Type=simple -ExecStart=/usr/local/bin/start-synth.sh --auto --verbose +ExecStart=/home/arduino/uno-q-audio-synth/scripts/start-synth.sh --auto --verbose ExecStop=/usr/bin/killall -s TERM pd StandardOutput=journal StandardError=journal diff --git a/system/pd-synth-i2s.service b/system/pd-synth-i2s.service index dd91e93..36d143a 100644 --- a/system/pd-synth-i2s.service +++ b/system/pd-synth-i2s.service @@ -6,9 +6,9 @@ Wants=usb-gadget-audio.service [Service] Type=simple User=arduino -ExecStart=/usr/local/bin/start-synth.sh --i2s +ExecStart=/home/arduino/uno-q-audio-synth/scripts/start-synth.sh --i2s ExecStop=/usr/bin/killall pd -WorkingDirectory=/home/arduino/pd +WorkingDirectory=/home/arduino/uno-q-audio-synth Restart=on-failure RestartSec=2 diff --git a/system/pd-synth.service b/system/pd-synth.service index c8a77ca..9f04504 100644 --- a/system/pd-synth.service +++ b/system/pd-synth.service @@ -6,9 +6,9 @@ Wants=usb-gadget-audio.service midi-bridge.service [Service] Type=simple User=arduino -ExecStart=/usr/local/bin/start-synth.sh +ExecStart=/home/arduino/uno-q-audio-synth/scripts/start-synth.sh ExecStop=/usr/bin/killall pd -WorkingDirectory=/home/arduino/pd +WorkingDirectory=/home/arduino/uno-q-audio-synth Restart=on-failure RestartSec=2 diff --git a/system/usb-gadget-audio.service b/system/usb-gadget-audio.service index 37e3516..95d06ed 100644 --- a/system/usb-gadget-audio.service +++ b/system/usb-gadget-audio.service @@ -5,8 +5,8 @@ Before=pd-synth.service [Service] Type=oneshot -ExecStart=/usr/local/bin/configure-usb-audio.sh start -ExecStop=/usr/local/bin/configure-usb-audio.sh stop +ExecStart=/home/arduino/uno-q-audio-synth/scripts/configure-usb-audio.sh start +ExecStop=/home/arduino/uno-q-audio-synth/scripts/configure-usb-audio.sh stop RemainAfterExit=yes Restart=on-failure RestartSec=2 diff --git a/system/usb-role-detect.service b/system/usb-role-detect.service index febf581..22af8c8 100644 --- a/system/usb-role-detect.service +++ b/system/usb-role-detect.service @@ -6,7 +6,7 @@ Before=pd-synth-auto.service [Service] Type=oneshot -ExecStart=/usr/local/bin/detect-usb-role.sh +ExecStart=/home/arduino/uno-q-audio-synth/scripts/detect-usb-role.sh RemainAfterExit=yes StandardOutput=journal