From 19d43c2939af28b8786114e43ce952bd8495cc7f Mon Sep 17 00:00:00 2001 From: Patrick Mulligan Date: Wed, 18 Feb 2026 20:11:23 -0500 Subject: [PATCH] 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 --- apps/machine/electron/main.ts | 4 +- apps/machine/src/main.ts | 4 +- apps/machine/vite.config.ts | 2 + deploy/nixos/README.md | 192 ++++++++++++++++ deploy/nixos/build-iso.sh | 64 ++++++ deploy/nixos/configuration.nix | 150 ++++++++++++ deploy/nixos/flake.lock | 44 ++++ deploy/nixos/flake.nix | 62 +++++ deploy/nixos/hardware/upboard.nix | 107 +++++++++ deploy/nixos/lamassu-atm.nix | 240 ++++++++++++++++++++ deploy/nixos/live.nix | 181 +++++++++++++++ deploy/nixos/provision-atm.sh | 99 ++++++++ deploy/nixos/udev/99-lamassu-hardware.rules | 57 +++++ 13 files changed, 1203 insertions(+), 3 deletions(-) create mode 100644 deploy/nixos/README.md create mode 100755 deploy/nixos/build-iso.sh create mode 100644 deploy/nixos/configuration.nix create mode 100644 deploy/nixos/flake.lock create mode 100644 deploy/nixos/flake.nix create mode 100644 deploy/nixos/hardware/upboard.nix create mode 100644 deploy/nixos/lamassu-atm.nix create mode 100644 deploy/nixos/live.nix create mode 100755 deploy/nixos/provision-atm.sh create mode 100644 deploy/nixos/udev/99-lamassu-hardware.rules diff --git a/apps/machine/electron/main.ts b/apps/machine/electron/main.ts index 6cc7e6f..04a9790 100644 --- a/apps/machine/electron/main.ts +++ b/apps/machine/electron/main.ts @@ -40,7 +40,9 @@ function loadEnvFile() { loadEnvFile() // Determine if we're in development -const isDev = process.env.NODE_ENV === 'development' || !app.isPackaged +const isDev = + process.env.ELECTRON_FORCE_PROD !== '1' && + (process.env.NODE_ENV === 'development' || !app.isPackaged) // Window dimensions (vertical ATM display) const WINDOW_WIDTH = 1080 diff --git a/apps/machine/src/main.ts b/apps/machine/src/main.ts index c643306..83bb576 100644 --- a/apps/machine/src/main.ts +++ b/apps/machine/src/main.ts @@ -1,6 +1,6 @@ import { createApp } from 'vue' import { createPinia } from 'pinia' -import { createRouter, createWebHistory } from 'vue-router' +import { createRouter, createWebHashHistory } from 'vue-router' import App from './App.vue' import './style.css' @@ -11,7 +11,7 @@ import CashInView from './views/CashInView.vue' // Create router const router = createRouter({ - history: createWebHistory(), + history: createWebHashHistory(), routes: [ { path: '/', name: 'idle', component: IdleView }, { path: '/cash-out', name: 'cash-out', component: CashOutView }, diff --git a/apps/machine/vite.config.ts b/apps/machine/vite.config.ts index 12c32ac..dbd0ef1 100644 --- a/apps/machine/vite.config.ts +++ b/apps/machine/vite.config.ts @@ -5,6 +5,8 @@ import tailwindcss from '@tailwindcss/vite' // https://vitejs.dev/config/ export default defineConfig({ + // Use relative paths so assets load correctly with file:// protocol (Electron production) + base: './', plugins: [vue(), tailwindcss()], resolve: { alias: { diff --git a/deploy/nixos/README.md b/deploy/nixos/README.md new file mode 100644 index 0000000..8e3fe83 --- /dev/null +++ b/deploy/nixos/README.md @@ -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@:/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@:/opt/lamassu-atm/ + +# Restart service +ssh lamassu@ "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 diff --git a/deploy/nixos/build-iso.sh b/deploy/nixos/build-iso.sh new file mode 100755 index 0000000..b15b226 --- /dev/null +++ b/deploy/nixos/build-iso.sh @@ -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@" +echo "sudo nano /var/lib/lamassu-atm/.env" +echo "sudo systemctl restart lamassu-atm" diff --git a/deploy/nixos/configuration.nix b/deploy/nixos/configuration.nix new file mode 100644 index 0000000..3c6b1d8 --- /dev/null +++ b/deploy/nixos/configuration.nix @@ -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"; + }; + }; +} diff --git a/deploy/nixos/flake.lock b/deploy/nixos/flake.lock new file mode 100644 index 0000000..2beea6f --- /dev/null +++ b/deploy/nixos/flake.lock @@ -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 +} diff --git a/deploy/nixos/flake.nix b/deploy/nixos/flake.nix new file mode 100644 index 0000000..367b2dd --- /dev/null +++ b/deploy/nixos/flake.nix @@ -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; + }; + }; +} diff --git a/deploy/nixos/hardware/upboard.nix b/deploy/nixos/hardware/upboard.nix new file mode 100644 index 0000000..2b990f7 --- /dev/null +++ b/deploy/nixos/hardware/upboard.nix @@ -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" + ''; +} diff --git a/deploy/nixos/lamassu-atm.nix b/deploy/nixos/lamassu-atm.nix new file mode 100644 index 0000000..b407d28 --- /dev/null +++ b/deploy/nixos/lamassu-atm.nix @@ -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 + ]; + }; +} diff --git a/deploy/nixos/live.nix b/deploy/nixos/live.nix new file mode 100644 index 0000000..fcf483f --- /dev/null +++ b/deploy/nixos/live.nix @@ -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" + ''; +} diff --git a/deploy/nixos/provision-atm.sh b/deploy/nixos/provision-atm.sh new file mode 100755 index 0000000..6f23fca --- /dev/null +++ b/deploy/nixos/provision-atm.sh @@ -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'" diff --git a/deploy/nixos/udev/99-lamassu-hardware.rules b/deploy/nixos/udev/99-lamassu-hardware.rules new file mode 100644 index 0000000..214e788 --- /dev/null +++ b/deploy/nixos/udev/99-lamassu-hardware.rules @@ -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