Initial commit: Arduino Uno Q USB Audio Synth project

- USB gadget (UAC1 + MIDI) with boot persistence
- USB host mode with auto-detection
- Pure Data synth patch with ALSA + UDP netreceive MIDI input
- MIDI rawmidi-to-UDP bridge (midi-bridge.py)
- LED matrix visualization (led-matrix-viz.py via arduino-router RPC)
- MCU sketch for LED matrix control (Arduino_RouterBridge + ArduinoGraphics)
- Conftest/OPA Rego policy validation suite for .pd files
- gen_synth_pd.py: programmatic .pd file generator
- System services: midi-bridge, led-matrix-viz, pd-synth-auto, usb-role-detect
- Justfile with deploy, enable, diagnostic recipes
- Diagnostic script (diag.sh)
This commit is contained in:
2026-06-22 22:46:36 -07:00
commit 8cd796e3dd
38 changed files with 2681 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
local M = {}
M.board = 'arduino:qualcomm:uno_q'
M.port = '/dev/ttyACM0'
M.baudrate = 115200
return M
+20
View File
@@ -0,0 +1,20 @@
# Arduino/C++ friendly format
BasedOnStyle: WebKit
IndentWidth: 4
TabWidth: 4
UseTab: Never
IndentCaseLabels: false
BreakBeforeBraces: Attach
SpaceBeforeParens: Never
SpaceAfterCStyleCast: false
PointerAlignment: Left
ColumnLimit: 100
SortIncludes: true
IncludeBlocks: Regroup
KeepEmptyLinesAtTheStartOfBlocks: false
+7
View File
@@ -0,0 +1,7 @@
__pycache__/
*.pyc
*.swp
.DS_Store
*.log
/tmp/
build/
+5
View File
@@ -0,0 +1,5 @@
{
"MD013": false,
"MD060": false,
"MD025": false
}
+157
View File
@@ -0,0 +1,157 @@
# Arduino Uno Q — USB Audio Synth
A 2-oscillator subtractive synthesizer running Pure Data on the Arduino Uno Q.
**Auto-detects** its USB role at boot and works in three modes — no config switching needed.
## Modes
| Mode | Connection | Audio Out | MIDI In | Use Case |
|------|-----------|-----------|---------|----------|
| **Gadget** | USB-C → Host computer | UAC1 gadget (virtual sound card) | gmidi gadget (over same cable) | Plug-and-play with DAWs |
| **Host** | USB hub → MIDI controller + USB audio interface | USB audio interface (your interface) | MIDI controller via hub | Standalone jamming |
| **I2S DAC** | I2S DAC on GPIO pins (e.g. PCM5102) | Analog line out via DAC | USB MIDI controller (gadget) | Embedded installs |
## Auto-Detection
At boot, `detect-usb-role.sh` probes the USB controller to determine the role:
```
USB cable plugged into Host? ─→ gadget mode (UAC1 + gmidi)
USB device plugged into Uno? ─→ host mode (drives USB audio interface + MIDI)
Neither? ─→ defaults to gadget (safe fallback)
```
A udev rule (`99-usb-role.rules`) re-runs detection on hotplug events and
restarts Pd with the new config — unplug from computer and plug into a hub,
it just works.
## Architecture
```
┌─────────────────────────────────────────────────────────┐
│ GADGET MODE │
│ [Host DAW] ←USB-C→ ┌──────────┐ │
│ │ gmidi │←─ ALSA seq ──→ Pd │
│ │ UAC1 │── ALSA pcm ──→ Pd │
│ └──────────┘ │
├─────────────────────────────────────────────────────────┤
│ HOST MODE │
│ [MIDI Ctrl] ─┐ │
│ [USB Audio] ─┼─USB hub─→ ┌──────────┐ │
│ │ │ USB host │ │
│ └──────────→ │ driver │──→ Pd │
│ └──────────┘ │
├─────────────────────────────────────────────────────────┤
│ I2S DAC MODE │
│ [PCM5102] ←GPIO(I2S)→ ALSA I2S driver ──→ Pd │
└─────────────────────────────────────────────────────────┘
```
## Project Structure
```
uno-q-audio-synth/
├── pd/
│ └── synth.pd # Pure Data synthesizer patch
├── system/
│ ├── configure-usb-audio.sh # UAC1 + gmidi gadget setup
│ ├── configure-usb-host.sh # Host-mode USB audio discovery
│ ├── detect-usb-role.sh # Boot-time USB role detection
│ ├── detect-i2s-device.sh # I2S device auto-detection
│ ├── on-usb-role-change.sh # udev hotplug handler
│ ├── asound.conf # ALSA config (gadget mode)
│ ├── asound-i2s.conf # ALSA config (I2S DAC mode)
│ ├── 99-usb-role.rules # udev rules for hotplug
│ ├── usb-role-detect.service # Early-boot role detection
│ ├── usb-gadget-audio.service # Gadget-only service
│ ├── pd-synth-auto.service # Unified auto-detect service
│ ├── pd-synth.service # Gadget-only Pd service
│ ├── pd-synth-i2s.service # I2S-only Pd service
│ └── detect-i2s-device.service # I2S probe service
├── scripts/
│ └── start-synth.sh # Manual dev startup (all modes)
├── install.sh # Board-side dependency install
├── justfile # Task runner
└── mise.toml # Tool version management
```
## Quick Start
### Deploy to board
```bash
export BOARD_HOST=uno-q.local # or IP if mDNS not working
just deploy # copy all files
just enable-auto # enable auto-detect mode
just reboot # apply
```
Once booted, plug into a computer — it appears as "Uno Q Audio Synth" with
audio sink + MIDI port. Or plug a USB hub with MIDI controller + audio
interface for standalone play.
### Development cycle
```bash
just cycle # deploy patch + restart Pd
just log # tail Pd logs
just status # role, ALSA cards, MIDI ports
```
### Manual testing
```bash
just test-audio # test gadget audio out
just test-audio-host # test host-mode USB audio interface
just run-remote # run Pd in foreground with auto-detect
```
## I2S DAC Wiring
Add a DAC later without reconfiguring. Common I2S DAC modules:
| DAC | Pins | Voltage |
|-----|------|---------|
| PCM5102 | BCK, DIN, LCK, 5V, GND | 3.3V5V |
| MAX98357 (speaker amp) | BCLK, DIN, LRCLK, Vin, GND | 2.7V5.5V |
| PCM5242 (balanced out) | I2S + I2C config | 3.3V |
Typical PCM5102 wiring to Uno Q GPIO:
```
PCM5102 → Uno Q GPIO
VIN → 5V
GND → GND
BCK → I2S_BCK (GPIO pin — check board docs)
DIN → I2S_DIN
LCK → I2S_LCK
```
Activate I2S mode: `just enable-i2s && just reboot`
## Controls (MIDI)
| Control | Mapping |
|---------|---------|
| MIDI Note On/Off | Play notes |
| Velocity | Volume |
| CC 74 | Filter cutoff |
| GUI sliders (Pd) | Detune, volume, ADSR |
## Requirements (on the Uno Q)
- Pure Data (`apt install puredata`)
- ALSA utilities (`apt install alsa-utils`)
- Linux kernel with UAC + gmidi gadget support (Qualcomm DragonWing)
## Pd Patch: synth.pd
- **OSC 1**: Saw wave, direct pitch
- **OSC 2**: Saw wave, detunable (±50 cents)
- **Mix**: Balanced stereo mix
- **Filter**: Low-pass (lop~) with adjustable cutoff
- **Envelope**: env~ (attack, decay, sustain, release)
- **Output**: Stereo 48kHz 16-bit via dac~
Open in Pd GUI to adjust slider positions and save.
Executable
+27
View File
@@ -0,0 +1,27 @@
#!/bin/bash
# install.sh — Install system dependencies on the Uno Q
# Run this on the board: ssh arduino@uno-q.local 'bash -s' < install.sh
set -euo pipefail
echo "[INSTALL] Updating packages..."
sudo apt update -qq
echo "[INSTALL] Installing Pure Data..."
sudo apt install -y -qq puredata alsa-utils
echo "[INSTALL] Installing USB gadget config..."
sudo cp system/configure-usb-audio.sh /usr/local/bin/
sudo chmod +x /usr/local/bin/configure-usb-audio.sh
sudo cp system/asound.conf /etc/asound.conf
echo "[INSTALL] Installing systemd services..."
sudo cp system/usb-gadget-audio.service /etc/systemd/system/
sudo cp system/pd-synth.service /etc/systemd/system/
sudo systemctl daemon-reload
echo "[INSTALL] Enabling services..."
sudo systemctl enable usb-gadget-audio
sudo systemctl enable pd-synth
echo "[INSTALL] Done. Reboot with: sudo reboot"
+448
View File
@@ -0,0 +1,448 @@
# Arduino Uno Q — USB Audio Synth
# 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')
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'"
+14
View File
@@ -0,0 +1,14 @@
# ═══════════════════════════════════════════════════════════
# Arduino Uno Q — USB Audio Synth
#
# Tasks are managed by just (see justfile), not mise.
# Run `just --list` for available commands.
# ═══════════════════════════════════════════════════════════
[tools]
just = "latest"
[env]
BOARD_HOST = "uno-q.local"
BOARD_USER = "arduino"
PD_PATCH = "synth.pd"
+271
View File
@@ -0,0 +1,271 @@
package main
# ─── Pd .pd file format validator (OPA v1.x syntax) ───
# Based on puredata.info/docs/developer/PdFileFormat
# OPA version: 1.15.2 (bundled with conftest 0.68.2)
# Input structure (from pd2json parser):
# {
# "x_record_count": N,
# "records": [{ "type": "obj"|"connect"|..., "x_index": N, ... }],
# "parse_errors": [...]
# }
# Get a record by its x_index
record_by_idx(idx) := r if {
some r_rec in input.records
r_rec.x_index == idx
r := r_rec
}
xcount := input.x_record_count
# Check if an object is a signal (~) type
is_signal(name) if {
endswith(name, "~")
}
# ═══════════════════════════════════════════════════════
# VIOLATIONS
# ═══════════════════════════════════════════════════════
# Conftest evaluates rules named `violation`, `violations`, `deny`, `warn`
# Out-of-bounds source index
violation contains msg if {
some rec in input.records
rec.type == "connect"
rec.src >= xcount
msg := sprintf("connect[%d]: src index %d out of range (max %d)", [rec.x_index, rec.src, xcount - 1])
}
# Out-of-bounds destination index
violation contains msg if {
some rec in input.records
rec.type == "connect"
rec.dst >= xcount
msg := sprintf("connect[%d]: dst index %d out of range (max %d)", [rec.x_index, rec.dst, xcount - 1])
}
# Negative source
violation contains msg if {
some rec in input.records
rec.type == "connect"
rec.src < 0
msg := sprintf("connect[%d]: negative src index %d", [rec.x_index, rec.src])
}
# Negative destination
violation contains msg if {
some rec in input.records
rec.type == "connect"
rec.dst < 0
msg := sprintf("connect[%d]: negative dst index %d", [rec.x_index, rec.dst])
}
# Negative outlet
violation contains msg if {
some rec in input.records
rec.type == "connect"
rec.outlet < 0
msg := sprintf("connect[%d]: negative outlet %d", [rec.x_index, rec.outlet])
}
# Negative inlet
violation contains msg if {
some rec in input.records
rec.type == "connect"
rec.inlet < 0
msg := sprintf("connect[%d]: negative inlet %d", [rec.x_index, rec.inlet])
}
# Self-loop
violation contains msg if {
some rec in input.records
rec.type == "connect"
rec.src == rec.dst
msg := sprintf("connect[%d]: self-loop (src=%d dst=%d)", [rec.x_index, rec.src, rec.dst])
}
# Duplicate connections
violation contains msg if {
some a, b in input.records
a.type == "connect"
b.type == "connect"
a.x_index < b.x_index
a.src == b.src
a.outlet == b.outlet
a.dst == b.dst
a.inlet == b.inlet
msg := sprintf("connect[%d]/[%d]: duplicate (%d,%d)->(%d,%d)", [a.x_index, b.x_index, a.src, a.outlet, a.dst, a.inlet])
}
# Signal-to-control connection
violation contains msg if {
some rec in input.records
rec.type == "connect"
src_rec := record_by_idx(rec.src)
dst_rec := record_by_idx(rec.dst)
src_rec.type == "obj"
dst_rec.type == "obj"
is_signal(src_rec.name)
not is_signal(dst_rec.name)
msg := sprintf("connect[%d]: signal (~) outlet %d (%s) to control inlet %d (%s)", [rec.x_index, rec.src, src_rec.name, rec.dst, dst_rec.name])
}
# No canvas definition
violation contains msg if {
not has_canvas
msg := "No #N canvas definition found"
}
has_canvas if {
some rec in input.records
rec.type == "canvas"
}
# Outlet exceeds known count (skip -1 = variable)
violation contains msg if {
some rec in input.records
rec.type == "connect"
src_rec := record_by_idx(rec.src)
src_rec.type == "obj"
known_outlets[src_rec.name]
known_outlets[src_rec.name] != -1
rec.outlet >= known_outlets[src_rec.name]
msg := sprintf("connect[%d]: obj %d (%s) has %d outlets, referenced outlet %d", [rec.x_index, rec.src, src_rec.name, known_outlets[src_rec.name], rec.outlet])
}
# Inlet exceeds known count (skip -1 = variable)
violation contains msg if {
some rec in input.records
rec.type == "connect"
dst_rec := record_by_idx(rec.dst)
dst_rec.type == "obj"
known_inlets[dst_rec.name]
known_inlets[dst_rec.name] != -1
rec.inlet >= known_inlets[dst_rec.name]
msg := sprintf("connect[%d]: obj %d (%s) has %d inlets, referenced inlet %d", [rec.x_index, rec.dst, dst_rec.name, known_inlets[dst_rec.name], rec.inlet])
}
# ═══════════════════════════════════════════════════════
# KNOWLEDGE BASE: inlet/outlet counts by object name
# ═══════════════════════════════════════════════════════
# -1 means variable/unknown
known_inlets := {
# Audio signal objects
"osc~": 1, "phasor~": 1, "saw~": 1, "noise~": 0,
"*~": 2, "+~": 2, "-~": 2, "/~": 2, "pow~": 2,
"lop~": 2, "hip~": 2, "bp~": 3, "vcf~": 3,
"cos~": 1, "wrap~": 1, "abs~": 1,
"clip~": 3, "env~": 4,
"dbtopower~": 1, "powertodb~": 1,
"mtof": 1, "ftom": 1,
"dac~": 2, "adc~": 0,
"sig~": 1, "snapshot~": 1,
"line~": 2, "vline~": 2,
"tabread4~": 2, "tabwrite~": 2, "tabread~": 2,
"send~": 1, "receive~": 1,
"delwrite~": 2, "delread~": 1, "vd~": 2,
"rfft~": 1, "rifft~": 1,
"inlet~": 0, "outlet~": 0,
"block~": 0, "switch~": 1,
# MIDI
"notein": 0, "noteout": 1,
"ctlin": 0, "ctlout": 1,
"midiin": 0, "midiout": 1,
# Data flow
"pack": -1, "unpack": 1,
"makenote": 2, "stripnote": 2,
"route": 1, "select": 1, "spigot": 2,
"trigger": 1, "t": 1,
"float": 1, "f": 1, "symbol": 1, "int": 1,
# Math
"+": 2, "-": 2, "*": 2, "/": 2, "pow": 2,
"div": 2, "mod": 2,
"max": 2, "min": 2,
"moses": 2, "until": 1,
"timer": 1, "random": 1,
# Data
"array": 1, "tabread": 1, "tabwrite": 2,
"text": 1, "textfile": 1,
"value": 0, "v": 0,
"soundfiler": 1,
# GUI
"openpanel": 0, "savepanel": 0,
"cnv": 0, "hsl": 1, "vsl": 1, "hradio": 1, "vradio": 1,
"bng": 0, "tgl": 0, "nbx": 1, "vu": 0,
"print": 1,
# Time
"pipe": 1, "delay": 1, "del": 1,
"metro": 1, "tempo": 1,
"line": 2, "poly": 2,
# Send/receive
"s": 1, "send": 1, "r": 0, "receive": 0,
"netsend": 2, "netreceive": 1,
# Misc
"loadbang": 0, "loadb": 0, "bang": 0,
"inlet": 0, "outlet": 0,
"expr": 1, "clone": 1,
}
# Conftest checks deny, warn, and violation (singular)
known_outlets := {
# Audio signal
"osc~": 1, "phasor~": 1, "saw~": 1, "noise~": 1,
"*~": 1, "+~": 1, "-~": 1, "/~": 1, "pow~": 1,
"lop~": 1, "hip~": 1, "bp~": 1, "vcf~": 1,
"cos~": 1, "wrap~": 1, "abs~": 1,
"clip~": 1, "env~": 1,
"dbtopower~": 1, "powertodb~": 1,
"mtof": 1, "ftom": 1,
"dac~": 0, "adc~": 2,
"sig~": 1, "snapshot~": 1,
"line~": 1, "vline~": 1,
"tabread4~": 1, "tabwrite~": 0, "tabread~": 1,
"send~": 0, "receive~": 1,
"delwrite~": 0, "delread~": 1, "vd~": 1,
"rfft~": 2, "rifft~": 1,
"inlet~": 1, "outlet~": 0,
"block~": 0, "switch~": 0,
# MIDI
"notein": 2, "noteout": 0,
"ctlin": 1, "ctlout": 0,
"midiin": 1, "midiout": 0,
# Data flow
"pack": 1, "unpack": -1,
"makenote": 0, "stripnote": 2,
"route": -1, "select": 1, "spigot": 2,
"trigger": -1, "t": -1,
"float": 1, "f": 1, "symbol": 1, "int": 1,
# Math
"+": 1, "-": 1, "*": 1, "/": 1,
"div": 1, "mod": 1, "pow": 1,
"max": 1, "min": 1,
"moses": 2, "until": 1,
"timer": 2, "random": 1,
# Data
"array": 2, "tabread": 1, "tabwrite": 0,
"text": 2, "textfile": 2,
"value": 1, "v": 1,
"soundfiler": 0,
# GUI
"openpanel": 1, "savepanel": 1,
"cnv": 1,
"hsl": 1, "vsl": 1, "hradio": 1, "vradio": 1,
"bng": 1, "tgl": 1, "nbx": 2, "vu": 0,
"print": 0,
# Time
"pipe": 1, "delay": 1, "del": 1,
"metro": 1, "tempo": 2,
"line": 1, "poly": 1,
# Send/receive
"s": 0, "send": 0, "r": 1, "receive": 1,
"netsend": 0, "netreceive": 1,
# Misc
"loadbang": 1, "loadb": 1, "bang": 1,
"inlet": 0, "outlet": 0,
"expr": 1, "clone": -1,
}
+161
View File
@@ -0,0 +1,161 @@
#!/usr/bin/env python3
"""Parse a .pd file into JSON for Conftest/Rego validation."""
import json, sys, re
from pathlib import Path
def parse_pd(filepath):
with open(filepath) as f:
text = f.read()
lines = text.split("\n")
records = []
x_index = 0
errors = []
for i, raw in enumerate(lines):
line = raw.strip()
if not line or line == ";":
continue
if not line.startswith("#"):
errors.append(f"line {i+1}: expected # prefix")
continue
raw = line[1:].strip()
if not raw.endswith(";"):
errors.append(f"line {i+1}: missing terminating semicolon")
continue
body = raw[:-1].strip()
if line.startswith("#N"):
nbody = body[1:].strip() # remove 'N'
parts = nbody.split(None, 1)
if parts[0] == "canvas":
tokens = parts[1].split()
rec = {
"type": "canvas",
"x": safe_int(tokens[0]),
"y": safe_int(tokens[1]),
"width": safe_int(tokens[2]),
"height": safe_int(tokens[3]),
"name": tokens[4] if len(tokens) > 4 else "",
}
records.append(rec)
else:
records.append({"type": "new", "data": rest})
elif line.startswith("#A"):
values = [safe_float(v) for v in rest.split()]
records.append({"type": "array_data", "values": values})
elif line.startswith("#X connect"):
xbody = body[1:].strip()
elems = xbody.split()
if len(elems) >= 5:
rec = {
"type": "connect",
"x_index": x_index,
"src": safe_int(elems[1]),
"outlet": safe_int(elems[2]),
"dst": safe_int(elems[3]),
"inlet": safe_int(elems[4]),
}
records.append(rec)
# Connect records do NOT increment Pd's object index
elif line.startswith("#X obj"):
xbody = body[1:].strip()
# xbody = "obj 20 20 notein"
elem, x_str, y_str, rest = xbody.split(None, 3)
obj_parts = rest.split()
rec = {
"type": "obj",
"x_index": x_index,
"x": safe_int(x_str),
"y": safe_int(y_str),
"name": obj_parts[0] if obj_parts else "",
"args": obj_parts[1:],
}
records.append(rec)
x_index += 1
elif line.startswith("#X msg"):
xbody = body[1:].strip()
elem, x_str, y_str, content = xbody.split(None, 3)
records.append({
"type": "msg",
"x_index": x_index,
"x": safe_int(x_str),
"y": safe_int(y_str),
"content": content,
})
x_index += 1
elif line.startswith("#X floatatom"):
xbody = body[1:].strip()
elems = xbody.split()
rec = {"type": "floatatom", "x_index": x_index, "x": safe_int(elems[1]), "y": safe_int(elems[2])}
if len(elems) > 3: rec["width"] = safe_int(elems[3])
if len(elems) > 4: rec["min"] = safe_float(elems[4])
if len(elems) > 5: rec["max"] = safe_float(elems[5])
records.append(rec)
x_index += 1
elif line.startswith("#X symbolatom"):
records.append({"type": "symbolatom", "x_index": x_index})
x_index += 1
elif line.startswith("#X text"):
xbody = body[1:].strip()
records.append({"type": "text", "x_index": x_index, "content": xbody})
x_index += 1
elif line.startswith("#X restore"):
xbody = body[1:].strip()
records.append({"type": "restore", "x_index": x_index, "data": xbody})
x_index += 1
elif line.startswith("#X coords"):
xbody = body[1:].strip()
records.append({"type": "coords", "x_index": x_index, "data": xbody})
x_index += 1
elif line.startswith("#X array"):
xbody = body[1:].strip()
records.append({"type": "array", "x_index": x_index, "data": xbody})
x_index += 1
else:
errors.append(f"line {i+1}: unknown record type: {line[:60]}")
return {
"file": str(filepath),
"x_record_count": x_index,
"records": records,
"parse_errors": errors,
}
def safe_int(s):
try:
# Handle Pd special values like "-" for empty
if s == "-":
return 0
return int(s)
except (ValueError, IndexError):
return 0
def safe_float(s):
try:
if s == "-":
return 0.0
return float(s)
except (ValueError, IndexError):
return 0.0
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: pd2json.py <file.pd> [file2.pd ...]")
sys.exit(1)
results = []
for arg in sys.argv[1:]:
results.append(parse_pd(arg))
print(json.dumps(results if len(results) > 1 else results[0], indent=2))
+14
View File
@@ -0,0 +1,14 @@
#N canvas 0 0 400 300 10;
#X obj 20 20 notein;
#X obj 80 20 osc~;
#X connect 0 0 1 0;
#X connect 0 1 1 1;
#X connect 0 0 1 0;
#X connect 99 0 1 0;
#X connect 0 0 99 0;
#X connect 0 0 0 0;
#X connect 0 -1 1 0;
#X obj 20 100 dac~;
#X connect 1 99 3 0;
#X connect 1 0 3 99;
#X connect 1 0 3 0;
+121
View File
@@ -0,0 +1,121 @@
#!/usr/bin/env python3
"""Validate .pd files using Conftest/OPA Rego policies."""
import json, sys, subprocess, tempfile, os, glob
from pathlib import Path
from pd2json import parse_pd
def main():
policy_dir = Path(__file__).parent
# Find conftest/opa (check mise install dirs first)
mise_dir = os.path.expanduser("~/.local/share/mise/installs")
for tool in ["conftest", "opa"]:
for ver_dir in sorted(glob.glob(os.path.join(mise_dir, tool, "*")), reverse=True):
for p in [os.path.join(ver_dir, tool), os.path.join(ver_dir, "bin", tool)]:
if os.path.isfile(p):
os.environ["PATH"] = os.path.dirname(p) + ":" + os.environ.get("PATH", "")
break
else:
continue
break
runner = None
for cmd in ["conftest", "opa"]:
try:
subprocess.run([cmd, "--version"], capture_output=True, timeout=5)
runner = cmd
break
except (FileNotFoundError, subprocess.TimeoutExpired):
continue
if not runner:
print("ERROR: neither 'conftest' nor 'opa' found in PATH.")
print("Install: pip install conftest or https://www.conftest.dev/")
sys.exit(1)
pd_files = sys.argv[1:] or list(Path(".").glob("*.pd"))
if not pd_files:
print("Usage: validate.py <file.pd> [file2.pd ...]")
sys.exit(1)
all_passed = True
for pf in pd_files:
pf = Path(pf)
data = parse_pd(pf)
if runner == "conftest":
result = run_conftest(data, policy_dir)
else:
result = run_opa(data, policy_dir)
if result["passed"]:
print(f"{pf.name} — all checks passed")
else:
all_passed = False
print(f"{pf.name}{len(result['violations'])} violation(s):")
for v in result["violations"]:
print(f"{v}")
sys.exit(0 if all_passed else 1)
def run_conftest(data, policy_dir):
with tempfile.NamedTemporaryFile(suffix=".json", mode="w", delete=False) as f:
json.dump(data, f)
json_path = f.name
try:
result = subprocess.run(
["conftest", "test", "--policy", str(policy_dir), json_path],
capture_output=True, text=True, timeout=30
)
out = result.stdout + result.stderr
violations = []
for l in out.split("\n"):
if "FAIL" in l or "failed" in l.lower():
violations.append(l.strip())
# Also parse structured output if json
if result.stdout and not violations:
try:
import json as j
report = j.loads(result.stdout)
for fname, checks in report.get("failures", {}).items():
for c in checks:
violations.append(c)
except:
pass
return {"passed": result.returncode == 0, "violations": violations}
finally:
os.unlink(json_path)
def run_opa(data, policy_dir):
pkg = "main"
query = "data.main.violations"
with tempfile.NamedTemporaryFile(suffix=".json", mode="w", delete=False) as f:
json.dump(data, f)
json_path = f.name
try:
result = subprocess.run(
["opa", "eval", "-d", str(policy_dir), "-d", json_path, "--format", "json", query],
capture_output=True, text=True, timeout=30
)
if result.returncode != 0:
return {"passed": False, "violations": [result.stderr.strip()]}
out = json.loads(result.stdout)
violations = []
for expr in out.get("result", []):
for binding in expr.get("expressions", []):
vals = binding.get("value", [])
if isinstance(vals, list):
violations.extend(vals)
return {"passed": len(violations) == 0, "violations": violations}
finally:
os.unlink(json_path)
if __name__ == "__main__":
main()
+79
View File
@@ -0,0 +1,79 @@
#N canvas 0 0 900 500 10;
#X obj 5 5 loadbang;
#X msg 5 25 pd dsp 1;
#X connect 0 0 1 0;
#X text 20 10 -- NOTE BUTTONS (click to play) --;
#X text 22 50 C3;
#X obj 20 70 bng 15 15 0 1 0 0 empty empty -2 -8 0 10 -262144 -1 -1;
#X msg 20 100 48 100;
#X obj 20 130 noteout;
#X connect 4 0 5 0;
#X connect 5 0 6 0;
#X text 62 50 D3;
#X obj 60 70 bng 15 15 0 1 0 0 empty empty -2 -8 0 10 -262144 -1 -1;
#X msg 60 100 50 100;
#X obj 60 130 noteout;
#X connect 8 0 9 0;
#X connect 9 0 10 0;
#X text 102 50 E3;
#X obj 100 70 bng 15 15 0 1 0 0 empty empty -2 -8 0 10 -262144 -1 -1;
#X msg 100 100 52 100;
#X obj 100 130 noteout;
#X connect 12 0 13 0;
#X connect 13 0 14 0;
#X text 142 50 F3;
#X obj 140 70 bng 15 15 0 1 0 0 empty empty -2 -8 0 10 -262144 -1 -1;
#X msg 140 100 53 100;
#X obj 140 130 noteout;
#X connect 16 0 17 0;
#X connect 17 0 18 0;
#X text 182 50 G3;
#X obj 180 70 bng 15 15 0 1 0 0 empty empty -2 -8 0 10 -262144 -1 -1;
#X msg 180 100 55 100;
#X obj 180 130 noteout;
#X connect 20 0 21 0;
#X connect 21 0 22 0;
#X text 222 50 A3;
#X obj 220 70 bng 15 15 0 1 0 0 empty empty -2 -8 0 10 -262144 -1 -1;
#X msg 220 100 57 100;
#X obj 220 130 noteout;
#X connect 24 0 25 0;
#X connect 25 0 26 0;
#X text 262 50 B3;
#X obj 260 70 bng 15 15 0 1 0 0 empty empty -2 -8 0 10 -262144 -1 -1;
#X msg 260 100 59 100;
#X obj 260 130 noteout;
#X connect 28 0 29 0;
#X connect 29 0 30 0;
#X text 302 50 C4;
#X obj 300 70 bng 15 15 0 1 0 0 empty empty -2 -8 0 10 -262144 -1 -1;
#X msg 300 100 60 100;
#X obj 300 130 noteout;
#X connect 32 0 33 0;
#X connect 33 0 34 0;
#X text 20 210 -- CONTROLS --;
#X obj 30 230 hsl 128 15 0 127 64 0 0 0 empty Filter-CC -2 -8 0 10 -262144 -1 -1 7200 1;
#X obj 30 270 ctlout 74;
#X connect 36 0 37 0;
#X text 20 310 -- AUDIO MONITOR --;
#X obj 30 330 adc~ 1;
#X obj 30 370 *~;
#X obj 30 410 dac~ 1 2;
#X obj 140 330 hsl 128 15 0 100 50 0 0 0 empty Monitor-Volume -2 -8 0 10 -262144 -1 -1 7200 1;
#X obj 140 370 / 100;
#X connect 39 0 40 0;
#X connect 40 0 41 0;
#X connect 40 0 41 1;
#X connect 42 0 43 0;
#X connect 43 0 40 1;
+64
View File
@@ -0,0 +1,64 @@
#N canvas 0 0 900 550 10;
#X obj 20 20 notein;
#X obj 80 50 pack 0 0;
#X obj 80 80 unpack 0 0;
#X obj 20 110 s midi-pitch;
#X obj 80 110 s midi-vel;
#X obj 400 20 netreceive -u 8081;
#X obj 400 60 route midi;
#X obj 400 100 route 144 128;
#X obj 400 140 unpack 0 0;
#X obj 480 140 unpack 0 0;
#X obj 400 180 s midi-pitch;
#X obj 480 180 s midi-vel;
#X obj 20 160 r midi-vel;
#X obj 20 190 r midi-pitch;
#X obj 20 220 stripnote;
#X obj 20 260 mtof;
#X obj 20 290 env~ 50 200 0.7 400;
#X obj 140 260 osc~;
#X obj 140 290 *~ 0.3;
#X obj 220 260 osc~;
#X obj 220 290 *~ 0.3;
#X obj 180 330 +~;
#X obj 180 360 *~ 0.5;
#X obj 180 400 lop~;
#X obj 180 460 *~;
#X obj 320 460 *~;
#X obj 250 530 dac~;
#X obj 350 330 hsl 128 15 100 18000 10000 0 0 0 empty Cutoff -2 -8 0 10 -262144 -1 -1 7200 1;
#X obj 350 430 hsl 128 15 0 100 40 0 0 0 empty Volume -2 -8 0 10 -262144 -1 -1 7200 1;
#X obj 350 460 / 100;
#X connect 0 0 1 0;
#X connect 0 1 1 1;
#X connect 1 0 2 0;
#X connect 2 0 3 0;
#X connect 2 1 4 0;
#X connect 5 0 6 0;
#X connect 6 0 7 0;
#X connect 7 0 8 0;
#X connect 7 1 9 0;
#X connect 8 0 10 0;
#X connect 8 1 11 0;
#X connect 9 0 10 0;
#X connect 9 1 11 0;
#X connect 12 0 14 1;
#X connect 13 0 14 0;
#X connect 14 0 15 0;
#X connect 15 0 17 0;
#X connect 15 0 19 0;
#X connect 17 0 18 0;
#X connect 19 0 20 0;
#X connect 18 0 21 0;
#X connect 20 0 21 1;
#X connect 21 0 22 0;
#X connect 22 0 23 0;
#X connect 23 0 24 0;
#X connect 14 0 16 0;
#X connect 16 0 24 1;
#X connect 24 0 25 0;
#X connect 27 0 23 1;
#X connect 28 0 29 0;
#X connect 29 0 25 1;
#X connect 25 0 26 0;
#X connect 25 0 26 1;
+103
View File
@@ -0,0 +1,103 @@
#!/bin/bash
# diag.sh — run on the board for diagnostics
set -e
echo "--- System ---"
echo " Kernel: $(uname -r)"
echo " Uptime: $(uptime -p | sed 's/up //')"
echo " Load: $(cat /proc/loadavg | cut -d' ' -f1-3)"
echo " Memory: $(free -h | awk '/^Mem:/{print $3 "/" $2}')"
echo " Disk: $(df -h / | awk 'NR==2{print $3 "/" $2 " (" $5 ")"}')"
echo ""
echo "--- USB Gadget (configfs) ---"
UDC=$(cat /sys/kernel/config/usb_gadget/g1/UDC 2>/dev/null || echo '(not bound)')
echo " UDC bound: $UDC"
for f in /sys/kernel/config/usb_gadget/g1/functions/*/; do
echo " Function: $(basename "$f")"
done
for c in /sys/kernel/config/usb_gadget/g1/configs/*/; do
cfg=$(basename "$c")
for l in "$c"/*.*/; do
[ -L "$l" ] && echo " linked: $(basename "$l") -> config $cfg"
done
done
echo ""
echo "--- UDC Hardware ---"
for u in /sys/class/udc/*/; do
name=$(basename "$u")
echo " Controller: $name"
echo " state: $(cat "$u/state" 2>/dev/null)"
echo " speed: $(cat "$u/current_speed" 2>/dev/null)"
echo " max_speed: $(cat "$u/maximum_speed" 2>/dev/null)"
done
role=$(cat /sys/class/usb_role/*/role 2>/dev/null || echo '(none)')
echo " USB role: $role"
echo " Role sw: $(ls /sys/class/usb_role/ 2>/dev/null | tr '\n' ' ')"
echo ""
echo "--- USB Audio Params ---"
for p in /sys/kernel/config/usb_gadget/g1/functions/uac1.gs0/p_* /sys/kernel/config/usb_gadget/g1/functions/uac1.gs0/c_*; do
name=$(basename "$p")
echo " $name: $(cat "$p" 2>/dev/null)"
done
echo ""
echo "--- USB MIDI Params ---"
for p in in_ports out_ports index; do
val=$(cat /sys/kernel/config/usb_gadget/g1/functions/midi.gs0/$p 2>/dev/null)
echo " $p: $val"
done
echo ""
echo "--- Modules ---"
lsmod | awk '$1 ~ /^(usb_f_|u_audio|libcomposite|snd_usb|snd_rawmidi|snd_soc|dwc3)/ {print " " $0}'
for m in usb_f_uac1 usb_f_midi dwc3-qcom-legacy; do
if lsmod 2>/dev/null | grep -q "^$m "; then
echo " $m: LOADED"
else
echo " $m: not loaded"
fi
done
echo ""
echo "--- ALSA Cards ---"
cat /proc/asound/cards
echo ""
aplay -l 2>/dev/null | awk '{print " " $0}'
echo ""
arecord -l 2>/dev/null | awk '{print " " $0}'
echo ""
echo "--- ALSA MIDI ---"
echo " Raw MIDI devices:"
ls /dev/snd/midi* 2>/dev/null | awk '{print " " $0}' || echo " (none)"
echo " ALSA sequencer:"
aconnect -lio 2>/dev/null | awk '{print " " $0}' || echo " (none)"
echo " amidi ports:"
amidi -l 2>/dev/null | awk '{print " " $0}' || echo " (none)"
echo ""
echo "--- Pd Status ---"
pd_pid=$(pgrep -x pd 2>/dev/null || echo "")
if [ -n "$pd_pid" ]; then
echo " PID: $pd_pid"
echo " Args: $(tr '\0' ' ' < /proc/$pd_pid/cmdline 2>/dev/null)"
echo " Uptime: $(ps -o etime= -p $pd_pid 2>/dev/null | xargs)"
echo " CPU: $(ps -o %cpu= -p $pd_pid 2>/dev/null | xargs)%"
echo " MEM: $(ps -o %mem= -p $pd_pid 2>/dev/null | xargs)%"
else
echo " Pd: not running"
fi
for svc in pd-synth-auto pd-synth pd-synth-i2s usb-role-detect usb-gadget-audio; do
state=$(systemctl is-active "$svc" 2>/dev/null || echo '(not found)')
enabled=$(systemctl is-enabled "$svc" 2>/dev/null || echo '(not found)')
echo " $svc: $state (enabled: $enabled)"
done
echo ""
echo "--- dmesg (USB/gadget errors) ---"
dmesg -l err,warn 2>/dev/null | grep -iE 'usb|gadget|uac1|midi|pd|audio|dwc3' | tail -20 | awk '{print " " $0}'
echo ""
dmesg 2>/dev/null | grep -i 'failed to start\|incorrect sample\|couldn.t find.*UDC\|probe.*failed' | tail -5 | awk '{print " " $0}'
+115
View File
@@ -0,0 +1,115 @@
#!/usr/bin/env python3
"""Generate synth.pd programmatically."""
from pathlib import Path
SCRIPT_DIR = Path(__file__).resolve().parent
OUTPUT = SCRIPT_DIR.parent / "pd" / "synth.pd"
def connect(c, src, sout, dst, din):
c.append((src, sout, dst, din))
def obj(o, x, y, *args):
idx = len(o)
o.append((x, y, "obj", *args))
return idx
def gen():
o = [] # objects
c = [] # connections
def Obj(x, y, *args):
return obj(o, x, y, *args)
# ── ALSA MIDI input (gadget mode) ──
notein = Obj(20, 20, "notein")
pack_note = Obj(80, 50, "pack 0 0")
unpack_note = Obj(80, 80, "unpack 0 0")
s_pitch_n = Obj(20, 110, "s midi-pitch")
s_vel_n = Obj(80, 110, "s midi-vel")
connect(c, notein, 0, pack_note, 0)
connect(c, notein, 1, pack_note, 1)
connect(c, pack_note, 0, unpack_note, 0)
connect(c, unpack_note, 0, s_pitch_n, 0)
connect(c, unpack_note, 1, s_vel_n, 0)
# ── UDP MIDI input (host mode via midi-bridge) ──
net = Obj(400, 20, "netreceive -u 8081")
route_midi = Obj(400, 60, "route midi")
route_note = Obj(400, 100, "route 144 128")
unpack2_on = Obj(400, 140, "unpack 0 0")
unpack2_off = Obj(480, 140, "unpack 0 0")
s_pitch_u = Obj(400, 180, "s midi-pitch")
s_vel_u = Obj(480, 180, "s midi-vel")
connect(c, net, 0, route_midi, 0)
connect(c, route_midi, 0, route_note, 0)
connect(c, route_note, 0, unpack2_on, 0)
connect(c, route_note, 1, unpack2_off, 0)
connect(c, unpack2_on, 0, s_pitch_u, 0)
connect(c, unpack2_on, 1, s_vel_u, 0)
connect(c, unpack2_off, 0, s_pitch_u, 0)
connect(c, unpack2_off, 1, s_vel_u, 0)
# ── Synth voice ──
r_vel = Obj(20, 160, "r midi-vel")
r_pitch = Obj(20, 190, "r midi-pitch")
strip = Obj(20, 220, "stripnote")
mtof = Obj(20, 260, "mtof")
env = Obj(20, 290, "env~ 50 200 0.7 400")
osc1 = Obj(140, 260, "osc~")
mul1 = Obj(140, 290, "*~ 0.3")
osc2 = Obj(220, 260, "osc~")
mul2 = Obj(220, 290, "*~ 0.3")
sum = Obj(180, 330, "+~")
scale = Obj(180, 360, "*~ 0.5")
filt = Obj(180, 400, "lop~")
amp = Obj(180, 460, "*~")
panscale = Obj(320, 460, "*~")
dac = Obj(250, 530, "dac~")
connect(c, r_vel, 0, strip, 1)
connect(c, r_pitch, 0, strip, 0)
connect(c, strip, 0, mtof, 0)
connect(c, mtof, 0, osc1, 0)
connect(c, mtof, 0, osc2, 0)
connect(c, osc1, 0, mul1, 0)
connect(c, osc2, 0, mul2, 0)
connect(c, mul1, 0, sum, 0)
connect(c, mul2, 0, sum, 1)
connect(c, sum, 0, scale, 0)
connect(c, scale, 0, filt, 0)
connect(c, filt, 0, amp, 0)
connect(c, strip, 0, env, 0)
connect(c, env, 0, amp, 1)
connect(c, amp, 0, panscale, 0)
# ── Controls ──
cutoff = Obj(350, 330, "hsl 128 15 100 18000 10000 0 0 0 empty Cutoff -2 -8 0 10 -262144 -1 -1 7200 1")
volume = Obj(350, 430, "hsl 128 15 0 100 40 0 0 0 empty Volume -2 -8 0 10 -262144 -1 -1 7200 1")
div = Obj(350, 460, "/ 100")
connect(c, cutoff, 0, filt, 1)
connect(c, volume, 0, div, 0)
connect(c, div, 0, panscale, 1)
# ── Output ──
connect(c, panscale, 0, dac, 0)
connect(c, panscale, 0, dac, 1)
# ── Write ──
lines = ["#N canvas 0 0 900 550 10;"]
for x, y, t, *args in o:
joined = " ".join(str(a) for a in args)
lines.append(f"#X obj {x} {y} {joined};")
for src, sout, dst, din in c:
lines.append(f"#X connect {src} {sout} {dst} {din};")
lines.append("")
return "\n".join(lines)
if __name__ == "__main__":
content = gen()
OUTPUT.parent.mkdir(parents=True, exist_ok=True)
OUTPUT.write_text(content)
print(f"Wrote {OUTPUT}")
# Print connections for debugging
for line in content.splitlines():
if line.startswith("#X connect"):
print(f" {line}")
+100
View File
@@ -0,0 +1,100 @@
#!/usr/bin/env python3
"""Listen for MIDI events via UDP and display note activity on LED matrix.
Uses RPC requests (not notifications) for reliability, with rate limiting.
"""
import socket, msgpack, time, os, re
VIZ_PORT = int(os.environ.get("VIZ_PORT", "8082"))
ROUTER_SOCK = os.environ.get("ROUTER_SOCK", "/var/run/arduino-router.sock")
DECAY_FRAMES = int(os.environ.get("DECAY_FRAMES", "8"))
MIN_INTERVAL = 1 / 15
COLS = 13
ROWS = 8
def pack_frame(pixels):
bits = 0
for row in range(ROWS):
for col in range(COLS):
if pixels[row][col]:
bits |= 1 << (row * COLS + col)
return (bits & 0xFFFFFFFF, (bits >> 32) & 0xFFFFFFFF,
(bits >> 64) & 0xFFFFFFFF, (bits >> 96) & 0xFFFFFFFF)
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)
router.connect(ROUTER_SOCK)
packer = msgpack.Packer()
active_notes = {}
last_frame = None
last_send = 0
buf = b""
msg_id = 0
while True:
now = time.monotonic()
try:
while True:
data, _ = sock.recvfrom(1024)
buf += data
except BlockingIOError:
pass
while b";" in buf:
msg, buf = buf.split(b";", 1)
msg = msg.decode().strip()
parts = msg.split()
if len(parts) >= 3 and parts[0] == "midi":
status = int(parts[1])
note = int(parts[2])
vel = int(parts[3]) if len(parts) >= 4 else 0
msg_type = status & 0xF0
if msg_type == 0x90 and vel > 0:
active_notes[note] = DECAY_FRAMES
elif msg_type == 0x80 or (msg_type == 0x90 and vel == 0):
active_notes.pop(note, None)
if now - last_send >= MIN_INTERVAL:
pixels = [[0] * COLS for _ in range(ROWS)]
for note, decay in list(active_notes.items()):
if decay <= 0:
del active_notes[note]
continue
active_notes[note] = decay - 1
note_min, note_max = 36, 96
row = 7 - int((note - note_min) / (note_max - note_min) * 7)
row = max(0, min(7, row))
col = note % 12
pixels[row][col] = 1
frame = pack_frame(pixels)
if frame != last_frame:
msg_id = (msg_id + 1) % 65535
params = [str(v) for v in frame]
req = packer.pack([0, msg_id, "draw_frame", params])
try:
router.sendall(req)
router.settimeout(0.3)
router.recv(128)
router.settimeout(2)
except socket.timeout:
pass
except OSError:
router.close()
router = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
router.settimeout(2)
router.connect(ROUTER_SOCK)
last_frame = frame
last_send = now
if __name__ == "__main__":
main()
+76
View File
@@ -0,0 +1,76 @@
#!/usr/bin/env python3
"""Bridge raw MIDI device to Pd via UDP. For host mode with MPK Mini.
Sends parsed MIDI messages as Pd-style 'midi status data1 data2;' strings
to UDP ports for Pd (8081) and LED matrix visualization (8082).
"""
import os, socket, time, select, struct
MIDI_DEV = os.environ.get("MIDI_BRIDGE_DEV", "/dev/snd/midiC0D0")
PD_HOST = os.environ.get("MIDI_BRIDGE_HOST", "127.0.0.1")
PD_PORT = int(os.environ.get("MIDI_BRIDGE_PORT", "8081"))
VIZ_PORT = int(os.environ.get("VIZ_PORT", "8082"))
def midi_msg(status, data1, data2=None):
if data2 is not None:
return f"midi {status} {data1} {data2};"
return f"midi {status} {data1};"
def main():
while not os.path.exists(MIDI_DEV):
time.sleep(0.5)
fd = os.open(MIDI_DEV, os.O_RDWR | os.O_NONBLOCK)
os.set_blocking(fd, False)
poll = select.poll()
poll.register(fd, select.POLLIN)
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
running = True
buf = bytearray()
while running:
try:
events = poll.poll(500)
for f, flags in events:
if flags & select.POLLIN:
data = os.read(fd, 1024)
if not data:
continue
for byte in data:
if byte & 0x80:
if len(buf) > 0 and buf[0] >= 0x80:
_flush(sock, buf, PD_HOST, PD_PORT, VIZ_PORT)
buf = bytearray([byte])
else:
buf.append(byte)
if len(buf) >= 3 and (buf[0] & 0xE0) in (0x80, 0x90, 0xA0, 0xB0, 0xE0):
_flush(sock, buf, PD_HOST, PD_PORT, VIZ_PORT)
buf = bytearray()
elif len(buf) >= 2 and (buf[0] & 0xE0) in (0xC0, 0xD0):
_flush(sock, buf, PD_HOST, PD_PORT, VIZ_PORT)
buf = bytearray()
except (BlockingIOError, OSError) as e:
if isinstance(e, OSError) and e.errno == 19:
time.sleep(1)
continue
if not isinstance(e, BlockingIOError):
raise
except KeyboardInterrupt:
running = False
os.close(fd)
sock.close()
def _flush(sock, buf, pd_host, pd_port, viz_port):
status = buf[0]
if (buf[0] & 0xF0) == 0xF0:
return
data1 = buf[1]
data2 = buf[2] if len(buf) >= 3 else None
msg = midi_msg(status, data1, data2)
sock.sendto(msg.encode(), (pd_host, pd_port))
sock.sendto(msg.encode(), (pd_host, viz_port))
if __name__ == "__main__":
main()
+152
View File
@@ -0,0 +1,152 @@
#!/bin/bash
set -euo pipefail
PD_DIR="/home/arduino/pd"
PATCH="$PD_DIR/synth.pd"
MODE="usb"
VERBOSE=false
die() { echo "[ERROR] $*" >&2; exit 1; }
info() { echo "[INFO] $*"; }
while [ $# -gt 0 ]; do
case "$1" in
-a|--auto) MODE="auto"; shift ;;
-i|--i2s) MODE="i2s"; shift ;;
-l|--loopback) MODE="loopback"; shift ;;
-v|--verbose) VERBOSE=true; shift ;;
-k|--kill) pkill -x pd 2>/dev/null && info "Pd stopped" || info "Pd not running"; exit 0 ;;
*) die "Unknown option: $1" ;;
esac
done
if ! command -v pd &>/dev/null; then
die "Pure Data not found. Install: apt install puredata"
fi
if [ ! -f "$PATCH" ]; then
die "Patch not found: $PATCH"
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_OPTS=(-alsamidi)
midi_dev=$(amidi -l 2>/dev/null | grep -v "^Dir" | head -1 | awk '{print $2}' | sed 's/\(hw:[0-9]*,[0-9]*\).*/\1/')
if [ -n "$midi_dev" ]; then
MIDI_OPTS+=(-midiadddev "$midi_dev")
info " MIDI device: $midi_dev"
fi
AUDIO_OPTS=(-audiooutdev 4 -audioindev 4)
PD_ARGS=(
-nogui -alsa
-send "pd dsp 1"
"${MIDI_OPTS[@]}"
"${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
fi
# --- Configure per-mode options ---
AUDIO_OPTS=()
MIDI_OPTS=(-alsamidi)
if [ "$MODE" = "usb" ]; then
info "Mode: USB gadget (UAC1 audio + gmidi MIDI)"
AUDIO_OPTS=(-audiooutdev 2 -audioindev 2)
if aplay -l 2>/dev/null | grep -q "UAC1Gadget"; then
info " USB gadget: OK"
midi_dev=$(amidi -l 2>/dev/null | grep "f_midi" | head -1 | awk '{print $2}')
if [ -n "$midi_dev" ]; then
MIDI_OPTS+=(-midiadddev "$midi_dev")
info " MIDI device: $midi_dev"
fi
else
info " USB gadget: NOT DETECTED (kernel modules needed)"
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")
fi
if [ "$MODE" = "loopback" ]; then
info "Mode: ALSA loopback (testing)"
AUDIO_OPTS=(-audiodev 2)
fi
PD_ARGS=(
-nogui
-alsa
"${MIDI_OPTS[@]}"
"${AUDIO_OPTS[@]}"
-send "pd dsp 1"
-audiobuf 20
-blocksize 64
-r 48000
-channels 1
-inchannels 0
-outchannels 1
-path "$PD_DIR"
"$PATCH"
)
if [ "$VERBOSE" = true ]; then
pd "${PD_ARGS[@]}" &
else
pd "${PD_ARGS[@]}" > /dev/null 2>&1 &
fi
PID=$!
info "Pd started (PID: $PID)"
info " Stop: kill $PID"
wait $PID
info "Pd exited (code: $?)"
+125
View File
@@ -0,0 +1,125 @@
#include "ArduinoGraphics.h"
#include "Arduino_LED_Matrix.h"
#include "Arduino_RouterBridge.h"
Arduino_LED_Matrix matrix;
String last_displayed = "";
String clear_matrix(String arg) {
matrix.clear();
last_displayed = "";
return "OK";
}
String draw_frame(String a_str, String b_str, String c_str, String d_str) {
uint32_t f[4];
f[0] = (uint32_t)a_str.toInt();
f[1] = (uint32_t)b_str.toInt();
f[2] = (uint32_t)c_str.toInt();
f[3] = (uint32_t)d_str.toInt();
matrix.loadFrame(f);
last_displayed = "frame";
return "OK";
}
String show_text(String text) {
matrix.clear();
matrix.beginText(0, 0, 0xFFFFFF);
matrix.text(text);
matrix.endText(SCROLL_LEFT);
last_displayed = "text:" + text;
return "OK";
}
String show_char(String c) {
if (c.length() == 0) return "ERROR: no char";
matrix.clear();
matrix.beginText(0, 0, 0xFFFFFF);
matrix.text(c);
matrix.endText(NO_SCROLL);
last_displayed = "char:" + c;
return "OK";
}
String show_two(String a, String b) {
matrix.clear();
matrix.beginText(0, 0, 0xFFFFFF);
matrix.text(a);
matrix.beginText(4, 0, 0xFFFFFF);
matrix.text(b);
matrix.endText(NO_SCROLL);
last_displayed = "two:" + a + b;
return "OK";
}
String set_brightness(String level) {
int l = level.toInt();
if (l < 1) l = 1;
if (l > 8) l = 8;
matrix.setGrayscaleBits(l);
matrix.clear();
return "OK";
}
String get_status(String arg) {
if (last_displayed.length() == 0) return "Matrix: cleared";
return "Matrix shows: " + last_displayed;
}
String play_heart(String arg) {
matrix.clear();
uint32_t hframes[][5] = {
{0x38022020, 0x810408a0, 0x2200e800, 0x20000000, 66},
{0x1c011010, 0x40820450, 0x11007400, 0x10000000, 66},
{0x0e008808, 0x20410228, 0x08803a00, 0x08000000, 66},
{0x07004404, 0x10208114, 0x04401d00, 0x04000000, 66},
{0x03802202, 0x0810408a, 0x02200e80, 0x02000000, 66},
{0x01c01101, 0x04082045, 0x01100740, 0x01000000, 66},
{0x00e00880, 0x82041022, 0x808803a0, 0x00000000, 66},
{0x00700440, 0x40020011, 0x004401c0, 0x00000000, 66},
{0x00380200, 0x20010008, 0x802000e0, 0x00000000, 66},
{0x00180100, 0x10008004, 0x00100060, 0x00000000, 66},
{0x00080080, 0x08004002, 0x00080020, 0x00000000, 66},
};
int n = sizeof(hframes) / sizeof(hframes[0]);
for (int i = 0; i < n * 3; i++) {
matrix.loadFrame(hframes[i % n]);
delay(66);
}
matrix.clear();
last_displayed = "heart animation";
return "OK";
}
void setup() {
matrix.begin();
matrix.textFont(Font_5x7);
matrix.textScrollSpeed(100);
matrix.clear();
matrix.beginText(0, 0, 0xFFFFFF);
matrix.text(".");
matrix.endText(NO_SCROLL);
delay(15000);
Bridge.begin();
Monitor.begin();
Bridge.provide_safe("draw_frame", draw_frame);
Bridge.provide_safe("show_text", show_text);
Bridge.provide_safe("show_char", show_char);
Bridge.provide_safe("show_two", show_two);
Bridge.provide_safe("clear", clear_matrix);
Bridge.provide_safe("set_brightness", set_brightness);
Bridge.provide("get_status", get_status);
Bridge.provide("play_heart", play_heart);
matrix.clear();
play_heart("");
Monitor.println("LED Matrix Bridge ready");
}
void loop() {
delay(1000);
}
+9
View File
@@ -0,0 +1,9 @@
profiles:
default:
fqbn: arduino:zephyr:unoq
platforms:
- platform: arduino:zephyr
libraries:
- Arduino_RouterBridge
- ArduinoGraphics
default_profile: default
+15
View File
@@ -0,0 +1,15 @@
# USB role change udev rule
# Triggers role re-detection when USB-C cable is plugged/unplugged
# or when USB devices are connected/disconnected in host mode.
#
# Install: sudo cp 99-usb-role.rules /etc/udev/rules.d/
# Reload: sudo udevadm control --reload-rules
# USB gadget/UDC events (dwc3 role changes)
ACTION=="change", SUBSYSTEM=="udc", RUN+="/usr/local/bin/on-usb-role-change.sh"
# USB device connect/disconnect (host mode)
ACTION=="add|remove", SUBSYSTEM=="usb", RUN+="/usr/local/bin/on-usb-role-change.sh"
# USB audio card (host mode audio interface)
ACTION=="add|remove", SUBSYSTEM=="sound", RUN+="/usr/local/bin/on-usb-role-change.sh"
+68
View File
@@ -0,0 +1,68 @@
# ALSA configuration for I2S DAC (e.g. PCM5102, PCM5242, MAX98357)
# Installed alongside asound.conf — activate via:
# sudo mv /etc/asound.conf /etc/asound.conf.bak
# sudo cp /etc/asound-i2s.conf /etc/asound.conf
#
# The I2S interface on the Qualcomm DragonWing typically appears as:
# hw:0,0 (Primary MI2S — default audio path)
# hw:MI2S,0 (Named MI2S device)
#
# Find available devices: aplay -l
# Default PCM — try common I2S device names
pcm.!default {
type plug
slave {
pcm "hw:I2S,0"
format S16_LE
channels 2
rate 48000
}
hint.description "I2S DAC (48kHz stereo)"
}
# Fallback: try hw:0,0 if named device not found
pcm.i2s-default {
type plug
slave {
pcm "hw:0,0"
format S16_LE
channels 2
rate 48000
}
}
# Software volume control (I2S DACs typically have no hardware mixer)
pcm.i2s-softvol {
type softvol
slave.pcm "hw:I2S,0"
control {
name "I2S Playback Volume"
card I2S
}
min_dB -51.0
max_dB 0.0
resolution 256
}
# Direct hardware access (bypass plug/softvol for low latency)
pcm.i2s-direct {
type plug
slave {
pcm "hw:I2S,0"
format S16_LE
channels 2
rate 48000
}
}
# Loopback test (no I2S hardware connected)
pcm.loopback {
type plug
slave {
pcm "hw:Loopback,0,0"
format S16_LE
channels 2
rate 48000
}
}
+44
View File
@@ -0,0 +1,44 @@
# ALSA configuration for USB gadget audio
# The UAC1 gadget appears as a ALSA card (typically card 1 or 2).
# This config routes Pd audio output to the USB gadget.
# Default PCM device for USB gadget audio
pcm.!default {
type plug
slave {
pcm "hw:UAC1Gadget,0"
format S16_LE
channels 2
rate 48000
}
}
# Control device
ctl.!default {
type hw
card UAC1Gadget
}
# Software volume control via ALSA (hardware may not have mixer)
pcm.softvol {
type softvol
slave.pcm "hw:UAC1Gadget,0"
control {
name "Master Playback Volume"
card UAC1Gadget
}
min_dB -51.0
max_dB 0.0
resolution 256
}
# Null sink for testing when no USB gadget is connected
pcm.loopback {
type plug
slave {
pcm "hw:Loopback,0,0"
format S16_LE
channels 2
rate 48000
}
}
+132
View File
@@ -0,0 +1,132 @@
#!/bin/bash
# configure-usb-audio.sh
# Configure the Arduino Uno Q as a composite USB gadget:
# UAC1 (USB Audio Class) + gmidi (USB MIDI)
#
# The host sees one device with both an audio sink and a MIDI port,
# allowing MIDI input over the same USB connection as audio output.
#
# Based on Linux USB gadget configfs setup.
# The Uno Q's USB peripheral controller is exposed via configfs.
set -euo pipefail
GADGET_DIR="/sys/kernel/config/usb_gadget"
GADGET_NAME="g1"
UDC_DIR="/sys/class/udc"
# Gadget config
VID="0x2341" # Arduino VID
PID="0x006A" # Custom PID (Uno Q audio + MIDI)
SERIAL="$(cat /sys/firmware/devicetree/base/serial-number 2>/dev/null | tr -d '\0' || cat /proc/cpuinfo 2>/dev/null | grep Serial | head -1 | awk '{print $3}' || echo 'UnoQ')"
MANUFACTURER="Arduino"
PRODUCT="Uno Q Audio Synth"
# USB audio params
CHANNELS=2 # stereo out
SAMPLE_RATE=48000
SAMPLE_SIZE=2 # bytes (2 = 16-bit; kernel UAC1 gadget expects bytes, not bits)
# Find UDC (USB Device Controller)
find_udc() {
for controller in "$UDC_DIR"/*; do
if [ -d "$controller" ]; then
basename "$controller"
return 0
fi
done
echo "ERROR: No UDC found" >&2
exit 1
}
create_gadget() {
local udc="$1"
# Remove existing gadget if present
if [ -d "$GADGET_DIR/$GADGET_NAME" ]; then
echo "" > "$GADGET_DIR/$GADGET_NAME/UDC" 2>/dev/null || true
for _ in $(seq 1 50); do
if [ ! -f "$GADGET_DIR/$GADGET_NAME/UDC" ] || [ -z "$(cat "$GADGET_DIR/$GADGET_NAME/UDC" 2>/dev/null)" ]; then
break
fi
sleep 0.1
done
for config in "$GADGET_DIR/$GADGET_NAME/configs"/*/; do
config=$(basename "$config")
for func in "$GADGET_DIR/$GADGET_NAME/configs/$config/"*.*/; do
func=$(basename "$func")
[ -L "$GADGET_DIR/$GADGET_NAME/configs/$config/$func" ] && rm -f "$GADGET_DIR/$GADGET_NAME/configs/$config/$func"
done
rmdir "$GADGET_DIR/$GADGET_NAME/configs/$config" 2>/dev/null || true
done
for func in "$GADGET_DIR/$GADGET_NAME/functions"/*/; do
func=$(basename "$func")
rmdir "$GADGET_DIR/$GADGET_NAME/functions/$func" 2>/dev/null || true
done
rmdir "$GADGET_DIR/$GADGET_NAME" 2>/dev/null || true
fi
mkdir -p "$GADGET_DIR/$GADGET_NAME"
echo "$VID" > "$GADGET_DIR/$GADGET_NAME/idVendor"
echo "$PID" > "$GADGET_DIR/$GADGET_NAME/idProduct"
echo "0x0100" > "$GADGET_DIR/$GADGET_NAME/bcdDevice"
echo "0x0200" > "$GADGET_DIR/$GADGET_NAME/bcdUSB"
mkdir -p "$GADGET_DIR/$GADGET_NAME/strings/0x409"
echo "$SERIAL" > "$GADGET_DIR/$GADGET_NAME/strings/0x409/serialnumber"
echo "$MANUFACTURER" > "$GADGET_DIR/$GADGET_NAME/strings/0x409/manufacturer"
echo "$PRODUCT" > "$GADGET_DIR/$GADGET_NAME/strings/0x409/product"
# Create UAC1 function (audio output, wide host compatibility)
mkdir -p "$GADGET_DIR/$GADGET_NAME/functions/uac1.gs0"
echo "$CHANNELS" > "$GADGET_DIR/$GADGET_NAME/functions/uac1.gs0/p_chmask"
echo "$SAMPLE_RATE" > "$GADGET_DIR/$GADGET_NAME/functions/uac1.gs0/p_srate"
echo "$SAMPLE_SIZE" > "$GADGET_DIR/$GADGET_NAME/functions/uac1.gs0/p_ssize"
echo "$CHANNELS" > "$GADGET_DIR/$GADGET_NAME/functions/uac1.gs0/c_chmask"
echo "$SAMPLE_RATE" > "$GADGET_DIR/$GADGET_NAME/functions/uac1.gs0/c_srate"
echo "$SAMPLE_SIZE" > "$GADGET_DIR/$GADGET_NAME/functions/uac1.gs0/c_ssize"
# Create MIDI function (MIDI input from host)
mkdir -p "$GADGET_DIR/$GADGET_NAME/functions/midi.gs0"
echo 1 > "$GADGET_DIR/$GADGET_NAME/functions/midi.gs0/in_ports"
echo 1 > "$GADGET_DIR/$GADGET_NAME/functions/midi.gs0/out_ports"
# Create configuration
mkdir -p "$GADGET_DIR/$GADGET_NAME/configs/c.1"
mkdir -p "$GADGET_DIR/$GADGET_NAME/configs/c.1/strings/0x409"
echo "Audio + MIDI" > "$GADGET_DIR/$GADGET_NAME/configs/c.1/strings/0x409/configuration"
echo 250 > "$GADGET_DIR/$GADGET_NAME/configs/c.1/MaxPower"
# Link both functions to the same config (composite device)
ln -s "$GADGET_DIR/$GADGET_NAME/functions/uac1.gs0" "$GADGET_DIR/$GADGET_NAME/configs/c.1/"
ln -s "$GADGET_DIR/$GADGET_NAME/functions/midi.gs0" "$GADGET_DIR/$GADGET_NAME/configs/c.1/"
# Bind to UDC
echo "$udc" > "$GADGET_DIR/$GADGET_NAME/UDC"
echo "USB gadget '$GADGET_NAME' bound to $udc (UAC1 audio + gmidi)"
}
case "${1:-start}" in
start)
udc=$(find_udc)
create_gadget "$udc"
;;
stop)
if [ -d "$GADGET_DIR/$GADGET_NAME" ]; then
udc=$(cat "$GADGET_DIR/$GADGET_NAME/UDC" 2>/dev/null || true)
[ -n "$udc" ] && echo "" > "$GADGET_DIR/$GADGET_NAME/UDC" 2>/dev/null || true
fi
echo "USB gadget '$GADGET_NAME' stopped"
;;
status)
if [ -d "$GADGET_DIR/$GADGET_NAME/UDC" ]; then
echo "Gadget: $(cat "$GADGET_DIR/$GADGET_NAME/UDC" 2>/dev/null || echo 'not bound')"
else
echo "Gadget: not configured"
fi
;;
*)
echo "Usage: $0 {start|stop|status}"
exit 1
;;
esac
+41
View File
@@ -0,0 +1,41 @@
#!/bin/bash
# configure-usb-host.sh — Wait for USB audio interface in host mode.
# Finds the first USB audio capture/playback device and writes device
# info to /run/usb-audio-dev for Pd to use.
#
# When the Uno Q acts as USB host (MIDI controller + USB audio interface
# connected via hub), the USB audio interface appears as a ALSA card
# driven by snd-usb-audio.
set -euo pipefail
DEV_FILE="/run/usb-audio-dev"
POLL_SECONDS=10
die() { echo "[USB-HOST] $*" >&2; exit 1; }
info() { echo "[USB-HOST] $*"; }
info "Waiting for USB audio interface..."
for ((i = 0; i < POLL_SECONDS; i++)); do
# Look for USB audio cards (not UAC1Gadget, not internal)
cards=$(aplay -l 2>/dev/null | grep -i "usb" | grep -vi "gadget" || true)
if [ -n "$cards" ]; then
# Pick the first USB audio card
card=$(echo "$cards" | head -1 | awk '{print $2}' | tr -d ':')
info "Found USB audio card: $card"
echo "hw:$card,0" > "$DEV_FILE"
cat "$DEV_FILE"
exit 0
fi
# Also check if any MIDI devices appeared
if command -v aseqdump &>/dev/null; then
midi_ports=$(aconnect -i -o -l 2>/dev/null | grep -i "usb" | head -1 || true)
if [ -n "$midi_ports" ]; then
info "Found USB MIDI device: $midi_ports"
fi
fi
sleep 1
done
die "No USB audio interface found after ${POLL_SECONDS}s (connect one and restart)"
+11
View File
@@ -0,0 +1,11 @@
[Unit]
Description=Determine and set I2S audio ALSA device
Before=pd-synth-i2s.service
[Service]
Type=oneshot
ExecStart=/usr/local/bin/detect-i2s-device.sh
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
+35
View File
@@ -0,0 +1,35 @@
#!/bin/bash
# detect-i2s-device.sh — Probe for available I2S audio devices
# and create /run/i2s-audio-dev containing the best candidate.
# Called by detect-i2s-device.service before pd-synth-i2s starts.
set -euo pipefail
DEVICE_FILE="/run/i2s-audio-dev"
write_dev() { echo "$1" > "$DEVICE_FILE"; echo "[DETECT] I2S device: $1"; exit 0; }
# Try named I2S devices first (most reliable)
for dev in "hw:I2S,0" "hw:MI2S,0" "hw:i2s,0" "hw:mi2s,0"; do
if aplay -l 2>/dev/null | grep -qi "${dev%,*}"; then
write_dev "$dev"
fi
done
# Try probing ALSA device list for anything that looks like I2S
while read -r card; do
id=$(echo "$card" | awk '{print $2}' | tr -d ':')
name=$(echo "$card" | cut -d: -f2- | xargs)
if echo "$name" | grep -qi -E "(i2s|mi2s|quat|tert|dac|audio)"; then
write_dev "hw:$id,0"
fi
done < <(cat /proc/asound/cards 2>/dev/null || true)
# Fallback to hw:0,0 (first audio device, may be HDMI or analog)
if aplay -l 2>/dev/null | grep -q "^card 0"; then
write_dev "hw:0,0"
fi
# Nothing found
echo "[DETECT] No I2S audio device found" >&2
echo "" > "$DEVICE_FILE"
exit 1
+94
View File
@@ -0,0 +1,94 @@
#!/bin/bash
# detect-usb-role.sh — Detect whether USB-C should be gadget or host.
# Writes result to /run/usb-role for consumption by pd-synth-auto.service.
#
# Detection methods (tried in order):
# 1. extcon — USB cable detection (most reliable on Qualcomm)
# 2. dwc3 debug — USB controller mode
# 3. UDC state — if a UDC is bound as configured, we're in gadget mode
# 4. lsusb — if USB devices appear (after host controller init), we're host
#
# Exit code: 0 if role detected, 1 if unknown
set -euo pipefail
ROLE_FILE="/run/usb-role"
unset ROLE
die() { echo "[USB-ROLE] $*" >&2; exit 1; }
# --- Method 1: extcon ---
check_extcon() {
for path in /sys/class/extcon/*/state; do
[ -f "$path" ] || continue
local state
state=$(cat "$path" 2>/dev/null || true)
if echo "$state" | grep -q "USB=1"; then
echo "gadget"
return 0
fi
done
return 1
}
# --- Method 2: dwc3 debugfs ---
check_dwc3() {
local mode_file
for mode_file in /sys/kernel/debug/usb/dwc3*/mode; do
[ -f "$mode_file" ] || continue
local mode
mode=$(cat "$mode_file" 2>/dev/null || true)
case "$mode" in
*peripheral*) echo "gadget"; return 0 ;;
*host*) echo "host"; return 0 ;;
esac
done
return 1
}
# --- Method 3: UDC state ---
check_udc() {
for path in /sys/class/udc/*/state; do
[ -f "$path" ] || continue
local state
state=$(cat "$path" 2>/dev/null || true)
case "$state" in
configured) echo "gadget"; return 0 ;;
esac
done
return 1
}
# --- Method 4: USB host devices ---
check_lsusb() {
if command -v lsusb &>/dev/null; then
local count
count=$(lsusb 2>/dev/null | grep -v "Linux Foundation" | wc -l)
# If we see non-root-hub devices, likely we're the host
if [ "$count" -gt 1 ]; then
echo "host"
return 0
fi
fi
return 1
}
# Try methods in order
ROLE=$(check_extcon || check_dwc3 || check_udc || echo "")
# If still unknown, check lsusb after brief delay (host controller needs time)
if [ -z "$ROLE" ]; then
sleep 2
ROLE=$(check_lsusb || echo "")
fi
# Write result
if [ -n "$ROLE" ]; then
echo "$ROLE" > "$ROLE_FILE"
echo "[USB-ROLE] Detected: $ROLE"
exit 0
else
echo "[USB-ROLE] Unknown — defaulting to gadget"
echo "gadget" > "$ROLE_FILE"
exit 0
fi
+15
View File
@@ -0,0 +1,15 @@
[Unit]
Description=LED Matrix MIDI Visualization
After=arduino-router.service
[Service]
User=arduino
Type=simple
ExecStart=/usr/local/bin/led-matrix-viz.py
Restart=on-failure
RestartSec=2
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
+14
View File
@@ -0,0 +1,14 @@
[Unit]
Description=MIDI rawmidi to Pd UDP bridge (MPK Mini → Pd)
After=multi-user.target
[Service]
Type=simple
ExecStart=/usr/local/bin/midi-bridge.py
Restart=on-failure
RestartSec=2
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
+26
View File
@@ -0,0 +1,26 @@
#!/bin/bash
# on-usb-role-change.sh — Called by udev on USB role/connect events.
# Re-detects role, stops/restarts Pd with appropriate config.
set -euo pipefail
ROLE_FILE="/run/usb-role"
LOG_TAG="[USB-HOTPLUG]"
# Debounce: skip if we just ran (within 3s)
LOCK="/tmp/usb-role-change.lock"
if [ -f "$LOCK" ]; then
age=$(($(date +%s) - $(stat -c %Y "$LOCK" 2>/dev/null || echo 0)))
[ "$age" -lt 3 ] && exit 0
fi
touch "$LOCK"
logger -t "$LOG_TAG" "USB role change event — re-detecting..."
/usr/local/bin/detect-usb-role.sh
# Restart Pd if it's running
if systemctl is-active pd-synth-auto &>/dev/null; then
logger -t "$LOG_TAG" "Restarting Pd with new role"
systemctl restart pd-synth-auto
fi
+17
View File
@@ -0,0 +1,17 @@
[Unit]
Description=Pure Data Synth (auto-detect: gadget or host)
Documentation=https://github.com/arduino/uno-q-audio-synth
After=usb-role-detect.service
Wants=usb-role-detect.service
[Service]
Type=simple
ExecStart=/usr/local/bin/start-synth.sh --auto --verbose
ExecStop=/usr/bin/killall -s TERM pd
StandardOutput=journal
StandardError=journal
Restart=on-failure
RestartSec=2
[Install]
WantedBy=multi-user.target
+16
View File
@@ -0,0 +1,16 @@
[Unit]
Description=Pure Data I2S DAC Synth (standalone)
After=network.target sound.target
Wants=configure-usb-audio.service
[Service]
Type=simple
User=arduino
ExecStart=/usr/local/bin/start-synth.sh --i2s
ExecStop=/usr/bin/killall pd
WorkingDirectory=/home/arduino/pd
Restart=on-failure
RestartSec=2
[Install]
WantedBy=multi-user.target
+16
View File
@@ -0,0 +1,16 @@
[Unit]
Description=Pure Data USB Audio Synth
After=network.target sound.target
Wants=usb-gadget-audio.service
[Service]
Type=simple
User=arduino
ExecStart=/usr/local/bin/start-synth.sh
ExecStop=/usr/bin/killall pd
WorkingDirectory=/home/arduino/pd
Restart=on-failure
RestartSec=2
[Install]
WantedBy=multi-user.target
+38
View File
@@ -0,0 +1,38 @@
#!/bin/bash
set -euo pipefail
# Reset USB controller and re-bind gadget
UDC="4e00000.usb"
GADGET="/sys/kernel/config/usb_gadget/g1"
# Find the driver binding
DRIVER=""
for d in /sys/bus/platform/drivers/*/; do
dname=$(basename "$d")
if [ -L "$d/$UDC" ] 2>/dev/null; then
DRIVER="$dname"
break
fi
done
if [ -z "$DRIVER" ]; then
# Try looking at the UDC device itself
UDC_PATH=$(readlink -f /sys/class/udc/$UDC/device 2>/dev/null || echo "")
if [ -n "$UDC_PATH" ]; then
DRIVER=$(basename "$(dirname "$UDC_PATH")" 2>/dev/null || echo "")
fi
fi
echo "Driver: ${DRIVER:-unknown}"
# Unbind
if [ -n "$DRIVER" ]; then
echo "$UDC" > "/sys/bus/platform/drivers/$DRIVER/unbind" 2>/dev/null && echo "Unbound" || echo "Unbind failed"
sleep 2
echo "$UDC" > "/sys/bus/platform/drivers/$DRIVER/bind" 2>/dev/null && echo "Rebound" || echo "Rebind failed"
sleep 1
fi
# Bind gadget
echo "$UDC" > "$GADGET/UDC" 2>&1 && echo "Gadget bound" || echo "Gadget bind failed"
cat /sys/class/udc/$UDC/state
+12
View File
@@ -0,0 +1,12 @@
[Unit]
Description=Configure USB Audio Gadget
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
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
+14
View File
@@ -0,0 +1,14 @@
[Unit]
Description=Detect USB role (gadget vs host)
DefaultDependencies=no
After=sysinit.target
Before=pd-synth-auto.service
[Service]
Type=oneshot
ExecStart=/usr/local/bin/detect-usb-role.sh
RemainAfterExit=yes
StandardOutput=journal
[Install]
WantedBy=sysinit.target