feat(deploy): services.bitspire.nfc.enable — declare the reader per machine
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.
This commit is contained in:
parent
ce80d75f95
commit
bb2ad39628
5 changed files with 137 additions and 95 deletions
|
|
@ -132,6 +132,28 @@ in
|
|||
description = "Camera device";
|
||||
};
|
||||
};
|
||||
|
||||
# Contactless (CCID) card reader for Bolt Card taps — ADR-003.
|
||||
#
|
||||
# Opt-in, and deliberately defaulted off: only some machines have a reader
|
||||
# fitted, and on a machine without one the app must not so much as
|
||||
# initialise nfc-pcsc, because pcsclite busy-spins Electron's main thread
|
||||
# when pcscd is absent (the full story is in nfc-service.ts). Per-model
|
||||
# truth lives in `nfcReaderForModel` in flake.nix, since sintra and tejo
|
||||
# share hardware/upboard.nix but only one of them has a reader.
|
||||
nfc = {
|
||||
enable = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = ''
|
||||
Enable the Bolt Card reader. Starts pcscd, authorises the `bitspire`
|
||||
user to talk to it and to the card via polkit, installs the
|
||||
wedge-recovery unit, and tells the app to initialise NFC at all.
|
||||
Leave false on machines with no reader fitted; cash-out over QR is
|
||||
unaffected either way.
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
|
|
@ -171,6 +193,70 @@ in
|
|||
ELECTRON_DISABLE_GPU=false
|
||||
'';
|
||||
|
||||
# ── Bolt Card reader (services.bitspire.nfc.enable) ─────────────────
|
||||
# Lifted out of hardware/batm3.nix and hardware/upboard.nix so that "is a
|
||||
# reader fitted" is one per-machine flag rather than a block copied into
|
||||
# each hardware file — upboard.nix is shared by sintra (OMNIKEY 5022) and
|
||||
# tejo (no reader), so a hardware file cannot answer the question.
|
||||
|
||||
# pcscd binds the CCID driver to the reader; the app talks to pcscd's
|
||||
# socket via nfc-pcsc rather than the USB device directly. Reader-agnostic
|
||||
# (Feitian KP382 on batm3, HID Global OMNIKEY 5022 on sintra).
|
||||
services.pcscd.enable = mkIf cfg.nfc.enable true;
|
||||
|
||||
# pcscd gates client access via polkit; without a rule the sandboxed
|
||||
# `bitspire` service user is "Rejected unauthorized PC/SC client".
|
||||
# Authorise it to talk to the daemon and the card, and to trigger the
|
||||
# wedge-recovery unit below.
|
||||
security.polkit.extraConfig = mkIf cfg.nfc.enable ''
|
||||
polkit.addRule(function(action, subject) {
|
||||
if ((action.id == "org.debian.pcsc-lite.access_pcsc" ||
|
||||
action.id == "org.debian.pcsc-lite.access_card") &&
|
||||
subject.user == "bitspire") {
|
||||
return polkit.Result.YES;
|
||||
}
|
||||
});
|
||||
polkit.addRule(function(action, subject) {
|
||||
if (action.id == "org.freedesktop.systemd1.manage-units" &&
|
||||
action.lookup("unit") == "nfc-reader-reset.service" &&
|
||||
subject.user == "bitspire") {
|
||||
return polkit.Result.YES;
|
||||
}
|
||||
});
|
||||
'';
|
||||
|
||||
# NFC reader wedge-recovery. A CCID reader (the Feitian R502-CL especially)
|
||||
# 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. This oneshot re-binds the reader's USB device (a software
|
||||
# replug); pcscd + nfc-pcsc then re-detect it on hotplug with no app
|
||||
# restart (verified on-device). The app (unprivileged `bitspire`) starts it
|
||||
# via the polkit rule above when it sees repeated read failures. Matches
|
||||
# the USB CCID interface class (0x0B), so a future reader swap needs no
|
||||
# config change.
|
||||
systemd.services.nfc-reader-reset = mkIf cfg.nfc.enable {
|
||||
description = "Power-cycle a wedged CCID NFC reader (USB re-bind)";
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
ExecStart = pkgs.writeShellScript "reset-nfc-reader" ''
|
||||
set -u
|
||||
found=0
|
||||
for iface in /sys/bus/usb/devices/*:*/bInterfaceClass; do
|
||||
[ -f "$iface" ] || continue
|
||||
[ "$(${pkgs.coreutils}/bin/cat "$iface" 2>/dev/null)" = "0b" ] || continue
|
||||
ifname=$(${pkgs.coreutils}/bin/basename "$(${pkgs.coreutils}/bin/dirname "$iface")")
|
||||
dev=''${ifname%%:*}
|
||||
echo "reset-nfc-reader: power-cycling CCID reader USB device $dev" >&2
|
||||
echo -n "$dev" > /sys/bus/usb/drivers/usb/unbind 2>/dev/null || true
|
||||
${pkgs.coreutils}/bin/sleep 2
|
||||
echo -n "$dev" > /sys/bus/usb/drivers/usb/bind 2>/dev/null || true
|
||||
found=1
|
||||
done
|
||||
[ "$found" = 1 ] || { echo "reset-nfc-reader: no CCID reader found" >&2; exit 1; }
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
# Main ATM service
|
||||
systemd.services.bitspire = {
|
||||
description = "bitSpire ATM Application";
|
||||
|
|
@ -181,6 +267,11 @@ in
|
|||
];
|
||||
wants = [ "network-online.target" ];
|
||||
|
||||
# Read by electron/main.ts. Lives in the unit rather than the .env
|
||||
# EnvironmentFile because .env is only written when absent, so a machine
|
||||
# provisioned months ago would never pick a new value up.
|
||||
environment.BITSPIRE_NFC_ENABLED = boolToString cfg.nfc.enable;
|
||||
|
||||
serviceConfig = {
|
||||
Type = "simple";
|
||||
User = "bitspire";
|
||||
|
|
|
|||
|
|
@ -85,65 +85,10 @@
|
|||
cpuFreqGovernor = "performance";
|
||||
};
|
||||
|
||||
# PC/SC daemon for the Feitian KP382 contactless reader (096e:0608, a CCID
|
||||
# smart-card reader) used for Bolt Card tap-to-pay on cash-out. Enabling it
|
||||
# binds the CCID driver to the reader; the app talks to pcscd's socket (via
|
||||
# nfc-pcsc) rather than the USB device directly. Harmless if no reader is
|
||||
# attached — pcscd just idles.
|
||||
services.pcscd.enable = true;
|
||||
|
||||
# pcscd gates client access via polkit; without a rule the sandboxed
|
||||
# `bitspire` service user is "Rejected unauthorized PC/SC client". Authorize
|
||||
# it to talk to the daemon and the card. The second rule lets the app trigger
|
||||
# the NFC reader wedge-recovery service (see nfc-reader-reset below).
|
||||
security.polkit.extraConfig = ''
|
||||
polkit.addRule(function(action, subject) {
|
||||
if ((action.id == "org.debian.pcsc-lite.access_pcsc" ||
|
||||
action.id == "org.debian.pcsc-lite.access_card") &&
|
||||
subject.user == "bitspire") {
|
||||
return polkit.Result.YES;
|
||||
}
|
||||
});
|
||||
polkit.addRule(function(action, subject) {
|
||||
if (action.id == "org.freedesktop.systemd1.manage-units" &&
|
||||
action.lookup("unit") == "nfc-reader-reset.service" &&
|
||||
subject.user == "bitspire") {
|
||||
return polkit.Result.YES;
|
||||
}
|
||||
});
|
||||
'';
|
||||
|
||||
# NFC reader wedge-recovery. The Feitian R502-CL CCID reader (and, less often,
|
||||
# any CCID reader) 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. This oneshot re-binds the reader's USB device (a
|
||||
# software replug); pcscd + nfc-pcsc then re-detect it on hotplug with no app
|
||||
# restart (verified on-device). The app (unprivileged `bitspire`) starts it via
|
||||
# the polkit rule above when it sees repeated read failures. Reader-agnostic:
|
||||
# it matches the USB CCID interface class (0x0B), so it also covers a future
|
||||
# ACR1252U swap without a config change.
|
||||
systemd.services.nfc-reader-reset = {
|
||||
description = "Power-cycle a wedged CCID NFC reader (USB re-bind)";
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
ExecStart = pkgs.writeShellScript "reset-nfc-reader" ''
|
||||
set -u
|
||||
found=0
|
||||
for iface in /sys/bus/usb/devices/*:*/bInterfaceClass; do
|
||||
[ -f "$iface" ] || continue
|
||||
[ "$(${pkgs.coreutils}/bin/cat "$iface" 2>/dev/null)" = "0b" ] || continue
|
||||
ifname=$(${pkgs.coreutils}/bin/basename "$(${pkgs.coreutils}/bin/dirname "$iface")")
|
||||
dev=''${ifname%%:*}
|
||||
echo "reset-nfc-reader: power-cycling CCID reader USB device $dev" >&2
|
||||
echo -n "$dev" > /sys/bus/usb/drivers/usb/unbind 2>/dev/null || true
|
||||
${pkgs.coreutils}/bin/sleep 2
|
||||
echo -n "$dev" > /sys/bus/usb/drivers/usb/bind 2>/dev/null || true
|
||||
found=1
|
||||
done
|
||||
[ "$found" = 1 ] || { echo "reset-nfc-reader: no CCID reader found" >&2; exit 1; }
|
||||
'';
|
||||
};
|
||||
};
|
||||
# The Feitian KP382 contactless reader (096e:0608) is declared as a machine
|
||||
# capability, not here: `nfcReaderForModel` in flake.nix drives
|
||||
# services.bitspire.nfc.enable, which owns pcscd, the polkit rules and the
|
||||
# wedge-recovery unit (deploy/nixos/bitspire-atm.nix).
|
||||
|
||||
# Disable suspend/hibernate for kiosk
|
||||
systemd.targets = {
|
||||
|
|
|
|||
|
|
@ -91,26 +91,9 @@
|
|||
cpuFreqGovernor = "performance";
|
||||
};
|
||||
|
||||
# PC/SC daemon for the HID Global OMNIKEY 5022 contactless reader
|
||||
# (076b:5022, a CCID smart-card reader) used for Bolt Card tap-to-enter
|
||||
# (ADR-003). pcscd binds the CCID driver; the app talks to pcscd's socket
|
||||
# (via nfc-pcsc) rather than the USB device directly. Device-agnostic —
|
||||
# same wiring as batm3's Feitian KP382; harmless if no reader is attached,
|
||||
# pcscd just idles. Shared by every upboard machine (sintra, tejo).
|
||||
services.pcscd.enable = true;
|
||||
|
||||
# pcscd gates client access via polkit; without a rule the sandboxed
|
||||
# `bitspire` service user is "Rejected unauthorized PC/SC client". Authorize
|
||||
# it to talk to the daemon and the card.
|
||||
security.polkit.extraConfig = ''
|
||||
polkit.addRule(function(action, subject) {
|
||||
if ((action.id == "org.debian.pcsc-lite.access_pcsc" ||
|
||||
action.id == "org.debian.pcsc-lite.access_card") &&
|
||||
subject.user == "bitspire") {
|
||||
return polkit.Result.YES;
|
||||
}
|
||||
});
|
||||
'';
|
||||
# No pcscd here. This file is shared by sintra (HID Global OMNIKEY 5022
|
||||
# fitted) and tejo (no reader), so the reader is declared per model via
|
||||
# `nfcReaderForModel` in flake.nix → services.bitspire.nfc.enable.
|
||||
|
||||
# Disable suspend/hibernate for kiosk
|
||||
systemd.targets = {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue