SimpleBLE

Usage

Install and use the SimpleRnBLE React Native binding on iOS and Android.

SimpleRnBLE exposes SimpleBLE to React Native apps on iOS and Android, with promises, events, and TypeScript types. It is a Nitro module: calls go straight from JavaScript to SimpleBLE's C++ API, without a bridge in between.

Requirements

  • React Native 0.80 or newer, with the New Architecture.
  • react-native-nitro-modules 0.37.
  • iOS 15.8 or newer.
  • Android 12 (API 31) or newer, with the Android NDK installed.

Install from npm

npm install simplernble react-native-nitro-modules
cd ios && pod install

Your app targets the same platform versions: set minSdkVersion = 31 in android/build.gradle, and platform :ios, '15.8' in ios/Podfile.

The package carries SimpleBLE prebuilt for iOS, and its sources for Android, which your app's first Android build compiles with the NDK. Development builds of the main branch are published under the dev tag:

npm install simplernble@dev

Expo

SimpleRnBLE contains native code, so Expo Go cannot load it. Use a development build, created with npx expo prebuild or EAS Build. Install the packages with npx expo install simplernble react-native-nitro-modules expo-build-properties, then set the platform versions and the iOS permission text in app.json:

{
  "expo": {
    "ios": {
      "infoPlist": {
        "NSBluetoothAlwaysUsageDescription": "This app uses Bluetooth to connect to your devices."
      }
    },
    "plugins": [
      ["expo-build-properties", { "android": { "minSdkVersion": 31 }, "ios": { "deploymentTarget": "15.8" } }]
    ]
  }
}

Permissions

On iOS, add NSBluetoothAlwaysUsageDescription to your app's Info.plist. iOS asks the user the first time the app uses Bluetooth.

On Android, SimpleRnBLE's manifest declares BLUETOOTH_SCAN, BLUETOOTH_CONNECT, and BLUETOOTH_ADVERTISE, and your app asks for them at runtime before using Bluetooth:

import { PermissionsAndroid, Platform } from 'react-native';

async function requestBluetooth(): Promise<boolean> {
  if (Platform.OS !== 'android') return true;
  const result = await PermissionsAndroid.requestMultiple([
    PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN,
    PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT,
  ]);
  return Object.values(result).every(status => status === PermissionsAndroid.RESULTS.GRANTED);
}

Add BLUETOOTH_ADVERTISE to the list to host a local peripheral.

BLUETOOTH_SCAN is declared with neverForLocation, so scanning needs no location permission, and Android leaves some beacons out of the results. To scan for beacons, remove the flag in your app's AndroidManifest.xml, under a <manifest> element that declares xmlns:tools="http://schemas.android.com/tools", and request ACCESS_FINE_LOCATION at runtime as well:

<uses-permission android:name="android.permission.BLUETOOTH_SCAN" tools:remove="android:usesPermissionFlags" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

First scan

import { Adapter } from 'simplernble';

const [adapter] = await Adapter.getAdapters();

adapter.on('scanFound', peripheral => {
  console.log(peripheral.identifier(), peripheral.address(), peripheral.rssi());
});
await adapter.scanFor(5000);

For more, see Quickstart, Read, Write, Notify, and the SimpleRnBLE Examples.

Calls, events, and lifetime

Calls that reach the Bluetooth stack return promises and never block the JavaScript thread. Calls on one object run in the order they were made: a read() issued after connect() waits for the connection. Data the library already holds, such as identifier(), rssi(), manufacturerData(), or services(), is returned directly.

Adapter, Peripheral, LocalPeripheral, and LocalCharacteristic emit events, with on(), once(), and off() as in Node.js. Events are delivered on the JavaScript thread in the order they happened.

Every enumeration returns the same Adapter object for an adapter, and an adapter gives you one Peripheral object per device, whether it came from scanFound, scanGetResults(), or getConnectedPeripherals(). Compare them with === and keep state on them, for example in React state or a Map.

Objects with listeners or subscriptions stay alive without other references, so remove listeners when a component unmounts:

useEffect(() => {
  const update = () => setPeripherals(adapter.scanGetResults());
  adapter.on('scanFound', update).on('scanUpdated', update);
  return () => {
    adapter.off('scanFound', update).off('scanUpdated', update);
  };
}, [adapter]);

Failures reject with a SimpleBLEError. Its code is SimpleBLE's error code, such as 'PERIPHERAL_NOT_CONNECTED' or 'GATT_CHARACTERISTIC_NOT_FOUND'. Invalid arguments are TypeErrors or RangeErrors.

Connect and use GATT

const SERVICE = '0000180f-0000-1000-8000-00805f9b34fb';
const BATTERY_LEVEL = '00002a19-0000-1000-8000-00805f9b34fb';

peripheral.on('disconnected', () => console.log('Disconnected'));
await peripheral.connect();

for (const service of peripheral.services()) {
  console.log(service.uuid, service.characteristics.map(c => c.uuid));
}

const level = await peripheral.read(SERVICE, BATTERY_LEVEL);
await peripheral.notify(SERVICE, BATTERY_LEVEL, value => console.log(`Battery: ${value[0]}%`));

UUIDs are full 128-bit strings. Values are Uint8Arrays, both read and written. writeRequest() waits for the peripheral to confirm, while writeCommand() does not.

notify() and indicate() call your function with each value until unsubscribe() or a disconnection; after a disconnection, subscribe again once reconnected. Calling either again for the same characteristic replaces the function.

Host a local peripheral

adapter.createLocalPeripheral() returns a LocalPeripheral that hosts GATT services on that adapter. Configure the advertisement and services, then call start(); call stop() before changing them.

const SERVICE = '12345678-1234-5678-1234-56789abcdef0';
const ECHO = '12345678-1234-5678-1234-56789abcdef1';

const host = adapter.createLocalPeripheral();
host.addAdvertisedService(SERVICE);

const echo = host.addService(SERVICE).addCharacteristic(ECHO, ['read', 'writeRequest', 'notify']);
await echo.setValue(Uint8Array.from('ready', c => c.charCodeAt(0)));

// Echo each write to subscribed clients.
echo.on('write', value => echo.setValue(value));
host.on('clientConnected', address => console.log(`${address} connected`));

await host.start();

setValue() stores a value and publishes it to subscribed clients when the characteristic can notify or indicate. Characteristics emit write with the written value, which value() already returns, and subscribed and unsubscribed when the first client subscribes and the last one leaves. The local peripheral emits clientConnected and clientDisconnected with the client's address, or a platform-specific identifier where the platform hides it.

On Android, setAdvertisedManufacturerData(companyId, data) adds manufacturer-specific data to the advertisement, and setConnectable(false) makes it non-connectable, for example to advertise a beacon. CoreBluetooth advertises only a local name and service UUIDs, so on iOS both throw OPERATION_NOT_SUPPORTED.

To compute values on demand, setReadHandler() takes a function that answers each read instead of the stored value, until you pass null. It can return a promise and take as long as it needs; the client's read waits for it. If the handler throws or rejects, the client reads an empty value and the error is uncaught, like an error thrown from a timer. setReadHandler() returns a promise that resolves once the characteristic uses the handler.

Configure backends

Config holds app-wide settings grouped by backend. Set them at startup, before the first call that reaches a backend.

import { Config } from 'simplernble';

Config.android.connectionPriorityRequest = 'high';

connectionPriorityRequest is requested after each Android connection: 'balanced', 'high', 'lowPower', or 'dck', and 'disabled', the default, keeps Android's choice. Config.android.reset() restores the defaults, and Config.resetAll() restores every group.

Logging

SimpleBLE writes its log to the system log, up to the info level: logcat on Android, under the SimpleBLE tag, and the Xcode console on iOS. Logging changes that for the whole app:

import { Logging } from 'simplernble';

Logging.level = 'warn';
Logging.setCallback(entry => console.warn(`[${entry.level}] ${entry.module}: ${entry.message}`));

setCallback() delivers each entry, with its level, module, file, line, function, and message, on the JavaScript thread in the order SimpleBLE logged them, and null discards entries. logToFile(path) appends them to a file in a directory your app can write to, and logToSystem() restores the default. Levels go from 'none', which turns logging off, to 'verbose'.

Platform-specific features

  • On iOS, host.setAdvertisementLocalName('Name') sets the local name in a local peripheral's advertisement before start(), and null removes it. This changes only the advertisement, not the device's Bluetooth name.
  • On iOS, await adapter.retrieveCachedPeripherals(identifiers) returns the peripherals CoreBluetooth already knows, by the identifiers address() returned earlier, without scanning. Unknown identifiers are skipped, and each peripheral is the same object a scan returns. connect() then reaches it if it is in range.

On Android, both throw OPERATION_NOT_SUPPORTED.

Platform notes

  • iOS: CoreBluetooth hides peripheral addresses, so address() is a UUID that iOS assigns to the peripheral for your app. It stays the same across launches, which makes it the identifier to store and pass to retrieveCachedPeripherals().
  • iOS: A local peripheral emits clientConnected when a client first subscribes, and clientDisconnected when it last unsubscribes, since CoreBluetooth reports no connections to it.
  • Android: SimpleRnBLE initializes itself when your app starts, so it needs no setup code. address() is the peripheral's Bluetooth address.
  • Simulators and emulators have no Bluetooth devices to reach. Test Bluetooth on a phone.

On this page