Skip to content

Latest commit

Β 

History

19 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

🏷️ RFID Reader App

Desktop application for USB RFID readers β€” 13.56 MHz, ISO14443A, S50/S70. Reads contactless cards, keeps a card database with permissions, and logs every scan.

CI Release License: MIT Electron React

πŸ‡΅πŸ‡± Wersja polska


πŸ”Œ How the reader works β€” read this first

Readers of this class (ProRock / Sycreader β€œSYC ID&IC USB Reader”, VID 0xFFFF / PID 0x0035, and most 13.56 MHz USB readers sold as β€œOTG”) have no driver, no serial port and no PC/SC API. They register as a USB HID keyboard: when a card comes close, the reader types its number character by character and usually appends Enter.

This app therefore captures keyboard events β€” nothing to install, no USB permissions to grant. Practical consequences:

  • works identically on macOS, Windows and Linux;
  • a scan only reaches the app while its window has focus;
  • the same card arrives in different shapes depending on the reader's hardware mode (see below).

Not supported: 125 kHz cards (EM4100 and friends) β€” a 13.56 MHz reader cannot read them, and no software can change that. Writing to cards is also out of scope: a keyboard-wedge reader only ever sends the UID, it has no channel to send commands back.

πŸš€ Key features

  • Real-time scanning β€” large, readable-from-across-the-room verdict: access granted / denied / unknown card, with owner details, every UID representation and an audio cue.
  • Card database β€” name, owner, role, block flag, validity range, note, scan counter, search.
  • Enrollment in one click β€” an unknown card can be added without leaving the scan panel; the scan that triggered it is retroactively corrected in the log.
  • Scan history β€” filter by result, date range and free text; paginated.
  • CSV export β€” cards and history, UTF-8 with BOM so Excel gets the accents right.
  • Reader diagnostics β€” shows the exact characters the reader sent, the typing speed (machine vs. human) and how the number is interpreted in each of the three modes.
  • Access rules β€” block flag, validity window, repeat-scan suppression, station name per reading.
  • Phone as a second entry point β€” the app can serve a small page on your LAN and show a QR code to pair a phone; card numbers typed there land in the same pipeline as the USB reader.
  • Akuvox cloud integration β€” assign a scanned card to a resident in an object (building) through the akubela OpenAPI, with connection test, dry-run mode and a redacted request log.
  • Local by default β€” no telemetry, no accounts. Data stays in one local file; the phone server and the cloud integration are both off until you turn them on.

πŸ› οΈ Tech stack

Layer Choice
Shell Electron 43 (context isolation on, nodeIntegration off, CSP in packaged builds)
UI React 19 + TypeScript 7, bundled by Vite 8
Logic Plain JavaScript in shared/ β€” shared by the main process and the renderer
Storage Single JSON file, written atomically (temp file + rename)
Hardware USB HID keyboard events (no native modules, no pip/node-gyp build step)
Phone bridge Node's built-in http server + a second Vite entry point for the phone page (2.8 kB)
Cloud integration fetch + a data-driven REST profile; secrets in the OS keystore (safeStorage)
Packaging electron-builder 26 β†’ dmg/zip, NSIS/portable, AppImage/deb/tar.gz

Zero production dependencies. npm install pulls dev tooling only, so there is nothing to compile and nothing that can break on a different OS or Node version.

πŸ“¦ Getting started

Prerequisites

  • Node.js 20 or newer (developed on 22).
  • A 13.56 MHz ISO14443A USB reader. The app also runs without one β€” you can type card numbers by hand.

Installation

git clone https://github.com/Ziut3k-dev/RFID-Reader-App.git
cd RFID-Reader-App
npm install

Run

npm start        # build the UI and launch the app
npm run dev      # development: Vite HMR + Electron
npm run dev:web  # UI only in a browser (data in localStorage) β€” handy for styling

πŸ”’ Card number formats

The reader's output mode is set in hardware. All common variants are recognised and reduced to one canonical number:

What the reader sends Interpretation
0004372425 10 decimal digits, zero-padded (DEC mode)
0042B7C9 8 hex characters (HEX mode)
C9B74200 hex with reversed byte order (LSB first)
0004372425,0042,44873 Wiegand β€” the number is the first field
04:A2:2B:9C:11:44:80 7-byte UID with separators

A card enrolled from a reading in one mode is still found after a reading in another β€” lookup checks both byte orders. History stores the card's canonical number, with the raw reading kept in a separate field.

The interpretation mode lives in Settings (AUTO / DEC / HEX). If you don't know which mode your reader uses, open Reader diagnostics and tap a card: it prints the raw characters and the result in all three modes side by side.

πŸ“± Phone as a second entry point

Enable the server under the Telefon tab. The app shows a QR code with a pairing URL like http://192.168.1.42:8787/s/<secret>; point the iPhone's Camera app at it and Safari opens the page. Phone and computer must be on the same Wi-Fi.

What the phone does: accepts a card number typed from a badge label, shows the verdict β€” granted / denied / unknown β€” and lists the recent scans. Those scans go through the same access rules as the USB reader, appear in history with the station suffixed /telefon, and pop up on the desktop scan panel so an operator watching the app sees them live.

What the phone cannot do: it cannot read a contactless card. Safari has no Web NFC (that API exists only in Chrome on Android), so a web page on an iPhone has no access to the NFC hardware at all. Reading cards with the iPhone's built-in reader would require a separate native app signed with Apple's NFC entitlement, which needs a paid Apple Developer membership. Contactless cards are read by the USB reader at the computer.

One extra route exists for cards that carry a printed QR code: encode http://<host>:<port>/q/<secret>/<number> on the label and scanning it with the system Camera app registers the scan directly, without opening the page.

Security: the server listens only while enabled and every request needs the secret from the pairing URL. Anyone who photographs the QR code can register scans, so regenerate the secret (one button) or stop the server when you're done, and keep it on a trusted network β€” the connection is not encrypted. See SECURITY.md for the full picture.

☁️ Akuvox cloud integration

Assigns a card read by the USB reader to a resident in the Akuvox cloud, under the Akuvox tab.

The API is akubela OpenAPI (developer.akubela.com) β€” Akuvox's documented cloud interface. Two things shaped the implementation:

  • It is command-style, not REST: one URL (…/method/manager-commands) and the operation goes in the JSON body as {"command": …, "id": <32 hex>, "param": {…}}. Responses are always {"success": bool, "timestamp": int, "result": …} β€” a logical failure arrives with HTTP 200, so the envelope is checked on every call.
  • The hierarchy is project β†’ building β†’ residence (β€œfamily”) β†’ account β†’ rf_card. The β€œobject” is a project, an apartment is a residence (residence_no is the human-visible number), and a resident is an account.

Getting access: the documentation is public, the credentials are not. client_id / client_secret come from akubela technical support (support@akubela.com), and the documentation requires you to develop against a test server (api.*.pre.akubela.com) before moving to production. Pick your region in the panel β€” an account belongs to one cloud (EU / US / Asia / Japan / Australia / China).

How it works: the card assignment is written locally as pending first, then sent. On success the card is marked synced with the cloud's rf_card_id; on failure it stays with the reason attached and can be retried. Nothing silently succeeds.

Built for installer work

Feature Why it matters on site
Several connections One per customer, each with its own credentials; switch with a radio button. Deleting a connection unlinks its cards but keeps the cards and the scan history.
Series mode Pick the object, then just tap card after card β€” each goes to the highlighted resident and the list advances. Unknown cards are added to the database automatically, named after the apartment and resident.
Offline queue In a basement with no signal, every send would burn a timeout. Assignments are collected locally and sent later.
Auto-resend When connectivity returns, queued assignments go out on their own (checked every minute) and you get a notification.
Read-back confirmation After a successful write, the resident's credentials are re-read. A cloud answering β€œsuccess” is not the same as the card being visible to the resident β€” the card is marked confirmed only after it is seen.
Duplicate warnings Before assigning: this card is already assigned, this resident already has a card, or this number already exists in the cloud. Warnings, not blocks β€” sometimes a second card is intentional.
Replace a lost card One action: revoke the old card in the cloud, block it locally (so a finder gets a refusal, not a blank), issue the new one to the same resident with the reason recorded.
Handover protocol CSV or a printable PDF per object: apartment, resident, card, UID, number, state, who issued it, date, notes β€” with signature lines. PDF is rendered by Electron's own printToPDF, no extra dependency.

Not settled by the documentation (surfaced in the panel, so it isn't a hidden assumption): the exact format of the card number field β€” the docs only give type String and the example "1234567". The panel lets you pick decimal, zero-padded decimal, hex or byte-reversed hex; after the first assignment, check the number in the Akuvox panel and adjust. Also unverified: whether a card starts working on devices immediately or needs an access-group assignment, and whether projects held in the older SmartPlus cloud are visible through this API.

Credentials are encrypted with the OS keystore and stored outside the card database. The access token lives in memory only. The request log masks secrets, and dry-run mode shows the exact request without sending it β€” useful when tuning the card format against a real tenant.

πŸ” Access rules

Evaluated in this order (shared/core.js, function evaluate):

  1. repeat scan β€” a card resting on the reader produces a stream of readings; repeats inside the suppression window (3 s by default) are dropped and never reach the log;
  2. unknown card β€” denied, or auto-enrolled when learning mode is on;
  3. blocked card β€” denied;
  4. outside validity range β€” denied, with the date in the reason;
  5. otherwise access granted.

πŸ’Ύ Data

One JSON file, written atomically so an interrupted write cannot corrupt the database:

  • packaged app β€” the OS application-data directory (macOS: ~/Library/Application Support/rfid-scanner/rfid-data.json);
  • run from the repository β€” data/rfid-data.json.

Settings shows the exact path, and File β†’ Show database file reveals it in the file manager. A corrupted file is never overwritten silently β€” it is set aside as *.corrupt-<timestamp>.

πŸ—‚οΈ Project structure

shared/        environment-independent logic
  core.js        card number parsing + access rules
  store.js       data store (pluggable persistence adapter)
  service.js     scan processing, enrollment, diagnostics
electron/
  main.js        window, menu, IPC, CSP
  preload.cjs    bridge to the renderer
  persistence.js JSON file adapter
  reader.js      USB reader detection (ioreg / lsusb / PnP)
src/           React UI
  hooks/useKeyboardWedge.ts   HID keyboard capture
scripts/
  make-icon.js   renders build/icon.svg to a 1024px PNG using Electron
tests/         logic tests (node:test)

The rules live in shared/, so the Electron app and the browser preview reach identical decisions β€” there is no second implementation to keep in sync.

βœ… Tests

npm test        # 32 tests: number parsing, store, access rules
npm run typecheck

πŸ—οΈ Building installers

npm run dist:mac     # dmg + zip (arm64, x64)
npm run dist:win     # NSIS installer + portable (x64, arm64 installer)
npm run dist:linux   # AppImage + deb + tar.gz (x64, arm64)
npm run pack         # unpacked app only β€” quick sanity check
npm run icon         # regenerate build/icon.png from build/icon.svg

Output lands in release/. Each installer format needs its own host OS for a reliable result (.dmg requires macOS, .exe requires Windows), which is what the release workflow is for.

Releasing through GitHub Actions

.github/workflows/release.yml builds all three platforms on their native runners, then attaches every artifact to a draft GitHub release:

# bump "version" in package.json first β€” the workflow checks it against the tag
git tag v1.0.0
git push origin v1.0.0

Running it manually from the Actions tab only builds and keeps the packages as run artifacts β€” a release is created from a tag only. Review the draft, then hit Publish. ci.yml runs tests, typecheck and a trial packaging on all three systems for every push and pull request.

Code scanning

codeql.yml runs CodeQL with the security-and-quality suite on every push, every pull request and weekly. One-time repository setting: GitHub enables its own default setup for code scanning on public repositories, and that blocks results uploaded by a custom workflow β€” the analysis step fails even though the scan itself ran fine. Disable it under Settings β†’ Code security β†’ Code scanning β†’ CodeQL analysis β†’ Disable, or delete codeql.yml and keep GitHub's default setup instead (scanning still works, you just don't control the query suite or the schedule).

Dependabot proposes dependency and action updates weekly. Electron is a dev dependency but ships inside the packaged app, so its advisories affect end users β€” worth keeping current.

Commit and tag signing

Commits and tags are signed with an SSH key, so GitHub shows them as Verified. Two different things are called β€œsigning” here β€” this is git signing (who authored the commit), separate from installer code signing below (who built the binary).

To set it up on a fresh clone:

ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_signing -C "you@example.com (git signing)"
git config gpg.format ssh
git config user.signingkey ~/.ssh/id_ed25519_signing.pub
git config commit.gpgsign true
git config tag.gpgsign true

Then add the public key to GitHub under Settings β†’ SSH and GPG keys β†’ New SSH key with key type Signing Key (an authentication key alone does not make commits Verified).

To verify signatures locally, git needs to know which keys to trust:

printf 'you@example.com namespaces="git" %s\n' "$(cut -d' ' -f1,2 ~/.ssh/id_ed25519_signing.pub)" \
  > ~/.ssh/allowed_signers
git config gpg.ssh.allowedSignersFile ~/.ssh/allowed_signers
git log --show-signature -1
git tag -v v1.0.0

Installer code signing

Builds are unsigned, so no secrets are required to release. To sign macOS builds, remove identity: null from electron-builder.yml and set CSC_LINK, CSC_KEY_PASSWORD, APPLE_ID, APPLE_APP_SPECIFIC_PASSWORD and APPLE_TEAM_ID as repository secrets. For Windows, provide CSC_LINK and CSC_KEY_PASSWORD with an Authenticode certificate.

Until then, first launch shows a warning: on macOS right-click β†’ Open, on Windows SmartScreen β†’ More info β†’ Run anyway.

🩺 Troubleshooting

Nothing happens when I tap a card. Check that the app window is focused and the indicator reads NasΕ‚uch aktywny (listening). Then open Settings β†’ Reader diagnostics: if no characters appear at all, the reader is not sending keystrokes β€” try another USB port, and confirm the card is ISO14443A (a 13.56 MHz reader cannot read 125 kHz cards).

The number doesn't match the label on the card. The reader runs in a different mode than the one configured. Diagnostics shows which mode produces the number printed on the card; select it in Settings.

One card got stored twice. Older entries may come from readings taken in different modes. Delete the duplicate under Cards; later readings will map to a single card.

One tap creates several log entries. Increase the repeat-scan suppression window in Settings.

🀝 Contributing

Issues and pull requests are welcome. Please run npm test and npm run typecheck before opening a PR β€” CI runs both, plus a trial packaging on macOS, Windows and Linux.

πŸ”’ Security

Found a vulnerability? Please report it privately β€” see SECURITY.md, which also describes the app's security posture (offline by design, unencrypted local data, unsigned releases) and what falls outside its threat model.

πŸ“„ License

MIT β€” do what you like, keep the copyright notice, no warranty.

About

A cross-platform RFID & NFC reader application for tag management and hardware integration.

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages