
Security News
Axios Supply Chain Attack Reaches OpenAI macOS Signing Pipeline, Forces Certificate Rotation
OpenAI rotated macOS signing certificates after a malicious Axios package reached its CI pipeline in a broader software supply chain attack.
react-native-ble-nitro
Advanced tools
High-performance React Native BLE library built on Nitro Modules
A high-performance React Native BLE library built on Nitro Modules.
Originally developed for Zyke Band - a fitness and health tracker created by a small team.
npm install react-native-nitro-modules react-native-ble-nitro
Add the plugin to your app.json or app.config.js:
{
"expo": {
"plugins": [
[
"react-native-ble-nitro",
{
"isBackgroundEnabled": true,
"modes": ["peripheral", "central"],
"bluetoothAlwaysPermission": "Allow $(PRODUCT_NAME) to connect to bluetooth devices"
}
]
]
}
}
Then prebuild and run:
npx expo prebuild
npx expo run:android
# or
npx expo run:ios
For bare React Native projects, the library auto-links. Just run:
npx pod-install # iOS only
import { BleNitro, BleNitroManager, BLEState, AndroidScanMode, type BLEDevice } from 'react-native-ble-nitro';
// Get the singleton instance
const ble = BleNitro.instance();
// Use custom manager instance (e.g. for iOS state restoration)
// It is recommended to create this instance in an extra file seperated from other BLE business logic for better fast-refresh support
const ble = new BleNitroManager({
restoreIdentifier: 'my-unique-identifier',
onRestoredState: (peripherals) => {
console.log('Restored peripherals:', peripherals);
},
});
// Check if Bluetooth is enabled
const isEnabled = ble.isBluetoothEnabled();
// Get current Bluetooth state
const state = ble.state();
// Returns: BLEState.PoweredOn, BLEState.PoweredOff, etc.
// Request to enable Bluetooth (Android only)
await ble.requestBluetoothEnable();
// Subscribe to state changes
const subscription = ble.subscribeToStateChange((state) => {
console.log('Bluetooth state changed:', state);
}, true); // true = emit initial state
// Unsubscribe from state changes
subscription.remove();
// Open Bluetooth settings
await ble.openSettings();
// Explicitly initialize BLE on iOS (optional, iOS only)
// Useful to trigger the Bluetooth permission dialog at a specific moment
// when using lazy iOS initialization.
ble.iosLazyInit();
// Start scanning for devices
ble.startScan({
serviceUUIDs: ['180d'], // Optional: filter by service UUIDs
rssiThreshold: -80, // Optional: minimum signal strength
allowDuplicates: false, // Optional: allow duplicate discoveries
androidScanMode: AndroidScanMode.Balanced // Optional: Android scan mode
}, (device) => {
console.log('Discovered device:', device);
}, (error) => {
// only called on Android
console.error('Scan error:', error);
});
// Stop scanning
ble.stopScan();
// Check if currently scanning
const isScanning = ble.isScanning();
// Get already connected devices
const connectedDevices = ble.getConnectedDevices(['180d']); // Optional: filter by service UUIDs
// Connect to a device with disconnect event handling
const deviceId = await ble.connect(deviceId, (deviceId, interrupted, error) => {
if (interrupted) {
console.log('Connection interrupted:', error);
// Handle unexpected disconnection (out of range, etc.)
} else {
console.log('Disconnected intentionally');
// Handle normal disconnection
}
});
// Connect without disconnect callback
const deviceId = await ble.connect(deviceId);
// You can also use findAndConnect to scan and connect in one step
// This could be useful for reconnecting after app restart or when device was disconnected unexpectedly
const deviceId = await ble.findAndConnect(deviceId, {
scanTimeout: 4000, // default 5000ms
onDisconnect: (deviceId, interrupted, error) => {
if (interrupted) {
console.log('Connection interrupted:', error);
// Handle unexpected disconnection (out of range, etc.)
} else {
console.log('Disconnected intentionally');
// Handle normal disconnection
}
}
});
// Disconnect from a device
await ble.disconnect(deviceId);
// Check connection status
const isConnected = ble.isConnected(deviceId);
// MTU negotiation (Android only, as iOS manages MTU automatically)
// iOS returns current MTU size
const mtu = await ble.requestMTU(deviceId, 256); // Request MTU size
// MTU negotiation (Android only)
// iOS manages MTU automatically, this method returns current MTU size
const newMTU = ble.requestMTU(deviceId, 247);
console.log('MTU set to:', newMTU);
// Read RSSI value
const rssi = await ble.readRSSI(deviceId);
console.log('Current RSSI:', rssi);
// Discover all services for a device
await ble.discoverServices(deviceId);
// Get discovered services
const services = await ble.getServices(deviceId);
// Returns: ['0000180d-0000-1000-8000-00805f9b34fb', '0000180f-0000-1000-8000-00805f9b34fb', ...]
// Always returns full 128-bit UUIDs
// Get characteristics for a service
const characteristics = ble.getCharacteristics(deviceId, serviceUUID);
// Returns: ['00002a37-0000-1000-8000-00805f9b34fb', '00002a38-0000-1000-8000-00805f9b34fb', ...]
// Always returns full 128-bit UUIDs
// Note: You can use either short or long form UUIDs as input:
const characteristics1 = ble.getCharacteristics(deviceId, '180d'); // Short form
const characteristics2 = ble.getCharacteristics(deviceId, '0000180d-0000-1000-8000-00805f9b34fb'); // Long form
// Both work identically - conversion handled automatically
// Get services with their characteristics
const servicesWithCharacteristics = await ble.getServicesWithCharacteristics(deviceId);
// Returns: [{ uuid: '0000180d-0000-1000-8000-00805f9b34fb', characteristics: ['00002a37-0000-1000-8000-00805f9b34fb', ...] }, ...]
// Read a characteristic value
const data = await ble.readCharacteristic(deviceId, serviceUUID, characteristicUUID);
// Returns: ArrayBuffer - binary data
// Example: Reading battery level
const batteryData = await ble.readCharacteristic(deviceId, '180f', '2a19');
const batteryLevel = batteryData[0]; // First byte is battery percentage
console.log('Battery level:', batteryLevel + '%');
// Write to a characteristic with response
const data = [0x01, 0x02, 0x03];
const result = await ble.writeCharacteristic(
deviceId,
serviceUUID,
characteristicUUID,
data, // Data as ArrayBuffer
true // withResponse = true (default)
);
// result is array of integers (may be empty depending on characteristic)
// Android returns the written data if withResponse=true and characteristic returns no data, on iOS it is an empty array
// Write without response (faster, no confirmation)
const emptyResult = await ble.writeCharacteristic(
deviceId,
serviceUUID,
characteristicUUID,
data,
false // withResponse = false
);
// emptyResult is always empty array
[!CAUTION] From version 1.8.0 on the returned subscription object has the type
AsyncSubscriptioninstead ofSubscriptionto indicate that theremovemethod is now async and returns a Promise for better multi-platform compatibility. From version 1.9.0 on thesubscribeToCharacteristicmethod is async, so use await when calling it. This was introduced to fix the handling of gatt queuing on Android.
[!IMPORTANT]
It is only possible to have one active notification subscription per specific characteristic. If you callsubscribeToCharacteristicagain for the same characteristic, the previous subscription won't receive any more updates and should be removed previously.
// Subscribe to characteristic notifications
const subscription = await ble.subscribeToCharacteristic( // before 1.9.0 this was synchronous
deviceId,
serviceUUID,
characteristicUUID,
(characteristicId, data) => {
console.log('Received notification:', data);
// Handle incoming data
}
);
// Unsubscribe from notifications
await subscription.remove();
// Or unsubscribe directly
await ble.unsubscribeFromCharacteristic(deviceId, serviceUUID, characteristicUUID);
const HEART_RATE_SERVICE = '180d';
const HEART_RATE_MEASUREMENT = '2a37';
// Connect and subscribe to heart rate
const autoConnectOnAndroid = true; // Optional: auto-reconnect on Android
const deviceId = await ble.connect(
heartRateDeviceId,
(deviceId, interrupted, error) => {
console.log('Device got Disconnected');
console.log('Was Interrupted?', interrupted);
console.log('Error:', error);
},
autoConnectOnAndroid,
);
await ble.discoverServices(deviceId);
const subscription = await ble.subscribeToCharacteristic(
deviceId,
HEART_RATE_SERVICE,
HEART_RATE_MEASUREMENT,
(_, data) => {
const heartRate = data[1]; // Second byte contains BPM
console.log('Heart rate:', heartRate, 'BPM');
}
);
// Unsubscribe when done
await subscription.remove();
const BATTERY_SERVICE = '180f';
const BATTERY_LEVEL_CHARACTERISTIC = '2a19';
const batteryData = await ble.readCharacteristic(
deviceId,
BATTERY_SERVICE,
BATTERY_LEVEL_CHARACTERISTIC
);
const batteryPercentage = batteryData[0];
console.log('Battery:', batteryPercentage + '%');
const CUSTOM_SERVICE = 'your-custom-service-uuid';
const COMMAND_CHARACTERISTIC = 'your-command-characteristic-uuid';
// Send a custom command
const enableLedCommand = [0x01, 0x1f, 0x01]; // Your protocol
await ble.writeCharacteristic(
deviceId,
CUSTOM_SERVICE,
COMMAND_CHARACTERISTIC,
enableLedCommand
);
🔧 Automatic UUID Conversion
This library automatically handles UUID conversion between 16-bit, 32-bit, and 128-bit formats:
// All input methods accept both short and long form UUIDs:
await ble.readCharacteristic(deviceId, '180d', '2a19'); // Short form ✅
await ble.readCharacteristic(deviceId, '0000180d-0000-1000-8000-00805f9b34fb', '00002a19-0000-1000-8000-00805f9b34fb'); // Long form ✅
// All output methods return full 128-bit UUIDs:
const services = await ble.getServices(deviceId);
// Always returns: ['0000180d-0000-1000-8000-00805f9b34fb', ...]
// Conversion happens automatically on the native side for maximum performance
// Manually normalize UUIDs to full 128-bit format (rarely needed)
const fullUUID = BleNitro.normalizeGattUUID('180d');
// Returns: '0000180d-0000-1000-8000-00805f9b34fb'
// Normalize multiple UUIDs
const fullUUIDs = BleNitro.normalizeGattUUIDs(['180d', '180f']);
// Returns: ['0000180d-0000-1000-8000-00805f9b34fb', '0000180f-0000-1000-8000-00805f9b34fb']
There is built-in support for iOS state restoration. You need to provide a unique identifier and a callback to handle restored peripherals. If no unique identifier is provided, state restoration is disabled.
[!CAUTION] From 1.7.0 on you have to create your own instance of
BleNitroManagerif you want to use state restoration. The singletonBleNitro.instance()will not have state restoration enabled by default anymore.
import { BleNitroManager, BLEDevice } from 'react-native-ble-nitro';
const customBleInstance = new BleNitroManager({
restoreIdentifier: 'my-unique-identifier', // unique identifier for state restoration
onRestoredState: (peripherals: BLEDevice[]) => {
console.log('Restored peripherals:', peripherals);
// Handle restored peripherals
}
});
// Enable state restoration in BleNitro singleton
const ble = BleNitro.instance();
ble.onRestoredState((peripherals) => {
console.log('Restored peripherals:', peripherals);
});
interface BLEDevice {
id: string;
name: string;
rssi: number;
manufacturerData: ManufacturerData;
serviceUUIDs: string[];
isConnectable: boolean;
}
interface ScanFilter {
serviceUUIDs?: string[];
rssiThreshold?: number;
allowDuplicates?: boolean;
androidScanMode?: AndroidScanMode;
}
interface Subscription {
remove: () => Promise<void>;
}
interface AsyncSubscription {
remove: () => Promise<void>;
}
enum BLEState {
Unknown = 'Unknown',
Resetting = 'Resetting',
Unsupported = 'Unsupported',
Unauthorized = 'Unauthorized',
PoweredOff = 'PoweredOff',
PoweredOn = 'PoweredOn'
}
enum AndroidScanMode {
LowLatency = 'LowLatency', // Highest power, fastest discovery
Balanced = 'Balanced', // Balanced power/discovery (default)
LowPower = 'LowPower', // Lowest power, slower discovery
Opportunistic = 'Opportunistic', // Only when other apps are scanning
}
// Callback types
type StateChangeCallback = (state: BLEState) => void;
type ScanEventCallback = (device: BLEDevice) => void;
type ScanErrorCallback = (error: string) => void; // Android only
type DisconnectEventCallback = (deviceId: string, interrupted: boolean, error: string) => void;
type CharacteristicUpdateCallback = (characteristicId: string, data: ArrayBuffer) => void;
Built on Nitro Modules for:
interface BleNitroPluginProps {
isBackgroundEnabled?: boolean; // Enable background BLE support
neverForLocation?: boolean; // Assert no location derivation [Android 12+]
modes?: ('peripheral' | 'central')[]; // iOS background modes
bluetoothAlwaysPermission?: string | false; // iOS permission message
androidAdvertisingEnabled?: boolean; // Android Peripheral mode (advertising)
iOSLazyInit?: boolean; // Lazy init BLE module to prevent permission dialog on app launch (delays state restoration)
}
To use lazy BLE initialization, add the following to your Info.plist:
<key>BLENitroLazyInit</key>
<true/>
With iOS Lazy initialization, BLE will be automatically initialized on the first API call (e.g. startScan(), connect(), state()), which will also trigger the permission dialog. Optionally, you can call iosLazyInit() to explicitly initialize BLE ahead of time:
bleNitro.iosLazyInit();
{
"modes": ["peripheral", "central"]
}
Adds these to Info.plist:
bluetooth-peripheral: Act as BLE peripheral in backgroundbluetooth-central: Scan/connect as central in backgroundAutomatically adds required permissions and also handling neverForLocation and advertise mode.
<!-- Basic Bluetooth -->
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<!-- Location (required for BLE scanning) -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<!-- Android 12+ -->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<!-- BLE Hardware Feature -->
<uses-feature android:name="android.hardware.bluetooth_le" android:required="false" />
import { PermissionsAndroid, Platform } from 'react-native';
const requestPermissionsAndroid = async () => {
if (Platform.OS !== 'android') {
return true
}
if (Platform.OS === 'android' && PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION) {
const apiLevel = parseInt(Platform.Version.toString(), 10);
if (apiLevel < 31) {
const result = await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION);
return (
result === PermissionsAndroid.RESULTS.GRANTED
);
}
if (PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN && PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT) {
const result = await PermissionsAndroid.requestMultiple([
PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN,
PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT,
PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION
])
return (
result['android.permission.BLUETOOTH_CONNECT'] === PermissionsAndroid.RESULTS.GRANTED &&
result['android.permission.BLUETOOTH_SCAN'] === PermissionsAndroid.RESULTS.GRANTED &&
result['android.permission.ACCESS_FINE_LOCATION'] === PermissionsAndroid.RESULTS.GRANTED
)
}
logMessage('Request permissions failed');
throw new Error('Request permissions failed');
}
};
const hasPermissions = await requestPermissionsAndroid();
// Then start scanning or other operations
# Install dependencies
npm install
# Generate native Nitro code
npx nitro-codegen
# Build TypeScript
npm run build
# Run tests
npm test
# Lint code
npm run lint
Start Android Studio from terminal to inherit correct PATH:
open -a Android\ Studio.app
react-native-ble-nitro/
├── src/
│ ├── specs/ # Nitro module TypeScript specs
│ ├── utils/ # Utility functions (UUID, Base64)
│ └── errors/ # BLE error handling
├── nitrogen/generated/ # Generated native code (Nitro)
├── plugin/ # Expo config plugin
├── ios/ # iOS native implementation (Swift)
├── android/ # Android native implementation (Kotlin)
└── docs/ # Documentation
We welcome contributions! Please see CONTRIBUTING.md for guidelines.
git clone https://github.com/YOUR_USERNAME/react-native-ble-nitro.gitgit remote add upstream https://github.com/zykeco/react-native-ble-nitro.gitnpm installnpx nitro-codegennpm testMIT License - see LICENSE file.
Made with ❤️ for the React Native community
FAQs
High-performance React Native BLE library built on Nitro Modules
We found that react-native-ble-nitro demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
OpenAI rotated macOS signing certificates after a malicious Axios package reached its CI pipeline in a broader software supply chain attack.

Security News
Open source is under attack because of how much value it creates. It has been the foundation of every major software innovation for the last three decades. This is not the time to walk away from it.

Security News
Socket CEO Feross Aboukhadijeh breaks down how North Korea hijacked Axios and what it means for the future of software supply chain security.