Haxo#
Role#
The Haxophone — an open-hardware electronic saxophone HAT on a Raspberry Pi Zero 2 W. A single-purpose musical instrument: nothing in the fleet depends on it, and it deliberately has no Tailscale (verifiably absent from the host config; it is reached over Wi-Fi on the LAN for the occasional rebuild).
Declared in hosts/default.nix as class rpi, tags ["rpi"], aarch64 —
built via the nixos-raspberrypi flake (raspberry-pi-02.base board module),
so the kernel/firmware/bootloader are a cache hit from
nixos-raspberrypi.cachix.org. That is the whole point of the Zero 2 W: the
original Zero W 1.1 is ARMv6, uncached, and a NixOS closure for it is a
days-long emulated build (see the honest status below).
What it runs#
haxophone.service—pkgs.haxo-rs(packaged inpkgs/haxo-rs/), the Rust note engine: scans the HAT’s key matrix + breath-pressure sensor via rppal GPIO/I2C and drives FluidSynth — a MIDI wind controller wired straight into a softsynth. Runs as root (rppal needs raw GPIO/I2C),--prog-number 66selects the GM sax patch, soundfont is nixpkgs’FluidR3_GM2-2.sf2. Audio goes out the HAT’s I2S (upstream’s USB-MIDI-gadgetExecStartPreis dropped).- Utility packages only:
fluidsynth,alsa-utils,i2c-tools,libraspberrypi. Docs/man/glances/clamav are all off — it’s a 512 MB Pi.
Hardware#
Raspberry Pi Zero 2 W — BCM2710A1 (4× Cortex-A53, aarch64), same SoC family as
octopi’s Pi 3B. The Haxophone HAT is configured through
nixos-raspberrypi’s structured config.txt interface
(hardware.raspberry-pi.config.all, values taken from haxo-rs’s
scripts/provision.sh):
- MAX98357A I2S codec/amp (
dt-overlays.max98357a,sdmode-pin = 4) — onboard analog audio is forced off so the HAT’s I2S card is the ALSA default. - MPXV7007DP breath-pressure sensor on I2C at 400 kHz
(
i2c_arm_baudrate = 400000);hardware.i2c.enableprovides/dev/i2c-*and thei2cgroup for haxo-rs andi2cdetect.
Builds never happen on-device: nix.settings.max-jobs = 0 offloads everything
to the fleet builders/substituters.
Building the SD image#
sops-nix derives the host’s age identity from its SSH host key, so the image
must ship haxo’s pre-generated host key or first boot can’t decrypt the
tsunami password (users.mutableUsers = false ⇒ no login at all) nor the
Wi-Fi PSK. The private key is never committed — it is injected at image build
time from a path you control, and the matching .pub is derived during the
build:
HAXO_HOST_KEY_PATH=/secure/ssh_host_ed25519_key \
nix build .#nixosConfigurations.haxo.config.system.build.sdImage --impure
If HAXO_HOST_KEY_PATH is unset the key step is skipped (fine for
nix flake check, useless for a real flash). The image is uncompressed
(sdImage.compressImage = false).
Networking#
Wi-Fi only (wlan0) on FalseBlue-Basic, with the same brcmfmac quirks as
octopi: WPA2-Personal forced (authProtocols = ["WPA-PSK"] — WPA3/SAE never
associates on this chip) and PMF/802.11w disabled (ieee80211w=0 — the FullMAC
firmware mishandles the global pmf=1 NixOS injects; association dies at
status=16). The PSK comes from the shared sops wifi secret octopi already
uses. networking.interfaces is force-cleared so systemd doesn’t wait ~90 s
for the eno1 that core/default.nix declares by default.
Honest status (what’s live vs. declared)#
The NixOS host config is complete and builds, but per the workaround comment in
pkgs/default.nix (PR #235), the physical haxophone is still the original
Pi Zero W 1.1 (ARMv6) running Raspberry Pi OS. Until the Zero 2 W swap lands,
only the haxo-rs binary is cross-compiled from this flake and copied over:
nix build .#haxo-rs-armv6l
scp result/bin/haxo001 pi@haxophone:/usr/local/bin/
Outstanding before the NixOS host is real:
- sops onboarding — haxo’s key must be added as a recipient in
.sops.yamlbefore it can decrypt the sharedwifiPSK (called out in the host config). - Host key generation + injection — the
HAXO_HOST_KEY_PATHbuild above, then flash. - Retire the
haxo-rs-armv6lcross-compile workaround inpkgs/default.nixonce the host is live.