# Arduino Uno Q — USB Audio Synth # 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') 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 # ───────────────────────────────────────────────────────── # Copy Pd patch to board deploy-patch: #!/usr/bin/env bash set -e SCP_OPTS="{{SSH_OPTS}}" echo "[DEPLOY] Patching to {{BOARD_USER}}@{{BOARD_HOST}}:{{PD_DIR}}" ssh $SCP_OPTS "{{BOARD_USER}}@{{BOARD_HOST}}" "mkdir -p {{PD_DIR}}" scp $SCP_OPTS pd/synth.pd "{{BOARD_USER}}@{{BOARD_HOST}}:{{PD_DIR}}/" # Copy system config files + scripts to board deploy-system: #!/usr/bin/env bash set -e SCP_OPTS="{{SSH_OPTS}}" echo "[DEPLOY] System config to {{BOARD_USER}}@{{BOARD_HOST}}" for script in configure-usb-audio.sh configure-usb-host.sh detect-usb-role.sh detect-i2s-device.sh on-usb-role-change.sh; do scp $SCP_OPTS "system/$script" "{{BOARD_USER}}@{{BOARD_HOST}}:{{SYSTEM_DIR}}/bin/" done ssh $SCP_OPTS "{{BOARD_USER}}@{{BOARD_HOST}}" 'echo "{{BOARD_PASS}}" | sudo -S chmod +x {{SYSTEM_DIR}}/bin/configure-usb-*.sh {{SYSTEM_DIR}}/bin/detect-*.sh {{SYSTEM_DIR}}/bin/on-usb-role-change.sh' scp $SCP_OPTS system/asound.conf "{{BOARD_USER}}@{{BOARD_HOST}}:/tmp/" scp $SCP_OPTS system/asound-i2s.conf "{{BOARD_USER}}@{{BOARD_HOST}}:/tmp/" ssh $SCP_OPTS "{{BOARD_USER}}@{{BOARD_HOST}}" 'echo "{{BOARD_PASS}}" | sudo -S mv /tmp/asound.conf /etc/asound.conf && echo "{{BOARD_PASS}}" | sudo -S mv /tmp/asound-i2s.conf /etc/asound-i2s.conf' scp $SCP_OPTS scripts/start-synth.sh "{{BOARD_USER}}@{{BOARD_HOST}}:{{SYSTEM_DIR}}/bin/" ssh $SCP_OPTS "{{BOARD_USER}}@{{BOARD_HOST}}" 'echo "{{BOARD_PASS}}" | sudo -S chmod +x {{SYSTEM_DIR}}/bin/start-synth.sh' # Deploy everything deploy: deploy-patch deploy-system @echo "[OK] All files deployed" # Deploy midi-bridge script and service to board deploy-midi-bridge: #!/usr/bin/env bash set -e SCP_OPTS="{{SSH_OPTS}}" echo "[DEPLOY] midi-bridge to {{BOARD_USER}}@{{BOARD_HOST}}" scp $SCP_OPTS scripts/midi-bridge.py "{{BOARD_USER}}@{{BOARD_HOST}}:/tmp/" ssh $SCP_OPTS "{{BOARD_USER}}@{{BOARD_HOST}}" "sudo mv /tmp/midi-bridge.py {{SYSTEM_DIR}}/bin/ && sudo chmod +x {{SYSTEM_DIR}}/bin/midi-bridge.py" scp $SCP_OPTS system/midi-bridge.service "{{BOARD_USER}}@{{BOARD_HOST}}:/tmp/" ssh $SCP_OPTS "{{BOARD_USER}}@{{BOARD_HOST}}" "sudo mv /tmp/midi-bridge.service /etc/systemd/system/ && sudo systemctl daemon-reload" echo "[OK] midi-bridge deployed" # Deploy LED matrix visualization script and service deploy-led-matrix-viz: #!/usr/bin/env bash set -e SCP_OPTS="{{SSH_OPTS}}" echo "[DEPLOY] led-matrix-viz to {{BOARD_USER}}@{{BOARD_HOST}}" scp $SCP_OPTS scripts/led-matrix-viz.py "{{BOARD_USER}}@{{BOARD_HOST}}:/tmp/" ssh $SCP_OPTS "{{BOARD_USER}}@{{BOARD_HOST}}" "sudo mv /tmp/led-matrix-viz.py {{SYSTEM_DIR}}/bin/ && sudo chmod +x {{SYSTEM_DIR}}/bin/led-matrix-viz.py" scp $SCP_OPTS system/led-matrix-viz.service "{{BOARD_USER}}@{{BOARD_HOST}}:/tmp/" ssh $SCP_OPTS "{{BOARD_USER}}@{{BOARD_HOST}}" "sudo mv /tmp/led-matrix-viz.service /etc/systemd/system/ && sudo systemctl daemon-reload" echo "[OK] led-matrix-viz deployed" # Deploy everything (patch + system + midi-bridge + led-matrix-viz) deploy-all: deploy deploy-midi-bridge deploy-led-matrix-viz @echo "[OK] All files and scripts deployed" # Enable midi-bridge service enable-midi-bridge: @ssh {{SSH_OPTS}} "{{BOARD_USER}}@{{BOARD_HOST}}" "sudo systemctl enable midi-bridge && echo '[OK] midi-bridge enabled'" # Enable led-matrix-viz service enable-led-matrix-viz: @ssh {{SSH_OPTS}} "{{BOARD_USER}}@{{BOARD_HOST}}" "sudo systemctl enable led-matrix-viz && echo '[OK] led-matrix-viz enabled'" # Start & enable host mode services (midi-bridge + led-matrix-viz) enable-host-mode: deploy-midi-bridge deploy-led-matrix-viz @ssh {{SSH_OPTS}} "{{BOARD_USER}}@{{BOARD_HOST}}" "\ sudo systemctl enable midi-bridge led-matrix-viz && \ sudo systemctl start midi-bridge led-matrix-viz && \ echo '[OK] Host mode services enabled and started'" # Install all systemd 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/" 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/; \ done && \ sudo mv /tmp/99-usb-role.rules /etc/udev/rules.d/ && \ sudo udevadm control --reload-rules && \ sudo systemctl daemon-reload" # Enable auto-detect mode (default — works for both gadget and host) enable-auto: install-services @ssh {{SSH_OPTS}} "{{BOARD_USER}}@{{BOARD_HOST}}" "\ sudo systemctl disable usb-gadget-audio pd-synth detect-i2s-device pd-synth-i2s 2>/dev/null; \ sudo systemctl enable usb-role-detect pd-synth-auto && \ echo '[OK] Auto-detect mode enabled — reboot to apply'" # Enable fixed USB gadget mode (always acts as peripheral) enable-gadget: install-services @ssh {{SSH_OPTS}} "{{BOARD_USER}}@{{BOARD_HOST}}" "\ sudo systemctl disable usb-role-detect pd-synth-auto detect-i2s-device pd-synth-i2s 2>/dev/null; \ sudo systemctl enable usb-gadget-audio pd-synth && \ echo '[OK] USB gadget mode enabled — reboot to apply'" # Enable fixed I2S DAC mode enable-i2s: install-services @ssh {{SSH_OPTS}} "{{BOARD_USER}}@{{BOARD_HOST}}" "\ sudo systemctl disable usb-role-detect pd-synth-auto usb-gadget-audio pd-synth 2>/dev/null; \ sudo systemctl enable detect-i2s-device pd-synth-i2s && \ sudo cp /etc/asound-i2s.conf /etc/asound.conf && \ echo '[OK] I2S DAC mode enabled — reboot to apply'" # Start Pd (auto-detect or whichever service is enabled) start: @ssh {{SSH_OPTS}} "{{BOARD_USER}}@{{BOARD_HOST}}" "\ sudo systemctl start pd-synth-auto 2>/dev/null || \ sudo systemctl start pd-synth 2>/dev/null || \ sudo systemctl start pd-synth-i2s 2>/dev/null || \ echo '[ERROR] No Pd service found'" # Stop Pd stop: @ssh {{SSH_OPTS}} "{{BOARD_USER}}@{{BOARD_HOST}}" "sudo killall pd 2>/dev/null; echo '[OK] Pd stopped'" # Restart Pd restart: stop start # Reboot board reboot: @ssh {{SSH_OPTS}} "{{BOARD_USER}}@{{BOARD_HOST}}" "echo '{{BOARD_PASS}}' | sudo -S reboot" # Run Pd manually in auto-detect mode (for testing) run-remote: #!/usr/bin/env bash set -e SCP_OPTS="{{SSH_OPTS}}" 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" # 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" # Deploy diagnostic script to board deploy-diag: @scp {{SSH_OPTS}} scripts/diag.sh "{{BOARD_USER}}@{{BOARD_HOST}}:{{SYSTEM_DIR}}/bin/diag.sh" # Show system status (quick — uses diag.sh on board) status: @scp {{SSH_OPTS}} -q scripts/diag.sh "{{BOARD_USER}}@{{BOARD_HOST}}:/tmp/diag.sh" && \ ssh {{SSH_OPTS}} "{{BOARD_USER}}@{{BOARD_HOST}}" "echo '{{BOARD_PASS}}' | sudo -S bash /tmp/diag.sh 2>/dev/null" | head -60 # Test MIDI input (list ALSA sequencer ports) midi-test: @ssh {{SSH_OPTS}} "{{BOARD_USER}}@{{BOARD_HOST}}" "aconnect -i -o -l" # Quick deploy and restart cycle cycle: deploy-patch @ssh {{SSH_OPTS}} "{{BOARD_USER}}@{{BOARD_HOST}}" "sudo killall -s TERM pd 2>/dev/null; sleep 2; echo '[OK] Pd restarted with new patch'" # Tail Pd logs (auto-detect mode) log: @ssh {{SSH_OPTS}} "{{BOARD_USER}}@{{BOARD_HOST}}" "\ for svc in pd-synth-auto pd-synth pd-synth-i2s; do \ if systemctl is-active \$svc &>/dev/null; then \ sudo journalctl -u \$svc -f -n 50; \ exit 0; \ fi; \ done; \ echo '[ERROR] No Pd service is running'" # Configure USB gadget manually init-usb-audio: @ssh {{SSH_OPTS}} "{{BOARD_USER}}@{{BOARD_HOST}}" "sudo configure-usb-audio.sh start" # Full setup: deploy + enable auto-detect + reboot setup: deploy enable-auto @echo "[SETUP] Deployed and auto-detect enabled. Run 'just reboot' to start." # ───────────────────────────────────────────────────────── # 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: #!/usr/bin/env bash set -e SCP_OPTS="{{SSH_OPTS}}" HOST="{{BOARD_USER}}@{{BOARD_HOST}}" echo "==============================" echo " Uno Q Audio Synth Diagnostics" echo "==============================" # Push and run the diagnostic script on the board scp $SCP_OPTS scripts/diag.sh "$HOST:/tmp/diag.sh" >/dev/null ssh $SCP_OPTS "$HOST" "echo '{{BOARD_PASS}}' | sudo -S bash /tmp/diag.sh" echo "" echo "--- USB bus (host-side) ---" if command -v lsusb &>/dev/null; then lsusb -d 2341:006a 2>/dev/null | awk '{print " " $0}' || echo " (no Uno Q device on host bus)" lsusb -d 2341:006a -t 2>/dev/null | awk '{print " " $0}' else echo " (lsusb not available on host)" fi echo "--- Host ALSA ---" echo " Audio:" aplay -l 2>/dev/null | grep -i "synth\|uno" | awk '{print " " $0}' || echo " (not found on host)" echo " MIDI:" aconnect -l 2>/dev/null | grep -i "synth\|uno\|midi" | awk '{print " " $0}' || echo " (not found on host)" amidi -l 2>/dev/null | grep -i "synth\|uno" | awk '{print " " $0}' || echo " (not found on host)" echo "" echo "==============================" echo " Diagnostics Complete" echo "==============================" # 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}'" # Validate all .pd files using Conftest/OPA Rego policies validate: 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'"