@antzsoft/rfid-reader-sdk
v0.2.3
Published
Reader-agnostic SDK for animal microchip (RFID/PIT) scanners over Bluetooth Classic (SPP/MFi), BLE, and serial — React Native, Node, Electron.
Readme
@antzsoft/rfid-reader-sdk
A reader-agnostic SDK for animal microchip (RFID/PIT) scanners. The architecture is split into three concerns: profiles (plain-data descriptions of reader models), transports (platform byte-movers), and a core (frame parsing, ISO 11784/11785 decode, de-duplication, auto-reconnect). The first shipped profile is the Destron Fearing GPR+, but the SDK is designed to support any scanner that emits ASCII identifiers over Bluetooth Classic, BLE, or serial — adding a new reader model never touches core or transports.
Supported readers
| Profile id | Factory / export | Connection | Notes |
|---|---|---|---|
| gpr-plus | gprPlus | Bluetooth Classic / MFi | ⚠ 2 values pending hardware capture (see Hardware capture guide) |
| generic-serial | genericSerial | Bluetooth Classic SPP or serial | Any line-based serial scanner |
| generic-ble-uart | genericBleUart(overrides?) | BLE GATT | Nordic UART defaults; pass overrides for OEM GATT UUIDs |
Supported platforms
| Transport | Export | Subpath | Notes |
|---|---|---|---|
| React Native iOS MFi (External Accessory) | RNClassicTransport | /react-native | iOS uses External Accessory framework |
| React Native Android SPP | RNClassicTransport | /react-native | Android uses RFCOMM |
| React Native BLE (both) | RNBleTransport | /react-native-ble | Profile must define ble config |
| Node / Electron serial | NodeSerialTransport | /node | Uses serialport peer dependency |
| Mock (anywhere) | MockTransport | /mock | Hardware-free development and tests |
Expo: Works in Expo Dev Build / EAS only. Bluetooth Classic and BLE are native modules that Expo Go does not bundle — the app will crash at runtime in Expo Go. Build a custom dev client with
expo-dev-client+ EAS.iOS Simulator / Android emulator: Neither has Bluetooth hardware. Always test on a real device.
Install
npm i @antzsoft/rfid-reader-sdkFor the Antz mobile app (file dependency in package.json):
{
"dependencies": {
"@antzsoft/rfid-reader-sdk": "file:../../rfid-reader-sdk"
}
}Install optional peer dependencies for your platform(s):
# React Native Bluetooth Classic (iOS MFi + Android SPP)
npm i react-native-bluetooth-classic
# React Native BLE
npm i react-native-ble-plx
# Node / Electron
npm i serialportQuick starts
React Native — Bluetooth Classic (iOS MFi + Android SPP)
import { gprPlus, suggestProfile } from "@antzsoft/rfid-reader-sdk";
import { createReader, RNClassicTransport } from "@antzsoft/rfid-reader-sdk/react-native";
// 1. Let the user pick their bonded device
const paired = await RNClassicTransport.listPaired();
const device = paired[0]; // or present a picker UI
// 2. Optionally suggest a profile from the device name (advisory only)
const profile = suggestProfile(device.name) ?? gprPlus;
// 3. Build the reader
const reader = createReader(profile, { device: { address: device.address } });
reader.on("read", (r) => {
console.log(r.id, r.technology, r.iso?.countryName);
});
reader.on("state", (s) => console.log("state:", s));
reader.on("error", (e) => console.error("error:", e));
await reader.connect();For a reader stuck in host/master mode, pass mode: "accept" and omit device — the app waits for the reader to connect (Android only).
React Native — BLE
import { genericBleUart } from "@antzsoft/rfid-reader-sdk";
import { createReader, RNBleTransport } from "@antzsoft/rfid-reader-sdk/react-native-ble"; // RNBleTransport importable for advanced use (custom BleManager)
// deviceId comes from your BLE peripheral scan
const profile = genericBleUart(); // or genericBleUart({ serviceUUID: "...", notifyCharUUID: "..." })
const reader = createReader(profile, { device: { id: peripheralId } });
reader.on("read", (r) => console.log(r.id));
await reader.connect();Node / Electron
import { genericSerial } from "@antzsoft/rfid-reader-sdk";
import { createReader, NodeSerialTransport } from "@antzsoft/rfid-reader-sdk/node";
const ports = await NodeSerialTransport.list();
const path = ports.find((p) => /GPR|rfcomm|Bluetooth/i.test(p.path))?.path ?? ports[0]?.path;
if (!path) throw new Error("No serial port found — is the reader paired?");
const reader = createReader(genericSerial, { device: { path } });
reader.on("read", (r) => console.log(r.id, r.technology));
await reader.connect();Mock (hardware-free dev / tests)
import { gprPlus } from "@antzsoft/rfid-reader-sdk";
import { createReader } from "@antzsoft/rfid-reader-sdk/mock";
const { reader, transport } = createReader(gprPlus);
reader.on("read", (r) => console.log("mock read:", r.id));
await reader.connect();
// Inject a frame as if the hardware sent it
transport.pushFrame("985121005612345\r\n");The ChipRead you receive
Every read event delivers a ChipRead:
{
"raw": "985121005612345",
"id": "985121005612345",
"technology": "FDX-B",
"iso": {
"countryCode": 985,
"nationalId": "121005612345",
"countryName": "Manufacturer (Destron Fearing / HomeAgain)",
"isManufacturerCode": true
},
"timestamp": 1751836800000,
"meta": {
"profileId": "gpr-plus",
"readerModel": "Destron Fearing GPR+",
"connectionType": "bluetooth_serial",
"platform": "ios"
}
}Mapping to the Antz scan API payload:
| Antz field | ChipRead field | Notes |
|---|---|---|
| microchip | id | Normalized chip identifier |
| raw_data | raw | Exact bytes from the reader |
| reader_model | meta.readerModel | Human label from the profile |
| connection_type | meta.connectionType | "bluetooth_serial" | "ble" | "serial" | "mock" |
| platform | meta.platform | "ios" | "android" | "node" |
| scanned_at | new Date(timestamp).toISOString() | Epoch ms → ISO 8601 string |
Connection lifecycle
disconnected
│
▼ connect()
connecting
│
▼ success
connected ──────── connection drop ──────▶ reconnecting
│ │
│ success ▼
│ connected
│ │
│ retries exhausted ▼
└───────────────────────────────────────── errorAuto-reconnect kicks in only after an established connection drops. An initial connect() that fails immediately rejects — it never retries.
Default policy: { retries: 5, backoffMs: 1000 } with exponential backoff (backoffMs × 2^(attempt−1)).
To change or disable:
// Fewer retries, faster backoff
createReader(profile, {
device: { address },
autoReconnect: { retries: 3, backoffMs: 500 },
});
// Disable reconnect entirely
createReader(profile, {
device: { address },
autoReconnect: false,
});TransportErrorCode — UI mapping:
| Code | Suggested UI state |
|---|---|
| bluetooth-off | "Bluetooth is turned off — please enable it" |
| not-paired | "Reader not paired — go to Bluetooth settings and pair it" |
| not-found | "Reader not found — is it powered on and in range?" |
| connection-failed | "Could not connect — try again" |
| connection-lost | "Connection lost — reconnecting…" |
| permission-denied | "Bluetooth permission denied — allow in Settings" |
| unsupported-platform | "This platform is not supported by this profile" |
import { TransportError } from "@antzsoft/rfid-reader-sdk";
reader.on("error", (e) => {
if (e instanceof TransportError) {
switch (e.code) {
case "bluetooth-off": showAlert("Enable Bluetooth"); break;
case "permission-denied": openSettings(); break;
default: showToast(e.message);
}
}
});Adding a new reader profile
Adding support for a new reader never touches core or transports — it is entirely a data change in src/profiles/.
- Create
src/profiles/<id>.tsand fill inReaderProfile:
import { ReaderProfile } from "./types";
export const myReader: ReaderProfile = {
id: "my-reader",
label: "My Reader Model X",
connection: "bluetooth-classic", // or "ble" or "serial"
matchName: /Model.X/i, // optional: auto-suggest by device name
ios: { protocolStrings: ["com.vendor.modelx"] }, // iOS MFi only
// frame: { parseFrame: (line) => ... } // only if non-standard framing
};- Register it in
src/profiles/index.ts:
export { myReader } from "./my-reader";
// ...
export const allProfiles: ReaderProfile[] = [gprPlus, genericSerial, genericBleUartProfile, myReader];Add
test/fixtures/<id>.jsonwith captured raw frames and expected decoded outputs.Run
npm test— the fixture-replay harness picks it up automatically.
Invariant: Core (src/core/) and transports (src/transports/) must not change when you add a profile.
Adding a new platform / transport
- Implement the
Transportinterface from@antzsoft/rfid-reader-sdk:
import { Transport } from "@antzsoft/rfid-reader-sdk";
export class MyTransport implements Transport {
readonly connectionType = "bluetooth_serial"; // or "ble" | "serial" | "mock" | "unknown"
deliversCompleteLines?: boolean; // true if each onData chunk is one complete line
async connect(): Promise<void> { /* open connection */ }
async disconnect(): Promise<void> { /* close and release */ }
onData(handler: (chunk: string | Uint8Array) => void): () => void {
// register handler, return unsubscribe fn
return () => {};
}
onError(handler: (error: Error) => void): () => void {
return () => {};
}
async write(data: string | Uint8Array): Promise<void> { /* optional */ }
}- Create
src/<entry>.ts(e.g.src/my-platform.ts) that exports the transport class and a pre-wiredcreateReader:
import { createReader as coreCreateReader, CreateReaderOptions } from "./create-reader";
import { ReaderProfile } from "./profiles/types";
import { MyTransport } from "./transports/my-transport";
export { MyTransport };
export function createReader(
profile: ReaderProfile,
options: Omit<CreateReaderOptions, "transport"> & { device: { address: string } }
) {
const transport = new MyTransport({ address: options.device.address });
return coreCreateReader(profile, { ...options, transport });
}Replace { address } with whatever locator your transport needs ({ path } for serial ports, { id } for BLE peripherals).
- Add the subpath to
package.jsonexports:
"./my-platform": {
"types": "./dist/my-platform.d.ts",
"default": "./dist/my-platform.js"
}Planned: Web Bluetooth is BLE-only in browsers — a future
/websubpath using the Web Bluetooth API would support BLE-capable readers (genericBleUart, any profile withconnection: "ble"). Classic SPP readers cannot connect from the browser.
iOS / Android app configuration
iOS — MFi External Accessory
Bluetooth Classic / MFi readers require the reader's protocol string(s) in Info.plist. Use the value from profile.ios.protocolStrings:
<!-- ios/<App>/Info.plist -->
<key>UISupportedExternalAccessoryProtocols</key>
<array>
<string>com.allflex.gpr</string> <!-- replace with the verified string -->
</array>Expo config plugin (recommended):
// app.json
{
"expo": {
"plugins": [
["with-rn-bluetooth-classic", {
"peripheralUsageDescription": "Connect to the microchip reader",
"protocols": ["com.allflex.gpr"]
}]
]
}
}Android — Bluetooth permissions
Add to AndroidManifest.xml:
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<!-- Android 12+ (API 31+) -->
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />Request BLUETOOTH_CONNECT at runtime on Android 12+ before calling reader.connect().
BLE (both platforms)
Use the react-native-ble-plx Expo config plugin:
{
"expo": {
"plugins": [
["react-native-ble-plx", {
"isBackgroundEnabled": false,
"modes": ["peripheral", "central"],
"bluetoothAlwaysPermission": "Allow $(PRODUCT_NAME) to connect to RFID readers"
}]
]
}
}Hardware capture guide
The gpr-plus profile contains two PENDING_HARDWARE_CAPTURE markers in src/profiles/gpr-plus.ts. These require a physical reader to resolve:
1. MFi protocol string (ios.protocolStrings)
The current placeholder is "com.allflex.gpr". To get the real value:
- From the device: On iOS, enumerate
EAAccessoryManager.sharedAccessoryManager().connectedAccessoriesand read.protocolStringswhile the GPR+ is connected. Alternatively, check the SerialMagic app's device-info screen. - From Destron Fearing/Allflex: Contact their developer support and request the MFi protocol string for the GPR+ model.
Once confirmed, update src/profiles/gpr-plus.ts:
ios: { protocolStrings: ["com.vendor.actual-string"] },...and update the protocols array in your app.json config plugin.
2. Exact frame format (frame.parseFrame)
The current assumption is "one ASCII ID per line, CR/LF terminated." Capture the real format by:
- Pair the GPR+ in OS Bluetooth settings.
- Open a serial terminal (e.g.
screen /dev/tty.GPR-Plus 9600on macOS, or PuTTY on Windows). - Scan a chip and record the exact bytes — including any prefix bytes, status fields, temperature suffixes, or checksums.
- Scan a temperature microchip and note whether a temperature value is appended to the frame.
- Power on the GPR+ with chips already stored in its 3,000-read memory and check whether stored scans are replayed on connect.
Once you have the real frame format, set frame.parseFrame in gpr-plus.ts:
export const gprPlus: ReaderProfile = {
// ...
frame: {
parseFrame: (line) => {
// parse prefix/checksum/temperature suffix
// return { raw, id, technology, timestamp } or null to ignore
},
},
};API reference
@antzsoft/rfid-reader-sdk (main)
| Export | Kind | Description |
|---|---|---|
| RfidReader | class | Connect, emit read / raw / state / error events |
| FrameParser | class | Reassembles SPP stream chunks into complete lines |
| decodeIso | function | Decode a 15-digit ISO 11784/11785 identifier |
| classify | function | Heuristic technology classification (FDX-B, HDX, etc.) |
| buildRead | function | Construct a ChipReadInput from raw text |
| ISO_PREFIXES | const | Country/manufacturer code lookup table |
| lookupPrefix | function | Look up a numeric country/manufacturer code |
| isManufacturerCode | function | True if code is in the ICAR manufacturer range (900–999) |
| Emitter | class | Typed event emitter used by RfidReader |
| TransportError | class | Tagged transport error with code: TransportErrorCode |
| asTransportError | function | Wrap an unknown error as a TransportError |
| detectPlatform | function | Detect current runtime platform |
| gprPlus | const | GPR+ profile |
| genericSerial | const | Generic serial profile |
| genericBleUart | function | BLE UART profile factory (Nordic UART defaults) |
| genericBleUartProfile | const | Default Nordic-UART instance |
| allProfiles | const | Array of all built-in profiles |
| getProfile | function | Look up a profile by stable id |
| suggestProfile | function | Suggest a profile from a device's advertised name (advisory) |
| createReader | function | Wire a profile + transport into a ready RfidReader |
| ReaderProfile | type | Profile data shape |
| ChipRead | type | Decoded chip read event payload |
| ChipReadInput | type | Pre-meta chip read (used internally) |
| ReaderMeta | type | Provenance stamp on every ChipRead |
| ConnectionState | type | "disconnected" \| "connecting" \| "connected" \| "reconnecting" \| "error" |
| TransportErrorCode | type | Machine-readable error category |
| Transport | type | Platform transport interface |
| ReaderOptions | type | Options for RfidReader constructor |
| CreateReaderOptions | type | Options for core createReader |
@antzsoft/rfid-reader-sdk/react-native
| Export | Kind | Description |
|---|---|---|
| createReader(profile, { device?, mode?, connectionOptions? }) | function | Build a reader over Bluetooth Classic SPP/MFi |
| RNClassicTransport | class | React Native Bluetooth Classic transport; has static listPaired() |
| RNClassicTransportOptions | type | Options for RNClassicTransport |
| RNCreateReaderOptions | type | Options for the RN createReader |
@antzsoft/rfid-reader-sdk/react-native-ble
| Export | Kind | Description |
|---|---|---|
| createReader(profile, { device: { id } }) | function | Build a reader over BLE GATT; throws if profile has no ble config |
| RNBleTransport | class | React Native BLE transport (react-native-ble-plx) |
| RNBleTransportOptions | type | Options for RNBleTransport |
| RNBleCreateReaderOptions | type | Options for the BLE createReader |
@antzsoft/rfid-reader-sdk/node
| Export | Kind | Description |
|---|---|---|
| createReader(profile, { device: { path } }) | function | Build a reader over an OS virtual serial (COM) port |
| NodeSerialTransport | class | Node/Electron serial transport; has static list() |
| NodeSerialTransportOptions | type | Options for NodeSerialTransport |
| NodeCreateReaderOptions | type | Options for the Node createReader |
@antzsoft/rfid-reader-sdk/mock
| Export | Kind | Description |
|---|---|---|
| createReader(profile, options?) | function | Returns { reader, transport } — hardware-free |
| MockTransport | class | Scriptable mock; pushFrame(data) injects bytes |
| MockTransportOptions | type | Options for MockTransport |
Build & test
npm install
npm run build # tsc → dist/
npm test # node --test (per-profile fixture replay + unit tests)