2968d57e1d
Validate and Test / validate-patches (push) Failing after 13s
Without Bridge.run() in loop(), the MCU never reads incoming draw_frame requests from the router. First frame goes through during setup handshake, but subsequent frames fill the router's send buffer and the viz stalls.
363 lines
16 KiB
Makefile
363 lines
16 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}}" "\
|
|
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"
|
|
|
|
# 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}'"
|
|
|
|
# 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'"
|