arm64 was the last image in the household whose identity was typed in by hand after building — Raspberry Pi Imager's OS Customisation dialog set the hostname per unit at flash time. That's the exact step CoreSystemConfig.json exists to remove, and it failed quietly when mistyped: spotify-connect-start reads $(hostname) at service start, so a typo doesn't error, it just puts a Spotify Connect device with the wrong room name in the picker. The builder now substitutes the endpoint's hostname from the audio_endpoints array into a generated copy of config/audio-endpoint.yaml and builds one image per room, published like every other image as iso-out/smarthome-<hostname>-<pairid>.img. The substitution is targeted rather than a YAML round-trip: that template is mostly comments explaining an unverified schema, and a load/dump would strip every one of them. It fails loudly if it doesn't match exactly once, rather than shipping an image named after the wrong room. The cost is honest and documented: one full rpi-image-gen run per arm64 room, qemu-emulated on an x86 builder, instead of one for all of them. build.arm64_prebake: false restores the single generic image for households with several arm64 rooms and a slow builder. Wi-Fi and SSH keys are still set in Imager on arm64 either way — only the hostname moved into the build. Also fixes the image-publishing search, which used `find -newer $BUILD_CONFIG` and silently found nothing when the build finished within the same filesystem timestamp granularity as the config write. Newest .img wins instead. Resolves the per-room-identity asymmetry in project-plan Phase 15.5. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|---|---|---|
| .. | ||
| configs | ||
| live-build-amd64/config | ||
| rpi-image-gen | ||
| README.md | ||
README.md
Headless audio endpoint
For rooms that don't get a Sway thin client (hosts/thin-client/) but still want a
speaker: a small headless appliance running its own independent Spotify Connect
receiver — its own device name, its own room, no shared/synced stream. Two
completely separate build pipelines, one per architecture, because the hardware
paths genuinely differ (see below) — not one image with a flag.
| arm64 (Raspberry Pi) | amd64 (mini PC) | |
|---|---|---|
| Audio hardware | HiFiBerry Amp2 HAT (GPIO, 60W, drives 4–8Ω passive speakers directly) | A USB DAC/amp, e.g. Fosi Audio V3 (USB Audio Class, drives passive speakers directly) |
| Build tool | rpi-image-gen | live-build (same tool hosts/thin-client/ uses) |
| Output | A bootable .img |
A bootable live ISO |
| Per-room identity | Baked in at build time from CoreSystemConfig.json — one build per room (set build.arm64_prebake: false to get one generic image named in Pi Imager instead) |
Baked in at build time — one build per room |
Both variants share one thing: configs/spotify-connect.service +
configs/spotify-connect-start, an architecture-independent systemd
unit/wrapper pair. Nothing else is shared — see each variant's own section below.
Why two separate pipelines, not one
- The HiFiBerry Amp2 is a Raspberry Pi GPIO HAT. There's no amd64 equivalent slot to attach one to — an x86 mini PC needs a USB DAC/amp instead, plug-in hardware, not a HAT.
- rpi-image-gen is Raspberry-Pi-specific. It doesn't target amd64. Rather than
reach for a third, unfamiliar toolchain for the x86 side, the amd64 image reuses
the exact tool this repo already has and has already proven for a bootable
image: live-build, the same one
tools/build-thin-client-iso.shdrives. Lower risk than learning a new toolchain for one variant.
Spotify Connect install: apt-first, documented fallback
Neither spotifyd nor librespot is in Debian bookworm main — same situation
hosts/thin-client/live-build/config/hooks/normal/0500-spotify-connect.hook.chroot
already solved for the thin client. Both variants here reuse that exact logic and
package order (spotifyd then librespot): try apt-get install for each (in
case a backport/third-party repo has been added), and if neither installs, stop
with a documented message rather than hardcoding a release URL that would
silently 404 later. If you hit that message on a real build:
- fetch a release binary from https://github.com/librespot-org/librespot/releases
(check the current tag yourself) into
/usr/bin/librespot, or cargo buildlibrespot in the relevant hook script, or- use raspotify instead — it wraps
librespot with its own packaging and systemd unit. If you go this route, drop
this repo's
spotify-connect.service/spotify-connect-startin favour of raspotify's own unit rather than layering both, or - add a third-party apt repo that carries one of the above.
Spotify Premium is required either way — Spotify Connect doesn't work on Free.
Audio output: direct ALSA, not PipeWire
Deliberately different from the thin client's PipeWire/WirePlumber stack: this is
a single-purpose headless appliance with exactly one audio-producing process, so
there's no multi-app mixing need PipeWire exists to solve, and neither image runs
a desktop session that would start a per-session PipeWire daemon anyway.
spotify-connect-start talks straight to ALSA via --device "${ALSA_DEVICE:-default}".
arm64: Raspberry Pi + HiFiBerry Amp2
⚠️ The biggest risk in this whole directory
rpi-image-gen/config/audio-endpoint.yaml and the build script were written from
rpi-image-gen's own README/documentation summary, not hands-on validated — no
ARM build environment or the tool itself was available while writing this. If
rpi-image-gen build -c config/audio-endpoint.yaml rejects the config:
rpi-image-gen layer --list
rpi-image-gen layer --describe <name>
on the real build host is the authoritative source. Adjust the keys in
audio-endpoint.yaml to match what that reports — the intent behind each section
(device target, packages, files to inject, which service to enable, the
build-time install hook) should carry over even if the exact key names don't.
Build
cd hosts/audio-endpoint/rpi-image-gen/scripts
./build-audio-endpoint-image-arm64.sh
Clones rpi-image-gen (to /opt/smart-home/src/rpi-image-gen by default,
override with RPI_IMAGE_GEN_SRC) if not already present, then builds. Output is
a .img under that clone's work/ directory.
Per-room identity
The hostname is baked in, one image per room — same as every other image in
this repo. tools/build-audio-endpoint-image-arm64.sh substitutes the endpoint's
hostname from CoreSystemConfig.json's audio_endpoints array into a generated
copy of config/audio-endpoint.yaml, and build-all.sh builds one per entry.
That's a change from the original design, which built one generic .img and left
identity to Raspberry Pi Imager's OS Customisation dialog at flash time. Imager
still works and is still the right tool for Wi-Fi and SSH keys (neither is baked
in) — but hostname was worth moving into the build for two reasons:
- It was the only identity in the household still typed in by hand after a
build, which is the exact step
CoreSystemConfig.jsonexists to remove. - It failed quietly when mistyped.
spotify-connect-startreads$(hostname)at every service start, so a typo doesn't error — it just puts a Spotify Connect device with the wrong room name in the picker, and you find out when you cast to the wrong room.
The cost is real: one full rpi-image-gen run per room instead of one for all
of them, and on an x86 build host each of those is a qemu-emulated arm64 bootstrap.
If you have several arm64 rooms and a slow builder, set "arm64_prebake": false in
the config's build section — that restores the single generic image, and
build-all.sh will build it once no matter how many arm64 rooms are listed.
Hardware setup
dtoverlay=hifiberry-dacplus in /boot/firmware/config.txt (from HiFiBerry's own
current docs, grouped under "DAC+ Standard/Amp2/Amp4" — worth confirming against
your specific board revision before first boot) plus disabling onboard audio, so
the HiFiBerry ends up as the only ALSA card (hw:0, which
systemd/spotify-connect.service.d/10-alsa-device.conf pins ALSA_DEVICE to).
amd64: mini PC + USB DAC/amp
Build
cd hosts/audio-endpoint/live-build-amd64/scripts
sudo ./build-audio-endpoint-iso-amd64.sh
Same shape as tools/build-thin-client-iso.sh: installs
live-build if missing, regenerates config/includes.chroot/ from the shared
../configs/ (git-ignored, generated — never hand-edit it), then runs
lb config && lb build. Output is a hybrid ISO, written to a USB stick and
live-booted — this image doesn't install itself to disk, same as the thin client.
Per-room identity
No Raspberry-Pi-Imager equivalent exists for a generic x86 ISO, so this variant
uses the thin client's own already-proven pattern instead: IMAGE_HOSTNAME in
the build script's CONFIGURATION block, baked in at build time. Edit it and
re-run the script once per room — same convention as the thin client's own
THINCLIENT_NAME/IMAGE_HOSTNAME. spotify-connect-start still just reads
$(hostname) at start time; on this variant that value was set at build time
instead of per-flash.
Hardware setup
Plug in a USB DAC/amp (e.g. Fosi Audio V3) — no driver needed, it's a standard
USB Audio Class device. One manual step is required per physical unit: this
repo can't know in advance which ALSA card index your specific mini PC assigns
the USB device (it may also have onboard audio competing for card 0, and which
one wins varies by board) — guessing one would be exactly the kind of "build
against a guess" this project avoids elsewhere (gesture-config.json's
camera_device, the ESP32 firmware's weather_entity_id). After first boot,
over SSH:
aplay -l # find the USB DAC's card index, e.g. card 1
sudo mkdir -p /etc/systemd/system/spotify-connect.service.d
sudo tee /etc/systemd/system/spotify-connect.service.d/10-alsa-device.conf <<'EOF'
[Service]
Environment=ALSA_DEVICE=hw:1
EOF
sudo systemctl daemon-reload
sudo systemctl restart spotify-connect
Manual verification still outstanding
Nothing here has been built, flashed, or booted on real hardware — no Raspberry Pi, no HiFiBerry Amp2, no x86 test box, and no rpi-image-gen install were available while writing this. In rough order of what to check first:
- rpi-image-gen's actual config/layer schema — see the risk callout above. The single most likely thing to need hand-adjustment.
- The
dtoverlay=hifiberry-dacplusline — cross-checked against HiFiBerry's own current docs, but not against your specific Amp2 board revision. - Whether
spotifyd/librespotactually install from bookworm's apt sources at all — if not, the documented fallback list above is untested against which of those routes is actually easiest today. - The amd64 live-build tree's structure — checked for internal consistency
against
build-thin-client-iso.sh's own already-working structure, but thelb config/lb buildinvocation itself hasn't been run for this specific, much-smaller package/hook set. - USB DAC/amp enumeration — whether
aplay -lreliably shows the DAC as a distinct, stable card index across reboots on a given mini PC, or whether it shifts (some boards' USB enumeration order isn't fully deterministic). Restart=alwaysbehaviour — whether librespot/spotifyd/raspotify ever get into a crash-loop state thatRestartSec=5doesn't recover from cleanly on a headless box nobody is watching.- Neither image has Home Assistant integration of any kind — no MQTT, no HA entities, nothing reachable from the smart-home control surface at all. This is a deliberate scope decision (a self-contained per-room Spotify Connect appliance, matching the "per-room independent" choice this design started from), not an oversight — if remote control/monitoring from HA is wanted later, that's a separate addition, not assumed here.