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

ble-faker

v1.3.5

Published

Scriptable BLE simulation toolchain for React Native development

Readme

ble-faker

CI npm License: MIT

Scriptable BLE peripheral simulator for React Native developers. Define your hardware's behavior in JavaScript, connect your app to it — no physical device required.

A complete working example lives in the ble-faker-demo repository.


Why ble-faker?

If you're building a React Native app that talks to BLE hardware, you've probably felt this pain:

  • You need the physical device nearby for every change you want to test
  • The device has limited availability, limited firmware control, or both
  • Automated tests and CI runs can't include live hardware
  • Demoing to stakeholders means praying the device cooperates

ble-faker replaces the physical device on your dev machine. Your app code stays unchanged — the same react-native-ble-plx calls that talk to real hardware talk to ble-faker instead. You get a browser dashboard to watch characteristic values change in real time and inject inputs without touching the app.

ble-faker demo


Your app code stays the same

The key point: ble-faker ships a drop-in replacement for react-native-ble-plx. You configure Metro to redirect the import — that's it. Every connectToDevice, monitorCharacteristicForService, and writeCharacteristicWithResponseForService call in your app works exactly as before, against simulated hardware.


Getting Started

1. Install

npm install --save-dev ble-faker
# or
pnpm add -D ble-faker

2. Create a mock folder

mocks/
└── heart-rate-monitors/     ← category folder (one per device type)
    ├── gatt-profile.json    ← GATT structure
    └── FF-00-11-22-33-02.js ← device logic, named by MAC address

gatt-profile.json — mirrors the addMockDevice() payload from react-native-ble-plx-mock:

{
  "serviceUUIDs": ["180D"],
  "isConnectable": true,
  "mtu": 247,
  "services": [
    {
      "uuid": "180D",
      "characteristics": [
        { "uuid": "2A37", "properties": { "read": true, "notify": true } },
        { "uuid": "2A39", "properties": { "write": true } }
      ]
    }
  ]
}

FF-00-11-22-33-02.js — device logic:

export default function (state, event) {
  if (event.kind === "start") {
    return [
      { name: "HR Monitor", rssi: -65 },
      { vars: { hr: 72 } },
      { out: [{ name: "bpm", label: "Heart Rate (bpm)" }] },
    ];
  }

  if (event.kind === "tick") {
    const hr = Number(state.vars.hr);
    return [["2A37", utils.packUint16(hr)], { set: { bpm: String(hr) } }];
  }

  if (event.kind === "notify" && event.uuid === "2A39") {
    // App wrote to the control point — reset heart rate
    return [{ vars: { hr: 72 } }];
  }

  return [];
}

No code at all? Just touch FF-00-11-22-33-02.js. ble-faker auto-generates browser controls from the GATT profile — output fields for readable/notifiable characteristics, input fields for writable ones. Good enough to verify your profile is wired up correctly before you write any logic.

Type checking in JavaScript device files — device logic runs in a plain JS sandbox (no TypeScript transpilation). Use JSDoc annotations for full IDE autocompletion and event narrowing with zero runtime cost:

/// <reference types="ble-faker/device" />

/** @type {import('ble-faker/device').DeviceLogicFn} */
export default function (state, event) {
  if (event.kind === "tick") {
    const hr = Number(state.vars.hr);
    return [["2A37", utils.packUint16(hr)]];
  }
  return [];
}

The /// <reference> directive activates utils global completions; the @type annotation narrows event to each specific event kind in if branches. No tsconfig changes needed.

3. Configure Metro

Add one line to your metro.config.js using the withBleFaker helper:

Expo managed workflow:

const { getDefaultConfig } = require("expo/metro-config");
const { withBleFaker } = require("ble-faker/metro");

const config = getDefaultConfig(__dirname);
module.exports = withBleFaker(config, { dir: "./mocks", label: "My App" });

Bare React Native:

const { getDefaultConfig, mergeConfig } = require("@react-native/metro-config");
const { withBleFaker } = require("ble-faker/metro");

const config = mergeConfig(getDefaultConfig(__dirname), {});
module.exports = withBleFaker(config, { dir: "./mocks", label: "My App" });

withBleFaker is a no-op unless BLE_MOCK=true is set in the environment, so it is safe to apply unconditionally.

withBleFaker options:

| Option | Default | Description | |---|---|---| | dir | (required) | Path to your mocks directory | | label | 'ble-faker' | Label shown in the browser dashboard | | port | from state file | Override the server port (auto-detected from ~/.ble-faker-server.json) | | env | 'BLE_MOCK' | Environment variable name that activates the mock |

4. Add scripts to your package.json

"scripts": {
  "ble:start":  "ble-faker --port 58083",
  "ble:stop":   "ble-faker stop",
  "start:mock": "cross-env BLE_MOCK=true expo start",
  "dev:mock":   "concurrently -k \"npm run ble:start\" \"npm run start:mock\""
}

cross-env and concurrently handle Windows env vars and parallel processes — add them with npm install --save-dev cross-env concurrently. dev:mock starts both the ble-faker server and Metro in one command.

5. Start the server

npm run ble:start

6. Run your app with the mock enabled

npm run start:mock
# or start both together:
npm run dev:mock

Restart Metro (not just reload) whenever you change metro.config.js.

7. Open the browser dashboard

http://localhost:58083 — lists all namespaces. Click through to a device to see its live output fields and interact with it via input controls.


Device Logic Reference

Events

| event.kind | When | Extra fields | | ------------ | -------------------------------------------------------------------------- | -------------------------- | | start | Namespace created or device file saved. Use to initialize state.vars. | — | | connect | App establishes a BLE connection | — | | disconnect | BLE connection closes. vars updates persist; char updates are discarded. | — | | tick | 1-second timer — only fires while a connection is open | — | | advertise | Server building the device list for the app's scan | — | | notify | App wrote to a characteristic | uuid, payload (base64) | | input | Browser UI form submitted | id, payload (string) |

Return commands

| Shape | Effect | | ------------------------------ | ------------------------------------------------------- | | ['uuid', base64] | Push a characteristic value to the app | | { name, rssi, … } | Patch the advertising packet (Partial<Device> fields) | | { vars: { key: value } } | Persist values into state.vars for the next call | | { in: [{ name, label }] } | Define browser input controls | | { out: [{ name, label }] } | Define browser output display fields | | { set: { field: 'value' } } | Push a string to a named browser output field | | { disconnect: true } | Simulate a device disconnection (see note below) | | { readError: { uuid } } | Make the app's next characteristic read for uuid fail | | { clearReadError: { uuid } } | Clear a previously set read error |

{ disconnect: true } note: The mock signals disconnection to the app by injecting a characteristic error on the first monitored characteristic. Apps that only monitor characteristics other than the first one in the GATT profile may not detect the simulated disconnect. If your disconnect handler isn't firing, ensure the app has an active monitor on the first characteristic defined in gatt-profile.json.

State

| Field | Type | Description | | ------------- | ------------------------- | ---------------------------------------------------- | | state.dev | Partial<Device> | Device advertising info (includes id as MAC) | | state.vars | Record<string, unknown> | Your persisted values — read-only, write via vars: | | state.chars | Record<string, string> | Current characteristic values by UUID (base64) | | state.ui | { ins, outs } | Current browser UI definition |

Available globals

No imports needed inside device logic files:

| Global | Description | | ------------------------------ | ----------------------------------------------- | | Buffer | Node.js Buffer | | Uint8Array, DataView | Binary views | | TextEncoder, TextDecoder | Web encoding API | | utils.toBase64(arr) | Uint8Array → base64 string | | utils.fromBase64(str) | base64 → Buffer | | utils.packUint8(n) | uint8 → base64 | | utils.packInt8(n) | int8 → base64 | | utils.packUint16(n) | little-endian uint16 → base64 | | utils.packInt16(n) | little-endian int16 → base64 | | utils.packUint32(n) | little-endian uint32 → base64 | | utils.packFloat32(n) | little-endian IEEE 754 float → base64 | | utils.unpackUint8(b64) | base64 → uint8 | | utils.unpackInt8(b64) | base64 → int8 | | utils.unpackUint16(b64) | base64 → little-endian uint16 | | utils.unpackInt16(b64) | base64 → little-endian int16 | | utils.unpackUint32(b64) | base64 → little-endian uint32 | | utils.unpackFloat32(b64) | base64 → little-endian IEEE 754 float | | console.log/warn/error | forwarded to server stdout, prefixed by deviceId|

All pack/unpack helpers use little-endian byte order, which is standard for BLE SIG characteristic specifications.

Sandbox

Logic files run in an isolated node:vm context with a 50ms CPU budget per call. No access to process, require, the filesystem, or the network.


Testing

ble-faker supports two testing modes depending on how close to the metal you want to test:

  • RNTL / Jest — render React Native components against the mock, assert on UI state. No simulator required. Fast.
  • Detox / Maestro — drive the full app in a simulator or on a device. The test client controls the device side while Detox/Maestro drives the UI side.

RNTL / Jest

This is the recommended approach for component-level integration tests. The entire test suite runs in Node.js — no simulator, no Metro, no Expo.

Install additional dependencies

npm install --save-dev @testing-library/react-native jest-expo
# or pnpm add -D ...

jest.config.js

module.exports = {
  preset: 'jest-expo',
  globalSetup: './jest.globalSetup.js',
  globalTeardown: './jest.globalTeardown.js',
  setupFilesAfterEnv: ['./jest.setup.ts'],
  moduleNameMapper: {
    // Redirect react-native-ble-plx → ble-faker mock (same as Metro redirect)
    '^react-native-ble-plx$': '<rootDir>/node_modules/ble-faker/dist/mock.js',
    // Allow imports from ble-faker/test in test files
    '^ble-faker/test$': '<rootDir>/node_modules/ble-faker/dist/test-client.js',
  },
  transformIgnorePatterns: [
    'node_modules/(?!(jest-)?react-native|@react-native(-community)?|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|react-native-svg|ble-faker)',
  ],
};

jest.globalSetup.js

module.exports = async function () {
  const { bleMockServer } = await import('ble-faker');
  await bleMockServer.start({ port: 58083 });
};

jest.globalTeardown.js

module.exports = async function () {
  const { bleMockServer } = await import('ble-faker');
  bleMockServer.stop();
};

Test file

import { render, screen, fireEvent, waitFor } from '@testing-library/react-native';
import { BleTestClient, BleNamespace, BleDevice } from 'ble-faker/test';
import { App } from '../src/App';

const MOCKS_DIR = path.join(__dirname, '../mocks');
const DEVICE_ID = 'ff-00-11-22-33-02';

let client: BleTestClient;
let ns: BleNamespace;
let device: BleDevice;

beforeAll(async () => {
  client = BleTestClient.connect(); // reads running server from ~/.ble-faker-server.json
  ns = await client.mount({
    dir: MOCKS_DIR,
    label: 'rntl-test',
    disableAutoTick: true, // drive the clock explicitly for deterministic tests
  });
  device = ns.device(DEVICE_ID);
});

afterAll(async () => {
  await client.unmount(ns);
});

it('shows updated heart rate after device tick', async () => {
  render(<App />);

  // Wait for BLE scan to populate the device list
  await waitFor(() => screen.getByText('HR Monitor'));
  fireEvent.press(screen.getByText('Connect'));

  // Advance the simulated clock — one tick fires the device's tick handler
  await device.tickN(1);

  // Assert the characteristic value was pushed to the app
  await device.waitForChar('2a37', /.+/);

  // Assert the UI reflects the new value
  await waitFor(() => screen.getByText('72 bpm'));
});

client.mount() with disableAutoTick: true stops the 1-second automatic tick, giving you full control over the device clock. Use device.tickN(n) to advance it by exactly n steps.

Key points

  • The server starts once in globalSetup and is shared across all test files — fast startup.
  • BleTestClient.mount() writes the namespace URLs directly into the mock's shared module, so the mock skips the Metro discovery step entirely. No global.fetch patching required.
  • Each test file mounts its own namespace. Use different dir paths for test isolation, or the same dir with dedicated device IDs.

Detox / Maestro

The React Native app runs with BLE_MOCK=true (driven by Detox, Maestro, or similar), while the test client controls the device side at the same time. For example, Detox taps a button in the app while your test advances the simulated clock:

it("shows updated temperature after device tick", async () => {
  const device = ns.device("ff-00-11-22-33-02");
  await detox.element(by.id("connectBtn")).tap();
  await device.tickN(60); // advance 1 simulated minute
  await expect(detox.element(by.id("temp"))).toHaveText("99°F");
});

Setup:

import { BleTestClient } from "ble-faker/test";
import { before, after, describe, it } from "node:test";

describe("heart rate monitor", () => {
  const client = BleTestClient.connect(); // reads running server from ~/.ble-faker-server.json
  let ns: Awaited<ReturnType<typeof client.mount>>;

  before(async () => {
    ns = await client.mount({ dir: "./mocks", label: "HR test" });
  });

  after(async () => {
    await client.unmount(ns);
  });

  it("pushes a heart rate characteristic on each tick", async () => {
    const device = ns.device("ff-00-11-22-33-02");
    await device.tickN(1);
    await device.waitForChar("2a37", /.+/);
  });
});

Controlling the device

| Method | Description | | ----------------------------- | ----------------------------------------------- | | device.input(name, payload) | Simulate a browser UI form submission | | device.tickN(n) | Advance the device clock by n ticks (max 100) | | device.forceDisconnect() | Trigger a simulated BLE disconnection |

Asserting

Both methods throw an AssertionError (from node:assert) on timeout — no manual assert call needed. pattern can be a string (exact match) or a RegExp. Default timeout is 5000ms.

Both also check the current value immediately — if it already matches when called, they return right away.

| Method | Description | | ----------------------------------------------- | ----------------------------------------- | | device.waitForOutput(name, pattern, timeout?) | Wait until a browser output field matches | | device.waitForChar(uuid, pattern, timeout?) | Wait until a characteristic value matches |

Inspecting last seen values

After any waitFor* call — including ones that timed out and threw — the last observed value is accessible:

try {
  await device.waitForOutput("mmode_out", "M", 3000);
} catch (e) {
  console.log(device.lastOutput("mmode_out")); // e.g. "B" — what was actually seen
  throw e;
}

| Method | Description | | ------------------------- | ------------------------------------------ | | device.lastOutput(name) | Last value seen for a browser output field | | device.lastChar(uuid) | Last value seen for a characteristic |

Running the server in CI

# GitHub Actions example
- name: Start ble-faker
  run: npx ble-faker --port 58083 &

- name: Run tests
  run: npm test

- name: Stop ble-faker
  run: npx ble-faker stop
  if: always()

For RNTL tests with globalSetup/globalTeardown, the server lifecycle is managed automatically by Jest — no manual CI step needed.

The server writes its URL and port to ~/.ble-faker-server.json on startup. BleTestClient.connect() reads from this file. In CI environments where the home directory is not writable or shared across steps, override the path with the BLE_FAKER_STATE environment variable:

- name: Start ble-faker
  run: npx ble-faker --port 58083 &
  env:
    BLE_FAKER_STATE: /tmp/ble-faker-server.json

- name: Run tests
  run: npm test
  env:
    BLE_FAKER_STATE: /tmp/ble-faker-server.json

Programmatic API

Embed the server in your own tooling or integration tests:

import { bleMockServer } from "ble-faker";

await bleMockServer.start({ dir: "./mocks", port: 58083 });
const { pid, url, port } = await bleMockServer.get();
await bleMockServer.stop();

Development commands

| Command | Description | | :---------------- | :-------------------------------------- | | pnpm dev | Watch mode (rebuilds on source changes) | | pnpm build | Compile to dist/ | | pnpm test | Run tests (Node.js native test runner) | | pnpm build:test | Build + test in one step | | pnpm lint | oxlint |


Contributing

PRs welcome. See docs/ARCHITECTURE.md for the internal design.

License: MIT