fix(machine): cooldown after failed NFC read to prevent reader wedge

Hammering a flaky CCID reader with rapid re-reads wedges it into a
present↔empty storm (only a USB replug clears it). After a failed read,
ignore card re-detections for 1.5s; successful reads don't cool down.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Patrick Mulligan 2026-08-05 20:46:18 +02:00
commit 73376a6c68

View file

@ -155,22 +155,28 @@ export async function startNfcReader(
r.autoProcessing = false r.autoProcessing = false
onStatus({ state: 'ready', reader: name }) onStatus({ state: 'ready', reader: name })
// Cooldown after a failed read: these cheap CCID readers can get wedged into
// a present↔empty storm when hammered, so ignore re-detections for a beat
// after a failure. Successful reads don't cool down.
let cooldownUntil = 0
r.on('card', async () => { r.on('card', async () => {
if (Date.now() < cooldownUntil) return
onStatus({ state: 'reading', reader: name }) onStatus({ state: 'reading', reader: name })
// Single attempt: hammering a flaky RF link with retries wedges these // Single attempt: retrying hammers a flaky RF link. A read is a few APDU
// cheap CCID readers. A read is a few APDU round-trips; if the card // round-trips; if the card shifts mid-read the transmit fails and the
// shifts mid-read the transmit fails and the user simply re-taps. // user simply re-taps.
try { try {
const lnurlw = await readNdefLnurlw((apdu, maxLen) => r.transmit(apdu, maxLen)) const lnurlw = await readNdefLnurlw((apdu, maxLen) => r.transmit(apdu, maxLen))
if (lnurlw) { if (lnurlw) {
onCard(lnurlw) onCard(lnurlw)
} else { return
onStatus({ state: 'error', reader: name, message: 'not a Bolt Card' })
} }
onStatus({ state: 'error', reader: name, message: 'not a Bolt Card' })
} catch (e) { } catch (e) {
onStatus({ state: 'error', reader: name, message: 'card read failed — hold steady & retap' }) onStatus({ state: 'error', reader: name, message: 'card read failed — hold steady & retap' })
void e void e
} }
cooldownUntil = Date.now() + 1500
}) })
r.on('card.off', () => onStatus({ state: 'card-removed', reader: name })) r.on('card.off', () => onStatus({ state: 'card-removed', reader: name }))
r.on('error', (err: unknown) => r.on('error', (err: unknown) =>