bitspire/deploy/nixos/live.nix
Padreug bf2b9427aa refactor(config): rename VITE_LAMASSU_* machine-config env vars to VITE_BITSPIRE_*
MACHINE_MODEL, FIAT_CODE, VALIDATOR_DEVICE, DISPENSER_DEVICE and CASSETTES
carried the old brand in their names. Renamed everywhere they are read
(device.ts, electron/main.ts), written (flake.nix, mkAtmApp.nix, live.nix,
provision-atm.sh, factory-reset-atm.sh) and documented (.env.example,
docs/device-configuration.md). No compatibility fallback in code: the
machine reads VITE_BITSPIRE_* and nothing else.

The deployed .env files are the one place the old names persist — sintra's
/var/lib/bitspire/.env holds all three keys today — and the machine reads
MACHINE_MODEL / FIAT_CODE / CASSETTES from that file on every boot. Renaming
the keys in code alone would boot a live machine on preset defaults (wrong
bays, wrong fiat) at the next nightly pull. So configuration.nix gains an
activation script, beside the existing lamassu→bitspire user migration,
that rewrites VITE_LAMASSU_* → VITE_BITSPIRE_* in that file. Idempotent;
runs before bitspire.service starts.
2026-10-09 21:57:38 +02:00

287 lines
12 KiB
Nix

# bitSpire ATM Live USB Configuration
# Bootable ISO for testing on physical hardware without installing to disk.
#
# Parameterized by machineModel (passed via specialArgs from flake.nix):
# "douro" - Bay Trail Atom, kernel 5.15 (i915 regression in newer kernels), eDP panel
# "tejo" - UP4000/UPBoard, default kernel 6.6, standard Intel GPU
#
# Builds with: nix build .#iso-douro or nix build .#iso-tejo
#
# Does NOT import hardware/upboard.nix (its fileSystems conflict with live boot).
# Instead, duplicates only the hardware-relevant kernel modules and GPU config.
{ config, lib, pkgs, pkgs-unstable, nixpkgs, machineModel ? "douro", atm-app, kioskLauncher, ... }:
let
# Fiat code per machine model (for envTemplate display only)
fiatCodeForModel = {
douro = "GTQ";
tejo = "GTQ";
sintra = "EUR";
batm3 = "USD";
}.${machineModel} or "USD";
# Minimal .env template (aiolabs/bitspire#70 remnant hygiene). Seed ONLY
# image-baked, non-maskable values. Relay + server pubkey come from the pairing
# SEED, operator pubkey + fee config come from LNbits over the transport — so we
# deliberately do NOT pre-seed those keys (a present-but-empty VITE_RELAY_URL /
# VITE_LNBITS_SERVER_PUBKEY / VITE_OPERATOR_PUBKEYS would win over the seed and
# mask its source). VITE_SPIRE_SEED is written by the wizard / provision-atm.sh;
# the dev-only VITE_ATM_PRIVATE_KEY fallback is omitted on purpose.
envTemplate = pkgs.writeText "bitspire-env" ''
VITE_BITSPIRE_MACHINE_MODEL=${machineModel}
VITE_BITSPIRE_FIAT_CODE=${fiatCodeForModel}
VITE_SPIRE_SEED=
ELECTRON_FORCE_PROD=1
DISPLAY=:0
'';
in
{
imports = [
# NixOS ISO image builder (nixpkgs path passed via specialArgs from flake.nix)
"${nixpkgs}/nixos/modules/installer/cd-dvd/iso-image.nix"
"${nixpkgs}/nixos/modules/profiles/all-hardware.nix"
# Reuse kiosk config (X11, openbox, users, networking)
./configuration.nix
# Reuse ATM systemd service module
./bitspire-atm.nix
]
# Sintra: share the UP Board serial hardware (validator/dispenser/printer
# modules + udev symlinks + console=tty0) with the installed image so the
# live ISO drives the same hardware. Safe to import here — unlike upboard.nix
# it declares no fileSystems, so there's no live-boot mount conflict.
++ lib.optionals (machineModel == "sintra") [ ./hardware/upboard-serial.nix ];
# ISO image settings
image.fileName = "bitspire-${machineModel}-live.iso";
isoImage = {
makeEfiBootable = true;
makeBiosBootable = true;
# Apply the isohybrid MBR + GPT/ESP so the image boots when dd'd to a USB
# stick — not just from optical media via El Torito. Without this the ISO
# has BIOS+UEFI El Torito boot catalogs but no partition table, and picky
# firmware (e.g. the Sintra's Aaeon UP Board) won't recognise the USB as
# bootable. Requires makeBiosBootable (isohdpfx.bin), set above.
makeUsbBootable = true;
squashfsCompression = "zstd -Xcompression-level 6";
};
# No fileSystems override needed — iso-image.nix handles squashfs + tmpfs root.
# We don't import hardware/upboard.nix, so there are no conflicting disk mounts.
# Kernel: Douro Bay Trail needs 5.15 LTS (i915 eDP regression in 6.x kernels)
boot.kernelPackages = lib.mkIf (machineModel == "douro") pkgs.linuxPackages_5_15;
# Boot: kernel modules and parameters (model-specific)
boot = {
initrd.availableKernelModules = [
"xhci_pci"
"ahci"
"usb_storage"
"sd_mod"
"sdhci_pci"
"i915"
"squashfs"
"iso9660"
"loop"
];
kernelModules = [
"kvm-intel"
"i2c-dev"
"spi-dev"
] ++ lib.optionals (machineModel == "tejo") [
"usbserial" # USB-to-serial adapters (ttyUSB0/1 for validator/printer)
"ftdi_sio" # FTDI USB serial (common in ATM peripherals)
"cp210x" # CP210x USB serial (alternative adapter)
] ++ lib.optionals (machineModel == "batm3") [
"cdc_acm" # USB CDC ACM for MEI BNR Advance validator
"usbserial" # USB-to-serial for F56 dispenser adapter
"ftdi_sio" # FTDI USB serial (common RS232 adapter)
"cp210x" # CP210x USB serial (alternative adapter)
];
kernelParams = [
"i915.enable_psr=0"
"quiet"
"splash"
] ++ lib.optionals (machineModel == "douro") [
# Bay Trail: preserve BIOS display init (matches working kernel 5.4 config)
"vt.handoff=7"
] ++ lib.optionals (machineModel == "tejo") [
# UP Board debug UART for serial console
"console=ttyS4,115200n8"
"console=tty0"
];
};
# Intel GPU support
hardware = {
graphics = {
enable = true;
extraPackages = with pkgs; [
intel-media-driver
libva-vdpau-driver
libvdpau-va-gl
];
};
enableRedistributableFirmware = true;
cpu.intel.updateMicrocode = true;
};
# Performance governor for responsive kiosk
powerManagement = {
enable = true;
cpuFreqGovernor = "performance";
};
# Disable suspend/hibernate
systemd.targets = {
sleep.enable = false;
suspend.enable = false;
hibernate.enable = false;
hybrid-sleep.enable = false;
};
# Enable the ATM service with live USB paths
services.bitspire = {
enable = true;
appDir = "${atm-app}";
};
# Allow unprivileged user namespaces (Electron sandbox needs this)
boot.kernel.sysctl."kernel.unprivileged_userns_clone" = 1;
# Override the systemd service for live USB environment
systemd.services.bitspire = {
serviceConfig = {
EnvironmentFile = lib.mkForce "/var/lib/bitspire/.env";
# Native modules need libstdc++ on NixOS
Environment = "LD_LIBRARY_PATH=${pkgs.stdenv.cc.cc.lib}/lib";
# Electron needs --no-sandbox in the live/testing environment
# --enable-logging makes renderer console.log visible in journalctl
ExecStart = lib.mkForce "${kioskLauncher}";
# Prevent Electron from consuming all RAM on memory-constrained ATMs
MemoryMax = lib.mkForce "1G";
# Disable all security hardening that conflicts with Electron
NoNewPrivileges = lib.mkForce false;
ProtectSystem = lib.mkForce false;
ProtectHome = lib.mkForce false;
PrivateTmp = lib.mkForce false;
# Allow access to all character devices (serial ports for HAL hardware)
DevicePolicy = lib.mkForce "auto";
DeviceAllow = lib.mkForce [ "char-* rw" ];
};
};
# Install the .env on first boot. Attrset form with deps=["users"] so the
# chown runs AFTER the bitspire user is created. Otherwise on a fresh live
# boot (where /var/lib/bitspire/.env doesn't exist yet) the chown runs in the
# default activation order — before `users` — and fails with
# "chown: invalid user: 'bitspire:bitspire'". The installed system skips this
# block because its .env already exists, which is why only live boots tripped.
system.activationScripts.bitspire-env = {
deps = [ "users" ];
text = ''
mkdir -p /var/lib/bitspire
if [ ! -f /var/lib/bitspire/.env ]; then
cp ${envTemplate} /var/lib/bitspire/.env
chmod 600 /var/lib/bitspire/.env
chown bitspire:bitspire /var/lib/bitspire/.env
fi
'';
};
# Reset display output after X starts (required for kexec boots where
# the GPU wasn't reinitialized by BIOS firmware). Only the eDP-panel models
# (Douro/Tejo) have an eDP-1 output; the Sintra drives HDMI-1, so the
# `xrandr --output eDP-1` here just errors out — skip it there.
systemd.services.display-reset = lib.mkIf (machineModel != "sintra") {
description = "Reset eDP display output";
after = [ "display-manager.service" ];
requires = [ "display-manager.service" ];
wantedBy = [ "graphical.target" ];
before = [ "bitspire.service" ];
serviceConfig = {
Type = "oneshot";
User = "bitspire";
Environment = "DISPLAY=:0";
ExecStart = "${pkgs.bash}/bin/bash -c '${pkgs.xorg.xrandr}/bin/xrandr --output eDP-1 --off; sleep 1; ${pkgs.xorg.xrandr}/bin/xrandr --output eDP-1 --auto'";
};
};
# Low-RAM models (Douro/Tejo, 2GB) need a swap cushion or they hard-freeze
# under memory pressure. The live system is RAM-rooted, so a /var/swapfile
# lives in tmpfs — pointless, and its init fails on a fresh boot. Use
# compressed RAM swap (zram) instead; no on-disk file required.
zramSwap.enable = true;
# The wg0 VPN tunnel (declared in configuration.nix) needs a provisioned key
# at /var/lib/wireguard/wg0.key, which a fresh live boot doesn't have — it
# fails and drags network-setup down with it. A live test image doesn't need
# the VPN, so drop the interface entirely.
networking.wireguard.interfaces = lib.mkForce { };
# Clean /tmp on boot to prevent stale Nix build artifacts from filling disk
boot.tmp.cleanOnBoot = true;
# SSH password auth disabled — machines are provisioned with SSH keys.
# Base configuration.nix sets PasswordAuthentication = false.
# Serial port udev rules — generic permissions for all models
services.udev.extraRules = lib.mkAfter (''
KERNEL=="ttyS[0-9]*", MODE="0666"
KERNEL=="ttyUSB[0-9]*", MODE="0666"
KERNEL=="ttyACM[0-9]*", MODE="0666"
'' + lib.optionalString (machineModel == "batm3") ''
# ── BATM3 MEI BNR Advance USB CDC ACM ──────────────────────────────
# Stable symlink for the bill validator (vendor 0x0bed = MEI)
SUBSYSTEM=="tty", ATTRS{idVendor}=="0bed", SYMLINK+="ttyValidator"
'' + lib.optionalString (machineModel == "tejo") ''
# ── Tejo serial port symlinks ──────────────────────────────────────
# Both UP Board and UP4000 rules are included so one ISO works on either.
# Rules match different kernel paths so they don't conflict.
# Printer (ttyJ4): UP Board via USB hub path, UP4000 via ttyUSB0
KERNELS=="1-7.2:1.0", SYMLINK+="ttyJ4"
KERNEL=="ttyUSB0", SYMLINK+="ttyJ4"
# Validator (ttyJ5): UP Board via USB hub path, UP4000 via ttyUSB1
KERNELS=="1-7.3:1.0", SYMLINK+="ttyJ5"
KERNEL=="ttyUSB1", SYMLINK+="ttyJ5"
# Dispenser (ttyJ7): UP Board uses ttyS1, UP4000 uses ttyS5
KERNEL=="ttyS1", SYMLINK+="ttyJ7"
KERNEL=="ttyS5", SYMLINK+="ttyJ7"
# Legacy ttyAMA0 alias
SUBSYSTEM=="tty", KERNEL=="ttyS1", SYMLINK+="ttyAMA0", GROUP="dialout"
# ── Camera devices ─────────────────────────────────────────────────
# QR scanner camera (A4tech USB, vendor 0ac8:0345)
SUBSYSTEM=="video4linux", ATTR{index}=="0", KERNELS=="1-5", ATTRS{idVendor}=="0ac8", ATTRS{idProduct}=="0345", SYMLINK+="video-scan"
SUBSYSTEM=="video4linux", ATTR{index}=="0", KERNELS=="1-2", ATTRS{idVendor}=="0ac8", ATTRS{idProduct}=="0345", SYMLINK+="video-scan"
# Front-facing camera
SUBSYSTEM=="video4linux", ATTR{index}=="0", KERNELS=="1-6", ATTRS{idVendor}=="0ac8", ATTRS{idProduct}=="0345", SYMLINK+="video-front"
SUBSYSTEM=="video4linux", ATTR{index}=="0", KERNELS=="1-3", ATTRS{idVendor}=="0ac8", ATTRS{idProduct}=="0345", SYMLINK+="video-front"
# ── LED SPI / peripherals ──────────────────────────────────────────
KERNEL=="spidev1.0", SYMLINK+="ledspi"
KERNEL=="spidev2.0", SYMLINK+="ledspi"
# SPI and I2C group permissions
SUBSYSTEM=="spidev", GROUP="spi", MODE="0660"
SUBSYSTEM=="i2c-dev", GROUP="i2c", MODE="0660"
# UP Board FPGA LED permissions
SUBSYSTEM=="leds", KERNEL=="upboard:*", ACTION=="add|change", RUN+="${pkgs.findutils}/bin/find /sys$devpath -type f -exec ${pkgs.coreutils}/bin/chmod g+u {} + -exec ${pkgs.coreutils}/bin/chown :leds {} +"
# Disable USB autosuspend (prevents serial adapters from sleeping)
ACTION=="add", SUBSYSTEM=="usb", TEST=="power/control", ATTR{power/control}="on"
'');
}