feat(deploy): add aarch64 Raspberry Pi 5 target (sd-image)

Proper aarch64 NixOS target for the DIY Pi 5 build, wired additively so
the x86 fleet path is untouched (both the new Pi sd-image and the
existing sintra-installed still evaluate cleanly):

- nixos-hardware input (raspberry-pi-5 module) for Pi kernel/firmware/GPU.
- aarch64 pkgs + pkgs-unstable + mkAtmApp instances (parallel to x86).
- mkPiConfig: aarch64 nixosSystem reusing the shared configuration.nix +
  bitspire-atm service, replicating the installed-config runtime (bitspire
  service, first-boot env seed, electron service override, swap). Drops
  the x86 fleet machinery for a first bring-up: no determinate/autoUpgrade
  (not yet fleet-managed) and no atm-tui (needs an aarch64 package).
- raspberry-pi-5.nix hardware module: extlinux boot, vc4/v3d KMS for X,
  primary UART free for a GPIO-wired validator, no-suspend, and stable
  /dev/ttyValidator* udev symlinks for USB-serial validator adapters
  (Apex 7600 RS-232 via adapter, NV10 USB+).
- nixosConfigurations.rpi5-installed + packages.aarch64-linux.sd-image-rpi5.

BUILD NOTE: the app closure (aarch64 electron/native addons) needs an
aarch64 builder — a native Pi/arm box or `boot.binfmt` qemu emulation on
an x86 host. Config evaluates on x86; it just can't build there.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018ivBosaWmv8vwFE7ejrdHW
This commit is contained in:
Padreug 2026-08-16 21:30:53 +02:00
commit a77bae50ee
3 changed files with 217 additions and 2 deletions

View file

@ -0,0 +1,63 @@
# Raspberry Pi 5 hardware module (aarch64).
#
# The bitSpire equivalent of upboard.nix, but for a Pi 5 instead of the x86
# UP Board. Kernel, firmware, GPU and bootloader come from the nixos-hardware
# `raspberry-pi-5` module (added alongside this one in flake.nix); here we set
# only the bitSpire-specific hardware glue: serial for the bill validators,
# the kiosk display driver, and no-suspend.
#
# Wiring the parts (see docs) to a Pi 5:
# - Bill validators (Apex 7600 RS-232, NV10 USB+): easiest via one USB-serial
# adapter each → stable /dev/ttyValidator* symlinks below. A GPIO-UART wire
# is also supported (primary UART enabled, console kept off it).
# - QR scanner: USB HID, no config.
# - 7" touchscreen: DSI or HDMI; the vc4/v3d KMS driver (from nixos-hardware)
# backs X.
# - Boot/root: USB-SATA SSD or the SD card.
{ 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 rpi5 module wires the firmware/u-boot. No systemd-boot
# (that's x86/UEFI, as on the UP Board).
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.
boot.kernelParams = lib.mkDefault [ "console=tty0" ];
# X uses the Pi GPU's kernel modesetting driver (vc4/v3d KMS from
# nixos-hardware). Electron renders through it as on the UP Board.
services.xserver.videoDrivers = lib.mkDefault [ "modesetting" ];
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.
# Covers the common bridges (FTDI, Silicon Labs CP210x, WCH CH340). If two
# adapters of the SAME chip are used, disambiguate by KERNELS/serial instead —
# tune during bring-up. The NV10 USB+ presents its own USB CDC serial; add its
# idVendor/idProduct here once known.
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"
'';
}

34
flake.lock generated
View file

@ -405,6 +405,24 @@
"type": "github"
}
},
"nixos-hardware": {
"inputs": {
"nixpkgs": "nixpkgs_3"
},
"locked": {
"lastModified": 1786867632,
"narHash": "sha256-ez+ubZlA1RtdjCB18a6zJ9M4u8qoPDy08EcnsW5M3Xw=",
"owner": "NixOS",
"repo": "nixos-hardware",
"rev": "ff17823245ab9ff7bcae6acf950bd89cba82c38c",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "nixos-hardware",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1773222311,
@ -482,6 +500,19 @@
}
},
"nixpkgs_3": {
"locked": {
"lastModified": 1767892417,
"narHash": "sha256-8bW3q88CEg2u4hSP66Vf4lpbLonHz7hqDNBMcCY7E9U=",
"rev": "3497aa5c9457a9d88d71fa93a4a8368816fbeeba",
"type": "tarball",
"url": "https://releases.nixos.org/nixos/unstable/nixos-26.05pre924538.3497aa5c9457/nixexprs.tar.xz"
},
"original": {
"type": "tarball",
"url": "https://channels.nixos.org/nixos-unstable/nixexprs.tar.xz"
}
},
"nixpkgs_4": {
"locked": {
"lastModified": 1779467186,
"narHash": "sha256-nOesoDCiXcUftqbRBMz9tt4blI5PvljMWbm3kuCA+0s=",
@ -503,7 +534,8 @@
"determinate": "determinate",
"devenv": "devenv",
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs_3",
"nixos-hardware": "nixos-hardware",
"nixpkgs": "nixpkgs_4",
"nixpkgs-unstable": "nixpkgs-unstable",
"rust-overlay": "rust-overlay"
}

122
flake.nix
View file

@ -36,9 +36,12 @@
url = "git+ssh://forgejo@git.atitlan.io/aiolabs/atm-tui.git";
inputs.nixpkgs.follows = "nixpkgs-unstable";
};
# Raspberry Pi 5 (aarch64) hardware support for the DIY Pi build.
nixos-hardware.url = "github:NixOS/nixos-hardware";
};
outputs = { self, nixpkgs, nixpkgs-unstable, flake-utils, rust-overlay, devenv, determinate, atm-tui }:
outputs = { self, nixpkgs, nixpkgs-unstable, flake-utils, rust-overlay, devenv, determinate, atm-tui, nixos-hardware }:
let
system = "x86_64-linux";
@ -59,6 +62,24 @@
src = self;
};
# aarch64 (Raspberry Pi 5) toolchain — a parallel set of pkgs + app
# builder for the DIY Pi build. Kept fully separate from the x86 fleet
# path so nothing above changes.
pkgsAarch64 = import nixpkgs {
system = "aarch64-linux";
config.allowUnfree = true;
};
pkgsUnstableAarch64 = import nixpkgs-unstable {
system = "aarch64-linux";
config.allowUnfree = true;
overlays = [ (import rust-overlay) ];
};
mkAtmAppAarch64 = import ./nix/mkAtmApp.nix {
pkgs = pkgsAarch64;
pkgs-unstable = pkgsUnstableAarch64;
src = self;
};
# Fiat code per machine model
fiatCodeForModel = {
douro = "GTQ";
@ -261,6 +282,92 @@
})
];
};
# Raspberry Pi 5 (aarch64) installed config — the DIY Pi build. Mirrors
# mkInstalledConfig's runtime (bitspire service, env activation, electron
# service override, swap) on aarch64 + Pi hardware, but deliberately drops
# the x86 fleet machinery for a first bring-up: no determinate/autoUpgrade
# (not yet a managed fleet member) and no atm-tui (add once it publishes an
# aarch64 package). Builds an SD image; needs an aarch64 builder (native
# Pi / arm box / binfmt emulation) — the app closure won't build on x86.
mkPiConfig = machineModel:
let
atm-app = mkAtmAppAarch64 {
model = machineModel;
fiatCode = fiatCodeForModel.${machineModel} or "USD";
};
fiatCode = fiatCodeForModel.${machineModel} or "USD";
in
nixpkgs.lib.nixosSystem {
system = "aarch64-linux";
specialArgs = {
pkgs-unstable = pkgsUnstableAarch64;
inherit atm-app;
};
modules = [
nixos-hardware.nixosModules.raspberry-pi-5
(nixpkgs + "/nixos/modules/installer/sd-card/sd-image-aarch64.nix")
./deploy/nixos/configuration.nix
./deploy/nixos/bitspire-atm.nix
./deploy/nixos/hardware/raspberry-pi-5.nix
({ config, lib, pkgs, pkgs-unstable, ... }: {
services.bitspire = {
enable = true;
appDir = "${atm-app}";
};
environment.systemPackages = [
(pkgs.writeShellScriptBin "fund-atm" ''
exec ${pkgs-unstable.nodejs}/bin/node ${atm-app}/dist-electron/fund-atm.bundle.cjs "$@"
'')
];
environment.variables.ATM_DB_PATH = "/var/lib/bitspire/state.db";
boot.kernel.sysctl."kernel.unprivileged_userns_clone" = 1;
security.sudo.wheelNeedsPassword = false;
# Same first-boot env seed as the x86 installed configs.
system.activationScripts.bitspire-env = ''
mkdir -p /var/lib/bitspire
if [ ! -f /var/lib/bitspire/.env ]; then
cp ${pkgs.writeText "bitspire-env-default" (''
VITE_LAMASSU_MACHINE_MODEL=${machineModel}
VITE_LAMASSU_FIAT_CODE=${fiatCode}
VITE_SPIRE_SEED=
ELECTRON_FORCE_PROD=1
DISPLAY=:0
'' + pkgs.lib.optionalString (config.services.bitspire.relayUrl != "") ''
VITE_RELAY_URL=${config.services.bitspire.relayUrl}
'' + pkgs.lib.optionalString (config.services.bitspire.lnbitsServerPubkey != "") ''
VITE_LNBITS_SERVER_PUBKEY=${config.services.bitspire.lnbitsServerPubkey}
'')} /var/lib/bitspire/.env
chmod 600 /var/lib/bitspire/.env
chown bitspire:bitspire /var/lib/bitspire/.env
fi
'';
# Electron runtime override (same flags as the x86 fleet, aarch64
# electron). No eDP display-reset here — that's UP-Board-specific;
# the Pi drives HDMI/DSI directly.
systemd.services.bitspire.serviceConfig = {
EnvironmentFile = lib.mkForce "/var/lib/bitspire/.env";
Environment = "LD_LIBRARY_PATH=${pkgs.stdenv.cc.cc.lib}/lib";
ExecStart = lib.mkForce "${pkgs-unstable.electron}/bin/electron --no-sandbox --disable-gpu-sandbox --disable-software-rasterizer --enable-logging ${atm-app}";
MemoryMax = lib.mkForce "2G";
NoNewPrivileges = lib.mkForce false;
ProtectSystem = lib.mkForce false;
ProtectHome = lib.mkForce false;
PrivateTmp = lib.mkForce false;
DevicePolicy = lib.mkForce "auto";
DeviceAllow = lib.mkForce [ "char-* rw" ];
};
swapDevices = [{ device = "/var/swapfile"; size = 2048; }];
boot.tmp.cleanOnBoot = true;
services.openssh.settings.PasswordAuthentication = lib.mkForce true;
})
];
};
in
{
# ── NixOS Configurations (top-level, not per-system) ──────────
@ -293,6 +400,10 @@
sintra-installed = mkInstalledConfig "sintra" ./deploy/nixos/hardware/upboard.nix;
batm3-installed = mkInstalledConfig "batm3" ./deploy/nixos/hardware/batm3.nix;
# Raspberry Pi 5 (aarch64) DIY build — Apex 7600 / NV10 over USB-serial.
# Build the SD image via packages.aarch64-linux.sd-image-rpi5.
rpi5-installed = mkPiConfig "rpi5";
# 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
@ -506,6 +617,15 @@
# Backwards compat
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):
# nix build .#packages.aarch64-linux.sd-image-rpi5
packages.aarch64-linux = {
sd-image-rpi5 = self.nixosConfigurations.rpi5-installed.config.system.build.sdImage;
atm-app-rpi5 = mkAtmAppAarch64 { model = "rpi5"; fiatCode = "USD"; };
};
}
//
# ── Dev shells (per-system via flake-utils) ───────────────────