Merge pull request 'deploy: bootable slim Sintra image + shared UP Board serial hardware' (#72) from deploy/sintra-boot-hardware into dev

Reviewed-on: #72
This commit is contained in:
padreug 2026-07-02 21:53:28 +00:00
commit a73f345606
5 changed files with 110 additions and 48 deletions

View file

@ -7,6 +7,16 @@
# System basics
system.stateVersion = "24.05";
# ── Image slimming (bitspire#70 sizing) ──────────────────────────────
# This is a single-purpose Electron kiosk; strip the desktop/multimedia
# baggage NixOS pulls in by default so the disk image stays lean.
# - speechd: text-to-speech (speech-dispatcher → espeak-ng → mbrola, ~1GB).
# An ATM does not talk.
# - documentation: man/info/NixOS manual — no one reads them on a kiosk.
services.speechd.enable = lib.mkForce false;
documentation.enable = false;
documentation.nixos.enable = false;
# Networking
networking = {
hostName = "bitspire";
@ -121,8 +131,10 @@
# Node.js for the application
pkgs-unstable.nodejs_22
# Camera support
v4l-utils
# Camera support. v4l-utils' default build drags in the whole Qt6 stack
# for its qv4l2 GUI (~0.5GB) — we only ever use the v4l2-ctl CLI, so drop
# the GUI.
(v4l-utils.override { withGUI = false; })
fswebcam
# ATM operations

View file

@ -0,0 +1,61 @@
# UP Board serial peripherals — the validator / dispenser / printer wiring
# shared by the INSTALLED configs (hardware/upboard.nix, used by both
# tejo-installed and sintra-installed) AND the sintra live ISO (live.nix).
# Single source of truth so the two artifacts can't drift — the earlier bug
# was exactly this drift (the sintra live ISO lacked ftdi_sio + the ttyJ7
# symlink, so the F56 dispenser failed while the installed image worked).
#
# Sintra IS a UP Board, so these are the UP Board rules; ttyS1/ttyS5 cover the
# older UP Board / UP4000 (Tejo) dispenser nodes and ttyS4 covers the Sintra
# (Apollo Lake) where the F56 is on the SoC MMIO UART. Only the device that
# actually exists at runtime gets the symlink, so all three coexist safely.
#
# Serial port mapping:
# ttyJ4 = Printer (Nippon NP-2511D-2)
# ttyJ5 = Validator (iVIZION, ID003)
# ttyJ7 = Dispenser (Fujitsu F53/F56)
{ lib, ... }:
{
boot.kernelModules = [
"usbserial" # USB-to-serial adapters
"ftdi_sio" # FTDI USB serial (the iVIZION validator bridge)
"cp210x" # CP210x USB serial (alternative adapter)
];
boot.kernelParams = [
# Do NOT route the kernel console through ttyS4 on Sintra. ttyS4 is the
# SoC's MMIO 16550A (the only real UART besides the legacy ttyS0 at I/O
# 0x3f8) and is wired to the Fujitsu F56 dispenser's RS-232 header. Holding
# it as console prevents userspace opening it at 9600 baud and HAL fails
# with "Input/output error setting custom baud rate of 9600". For serial
# debug, point console at ttyS0 instead.
"console=tty0"
];
services.udev.extraRules = lib.mkAfter ''
# Generic serial port permissions (so the non-root HAL user can open them)
KERNEL=="ttyS[0-9]*", MODE="0666"
KERNEL=="ttyUSB[0-9]*", MODE="0666"
KERNEL=="ttyACM[0-9]*", MODE="0666"
# Printer (ttyJ4)
KERNELS=="1-7.2:1.0", SYMLINK+="ttyJ4"
KERNEL=="ttyUSB0", SYMLINK+="ttyJ4"
# Validator (ttyJ5)
KERNELS=="1-7.3:1.0", SYMLINK+="ttyJ5"
KERNEL=="ttyUSB1", SYMLINK+="ttyJ5"
# Dispenser (ttyJ7). ttyS1/ttyS5 = older UP Board / UP4000; ttyS4 = Sintra.
KERNEL=="ttyS1", SYMLINK+="ttyJ7"
KERNEL=="ttyS4", SYMLINK+="ttyJ7"
KERNEL=="ttyS5", SYMLINK+="ttyJ7"
# Legacy ttyAMA0 alias
SUBSYSTEM=="tty", KERNEL=="ttyS1", SYMLINK+="ttyAMA0", GROUP="dialout"
# Disable USB autosuspend (prevents serial adapters from sleeping)
ACTION=="add", SUBSYSTEM=="usb", TEST=="power/control", ATTR{power/control}="on"
'';
}

View file

@ -11,6 +11,10 @@
{ config, lib, pkgs, ... }:
{
# Serial peripherals (validator/dispenser/printer modules + udev symlinks +
# console=tty0) are shared with the live ISO via ./upboard-serial.nix.
imports = [ ./upboard-serial.nix ];
boot = {
loader = {
systemd-boot.enable = true;
@ -42,21 +46,12 @@
"kvm-intel"
"i2c-dev"
"spi-dev"
"usbserial" # USB-to-serial adapters
"ftdi_sio" # FTDI USB serial
"cp210x" # CP210x USB serial
# Serial modules (usbserial/ftdi_sio/cp210x) → ./upboard-serial.nix.
];
kernelParams = [
"i915.enable_psr=0"
# NOTE: do NOT route the kernel console through ttyS4 on Sintra.
# ttyS4 is the SoC's MMIO 16550A (the only real UART besides the
# legacy ttyS0 at I/O 0x3f8) and is wired to the Fujitsu F56
# dispenser's RS-232 header on Sintra. Holding it as console
# prevents userspace from opening it at 9600 baud and HAL fails
# with "Input/output error setting custom baud rate of 9600".
# If you want serial debug, point console at ttyS0 instead.
"console=tty0"
# console=tty0 (keeps ttyS4 free for the F56) → ./upboard-serial.nix.
"quiet"
"splash"
];
@ -104,37 +99,9 @@
hybrid-sleep.enable = false;
};
# Serial port permissions + tejo-specific symlinks
# Camera + LED/SPI peripherals. The serial rules (validator/dispenser/printer
# symlinks + permissions) are shared with the live ISO in ./upboard-serial.nix.
services.udev.extraRules = lib.mkAfter ''
# Generic serial port permissions
KERNEL=="ttyS[0-9]*", MODE="0666"
KERNEL=="ttyUSB[0-9]*", MODE="0666"
KERNEL=="ttyACM[0-9]*", MODE="0666"
# ── Tejo serial port symlinks ──────────────────────────────────────
# Both UP Board and UP4000 rules included (match different kernel paths)
# Printer (ttyJ4)
KERNELS=="1-7.2:1.0", SYMLINK+="ttyJ4"
KERNEL=="ttyUSB0", SYMLINK+="ttyJ4"
# Validator (ttyJ5)
KERNELS=="1-7.3:1.0", SYMLINK+="ttyJ5"
KERNEL=="ttyUSB1", SYMLINK+="ttyJ5"
# Dispenser (ttyJ7).
# ttyS1 / ttyS5 cover earlier UP Board variants where the dispenser
# lands on those kernel-enumerated serial nodes; ttyS4 covers the
# Sintra (UP Board Atom/Apollo Lake) where the dispenser is wired
# to the SoC's MMIO UART. Whichever device actually exists at
# runtime gets the ttyJ7 symlink.
KERNEL=="ttyS1", SYMLINK+="ttyJ7"
KERNEL=="ttyS4", SYMLINK+="ttyJ7"
KERNEL=="ttyS5", SYMLINK+="ttyJ7"
# Legacy ttyAMA0 alias
SUBSYSTEM=="tty", KERNEL=="ttyS1", SYMLINK+="ttyAMA0", GROUP="dialout"
# ── Camera devices ─────────────────────────────────────────────────
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"
@ -147,8 +114,5 @@
SUBSYSTEM=="spidev", GROUP="spi", MODE="0660"
SUBSYSTEM=="i2c-dev", GROUP="i2c", MODE="0660"
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"
'';
}

View file

@ -48,7 +48,12 @@ in
# 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";

View file

@ -342,6 +342,25 @@
fileSystems."/".device = lib.mkForce "/dev/disk/by-label/nixos-usb";
fileSystems."/boot".device = lib.mkForce "/dev/disk/by-label/ESP-USB";
system.autoUpgrade.enable = lib.mkForce false;
# The Sintra's Aaeon firmware USB-boots in Legacy/BIOS mode — it
# boots the live ISO via its isolinux (BIOS) El Torito image, not
# the UEFI ESP. systemd-boot is UEFI-only, so a dd'd systemd-boot
# image isn't recognised as bootable. Switch THIS USB image to
# GRUB with BOTH BIOS (MBR + bios_grub partition, via the "hybrid"
# table below) and UEFI (removable /EFI/BOOT/BOOTX64.EFI) — mirroring
# the live ISO's dual boot — so it boots on Legacy and UEFI alike.
# Scoped to the USB image; the eMMC install keeps systemd-boot.
boot.loader.systemd-boot.enable = lib.mkForce false;
boot.loader.efi.canTouchEfiVariables = lib.mkForce false;
boot.loader.grub = {
enable = lib.mkForce true;
efiSupport = true;
efiInstallAsRemovable = true;
# make-disk-image's build VM exposes the image as /dev/vda;
# GRUB installs its BIOS stage to that disk's MBR.
devices = lib.mkForce [ "/dev/vda" ];
};
})
];
};
@ -349,7 +368,8 @@
inherit pkgs lib;
config = cfg.config;
format = "raw";
partitionTableType = "efi";
# hybrid = GPT + bios_grub partition + ESP → BIOS + UEFI bootable.
partitionTableType = "hybrid";
diskSize = "auto";
label = "nixos-usb"; # ext4 root label (make-disk-image -L)
};