Files
uno-q-audio-synth/justfile
T
david d4856804ff
Validate and Test / validate-patches (push) Failing after 13s
Fix onboard audio: apply ALSA UCM HiFi verb before Pd start
- Add alsaucm -c hw:0 set _verb HiFi to start-synth.sh (onboard mode)
- Add UCM verb to install.sh
- Add UCM verb to justfile fix-ucm recipe
- Without this, the DSP audio routing is not configured and no sound
  reaches the headphone output
2026-06-24 17:31:00 -07:00

447 lines
19 KiB
Makefile

# Arduino Uno Q — USB Audio Synth
# Run with: just <recipe>
# Override board host: just BOARD_HOST=192.168.1.100 deploy
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')
PD_DIR := "/home/" + BOARD_USER + "/pd"
SYSTEM_DIR := "/usr/local"
SSH_OPTS := "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i " + SSH_KEY
default:
@just --list
# ─────────────────────────────────────────────────────────
# FACTORY SETUP — first-time board configuration
# ─────────────────────────────────────────────────────────
# Full factory setup: connect to board via USB (ADB), set password,
# connect WiFi, enable SSH, install Pd, deploy project
factory-setup:
#!/usr/bin/env bash
set -e
echo "[SETUP] Arduino Uno Q — Factory to Synth"
echo ""
# Check ADB
if ! command -v adb &>/dev/null; then
echo "[ERROR] adb not found. Install: sudo apt install adb"
exit 1
fi
DEVICE=$(adb devices | grep -v "List" | grep "device$" | head -1 | awk '{print $1}')
if [ -z "$DEVICE" ]; then
echo "[ERROR] No ADB device found. Connect Uno Q via USB."
exit 1
fi
echo "[OK] Found ADB device: $DEVICE"
# Reset expired password
echo "[STEP] Setting board user password..."
adb -s "$DEVICE" shell 'printf "{{BOARD_PASS}}\n{{BOARD_PASS}}\n" | passwd' 2>/dev/null
# Connect WiFi
read -p "WiFi SSID: " WIFI_SSID
read -rsp "WiFi password: " WIFI_PASS
echo
adb -s "$DEVICE" shell "echo '{{BOARD_PASS}}' | sudo -S nmcli device wifi connect \"$WIFI_SSID\" password \"$WIFI_PASS\""
# Get IP
IP=$(adb -s "$DEVICE" shell "ip addr show wlan0 2>/dev/null | grep 'inet ' | awk '{print \$2}' | cut -d/ -f1")
if [ -z "$IP" ]; then
echo "[ERROR] Could not get IP address"
exit 1
fi
echo "[OK] Board IP: $IP"
# Generate SSH host keys + enable SSH
adb -s "$DEVICE" shell 'echo "{{BOARD_PASS}}" | sudo -S ssh-keygen -A 2>/dev/null'
adb -s "$DEVICE" shell 'echo "{{BOARD_PASS}}" | sudo -S systemctl enable --now ssh 2>/dev/null'
# Install SSH key
KEY=$(cat "{{SSH_KEY}}.pub" 2>/dev/null || echo "")
if [ -n "$KEY" ]; then
adb -s "$DEVICE" shell "mkdir -p ~/.ssh && echo '$KEY' >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys && chmod 700 ~/.ssh"
echo "[OK] SSH key installed"
else
echo "[WARN] No SSH key at {{SSH_KEY}}.pub — generate with: ssh-keygen -t ed25519 -f {{SSH_KEY}}"
fi
# Install packages
echo "[STEP] Installing puredata (this may take a while)..."
ssh {{SSH_OPTS}} "{{BOARD_USER}}@$IP" 'echo "{{BOARD_PASS}}" | sudo -S apt-get install -y -qq puredata alsa-utils 2>&1 | tail -3'
echo ""
echo "[DONE] Board is ready at $IP"
echo " Run: just BOARD_HOST=$IP setup"
# ─────────────────────────────────────────────────────────
# DEPLOY (git-based: commit+push locally, just deploy on board)
# ─────────────────────────────────────────────────────────
REPO_DIR := "/home/" + BOARD_USER + "/uno-q-audio-synth"
REPO_URL := "https://git.notsosm.art/david/uno-q-audio-synth.git"
# First-time install: clone repo + install services + fix UCM
install:
@ssh {{SSH_OPTS}} "{{BOARD_USER}}@{{BOARD_HOST}}" "\
rm -rf {{REPO_DIR}} 2>/dev/null; \
git clone {{REPO_URL}} {{REPO_DIR}} && \
echo '[OK] Repo cloned' && \
chmod +x {{REPO_DIR}}/scripts/*.py {{REPO_DIR}}/scripts/*.sh && \
bash {{REPO_DIR}}/install.sh && \
echo '[OK] Services installed'"
@echo "[OK] Run: just deploy"
# Deploy + reload services after git pull
deploy:
@git push origin HEAD 2>&1
@ssh {{SSH_OPTS}} "{{BOARD_USER}}@{{BOARD_HOST}}" "\
cd {{REPO_DIR}} && git stash && git pull && \
git config core.hooksPath .githooks && \
chmod +x scripts/*.py scripts/*.sh 2>/dev/null; \
echo '{{BOARD_PASS}}' | sudo -S systemctl daemon-reload && \
echo '{{BOARD_PASS}}' | sudo -S systemctl stop midi-bridge pd-synth-onboard led-matrix-viz && \
echo '{{BOARD_PASS}}' | sudo -S systemctl restart prometheus-exporter && \
echo '{{BOARD_PASS}}' | sudo -S systemctl start synth-diagnostic && \
echo '{{BOARD_PASS}}' | sudo -S systemctl restart midi-bridge pd-synth-onboard led-matrix-viz && \
echo '[OK] Deployed'"
# Start synth (onboard audio)
synth-onboard:
@ssh {{SSH_OPTS}} "{{BOARD_USER}}@{{BOARD_HOST}}" "sudo systemctl start pd-synth-onboard && echo '[OK] Synth started'"
# Stop synth
synth-stop:
@ssh {{SSH_OPTS}} "{{BOARD_USER}}@{{BOARD_HOST}}" "sudo systemctl stop pd-synth-onboard && echo '[OK] Stopped'"
# Restart all services
synth-restart:
@ssh {{SSH_OPTS}} "{{BOARD_USER}}@{{BOARD_HOST}}" "\
sudo systemctl restart midi-bridge pd-synth-onboard led-matrix-viz && \
echo '[OK] All services restarted'"
# Show service status
status:
@ssh {{SSH_OPTS}} "{{BOARD_USER}}@{{BOARD_HOST}}" "\
echo '=== Pd ==='; pgrep -a pd | grep -v watchdog | grep -v kswapd || echo '(not running)'; \
echo '=== Services ==='; \
for svc in midi-bridge pd-synth-onboard led-matrix-viz; do \
echo -n \" \$svc: \"; systemctl is-active \$svc 2>/dev/null || echo 'inactive'; \
done; \
echo '=== Audio ==='; cat /proc/asound/cards; \
echo '=== Git ==='; cd {{REPO_DIR}} && git log --oneline -1"
# Enable midi-bridge service
enable-midi-bridge:
@ssh {{SSH_OPTS}} "{{BOARD_USER}}@{{BOARD_HOST}}" "sudo systemctl enable midi-bridge && echo '[OK] midi-bridge enabled'"
# Reboot board
reboot:
@ssh {{SSH_OPTS}} "{{BOARD_USER}}@{{BOARD_HOST}}" "echo '{{BOARD_PASS}}' | sudo -S reboot"
# 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}}" "\
echo '{{BOARD_PASS}}' | sudo -S alsaucm -c hw:0 set _verb HiFi 2>/dev/null && \
echo '[OK] UCM verb applied' || \
echo '[WARN] UCM verb failed' && \
FILE=/usr/share/alsa/ucm2/codecs/qcom-lpass/rx-macro/HeadphoneEnableSeq.conf && \
FIXED=0 && \
if grep -q 'RX HPH Mode' \"\$FILE\" 2>/dev/null; then \
echo '{{BOARD_PASS}}' | sudo -S sed -i '/RX HPH Mode/d' \"\$FILE\" && \
FIXED=1; \
fi && \
if ! grep -q 'RX INT0_1 INTERP' \"\$FILE\" 2>/dev/null; then \
echo ' cset \"name=RX INT0_1 INTERP\" \"RX INT0_1 MIX1\"' | sudo tee -a \"\$FILE\" >/dev/null && \
FIXED=1; \
fi && \
if ! grep -q 'RX INT1_1 INTERP' \"\$FILE\" 2>/dev/null; then \
echo ' cset \"name=RX INT1_1 INTERP\" \"RX INT1_1 MIX1\"' | sudo tee -a \"\$FILE\" >/dev/null && \
FIXED=1; \
fi && \
if [ \"\$FIXED\" = \"1\" ]; then \
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"
# Test audio (host mode — first USB audio interface)
test-audio-host:
@ssh {{SSH_OPTS}} "{{BOARD_USER}}@{{BOARD_HOST}}" "\
card=\$(aplay -l 2>/dev/null | grep -i usb | grep -vi gadget | head -1 | awk '{print \$2}' | tr -d ':'); \
if [ -n \"\$card\" ]; then \
speaker-test -D hw:\$card,0 -c 2 -t sine -f 440 -l 1; \
else \
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"
# Full deploy + restart synth
cycle: deploy synth-stop synth-onboard
# Tail synth logs (last 50 lines)
log:
@ssh {{SSH_OPTS}} "{{BOARD_USER}}@{{BOARD_HOST}}" "tail -50 /tmp/synth.log"
# Full setup: install repo on board + fix UCM + start synth
setup: install fix-ucm synth-onboard
@echo "[SETUP] Done. Synth is running on onboard audio."
# ─────────────────────────────────────────────────────────
# LOCAL TESTING (on your computer)
# ─────────────────────────────────────────────────────────
# Open local plugdata controller + monitor for Uno Q synth
play-local:
#!/usr/bin/env bash
set -e
echo "[PLAY] Opening plugdata controller for Uno Q synth..."
echo ""
if ! command -v plugdata &>/dev/null; then
echo "[ERROR] plugdata not found"
exit 1
fi
if ! aconnect -l 2>/dev/null | grep -q "Uno Q Audio Synth"; then
echo "[ERROR] Uno Q not found in ALSA sequencer — is it connected?"
exit 1
fi
MIDI_PORT=$(aconnect -l 2>/dev/null | grep "Uno Q Audio Synth" | head -1 | awk '{print $2}' | tr -d ':')
echo " MIDI port: $MIDI_PORT (Uno Q Audio Synth)"
echo " Audio in: hw:Synth,0 (UAC1 gadget — synth return)"
echo " Patch: pd/controller.pd"
echo ""
echo " IN PLUGDATA, configure:"
echo " Settings → Audio → Device Type: ALSA"
echo " Settings → Audio → Input: hw:Synth,0"
echo " Settings → Audio → Output: Default"
echo " Settings → MIDI Input: all — MIDI Output: Uno Q"
echo " Enable DSP (Ctrl+D)"
echo ""
plugdata pd/controller.pd
# Start interactive play session: on-screen keyboard + audio monitor
play:
#!/usr/bin/env bash
set -e
echo "[PLAY] Starting Uno Q synth test environment..."
echo ""
# Install vmpk if missing
if ! command -v vmpk &>/dev/null; then
echo "[STEP] Installing vmpk..."
sudo apt-get install -y -qq vmpk
fi
# Verify Uno Q is connected via ALSA (should be visible through PipeWire)
if ! aconnect -l 2>/dev/null | grep -q "Uno Q Audio Synth"; then
echo "[ERROR] Uno Q not found in ALSA sequencer"
exit 1
fi
if ! pw-cli list-objects 2>/dev/null | grep -q "Uno Q Audio Synth"; then
echo "[ERROR] Uno Q not found via PipeWire"
[ -z "$(lsusb -d 2341:006a 2>/dev/null)" ] && echo " Device not showing in lsusb either!"
exit 1
fi
# Start vmpk in background
echo "[STEP] Starting on-screen keyboard..."
vmpk &
VMPK_PID=$!
sleep 1
# Route vmpk MIDI -> Uno Q via ALSA sequencer
VMPK_PORT=$(aconnect -l 2>/dev/null | grep -B1 "Virtual MIDI Piano" | head -1 | awk '{print $2}' | tr -d ':')
UNOQ_PORT=$(aconnect -l 2>/dev/null | grep "Uno Q Audio Synth" | head -1 | awk '{print $2}' | tr -d ':')
if [ -n "$VMPK_PORT" ] && [ -n "$UNOQ_PORT" ]; then
aconnect "$VMPK_PORT" "$UNOQ_PORT" 2>/dev/null && echo " MIDI: vmpk -> Uno Q Synth"
else
echo " [WARN] Auto-connect failed. Use: aconnect \$VMPK_PORT \$UNOQ_PORT"
fi
# Route Uno Q audio -> host speakers via PipeWire
UNOQ_CAPTURE=$(pw-cli list-objects 2>/dev/null | grep -B1 "alsa_input.*Uno_Q" | grep "node.name" | head -1 | awk -F'"' '{print $2}')
HOST_OUTPUT="alsa_output.pci-0000_00_1f.3.analog-stereo"
if [ -n "$UNOQ_CAPTURE" ]; then
pw-link "$UNOQ_CAPTURE:capture_MONO" "$HOST_OUTPUT:playback_FL" 2>/dev/null && \
pw-link "$UNOQ_CAPTURE:capture_MONO" "$HOST_OUTPUT:playback_FR" 2>/dev/null && \
echo " Audio: Uno Q Synth -> host speakers"
else
echo " [WARN] Could not find Uno Q audio capture node"
fi
echo ""
echo "=============================="
echo " Synth is LIVE!"
echo " Click keys in the vmpk window"
echo " Close vmpk or Ctrl+C to stop"
echo "=============================="
echo ""
wait $VMPK_PID 2>/dev/null
echo "[STOP] Cleaning up..."
pw-disconnect "$UNOQ_CAPTURE:capture_MONO" "$HOST_OUTPUT:playback_FL" 2>/dev/null || true
pw-disconnect "$UNOQ_CAPTURE:capture_MONO" "$HOST_OUTPUT:playback_FR" 2>/dev/null || true
kill $VMPK_PID 2>/dev/null || true
aconnect -x 2>/dev/null || true
echo "[OK] Done"
# Stop test session
stop-play:
@pw-disconnect "$(pw-cli list-objects 2>/dev/null | grep -B1 "alsa_input.*Uno_Q" | grep "node.name" | head -1 | awk -F'"' '{print $2}'):capture_MONO" "alsa_output.pci-0000_00_1f.3.analog-stereo:playback_FL" 2>/dev/null; \
pw-disconnect "$(pw-cli list-objects 2>/dev/null | grep -B1 "alsa_input.*Uno_Q" | grep "node.name" | head -1 | awk -F'"' '{print $2}'):capture_MONO" "alsa_output.pci-0000_00_1f.3.analog-stereo:playback_FR" 2>/dev/null; \
killall vmpk 2>/dev/null; aconnect -x 2>/dev/null; echo "[OK] Stopped"
# Show current MIDI routing
midi-route:
@echo "=== ALSA MIDI Connections ===" && aconnect -l && echo "" && echo "=== Active connections ===" && aconnect -lio 2>/dev/null | grep -A1 "Connected From\|Connected To" || echo "(none)"
# ─────────────────────────────────────────────────────────
# DIAGNOSTICS
# ─────────────────────────────────────────────────────────
# Full diagnostic dump of the board
diag:
@ssh {{SSH_OPTS}} "{{BOARD_USER}}@{{BOARD_HOST}}" "\
echo '=== Git ==='; cd {{REPO_DIR}} && git log --oneline -3; \
echo '=== Pd ==='; pgrep -a pd 2>/dev/null || echo '(not running)'; \
echo '=== Audio ==='; cat /proc/asound/cards; \
echo '=== PipeWire ==='; wpctl status 2>&1 | head -10; \
echo '=== USB ==='; lsusb 2>/dev/null"
# Show kernel module info for custom modules
modinfo:
@ssh {{SSH_OPTS}} "{{BOARD_USER}}@{{BOARD_HOST}}" "\
for m in usb_f_uac1 usb_f_midi; do \
echo \"=== \$m ===\"; \
modinfo \$m 2>/dev/null | grep -E 'filename|description|version|parm' | awk '{print \" \" \$0}'; \
echo ''; \
done"
# Check kernel config for USB gadget features
kconfig:
@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}'"
# 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
flash-sketch:
@ssh {{SSH_OPTS}} "{{BOARD_USER}}@{{BOARD_HOST}}" "\
cd {{REPO_DIR}} && \
INCS=\$(find /home/arduino/Arduino/libraries -type d -not -path '*/Arduino_RouterBridge*' -not -path '*/Arduino_RPClite*' 2>/dev/null | sort -u | sed 's/^/-I/' | tr '\n' ' ') && \
arduino-cli compile --upload --fqbn arduino:zephyr:unoq --build-property \"build.extra_flags=\$INCS\" sketch/"
# Validate all .pd files using Conftest/OPA Rego policies
validate:
python3 pd-validator/validate.py pd/*.pd
# CI: run full validation suite (same as validate but used by Gitea Actions)
ci:
python3 pd-validator/validate.py pd/*.pd
# Install udev rules for hotplug role changes
install-udev-rules:
@ssh {{SSH_OPTS}} "{{BOARD_USER}}@{{BOARD_HOST}}" "\
sudo cp /etc/udev/rules.d/99-usb-role.rules /etc/udev/rules.d/99-usb-role.rules.bak 2>/dev/null; \
sudo cp /tmp/99-usb-role.rules /etc/udev/rules.d/ && \
sudo udevadm control --reload-rules && \
echo '[OK] udev rules installed'"