From 91c6994dd4f20f671f102c3c9a9dbfd349250c71 Mon Sep 17 00:00:00 2001 From: Padreug Date: Sun, 20 Sep 2026 11:26:17 +0200 Subject: [PATCH] feat(deploy): add aarch64 Raspberry Pi 4 target MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_013A6683cCHnQxFUosx1krY4 --- deploy/nixos/README.md | 9 +- deploy/nixos/hardware/raspberry-pi-4.nix | 69 +++++++++++ docs/raspberry-pi-setup.md | 145 +++++++++++++++++++++++ flake.nix | 19 ++- 4 files changed, 238 insertions(+), 4 deletions(-) create mode 100644 deploy/nixos/hardware/raspberry-pi-4.nix create mode 100644 docs/raspberry-pi-setup.md diff --git a/deploy/nixos/README.md b/deploy/nixos/README.md index 2bfc333..7dff65a 100644 --- a/deploy/nixos/README.md +++ b/deploy/nixos/README.md @@ -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 diff --git a/deploy/nixos/hardware/raspberry-pi-4.nix b/deploy/nixos/hardware/raspberry-pi-4.nix new file mode 100644 index 0000000..50388e9 --- /dev/null +++ b/deploy/nixos/hardware/raspberry-pi-4.nix @@ -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" + ''; +} diff --git a/docs/raspberry-pi-setup.md b/docs/raspberry-pi-setup.md new file mode 100644 index 0000000..d6c525f --- /dev/null +++ b/docs/raspberry-pi-setup.md @@ -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` | + +`-image` is what you flash; `-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--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 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@ '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` diff --git a/flake.nix b/flake.nix index ab640f7..029a879 100644 --- a/flake.nix +++ b/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"; }; }; } //