Skip to content

About

Read contactless ID cards from an RFIDeas pcProx reader and expose the results as a live web page and a small JSON API.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

1 Commit

Folders and files

Repository files navigation

ID Card Reader w/ Data Output

Read contactless ID cards from an RFIDeas pcProx reader and expose the results as a live web page and a small JSON API.

The reader is a USB HID device. In its default mode it emulates a keyboard and simply types the card number into whatever window has focus, which is awkward to consume programmatically. Many readers are also shipped with keystroke output disabled ("SDK mode"), in which case they read cards but type nothing at all.

This project talks to the reader over its vendor HID feature-report channel instead, so it works in both cases and never leaks card numbers into whatever application happens to be focused.

   card  ->  pcProx reader  ->  HID feature reports  ->  scanserver.py
                                                             |
                                             live web page + JSON API

Features

  • Live web page showing the most recent scan, updated over Server-Sent Events
  • GET /api/latest, /api/history, /api/health JSON endpoints
  • Scans appended to scans.jsonl
  • Decodes bit length, raw hex, decimal, and facility code / card number
  • Survives the reader being unplugged and replugged
  • No third-party dependencies -- Python standard library only

Compatibility

Communication uses the pcProx vendor feature-report protocol, common to the RFIDeas pcProx family (USB vendor ID 0x0c27).

Device Status
RFIDeas pcProx RDR-6082AKU (125 kHz) Verified -- firmware 17.6.3, device type 0x0004, USB 0c27:3bfa
Other RFIDeas pcProx RDR-60xx (125 kHz) Expected to work; untested
RFIDeas pcProx Plus RDR-80xx (dual-frequency) Expected to work; untested
Readers from other vendors Not supported -- different protocol entirely

If you test a model not listed here, a report either way is welcome.

Card formats

The decoder is generic: it reads the bit count reported by the device and decodes whatever length comes back, so any card the hardware can read will be displayed with its raw hex, bit count and decimal value.

The facility-code / card-number split is format-specific. Currently decoded:

Format Bits Layout
HID H10301 26 1 parity, 8-bit facility code, 16-bit card number, 1 parity
HID D10202 33 1 parity, 7-bit facility code, 24-bit card number, 1 parity

Other bit lengths still display correctly; they just aren't split into facility/card fields.

Frequency is a hardware limit

A 125 kHz reader cannot read a 13.56 MHz card (MIFARE, DESFire, iCLASS, transit cards) at all, regardless of configuration -- they are different radios, not different formats. You need a dual-frequency reader for those.

Note also that on a modern 13.56 MHz credential, any pcProx returns only the card's CSN/UID. Protected application data is encrypted under keys held by the card issuer and is not readable by off-the-shelf hardware.

Requirements

  • Python 3.8+ (standard library only), or Docker
  • Read/write access to the reader's /dev/hidraw* node

Setup

Both device nodes are root-owned by default. Install the udev rule to grant access to the plugdev group:

sudo cp udev/60-rfideas-pcprox.rules /etc/udev/rules.d/
sudo udevadm control --reload-rules && sudo udevadm trigger

Then unplug and replug the reader so the new permissions apply.

Running

Directly:

python3 scanserver.py

With Docker Compose:

docker compose up -d

Open http://<host>:8090 and present a card.

Configuration

Variable Default Meaning
BADGE_PORT 8090 HTTP port
BADGE_POLL_INTERVAL 0.5 Seconds between card-buffer polls. Do not lower -- see below
BADGE_RETRIGGER_GAP 3.0 Seconds before a resting card counts as a new presentation
BADGE_STORE /app/scans.jsonl Where scans are appended
BADGE_DEVICE (auto) Explicit /dev/hidraw* path

Other tools

  • pcprox.py -- low-level access. info (descriptors only, sends nothing), get (read the current feature report), cmd (send explicit bytes).
  • readbadge.py -- reads a reader that is configured for keystroke output, by decoding its evdev keystrokes instead. Use --grab so card numbers do not reach the focused window.

Operating notes

These are non-obvious hardware behaviours, all of which fail silently. They are worth reading before modifying the polling code.

The reader cannot be polled fast

The reader cannot service continuous HID feature-report traffic and scan the RF field at the same time. Polling the card buffer at 10 Hz stops it detecting cards completely -- no beep, no LED change, no data, and no error.

A card read only stays in the buffer for roughly 1 second, so it cannot be polled arbitrarily slowly either. The 0.5s default sits between those bounds.

Some commands disable the reader until it is physically unplugged

scanserver.py sends only 0x8f and 0x8e (the two card-read commands). Do not add any of these:

Command Effect
0x80 0x81 0x82 Selecting a config page puts the reader into configuration mode, where it stops reading cards
0x8a "Device info" behaves the same way -- silently kills card detection
0x90 "Finish configuration". Sent when the device is not in config mode, it loads a blank config into RAM and card reading dies

The RAM configuration is only reloaded from EEPROM at power-on. A USB-level reset (USBDEVFS_RESET) therefore does not recover the reader, because the device never loses power. Only physically unplugging it does.

Symptom: the reader stops beeping and its LED stops changing colour.

Stale file descriptors return zeros instead of failing

If the reader is unplugged and replugged, an already-open /dev/hidraw* handle goes stale, and ioctls against it keep succeeding while returning all zeros rather than erroring. Every scan then vanishes with no error anywhere.

The poll loop guards against this by comparing the USB devnum from sysfs every 2 seconds and reconnecting when it changes. Using sysfs rather than a device command costs no USB traffic, which matters given the polling constraint above.

A resting card is re-read repeatedly

A card left sitting on the reader is re-read roughly every 1.7 seconds, and the buffer goes empty in between -- so "buffer empty" does not mean "card removed". BADGE_RETRIGGER_GAP treats repeat sightings of one card as a single presentation. Presenting a different card is never suppressed.

Troubleshooting

GET /api/health reports polls, seconds_since_last_poll, seconds_since_last_scan and uptime. A climbing polls count proves the loop is alive, which distinguishes a stalled reader from one that simply has not been presented a card.

If the reader has stopped beeping and changing colour, it is in the wedged state described above: unplug it, wait a few seconds, and plug it back in.

Privacy

Card numbers are credentials. scans.jsonl contains real credential numbers and is excluded by .gitignore -- keep it that way, and do not paste scan output into public issue reports without redacting it.

Author

Created by Jake Thygeson (@jthy10).

Acknowledgements

The pcProx feature-report protocol is not publicly documented by the vendor. The command set used here was derived from the protocol documentation in the python-pcprox project by Michael Farrell, which reverse-engineered it. That project is licensed GPL-3.0; no code from it is used here, but the protocol documentation made this possible.

RFIDeas and pcProx are trademarks of their respective owner. This project is neither affiliated with nor endorsed by them.

License

MIT -- see LICENSE.

About

Read contactless ID cards from an RFIDeas pcProx reader and expose the results as a live web page and a small JSON API.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Contributors

Languages