Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
9cec8c2
feat: decouple provision scripts from DietPi internals
thomaseleff Jun 22, 2026
8ea082b
feat: disable WiFi power save mode
thomaseleff Jun 22, 2026
641415c
fix: prevent HDMI audio dropout in CamillaDSP configs
thomaseleff Jun 22, 2026
b828ad2
feat: add HDMI boot configuration and camilladsp-hdmi-wait.path unit
thomaseleff Jun 22, 2026
dbe84fb
feat: add boot banner warning against dietpi-config usage
thomaseleff Jun 22, 2026
c1655ac
test: replace CamillaDSP mock with real binary in a testcontainer
thomaseleff Jun 22, 2026
b4b3205
docs: clarify CamillaDSP test strategy in AGENTS.md
thomaseleff Jun 24, 2026
b629cd6
ci: build CamillaDSP test image before running test suite
thomaseleff Jun 24, 2026
d95d76c
test: build CamillaDSP image via testcontainers DockerImage
thomaseleff Jun 24, 2026
2437379
test: build snapserver image via testcontainers DockerImage
thomaseleff Jun 24, 2026
65167c3
fix: download CamillaDSP to temp file before extracting in Dockerfile
thomaseleff Jun 24, 2026
a833b07
fix: correct CamillaDSP release asset arch and error test fixture
thomaseleff Jun 24, 2026
e957ab5
fix: CamillaDSP container binds to 0.0.0.0 and client handles Invalid…
thomaseleff Jun 24, 2026
020422f
fix: switch HDMI overlay to KMS and force hotplug for headless audio
thomaseleff Jul 2, 2026
a746dae
fix: remove redundant camilladsp-hdmi-wait.path, enable camilladsp.se…
thomaseleff Jul 2, 2026
4162180
fix: make dtoverlay audio-device selection explicit and opt-in
thomaseleff Jul 2, 2026
bb87ece
chore: remove dead commented-out code from setup scripts
thomaseleff Jul 2, 2026
f29b9eb
refactor: extract shared setup.sh patterns into os/dietpi/lib helpers
thomaseleff Jul 2, 2026
413d5bb
style: normalize blank-line spacing in setup.sh scripts
thomaseleff Jul 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions audera/clients/camilladsp.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ def _call(self, command: str, value=None) -> dict:
ws.send(json.dumps(payload))
response = json.loads(ws.recv())
# CamillaDSP wraps responses as {"CommandName": {"result": "Ok/Error", ...}}
# Unknown commands return {"Invalid": {"error": "..."}} instead
if isinstance(response, dict) and 'Invalid' in response:
raise RuntimeError('CamillaDSP error [%s]: %s' % (command, response['Invalid']))
inner = response.get(command, response) if isinstance(response, dict) else response
if isinstance(inner, dict) and inner.get('result') == 'Error':
raise RuntimeError('CamillaDSP error [%s]: %s' % (command, inner))
Expand Down
11 changes: 5 additions & 6 deletions audera/conf/player/camilladsp.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,11 @@ devices:
# # type: AccurateAsync # Best quality; intended for Pi 4 or desktop PCs
# # capture_samplerate: 44100 # Uncomment if source is 44.1k and DAC is 48k

# ADVANCED POWER/SAFETY SETTINGS: Saves CPU when music is paused
silence_threshold: -100
silence_timeout: 10.0

# HANGING: Ensures CamillaDSP exits cleanly if the input sample rate changes
stop_on_rate_change: true
# HDMI STABILITY: Keep the ALSA device open at all times to prevent HDMI audio dropout.
# Closing the device after silence causes the HDMI sink to de-clock and drop the connection.
silence_threshold: null # keep ALSA device open, prevent HDMI dropout
silence_timeout: null
stop_on_rate_change: false

capture:
type: Alsa
Expand Down
11 changes: 5 additions & 6 deletions audera/conf/streamer/camilladsp.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,11 @@ devices:
# # type: AccurateAsync # Best quality; intended for Pi 4 or desktop PCs
# # capture_samplerate: 44100 # Uncomment if source is 44.1k and DAC is 48k

# ADVANCED POWER/SAFETY SETTINGS: Saves CPU when music is paused
silence_threshold: -100
silence_timeout: 10.0

# HANGING: Ensures CamillaDSP exits cleanly if the input sample rate changes
stop_on_rate_change: true
# HDMI STABILITY: Keep the ALSA device open at all times to prevent HDMI audio dropout.
# Closing the device after silence causes the HDMI sink to de-clock and drop the connection.
silence_threshold: null # keep ALSA device open, prevent HDMI dropout
silence_timeout: null
stop_on_rate_change: false

capture:
type: Alsa
Expand Down
4 changes: 4 additions & 0 deletions audera/services/netifaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,8 @@ async def connect(
f'{get_preferred_security_type(supported_security_types)}',
'wifi-sec.psk',
f'{password}',
'802-11-wireless.powersave',
'2',
'connection.autoconnect',
'yes',
],
Expand All @@ -285,6 +287,8 @@ async def connect(
f'{ssid}',
'ssid',
f'{ssid}',
'802-11-wireless.powersave',
'2',
'connection.autoconnect',
'yes',
],
Expand Down
1 change: 1 addition & 0 deletions docs/dev/PROVISION.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ Run the script from the project root using Git's bundled bash.
| `-u, --user` | `root` | SSH user |
| `-p, --port` | `22` | SSH port |
| `-i, --identity` | | SSH private key file |
| `-a, --audio-device` | | Configure `dtoverlay` for the attached audio device: `hdmi`, `digiamp-plus`, `dac-plus`, `hifiberry-dac-plus`. Unset leaves the existing `dtoverlay` untouched. |
| `--no-reboot` | | Skip final reboot; leaves device running for inspection |
| `--wipe-networks` | | Delete all NetworkManager connections before reboot (triggers WiFi wizard on next boot) |
| `--check` | | After reboot, poll until device is reachable then verify systemd services |
Expand Down
151 changes: 151 additions & 0 deletions os/dietpi/lib/common.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
#!/bin/bash

# Shared install/setup helpers for Audera device setup scripts.
# Sourced by player/automation/setup.sh and streamer/automation/setup.sh.

# Color formatting
RED='\033[0;31m'
GREEN='\033[0;32m'
RESET='\033[0m'

# Prints the Audera ASCII-art logo to the console
# The logo must be wrapped in single quotes ' ' to avoid escaping characters
# due to the nature of having double backslashes, like '\\' in the logo
print_logo() {
echo ' ________ ___ ___ ________ _______ ________ ________ '
echo '|\ __ \|\ \|\ \|\ ___ \|\ ___\|\ __ \|\ __ \ '
echo '\ \ \|\ \ \ \\\ \ \ \_|\ \ \ \__|\ \ \|\ \ \ \|\ \ '
echo ' \ \ __ \ \ \\\ \ \ \ \\ \ \ __\\ \ /\ \ __ \ '
echo ' \ \ \ \ \ \ \\\ \ \ \_\\ \ \ \_|_\ \ \ \ \ \ \ \ \ '
echo ' \ \__\ \__\ \______/\ \______/\ \______\ \__\\ _\\ \__\ \__\ '
echo ' \|__|\|__|\|______| \|______| \|______|\|__|\|__|\|__|\|__| '
}

# Ensures the script is running as root
require_root() {
if [[ $EUID -ne 0 ]]; then
echo -e "${RED}*** CRITICAL: The setup-script must be run as {sudo}.${RESET}"
exit 1
fi
}

# Loads the ALSA loopback module (needed for CamillaDSP <-> Snapclient audio path)
# index=7 keeps the loopback off hw:0 so physical card indices are stable
setup_alsa_loopback() {
echo "options snd-aloop index=7" > /etc/modprobe.d/snd-aloop.conf
echo "snd-aloop" > /etc/modules-load.d/snd-aloop.conf
modprobe snd-aloop
}

# Downloads, extracts, and installs the given CamillaDSP version to /usr/local/bin
install_camilladsp() {
local version="$1"
local archive="camilladsp-linux-aarch64.tar.gz"
local url="https://github.com/HEnquist/camilladsp/releases/download/v${version}/${archive}"
wget -q "$url" -O "/tmp/${archive}"
tar -xzf "/tmp/${archive}" -C /usr/local/bin/
chmod +x /usr/local/bin/camilladsp
rm "/tmp/${archive}"
mkdir -p /etc/camilladsp
}

# Writes the camilladsp systemd service unit, captures from ALSA loopback, plays to
# physical DAC (hw:0)
write_camilladsp_service() {
local config_path="$1"
local statefile_path="$2"
cat > /etc/systemd/system/camilladsp.service <<EOF
[Unit]
Description=CamillaDSP
After=sound.target snapclient.service
StartLimitIntervalSec=0

[Service]
ExecStart=/usr/local/bin/camilladsp $config_path --statefile $statefile_path -p 1234 --address 0.0.0.0
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF
}

# Installs uv if it is not already present
install_uv() {
if ! command -v uv &> /dev/null; then
curl -LsSf https://astral.sh/uv/install.sh | env UV_INSTALL_DIR=/usr/local/bin sh
fi
}

# Installs the audera CLI from the given git repo/branch
install_audera_cli() {
local repo_url="$1"
local branch="$2"
UV_TOOL_BIN_DIR=/usr/local/bin uv tool install --reinstall "git+${repo_url}@${branch}"
export PATH="/usr/local/bin:$PATH"
}

# Purges ifupdown, which will conflict with Network-Manager if both are installed,
# and comments out all configuration from `/etc/network/interfaces`
purge_ifupdown() {
if systemctl is-active --quiet ifupdown; then
systemctl stop ifupdown
systemctl disable ifupdown
fi
apt-get purge -y ifupdown
sed -i '/^[[:space:]]*[^#[:space:]]/s/^/# /' /etc/network/interfaces
}

# Sets up network-manager to manage all network devices, even those configured
# within `/etc/network/interfaces`
setup_network_manager() {
sed -i '/^\[ifupdown\]/,/^\[/s/managed=false/managed=true/' /etc/NetworkManager/NetworkManager.conf
systemctl enable NetworkManager
systemctl start NetworkManager
nmcli networking on
}

# Disables WiFi power save globally
disable_wifi_powersave() {
mkdir -p /etc/NetworkManager/conf.d
cat > /etc/NetworkManager/conf.d/wifi-powersave.conf <<'EOF'
[connection]
wifi.powersave = 2
EOF
}

# Derives the hostname from the eth0 (falling back to wlan0) MAC address, sets it,
# and appends it to /etc/hosts; echoes the derived hostname so the caller can
# capture it
derive_hostname_from_mac() {
local mac short new_hostname
mac=$(cat /sys/class/net/eth0/address 2>/dev/null || cat /sys/class/net/wlan0/address)
short=$(echo "$mac" | tr -d ':' | tail -c 7)
new_hostname="audera-${short}"
hostnamectl set-hostname "$new_hostname"
echo "127.0.1.1 $new_hostname" >> /etc/hosts
echo "$new_hostname"
}

# Writes the boot banner printed at login
write_boot_banner() {
cat > /etc/profile.d/50-audera-banner.sh <<'EOF'
#!/bin/sh
printf '\033[36m'
cat << 'LOGO'
________ ___ ___ ________ _______ ________ ________
|\ __ \|\ \|\ \|\ ___ \|\ ___\|\ __ \|\ __ \
\ \ \|\ \ \ \\\ \ \ \_|\ \ \ \__|\ \ \|\ \ \ \|\ \
\ \ __ \ \ \\\ \ \ \ \\ \ \ __\\ \ /\ \ __ \
\ \ \ \ \ \ \\\ \ \ \_\\ \ \ \_|_\ \ \ \ \ \ \ \ \
\ \__\ \__\ \______/\ \______/\ \______\ \__\\ _\\ \__\ \__\
\|__|\|__|\|______| \|______| \|______|\|__|\|__|\|__|\|__|
LOGO
printf '\033[0m\n'
printf ' \033[1maudera\033[0m — composable audio for your hardware\n\n'
printf ' \033[33m!\033[0m Do not use \033[1mdietpi-config\033[0m to manage WiFi or audio hardware.\n'
printf ' WiFi: nmcli device wifi connect <SSID> password <PASS>\n'
printf ' Audio: aplay -l | nano /boot/firmware/config.txt\n\n'
EOF
chmod +x /etc/profile.d/50-audera-banner.sh
}
72 changes: 72 additions & 0 deletions os/dietpi/lib/config.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
#!/bin/bash

# Shared idempotent config-injection helpers for Audera device setup scripts.
# Sourced by player/automation/setup.sh and streamer/automation/setup.sh.

# Idempotently sets a `key=value` line in a config file — replaces an active or
# commented-out line matching the key, or appends the line if the key is absent
set_config_line() {
local file="$1"
local key="$2"
local line="$3"
if grep -qE "^${key}=" "$file"; then
sed -i "s|^${key}=.*|${line}|" "$file"
elif grep -qE "^#[[:space:]]*${key}=" "$file"; then
sed -i "s|^#[[:space:]]*${key}=.*|${line}|" "$file"
else
echo "$line" >> "$file"
fi
}

# Idempotently sets a `key=value` kernel command-line parameter — replaces an
# existing occurrence of the key, or appends the parameter if the key is absent
set_cmdline_param() {
local file="$1"
local key="$2"
local param="$3"
if grep -qE "(^| )${key}=" "$file"; then
sed -i -E "s/(^| )${key}=[^ ]*/\1${param}/" "$file"
else
sed -i "s/\$/ ${param}/" "$file"
fi
}

# Configures /boot/firmware/config.txt (and cmdline.txt, for hdmi) for the given
# audio device; a no-op (with a message) when $1 is empty
configure_audio_device() {
local audio_device="$1"
if [ -z "$audio_device" ]; then
echo ">>> No --audio-device specified; leaving existing dtoverlay untouched"
return
fi
echo ">>> Configuring audio device: $audio_device"
case "$audio_device" in
hdmi)
set_config_line /boot/firmware/config.txt 'hdmi_force_hotplug' 'hdmi_force_hotplug=1'
set_config_line /boot/firmware/config.txt 'hdmi_drive' 'hdmi_drive=2'
set_config_line /boot/firmware/config.txt 'hdmi_force_edid_audio' 'hdmi_force_edid_audio=1'
set_config_line /boot/firmware/config.txt 'hdmi_group' 'hdmi_group=1'
set_config_line /boot/firmware/config.txt 'hdmi_mode' 'hdmi_mode=16'
set_config_line /boot/firmware/config.txt 'dtoverlay' 'dtoverlay=vc4-kms-v3d'
set_config_line /boot/firmware/config.txt 'dtparam=audio' 'dtparam=audio=on'
set_cmdline_param /boot/firmware/cmdline.txt 'vc4\.force_hotplug' 'vc4.force_hotplug=3'
;;
digiamp-plus)
set_config_line /boot/firmware/config.txt 'dtoverlay' 'dtoverlay=rpi-digiampplus'
set_config_line /boot/firmware/config.txt 'dtparam=audio' 'dtparam=audio=off'
;;
dac-plus)
set_config_line /boot/firmware/config.txt 'dtoverlay' 'dtoverlay=rpi-dacplus'
set_config_line /boot/firmware/config.txt 'dtparam=audio' 'dtparam=audio=off'
;;
hifiberry-dac-plus)
set_config_line /boot/firmware/config.txt 'dtoverlay' 'dtoverlay=hifiberry-dacplus'
set_config_line /boot/firmware/config.txt 'dtparam=audio' 'dtparam=audio=off'
;;
*)
echo -e "${RED}*** CRITICAL: Unknown --audio-device '${audio_device}'. Valid values: hdmi, digiamp-plus, dac-plus, hifiberry-dac-plus.${RESET}"
exit 1
;;
esac
echo -e "[ ${GREEN}OK${RESET} ] Audio device configured successfully"
}
Loading