Skip to content

Latest commit

 

History

History
1188 lines (897 loc) · 44.3 KB

File metadata and controls

1188 lines (897 loc) · 44.3 KB

Munim Technologies Bluetooth

munim-bluetooth

Package version License: Apache-2.0 Downloads Total Downloads

Works with Expo  •  Read the Documentation  •  Report Issues

Follow Munim Technologies

Munim Technologies on GitHub   Munim Technologies on LinkedIn   Munim Technologies Website

Introduction

munim-bluetooth is a comprehensive React Native Bluetooth library for BLE central/peripheral work, Android-only Classic Bluetooth APIs, LE L2CAP channels where the OS exposes them, and Apple Multipeer Connectivity for iOS/iPadOS peer messaging. It lets your React Native app advertise services, scan, connect, read, write, subscribe, exchange nearby peer messages, and check platform capabilities before using optional APIs.

Fully compatible with Expo! Works seamlessly with both Expo managed and bare workflows.

Built with React Native's Nitro modules architecture for high performance and reliability.

Note: Bluetooth is heavily platform-gated. This library exposes the features that iOS and Android make available to third-party apps, and it reports unsupported OS-level capabilities through getCapabilities() or explicit unsupported errors instead of silently pretending they work.

Table of contents

📚 Documentation

Learn about building BLE apps in our documentation!

🚀 Features

Peripheral Mode

  • 🔵 BLE Peripheral Mode: Transform your React Native app into a BLE peripheral device
  • 📡 Service Advertising: Advertise custom GATT services with multiple characteristics
  • 🔄 Real-time Communication: Support for read, write, and notify operations
  • Platform-Aware BLE Advertising: Use service UUIDs and local names cross-platform, plus Android advertising payload data where the OS allows it
  • 🔧 Dynamic Updates: Update advertising data while advertising is active

Central Mode

  • 🔍 Device Scanning: Scan for BLE devices with filtering options
  • 🔗 Device Connection: Connect and disconnect from BLE devices
  • 📊 GATT Operations: Discover services, read/write characteristics
  • 🔔 Notifications: Subscribe to characteristic notifications/indications
  • 📶 RSSI Monitoring: Read signal strength for connected devices

Additional Features

  • 📱 Cross-platform: Works on both iOS and Android
  • 🧭 Capability Reporting: getCapabilities() reports platform and hardware support before you call optional APIs
  • 🕸️ Apple Multipeer Transport: iOS/iPadOS devices can discover, invite, and message nearby peers with Apple's Multipeer Connectivity
  • 🧵 LE L2CAP Channels: Stream payloads over LE Credit Based Channels on supported iOS and Android versions
  • 🔌 Android Classic Bluetooth: Android RFCOMM client/server messaging for SPP-style devices
  • 🎯 TypeScript Support: Full TypeScript definitions included
  • High Performance: Built with React Native's Nitro modules architecture
  • 🚀 Expo Compatible: Works seamlessly with Expo managed and bare workflows
  • 🔐 Permission Handling: Built-in permission request helpers

Platform Support Matrix

Capability iOS Android Notes
Peripheral advertising iOS only allows CoreBluetooth-supported advertising keys such as local name and service UUIDs. Android splits primary advertising data and scan response data to stay within BLE size limits.
Peripheral GATT services Read and write requests are handled natively on both platforms. Included services are wired when supplied in setServices().
Peripheral notify/indicate subscriptions Subscribe/unsubscribe events are emitted when centrals change CCC state.
Central scan Android scan failures emit scanFailed.
Central connect/disconnect connect() has a native 15 second timeout.
Central service discovery Emits servicesDiscovered in addition to resolving the Promise. Native timeout rejects if callbacks do not arrive.
Central characteristic read Resolves with hex-encoded values. Native timeout rejects if callbacks do not arrive.
Central characteristic write Supports write and writeWithoutResponse. With-response writes have native timeout protection.
Central descriptor read/write Uses readDescriptor() and writeDescriptor() with hex-encoded values. Native timeout rejects if callbacks do not arrive.
Central notify/indicate subscription Values emit through characteristicValueChanged.
RSSI read Resolves with dBm.
ATT MTU request Android supports requestMTU(). iOS negotiates ATT MTU internally and does not expose a public setter.
BLE PHY read/preference Android 8+ supports readPhy() and setPreferredPhy() when hardware allows it.
Pairing/bond state Android supports bond state and starts/removes bonds. iOS handles pairing automatically and does not expose bond management through CoreBluetooth.
Extended advertising Android 8+ supports startExtendedAdvertising() on hardware with LE extended advertising. iOS does not expose BLE extended advertising.
BLE L2CAP channel streams iOS uses CoreBluetooth LE Credit Based Channels. Android requires Android 10+ for LE CoC sockets.
Classic Bluetooth RFCOMM Android supports discovery, SPP-style RFCOMM client connections, server/listener sockets, disconnect, write, and receive events. iOS apps cannot use public Classic Bluetooth RFCOMM APIs.
Apple Multipeer Connectivity iOS/iPadOS devices can discover peers, auto-invite/accept sessions, and exchange encrypted messages. Android cannot join Apple's Multipeer sessions; use BLE/GATT for iOS-to-Android.

Call getCapabilities() at runtime when you need optional behavior. Platform support can still vary by OS version, hardware, permissions, and app background state.

📦 Installation

React Native CLI

npm install munim-bluetooth react-native-nitro-modules
# or
yarn add munim-bluetooth react-native-nitro-modules

Expo

npx expo install munim-bluetooth react-native-nitro-modules

Note: This library requires Expo SDK 50+ and works with both managed and bare workflows. To support Nitro modules, you need React Native version v0.78.0 or higher.

Important: This package requires a native development build in Expo. It does not work in Expo Go. After installing, run npx expo run:ios, npx expo run:android, or create a development build with EAS.

iOS Setup

For iOS, the library is automatically linked. However, you need to add the following to your Info.plist:

<key>NSBluetoothAlwaysUsageDescription</key>
<string>This app uses Bluetooth for BLE communication</string>
<key>NSBluetoothPeripheralUsageDescription</key>
<string>This app uses Bluetooth to create a peripheral device</string>
<key>NSLocalNetworkUsageDescription</key>
<string>This app uses the local network to discover and communicate with nearby peer devices</string>
<key>NSBonjourServices</key>
<array>
  <string>_munim-mesh._tcp</string>
</array>

For Expo projects, add these permissions to your app.json:

{
  "expo": {
    "ios": {
      "infoPlist": {
        "NSBluetoothAlwaysUsageDescription": "This app uses Bluetooth for BLE communication",
        "NSBluetoothPeripheralUsageDescription": "This app uses Bluetooth to create a peripheral device",
        "NSLocalNetworkUsageDescription": "This app uses the local network to discover and communicate with nearby peer devices",
        "NSBonjourServices": ["_munim-mesh._tcp"]
      }
    }
  }
}

With the included Expo config plugin, the default munim-mesh Multipeer service is declared automatically. For custom service types:

{
  "expo": {
    "plugins": [
      [
        "munim-bluetooth",
        {
          "multipeerServiceTypes": ["anonmesh", "munim-mesh"],
          "localNetworkUsageDescription": "This app discovers nearby private wallet peers."
        }
      ]
    ]
  }
}

Android Setup

For Android, add the following permissions to your AndroidManifest.xml:

<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

For Expo projects, add these permissions to your app.json:

{
  "expo": {
    "android": {
      "permissions": [
        "android.permission.BLUETOOTH",
        "android.permission.BLUETOOTH_ADMIN",
        "android.permission.BLUETOOTH_ADVERTISE",
        "android.permission.BLUETOOTH_SCAN",
        "android.permission.BLUETOOTH_CONNECT",
        "android.permission.ACCESS_FINE_LOCATION",
        "android.permission.ACCESS_COARSE_LOCATION"
      ]
    }
  }
}

Device-to-Device Messaging

For phone-to-phone apps, treat advertising as discovery and GATT as the reliable message channel:

  • A peripheral advertises one or more service UUIDs, defines writable/readable/notifiable characteristics with setServices(), and listens for peripheralReadRequest, peripheralWriteRequest, peripheralSubscribed, and peripheralUnsubscribed.
  • A central scans for that service, connects, discovers services, reads or writes the characteristic, and subscribes for notifications through characteristicValueChanged.
  • Multiple nearby centrals can connect, write, and subscribe at the same time. Track peers by deviceId on the central side and centralId on the peripheral side.

Advertising payload caveat: Android can advertise manufacturer data, service data, TX power, appearance, and related fields subject to BLE payload size and hardware limits. iOS public CoreBluetooth peripheral advertising only exposes local name and service UUIDs, so arbitrary relay bytes should go in a GATT characteristic for iOS-to-iOS and iOS-to-Android communication. If you need a tiny discovery hint on iOS, encode it into your advertised service UUID choice or local name with the normal privacy and size tradeoffs.

Apple Multipeer Connectivity

For iOS-to-iOS or iPadOS-to-iOS communication, startMultipeerSession() exposes Apple's Multipeer Connectivity as a higher-level peer transport. It advertises and browses using a Bonjour service type, auto-invites discovered peers by default, accepts incoming invitations by default, and sends hex-encoded payloads to one peer or all connected peers.

import {
  addEventListener,
  sendMultipeerMessage,
  startMultipeerSession,
  stopMultipeerSession,
} from 'munim-bluetooth'

startMultipeerSession({
  serviceType: 'munim-mesh',
  displayName: 'Sheehan iPhone',
  discoveryInfo: [{ key: 'role', value: 'wallet-peer' }],
  autoInvite: true,
  autoAcceptInvitations: true,
  encryptionPreference: 'required',
})

addEventListener('multipeerPeerStateChanged', async (peer) => {
  if (peer.state === 'connected') {
    await sendMultipeerMessage('68656c6c6f', [peer.id], true)
  }
})

addEventListener('multipeerMessageReceived', ({ displayName, value }) => {
  console.log('message from', displayName, value)
})

// Later:
stopMultipeerSession()

Multipeer service types must be 1-15 lowercase letters/numbers/hyphens, and the matching Bonjour entry must be declared in iOS Info.plist as _<serviceType>._tcp (for example _munim-mesh._tcp). The Expo config plugin adds _munim-mesh._tcp by default and accepts a multipeerServiceTypes option for custom service types.

Background and Terminated Behavior

startBackgroundSession() starts a best-effort BLE session for apps that need nearby communication after the user leaves the app.

State iOS Android
App in background or suspended Supported when UIBackgroundModes includes bluetooth-central and/or bluetooth-peripheral. The Expo config plugin adds both by default. Supported through a foreground service with connectedDevice service type and a user-visible notification.
App terminated by the system Best-effort CoreBluetooth state restoration is enabled when background modes are present. The package uses restoration identifiers and emits backgroundSessionRestored when CoreBluetooth restores central/peripheral state. On iOS 26 and later, Apple's Bluetooth relaunch rules require AccessorySetupKit eligibility for background relaunch, so arbitrary phone-to-phone BLE mesh apps should not depend on terminated-state relaunch. The foreground service persists its session config and uses START_STICKY; if the process is recreated, it restores scan, advertising, and a native GATT characteristic store from the services configured with setServices().
User force-quits / force-stops the app Not supported by iOS for ongoing app-owned BLE work. The user has explicitly stopped the app. Not supported after Android force stop. The OS prevents the app from running again until the user opens it or another allowed user/system action starts it.

Background sessions are for keeping discovery and small GATT messages alive. They do not make JavaScript execute indefinitely. If the process is alive, normal JS events such as peripheralWriteRequest and characteristicValueChanged continue. After a system restart, iOS may relaunch the app only when Apple's current CoreBluetooth restoration rules allow it; Android restores native scan/advertise/GATT state in the foreground service, and app-specific business logic should reconcile state when the app opens again.

import {
  setServices,
  startBackgroundSession,
  stopBackgroundSession,
} from 'munim-bluetooth'

setServices([
  {
    uuid: SERVICE_UUID,
    characteristics: [
      {
        uuid: CHARACTERISTIC_UUID,
        properties: ['read', 'write', 'writeWithoutResponse', 'notify'],
        value: '70696e67',
      },
    ],
  },
])

startBackgroundSession({
  serviceUUIDs: [SERVICE_UUID],
  localName: 'MunimPeer',
  scanMode: 'lowPower',
  androidNotificationTitle: 'Nearby mode',
  androidNotificationText: 'Keeping Bluetooth available nearby',
})

// Later:
stopBackgroundSession()

⚡ Quick Start

Basic Usage - Peripheral Mode

import { startAdvertising, stopAdvertising, setServices } from 'munim-bluetooth'

// Start advertising with basic options
startAdvertising({
  serviceUUIDs: ['180D', '180F'],
  localName: 'My Device',
})

// Set GATT services
setServices([
  {
    uuid: '180D',
    characteristics: [
      {
        uuid: '2A37',
        properties: ['read', 'write', 'writeWithoutResponse', 'notify'],
        value: '48656c6c6f20576f726c64',
      },
    ],
  },
])

// Stop advertising
stopAdvertising()

Basic Usage - Central Mode

import {
  addDeviceFoundListener,
  isBluetoothEnabled,
  requestBluetoothPermission,
  startScan,
  stopScan,
  connect,
  discoverServices,
  readCharacteristic,
  subscribeToCharacteristic,
} from 'munim-bluetooth'

const hasPermission = await requestBluetoothPermission()
if (!hasPermission) {
  throw new Error('Bluetooth permission was not granted')
}

const enabled = await isBluetoothEnabled()
if (!enabled) {
  throw new Error('Bluetooth is turned off')
}

const removeDeviceFoundListener = addDeviceFoundListener((device) => {
  console.log('Found device:', device.id, device.name)
})

startScan({
  serviceUUIDs: ['180D'],
  allowDuplicates: false,
  scanMode: 'balanced',
})

// Later, after choosing a discovered device ID:
await connect('device-id-here')
const services = await discoverServices('device-id-here')
const value = await readCharacteristic('device-id-here', '180D', '2A37')
subscribeToCharacteristic('device-id-here', '180D', '2A37')

// Cleanup when finished scanning
stopScan()
removeDeviceFoundListener()

Peripheral Write and Subscribe Events

import {
  addEventListener,
  setServices,
  startAdvertising,
  updateCharacteristicValue,
} from 'munim-bluetooth'

const SERVICE_UUID = '71f271d0-8f4c-4c4d-8a2d-6f3a9497b41d'
const CHARACTERISTIC_UUID = '4ad4a6d2-3f4a-477c-9832-5e0d8f7654d8'

setServices([
  {
    uuid: SERVICE_UUID,
    characteristics: [
      {
        uuid: CHARACTERISTIC_UUID,
        properties: ['read', 'write', 'writeWithoutResponse', 'notify'],
        value: '70696e67',
      },
    ],
  },
])

addEventListener('peripheralWriteRequest', ({ centralId, value }) => {
  console.log('Peer wrote', centralId, value)
  updateCharacteristicValue(SERVICE_UUID, CHARACTERISTIC_UUID, value, true)
})

addEventListener('peripheralSubscribed', ({ centralId }) => {
  console.log('Peer subscribed', centralId)
  updateCharacteristicValue(SERVICE_UUID, CHARACTERISTIC_UUID, '706f6e67', true)
})

startAdvertising({
  serviceUUIDs: [SERVICE_UUID],
  localName: 'MunimPeer',
})

Advanced Usage with Android Advertising Data Types

import {
  startAdvertising,
  updateAdvertisingData,
  getAdvertisingData,
  type AdvertisingDataTypes,
} from 'munim-bluetooth'

// Android advertising data configuration. iOS peripheral advertising only
// broadcasts service UUIDs/local name through public CoreBluetooth APIs.
const advertisingData: AdvertisingDataTypes = {
  // 0x01 - Flags (LE General Discoverable Mode, BR/EDR Not Supported)
  flags: 0x06,

  // 0x02-0x07 - Service UUIDs
  completeServiceUUIDs16: ['180D', '180F'],
  incompleteServiceUUIDs128: ['0000180D-0000-1000-8000-00805F9B34FB'],

  // 0x08-0x09 - Local Name
  completeLocalName: 'My Smart Device',
  shortenedLocalName: 'SmartDev',

  // 0x0A - Tx Power Level
  txPowerLevel: -12,

  // 0x14-0x15 - Service Solicitation
  serviceSolicitationUUIDs16: ['180D'],
  serviceSolicitationUUIDs128: ['0000180D-0000-1000-8000-00805F9B34FB'],

  // 0x16, 0x20, 0x21 - Service Data
  serviceData16: [
    { uuid: '180D', data: '0102030405' },
    { uuid: '180F', data: '060708090A' },
  ],
  serviceData32: [
    { uuid: '0000180D-0000-1000-8000-00805F9B34FB', data: '0B0C0D0E0F' },
  ],

  // 0x19 - Appearance (partial support)
  appearance: 0x03c0, // Generic Watch

  // 0x1F - Service Solicitation (32-bit)
  serviceSolicitationUUIDs32: ['0000180D'],

  // 0xFF - Manufacturer Specific Data
  manufacturerData: '4C000215FDA50693A4E24FB1AFCFC6EB0764782500010001C5',
}

// Start advertising with Android payload data and cross-platform service UUIDs.
startAdvertising({
  serviceUUIDs: ['180D', '180F'],
  advertisingData: advertisingData,
})

// Update advertising data dynamically
updateAdvertisingData({
  flags: 0x04,
  completeLocalName: 'Updated Device Name',
  txPowerLevel: -8,
})

// Get current advertising data
const currentData = await getAdvertisingData()
console.log('Current advertising data:', currentData)

🔧 API Reference

Peripheral Functions

startAdvertising(options)

Starts BLE advertising with the specified options.

Parameters:

  • options (object):
    • serviceUUIDs (string[]): Array of service UUIDs to advertise
    • localName? (string): Device name (legacy support)
    • manufacturerData? (string): Manufacturer data in hex format (legacy Android advertising support)
    • advertisingData? (AdvertisingDataTypes): Platform-aware advertising data. Android can advertise payload fields; iOS advertises local name and service UUIDs.

updateAdvertisingData(advertisingData)

Updates the advertising data while advertising is active.

Parameters:

  • advertisingData (AdvertisingDataTypes): New advertising data

getAdvertisingData()

Returns a Promise that resolves to the current advertising data.

Returns: Promise

stopAdvertising()

Stops BLE advertising.

setServices(services)

Sets GATT services and characteristics.

Parameters:

  • services (array): Array of service objects

updateCharacteristicValue(serviceUUID, characteristicUUID, value, notify)

Updates a local peripheral characteristic value. When notify is true, the new hex-encoded value is pushed to subscribed centrals using notify/indicate where the characteristic supports it.

Central Functions

isBluetoothEnabled()

Checks if Bluetooth is enabled on the device.

Returns: Promise

requestBluetoothPermission()

Requests Bluetooth permissions (Android) or checks authorization status (iOS).

Returns: Promise

getCapabilities()

Returns the platform/device Bluetooth feature set.

Returns: Promise

startBackgroundSession(options)

Starts a best-effort background BLE session. Android starts a foreground service that restores scan, advertising, and configured GATT services after normal process recreation. iOS keeps CoreBluetooth managers configured with state restoration identifiers when Bluetooth background modes are present; terminated-state relaunch is still subject to Apple's CoreBluetooth relaunch rules, including the iOS 26 AccessorySetupKit restriction.

Parameters:

  • serviceUUIDs (string[]): Service UUIDs to advertise and scan for.
  • localName? (string): Local name to advertise where supported.
  • allowDuplicates? (boolean): Whether scan callbacks may repeat the same device.
  • scanMode? ('lowPower' | 'balanced' | 'lowLatency'): Android scan mode preference.
  • androidNotificationChannelId?, androidNotificationChannelName?, androidNotificationTitle?, androidNotificationText?: Android foreground service notification options.

stopBackgroundSession()

Stops the active background BLE session and clears persisted Android service restore state.

startMultipeerSession(options)

Starts Apple Multipeer Connectivity discovery and messaging on iOS/iPadOS.

Parameters:

  • serviceType (string): Bonjour service type, 1-15 lowercase letters/numbers/hyphens.
  • displayName? (string): Name shown to nearby peers.
  • discoveryInfo? ({ key: string; value: string }[]): Small discovery metadata.
  • autoInvite? (boolean): Automatically invite discovered peers. Defaults to true.
  • autoAcceptInvitations? (boolean): Automatically accept incoming invitations. Defaults to true.
  • inviteTimeout? (number): Invitation timeout in seconds. Defaults to 30.
  • encryptionPreference? ('none' | 'optional' | 'required'): Defaults to required.

stopMultipeerSession()

Stops the local Multipeer advertiser, browser, and session.

inviteMultipeerPeer(peerId)

Invites a discovered Multipeer peer when autoInvite is disabled or you want manual control.

getMultipeerPeers()

Returns discovered and connected Multipeer peers for the active runtime session.

sendMultipeerMessage(value, peerIds?, reliable?)

Sends a hex-encoded payload to connected Multipeer peers. Omit peerIds to broadcast to every connected peer. reliable defaults to true.

startScan(options?)

Starts scanning for BLE devices.

Parameters:

  • options? (object):
    • serviceUUIDs? (string[]): Filter by service UUIDs
    • allowDuplicates? (boolean): Allow duplicate scan results
    • scanMode? ('lowPower' | 'balanced' | 'lowLatency'): Scan mode

stopScan()

Stops scanning for BLE devices.

connect(deviceId)

Connects to a BLE device.

Parameters:

  • deviceId (string): The unique identifier of the device

Returns: Promise. The promise rejects if the native connection does not complete within 15 seconds.

disconnect(deviceId)

Disconnects from a BLE device.

Parameters:

  • deviceId (string): The unique identifier of the device

discoverServices(deviceId)

Discovers GATT services for a connected device.

Parameters:

  • deviceId (string): The unique identifier of the connected device

Returns: Promise<GATTService[]>. The promise rejects if native service discovery does not complete within 15 seconds.

readCharacteristic(deviceId, serviceUUID, characteristicUUID)

Reads a characteristic value from a connected device.

Parameters:

  • deviceId (string): The unique identifier of the connected device
  • serviceUUID (string): The UUID of the service
  • characteristicUUID (string): The UUID of the characteristic

Returns: Promise. The promise rejects if the native read callback does not arrive within 15 seconds.

readDescriptor(deviceId, serviceUUID, characteristicUUID, descriptorUUID)

Reads a descriptor value from a connected device.

Returns: Promise. The promise rejects if descriptor discovery/read callbacks do not arrive within 15 seconds.

writeCharacteristic(deviceId, serviceUUID, characteristicUUID, value, writeType?)

Writes a value to a characteristic on a connected device.

Parameters:

  • deviceId (string): The unique identifier of the connected device
  • serviceUUID (string): The UUID of the service
  • characteristicUUID (string): The UUID of the characteristic
  • value (string): The value to write (hex string)
  • writeType? ('write' | 'writeWithoutResponse'): Write type

Returns: Promise. With-response writes reject if the native write callback does not arrive within 15 seconds.

writeDescriptor(deviceId, serviceUUID, characteristicUUID, descriptorUUID, value)

Writes a descriptor value to a connected device.

Returns: Promise. The promise rejects if descriptor discovery/write callbacks do not arrive within 15 seconds.

subscribeToCharacteristic(deviceId, serviceUUID, characteristicUUID)

Subscribes to notifications/indications from a characteristic.

Parameters:

  • deviceId (string): The unique identifier of the connected device
  • serviceUUID (string): The UUID of the service
  • characteristicUUID (string): The UUID of the characteristic

unsubscribeFromCharacteristic(deviceId, serviceUUID, characteristicUUID)

Unsubscribes from notifications/indications from a characteristic.

Parameters:

  • deviceId (string): The unique identifier of the connected device
  • serviceUUID (string): The UUID of the service
  • characteristicUUID (string): The UUID of the characteristic

Events

Use addEventListener(eventName, callback) for BLE status and data events.

Event Payload
deviceFound Discovered BLE device payload: { id, name?, localName?, rssi?, serviceUUIDs?, serviceData?, manufacturerData?, txPowerLevel?, isConnectable?, advertisingData? }.
onDeviceFound, scanResult Legacy aliases for deviceFound.
scanFailed { errorCode, message } on Android scan callback failure.
advertisingStarted Empty payload when advertising starts.
advertisingStartFailed Android: { errorCode, message }; iOS: { error }.
classicDeviceFound Android Classic discovery result: { id, name, bondState }.
classicScanFailed, classicScanFinished Android Classic discovery status events.
classicConnected, classicDisconnected Android Classic RFCOMM connection status: { deviceId }.
classicConnectionReceived Android Classic RFCOMM inbound connection: { deviceId }.
classicServerStarted, classicServerStopped Android Classic RFCOMM listener status.
classicDataReceived Android Classic RFCOMM data: { deviceId, value }.
deviceConnected { deviceId }
deviceDisconnected { deviceId }
servicesDiscovered { deviceId, services }
characteristicValueChanged { deviceId, serviceUUID, characteristicUUID, value }
l2capChannelPublished, l2capChannelUnpublished Local LE L2CAP channel lifecycle status.
l2capChannelOpened, l2capChannelClosed LE L2CAP stream lifecycle status.
l2capChannelPublishFailed, l2capChannelOpenFailed LE L2CAP failure status.
l2capDataReceived LE L2CAP stream data: { channelId, psm, deviceId, value }.
rssiUpdated { deviceId, rssi }
peripheralReadRequest { centralId, serviceUUID, characteristicUUID, value }
peripheralWriteRequest { centralId, serviceUUID, characteristicUUID, value }
peripheralSubscribed { centralId, serviceUUID, characteristicUUID }
peripheralUnsubscribed { centralId, serviceUUID, characteristicUUID }
backgroundSessionStarted { platform, serviceUUIDs?, localName? }
backgroundSessionStopped { platform }
backgroundSessionRestored { platform, role?, isScanning?, isAdvertising?, serviceUUIDs?, deviceIds? }
backgroundSessionStartFailed { platform, error }
multipeerStarted, multipeerStopped, multipeerStartFailed Apple Multipeer lifecycle status.
multipeerPeerFound, multipeerPeerLost, multipeerPeerStateChanged Apple Multipeer peer discovery and connection state.
multipeerMessageReceived Apple Multipeer data: { peerId, displayName, value }.

getConnectedDevices()

Gets list of currently connected devices.

Returns: Promise<string[]>

readRSSI(deviceId)

Reads RSSI (signal strength) for a connected device.

Parameters:

  • deviceId (string): The unique identifier of the connected device

Returns: Promise

requestMTU(deviceId, mtu)

Requests an ATT MTU on Android. iOS rejects with an unsupported error because CoreBluetooth negotiates MTU internally.

Returns: Promise

setPreferredPhy(deviceId, txPhy, rxPhy, phyOption?)

Sets preferred BLE PHY on Android 8+ when hardware supports it. iOS rejects with an unsupported error.

Returns: Promise

readPhy(deviceId)

Reads the current BLE PHY on Android 8+ when hardware supports it. iOS rejects with an unsupported error.

Returns: Promise

getBondState(deviceId)

Returns Android bond state. iOS resolves to unsupported.

Returns: Promise

createBond(deviceId)

Starts Android pairing/bonding. iOS rejects with an unsupported error.

Returns: Promise

removeBond(deviceId)

Removes an Android bond when the OS exposes that operation. iOS rejects with an unsupported error.

Returns: Promise

startExtendedAdvertising(options)

Starts an Android BLE extended advertising set on Android 8+ hardware that supports LE extended advertising. iOS rejects with an unsupported error.

Returns: Promise

stopExtendedAdvertising(advertisingId)

Stops an Android BLE extended advertising set.

publishL2CAPChannel(), openL2CAPChannel(), sendL2CAPData()

Opens BLE L2CAP channel streams. iOS uses CoreBluetooth LE Credit Based Channels. Android requires Android 10+.

startClassicScan(), connectClassic(), startClassicServer(), writeClassic()

Android Classic Bluetooth RFCOMM discovery, client connection, server listener, write, disconnect, and receive events. iOS rejects with explicit unsupported errors because public iOS APIs do not expose arbitrary Classic RFCOMM.

Types

AdvertisingDataTypes

Platform-aware interface for BLE advertising data types. Android can advertise these payload fields when hardware and payload size allow it. iOS can scan many of these fields from other peripherals, but iOS peripheral advertising is limited to local name and service UUIDs.

interface AdvertisingDataTypes {
  // 0x01 - Flags
  flags?: number

  // 0x02-0x07 - Service UUIDs
  incompleteServiceUUIDs16?: string[]
  completeServiceUUIDs16?: string[]
  incompleteServiceUUIDs32?: string[]
  completeServiceUUIDs32?: string[]
  incompleteServiceUUIDs128?: string[]
  completeServiceUUIDs128?: string[]

  // 0x08-0x09 - Local Name
  shortenedLocalName?: string
  completeLocalName?: string

  // 0x0A - Tx Power Level
  txPowerLevel?: number

  // 0x14-0x15 - Service Solicitation
  serviceSolicitationUUIDs16?: string[]
  serviceSolicitationUUIDs128?: string[]

  // 0x16, 0x20, 0x21 - Service Data
  serviceData16?: Array<{
    uuid: string
    data: string
  }>
  serviceData32?: Array<{
    uuid: string
    data: string
  }>
  serviceData128?: Array<{
    uuid: string
    data: string
  }>

  // 0x19 - Appearance
  appearance?: number

  // 0x1F - Service Solicitation (32-bit)
  serviceSolicitationUUIDs32?: string[]

  // 0xFF - Manufacturer Specific Data
  manufacturerData?: string
}

Supported BLE Advertising Data Types

Hex Type Name Description Advertise Support Example
0x01 Flags Basic device capabilities Android flags: 0x06
0x02-0x07 Service UUIDs Service UUIDs offered iOS + Android completeServiceUUIDs16: ['180D']
0x08-0x09 Local Name Device name iOS + Android completeLocalName: 'My Device'
0x0A Tx Power Level Transmit power in dBm Android txPowerLevel: -12
0x14-0x15 Service Solicitation Services being sought Android serviceSolicitationUUIDs16: ['180D']
0x16, 0x20, 0x21 Service Data Data associated with services Android serviceData16: [{uuid: '180D', data: '010203'}]
0x19 Appearance Appearance category Android appearance: 0x03C0
0x1F Service Solicitation (32-bit) 32-bit services being solicited Android serviceSolicitationUUIDs32: ['0000180D']
0xFF Manufacturer Specific Data Vendor-defined data Android manufacturerData: '4748494A4B4C4D4E'

Note: This library focuses on reliable phone-to-phone BLE behavior exposed by public iOS and Android APIs. Bluetooth Mesh, LE Audio broadcast/isoc streams, and arbitrary iOS advertising payloads are not exposed by the mobile OS APIs this package can use.

📖 Usage Examples

Health Device Example

import { startAdvertising, setServices } from 'munim-bluetooth'

// Health device advertising. The payload fields inside advertisingData are
// advertised on Android; iOS uses serviceUUIDs/localName for advertising.
startAdvertising({
  serviceUUIDs: ['180D', '180F'], // Heart Rate, Battery Service
  advertisingData: {
    flags: 0x06, // LE General Discoverable Mode, BR/EDR Not Supported
    completeLocalName: 'Health Monitor',
    appearance: 0x03c0, // Generic Watch
    txPowerLevel: -8,
    manufacturerData: '0102030405', // Custom health data
    serviceData16: [
      { uuid: '180D', data: '6400' }, // Heart rate: 100 bpm
      { uuid: '180F', data: '64' }, // Battery: 100%
    ],
  },
})

// Set up GATT services
setServices([
  {
    uuid: '180D', // Heart Rate Service
    characteristics: [
      {
        uuid: '2A37', // Heart Rate Measurement
        properties: ['read', 'notify'],
        value: '6400', // 100 bpm
      },
    ],
  },
  {
    uuid: '180F', // Battery Service
    characteristics: [
      {
        uuid: '2A19', // Battery Level
        properties: ['read', 'notify'],
        value: '64', // 100%
      },
    ],
  },
])

Smart Home Device Example

import { startAdvertising, updateAdvertisingData } from 'munim-bluetooth'

// Smart home device. The payload fields inside advertisingData are advertised
// on Android; for iOS peers, put live state in GATT characteristics.
startAdvertising({
  serviceUUIDs: ['1812', '180F'], // HID, Battery Service
  advertisingData: {
    flags: 0x04, // LE General Discoverable Mode
    completeLocalName: 'Smart Light Bulb',
    appearance: 0x03c1, // Generic Light Fixture
    manufacturerData: '0102030405', // Custom light data
    serviceData16: [
      { uuid: '1812', data: '01' }, // HID: Keyboard
      { uuid: '180F', data: '64' }, // Battery: 100%
    ],
  },
})

// Update advertising data when light state changes
updateAdvertisingData({
  manufacturerData: '0102030406', // Updated light data
  serviceData16: [
    { uuid: '1812', data: '02' }, // HID: Mouse
    { uuid: '180F', data: '50' }, // Battery: 80%
  ],
})

Basic Peripheral Setup

import React, { useEffect } from 'react'
import { Text } from 'react-native'
import {
  startAdvertising,
  stopAdvertising,
  setServices,
} from 'munim-bluetooth'

const MyPeripheral = () => {
  useEffect(() => {
    // Configure services
    setServices([
      {
        uuid: '1800', // Generic Access Service
        characteristics: [
          {
            uuid: '2a00', // Device Name
            properties: ['read'],
            value: 'MyDevice',
          },
          {
            uuid: '2a01', // Appearance
            properties: ['read'],
            value: '0x03C0', // Generic Computer
          },
        ],
      },
      {
        uuid: '1801', // Generic Attribute Service
        characteristics: [
          {
            uuid: '2a05', // Service Changed
            properties: ['indicate'],
          },
        ],
      },
    ])

    // Start advertising
    startAdvertising({
      serviceUUIDs: ['1800', '1801'],
      localName: 'MyReactNativePeripheral',
    })

    // Cleanup on unmount
    return () => {
      stopAdvertising()
    }
  }, [])

  return <Text>Peripheral is running...</Text>
}

Device Scanner Example

import React, { useState, useEffect } from 'react'
import { Text, View } from 'react-native'
import {
  addDeviceFoundListener,
  addEventListener,
  disconnect,
  isBluetoothEnabled,
  requestBluetoothPermission,
  startScan,
  stopScan,
  connect,
  discoverServices,
  readCharacteristic,
  subscribeToCharacteristic,
} from 'munim-bluetooth'

const DeviceScanner = () => {
  const [devices, setDevices] = useState([])
  const [connectedDevice, setConnectedDevice] = useState(null)

  useEffect(() => {
    let removeDeviceFoundListener = () => {}
    let removeCharacteristicListener = () => {}

    const start = async () => {
      const hasPermission = await requestBluetoothPermission()
      if (!hasPermission) {
        return
      }

      const enabled = await isBluetoothEnabled()
      if (!enabled) {
        return
      }

      removeDeviceFoundListener = addDeviceFoundListener((device) => {
        setDevices((currentDevices) => {
          const alreadyExists = currentDevices.some(
            (currentDevice) => currentDevice.id === device.id
          )

          if (alreadyExists) {
            return currentDevices
          }

          return [...currentDevices, device]
        })
      })

      removeCharacteristicListener = addEventListener(
        'characteristicValueChanged',
        (event) => {
          console.log('Characteristic changed:', event)
        }
      )

      startScan({
        serviceUUIDs: ['180D'], // Filter by Heart Rate service
        allowDuplicates: false,
        scanMode: 'balanced',
      })
    }

    void start()

    // Cleanup
    return () => {
      stopScan()
      removeDeviceFoundListener()
      removeCharacteristicListener()

      if (connectedDevice) {
        disconnect(connectedDevice)
      }
    }
  }, [connectedDevice])

  const handleConnect = async (deviceId) => {
    await connect(deviceId)
    setConnectedDevice(deviceId)

    // Discover services
    const services = await discoverServices(deviceId)
    console.log('Services:', services)

    // Read a characteristic
    const value = await readCharacteristic(deviceId, '180D', '2A37')
    console.log('Heart Rate:', value)

    // Subscribe to notifications
    subscribeToCharacteristic(deviceId, '180D', '2A37')
  }

  return (
    <View>
      <Text>Found {devices.length} devices</Text>
      {/* Render device list */}
    </View>
  )
}

🔍 Troubleshooting

Common Issues

  1. Permission Denied: Ensure you have the necessary Bluetooth permissions in your app
  2. Advertising Not Starting: Check that Bluetooth is enabled on the device
  3. Services Not Visible: Verify that your service UUIDs are properly formatted
  4. Scanning Not Working: On Android 6.0+, ensure location permissions are granted
  5. Connection Fails: Verify the device is in range and advertising

Expo-Specific Issues

  1. Development Build Required: This library requires a development build in Expo. Use npx expo run:ios, npx expo run:android, or an EAS development build. Expo Go is not supported.
  2. Permissions Not Working: Make sure you've added the permissions to your app.json as shown in the setup section
  3. Build Errors: Ensure you're using Expo SDK 50+ and have the latest Expo CLI
  4. Nitro Modules: Make sure you have react-native-nitro-modules installed and configured

Debug Mode

Enable debug logging by setting the following environment variable:

export REACT_NATIVE_BLUETOOTH_DEBUG=1

👏 Contributing

We welcome contributions! Please see our Contributing Guide for details on how to submit pull requests, report issues, and contribute to the project.

📄 License

This project is licensed under the Apache License 2.0 - see the LICENSE file for details.


Star the Munim Technologies repo on GitHub to support the project