From 73376a6c68b79bdd7ba57ea19a1b399c159dcad2 Mon Sep 17 00:00:00 2001 From: Patrick Mulligan Date: Wed, 5 Aug 2026 20:46:18 +0200 Subject: [PATCH] fix(machine): cooldown after failed NFC read to prevent reader wedge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- apps/machine/electron/nfc-service.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/apps/machine/electron/nfc-service.ts b/apps/machine/electron/nfc-service.ts index 970d17d..ca9ace1 100644 --- a/apps/machine/electron/nfc-service.ts +++ b/apps/machine/electron/nfc-service.ts @@ -155,22 +155,28 @@ export async function startNfcReader( r.autoProcessing = false 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 () => { + if (Date.now() < cooldownUntil) return onStatus({ state: 'reading', reader: name }) - // Single attempt: hammering a flaky RF link with retries wedges these - // cheap CCID readers. A read is a few APDU round-trips; if the card - // shifts mid-read the transmit fails and the user simply re-taps. + // Single attempt: retrying hammers a flaky RF link. A read is a few APDU + // round-trips; if the card shifts mid-read the transmit fails and the + // user simply re-taps. try { const lnurlw = await readNdefLnurlw((apdu, maxLen) => r.transmit(apdu, maxLen)) if (lnurlw) { onCard(lnurlw) - } else { - onStatus({ state: 'error', reader: name, message: 'not a Bolt Card' }) + return } + onStatus({ state: 'error', reader: name, message: 'not a Bolt Card' }) } catch (e) { onStatus({ state: 'error', reader: name, message: 'card read failed — hold steady & retap' }) void e } + cooldownUntil = Date.now() + 1500 }) r.on('card.off', () => onStatus({ state: 'card-removed', reader: name })) r.on('error', (err: unknown) =>