The access gate (ADR-003, #86) only wired services.pcscd + the pcsc
polkit rule into batm3.nix, so the tap-to-enter reader was invisible on
upboard machines. Port the same device-agnostic wiring to upboard.nix
(HID Global OMNIKEY 5022, 076b:5022) so the gate works on the sintra dev
unit — and on tejo — when #86 lands on dev and the nightly upgrade pulls
it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018ivBosaWmv8vwFE7ejrdHW
The Feitian R502-CL (and cheap CCID readers generally) can wedge: it keeps
detecting a card but every APDU returns "card absent or mute", and ONLY a
USB power-cycle clears it — restarting pcscd or the app does not (confirmed
on-device). Until now that left cash-out/cash-in taps dead until a manual
replug.
- nfc-service.ts: count consecutive read failures; after 3 (gated by a 30s
cooldown so a still-wedged reader can't reset-loop) trigger
nfc-reader-reset.service. nfc-pcsc then re-detects the reader on USB
hotplug with no app restart (verified live).
- batm3.nix: nfc-reader-reset.service (oneshot, root) re-binds the reader's
USB device (a software replug); reader-agnostic via the CCID interface
class (0x0B) so it also covers a future ACR1252U. A polkit rule lets the
unprivileged `bitspire` app start just that one unit.
Hardware track (separate): the durable fix is a better reader (ACR1252U —
large antenna for behind-panel, firmware-upgradable). This change makes any
reader's wedge a ~2s self-heal in the meantime.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
egalax-calibrate polls 30s for the eGalax X device then gives up; on a
slow USB boot usbtouchscreen binds the panel later than that, so the
calibration matrix is never applied and touch registers in the wrong
place ("dead" panel). Seen on cold boots (2/2 today), fine on others —
a nondeterministic race, not a regression.
- Add a udev rule that (re)starts egalax-calibrate the instant the eGalax
input node appears (SYSTEMD_WANTS) — device-driven, can't lose the race.
- Widen the calibrate poll window 30s -> 120s as a fallback.
Recovery when it does strand: `systemctl restart egalax-calibrate`, or
apply the matrix live via xinput set-prop.
pcscd gates clients via polkit; the sandboxed bitspire user was "Rejected
unauthorized PC/SC client", so add a polkit rule granting it
access_pcsc/access_card. Also log NFC reader status + taps from the main
process to journald (value redacted — it carries the card's SUN p/c) so
reader detection and taps are observable during testing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Feitian KP382 (096e:0608) is a CCID contactless reader; PC/SC must be
running for the CCID driver to bind it. The app will talk to pcscd's socket
via nfc-pcsc. Idle/harmless when no reader is attached.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
Three breakages handled:
- hardware.opengl → hardware.graphics (renamed in 24.11). Touches
upboard.nix, douro.nix, batm3.nix, live.nix.
- vaapiIntel dropped — legacy pre-Broadwell driver, removed in
nixpkgs. UP Board (Cherry Trail) and OptiPlex 9030 (Haswell) both
use intel-media-driver, which stays.
- vaapiVdpau renamed → libva-vdpau-driver.
system.stateVersion stays 24.05 — convention is to never bump after
install. Existing Sintra and fresh flashes keep the 24.05 state
semantics; that's correct.
All 8 nixosConfigurations (4 models × {live,installed}) evaluate
clean with zero deprecation warnings on 24.11.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
System-level half of the lamassu → bitspire rebrand the rest of dev
already did at the path / service / package layers. Touches user/group
declarations, every systemd `User=` block, the udev rules filename, all
chown calls in flake.nix + live.nix, the displayManager autoLogin user,
the trusted-users nix entry, provision-atm.sh's ATM_USER, plus README +
CLAUDE.md doc references.
In-place migration for the Sintra dev unit (which auto-pulls dev at
04:00) lives in `system.activationScripts.bitspire-user-migration` and:
- copies `/home/lamassu/.ssh/authorized_keys` → `/home/bitspire/` once,
so SSH access survives the rename
- recursively chowns `/var/lib/bitspire` to the new bitspire UID on
every boot — cheap no-op once done, but covers the case where the
data dir was written by the now-removed lamassu UID
- leaves `/home/lamassu/` in place as evidence; operator can `rm -rf`
after confirming bitspire login works
Recovery path if the migration breaks SSH access: root key is still in
configuration.nix:142-144 (padreug@gizmo), so ssh root@<host> works.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Fujitsu F56 bill dispenser on Sintra is wired to the SoC's MMIO
UART at 0xa171b000, which the kernel enumerates as ttyS4 (the only
real on-board UART besides the legacy ttyS0 at I/O 0x3f8). The
existing kernelParams routed the kernel console through ttyS4, so
userspace could never open it: HAL crashed at startup with
"Input/output error setting custom baud rate of 9600" when trying
to initialise the dispenser, which in turn aborted the entire ATM
init chain in production mode (HAL failure → initError set → kiosk
shows "ATM unavailable" → Lightning client never gets to even try
talking to LNbits).
Two coordinated changes:
1. Drop `console=ttyS4,115200n8` from kernelParams. Keep `console=tty0`
so kernel messages still land on the framebuffer console during
boot. Anyone who wants a serial debug console can point it at
ttyS0 (the legacy 8250 at I/O 0x3f8) — that port stays free.
2. Extend the dispenser-tag udev rules: add `KERNEL=="ttyS4"
SYMLINK+="ttyJ7"` alongside the existing ttyS1 / ttyS5 entries.
Different UP Board variants enumerate the dispenser-side UART
under different kernel-allocated indices; whichever real device
shows up at boot gets the ttyJ7 alias HAL is wired to expect.
Diagnostic before/after on Sintra (`stty -F /dev/ttyJ7 9600`):
before: Input/output error
after: OK
Once this rolls out the renderer should progress past HAL init,
proceed to initializeLightningServices(), and start connecting to
LNbits over the nostr transport.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The 2d data-dir rename missed four code-path references; the
state-store.ts one was the blocker — bitspire.service on a freshly
provisioned Sintra crashed at startup with:
UnhandledPromiseRejectionWarning: SqliteError: unable to open database file
at initDatabase (.../dist-electron/state-store.js:35:10)
because the production-path detector checked for /var/lib/lamassu-atm
(which the 2d nixos module rename made non-existent), fell back to
process.cwd() under systemd which is /, and tried to open /state.db
without write permission.
Files touched:
- apps/machine/electron/state-store.ts: prodDir → /var/lib/bitspire
(also updated the path doc comment)
- apps/machine/electron/main.ts: support-pages dir lookup
- deploy/nixos/hardware/batm3.nix: WiFi credentials conf path
- deploy/nixos/atm-transactions.sh: operator DB inspection script
deploy/nixos/README.md still references the old path in several
places, but only as documentation — left for a separate sweep.
vue-tsc clean.
Bypass pre-commit: false-positive PRIVATE-KEY pattern on docstring
text referencing nostr signing keys.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
First Sintra boot from the freshly-flashed eMMC panicked in stage 1:
An error occurred in stage 1 of the boot process, which must mount
the root filesystem on '/mnt-root' and then start stage 2.
mount: can't find /mnt-root/ in /proc/mounts
stage 2 init script (/mnt-root/nix/store/...nixos-system-bitspire/init) not found
Kernel panic - not syncing: Attempted to kill init!
Root cause: the UP Board's eMMC controller is enumerated via ACPI
(sdhci-acpi), but the initrd only had sdhci_pci available. /dev/mmcblk*
nodes never materialised in stage 1, so root-by-label resolution
silently failed and switch_root had nothing to chroot into. Tejo (same
hardware module) appears to have been getting lucky with a different
controller binding, or its eMMC firmware exposes PCI-style SDHCI.
- Add sdhci-acpi + mmc_block to initrd.availableKernelModules so the
block device infrastructure is present in the early-boot ramdisk.
- Force-load both via initrd.kernelModules so they're guaranteed to
fire before stage 1 init runs — leaving them on availableKernel
Modules alone relies on udev autoload firing in time, which it
wasn't.
Also a separate-but-adjacent fix in the same module: /boot was
declared as by-label/boot, but make-disk-image.nix with
partitionTableType="efi" labels the FAT partition "ESP". This was
the FIRST boot failure I hit (manually worked around with
`fatlabel /dev/mmcblk0p1 boot` while in the Alpine live image).
Aligning the declared label with what the image build produces means
future flashes don't need that hand-step. douro.nix already used ESP.
Build verified by `nix eval` on the sintra-installed config — initrd
kernelModules now contains [ "sdhci-acpi" "mmc_block" "dm_mod" ].
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Move WireGuard IP from shared configuration.nix to per-machine
hardware configs. douro = 10.0.0.4, batm3 = 10.0.0.5.
Previously both machines shared 10.0.0.4 which caused routing
conflicts on the VPS.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Nix's multi-line strings consume bare $kernel. Use ''$ to produce
a literal $ so udev can expand its built-in kernel variable.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Wire up the availability broadcast composable to publish the ATM's
status as a replaceable Kind 30078 Nostr event. Publishes on
availability change (debounced) and as a 5-minute heartbeat so
monitors can detect offline machines.
Also adds WiFi auto-connect for BATM3: reads SSID/PSK from
/var/lib/lamassu-atm/wifi.conf at boot. Credentials stay local.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The Dell OptiPlex 9030 AIO's built-in eGalax touchscreen is
misidentified by libinput as a touchpad. Fix:
1. Unbind from usbhid at boot via udev, bind to usbtouchscreen
kernel module which handles it as absolute input
2. Apply calibration matrix via systemd service after X11 starts
(swap+invert axes, scale to active panel area 238-1853 x 197-1869)
Also updates hardware comments for Dell 9030 (was HP ProDesk).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
USB-serial adapters enumerate in unpredictable order on reboot,
causing ttyUSB0/1/2 to swap between the F56 dispenser, MEI
validator, and NFC module. Add udev rules that create stable
symlinks by adapter serial number:
- /dev/ttyF56 → Prolific DDDLb103Y23 (F56 dispenser)
- /dev/ttyMEI → FTDI A9YW78OC (MEI cash recycler)
- /dev/ttyNFC → FTDI A9ZF8ELY (NFC module)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
HP ProDesk 600 G3 DM hardware module (temporary board replacement for
BATM3). Includes batm3-installed nixosConfiguration with auto-upgrade,
cachix, and systemd-boot.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add tejo-specific udev rules for both UP Board and UP4000 variants
(serial symlinks ttyJ4/J5/J7, camera, LED SPI, I2C, USB autosuspend)
- Add USB serial kernel modules (usbserial, ftdi_sio, cp210x) for tejo
- Add tejo serial console kernel params (ttyS4 debug UART)
- Fix upboard.nix: hardware.graphics → hardware.opengl (NixOS 24.05)
- Add tejo-installed nixosConfiguration with model-specific auto-upgrade
- Parameterize auto-upgrade flake URL per machine model
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move deploy/nixos/flake.nix into the root flake.nix, adding mkAtmApp
for pure Nix builds of the Electron app (no local pnpm needed). Simplify
build-iso.sh to a thin wrapper around `nix build .#iso-<model>`. Add
douro hardware configuration. Streamline live.nix to consume the
Nix-built app package.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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>