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

react-native-ble-scanner-kit

v1.0.14

Published

A React Native module for Bluetooth Low Energy (BLE) device scanning and management with distance estimation and device tracking capabilities

Readme

react-native-ble-scanner-kit

Proprietary Software — Copyright (c) 2024 Ant-Tech. All rights reserved. Unauthorized use is prohibited. See LICENSE for details.

A React Native module for Bluetooth Low Energy (BLE) device scanning with real-time distance estimation, device tracking, and proximity-based alerts.

Supports React Native New Architecture (Turbo Modules) and Android 16KB page alignment (API 35+).

Requirements

| Dependency | Version | |---|---| | React Native | >= 0.73.0 | | React | >= 18.0.0 | | react-native-permissions | >= 4.0.0 | | Android minSdk | 24 | | iOS | 13.4+ |

Installation

npm install react-native-ble-scanner-kit react-native-permissions
# or
yarn add react-native-ble-scanner-kit react-native-permissions

iOS

cd ios && pod install

Add to your Info.plist:

<key>NSBluetoothAlwaysUsageDescription</key>
<string>This app uses Bluetooth to scan for nearby BLE devices.</string>
<key>NSBluetoothPeripheralUsageDescription</key>
<string>This app uses Bluetooth to scan for nearby BLE devices.</string>

Android

Permissions are declared in the module's AndroidManifest.xml and will be merged automatically:

  • BLUETOOTH, BLUETOOTH_ADMIN
  • BLUETOOTH_SCAN, BLUETOOTH_CONNECT
  • ACCESS_FINE_LOCATION
  • VIBRATE

No additional setup needed.


Quick Start

import {
  initializeBluetooth,
  startScan,
  stopScan,
} from 'react-native-ble-scanner-kit';

// 1. Initialize (checks permissions + Bluetooth state)
await initializeBluetooth();

// 2. Start scanning
const emitter = startScan();

// 3. Listen for devices
emitter.addListener('onDeviceFound', (device) => {
  console.log(device.id, device.name, device.rssi, device.estimatedDistance);
});

// 4. Stop scanning when done
stopScan();

API Reference

initializeBluetooth(): Promise<void>

Requests BLE permissions (Android) and verifies that Bluetooth is powered on. Must be called before scanning.

Throws:

  • MODULE_NOT_FOUND — Native module not linked.
  • PERMISSION_DENIED — One or more required permissions were denied.
  • NOT_SUPPORTED — Device does not support Bluetooth.
  • BT_OFF — Bluetooth is powered off.
try {
  await initializeBluetooth();
} catch (error) {
  console.error(error.message);
}

startScan(targetId?, config?, isUseDeviceID?): NativeEventEmitter

Starts a BLE scan and returns a NativeEventEmitter for event subscription.

| Parameter | Type | Default | Description | |---|---|---|---| | targetId | string | undefined | Filter by device name/ID. Only matching devices emit events. | | config | SentiDriveScanConfigs | See defaults below | Scan behavior, RSSI filtering, and pulse alert configuration. | | isUseDeviceID | boolean | false | When true, filters by MAC address (Android) / UUID (iOS) instead of device name. |

Default configuration:

{
  scanConfig: {
    scanDuration: 30000,    // 30 seconds
    enableVibration: false,
    enableSound: false,
    txPower: -59,           // dBm (calibrated at 1 meter)
    n: 2,                   // Path loss exponent (free space)
  },
  rssiFilter: {
    enableKalmanFilter: false,
    kalmanProcessNoise: 0.01,
    kalmanMeasurementNoise: 0.25,
  },
  pulseConfigs: [
    { distance: 20, intervalMs: 5000 },
    { distance: 10, intervalMs: 3000 },
    { distance: 5,  intervalMs: 2000 },
    { distance: 3,  intervalMs: 1000 },
    { distance: 1,  intervalMs: 500  },
  ],
}

Example — Scan for a specific device with Kalman filtering:

const emitter = startScan('AA:BB:CC:DD:EE:FF', {
  scanConfig: {
    scanDuration: 60000,
    enableVibration: true,
    txPower: -65,
    n: 2.5,
  },
  rssiFilter: {
    enableKalmanFilter: true,
    kalmanProcessNoise: 0.01,
    kalmanMeasurementNoise: 0.5,
  },
  pulseConfigs: [
    { distance: 10, intervalMs: 2000 },
    { distance: 3,  intervalMs: 500  },
  ],
}, true); // isUseDeviceID = true (filter by MAC address)

Example — Scan for all devices:

const emitter = startScan();

emitter.addListener('onDeviceFound', (device) => {
  console.log(`${device.name} is ~${device.estimatedDistance.toFixed(1)}m away`);
});

stopScan(): void

Stops the active scan. Cancels all vibration, stops sound playback, clears device trackers, and removes internal event listeners. Safe to call even if no scan is active.

stopScan();

Important: Always call stopScan() when your component unmounts or the scan is no longer needed. This prevents timer and listener leaks.

// React hook example
useEffect(() => {
  const emitter = startScan();
  const sub = emitter.addListener('onDeviceFound', handleDevice);
  return () => {
    sub.remove();
    stopScan();
  };
}, []);

estimateDistance(rssi, txPower, n): number

Calculates distance in meters using the log-distance path loss model:

distance = 10 ^ ((txPower - rssi) / (10 * n))

| Parameter | Type | Description | |---|---|---| | rssi | number | Received signal strength in dBm | | txPower | number | Calibrated TX power at 1 meter (dBm) | | n | number | Path loss exponent (2 = free space, 2.5–4 = indoor) |

const distance = estimateDistance(-72, -59, 2); // ~4.47 meters

getBluetoothStatus(): Promise<BluetoothStatus>

Returns the current Bluetooth adapter state.

const status = await getBluetoothStatus(); // 'on' | 'off'

openBluetoothSettings(): void

Opens the system Bluetooth settings screen (iOS opens app settings, Android opens Bluetooth settings).

openBluetoothSettings();

onBluetoothStatusChange(callback): () => void

Subscribes to Bluetooth state changes. Returns an unsubscribe function.

const unsubscribe = onBluetoothStatusChange((status) => {
  console.log('Bluetooth is now:', status);
  // status: 'on' | 'off' | 'turningOn' | 'turningOff' | ...
});

// Later: stop listening
unsubscribe();

setDebugEnabled(enabled: boolean): void

Enables or disables debug logging to the console.

setDebugEnabled(true);

setSoundFileName(fileName: string): void

Sets the sound file for proximity alerts. Requires react-native-sound-player to be installed.

setSoundFileName('alert.mp3');

The sound file must be bundled in your app's native assets (Android res/raw, iOS main bundle).


startVibration(duration?: number): void

Triggers a single vibration. Only works while a scan is active.

startVibration(200); // 200ms vibration
startVibration();    // default 500ms

KalmanFilter

Standalone 1D Kalman filter class for custom RSSI smoothing.

import { KalmanFilter } from 'react-native-ble-scanner-kit';

const filter = new KalmanFilter(0.01, 0.25); // processNoise, measurementNoise
const smoothed = filter.update(-68);
filter.reset();

Device Tracker Functions

These are used internally by startScan() but exported for advanced use cases.

| Function | Description | |---|---| | updateDeviceTracker(device, enableKalman?, processNoise?, measurementNoise?) | Creates or updates a tracked device entry. | | clearDeviceTimer(deviceId) | Clears the lost-device timeout and pulse interval for a specific device. | | startDeviceLostTimer(device) | Starts a 5-second timeout that emits onDeviceLost if not seen again. | | clearAllDeviceTrackers() | Clears all tracked devices, timers, and Kalman filter state. |


Events

Subscribe via the emitter returned by startScan():

| Event | Payload | Description | |---|---|---| | onDeviceFound | DeviceFoundEvent | A matching device was discovered (with distance). | | onDeviceLost | { id, name, lastSeenAt } | A tracked device was not seen for 5 seconds. | | onStopped | { message } | Scan stopped ("Timeout" or "Stopped"). | | onError | ErrorEvent | An error occurred. | | onBluetoothStateChanged | { bluetoothStatus } | Bluetooth adapter state changed. |

const emitter = startScan('MyDevice');

emitter.addListener('onDeviceFound', (device) => {
  // { id: 'AA:BB:...', name: 'MyDevice', rssi: -62, estimatedDistance: 3.5 }
});

emitter.addListener('onDeviceLost', (info) => {
  console.log(`Lost device ${info.id} at ${info.lastSeenAt}`);
});

emitter.addListener('onStopped', ({ message }) => {
  console.log('Scan ended:', message);
});

emitter.addListener('onError', ({ code, message }) => {
  console.error(`[${code}] ${message}`);
});

Types

type BluetoothStatus =
  | 'on' | 'off' | 'turningOff' | 'turningOn'
  | 'unauthorized' | 'unsupported' | 'resetting' | 'unknown';

type DeviceFoundEvent = {
  id: string;              // MAC address (Android) or UUID (iOS)
  name: string;            // Device name or "Unknown"
  rssi: number;            // Signal strength in dBm
  estimatedDistance: number; // Calculated distance in meters
};

type ScanConfigs = {
  scanDuration?: number;     // Scan timeout in ms (default: 30000)
  enableVibration?: boolean; // Vibrate on proximity (default: false)
  enableSound?: boolean;     // Play sound on proximity (default: false)
  txPower?: number;          // TX power at 1m in dBm (default: -59)
  n?: number;                // Path loss exponent (default: 2)
};

type RSSIFilter = {
  enableKalmanFilter?: boolean;       // Enable RSSI smoothing (default: false)
  kalmanProcessNoise?: number;        // Q parameter (default: 0.01)
  kalmanMeasurementNoise?: number;    // R parameter (default: 0.25)
};

type PulseConfig = {
  distance: number;    // Max distance in meters for this config
  intervalMs: number;  // Alert interval in milliseconds
};

type SentiDriveScanConfigs = {
  rssiFilter?: RSSIFilter;
  scanConfig?: ScanConfigs;
  pulseConfigs?: PulseConfig[];
};

type ErrorEvent = {
  code: 'BT_OFF' | 'PERMISSION_DENIED' | 'DEVICE_NOT_FOUND'
      | 'SCAN_FAILED' | 'NOT_SUPPORTED' | 'INVALID_TARGET_ID'
      | 'IOS_FOREGROUND_REQUIRED';
  message: string;
};

type PermissionResult = {
  success: boolean;
  failedPermissions: string[];
};

How Distance Estimation Works

The module uses the log-distance path loss model:

distance = 10 ^ ((txPower - rssi) / (10 * n))
  • txPower: The RSSI measured at exactly 1 meter from the BLE device. Device-specific, should be calibrated. Common default: -59 dBm.
  • n: The path loss exponent. 2.0 for free space, 2.5–4.0 for indoor environments with obstacles.
  • Kalman filter: When enabled, smooths RSSI readings to reduce distance jitter.

Calibration Tips

  1. Place the BLE device at exactly 1 meter from the phone.
  2. Record RSSI readings for 30 seconds.
  3. Average the readings — this is your txPower value.
  4. For n, start with 2.0 and increase if distance readings are too short in your environment.

How Pulse Alerts Work

Pulse configs define distance-based alert intervals. The module matches the smallest distance threshold that the device is within:

pulseConfigs: [
  { distance: 20, intervalMs: 5000 },  // 10–20m: alert every 5s
  { distance: 10, intervalMs: 3000 },  // 5–10m:  alert every 3s
  { distance: 5,  intervalMs: 2000 },  // 3–5m:   alert every 2s
  { distance: 3,  intervalMs: 1000 },  // 1–3m:   alert every 1s
  { distance: 1,  intervalMs: 500  },  // <1m:    alert every 500ms
]

When a device moves closer, the pulse interval automatically shortens. When it moves beyond all configured distances, alerts stop.


Architecture

┌─────────────────────────────────────────┐
│          JavaScript / TypeScript        │
│                                         │
│  scanner.ts       — Scan orchestration  │
│  deviceTracker.ts — Device state mgmt   │
│  kalmanFilter.ts  — RSSI smoothing      │
│  pulse.ts         — Proximity alerts    │
│  sound.ts         — Audio playback      │
│  permissions.ts   — Permission requests │
└────────────────┬────────────────────────┘
                 │ NativeEventEmitter
┌────────────────┴────────────────────────┐
│         Native Module (Turbo Module)    │
├──────────────────┬──────────────────────┤
│  Android (Kotlin)│     iOS (Obj-C++)    │
│                  │                      │
│  BluetoothLe-    │  CBCentralManager    │
│  Scanner API     │  + CoreBluetooth     │
│                  │                      │
│  BroadcastRcvr   │  Weak delegate proxy │
│  (BT state)      │  (no retain cycles)  │
│                  │                      │
│  AtomicBoolean   │  Block-based timers  │
│  (thread-safe)   │  (no timer leaks)    │
└──────────────────┴──────────────────────┘

New Architecture Support

This module supports both the old bridge and New Architecture (Turbo Modules):

  • TypeScript spec at src/NativeSentiDriveBleModule.ts drives codegen for both platforms.
  • Android: Extends codegen-generated NativeSentiDriveBleModuleSpec base class.
  • iOS: Conditionally conforms to NativeSentiDriveBleModuleSpec protocol via #ifdef RCT_NEW_ARCH_ENABLED.

No consumer configuration needed — it works automatically based on your React Native setup.

Android 16KB Page Alignment

The module targets SDK 35 and uses jniLibs.useLegacyPackaging = false for compatibility with Android 15+ devices that use 16KB memory pages.


Full Example

import React, { useEffect, useState } from 'react';
import { View, Text, Button, FlatList } from 'react-native';
import {
  initializeBluetooth,
  startScan,
  stopScan,
  onBluetoothStatusChange,
  setDebugEnabled,
  type DeviceFoundEvent,
  type BluetoothStatus,
} from 'react-native-ble-scanner-kit';

export default function BLEScanner() {
  const [devices, setDevices] = useState<DeviceFoundEvent[]>([]);
  const [scanning, setScanning] = useState(false);
  const [btStatus, setBtStatus] = useState<BluetoothStatus>('unknown');

  useEffect(() => {
    setDebugEnabled(__DEV__);
    const unsubscribe = onBluetoothStatusChange(setBtStatus);
    return unsubscribe;
  }, []);

  const handleStartScan = async () => {
    try {
      await initializeBluetooth();

      const emitter = startScan(undefined, {
        scanConfig: { scanDuration: 30000, enableVibration: true },
        rssiFilter: { enableKalmanFilter: true },
      });

      setScanning(true);
      setDevices([]);

      emitter.addListener('onDeviceFound', (device) => {
        setDevices((prev) => {
          const idx = prev.findIndex((d) => d.id === device.id);
          if (idx >= 0) {
            const updated = [...prev];
            updated[idx] = device;
            return updated;
          }
          return [...prev, device];
        });
      });

      emitter.addListener('onDeviceLost', ({ id }) => {
        setDevices((prev) => prev.filter((d) => d.id !== id));
      });

      emitter.addListener('onStopped', () => setScanning(false));
    } catch (error) {
      console.error('Scan failed:', error);
    }
  };

  const handleStopScan = () => {
    stopScan();
    setScanning(false);
  };

  return (
    <View style={{ flex: 1, padding: 20 }}>
      <Text>Bluetooth: {btStatus}</Text>
      <Button
        title={scanning ? 'Stop Scan' : 'Start Scan'}
        onPress={scanning ? handleStopScan : handleStartScan}
      />
      <FlatList
        data={devices}
        keyExtractor={(item) => item.id}
        renderItem={({ item }) => (
          <View style={{ paddingVertical: 8 }}>
            <Text style={{ fontWeight: 'bold' }}>{item.name}</Text>
            <Text>ID: {item.id}</Text>
            <Text>RSSI: {item.rssi} dBm</Text>
            <Text>Distance: ~{item.estimatedDistance.toFixed(1)}m</Text>
          </View>
        )}
      />
    </View>
  );
}

Troubleshooting

Permission Denied

try {
  await initializeBluetooth();
} catch (error) {
  if (error.message.includes('PERMISSION_DENIED')) {
    Alert.alert(
      'Permissions Required',
      'Please enable Bluetooth and Location permissions in Settings.',
      [{ text: 'Open Settings', onPress: () => openBluetoothSettings() }]
    );
  }
}

iOS Foreground Requirement

On iOS, BLE scanning requires the app to be in foreground:

import { AppState } from 'react-native';

useEffect(() => {
  const sub = AppState.addEventListener('change', (state) => {
    if (state === 'background') stopScan();
  });
  return () => sub.remove();
}, []);

Sound Not Playing

  • Android: Ensure the file is in android/app/src/main/res/raw/
  • iOS: Ensure the file is added to the Xcode project bundle
  • Install react-native-sound-player: npm install react-native-sound-player

Development

# Install dependencies
npm install

# Type check
npm run typecheck

# Build
npm run build

# Run tests
npm test

# Run tests with coverage
npm run test:coverage

License

Proprietary — Copyright (c) 2024 Ant-Tech. All rights reserved.

This software is proprietary. Unauthorized use, copying, modification, or distribution is strictly prohibited. To obtain a license for commercial or personal use, contact Ant-Tech.