This is the white screen. Not graphics, not the bundle, not pairing.
The app constructs @pokusew/pcsclite at startup. That calls
SCardEstablishContext(), which calls SCardCheckDaemonAvailability(), which
— finding no pcscd — BUSY-LOOPS in fstatat64 at ~92% CPU rather than
returning an error. It runs on Electron's main thread before the
BrowserWindow is created, so no window is ever made and the panel stays
white.
It is a hang, not a crash, which is why it presented so badly. Nothing
throws. Nothing is logged after "[StateStore] Initialized database". The
process looks healthy: systemd reports the service active, Electron is
running, and there is even a gpu-process. But a setInterval registered
before startup never fires once in 32 seconds, the main thread sits in
state R, and the remote debugger reports zero page targets.
V8's own tooling cannot see it either, because the thread never yields to
the inspector: Debugger.pause returns nothing and Profiler.stop times out.
A native backtrace was the only thing that worked:
#0 fstatat64 libc
#1 SCardCheckDaemonAvailability libpcsclite
#2 SCardEstablishContext libpcsclite
#3 PCSCLite::PCSCLite() pcsclite.node
#4 PCSCLite::New(...)
Ruled out along the way, each by direct test on the machine: graphics (it
fails identically with the GPU fully disabled), Electron on aarch64 (a
minimal app renders fine), the Vue bundle (loading the real index.html
from a minimal main process mounts the app and reaches "[ATM] State
machine initialized"), the preload script, the CSP, kiosk and fullscreen
window options, better-sqlite3, /dev/shm, memory, page size, X
authorisation, and isDev.
upboard.nix and batm3.nix both enable pcscd for their real readers, which
is why no x86 machine has ever hit this. This module did not, and that was
the entire difference. pcscd with no reader attached simply idles, so
enabling it costs nothing.
Worth noting for the wider fleet: any future board that omits pcscd
inherits this, and it presents as a blank screen with a healthy-looking
service. The robust fix is for the app to not block its main thread on a
card-reader handshake at all — the NFC path is already documented as
best-effort — but that is an app change and this unblocks the hardware.
Two findings from the first Pi 4 (actually a CM4) bring-up, chased from a
white screen to hardware-accelerated X.
CMA. The vc4 display pipeline allocates its framebuffer from the contiguous
memory area, and the default reservation here is 32MiB with ~11MiB free. The
attached panel is 3840x1080, whose framebuffer is ~16.6MB before double
buffering, so X picked the mode and then died:
Output HDMI-1 using initial mode 3840x1080 +0+0
(EE) AddScreen/ScreenInit failed for driver 0
nixos-hardware's fkms-3d injected a CMA overlay alongside its display one;
cma=256M replaces that half. This part is declarative, since kernelParams
reach the extlinux APPEND line.
The display itself is NOT declarative, and that is the uncomfortable part.
This board boots the FIRMWARE's vendor DTB, not the DTBs NixOS builds: the
live device tree carries __symbols__ and mainline's do not, and U-Boot found
no FDTDIR match for compatible "raspberrypi,4-compute-module" so it passed
the firmware's DTB straight through. hardware.deviceTree.overlays therefore
cannot reach the running tree at all, which is also why the earlier fkms-3d
removal fixed a build error without fixing the display.
In the vendor DTB every display node ships disabled, so config.txt needs
`dtoverlay=vc4-kms-v3d,noaudio` and /boot/firmware/overlays/ has to be
populated from raspberrypifw. Three traps in that one line, each of which
failed silently:
- the NixOS sd-image writes the DTBs to the firmware partition but NOT the
overlays, so the directory ships empty and dtoverlay= does nothing
- copying only vc4-kms-v3d.dtbo is insufficient; the firmware remaps that
to vc4-kms-v3d-pi4.dtbo on this board, so the whole directory goes
- without noaudio, vc4_hdmi cannot register its PCM component, returns
-517 (EPROBE_DEFER) forever, and the DRM device never registers, so X
finds no card. We deleted the audio stack anyway.
Result on the machine: card1 is vc4-drm with HDMI-A-1 connected, card2 is
v3d, and X reports
glamor X acceleration enabled on V3D 4.2.14.0
against swrast before. The manual steps are written into the module so the
next person does not rediscover them from a blank screen, but they are lost
on a reflash and belong in the image builder. Follow-up.
With the mainline kernel from the previous commit, the device-tree overlay
step fails outright:
Applying overlay rpi4-cma-overlay
Applying overlay rpi4-vc4-fkms-v3d-overlay
libfdt.FdtException: pylibfdt error -1: FDT_ERR_NOTFOUND
nixos-hardware's fkms-3d applies two overlays that patch nodes present in
the Raspberry Pi VENDOR kernel's DTBs and absent from mainline's. Predicted
when the kernel changed; this is it arriving.
It is also the wrong thing to want. "fkms" is FIRMWARE KMS, the older
arrangement where the VideoCore firmware owns the display and Linux drives
it at arm's length. Mainline does full KMS, and mainline's own
bcm2711-rpi-4-b.dtb already describes the hardware: it carries
brcm,bcm2711-vc5 and brcm,2711-v3d nodes, confirmed by decompiling the DTB
with dtc. The vc4 and v3d drivers bind to those directly, no overlay
involved.
So the overlay was not providing capability, it was translating for a
kernel we no longer use.
hardware.deviceTree.overlays is now empty, so there is nothing left for the
overlay builder to fail on. Verified by evaluating the config.
No replacement needed for videoDrivers either. fkms-3d used to set it as a
side effect, but the shared configuration.nix already declares modesetting,
which is correct for full KMS and is what the x86 machines use. Setting it
again here just produced ["modesetting" "modesetting"].
Still unproven on hardware: whether X comes up on vc4 rather than falling
back to a framebuffer. That is the next thing to read out of
/var/log/X.0.log once the machine boots, and it is now a minutes-long
iteration rather than a kernel compile per attempt.
nixos-hardware's raspberry-pi/4 module mkDefaults boot.kernelPackages to
the Raspberry Pi vendor kernel (linux-rpi, via common/kernel.nix). That
kernel is in no binary cache: Hydra does not build nixos-hardware's
overlay kernels, and it is not in aiolabs.cachix.org either. So every Pi
compiles a kernel from source, on an SD card, and recompiles on every
bump.
Found the hard way during the first Pi 4 bring-up, which spent hours on
building linux-rpi-6.18.39-stable_20260724 (buildPhase):
CC [M] fs/overlayfs/inode.o
before anyone looked closely enough to notice it was not the app. I had
told the operator only the app would build, having checked that Electron
and the Pi firmware were cached and never checked the kernel.
Mainline aarch64 kernels are cached, and mainline demonstrably boots a
Pi 4 — it is what the stock NixOS aarch64 SD image runs, which is how this
machine was bootstrapped in the first place. The vendor kernel's
Pi-specific patches buy nothing this kiosk needs: display, USB serial and
WiFi are all mainline, and vc4/v3d KMS has been mainline for years.
before: linux-rpi-6.18.39-stable_20260724 not cached, hours to build
after: linux-6.12.90 cached, downloads
Verified the config still evaluates, which also clears nixos-hardware's
assertion that the kernel be at least 6.1.
Watch the graphics path. fkms-3d is a nixos-hardware overlay built around
the vendor kernel's firmware-KMS route; the mainline equivalent is full KMS
(vc4-kms-v3d). If X lands on the framebuffer with Electron rendering in
software, that overlay is where to look, not this kernel choice.
rpi5 is left alone deliberately and still carries the vendor kernel, so it
will pay the same cost whenever someone first builds it. Mainline Pi 5
support is younger than Pi 4's and the RP1 southbridge needed vendor
patches for longer, so that one wants its own check rather than the same
change applied on faith.
The Pi 4 twin of the Pi 5 build: `rpi4-installed` (in-place rebuild target),
`rpi4-image`, and `packages.aarch64-linux.{sd-image-rpi4,atm-app-rpi4}`, all
through the board-keyed machinery of the previous commit. The shared runtime
is untouched; only the board pair is new.
deploy/nixos/hardware/raspberry-pi-4.nix mirrors raspberry-pi-5.nix line for
line except where the boards differ:
- KMS for the kiosk display is an opt-in on the Pi 4
(`hardware.raspberry-pi."4".fkms-3d`), which also injects the CMA + vc4
device-tree overlays; the Pi 5 gets it by default. Without it X falls back
to the framebuffer and Electron renders in software.
- fkms-3d sets videoDrivers itself, so the module doesn't.
Everything else — extlinux boot, console pinned to tty0 so the GPIO UART is
free for a validator, no-suspend, the ttyValidator{0,1,2} udev symlinks — is
identical by design.
Evaluation-verified only: rpi4-installed/rpi4-image instantiate, and against
rpi5 they differ solely in the expected places (bcm2711 device tree, the two
fkms overlays, the rpiVersion=4 kernel, no clk-rp1 in initrd, machine model
in the env seed). Not yet booted on hardware; the doc says so.
docs/raspberry-pi-setup.md covers both boards — build, flash, first boot +
provisioning via the spire seed, in-place updates, peripherals — since #87
shipped the Pi 5 without one. It replaces a never-committed Pi 4 sketch
(parked on wip/rpi4-sketch) whose flake wiring didn't evaluate and whose
provisioning section predated the pairing seed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013A6683cCHnQxFUosx1krY4