feat(deploy): add aarch64 Raspberry Pi 4 target

The Pi 4 twin of the Pi 5 build: `rpi4-installed` (in-place rebuild target),
`rpi4-image`, and `packages.aarch64-linux.{sd-image-rpi4,atm-app-rpi4}`, all
through the board-keyed machinery of the previous commit. The shared runtime
is untouched; only the board pair is new.

deploy/nixos/hardware/raspberry-pi-4.nix mirrors raspberry-pi-5.nix line for
line except where the boards differ:
- KMS for the kiosk display is an opt-in on the Pi 4
  (`hardware.raspberry-pi."4".fkms-3d`), which also injects the CMA + vc4
  device-tree overlays; the Pi 5 gets it by default. Without it X falls back
  to the framebuffer and Electron renders in software.
- fkms-3d sets videoDrivers itself, so the module doesn't.
Everything else — extlinux boot, console pinned to tty0 so the GPIO UART is
free for a validator, no-suspend, the ttyValidator{0,1,2} udev symlinks — is
identical by design.

Evaluation-verified only: rpi4-installed/rpi4-image instantiate, and against
rpi5 they differ solely in the expected places (bcm2711 device tree, the two
fkms overlays, the rpiVersion=4 kernel, no clk-rp1 in initrd, machine model
in the env seed). Not yet booted on hardware; the doc says so.

docs/raspberry-pi-setup.md covers both boards — build, flash, first boot +
provisioning via the spire seed, in-place updates, peripherals — since #87
shipped the Pi 5 without one. It replaces a never-committed Pi 4 sketch
(parked on wip/rpi4-sketch) whose flake wiring didn't evaluate and whose
provisioning section predated the pairing seed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013A6683cCHnQxFUosx1krY4
This commit is contained in:
Padreug 2026-09-20 11:26:17 +02:00
commit 91c6994dd4
4 changed files with 238 additions and 4 deletions

145
docs/raspberry-pi-setup.md Normal file
View file

@ -0,0 +1,145 @@
# Deploying bitSpire to a Raspberry Pi (4 or 5)
The DIY reference build: a Raspberry Pi, a bill validator on USB-serial, a
touchscreen and a QR scanner. Both boards share one runtime in `flake.nix`
(`piBaseModules`) and differ only in their hardware glue module:
| Board | `nixosConfigurations` | Flashable image | Hardware glue |
|---|---|---|---|
| Raspberry Pi 5 | `rpi5-installed`, `rpi5-image` | `packages.aarch64-linux.sd-image-rpi5` | `deploy/nixos/hardware/raspberry-pi-5.nix` |
| Raspberry Pi 4 | `rpi4-installed`, `rpi4-image` | `packages.aarch64-linux.sd-image-rpi4` | `deploy/nixos/hardware/raspberry-pi-4.nix` |
`<board>-image` is what you flash; `<board>-installed` is what a running Pi
rebuilds itself against afterwards. They share every module except the root
filesystem declaration and the SD-image builder — see the comments above
`mkPiInstalled` / `mkPiImage` in `flake.nix` for why they're split.
**Status:** the Pi 4 target is evaluation-verified (it instantiates, and its
configuration differs from the Pi 5's only in the expected board-specific
places) but has not yet been booted on hardware. Treat the first bring-up as
exactly that.
## What's different from the x86 fleet
- **aarch64.** Any Pi build needs an aarch64 builder — a Pi itself, an ARM
box, or `boot.binfmt.emulatedSystems = [ "aarch64-linux" ]` on an x86 host.
The Electron app closure will not build on plain x86.
- **Boot.** Raspberry Pi firmware + U-Boot + extlinux (from `nixos-hardware`),
not systemd-boot. The image is an SD-card image, not a GPT disk image.
- **No `determinate`, no `atm-tui`** in the Pi runtime yet (neither ships an
aarch64 package). Add them when they do.
- **Cachix is pre-wired** (`aiolabs.cachix.org` in `nix.settings`), so an
in-place rebuild substitutes the heavy closure rather than compiling on the
Pi — provided the closure was pushed there first.
## Board differences that matter
| | Pi 5 | Pi 4 |
|---|---|---|
| SoC / device tree | BCM2712 (`bcm2712*-rpi-*.dtb`) | BCM2711 (`bcm2711-rpi-4*.dtb`) |
| Kiosk GPU / KMS | vc4/v3d on by default | opt-in via `hardware.raspberry-pi."4".fkms-3d` (the glue module enables it; it also injects the CMA + vc4 overlays) |
| RAM | 4–16 GB | 4 GB is the floor for Electron; 8 GB is comfortable |
| Root storage | SD, USB, or NVMe over PCIe | SD or USB only |
| Power | 5 V / 5 A USB-C PD | 5 V / 3 A; Electron startup is the peak |
Everything else — validator serial symlinks, `console=tty0` keeping the GPIO
UART free, no-suspend, the bitspire service — is identical between the two
glue modules by design. Keep it that way: a change to one almost certainly
belongs in the other.
## 1. Build the image
On (or via) an aarch64 builder:
```bash
nix build .#packages.aarch64-linux.sd-image-rpi4 # or sd-image-rpi5
ls result/sd-image/
# → nixos-image-sd-card-<version>-aarch64-linux.img.zst
```
## 2. Flash
```bash
lsblk -f # identify the SD card — NOT your main disk
zstd -dc result/sd-image/*.img.zst | sudo dd of=/dev/sdX bs=4M status=progress conv=fsync
```
The image carries two labelled partitions the installed config expects:
`FIRMWARE` (vfat, Pi firmware + U-Boot) and `NIXOS_SD` (ext4 root). The root
partition grows to fill the card on first boot.
## 3. First boot
Insert the card, connect Ethernet and power. The Pi boots into the kiosk with
no pairing, so the screen shows the pairing wizard — with a camera it waits
for a QR; without one it says so and tells you to provision `VITE_SPIRE_SEED`
instead. It also comes up with sshd and password auth enabled, same as the
x86 installed configs — this is the provisioning window.
Provision exactly as for a Sintra — from the dev box, with the spire seed
minted by spirekeeper:
```bash
SPIRE_SEED='spire-seed:v1:…' bash deploy/nixos/provision-atm.sh <pi-ip> 22
```
That writes `/var/lib/bitspire/.env` and restarts the service; the seed
carries the relay and the LNbits transport pubkey, so nothing else is needed.
Alternatively, show the seed's QR to the machine's camera and let the
on-screen wizard do the same thing. Verify with:
```bash
ssh bitspire@<pi-ip> 'journalctl -u bitspire -n 50 --no-pager | grep "\["'
# expect [Signer] Pairing to bunker … then [Lightning] LNbits client initialized
```
The dev-only `VITE_ATM_PRIVATE_KEY` fallback works here too for a bench
setup without a bunker — see `provision-atm.sh`'s header for the variables.
## 4. Updating in place
Once flashed, never re-flash for a software update. The `-installed` target
is the aarch64 twin of the fleet's rebuild ritual:
```bash
sudo nixos-rebuild switch --flake \
"git+ssh://forgejo@git.atitlan.io/aiolabs/bitspire.git?ref=dev#rpi4-installed"
```
(Pi 5: `#rpi5-installed`.) With the closure on cachix this is a download, not
a build. If it starts compiling Electron on the Pi, the closure wasn't pushed
— stop, build on the aarch64 builder, `cachix push aiolabs`, retry.
## 5. Wiring the peripherals
- **Bill validator** — over a USB-serial adapter. The glue modules give
stable symlinks so `.env` never depends on enumeration order:
FTDI → `/dev/ttyValidator0`, CP210x → `/dev/ttyValidator1`,
CH340 → `/dev/ttyValidator2`. Two adapters of the *same* chip need
disambiguating by `KERNELS`/serial in the udev rule — do that at bring-up.
A validator wired straight to the GPIO UART (pins 14/15) also works: the
kernel console is pinned to `tty0` precisely so that UART stays free.
- **QR scanner** — USB HID keyboard-emulation, no configuration.
- **Touchscreen** — DSI or HDMI. X runs on the Pi's KMS driver.
- **Serial console for debugging** — there isn't one by default (see above).
Use SSH, or temporarily add `console=ttyAMA0,115200` to
`boot.kernelParams` in the glue module.
## Hardware notes for 24/7 operation
- **Storage.** SD cards have finite write endurance; for anything beyond a
bench build put root on a USB-SATA SSD (both boards) or NVMe (Pi 5).
`state.db` and the journal are the writers.
- **Thermal.** Both boards throttle under sustained load without cooling.
Heatsinks at minimum; the official active cooler for the Pi 5.
- **Power.** Brown-outs on Electron startup look like random reboots. Use the
official supply or one rated above the board's peak, never a hub.
- **Swap.** The runtime provisions a 2 GB swapfile so memory pressure degrades
instead of hard-freezing. On a 4 GB Pi 4 that is not optional.
## Related
- `deploy/nixos/README.md` — the x86 fleet pipeline this mirrors
- `docs/machine-installation.md` — why images rather than `nixos-install`
- `deploy/nixos/hardware/raspberry-pi-{4,5}.nix` — the per-board glue
- `flake.nix` — `piBoards`, `piBaseModules`, `mkPiInstalled`, `mkPiImage`