Hardware bridge · Fleet deployment

Making a badge reader work in a browser that can't see it

A contactless badge reader is a solved problem right up until you want a web page to read it. The browser has no general access to the card stack, the desktop is centrally managed and nobody is going to walk to each one, and the kiosk sits untouched from Friday to Monday. Most of the engineering here went not into reading a card, but into the reader still working on Monday morning.

An estimated $41,731 a year in labour time saved, by the site's own tracker. Where the figure comes from

450 ms kiosk poll interval
3 independent restart paths
8 power settings pinned per machine
Confidential client & operation

What it saves

$41,731 a year, estimated

The site's efficiency-savings tracker timed this task by hand and again with the software. The labour time saved comes to an estimated $41,731 a year.

TaskSaved a year
Badge sign-in for meetings$41,731

Meeting sign-in counts the whole room: nobody starts until the last person is signed in, so every attendee waits out the line.

Estimated by the site's own efficiency-savings tracker: each task timed by hand and again with the software, the time saved valued at a fully loaded labour rate over a 252-day working year. Labour time only, to the whole dollar.

Built for the badge-driven kiosk screens of a contract-logistics site, running on centrally managed Windows desktops that the application team could not configure by hand.

Badge reader to browser: a loopback relay that restarts itself when the reader wedgesA sequence diagram with three lifelines: the badge reader on the USB card stack, the local relay service we built, and the kiosk page in the browser. The relay keeps a continuous status-change wait open so the reader's field stays powered through idle, and makes a direct read on every pass so a quick pull-and-replace is caught. A presented card becomes one identifier, which expires after 30 seconds on a monotonic clock. The kiosk page polls the relay over loopback HTTP every 450 milliseconds for the next scan and separately for health, which reports the age of the reader loop's heartbeat in milliseconds. An unknown badge opens a register-it dialog and alerts leadership, throttled to once per badge per 12 hours. The lower block shows the failure: the reader thread blocked inside an uninterruptible card-stack call while the old health check still answered 200; now a watchdog thread that never touches card hardware watches the heartbeat and, past the timeout, the process relaunches itself detached and exits, retrying its port bind 10 times. Three independent restart paths exist and none fires until a reader has been seen this session. The footer lists how it ships to the fleet as a managed Win32 app.LOOPBACK HTTP · THE BROWSER POLLS, THE RELAY READSBadge readerUSB · PC/SC card stackLocal relay serviceone windowless executableKiosk page in the browserhas no access to the card stackcontinuous status-change waitkeeps the reader's field powered through a long idlecard presented: identifier reada direct read each pass catches a quick pull-and-replaceone scan per presentation · expires after 30 s on amonotonic clockGET next-scan · every 450 msloopback HTTP: no browser flag, no per-machine setupidentifierunknown badge: a register-it dialog on the kiosk, andleadership alerted, throttled to 12 h per badgeGET healthanswers 200 with the heartbeat age in msWindows suspends idle USB devices, and asuspended reader still enumerates: beingplugged in proves nothingWHEN THE READER STOPS ANSWERINGTHE OLD FAILURENOWReader thread blocksinside an uninterruptiblecard-stack callHealth still said 200the old check proved only thatthe process was aliveHeartbeat every loopa watchdog thread that nevertouches card hardware watchesthe stampRelaunch detached, exitpast the timeout; the port bind isretried 10 times3 independent restart pathsloop recoverywatchdogdeployment task restart-on-failurenone fires until a reader has been seen this sessionSHIPPED TO THE FLEETships as a managed Win32 applogon task runs it as the signed-in user8 power settings pinned on every power schemedry-run and revert
The browser cannot see the reader, so a local relay reads the card stack and the kiosk page polls it over loopback HTTP every 450 ms. A heartbeat watched from outside the reader thread restarts the relay when the reader stops answering.

How does a browser kiosk read a badge it cannot see?

A browser kiosk reads a badge through a small local service. Browsers have no general access to the smartcard stack, so a relay on each Windows desktop holds the reader open and hands card IDs to the kiosk page over loopback. A heartbeat watchdog restarts it when the reader wedges, and Intune installs it across the fleet.

Kiosk screens on the floor identify people by badge. The card is a contactless smartcard, the reader is a USB device on a Windows desktop, and the thing that needs the number is a page in a browser. Browsers do not get to talk to that hardware. One browser engine has since added a smartcard API, but switching it on needs a flag or an enterprise policy applied per machine — a per-machine dependency in a place where per-machine anything is the thing you are trying to avoid.

Then the reader started failing, and the way it failed was worse than not working at all. It would read badges for days and then quietly stop, while the service's own health check kept answering normally. Separately, after a long idle stretch — a weekend with nobody at the kiosk — it would come back dead and stay dead until somebody physically unplugged it and plugged it back in.

  • The browser cannot reach the card stack without a flag or policy set on each machine
  • A contactless card needs a beat in the field before it answers; the first read after a tap can land while it is still coupling
  • Polling the reader by connecting to it lets the antenna power down between taps, so the next tap is missed entirely
  • A fast pull-and-replace produces no card-present event, because the empty state in between is never sampled
  • Windows suspends idle USB devices, and the reader frequently fails to re-enumerate on resume
  • A user-space process cannot force a USB re-enumeration without administrator rights

What we built

A small local service on the desktop, the kind of Windows services and local hardware work a browser cannot do alone. It holds the card stack open, reads card identifiers, and exposes them on loopback with two endpoints: one that reports health, one that hands over the next scan. The kiosk page polls it roughly twice a second. The server also carries proxy routes to the relay behind an authenticated session — documented as a way around browser cross-origin restrictions — with a two-second timeout, so a relay that is not answering returns a 503 rather than hanging the page waiting on it, which is what integration that holds up when the other end is down means at the scale of one kiosk.

The reading loop took the longest to get right, and every decision in it came from a symptom on the real hardware rather than from the documentation. Connect-polling was replaced by a continuous status-change wait after it turned out that only the latter keeps the reader's field powered through a long idle. The direct read was then put back on every pass, because an event-only loop silently drops a card that is pulled and replaced too quickly to register as absent.

An unregistered badge is handled twice over. The kiosk opens a register-it dialog for whoever is standing there, and independently alerts site leadership — because the person who tapped may well walk away from the dialog. That alert is throttled to once per badge per twelve hours so a badge left sitting on a reader cannot flood a mailbox, and it is dropped entirely if the badge turns out to have been registered on another reader in the meantime.

  • One scan per presentation, keyed off how long the card was genuinely absent rather than a fixed timer — a fob resting on the reader stays one scan, a deliberate lift-and-replace scans again
  • Scans expire after thirty seconds, timed on a monotonic clock so a system clock change cannot make a stale badge look fresh
  • Name lookup and desktop notification run on their own bounded queue; when it fills, the notification is dropped rather than stalling card reading
  • Magnetic-track wrappers are stripped on every input path, so one card yields one identifier however it was read
  • Check-in is guarded by a pre-check and a uniqueness constraint, and the constraint violation is reported as a duplicate rather than an error
  • One route is deliberately left unauthenticated — the one the relay itself calls to put a name to a badge — with the reasoning written beside it

Why the health check said 200 while badges stopped reading

The symptom reported from the floor was precise, and damning once you sit with it: badges stop being read, and the health endpoint keeps returning 200. That is exactly what you would expect. The web thread was fine. The reader thread was blocked inside a synchronous card-stack call that cannot be interrupted from Python, and a health check that only proves the process is alive proves nothing about the one function anybody cares about.

The service's own recovery could not catch it either. That check sat after the blocking call, and it keyed off whether the reader was still enumerated — and a suspended reader still enumerates.

So the reader loop stamps a heartbeat at the top of every iteration, and a second thread that never touches card hardware watches that timestamp. If it stops advancing past the timeout, the process relaunches itself detached and exits, which from user space is the only reliable way to get a clean card stack back. The age of that heartbeat is also published on the health endpoint in milliseconds, so the failure is now visible from outside instead of merely inferable.

  • Three independent paths bring the relay back: the reader loop's own recovery, the out-of-thread watchdog, and the deployment task's restart-on-failure
  • Both in-process paths refuse to fire until a reader has actually been seen this session, so a desktop with nothing plugged in cannot storm restarts
  • If the replacement process cannot be spawned, the timer resets rather than hot-looping on the attempt
  • The replacement retries its port bind ten times, because the instance it is replacing may still hold the port for a moment
  • The service asks Windows to stay out of idle standby while it runs, with a documented switch to give that up and a note on exactly what you lose by setting it

Three independent ways the relay comes back

Restart pathWhat it catchesIts guard
The reader loop's own recoveryA reader that stops enumeratingFires only after a reader has been seen this session
The out-of-thread watchdogA reader thread blocked inside a card-stack call, seen as a heartbeat that stops advancingFires only after a reader has been seen; a failed spawn resets the timer rather than hot-looping
The deployment taskA relay process that exitsRestart-on-failure in the logon task, running as the signed-in user with least privilege

Shipping it to an Intune-managed fleet

One desktop is a script. A fleet is a packaging problem, and the answer was the same Intune enrollment and Win32 app deployment the desktops already lived under. The relay builds into a single windowed executable and ships as a managed Win32 application: the installer runs with machine privilege, drops the payload, and registers a logon task that runs the relay as whichever ordinary user is signed in, with least privilege. It has to be the interactive session — desktop notifications cannot be raised from the service session, and the browser reaches the relay on the user's own loopback address.

Version is one constant in the relay source. The build script reads it from there and prints the exact value the deployment detection rule has to be set to, and the health endpoint reports the same constant at runtime. Detection rules like that one are routine once a fleet is managed this way; the guide to moving device management to Intune covers getting there.

Idle suspend is a machine setting rather than an application one, so it is a separate elevated script. It pins eight power settings across the USB, PCI Express, sleep and disk groups on every power scheme on the box — every scheme, so switching plans cannot quietly undo it — unticks the Device Manager checkbox that lets Windows power down every USB hub and smart card reader on the machine, with a switch to narrow that to just the readers and the hub chain above them, and sets the smartcard service to start automatically.

  • Everything goes through supported interfaces — the power CLI, the management class behind the Device Manager checkbox, the service manager — and never by writing device registry keys directly
  • A dry-run switch prints every change and makes none
  • The first apply records a baseline file; revert restores only what that file records, and says loudly when it has to fall back to documented defaults instead
  • Drivers that do not implement the power checkbox are counted as unsupported and left alone rather than forced
  • Uninstall is idempotent, so a re-run against a half-removed machine still succeeds
  • The build fails hard if the virtual environment is not the pinned interpreter version — it had silently built against the wrong one once already
  • The known limitation is documented rather than hidden: the fixed loopback port means two simultaneous interactive sessions would collide

The result

The kiosk reads real badges with no browser flag, no per-machine setup and no site visit. When the reader wedges, the relay notices and restarts itself. When the machine would have powered the reader down, the settings that allowed it are already off. When a new build ships, the fleet detects it and takes it.

The transferable lesson here is about health checks. The original endpoint returned 200 whenever the process was alive, which is the default thing to build and is worse than useless — it produced confident green status while the only function that mattered had stopped. A health check is only worth having if it can report the specific failure you are actually afraid of.

Questions about this build

Why not use the browser's own smartcard API?

One browser engine has added a smartcard API, but switching it on takes a flag or an enterprise policy set on each machine, which is the per-machine dependency a managed kiosk fleet is trying to avoid. A loopback relay needs nothing from the browser: the kiosk page polls it about twice a second, and the server's authenticated proxy routes answer a 503 within two seconds when the relay is down.

Why does a USB badge reader stop working after a weekend?

Windows suspends idle USB devices, and the reader often fails to re-enumerate on resume, so it stays dead until somebody unplugs it. A user-space process cannot force a re-enumeration without administrator rights, so the fix is a machine setting: an elevated script pins eight power settings on every power scheme, unticks the power-saving checkbox for USB hubs and readers, and records a baseline so revert restores only what it changed.

Built with

  • Python
  • PC/SC smartcard stack
  • Flask
  • PyInstaller
  • PowerShell
  • Microsoft Intune

On the details we left out

Client, location, system names and volumes are withheld deliberately; the savings above are the tracker's dollar estimates, with the minutes, rates and headcount behind them left out. We're happy to go deeper on architecture and approach on a call.

Services this draws on

System Integration

Connect aging systems to modern APIs, data platforms, and SaaS tools.

See System Integration →

Custom Software & Web Development

Websites, internal tools, APIs, and automation for teams without a dev department.

See Custom Software & Web Development →

Enterprise IT Services

Managed and co-managed IT across Texas with 24/7 support, from a ten-person office up — Active Directory to the printer that keeps dropping offline.

See Enterprise IT Services →

More projects

Optimization · Industrial hardware

Making eight label printers stop locking up

Label printers wedged under load and needed power-cycling. The fix was understanding what the hardware actually does — and sending it about 150 times less data.

Read the story →
Operations platform · Built for ourselves

One app that runs the whole company, and keeps score

The Do-Engine is the operations app we built to run our own business: a task board, a client book, a document vault, invoicing, a help desk and a mail client in one page, with a single XP ledger scoring everything that gets finished.

Read the story →
Platform · Multi-year build

One application that replaced a decade of spreadsheets

A warehouse ran its floor on shared spreadsheets, emailed reports, and tribal knowledge. We replaced all of it with a single internal platform — and kept it running while people worked.

Read the story →
Get in touch

Is there a device your web app cannot reach?

Name the reader, scale or printer and the desktop it sits on. Talk to an engineer.

Prefer to talk?

Call 832-598-8234 or email msco@stoneagesoftware.com. Houston, Texas — serving Houston, The Woodlands, Conroe, Sugar Land, Katy, Pearland, and the Greater Houston metro.

What happens next

We read your message, an engineer replies with questions or a straight answer, and if a call would help, we book one. From the first reply on, you're talking to the people who'd do the work.

No obligation — we reply within one business day. This form sends your details to Stone Age Software LLC so we can respond to you. See our privacy policy for how we handle them. This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.