feat(deploy): add NixOS live USB ISO for ATM hardware testing

Add NixOS configuration to build a bootable live USB ISO that runs the
ATM Electron app in kiosk mode on physical hardware (UpBoard). The ISO
boots from squashfs, auto-starts X11/openbox, and launches Electron in
production mode.

Key changes:
- deploy/nixos/live.nix: Live USB module (squashfs+tmpfs, no disk install)
- deploy/nixos/flake.nix: Nix flake with ISO build output
- deploy/nixos/provision-atm.sh: Auto-provision LP credentials via API
- deploy/nixos/build-iso.sh: End-to-end build workflow script
- apps/machine: Fix Electron production mode (ELECTRON_FORCE_PROD),
  Vue Router hash mode for file:// protocol, relative asset paths

Build: cd deploy/nixos && bash build-iso.sh
Test:  qemu-system-x86_64 -enable-kvm -m 2G -cdrom result/iso/*.iso

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Patrick Mulligan 2026-02-18 20:11:23 -05:00
commit 19d43c2939
13 changed files with 1203 additions and 3 deletions

192
deploy/nixos/README.md Normal file
View file

@ -0,0 +1,192 @@
# Lamassu ATM NixOS Deployment
NixOS configuration for deploying Lamassu Next ATM software on UP Board hardware.
## Quick Start
### 1. Build Installation ISO
```bash
cd deploy/nixos
nix build .#iso
```
The ISO will be in `result/iso/`.
### 2. Install on UP Board
1. Write ISO to USB drive:
```bash
sudo dd if=result/iso/*.iso of=/dev/sdX bs=4M status=progress
```
2. Boot UP Board from USB
3. Run the installer:
```bash
sudo nixos-install --flake .#lamassu-atm
```
4. Reboot and remove USB
### 3. Post-Install Configuration
SSH into the machine and configure:
```bash
# Set up the ATM application
sudo mkdir -p /opt/lamassu-atm
sudo chown lamassu:lamassu /opt/lamassu-atm
# Copy the built Electron app
scp -r apps/machine/dist/* lamassu@<atm-ip>:/opt/lamassu-atm/
# Configure the ATM
sudo nano /etc/lamassu-atm/config.env
```
## Configuration Options
Edit `/etc/nixos/configuration.nix` to customize:
```nix
{
services.lamassu-atm = {
enable = true;
# Nostr relay for ATM communication
relayUrl = "wss://relay.lamassu.is";
# Lightning.Pub instance
lightningPubUrl = "https://lp.lamassu.is";
# Hardware configuration
billValidator = {
enable = true;
device = "/dev/ttyUSB0";
type = "id003"; # or "mei", "ccnet"
};
billDispenser = {
enable = false; # Enable for two-way machines
device = "/dev/ttyUSB1";
type = "puloon";
};
camera = {
enable = true;
device = "/dev/video0";
};
};
}
```
## Hardware Support
### Bill Validators
- **ID-003** (JCM) - Most common in Lamassu machines
- **MEI** (Mars Electronics)
- **CCNET** (CashCode)
### Bill Dispensers
- **Puloon** - LCDM series
- **Genmega**
### Cameras
- Any V4L2-compatible USB camera
## File Structure
```
deploy/nixos/
├── flake.nix # Nix flake entry point
├── configuration.nix # Base system configuration
├── lamassu-atm.nix # ATM service module
├── hardware/
│ └── upboard.nix # UP Board hardware config
├── udev/
│ └── 99-lamassu-hardware.rules # Hardware device rules
└── README.md # This file
```
## Troubleshooting
### Check ATM service status
```bash
sudo systemctl status lamassu-atm
sudo journalctl -u lamassu-atm -f
```
### Check hardware detection
```bash
# List serial devices
ls -la /dev/ttyUSB* /dev/ttyACM*
# Check for bill validator symlink
ls -la /dev/bill-validator
# Test camera
v4l2-ctl --list-devices
```
### Manual service control
```bash
sudo systemctl restart lamassu-atm
sudo systemctl stop lamassu-atm
```
### Debug mode
```bash
# Run manually with verbose output
sudo -u lamassu DISPLAY=:0 LOG_LEVEL=debug electron /opt/lamassu-atm
```
## Updating
### Update system
```bash
sudo nixos-rebuild switch --flake /etc/nixos#lamassu-atm
```
### Update ATM application
```bash
# Build new version
cd lamassu-next/apps/machine
pnpm run build
# Copy to ATM
scp -r dist/* lamassu@<atm-ip>:/opt/lamassu-atm/
# Restart service
ssh lamassu@<atm-ip> "sudo systemctl restart lamassu-atm"
```
## Development vs Production
For development/testing, you can use the regtest docker environment:
```bash
cd lamassu-next
./docker/dev.sh up --fund
./docker/dev.sh atm
```
For production, deploy this NixOS configuration and point to your production Nostr relay and Lightning.Pub instance.
## Security Notes
- The default `lamassu` user has `wheel` access for initial setup
- Remove wheel access after configuration: `sudo gpasswd -d lamassu wheel`
- SSH is enabled by default - configure key-based auth and disable password auth
- Firewall blocks all incoming connections by default

64
deploy/nixos/build-iso.sh Executable file
View file

@ -0,0 +1,64 @@
#!/usr/bin/env bash
# Build a bootable NixOS Live USB ISO for testing the ATM Electron app
# on physical hardware (UpBoard).
#
# Usage: bash build-iso.sh
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
MACHINE_DIR="$REPO_ROOT/apps/machine"
echo "=== Building Lamassu ATM Live USB ISO ==="
# Step 1: Build the Electron app
echo ""
echo "--- Step 1: Building Electron app ---"
cd "$REPO_ROOT"
pnpm run build --filter=@lamassu/machine
# Step 2: Verify build outputs exist
echo ""
echo "--- Step 2: Verifying build outputs ---"
if [ ! -d "$MACHINE_DIR/dist" ]; then
echo "ERROR: $MACHINE_DIR/dist not found. Build failed?"
exit 1
fi
if [ ! -d "$MACHINE_DIR/dist-electron" ]; then
echo "ERROR: $MACHINE_DIR/dist-electron not found. Build failed?"
exit 1
fi
echo "OK: dist/ and dist-electron/ present"
# Step 3: Build the NixOS ISO
echo ""
echo "--- Step 3: Building NixOS ISO (this takes a while) ---"
cd "$SCRIPT_DIR"
export MACHINE_DIR="$MACHINE_DIR"
nix build .#iso --impure --show-trace
# Step 4: Print results
ISO_PATH=$(ls result/iso/*.iso 2>/dev/null | head -1)
if [ -z "$ISO_PATH" ]; then
echo "ERROR: ISO not found in result/iso/"
exit 1
fi
ISO_SIZE=$(du -h "$ISO_PATH" | cut -f1)
echo ""
echo "=== ISO built successfully ==="
echo "File: $ISO_PATH"
echo "Size: $ISO_SIZE"
echo ""
echo "--- Test in QEMU ---"
echo "qemu-system-x86_64 -enable-kvm -m 2G \\"
echo " -bios /usr/share/edk2-ovmf/OVMF_CODE.fd \\"
echo " -cdrom $ISO_PATH -display gtk"
echo ""
echo "--- Write to USB flash drive ---"
echo "sudo dd if=$ISO_PATH of=/dev/sdX bs=4M status=progress oflag=sync"
echo ""
echo "--- After booting, SSH in and configure ---"
echo "ssh lamassu@<atm-ip>"
echo "sudo nano /var/lib/lamassu-atm/.env"
echo "sudo systemctl restart lamassu-atm"

View file

@ -0,0 +1,150 @@
# Lamassu ATM NixOS Configuration
# Base system configuration for ATM kiosk
{ config, lib, pkgs, pkgs-unstable, ... }:
{
# System basics
system.stateVersion = "24.05";
# Networking
networking = {
hostName = "lamassu-atm";
# Use NetworkManager for easy WiFi configuration
networkmanager.enable = true;
# Firewall - minimal exposure
firewall = {
enable = true;
allowedTCPPorts = [ ]; # ATM initiates all connections
allowedUDPPorts = [ ];
};
};
# Timezone - set to your location
time.timeZone = "UTC";
# Locale
i18n.defaultLocale = "en_US.UTF-8";
# Users
users.groups.lamassu = { };
users.users.lamassu = {
isNormalUser = true;
group = "lamassu";
description = "Lamassu ATM";
extraGroups = [
"wheel" # For admin access
"video" # GPU access
"audio" # Sound
"dialout" # Serial ports
"plugdev" # USB devices
"networkmanager" # Network config
];
# No password - kiosk mode
initialPassword = "lamassu";
};
# Kiosk display configuration
services.xserver = {
enable = true;
# Display manager - auto-login
displayManager = {
autoLogin = {
enable = true;
user = "lamassu";
};
};
# No desktop environment - just the ATM app
desktopManager.xterm.enable = false;
# Basic window manager for Electron
windowManager.openbox.enable = true;
# Disable screen blanking
serverFlagsSection = ''
Option "BlankTime" "0"
Option "StandbyTime" "0"
Option "SuspendTime" "0"
Option "OffTime" "0"
'';
# Intel driver
videoDrivers = [ "modesetting" ];
};
# Audio (for transaction sounds)
security.rtkit.enable = true;
services.pipewire = {
enable = true;
alsa.enable = true;
pulse.enable = true;
};
# System packages
environment.systemPackages = with pkgs; [
# System utilities
htop
vim
git
curl
wget
# Hardware debugging
usbutils
pciutils
lsof
# Serial port tools
minicom
screen
# For the Electron app
pkgs-unstable.electron
# Node.js for the application
pkgs-unstable.nodejs_22
# Camera support
v4l-utils
fswebcam
];
# Enable SSH for remote administration
services.openssh = {
enable = true;
settings = {
PasswordAuthentication = false;
PermitRootLogin = "no";
};
};
# Auto-updates (optional - disabled by default for stability)
# system.autoUpgrade.enable = false;
# Journal configuration
services.journald = {
extraConfig = ''
SystemMaxUse=100M
MaxRetentionSec=1week
'';
};
# Nix settings
nix = {
settings = {
experimental-features = [ "nix-command" "flakes" ];
auto-optimise-store = true;
};
# Garbage collection
gc = {
automatic = true;
dates = "weekly";
options = "--delete-older-than 7d";
};
};
}

44
deploy/nixos/flake.lock generated Normal file
View file

@ -0,0 +1,44 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1735563628,
"narHash": "sha256-OnSAY7XDSx7CtDoqNh8jwVwh4xNL/2HaJxGjryLWzX8=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "b134951a4c9f3c995fd7be05f3243f8ecd65d798",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-24.05",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-unstable": {
"locked": {
"lastModified": 1771177547,
"narHash": "sha256-trTtk3WTOHz7hSw89xIIvahkgoFJYQ0G43IlqprFoMA=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "ac055f38c798b0d87695240c7b761b82fc7e5bc2",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs",
"nixpkgs-unstable": "nixpkgs-unstable"
}
}
},
"root": "root",
"version": 7
}

62
deploy/nixos/flake.nix Normal file
View file

@ -0,0 +1,62 @@
{
description = "Lamassu Next ATM - NixOS Deployment";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05";
# For Electron/Node.js packaging
nixpkgs-unstable.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
};
outputs = { self, nixpkgs, nixpkgs-unstable }:
let
system = "x86_64-linux";
pkgs = import nixpkgs {
inherit system;
config.allowUnfree = true;
};
pkgs-unstable = import nixpkgs-unstable {
inherit system;
config.allowUnfree = true;
};
in
{
# NixOS configuration for UP Board ATM (installed to disk)
nixosConfigurations.lamassu-atm = nixpkgs.lib.nixosSystem {
inherit system;
specialArgs = { inherit pkgs-unstable; };
modules = [
./hardware/upboard.nix
./configuration.nix
./lamassu-atm.nix
];
};
# Live USB configuration (boots from USB, no disk install)
nixosConfigurations.lamassu-live = nixpkgs.lib.nixosSystem {
inherit system;
specialArgs = { inherit pkgs-unstable nixpkgs; };
modules = [
./live.nix
];
};
# Standalone module for importing into existing NixOS configs
nixosModules.default = import ./lamassu-atm.nix;
nixosModules.lamassu-atm = import ./lamassu-atm.nix;
packages.${system} = {
# ISO image for live USB testing
iso = self.nixosConfigurations.lamassu-live.config.system.build.isoImage;
# SD card image (if needed)
sdcard = self.nixosConfigurations.lamassu-atm.config.system.build.sdImage;
};
};
}

View file

@ -0,0 +1,107 @@
# UP Board Hardware Configuration
# Supports UP Board, UP Squared, and similar Intel Atom/Celeron boards
# commonly used in Lamassu ATM machines
{ config, lib, pkgs, ... }:
{
# Boot configuration for UP Board
# UP Board uses 64-bit CPU but some models have 32-bit UEFI
boot = {
loader = {
# Use systemd-boot for UEFI systems
systemd-boot.enable = true;
efi.canTouchEfiVariables = true;
# Timeout for boot menu (useful for remote debugging)
timeout = 3;
};
# Kernel modules needed for UP Board hardware
initrd.availableKernelModules = [
"xhci_pci" # USB 3.0
"ahci" # SATA
"usb_storage" # USB mass storage
"sd_mod" # SCSI disk
"sdhci_pci" # SD card (eMMC)
"i915" # Intel graphics
];
kernelModules = [
"kvm-intel" # Virtualization (if needed)
"i2c-dev" # I2C for hardware control
"spi-dev" # SPI for hardware control
];
# UP Board specific kernel parameters
kernelParams = [
# Intel graphics
"i915.enable_psr=0"
# Serial console for debugging (UP Board has debug UART)
"console=ttyS4,115200n8"
"console=tty0"
# Quiet boot for kiosk mode
"quiet"
"splash"
];
};
# Filesystem configuration
# Adjust these to match your actual disk layout
fileSystems."/" = {
device = "/dev/disk/by-label/nixos";
fsType = "ext4";
};
fileSystems."/boot" = {
device = "/dev/disk/by-label/boot";
fsType = "vfat";
};
# Hardware-specific packages
hardware = {
# Intel GPU support
graphics = {
enable = true;
extraPackages = with pkgs; [
intel-media-driver # VAAPI driver for newer Intel
vaapiIntel # VAAPI driver (legacy)
vaapiVdpau
libvdpau-va-gl
];
};
# Enable firmware for Intel hardware
enableRedistributableFirmware = true;
# CPU microcode updates
cpu.intel.updateMicrocode = true;
};
# Power management
powerManagement = {
enable = true;
cpuFreqGovernor = "performance"; # ATM should be responsive
};
# Disable suspend/hibernate for kiosk
systemd.targets = {
sleep.enable = false;
suspend.enable = false;
hibernate.enable = false;
hybrid-sleep.enable = false;
};
# Serial port access for bill validator/dispenser
# UP Board GPIO/UART pins
services.udev.extraRules = ''
# UP Board serial ports
KERNEL=="ttyS[0-9]*", MODE="0666"
# USB serial adapters (common for bill validators)
KERNEL=="ttyUSB[0-9]*", MODE="0666"
KERNEL=="ttyACM[0-9]*", MODE="0666"
'';
}

View file

@ -0,0 +1,240 @@
# Lamassu ATM Service Module
# Manages the ATM Electron application and related services
{ config, lib, pkgs, pkgs-unstable, ... }:
with lib;
let
cfg = config.services.lamassu-atm;
in
{
options.services.lamassu-atm = {
enable = mkEnableOption "Lamassu ATM service";
relayUrl = mkOption {
type = types.str;
default = "wss://relay.lamassu.is";
description = "Nostr relay URL for ATM communication";
};
lightningPubUrl = mkOption {
type = types.str;
default = "https://lp.lamassu.is";
description = "Lightning.Pub instance URL";
};
appDir = mkOption {
type = types.path;
default = "/opt/lamassu-atm";
description = "Directory containing the ATM application";
};
dataDir = mkOption {
type = types.path;
default = "/var/lib/lamassu-atm";
description = "Directory for ATM data and configuration";
};
logLevel = mkOption {
type = types.enum [ "error" "warn" "info" "debug" ];
default = "info";
description = "Logging level for the ATM application";
};
# Hardware configuration
billValidator = {
enable = mkOption {
type = types.bool;
default = true;
description = "Enable bill validator support";
};
device = mkOption {
type = types.str;
default = "/dev/ttyUSB0";
description = "Serial device for bill validator";
};
type = mkOption {
type = types.enum [ "id003" "mei" "ccnet" ];
default = "id003";
description = "Bill validator protocol type";
};
};
billDispenser = {
enable = mkOption {
type = types.bool;
default = false;
description = "Enable bill dispenser support (two-way machines)";
};
device = mkOption {
type = types.str;
default = "/dev/ttyUSB1";
description = "Serial device for bill dispenser";
};
type = mkOption {
type = types.enum [ "puloon" "genmega" ];
default = "puloon";
description = "Bill dispenser type";
};
};
camera = {
enable = mkOption {
type = types.bool;
default = true;
description = "Enable camera for QR code scanning";
};
device = mkOption {
type = types.str;
default = "/dev/video0";
description = "Camera device";
};
};
};
config = mkIf cfg.enable {
# Create data directory
systemd.tmpfiles.rules = [
"d ${cfg.dataDir} 0750 lamassu lamassu -"
"d ${cfg.dataDir}/logs 0750 lamassu lamassu -"
];
# Environment file for ATM configuration
environment.etc."lamassu-atm/config.env".text = ''
# Lamassu ATM Configuration
RELAY_URL=${cfg.relayUrl}
LIGHTNING_PUB_URL=${cfg.lightningPubUrl}
LOG_LEVEL=${cfg.logLevel}
DATA_DIR=${cfg.dataDir}
# Hardware
BILL_VALIDATOR_ENABLED=${boolToString cfg.billValidator.enable}
BILL_VALIDATOR_DEVICE=${cfg.billValidator.device}
BILL_VALIDATOR_TYPE=${cfg.billValidator.type}
BILL_DISPENSER_ENABLED=${boolToString cfg.billDispenser.enable}
BILL_DISPENSER_DEVICE=${cfg.billDispenser.device}
BILL_DISPENSER_TYPE=${cfg.billDispenser.type}
CAMERA_ENABLED=${boolToString cfg.camera.enable}
CAMERA_DEVICE=${cfg.camera.device}
# Display
DISPLAY=:0
ELECTRON_DISABLE_GPU=false
'';
# Main ATM service
systemd.services.lamassu-atm = {
description = "Lamassu ATM Application";
wantedBy = [ "graphical.target" ];
after = [ "graphical.target" "network-online.target" ];
wants = [ "network-online.target" ];
serviceConfig = {
Type = "simple";
User = "lamassu";
Group = "lamassu";
WorkingDirectory = cfg.appDir;
# Environment
EnvironmentFile = "/etc/lamassu-atm/config.env";
# Start the Electron app
ExecStart = "${pkgs-unstable.electron}/bin/electron ${cfg.appDir}";
# Restart policy
Restart = "always";
RestartSec = 5;
# Resource limits
MemoryMax = "1G";
CPUQuota = "80%";
# Security hardening
NoNewPrivileges = true;
ProtectSystem = "strict";
ProtectHome = true;
ReadWritePaths = [ cfg.dataDir "/tmp" ];
PrivateTmp = true;
# Allow device access for hardware
DeviceAllow = [
"/dev/ttyUSB* rw"
"/dev/ttyACM* rw"
"/dev/ttyS* rw"
"/dev/video* rw"
];
};
# Pre-start script to verify hardware
preStart = ''
echo "Lamassu ATM starting..."
echo "Relay: ${cfg.relayUrl}"
echo "Lightning.Pub: ${cfg.lightningPubUrl}"
# Check bill validator if enabled
if [ "${boolToString cfg.billValidator.enable}" = "true" ]; then
if [ ! -c "${cfg.billValidator.device}" ]; then
echo "Warning: Bill validator device ${cfg.billValidator.device} not found"
fi
fi
# Check camera if enabled
if [ "${boolToString cfg.camera.enable}" = "true" ]; then
if [ ! -c "${cfg.camera.device}" ]; then
echo "Warning: Camera device ${cfg.camera.device} not found"
fi
fi
'';
};
# Openbox autostart for kiosk mode
environment.etc."xdg/openbox/autostart".text = ''
# Disable screen saver and power management
xset s off
xset -dpms
xset s noblank
# Hide cursor after inactivity
unclutter -idle 3 &
# Start ATM (handled by systemd, but ensure display is ready)
sleep 2
'';
# udev rules for ATM hardware
services.udev.extraRules = ''
# ID-003 Bill Validator (JCM)
SUBSYSTEM=="tty", ATTRS{idVendor}=="0451", ATTRS{idProduct}=="3410", MODE="0666", SYMLINK+="bill-validator"
# MEI Bill Validator
SUBSYSTEM=="tty", ATTRS{idVendor}=="0b00", MODE="0666", SYMLINK+="bill-validator"
# CCNET Bill Validator (CashCode)
SUBSYSTEM=="tty", ATTRS{idVendor}=="0x1b5a", MODE="0666", SYMLINK+="bill-validator"
# Puloon Bill Dispenser
SUBSYSTEM=="tty", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6001", MODE="0666", SYMLINK+="bill-dispenser"
# Generic USB-Serial adapters
SUBSYSTEM=="tty", ATTRS{idVendor}=="067b", MODE="0666"
SUBSYSTEM=="tty", ATTRS{idVendor}=="0403", MODE="0666"
SUBSYSTEM=="tty", ATTRS{idVendor}=="10c4", MODE="0666"
# Camera access
SUBSYSTEM=="video4linux", MODE="0666"
'';
# Additional packages for hardware support
environment.systemPackages = with pkgs; [
unclutter # Hide cursor
];
};
}

181
deploy/nixos/live.nix Normal file
View file

@ -0,0 +1,181 @@
# Lamassu ATM Live USB Configuration
# Bootable ISO for testing on physical hardware (UpBoard) without installing to disk.
# Builds with: nix build .#iso
#
# 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, ... }:
let
# Pre-built Electron app copied into the Nix store.
# Build first: pnpm run build --filter=@lamassu/machine
# Requires --impure: reads MACHINE_DIR env var to find build artifacts (dist/ is gitignored)
machineDir = builtins.getEnv "MACHINE_DIR";
atm-app = assert machineDir != ""
|| throw "MACHINE_DIR env var must be set (use build-iso.sh or: export MACHINE_DIR=/path/to/apps/machine)";
pkgs.runCommand "lamassu-atm-app" { } ''
mkdir -p $out
cp -r ${builtins.path { path = machineDir + "/dist"; name = "dist"; }} $out/dist
cp -r ${builtins.path { path = machineDir + "/dist-electron"; name = "dist-electron"; }} $out/dist-electron
cp ${builtins.path { path = machineDir + "/package.json"; name = "package.json"; }} $out/package.json
'';
# .env template with regtest defaults pointing to the dev machine
envTemplate = pkgs.writeText "lamassu-atm-env" ''
# Lamassu ATM Live USB Configuration
# Edit this file and restart: sudo systemctl restart lamassu-atm
#
# Dev machine LAN address (adjust to your network)
VITE_RELAY_URL=ws://192.168.1.190:7777
VITE_LIGHTNING_PUB_API_URL=http://192.168.1.190:1776
VITE_EXTENSION_API_URL=http://192.168.1.190:1777
# Lightning.Pub credentials (fill in after boot)
VITE_LIGHTNING_PUB_PUBKEY=
VITE_ATM_PRIVATE_KEY=
VITE_ADMIN_TOKEN=
VITE_APP_ID=
VITE_APP_TOKEN=
VITE_LINKING_TOKEN=
# Machine configuration
VITE_LAMASSU_MACHINE_MODEL=sintra
VITE_LAMASSU_FIAT_CODE=USD
# Hardware (uncomment and set for real hardware)
# VITE_LAMASSU_VALIDATOR_DEVICE=/dev/ttyUSB0
# VITE_LAMASSU_DISPENSER_DEVICE=/dev/ttyUSB1
# VITE_LAMASSU_CASSETTES=
# Force production mode when running via `electron /path`
ELECTRON_FORCE_PROD=1
# Display
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
./lamassu-atm.nix
];
# ISO image settings
isoImage = {
isoName = "lamassu-atm-live.iso";
makeEfiBootable = true;
makeBiosBootable = 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.
# Boot: UpBoard-relevant kernel modules (from hardware/upboard.nix) without disk mounts
boot = {
initrd.availableKernelModules = [
"xhci_pci"
"ahci"
"usb_storage"
"sd_mod"
"sdhci_pci"
"i915"
"squashfs"
"iso9660"
"loop"
];
kernelModules = [
"kvm-intel"
"i2c-dev"
"spi-dev"
];
kernelParams = [
"i915.enable_psr=0"
"quiet"
"splash"
];
};
# Intel GPU support (from hardware/upboard.nix)
# NB: nixos-24.05 uses hardware.opengl, not hardware.graphics
hardware = {
opengl = {
enable = true;
extraPackages = with pkgs; [
intel-media-driver
vaapiIntel
vaapiVdpau
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.lamassu-atm = {
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.lamassu-atm = {
serviceConfig = {
EnvironmentFile = lib.mkForce "/var/lib/lamassu-atm/.env";
# Electron needs --no-sandbox in the live/testing environment
ExecStart = lib.mkForce "${pkgs-unstable.electron}/bin/electron --no-sandbox --disable-gpu-sandbox ${atm-app}";
# Disable all security hardening that conflicts with Electron
NoNewPrivileges = lib.mkForce false;
ProtectSystem = lib.mkForce false;
ProtectHome = lib.mkForce false;
PrivateTmp = lib.mkForce false;
};
};
# Install the .env template on first boot
system.activationScripts.lamassu-env = ''
mkdir -p /var/lib/lamassu-atm
if [ ! -f /var/lib/lamassu-atm/.env ]; then
cp ${envTemplate} /var/lib/lamassu-atm/.env
chmod 600 /var/lib/lamassu-atm/.env
chown lamassu:lamassu /var/lib/lamassu-atm/.env
fi
'';
# Allow SSH with password for initial setup on the live system
services.openssh.settings.PasswordAuthentication = lib.mkForce true;
# Serial port udev rules (from hardware/upboard.nix)
services.udev.extraRules = lib.mkAfter ''
KERNEL=="ttyS[0-9]*", MODE="0666"
KERNEL=="ttyUSB[0-9]*", MODE="0666"
KERNEL=="ttyACM[0-9]*", MODE="0666"
'';
}

99
deploy/nixos/provision-atm.sh Executable file
View file

@ -0,0 +1,99 @@
#!/usr/bin/env bash
# Provision a running ATM (live USB or QEMU VM) with Lightning.Pub credentials.
# Extracts credentials from the dev docker stack and writes them to the ATM's .env via SSH.
#
# Usage:
# bash provision-atm.sh # defaults: SSH to localhost:2222 (QEMU)
# bash provision-atm.sh 192.168.1.50 # SSH to a real ATM on the LAN
# bash provision-atm.sh 192.168.1.50 22 # custom SSH port
set -euo pipefail
ATM_HOST="${1:-localhost}"
ATM_SSH_PORT="${2:-2222}"
ATM_USER="lamassu"
LP_API="http://localhost:1776"
ADMIN_TOKEN="lamassu-dev-admin-token"
ATM_PRIVATE_KEY="f391a2c3fc734f443b0f685688a0441b5fb9805853c0023f570c5a3c6412b136"
echo "=== Provisioning ATM at $ATM_HOST:$ATM_SSH_PORT ==="
# Step 1: Extract Lightning.Pub pubkey from docker logs
echo ""
echo "--- Step 1: Getting Lightning.Pub pubkey ---"
PUBKEY=$(docker logs lamassu-lightning-pub 2>&1 | grep -oP 'pubkey:\s*\K[a-f0-9]+' | tail -1)
if [ -z "$PUBKEY" ]; then
echo "ERROR: Could not extract pubkey from lamassu-lightning-pub logs."
echo "Is the docker stack running? Try: docker ps | grep lightning-pub"
exit 1
fi
echo "Pubkey: ${PUBKEY:0:16}..."
# Step 2: Create ATM app via admin API
echo ""
echo "--- Step 2: Creating ATM app ---"
RESPONSE=$(curl -s -X POST "$LP_API/api/admin/app/add" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-d '{"name":"lamassu-atm-live","allow_user_creation":true}' 2>/dev/null)
if echo "$RESPONSE" | grep -q '"status":"OK"'; then
APP_ID=$(echo "$RESPONSE" | grep -oP '"id":"\K[^"]+')
APP_TOKEN=$(echo "$RESPONSE" | grep -oP '"auth_token":"\K[^"]+')
echo "Created app: ${APP_ID:0:16}..."
else
echo "WARN: Could not create app (may already exist). Response:"
echo "$RESPONSE"
echo ""
echo "If the app already exists, check docker/dev-state/ for cached credentials."
exit 1
fi
# Step 3: Determine the host IP as seen from the ATM
# For QEMU user-mode networking, the host is at 10.0.2.2
# For real hardware on LAN, use the dev machine's LAN IP
if [ "$ATM_HOST" = "localhost" ]; then
HOST_IP="10.0.2.2"
echo ""
echo "--- QEMU detected: using $HOST_IP as host gateway ---"
else
HOST_IP=$(hostname -I | awk '{print $1}')
echo ""
echo "--- LAN ATM: using $HOST_IP as dev machine address ---"
fi
# Step 4: Write .env to the ATM via SSH
echo ""
echo "--- Step 3: Writing .env to ATM ---"
ENV_CONTENT="# Lamassu ATM Configuration
# Auto-generated by provision-atm.sh on $(date -Iseconds)
# Lightning.Pub connection
VITE_RELAY_URL=ws://$HOST_IP:7777
VITE_LIGHTNING_PUB_PUBKEY=$PUBKEY
VITE_LIGHTNING_PUB_API_URL=http://$HOST_IP:1776
VITE_EXTENSION_API_URL=http://$HOST_IP:1777
# Credentials
VITE_ADMIN_TOKEN=$ADMIN_TOKEN
VITE_ATM_PRIVATE_KEY=$ATM_PRIVATE_KEY
VITE_APP_ID=$APP_ID
VITE_APP_TOKEN=$APP_TOKEN
# Machine configuration
VITE_LAMASSU_MACHINE_MODEL=sintra
VITE_LAMASSU_FIAT_CODE=USD
# Force production mode
ELECTRON_FORCE_PROD=1
DISPLAY=:0"
ssh -o StrictHostKeyChecking=no -p "$ATM_SSH_PORT" "$ATM_USER@$ATM_HOST" \
"echo '$ENV_CONTENT' | sudo tee /var/lib/lamassu-atm/.env > /dev/null && sudo systemctl restart lamassu-atm"
echo ""
echo "=== ATM provisioned successfully ==="
echo ""
echo "Credentials written to /var/lib/lamassu-atm/.env"
echo "ATM service restarted. It should connect to Lightning.Pub at $HOST_IP."
echo ""
echo "To check status: ssh -p $ATM_SSH_PORT $ATM_USER@$ATM_HOST 'sudo journalctl -u lamassu-atm -f'"

View file

@ -0,0 +1,57 @@
# Lamassu ATM Hardware udev Rules
# Place in /etc/udev/rules.d/ or use services.udev.extraRules in NixOS
# ============================================
# Bill Validators
# ============================================
# ID-003 Bill Validator (JCM/Japan Cash Machine)
# Uses TI USB-Serial chip
SUBSYSTEM=="tty", ATTRS{idVendor}=="0451", ATTRS{idProduct}=="3410", MODE="0666", GROUP="dialout", SYMLINK+="bill-validator"
# MEI Bill Validator (Mars Electronics)
SUBSYSTEM=="tty", ATTRS{idVendor}=="0b00", MODE="0666", GROUP="dialout", SYMLINK+="bill-validator"
# CCNET Bill Validator (CashCode)
SUBSYSTEM=="tty", ATTRS{idVendor}=="1b5a", MODE="0666", GROUP="dialout", SYMLINK+="bill-validator"
# ============================================
# Bill Dispensers
# ============================================
# Puloon Bill Dispenser (uses FTDI chip)
SUBSYSTEM=="tty", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6001", MODE="0666", GROUP="dialout", SYMLINK+="bill-dispenser"
# Genmega Bill Dispenser
SUBSYSTEM=="tty", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6015", MODE="0666", GROUP="dialout", SYMLINK+="bill-dispenser"
# ============================================
# Generic USB-Serial Adapters
# ============================================
# Prolific PL2303
SUBSYSTEM=="tty", ATTRS{idVendor}=="067b", MODE="0666", GROUP="dialout"
# FTDI
SUBSYSTEM=="tty", ATTRS{idVendor}=="0403", MODE="0666", GROUP="dialout"
# Silicon Labs CP210x
SUBSYSTEM=="tty", ATTRS{idVendor}=="10c4", MODE="0666", GROUP="dialout"
# CH340/CH341
SUBSYSTEM=="tty", ATTRS{idVendor}=="1a86", MODE="0666", GROUP="dialout"
# ============================================
# Camera
# ============================================
# Allow access to all video4linux devices (cameras)
SUBSYSTEM=="video4linux", MODE="0666", GROUP="video"
# ============================================
# Printers
# ============================================
# Common thermal receipt printers
SUBSYSTEM=="usb", ATTRS{idVendor}=="04b8", MODE="0666", GROUP="lp" # Epson
SUBSYSTEM=="usb", ATTRS{idVendor}=="0519", MODE="0666", GROUP="lp" # Star Micronics