@syncmesh/rn-ble
v0.1.0
Published
React Native BLE central and peripheral module built with Expo Modules
Maintainers
Readme
@syncmesh/rn-ble
React Native BLE central and peripheral, for Expo and bare React Native apps. Built on Expo Modules, with one TypeScript surface across iOS (CoreBluetooth) and Android (BluetoothLe*).
Features
- Central — scan, connect, discover services, read / write / subscribe characteristics, read / write descriptors, MTU, RSSI
- Peripheral — advertise, publish GATT services and characteristics, push values to subscribers
- Cancellable transactions with deadline-based timeouts
- iOS State Preservation & Restoration built in
- Android bonding (list, create, remove)
- Expo config plugin that wires permissions, background modes, and the BLE hardware feature flag for you
Platform support
| Platform | Central | Peripheral | Bonding | Background |
| --- | --- | --- | --- | --- |
| iOS | Yes | Yes | Implicit (CoreBluetooth) | bluetooth-central, bluetooth-peripheral |
| Android | Yes | Yes | Yes | OS-managed (declare a foreground service in your app for long-running scans) |
| Web | No | No | No | No |
| Expo Go | No | No | No | No |
Installation
npm install @syncmesh/rn-ble
# or: yarn add @syncmesh/rn-ble
# or: bun add @syncmesh/rn-bleThen, depending on your project type:
- Expo (managed or prebuild) — add the config plugin below, then run
npx expo prebuild. - Bare React Native — run
cd ios && pod install.
Expo Go is not supported; you need a development build.
Expo config plugin
The plugin wires up the iOS Info.plist and Android AndroidManifest.xml entries needed for BLE central + peripheral use, so you don't have to edit those files by hand. It always declares <uses-feature android:name="android.hardware.bluetooth_le" android:required="true" /> on Android — Google Play will only surface the app to BLE-capable devices.
{
"expo": {
"plugins": [
[
"@syncmesh/rn-ble",
{
"neverForLocation": true,
"modes": ["central", "peripheral"],
"bluetoothAlwaysPermission": "Allow $(PRODUCT_NAME) to connect to nearby Bluetooth devices."
}
]
]
}
}| Option | Type | Default | Platform | Effect |
| --- | --- | --- | --- | --- |
| neverForLocation | boolean | false | Android | Marks BLUETOOTH_SCAN with usesPermissionFlags="neverForLocation" and caps the legacy ACCESS_*_LOCATION permissions at maxSdkVersion="30". |
| modes | ("central" \| "peripheral")[] | [] | iOS | Adds entries to UIBackgroundModes so the app keeps scanning, advertising, or holding GATT connections while backgrounded. |
| bluetoothAlwaysPermission | string \| false | A default sentence | iOS | Sets NSBluetoothAlwaysUsageDescription. Pass false to skip writing it. |
Quick start
Central — scan and connect
import { BleManager } from "@syncmesh/rn-ble";
const SERVICE_UUID = "12345678-1234-5678-1234-56789abcdef0";
const state = await BleManager.getState();
if (state !== "poweredOn") {
throw new Error(`Bluetooth not ready: ${state}`);
}
await BleManager.startScan({ serviceUuids: [SERVICE_UUID] });
const scanSub = BleManager.addListener("onScanResult", async (scan) => {
await BleManager.stopScan();
const connection = await BleManager.connect(scan.peripheralId);
const discovery = await BleManager.discoverServices(connection.connectionId);
console.log(discovery.services);
});Peripheral — publish and advertise
import { BleManager } from "@syncmesh/rn-ble";
const SERVICE_UUID = "12345678-1234-5678-1234-56789abcdef0";
const CHARACTERISTIC_UUID = "12345678-1234-5678-1234-56789abcdef1";
await BleManager.publishServices({
services: [
{
uuid: SERVICE_UUID,
characteristics: [
{
uuid: CHARACTERISTIC_UUID,
properties: ["read", "write", "notify"],
initialValueBase64: "aGVsbG8=",
},
],
},
],
});
await BleManager.startAdvertising({
localName: "SyncMesh Demo",
serviceUuids: [SERVICE_UUID],
});Core concepts
peripheralIdis the platform identifier of the remote device:CBPeripheral.identifier.uuidStringon iOS, a Bluetooth address (or randomized address) on Android.connectionIdmatchesperipheralIdonce a connection is open.- All characteristic and descriptor payloads are exchanged as base64 strings.
- Call
discoverServices()before anyread,write,subscribe,unsubscribe,readDescriptor, orwriteDescriptor. - Most per-connection operations accept an optional
transactionIdyou can later pass tocancelTransaction(transactionId).
API
The full typed surface lives in src/types.ts; native implementations are in ios/RNBleModule.swift and android/src/main. Bonding methods (getBondedDevices, createBond, removeBond) and requestConnectionPriority are Android-only and throw RNBleNotSupportedException on iOS.
import { BleManager } from "@syncmesh/rn-ble";
import type {
BleAdapterState,
BleAdvertisingOptions,
BleConnection,
BleConnectOptions,
BleDiscoveryResult,
BlePeripheralSpec,
BleScanOptions,
BleScanResult,
BleWriteType,
} from "@syncmesh/rn-ble";Platform divergences
- iOS advertising payload — only
localNameandserviceUuidsare honored.manufacturerDataBase64/serviceDataBase64are Android-only. autoConnect— Android pass-through toconnectGatt; ignored on iOS (warning logged).requestMtu— Android triggersBluetoothGatt.requestMtu; iOS negotiates automatically and the argument is ignored.scanMode— Android maps toScanSettings.SCAN_MODE_*; iOS records but does not apply.onRestored— iOS only, fired when CoreBluetooth relaunches the app via State Preservation & Restoration.
Events
Subscribe with BleManager.addListener(name, handler); the returned subscription has .remove(). Payload shapes are in src/types.ts.
| Event | When it fires |
| --- | --- |
| onStateChanged | Adapter state transitions |
| onScanResult | Each peripheral discovered while scanning |
| onConnectionStateChanged | Connect, disconnect, disconnect failures |
| onCharacteristicValueChanged | A subscribed remote characteristic sends a new value |
| onCharacteristicWriteRequested | A remote central writes to one of your published local characteristics |
| onCharacteristicReadRequested | A remote central reads one of your published local characteristics |
| onSubscribersChanged | Centrals subscribe or unsubscribe from a published characteristic |
| onAdvertisingStateChanged | Advertising starts or stops |
| onError | Native-side operational failure |
| onLog | Diagnostic log from the native module |
| onRestored (iOS) | CoreBluetooth relaunched the app in the background via State Preservation & Restoration |
Contributing
Issues and PRs are welcome. To work on the package locally:
bun install
bun run typecheck
bun test
bun run build:plugin # rebuild the Expo config pluginLicense
Apache-2.0 — see LICENSE.
