npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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-sdk

For 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 serialport

Quick 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 ▼
    └───────────────────────────────────────── error

Auto-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/.

  1. Create src/profiles/<id>.ts and fill in ReaderProfile:
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
};
  1. Register it in src/profiles/index.ts:
export { myReader } from "./my-reader";
// ...
export const allProfiles: ReaderProfile[] = [gprPlus, genericSerial, genericBleUartProfile, myReader];
  1. Add test/fixtures/<id>.json with captured raw frames and expected decoded outputs.

  2. 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

  1. Implement the Transport interface 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 */ }
}
  1. Create src/<entry>.ts (e.g. src/my-platform.ts) that exports the transport class and a pre-wired createReader:
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).

  1. Add the subpath to package.json exports:
"./my-platform": {
  "types": "./dist/my-platform.d.ts",
  "default": "./dist/my-platform.js"
}

Planned: Web Bluetooth is BLE-only in browsers — a future /web subpath using the Web Bluetooth API would support BLE-capable readers (genericBleUart, any profile with connection: "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().connectedAccessories and read .protocolStrings while 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:

  1. Pair the GPR+ in OS Bluetooth settings.
  2. Open a serial terminal (e.g. screen /dev/tty.GPR-Plus 9600 on macOS, or PuTTY on Windows).
  3. Scan a chip and record the exact bytes — including any prefix bytes, status fields, temperature suffixes, or checksums.
  4. Scan a temperature microchip and note whether a temperature value is appended to the frame.
  5. 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)