From 4d0e42f2898c67e690630e5f83d88079ac7bd884 Mon Sep 17 00:00:00 2001 From: Patrick Mulligan Date: Thu, 30 Jul 2026 00:40:06 +0200 Subject: [PATCH 1/5] fix(hal): EBDS escrow stack/return latch + return-on-disable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cash-in stalled on the batm3: a note reached escrow and was read, but the acceptor never stacked or returned it, and the customer was never credited. Root cause: EBDS carries the stack/return decision as bits in the omnibus *poll* command, but the driver sent stack()/reject() as a single one-shot frame while a free-running 100ms poller kept sending plain polls. The lone stack frame races/collides with the poller (or its ack desyncs), gets dropped, and the device holds the note in escrow indefinitely. - ebds-rs232: latch the escrow decision (`pendingAction`) into the poll command byte and re-assert it on every poll until the device leaves escrow (cleared in _process when `!escrowed`). A dropped frame is now simply retried on the next poll. - hal-service: return an escrowed note on disableValidator() — disable alone does not release it on EBDS, so an inactivity timeout / cancel previously stranded the bill in the transport (observed on the batm3). - atm store: stringify the `[ATM] Sending event` / `[ATM] State` logs — they were printing `[object Object]`, which blinded the cash-in trace. Verified: hal builds, machine app typechecks. Hardware behaviour to be confirmed on the batm3 (no unit tests exist for this serial driver). Co-Authored-By: Claude Opus 4.8 --- apps/machine/electron/hal-service.ts | 9 ++++ apps/machine/src/stores/atm.ts | 4 +- .../hal/src/validators/ebds/ebds-rs232.ts | 42 +++++++++++++++---- 3 files changed, 46 insertions(+), 9 deletions(-) diff --git a/apps/machine/electron/hal-service.ts b/apps/machine/electron/hal-service.ts index 72f3480..2ec863b 100644 --- a/apps/machine/electron/hal-service.ts +++ b/apps/machine/electron/hal-service.ts @@ -221,6 +221,15 @@ export async function initializeHal(config: HalConfig): Promise { }, disableValidator: () => { + // If a note is sitting in escrow when we disable (inactivity timeout, + // cancel, or leaving the insert screen), return it to the customer. + // Disabling alone does NOT release an escrowed note on EBDS — it would + // be stranded in the transport until the next power cycle. + if (escrowDenomination !== null) { + console.log('[HAL] Returning escrowed bill on disable:', escrowDenomination) + escrowDenomination = null + validator?.reject() + } validator?.disable() validator?.lightOff() }, diff --git a/apps/machine/src/stores/atm.ts b/apps/machine/src/stores/atm.ts index a770dae..7119500 100644 --- a/apps/machine/src/stores/atm.ts +++ b/apps/machine/src/stores/atm.ts @@ -458,7 +458,7 @@ export const useAtmStore = defineStore('atm', () => { actor.value.subscribe((newSnapshot: SnapshotFrom) => { const prevSnapshot = snapshot.value snapshot.value = newSnapshot - console.log('[ATM] State:', newSnapshot.value) + console.log('[ATM] State:', JSON.stringify(newSnapshot.value)) // Detect transition into a complete state const state = newSnapshot.value @@ -1374,7 +1374,7 @@ export const useAtmStore = defineStore('atm', () => { console.error('[ATM] Cannot send event: machine not initialized') return } - console.log('[ATM] Sending event:', event) + console.log('[ATM] Sending event:', event.type, JSON.stringify(event)) actor.value.send(event) } diff --git a/packages/hal/src/validators/ebds/ebds-rs232.ts b/packages/hal/src/validators/ebds/ebds-rs232.ts index 6673a25..07820b1 100644 --- a/packages/hal/src/validators/ebds/ebds-rs232.ts +++ b/packages/hal/src/validators/ebds/ebds-rs232.ts @@ -313,6 +313,13 @@ export class EbdsRs232 extends EventEmitter { private serial: SerialPort | null = null private ack: number = 0x0 private enabledDenominations: number = 0x00 + // Latched escrow decision. In EBDS the stack/return choice is NOT a one-shot + // message — it's carried as bits in the omnibus poll command, and the device + // holds the escrowed note until a poll asserts stack or return. We keep the + // action set and re-assert it on every poll until the device leaves escrow + // (cleared in _process), so a single dropped/collided frame no longer strands + // the note in escrow forever. + private pendingAction: 'none' | 'stack' | 'return' = 'none' private lastStatusFlags: string | null = null private firmwareLogged: boolean = false @@ -405,23 +412,38 @@ export class EbdsRs232 extends EventEmitter { // -- Commands (Appendix D, Controller Message) --------------------------- - /** Send an Omnibus poll command with current denomination mask */ + /** + * Command byte 1 for the omnibus poll, encoding any latched escrow action. + * `stack` (0x3f) and `return` (0x5f) differ from the plain poll (0x1b) only + * in the stack/return bits; while an action is latched every poll re-asserts + * it until the device acts. + */ + private commandByte(): number { + if (this.pendingAction === 'stack') return 0x3f + if (this.pendingAction === 'return') return 0x5f + return 0x1b + } + + /** Send an Omnibus poll command with the current mask + latched action */ poll(): void { - this._dispatch([this.enabledDenominations, 0x1b, 0x10]) + this._dispatch([this.enabledDenominations, this.commandByte(), 0x10]) } - /** Stack the bill currently in escrow */ + /** Latch "stack the escrowed note"; re-asserted each poll until it takes. */ stack(): void { - this._dispatch([this.enabledDenominations, 0x3f, 0x10]) + this.pendingAction = 'stack' + this.poll() } - /** Reject/return the bill currently in escrow */ + /** Latch "return the escrowed note"; re-asserted each poll until it takes. */ reject(): void { - this._dispatch([this.enabledDenominations, 0x5f, 0x10]) + this.pendingAction = 'return' + this.poll() } /** Send initial setup command (disable all, reset state) */ reset(): void { + this.pendingAction = 'none' this._dispatch([0x00, 0x1b, 0x10]) } @@ -470,7 +492,13 @@ export class EbdsRs232 extends EventEmitter { validatePacket(packet) const result = interpret(packet) if (result) { - if (result.destructedData) this._logStatusOnChange(result.destructedData) + if (result.destructedData) { + // Clear a latched stack/return once the device has left escrow — it + // is now stacking/returning/idle, so we must stop asserting the + // action or it would leak onto the next note. + if (!result.destructedData[0].escrowed) this.pendingAction = 'none' + this._logStatusOnChange(result.destructedData) + } this.emit('message', result) } } catch (ex) { From eafdce36b6627ae48946238fbcc4c141b4949762 Mon Sep 17 00:00:00 2001 From: Patrick Mulligan Date: Thu, 30 Jul 2026 00:57:48 +0200 Subject: [PATCH 2/5] fix(config): point batm3 validator at /dev/ttyMEI (was nonexistent ttyACM0) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The batm3 preset defaulted the EBDS validator to /dev/ttyACM0, assuming a CDC-ACM BNR Advance. The actual MEI acceptor enumerates as a USB-serial device (ttyUSB*), exposed via the stable udev symlink /dev/ttyMEI (batm3.nix). Because /dev/ttyACM0 never existed, hal-service skipped the validator entirely and logged "[HAL] No validator — cash-in disabled", so Buy Bitcoin silently ignored inserted bills. Verified on hardware: with the correct device the validator starts, and (with the EBDS latch fix) a bill escrows → stacks → credits. Removes the need for the VITE_LAMASSU_VALIDATOR_DEVICE=/dev/ttyMEI per-box override. Co-Authored-By: Claude Opus 4.8 --- apps/machine/src/config/device.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/machine/src/config/device.ts b/apps/machine/src/config/device.ts index 9a22ab8..ae4ac00 100644 --- a/apps/machine/src/config/device.ts +++ b/apps/machine/src/config/device.ts @@ -139,7 +139,12 @@ export const MACHINE_PRESETS: Record Date: Thu, 30 Jul 2026 01:14:11 +0200 Subject: [PATCH 3/5] fix(config): use stable /dev/ttyF56 symlink for batm3 dispenser MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The batm3 dispenser used the raw /dev/ttyUSB0, which is enumeration-order dependent — a re-plug or reboot could reassign ttyUSB0 to a different adapter. Switch to the stable udev symlink /dev/ttyF56 (batm3.nix, serial DDDLb103Y23), matching the validator's /dev/ttyMEI, so both peripherals bind by identity and survive re-enumeration (incl. on an internal-SATA flash). Per-box override: VITE_LAMASSU_DISPENSER_DEVICE. Co-Authored-By: Claude Opus 4.8 --- apps/machine/src/config/device.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/machine/src/config/device.ts b/apps/machine/src/config/device.ts index ae4ac00..cffdaa3 100644 --- a/apps/machine/src/config/device.ts +++ b/apps/machine/src/config/device.ts @@ -148,7 +148,12 @@ export const MACHINE_PRESETS: Record Date: Wed, 29 Jul 2026 17:47:00 +0200 Subject: [PATCH 4/5] feat(deploy): USB-bootable batm3 test image (disk-image-batm3-usb) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a USB-bootable BATM3 disk-image target plus the batm3 hardware changes that make a dd'd USB stick boot reliably on the Dell 9030 AIO. flake.nix — new `disk-image-batm3-usb` target: - Distinct partition labels (nixos-usb / ESP-USB) so stage-1 by-label resolution can't latch onto an internal SATA drive that already holds a generic nixos/ESP-labelled install. Post-build mlabel relabels the ESP FAT volume to ESP-USB (bootloader files untouched; UEFI still loads /EFI/BOOT/BOOTX64.EFI). - /boot mounted nofail + short device-timeout: the firmware already loaded the bootloader before Linux; without nofail a slow/late ESP-USB enumeration drops to emergency mode with root locked — a dead end. - NO growPartition/autoResize on the USB image: sfdisk rewriting the partition table on first boot is the single most bus-stressing write, and flaky USB bridges drop off the bus mid-rewrite (sfdisk wedges in uninterruptible D-state and ESP-USB vanishes with the device, so /boot times out too). Persistent state is a few MB and the image already ships ~2GB free in root. The internal-SATA disk-image-batm3 keeps growPartition — a real AHCI SSD won't drop the bus. - autoUpgrade off (test image, not a managed fleet member). batm3.nix — USB-boot reliability: - Add usb_storage to initrd.availableKernelModules so stage-1 binds the stick and /dev/disk/by-label/* appears. - Blacklist uas + usbcore.autosuspend=-1: force the slower-but-reliable Bulk-Only Transport path and stop the boot medium being power-suspended mid-I/O — both were causing "device offline error" bus drops. Co-Authored-By: Claude Opus 4.8 --- deploy/nixos/hardware/batm3.nix | 17 ++++++++ flake.nix | 77 +++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+) diff --git a/deploy/nixos/hardware/batm3.nix b/deploy/nixos/hardware/batm3.nix index 2a83f2d..ce8e1fe 100644 --- a/deploy/nixos/hardware/batm3.nix +++ b/deploy/nixos/hardware/batm3.nix @@ -17,8 +17,22 @@ "ahci" "usbhid" "sd_mod" + # USB mass-storage: required to boot the dd'd image from a USB stick + # (stage-1 must bind the flash drive as a SCSI disk so + # /dev/disk/by-label/nixos appears). Harmless on the internal-SATA + # install, where ahci+sd_mod already cover the root device. + # + # NOTE: deliberately NO "uas" here. Many USB sticks/bridges advertise + # UAS but drop off the bus ("device offline error, dev sdb") under the + # sustained write load of first-boot growPartition/journal/swapfile. + # Blacklisting uas below forces the slower-but-reliable usb-storage + # (Bulk-Only Transport) path. SATA/eMMC installs don't use uas anyway. + "usb_storage" ]; + # Keep the USB flash drive off the flaky UAS driver (see note above). + blacklistedKernelModules = [ "uas" ]; + kernelModules = [ "kvm-intel" "usbtouchscreen" @@ -27,6 +41,9 @@ kernelParams = [ "quiet" "splash" + # Disable USB autosuspend so the boot medium (and kiosk peripherals) + # aren't power-suspended mid-I/O — another cause of "device offline". + "usbcore.autosuspend=-1" ]; }; diff --git a/flake.nix b/flake.nix index 3aa61e9..3fe1bba 100644 --- a/flake.nix +++ b/flake.nix @@ -424,6 +424,83 @@ printf 'verify ESP label: '; mlabel -i "$out/nixos.img@@$espStart" -s :: || true ''; + # USB-bootable BATM3 TEST image with DISTINCT partition labels + # (nixos-usb / ESP-USB). The plain disk-image-batm3 reuses the generic + # nixos/ESP labels, so a USB stick carrying it, booted on a batm3 whose + # internal SATA drive ALREADY holds a nixos/ESP-labelled install, makes + # stage-1's by-label/nixos resolve to the internal drive (larger fs, + # journal recovers) instead of the stick — the stage-2 init path baked + # into the USB's boot entry isn't on that root, so stage 1 aborts. + # Distinct labels make stage-1 pick the stick unambiguously WITHOUT + # touching the internal drive. Unlike disk-image-sintra-usb this keeps + # systemd-boot: the batm3 firmware UEFI-USB-boots fine via the ESP's + # /EFI/BOOT/BOOTX64.EFI removable fallback, so no GRUB/hybrid-table + # change is needed — only the label disambiguation here plus the + # usb_storage/uas initrd modules (in batm3.nix). Does NOT grow to fill + # the stick (see the growPartition note below — sfdisk on first boot + # wedges flaky USB bridges); auto-upgrade off (test image, not a managed + # fleet member — also stops scheduled bootloader writes landing on the + # internal drive's ESP). + disk-image-batm3-usb = + let + cfg = self.nixosConfigurations.batm3-installed.extendModules { + modules = [ + ({ lib, ... }: { + fileSystems."/".device = lib.mkForce "/dev/disk/by-label/nixos-usb"; + fileSystems."/boot".device = lib.mkForce "/dev/disk/by-label/ESP-USB"; + # /boot must NOT be a hard boot dependency on the USB test + # image. The firmware already loaded the bootloader from the + # ESP before Linux started; /boot is only remounted so the OS + # can *update* the bootloader — which this image never does + # (autoUpgrade off, no nixos-rebuild on the stick). Without + # nofail, a slow/late ESP-USB enumeration (BOT is slower than + # UAS) blows past systemd's 90s device-timeout and drops to + # emergency mode — with root locked, an unrecoverable dead end. + # nofail + a short timeout lets the (already-mounted) root carry + # the boot to completion; /boot mounts if/when the ESP shows up. + fileSystems."/boot".options = [ "nofail" "x-systemd.device-timeout=10s" ]; + # DELIBERATELY NO growPartition/autoResize on the USB image. + # growPartition runs sfdisk to rewrite the stick's partition + # table on first boot — the single most bus-stressing write of + # the boot. Flaky USB bridges drop off the bus mid-rewrite + # (sfdisk hangs forever as an uninterruptible D-state task) and, + # worse, partition 1 (ESP-USB) vanishes with the device, so + # /boot times out too. The kiosk's persistent state (state.db, + # .env, wifi.conf, logs) is a few MB and the built image already + # carries ~2GB free inside root — growing to fill the stick buys + # nothing and costs reliability. The internal-SATA target + # (disk-image-batm3) keeps growPartition: a real AHCI SSD won't + # drop the bus and there filling the disk is worth it. + system.autoUpgrade.enable = lib.mkForce false; + }) + ]; + }; + baseImage = import (nixpkgs + "/nixos/lib/make-disk-image.nix") { + inherit pkgs lib; + config = cfg.config; + format = "raw"; + partitionTableType = "efi"; + diskSize = "auto"; + label = "nixos-usb"; # ext4 root label (make-disk-image -L) + }; + in + pkgs.runCommand "nixos-disk-image-batm3-usb" + { nativeBuildInputs = [ pkgs.parted pkgs.mtools ]; } + '' + mkdir -p $out + cp --sparse=always ${baseImage}/nixos.img $out/nixos.img + chmod +w $out/nixos.img + # make-disk-image hardcodes the ESP FAT label to "ESP"; relabel the + # volume to ESP-USB so /boot (by-label/ESP-USB) can't resolve to an + # internal drive's ESP. Volume label only — bootloader files are + # untouched, and UEFI loads /EFI/BOOT/BOOTX64.EFI regardless. + espStart=$(parted -sm "$out/nixos.img" unit B print | awk -F: '$1==1 {gsub("B","",$2); print $2}') + echo "ESP partition starts at byte $espStart — relabelling to ESP-USB" + export MTOOLS_SKIP_CHECK=1 + mlabel -i "$out/nixos.img@@$espStart" ::ESP-USB + printf 'verify ESP label: '; mlabel -i "$out/nixos.img@@$espStart" -s :: || true + ''; + # Backwards compat iso = self.nixosConfigurations.douro.config.system.build.isoImage; }; From f52d942e577be41c16a1db58eb7652f159698aeb Mon Sep 17 00:00:00 2001 From: Patrick Mulligan Date: Wed, 29 Jul 2026 17:48:53 +0200 Subject: [PATCH 5/5] fix(deploy): eGalax touchscreen on batm3 (kernel 6.6 + calibration) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Dell 9030 AIO's built-in eGalax SAW panel (0eef:0001) was unusable: touches either didn't register or landed in the wrong place. Full fix: - Pin linuxPackages_6_6. On 25.11's default 6.12 kernel hid-multitouch grabs the controller and mis-parses its HID report (axes read stuck) and usbtouchscreen refuses to bind. On 6.6 usbtouchscreen binds and produces a clean single-touch ABS device (the known-good internal-SATA install runs 6.6.68). Mirrors douro.nix's per-hardware kernel pin. - udev rule now modprobes usbtouchscreen ITSELF before unbinding usbhid and handing over via new_id. On a USB boot systemd-udev-trigger fires this rule (~2s) before systemd-modules-load loads usbtouchscreen (~12s), so new_id previously hit a not-yet-loaded driver and the panel bound to nothing. Loading it inline removes the boot-ordering race. - Add an X evdev InputClass (99-egalax.conf) so X uses evdev + the transformation matrix rather than libinput. Mirrors the working internal-SATA install. - egalax-calibrate: add XAUTHORITY (=/home/bitspire/.Xauthority) — the actual boot-time bug. Without the auth cookie xinput died with "Invalid MIT-MAGIC-COOKIE-1 key / Unable to connect to X server", so the coordinate-transformation matrix was never applied and touches landed in the wrong place. Also replace the fixed ExecStartPre sleep with a 30s retry loop on the eGalax X device appearing — more robust to boot timing than a race against display-manager. Co-Authored-By: Claude Opus 4.8 --- deploy/nixos/hardware/batm3.nix | 70 +++++++++++++++++++++++++++++---- 1 file changed, 62 insertions(+), 8 deletions(-) diff --git a/deploy/nixos/hardware/batm3.nix b/deploy/nixos/hardware/batm3.nix index ce8e1fe..a92159e 100644 --- a/deploy/nixos/hardware/batm3.nix +++ b/deploy/nixos/hardware/batm3.nix @@ -12,6 +12,15 @@ timeout = 3; }; + # Pin the 6.6 LTS kernel. The Dell 9030 AIO's eGalax SAW touch panel + # (0eef:0001) works with the usbtouchscreen driver on 6.6 (the known-good + # internal-SATA install runs 6.6.68). On 25.11's default 6.12 kernel this + # old controller regressed: hid-multitouch grabs it and mis-parses the HID + # report ("failed to fetch feature 7", axes read stuck), usbtouchscreen + # refuses it, and touch is unusable regardless of udev/X config. Matching + # douro.nix's per-hardware kernel pin. Re-test touch before bumping this. + kernelPackages = pkgs.linuxPackages_6_6; + initrd.availableKernelModules = [ "xhci_pci" "ahci" @@ -122,10 +131,20 @@ ''; # eGalax touchscreen (Dell 9030 AIO built-in panel) - # The eGalax HID descriptor confuses libinput (treats it as touchpad). - # Fix: unbind from usbhid at boot, bind to usbtouchscreen kernel module, - # then apply calibration matrix after X11 starts. - # Unbind eGalax from usbhid, bind to usbtouchscreen + # By default usbhid/hid-multitouch claim the eGalax and mis-parse its + # HID report descriptor (X axis reads as stuck), so touch is unusable. + # Fix: hand the device to the usbtouchscreen kernel driver, which parses + # the raw eGalax protocol into a clean single-touch ABS device that the + # X evdev driver + calibration matrix (below) map correctly. This mirrors + # the known-good internal-SATA install. + # + # The RUN command modprobes usbtouchscreen ITSELF before unbinding usbhid + # and handing over via new_id. usbtouchscreen is also in boot.kernelModules + # (systemd-modules-load), but on a USB boot systemd-udev-trigger fires this + # rule (~2s) BEFORE modules-load gets usbtouchscreen in (~12s) — so the + # new_id write hit a not-yet-loaded driver and the panel was left bound to + # nothing. Loading it inline here makes the handoff independent of that + # boot-ordering race (on internal-SATA boot the order happened to work). services.udev.extraRules = lib.mkAfter '' KERNEL=="ttyS[0-9]*", MODE="0666" KERNEL=="ttyUSB[0-9]*", MODE="0666" @@ -133,7 +152,25 @@ SUBSYSTEM=="tty", ATTRS{serial}=="DDDLb103Y23", SYMLINK+="ttyF56", MODE="0666" SUBSYSTEM=="tty", ATTRS{serial}=="A9YW78OC", SYMLINK+="ttyMEI", MODE="0666" SUBSYSTEM=="tty", ATTRS{serial}=="A9ZF8ELY", SYMLINK+="ttyNFC", MODE="0666" - ACTION=="add", SUBSYSTEM=="usb", ATTRS{idVendor}=="0eef", ATTRS{idProduct}=="0001", RUN+="${pkgs.bash}/bin/bash -c 'echo ''$kernel:1.0 > /sys/bus/usb/drivers/usbhid/unbind 2>/dev/null; echo 0eef 0001 > /sys/bus/usb/drivers/usbtouchscreen/new_id 2>/dev/null'" + ACTION=="add", SUBSYSTEM=="usb", ATTRS{idVendor}=="0eef", ATTRS{idProduct}=="0001", RUN+="${pkgs.bash}/bin/bash -c '${pkgs.kmod}/bin/modprobe usbtouchscreen 2>/dev/null; echo ''$kernel:1.0 > /sys/bus/usb/drivers/usbhid/unbind 2>/dev/null; echo 0eef 0001 > /sys/bus/usb/drivers/usbtouchscreen/new_id 2>/dev/null'" + ''; + + # Force the X evdev driver on the eGalax (not libinput). The usbtouchscreen + # node is a plain single-touch absolute device; evdev + the transformation + # matrix in egalax-calibrate below give correct orientation. Mirrors the + # working internal-SATA install's /etc/X11/xorg.conf.d/99-egalax.conf. + environment.etc."X11/xorg.conf.d/99-egalax.conf".text = '' + Section "InputClass" + Identifier "eGalax Touchscreen" + MatchVendor "0eef" + MatchProduct "0001" + MatchDevicePath "/dev/input/event*" + Driver "evdev" + Option "InvertY" "false" + Option "InvertX" "false" + Option "SwapAxes" "false" + Option "Calibration" "" + EndSection ''; # Apply touchscreen calibration after X11 starts @@ -147,9 +184,26 @@ Type = "oneshot"; RemainAfterExit = true; User = "bitspire"; - Environment = "DISPLAY=:0"; - ExecStartPre = "${pkgs.coreutils}/bin/sleep 3"; - ExecStart = "${pkgs.xorg.xinput}/bin/xinput set-prop 'eGalax Inc. USB TouchController' 'Coordinate Transformation Matrix' 0 -1.268 1.147 -1.224 0 1.118 0 0 1"; + # DISPLAY *and* XAUTHORITY — without the auth cookie xinput dies with + # "Invalid MIT-MAGIC-COOKIE-1 key / Unable to connect to X server" and + # the matrix is never applied, so touches register but land in the wrong + # place (the panel then feels dead). This was the actual boot-time bug. + Environment = [ "DISPLAY=:0" "XAUTHORITY=/home/bitspire/.Xauthority" ]; + # Wait for the eGalax X device to appear (usbtouchscreen binds a little + # after display-manager on a USB boot) and retry, instead of a fixed + # sleep — more robust to boot timing. Matrix: swap X/Y + invert + scale + # to the active panel area (matches the known-good internal install). + ExecStart = pkgs.writeShellScript "egalax-calibrate" '' + for i in $(${pkgs.coreutils}/bin/seq 1 30); do + if ${pkgs.xorg.xinput}/bin/xinput list --name-only 2>/dev/null | ${pkgs.gnugrep}/bin/grep -qx 'eGalax Inc. USB TouchController'; then + exec ${pkgs.xorg.xinput}/bin/xinput set-prop 'eGalax Inc. USB TouchController' \ + 'Coordinate Transformation Matrix' 0 -1.268 1.147 -1.224 0 1.118 0 0 1 + fi + ${pkgs.coreutils}/bin/sleep 1 + done + echo "egalax-calibrate: eGalax device not found after 30s" >&2 + exit 1 + ''; }; };