feat: Raspberry Pi 4 (aarch64) target #110
4 changed files with 238 additions and 4 deletions
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
commit
91c6994dd4
|
|
@ -14,7 +14,9 @@ deploy/nixos/
|
|||
├── hardware/
|
||||
│ ├── douro.nix # Dell OptiPlex 9030 AIO (stock Douro motherboard; SATA SSD, eGalax touch)
|
||||
│ ├── batm3.nix # GeneralBytes BATM3 chassis with a Dell OptiPlex 9030 AIO grafted in (custom mod; WireGuard wired in)
|
||||
│ └── upboard.nix # Aaeon UP Board (Sintra + tejo; eMMC root via sdhci-acpi + mmc_block)
|
||||
│ ├── upboard.nix # Aaeon UP Board (Sintra + tejo; eMMC root via sdhci-acpi + mmc_block)
|
||||
│ ├── raspberry-pi-5.nix # Raspberry Pi 5 (aarch64) — DIY build; board glue on top of nixos-hardware
|
||||
│ └── raspberry-pi-4.nix # Raspberry Pi 4 (aarch64) — same, previous-generation board
|
||||
├── udev/
|
||||
│ └── 99-bitspire-hardware.rules # additional udev rules (loaded via configuration.nix)
|
||||
├── provision-atm.sh # Push LNbits credentials to a deployed ATM via SSH
|
||||
|
|
@ -36,6 +38,11 @@ Each ATM model has two flake outputs:
|
|||
|
||||
Models: `douro`, `tejo`, `sintra`, `batm3`.
|
||||
|
||||
The Raspberry Pi boards (`rpi5`, `rpi4`) are aarch64 and follow a different
|
||||
pipeline — SD-card image instead of GPT disk image, U-Boot instead of
|
||||
systemd-boot, and they need an aarch64 builder. See
|
||||
[`docs/raspberry-pi-setup.md`](../../docs/raspberry-pi-setup.md).
|
||||
|
||||
```bash
|
||||
# Build a Sintra disk image
|
||||
nix build .#disk-image-sintra
|
||||
|
|
|
|||
69
deploy/nixos/hardware/raspberry-pi-4.nix
Normal file
69
deploy/nixos/hardware/raspberry-pi-4.nix
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
# Raspberry Pi 4 hardware module (aarch64).
|
||||
#
|
||||
# The Pi 4 twin of raspberry-pi-5.nix. Kernel, firmware, bootloader and device
|
||||
# tree come from the nixos-hardware `raspberry-pi-4` module (paired with this
|
||||
# file in flake.nix's piBoards); here we set only the bitSpire-specific
|
||||
# hardware glue: serial for the bill validators, the kiosk display driver, and
|
||||
# no-suspend. The wiring notes in raspberry-pi-5.nix apply unchanged — same
|
||||
# validators over USB-serial, same QR scanner, same touchscreen options.
|
||||
#
|
||||
# What differs from the Pi 5:
|
||||
# - GPU/KMS: the Pi 5 module enables vc4/v3d modesetting by default; on the
|
||||
# Pi 4 it is an opt-in (`fkms-3d`) that also injects the CMA + vc4 device
|
||||
# tree overlays. Without it X falls back to the plain framebuffer and
|
||||
# Electron renders in software.
|
||||
# - Memory: 4 GB is the floor for Electron + the kiosk; 8 GB is comfortable.
|
||||
# The shared Pi runtime's MemoryMax=2G leaves headroom on either.
|
||||
# - No PCIe (the Pi 5's NVMe path); boot/root is SD or USB-SATA only.
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
{
|
||||
# aarch64 target. (The flake instantiates this config with aarch64 pkgs; this
|
||||
# line documents/asserts it.)
|
||||
nixpkgs.hostPlatform = lib.mkDefault "aarch64-linux";
|
||||
|
||||
# Bootloader: the aarch64 sd-image uses the extlinux-compatible generator;
|
||||
# nixos-hardware's rpi4 module wires the firmware/u-boot. No systemd-boot.
|
||||
boot.loader.grub.enable = lib.mkDefault false;
|
||||
boot.loader.generic-extlinux-compatible.enable = lib.mkDefault true;
|
||||
|
||||
# Primary UART (GPIO 14/15) available for a GPIO-wired validator. Keep the
|
||||
# serial console OFF it so the validator owns the line — mirrors upboard.nix
|
||||
# keeping ttyS4 free for the dispenser. USB-serial adapters are unaffected.
|
||||
#
|
||||
# Not mkDefault: kernelParams is list-merged, and only definitions at the
|
||||
# highest priority survive. nixpkgs sets loglevel/lsm at normal priority, so
|
||||
# a mkDefault list here is dropped entirely — and with no console= at all
|
||||
# the kernel falls back to the device tree's stdout-path, i.e. this UART.
|
||||
boot.kernelParams = [ "console=tty0" ];
|
||||
|
||||
# Kiosk display: vc4/v3d kernel modesetting via the firmware KMS overlay.
|
||||
# This is the Pi 4's equivalent of the Pi 5's default KMS path — it also
|
||||
# sets services.xserver.videoDrivers to modesetting (fbdev fallback), so we
|
||||
# don't set that here. Electron renders through it as on the UP Board.
|
||||
hardware.raspberry-pi."4".fkms-3d.enable = true;
|
||||
|
||||
hardware.enableRedistributableFirmware = true;
|
||||
|
||||
# Kiosk: never sleep.
|
||||
systemd.targets = {
|
||||
sleep.enable = false;
|
||||
suspend.enable = false;
|
||||
hibernate.enable = false;
|
||||
hybrid-sleep.enable = false;
|
||||
};
|
||||
|
||||
# Stable device symlinks for USB-serial bill-validator adapters, so the ATM
|
||||
# config can point at /dev/ttyValidator0 regardless of enumeration order.
|
||||
# Identical to the Pi 5 module — same adapters, same bridges. If two adapters
|
||||
# of the SAME chip are used, disambiguate by KERNELS/serial instead — tune
|
||||
# during bring-up.
|
||||
services.udev.extraRules = lib.mkAfter ''
|
||||
# FTDI (e.g. FT232R) → ttyValidator0
|
||||
SUBSYSTEM=="tty", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6001", SYMLINK+="ttyValidator0"
|
||||
# Silicon Labs CP210x → ttyValidator1
|
||||
SUBSYSTEM=="tty", ATTRS{idVendor}=="10c4", ATTRS{idProduct}=="ea60", SYMLINK+="ttyValidator1"
|
||||
# WCH CH340 → ttyValidator2
|
||||
SUBSYSTEM=="tty", ATTRS{idVendor}=="1a86", ATTRS{idProduct}=="7523", SYMLINK+="ttyValidator2"
|
||||
'';
|
||||
}
|
||||
145
docs/raspberry-pi-setup.md
Normal file
145
docs/raspberry-pi-setup.md
Normal 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`
|
||||
19
flake.nix
19
flake.nix
|
|
@ -317,6 +317,10 @@
|
|||
hardware = nixos-hardware.nixosModules.raspberry-pi-5;
|
||||
glue = ./deploy/nixos/hardware/raspberry-pi-5.nix;
|
||||
};
|
||||
rpi4 = {
|
||||
hardware = nixos-hardware.nixosModules.raspberry-pi-4;
|
||||
glue = ./deploy/nixos/hardware/raspberry-pi-4.nix;
|
||||
};
|
||||
};
|
||||
|
||||
# Shared module list (everything EXCEPT the root fs and the sd-image
|
||||
|
|
@ -483,6 +487,12 @@
|
|||
rpi5-installed = mkPiInstalled "rpi5";
|
||||
rpi5-image = mkPiImage "rpi5";
|
||||
|
||||
# Raspberry Pi 4 (aarch64) — same runtime and products as rpi5, on the
|
||||
# previous-generation board (see deploy/nixos/hardware/raspberry-pi-4.nix
|
||||
# for what differs). 4 GB minimum for Electron; 8 GB comfortable.
|
||||
rpi4-installed = mkPiInstalled "rpi4";
|
||||
rpi4-image = mkPiImage "rpi4";
|
||||
|
||||
# USB-bootable variant of batm3-installed. This is the config the
|
||||
# flashed USB stick actually runs — distinct fs labels so stage-1 can't
|
||||
# latch the internal drive, nofail /boot, no growPartition, autoUpgrade
|
||||
|
|
@ -697,13 +707,16 @@
|
|||
iso = self.nixosConfigurations.douro.config.system.build.isoImage;
|
||||
};
|
||||
|
||||
# ── Packages (aarch64-linux — Raspberry Pi 5 build) ───────────
|
||||
# Flashable SD image for the Pi 5. Build on an aarch64 builder (native Pi
|
||||
# / arm box / `boot.binfmt` emulation on this x86 host):
|
||||
# ── Packages (aarch64-linux — Raspberry Pi builds) ────────────
|
||||
# Flashable SD images for the Pi boards. Build on an aarch64 builder
|
||||
# (native Pi / arm box / `boot.binfmt` emulation on this x86 host):
|
||||
# nix build .#packages.aarch64-linux.sd-image-rpi5
|
||||
# nix build .#packages.aarch64-linux.sd-image-rpi4
|
||||
packages.aarch64-linux = {
|
||||
sd-image-rpi5 = self.nixosConfigurations.rpi5-image.config.system.build.sdImage;
|
||||
atm-app-rpi5 = mkAtmAppAarch64 { model = "rpi5"; fiatCode = "USD"; };
|
||||
sd-image-rpi4 = self.nixosConfigurations.rpi4-image.config.system.build.sdImage;
|
||||
atm-app-rpi4 = mkAtmAppAarch64 { model = "rpi4"; fiatCode = "USD"; };
|
||||
};
|
||||
}
|
||||
//
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue