onboard audio: JMISC HPH wiring, UCM fix, update scripts for git deploy

This commit is contained in:
2026-06-23 21:25:51 -07:00
parent a135dc4452
commit d239d58c44
22 changed files with 227 additions and 77 deletions
+1
View File
@@ -5,3 +5,4 @@ __pycache__/
*.log
/tmp/
build/
.env
+32 -3
View File
@@ -2,7 +2,9 @@
# Run with: just <recipe>
# 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"
+3 -1
View File
@@ -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"
+169
View File
@@ -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
-1
View File
@@ -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)
+11 -61
View File
@@ -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
+1 -1
View File
@@ -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]
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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