From 5a420119df486e9db110a9f29faccbff4cd98708 Mon Sep 17 00:00:00 2001 From: Padreug Date: Wed, 1 Jul 2026 22:42:49 +0200 Subject: [PATCH 1/3] perf(deploy): slim the kiosk closure (disable TTS, Qt, docs) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The disk image was ~6.2 GiB of closure, largely desktop/multimedia baggage a single-purpose Electron kiosk never uses. Cut the clearly-unused stacks: - services.speechd off → drops speech-dispatcher's espeak-ng + mbrola voices (~1 GB text-to-speech). An ATM does not talk. - v4l-utils built withGUI=false → drops the entire Qt6 stack (~0.5 GB) that only backed the qv4l2 GUI; the v4l2-ctl CLI we actually use for the camera stays. - documentation off (man/info/NixOS manual) — nobody reads them on a kiosk. Closure 6.2 → 5.0 GiB. The remaining bulk is electron's own runtime (gtk4/ gstreamer/pipewire, unavoidable), mesa+llvm (GPU), and linux-firmware — those need heavier / riskier work to touch. Distribute the image as .img.zst. Co-Authored-By: Claude Opus 4.8 --- deploy/nixos/configuration.nix | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/deploy/nixos/configuration.nix b/deploy/nixos/configuration.nix index d473a42..2306f5a 100644 --- a/deploy/nixos/configuration.nix +++ b/deploy/nixos/configuration.nix @@ -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 -- 2.55.0 From c8745addbcba816e8327d17dac610f249c15c372 Mon Sep 17 00:00:00 2001 From: Padreug Date: Thu, 2 Jul 2026 14:49:27 +0200 Subject: [PATCH 2/3] fix(deploy): make disk-image-sintra-usb BIOS+UEFI bootable (GRUB) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The USB disk image was systemd-boot (UEFI-only) with make-disk-image's "efi" table (pure GPT + ESP, protective MBR). The Sintra's Aaeon UP Board firmware USB-boots in Legacy/BIOS mode — it boots the live ISO via that ISO's isolinux (BIOS) El Torito image, not the UEFI ESP — so a dd'd systemd-boot image has no BIOS boot code to execute and the firmware won't list it (a hand-added hybrid MBR didn't help: nothing to run). Switch the USB target to GRUB with BIOS + UEFI on make-disk-image's "hybrid" table: it adds a bios_grub partition, GRUB writes its BIOS stage to the MBR AND a removable /EFI/BOOT/BOOTX64.EFI — mirroring the live ISO's dual boot. The Aaeon now lists it (as two "ia android" entries, BIOS + UEFI) and boots it. Scoped to disk-image-sintra-usb only; the eMMC install keeps systemd-boot. ESP stays partition 1 so the ESP-USB relabel step is unchanged. Verified on hardware: booted from USB into the wizard with the full upboard.nix hardware config. Co-Authored-By: Claude Opus 4.8 --- flake.nix | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/flake.nix b/flake.nix index a09ffdd..3a678da 100644 --- a/flake.nix +++ b/flake.nix @@ -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) }; -- 2.55.0 From 7e90719508e17bf0c7385e9d4a8edd797ac0ef47 Mon Sep 17 00:00:00 2001 From: Padreug Date: Thu, 2 Jul 2026 18:40:12 +0200 Subject: [PATCH 3/3] refactor(deploy): share UP Board serial hardware between installed + live ISO MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The sintra live ISO (live.nix) had no serial support — ftdi_sio and the ttyJ5/ttyJ7 udev symlinks were only in hardware/upboard.nix (installed), so booting iso-sintra on real hardware failed on the validator + F56 dispenser while the disk image worked. The two definitions had already drifted (live's tejo block lacked ttyS4). Extract the UP Board serial peripherals (usbserial/ftdi_sio/cp210x, the ttyJ4/ttyJ5/ttyJ7 udev symlinks + permissions, console=tty0) into hardware/upboard-serial.nix and import it from both upboard.nix (installed tejo + sintra) and live.nix (sintra only). Single source of truth — the two artifacts can't drift again. Named upboard-serial (not sintra-serial) since upboard.nix serves both tejo-installed and sintra-installed. Camera + LED/SPI rules stay inline in upboard.nix (installed-specific; the pairing camera works via getUserMedia without the scanner symlink). Verified by eval: live sintra now carries ftdi_sio + console=tty0 + ttyJ7; installed sintra/tejo unchanged (serial present, camera present, no console dupe). Co-Authored-By: Claude Opus 4.8 --- deploy/nixos/hardware/upboard-serial.nix | 61 ++++++++++++++++++++++++ deploy/nixos/hardware/upboard.nix | 52 ++++---------------- deploy/nixos/live.nix | 7 ++- 3 files changed, 75 insertions(+), 45 deletions(-) create mode 100644 deploy/nixos/hardware/upboard-serial.nix diff --git a/deploy/nixos/hardware/upboard-serial.nix b/deploy/nixos/hardware/upboard-serial.nix new file mode 100644 index 0000000..dfbf947 --- /dev/null +++ b/deploy/nixos/hardware/upboard-serial.nix @@ -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" + ''; +} diff --git a/deploy/nixos/hardware/upboard.nix b/deploy/nixos/hardware/upboard.nix index b3fe1f3..d213ff7 100644 --- a/deploy/nixos/hardware/upboard.nix +++ b/deploy/nixos/hardware/upboard.nix @@ -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" ''; } diff --git a/deploy/nixos/live.nix b/deploy/nixos/live.nix index 7f0a190..44255e9 100644 --- a/deploy/nixos/live.nix +++ b/deploy/nixos/live.nix @@ -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"; -- 2.55.0