feat: Raspberry Pi 5 (aarch64) target + Pyramid Apex RS-232 bill validator #87

Open
padreug wants to merge 4 commits from feat/rpi5-apex-hal into dev
Showing only changes of commit d8680f67ae - Show all commits

feat(deploy): split rpi5 target into installed + image variants

`rpi5-installed` previously bundled the sd-image-aarch64 module, so it
was only good for building a flashable image — a `nixos-rebuild switch`
against it (local or the git+ssh remote form) would drag in the image
builder and its image-specific fs wiring. Split it, mirroring the x86
fleet's mkLiveConfig/mkInstalledConfig separation:

- rpi5-installed → in-place rebuild target. Declares the flashed media's
  own root fs (NIXOS_SD / FIRMWARE labels), nothing image-specific. This
  is what
    sudo nixos-rebuild switch --flake \
      "git+ssh://forgejo@git.atitlan.io/aiolabs/bitspire.git?ref=<branch>#rpi5-installed"
  targets, the aarch64 equivalent of the sintra/douro deploy ritual.
- rpi5-image → same shared runtime + the sd-image builder. Its
  system.build.sdImage is the flashable artifact; packages.aarch64-linux
  .sd-image-rpi5 now points here.

Shared runtime extracted into piBaseModules/mkPiRuntime; folded the
aiolabs cachix substituter + trusted key into the Pi's nix.settings so a
remote rebuild substitutes the heavy aarch64 closure instead of building
it on the Pi (no max-jobs/timeout watchdog — the Pi 5 can build locally
if it must).

Verified: rpi5-installed evaluates to a valid system toplevel (root fs
present), rpi5-image/sd-image-rpi5 to the .img.zst builder, and x86
sintra-installed is byte-identically unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SGUJJjBDuYwRaWSkFdK3bm
Padreug 2026-08-17 08:41:52 +02:00

225
flake.nix
View file

@ -283,89 +283,151 @@
]; ];
}; };
# Raspberry Pi 5 (aarch64) installed config — the DIY Pi build. Mirrors # Raspberry Pi 5 (aarch64) DIY build. Two products from one shared runtime:
# mkInstalledConfig's runtime (bitspire service, env activation, electron # - mkPiInstalled: the in-place rebuild target. `nixos-rebuild switch
# service override, swap) on aarch64 + Pi hardware, but deliberately drops # --flake .#rpi5-installed` (or the git+ssh remote form) targets this.
# the x86 fleet machinery for a first bring-up: no determinate/autoUpgrade # Declares the flashed media's own root fs (NIXOS_SD / FIRMWARE) and
# (not yet a managed fleet member) and no atm-tui (add once it publishes an # NOTHING image-specific, so a switch on a running Pi never trips over
# aarch64 package). Builds an SD image; needs an aarch64 builder (native # the sd-image builder.
# Pi / arm box / binfmt emulation) — the app closure won't build on x86. # - mkPiImage: the same runtime + the aarch64 sd-image module, whose
mkPiConfig = machineModel: # system.build.sdImage is the flashable artifact. The module supplies
let # its OWN NIXOS_SD/FIRMWARE fileSystems + u-boot firmware, so we must
atm-app = mkAtmAppAarch64 { # not re-declare the root fs here (double definition = eval conflict).
model = machineModel; #
fiatCode = fiatCodeForModel.${machineModel} or "USD"; # The runtime mirrors mkInstalledConfig (bitspire service, env activation,
}; # electron override, cache substituters) on aarch64 + Pi hardware, but
# deliberately drops the x86 fleet machinery for first bring-up: no
# determinate, no atm-tui (add once it ships an aarch64 package). Any Pi
# build needs an aarch64 builder (native Pi / arm box / binfmt emulation) —
# the app closure won't build on x86.
mkPiRuntime = machineModel: {
atm-app = mkAtmAppAarch64 {
model = machineModel;
fiatCode = fiatCodeForModel.${machineModel} or "USD"; fiatCode = fiatCodeForModel.${machineModel} or "USD";
in };
fiatCode = fiatCodeForModel.${machineModel} or "USD";
};
# Shared module list (everything EXCEPT the root fs and the sd-image
# builder). atm-app/fiatCode are threaded in so both products share one
# evaluated app closure.
piBaseModules = { machineModel, atm-app, fiatCode }: [
nixos-hardware.nixosModules.raspberry-pi-5
./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;
# Pull from the aiolabs binary cache so a `nixos-rebuild switch`
# (local or the git+ssh remote form) substitutes the heavy aarch64
# closure instead of compiling on the Pi. Mirrors the x86 fleet's
# nix.settings, minus max-jobs/timeout — the Pi 5 can actually build
# locally if it must, so we don't want the 60s watchdog killing a
# legitimate first build.
nix.settings = {
trusted-users = [ "root" "bitspire" ];
substituters = [ "https://cache.nixos.org" "https://aiolabs.cachix.org" ];
trusted-public-keys = [
"cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY="
"aiolabs.cachix.org-1:PrAjsGU9PE77tFKP2+iO+mgR88c4xv3utM9JmpTblUQ="
];
};
# 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-place rebuild target — declares the flashed media's own filesystems
# (the labels mkPiImage's sd-image module writes), no image builder.
mkPiInstalled = machineModel:
let rt = mkPiRuntime machineModel; in
nixpkgs.lib.nixosSystem { nixpkgs.lib.nixosSystem {
system = "aarch64-linux"; system = "aarch64-linux";
specialArgs = { specialArgs = {
pkgs-unstable = pkgsUnstableAarch64; pkgs-unstable = pkgsUnstableAarch64;
inherit atm-app; inherit (rt) atm-app;
}; };
modules = [ modules = (piBaseModules { inherit machineModel; inherit (rt) atm-app fiatCode; }) ++ [
nixos-hardware.nixosModules.raspberry-pi-5 {
fileSystems."/" = {
device = "/dev/disk/by-label/NIXOS_SD";
fsType = "ext4";
};
fileSystems."/boot/firmware" = {
device = "/dev/disk/by-label/FIRMWARE";
fsType = "vfat";
options = [ "nofail" "noauto" ];
};
}
];
};
# Flashable SD/USB image — same runtime + the aarch64 sd-image builder,
# which brings its own NIXOS_SD/FIRMWARE fileSystems and the u-boot
# firmware. Its system.build.sdImage is exposed as
# packages.aarch64-linux.sd-image-rpi5.
mkPiImage = machineModel:
let rt = mkPiRuntime machineModel; in
nixpkgs.lib.nixosSystem {
system = "aarch64-linux";
specialArgs = {
pkgs-unstable = pkgsUnstableAarch64;
inherit (rt) atm-app;
};
modules = (piBaseModules { inherit machineModel; inherit (rt) atm-app fiatCode; }) ++ [
(nixpkgs + "/nixos/modules/installer/sd-card/sd-image-aarch64.nix") (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 in
@ -401,8 +463,13 @@
batm3-installed = mkInstalledConfig "batm3" ./deploy/nixos/hardware/batm3.nix; batm3-installed = mkInstalledConfig "batm3" ./deploy/nixos/hardware/batm3.nix;
# Raspberry Pi 5 (aarch64) DIY build — Apex 7600 / NV10 over USB-serial. # 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 → in-place rebuild target:
rpi5-installed = mkPiConfig "rpi5"; # sudo nixos-rebuild switch --flake \
# "git+ssh://forgejo@git.atitlan.io/aiolabs/bitspire.git?ref=<branch>#rpi5-installed"
# rpi5-image → source of the flashable image
# (packages.aarch64-linux.sd-image-rpi5).
rpi5-installed = mkPiInstalled "rpi5";
rpi5-image = mkPiImage "rpi5";
# USB-bootable variant of batm3-installed. This is the config the # USB-bootable variant of batm3-installed. This is the config the
# flashed USB stick actually runs — distinct fs labels so stage-1 can't # flashed USB stick actually runs — distinct fs labels so stage-1 can't
@ -623,7 +690,7 @@
# / arm box / `boot.binfmt` emulation on this x86 host): # / arm box / `boot.binfmt` emulation on this x86 host):
# nix build .#packages.aarch64-linux.sd-image-rpi5 # nix build .#packages.aarch64-linux.sd-image-rpi5
packages.aarch64-linux = { packages.aarch64-linux = {
sd-image-rpi5 = self.nixosConfigurations.rpi5-installed.config.system.build.sdImage; sd-image-rpi5 = self.nixosConfigurations.rpi5-image.config.system.build.sdImage;
atm-app-rpi5 = mkAtmAppAarch64 { model = "rpi5"; fiatCode = "USD"; }; atm-app-rpi5 = mkAtmAppAarch64 { model = "rpi5"; fiatCode = "USD"; };
}; };
} }