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
Maintainers
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-permissionsiOS
cd ios && pod installAdd 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_ADMINBLUETOOTH_SCAN,BLUETOOTH_CONNECTACCESS_FINE_LOCATIONVIBRATE
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 metersgetBluetoothStatus(): 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 500msKalmanFilter
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.0for free space,2.5–4.0for indoor environments with obstacles. - Kalman filter: When enabled, smooths RSSI readings to reduce distance jitter.
Calibration Tips
- Place the BLE device at exactly 1 meter from the phone.
- Record RSSI readings for 30 seconds.
- Average the readings — this is your
txPowervalue. - For
n, start with2.0and 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.tsdrives codegen for both platforms. - Android: Extends codegen-generated
NativeSentiDriveBleModuleSpecbase class. - iOS: Conditionally conforms to
NativeSentiDriveBleModuleSpecprotocol 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:coverageLicense
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.
