The bitspire-env activation seeds .env only when ABSENT (never refreshes on redeploy), and env WINS over the pairing seed — so any value written at first boot is frozen for the disk's life and silently masks the seed's source. That's how a dead relay.aiolabs.dev and a provisioned VITE_OPERATOR_PUBKEYS made stale installs "work" while a fresh machine broke. Seed ONLY image-baked, non-maskable values (model, fiat, ELECTRON_FORCE_PROD, DISPLAY, empty VITE_SPIRE_SEED placeholder). Relay + server pubkey come from the seed; operator pubkey + fee config come from LNbits over the transport — so those keys are no longer pre-seeded at all. VITE_RELAY_URL / VITE_LNBITS_SERVER_PUBKEY are emitted only when the operator deliberately pins them via the Nix options (an explicit override). Also drops the inert RELAY_URL/LNBITS_SERVER_PUBKEY lines from /etc/bitspire/config.env (never loaded — EnvironmentFile is forced to .env). Verified: built sintra-installed .env template is 5 lines, 0 maskable vars. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
287 lines
12 KiB
Nix
287 lines
12 KiB
Nix
# Lamassu 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, ... }:
|
|
|
|
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_LAMASSU_MACHINE_MODEL=${machineModel}
|
|
VITE_LAMASSU_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 "${pkgs-unstable.electron}/bin/electron --no-sandbox --disable-gpu-sandbox --disable-gpu --disable-software-rasterizer --enable-logging ${atm-app}";
|
|
# 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"
|
|
'');
|
|
}
|