fix(nfc): make the Bolt Card reader an opt-in machine capability #115

Merged
padreug merged 2 commits from fix/nfc-opt-in into dev 2026-09-29 20:51:37 +00:00
Owner

The douro has been hard-wedged since it was flashed, and this is why.

nfc-pcsc's pcsclite binding does not fail when pcscd isn't running — it retries SCardEstablishContext in a tight loop on the calling thread, which is Electron's main thread. Measured on the douro: 76,011 failing newfstatat("/run/pcscd/pcscd.comm") in 6 seconds, main thread pinned at its 80% CPUQuota ceiling for 11 hours, renderer a zombie that was never reaped, nothing in the journal after [StateStore], and SIGTERM ignored so systemctl stop had to time out and SIGKILL. The watchdog couldn't help: all three layers need the event loop that's blocked.

pcscd was enabled in hardware/batm3.nix and hardware/upboard.nix, which can't express "is a reader fitted" — upboard.nix is shared by sintra (OMNIKEY 5022) and tejo (nothing fitted), so tejo inherited pcscd it has no use for, while the douro, with its own hardware file, got none and wedged.

Two commits:

  1. Check for the pcscd socket before touching the binding. Self-contained — this alone unwedges the douro, and it covers pcscd dying at runtime on a machine that does have a reader.
  2. services.bitspire.nfc.enable owns pcscd, the polkit rules and the wedge-recovery unit, and hands the app BITSPIRE_NFC_ENABLED so it doesn't initialise nfc-pcsc at all without a reader. Per-model truth is nfcReaderForModel in flake.nix, next to fiatCodeForModel and upgradeWindowForModel. batm3 and sintra true; douro and tejo flip when readers are fitted.

The flag goes through the unit's Environment, not /var/lib/bitspire/.env — .env is only written when absent, so an already-provisioned machine would never pick up a new value.

Verified: pcscd/BITSPIRE_NFC_ENABLED/nfc-reader-reset evaluate true for batm3-installed and sintra-installed, false for douro-installed and tejo-installed. tsc clean on the electron tsconfig, nfc-service tests pass, prettier clean.

Behaviour is unchanged on batm3 and sintra, but note batm3 can't pick this up yet — its nightly upgrade has been failing since Sep 24, filed separately.

The douro has been hard-wedged since it was flashed, and this is why. `nfc-pcsc`'s pcsclite binding does not fail when pcscd isn't running — it retries `SCardEstablishContext` in a tight loop on the calling thread, which is Electron's main thread. Measured on the douro: 76,011 failing `newfstatat("/run/pcscd/pcscd.comm")` in 6 seconds, main thread pinned at its 80% CPUQuota ceiling for 11 hours, renderer a zombie that was never reaped, nothing in the journal after `[StateStore]`, and SIGTERM ignored so `systemctl stop` had to time out and SIGKILL. The watchdog couldn't help: all three layers need the event loop that's blocked. pcscd was enabled in `hardware/batm3.nix` and `hardware/upboard.nix`, which can't express "is a reader fitted" — `upboard.nix` is shared by sintra (OMNIKEY 5022) and tejo (nothing fitted), so tejo inherited pcscd it has no use for, while the douro, with its own hardware file, got none and wedged. Two commits: 1. Check for the pcscd socket before touching the binding. Self-contained — this alone unwedges the douro, and it covers pcscd dying at runtime on a machine that does have a reader. 2. `services.bitspire.nfc.enable` owns pcscd, the polkit rules and the wedge-recovery unit, and hands the app `BITSPIRE_NFC_ENABLED` so it doesn't initialise nfc-pcsc at all without a reader. Per-model truth is `nfcReaderForModel` in flake.nix, next to `fiatCodeForModel` and `upgradeWindowForModel`. batm3 and sintra true; douro and tejo flip when readers are fitted. The flag goes through the unit's `Environment`, not `/var/lib/bitspire/.env` — .env is only written when absent, so an already-provisioned machine would never pick up a new value. Verified: `pcscd`/`BITSPIRE_NFC_ENABLED`/`nfc-reader-reset` evaluate true for batm3-installed and sintra-installed, false for douro-installed and tejo-installed. tsc clean on the electron tsconfig, nfc-service tests pass, prettier clean. Behaviour is unchanged on batm3 and sintra, but note batm3 can't pick this up yet — its nightly upgrade has been failing since Sep 24, filed separately.
nfc-pcsc's pcsclite binding does not fail when pcscd is not running — it
retries SCardEstablishContext in a tight loop on the calling thread, which
here is Electron's main thread. ~12k stat()s a second on
/run/pcscd/pcscd.comm, event loop dead: the window never paints, the
renderer is never reaped, and the watchdog can't fire because it needs the
same event loop. The douro sat like that for 11 hours at 80% CPU (its
CPUQuota ceiling), ignoring SIGTERM, with nothing in the journal after
[StateStore].

Check the socket exists before touching the binding. This is what makes
the "best-effort, every failure swallowed into a status callback" contract
in the module header true, and it also covers pcscd dying at runtime on a
machine that does have a reader.
pcscd was enabled in hardware/batm3.nix and hardware/upboard.nix, which
cannot express "is a reader fitted": upboard.nix is shared by sintra (HID
Global OMNIKEY 5022) and tejo (nothing fitted), so tejo inherited pcscd it
has no use for, while the douro — with its own hardware file — got none and
wedged on every boot.

Make it a machine capability instead. services.bitspire.nfc.enable owns
pcscd, the two polkit rules and the wedge-recovery unit, and hands the app
a BITSPIRE_NFC_ENABLED flag so it doesn't initialise nfc-pcsc at all on a
machine with no reader. Per-model truth lives in nfcReaderForModel in
flake.nix next to fiatCodeForModel and upgradeWindowForModel, since a
shared hardware file can't answer the question. batm3 and sintra are true;
douro and tejo flip to true when readers are fitted.

The flag goes through the unit's Environment rather than
/var/lib/bitspire/.env, because .env is only written when absent — a
machine provisioned months ago would never pick up a new value.
padreug deleted branch fix/nfc-opt-in 2026-09-29 20:51:38 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
aiolabs/bitspire!115
No description provided.