@cloud2303/react-native-ble
v0.0.3
Published
A Nitro-powered React Native BLE module with a WeChat Mini Program-style Promise API
Readme
@cloud2303/react-native-ble
A cross-platform React Native BLE module with a WeChat Mini Program/Taro-style Promise API. The transport is implemented as a Nitro HybridObject using CoreBluetooth on iOS and Blessed on Android.
Requirements
- React Native with the New Architecture enabled.
react-native-nitro-modules0.37.1.- iOS 16.4 or newer.
- Android API 28 or newer.
- A development build or a native React Native build. Expo Go is not supported.
Android uses blessed-kotlin 3.0.12. The Expo 57 app uses the default
Kotlin compiler supplied by Expo / React Native, without a compiler override.
For Expo apps, set android.minSdkVersion: 28 in expo-build-properties.
Installation
pnpm add @cloud2303/react-native-ble react-native-nitro-modulesFor an Expo app, add the config plugin to app.json or app.config.ts:
{
"expo": {
"plugins": [
[
"@cloud2303/react-native-ble",
{
"bluetoothAlwaysPermission": "Allow $(PRODUCT_NAME) to connect to Bluetooth devices.",
"modes": ["central"]
}
]
]
}
}The plugin accepts these options:
bluetoothAlwaysPermission: iOS Bluetooth usage description. Set it tofalseto leave the existing value unchanged.modes: iOS background Bluetooth modes, such ascentralorperipheral.
After changing native configuration, rebuild the development client:
pnpm expo prebuild
pnpm expo run:ios
# or
pnpm expo run:androidBasic usage
The public API is exposed through the wechatBle singleton. Call
openBluetoothAdapter() before any other BLE operation. The first call also
requests the required Android Bluetooth permissions.
import { wechatBle } from '@cloud2303/react-native-ble'
const SERVICE_UUID = '0000180d-0000-1000-8000-00805f9b34fb'
const CHARACTERISTIC_UUID = '00002a37-0000-1000-8000-00805f9b34fb'
const onDeviceFound = ({ devices }: { devices: { deviceId: string; name: string }[] }) => {
const device = devices[0]
if (device) {
console.log('Found device:', device.deviceId, device.name)
}
}
await wechatBle.openBluetoothAdapter()
wechatBle.onBluetoothDeviceFound(onDeviceFound)
await wechatBle.startBluetoothDevicesDiscovery({
services: [SERVICE_UUID],
allowDuplicatesKey: false,
})
const { devices } = await wechatBle.getBluetoothDevices()
const deviceId = devices[0]?.deviceId
if (deviceId) {
await wechatBle.stopBluetoothDevicesDiscovery()
await wechatBle.createBLEConnection({ deviceId, timeout: 15_000 })
const { services } = await wechatBle.getBLEDeviceServices({ deviceId })
const { characteristics } = await wechatBle.getBLEDeviceCharacteristics({
deviceId,
serviceId: services[0].uuid,
})
await wechatBle.notifyBLECharacteristicValueChange({
deviceId,
serviceId: services[0].uuid,
characteristicId: characteristics[0].uuid,
state: true,
})
await wechatBle.closeBLEConnection({ deviceId })
}
wechatBle.offBluetoothDeviceFound(onDeviceFound)
await wechatBle.closeBluetoothAdapter()Notification mode
notifyBLECharacteristicValueChange uses state: true to enable receiving
updates and state: false to disable them.
- Omit
typeto prefer Notify, falling back to Indicate when only that mode is supported. Neither property supported results inerrCode: 10007. - Set
type: 'notification'ortype: 'indication'to validate that the characteristic supports that property before enabling updates. Unsupported properties result inerrCode: 10007. - Android selects the requested mode when starting a subscription. On iOS,
CoreBluetooth chooses the mode; when both properties are present, it uses
Notify even if
type: 'indication'passed the capability check. - Disabling updates does not require the characteristic to support the supplied
mode. A repeated call with the same
statedoes not restart the subscription; on Android, disable it first if you need to change an active subscription's mode.
See Apple's setNotifyValue documentation.
Operation queues and cancellation
Service/characteristic discovery, RSSI reads, notification configuration,
characteristic reads/writes (including writeNoResponse), and MTU changes run
serially per device. Different devices have independent queues.
Operations still waiting in the JS queue reject immediately when:
closeBLEConnectionis called or a disconnect event arrives:errCode: 10006.closeBluetoothAdapteris called:errCode: 10000for all device queues.- The adapter becomes unavailable:
errCode: 10001for all device queues.
Already submitted operations finish or fail through their native callbacks and
native timeouts. Adapter close owns cancellation of native work; JS does not
wrap every native call in another cancellation layer. JS connection timeout
timers are cleared on adapter close, and their waits reject with 10000 so an
old timer cannot disconnect a reopened adapter. Reconnecting starts a fresh
queue; old tasks cannot resume the discarded queue, and an old connection
result cannot change the new session's state.
The two internal adapter/connection subscriptions support queue cancellation
even without public state listeners. Close releases these two subscriptions
and resets scanning flags and caches in finally; open recreates them.
Public event subscriptions remain controlled by the caller's matching
on... / off... calls, with no automatic restoration layer. Failed native
initialization also attempts to close its partially created adapter before
reporting the original error.
Business reply matching, retries, and reply timeouts belong in the application's Store; write completion does not mean a business reply has arrived.
API overview
The methods intentionally follow the WeChat BLE naming and result shape:
- Adapter:
openBluetoothAdapter,closeBluetoothAdapter,getBluetoothAdapterState. - Discovery:
startBluetoothDevicesDiscovery,stopBluetoothDevicesDiscovery,getBluetoothDevices. - Connection:
createBLEConnection,closeBLEConnection,getConnectedBluetoothDevices. - GATT:
getBLEDeviceServices,getBLEDeviceCharacteristics,getBLEDeviceRSSI,readBLECharacteristicValue,writeBLECharacteristicValue,notifyBLECharacteristicValueChange. - Pairing and MTU:
makeBluetoothPair,isBluetoothDevicePaired,setBLEMTU,getBLEMTU. - Events:
onBluetoothDeviceFound,onBluetoothAdapterStateChange,onBLEConnectionStateChange,onBLECharacteristicValueChange, andonBLEMTUChange, each with a matchingoff...method.
For page teardown, the separately exported
closeBluetoothAdapterIfInitialized() checks the runtime's initialization state
before closing. It does nothing when unopened, waits for an in-flight open or
close, and propagates failures. Native adapter close stops scanning and releases
device connections. The standard wechatBle.closeBluetoothAdapter() still
rejects with 10000 when called before initialization.
Writes accept an ArrayBuffer. For example:
const value = Uint8Array.from([0x01, 0x02]).buffer
await wechatBle.writeBLECharacteristicValue({
deviceId,
serviceId: SERVICE_UUID,
characteristicId: CHARACTERISTIC_UUID,
value,
writeType: 'write',
})Successful calls resolve to an object containing an errMsg such as
"openBluetoothAdapter:ok". Native and validation failures throw
WechatBleError when they can be mapped to a WeChat error code; inspect
errCode, nativeCode, and errMsg when handling failures.
UUIDs are normalized to lowercase canonical UUIDs. BLE operations for the same device are serialized to avoid overlapping GATT requests.
Development
pnpm install
pnpm typecheck
pnpm test
pnpm lint
pnpm specs
pnpm build:pluginspecs regenerates the Nitro bindings using the pinned Nitro/Nitrogen version.
Do not edit lib/ or plugin/build/ by hand; they are generated during the
package build.
License
MIT
Nitro generation on iOS
Nitro/Nitrogen 0.37.1 still requires the indexed C++ vector access workaround
for this project's Swift bridges. Unpatched generation fails with
std::vector ... has no member map on Xcode 26.6. After running specs, apply
the repository repair workflow
before building iOS; regeneration overwrites those generated-file repairs.
