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
- Live web page showing the most recent scan, updated over Server-Sent Events
GET /api/latest,/api/history,/api/healthJSON 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
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.
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.
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.
- Python 3.8+ (standard library only), or Docker
- Read/write access to the reader's
/dev/hidraw*node
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 triggerThen unplug and replug the reader so the new permissions apply.
Directly:
python3 scanserver.pyWith Docker Compose:
docker compose up -dOpen http://<host>:8090 and present a card.
| 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 |
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--grabso card numbers do not reach the focused window.
These are non-obvious hardware behaviours, all of which fail silently. They are worth reading before modifying the polling code.
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.
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.
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 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.
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.
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.
Created by Jake Thygeson (@jthy10).
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.
MIT -- see LICENSE.