BLE support for expo apps
npm install expo-ble---
> 📢 Hey there!
>
> This library is not affiliated with or maintained by the Expo team — it's built by me, Daniel Reiling! 👋
>
> It's the BLE library I wish Expo came with, so I made it myself. Built with Expo's Modules API for seamless integration. Feedback and contributions are always welcome!
---
- Cross-platform: Works seamlessly on iOS and Android
- Expo SDK compatible: Built using Expo Modules API
- Expo Config Plugin: Automatic project configuration with zero manual setup
- TypeScript first: Full TypeScript support with comprehensive type definitions
- Permission handling: Automatic handling of platform-specific permissions
- Event-driven architecture: Real-time updates for device discovery and connection states
- Simple API: Easy-to-use singleton pattern for BLE operations
- Modern: Uses the latest native APIs (CoreBluetooth on iOS, Android BLE APIs)
- Expo SDK 50+
- React Native 0.71+
- iOS 13.0+
- Android 5.0+ (API level 21+)
``bash`
npx expo install expo-ble
This library includes an Expo Config Plugin that automatically configures your iOS and Android projects. Simply add the plugin to your app.json or app.config.js:
`json`
{
"expo": {
"plugins": ["expo-ble"]
}
}
#### Config Plugin Options
You can customize the plugin behavior by passing options:
`json`
{
"expo": {
"plugins": [
[
"expo-ble",
{
"bluetoothAlwaysUsageDescription": "Custom message for Bluetooth permission",
"androidNeedsLocationPermission": true,
"iosBackgroundModes": false
}
]
]
}
}
Available options:
- bluetoothAlwaysUsageDescription (string): iOS usage description for Bluetooth permissionbluetoothPeripheralUsageDescription
- (string): iOS usage description for peripheral mode (optional)androidNeedsLocationPermission
- (boolean): Whether to add location permission for Android 11 and below (default: true)iosBackgroundModes
- (boolean): Enable iOS background modes for BLE (default: false)
If you're not using the config plugin, you can manually configure your project:
#### iOS Configuration
Add the following to your Info.plist:
`xml`
#### Android Configuration
The library automatically adds the required permissions to your Android manifest via the config plugin:
- BLUETOOTH and BLUETOOTH_ADMIN (Android 11 and below)BLUETOOTH_SCAN
- and BLUETOOTH_CONNECT (Android 12+)ACCESS_FINE_LOCATION
- (for BLE scanning on Android 11 and below)
For bare React Native projects, run:
`bash`
npx expo prebuild
`typescript`
import bleManager, { BleState, NativeDevice } from 'expo-ble';
`typescript
// Initialize the BLE manager
await bleManager.initialize();
// Listen to Bluetooth state changes
bleManager.onStateChange((state: BleState) => {
console.log('Bluetooth state:', state);
if (state === BleState.PoweredOn) {
// Bluetooth is ready to use
}
});
`
`typescript
// Request necessary permissions
const result = await bleManager.requestPermissions();
if (result.granted) {
console.log('Permissions granted!');
} else {
console.log('Permissions denied');
}
`
`typescript
// Start scanning for BLE devices
await bleManager.startScan({
serviceUUIDs: [], // Optional: filter by service UUIDs
timeout: 10000 // Optional: auto-stop after 10 seconds
});
// Listen for discovered devices
bleManager.onDeviceFound((device: NativeDevice) => {
console.log('Found device:', device.name || device.id);
console.log('RSSI:', device.rssi);
});
// Stop scanning manually
await bleManager.stopScan();
`
`typescript`
try {
// Connect to a device
const device = await bleManager.connect(deviceId, {
timeout: 5000 // 5 second timeout
});
console.log('Connected to:', device.name);
// Disconnect when done
await bleManager.disconnect(deviceId);
} catch (error) {
console.error('Connection failed:', error);
}
`typescript
import React, { useState, useEffect } from 'react';
import { View, Button, FlatList, Text } from 'react-native';
import bleManager, { BleState, NativeDevice } from 'expo-ble';
export default function App() {
const [devices, setDevices] = useState
const [isScanning, setIsScanning] = useState(false);
useEffect(() => {
// Initialize BLE
bleManager.initialize();
// Subscribe to device discovery
const unsubscribe = bleManager.onDeviceFound((device) => {
setDevices(prev => {
const exists = prev.find(d => d.id === device.id);
if (exists) {
return prev.map(d => d.id === device.id ? device : d);
}
return [...prev, device];
});
});
return () => {
unsubscribe();
bleManager.destroy();
};
}, []);
const startScan = async () => {
try {
setDevices([]);
setIsScanning(true);
await bleManager.startScan({ timeout: 10000 });
setTimeout(() => setIsScanning(false), 10000);
} catch (error) {
console.error('Scan failed:', error);
setIsScanning(false);
}
};
const stopScan = async () => {
await bleManager.stopScan();
setIsScanning(false);
};
return (
title={isScanning ? "Stop Scan" : "Start Scan"}
onPress={isScanning ? stopScan : startScan}
/>
keyExtractor={item => item.id}
renderItem={({ item }) => (
{item.id} • RSSI: {item.rssi}
)}
/>
);
}
`
The main class for all BLE operations. Access the singleton instance via the default export.
#### Methods
| Method | Description | Parameters | Returns |
|--------|-------------|------------|---------|
| initialize() | Initialize the BLE manager | None | Promise |requestPermissions()
| | Request BLE permissions | None | Promise |checkPermissions()
| | Check current permission status | None | Promise |startScan(options?)
| | Start scanning for devices | ScanOptions | Promise |stopScan()
| | Stop scanning | None | Promise |connect(deviceId, options?)
| | Connect to a device | string, ConnectionOptions | Promise |disconnect(deviceId)
| | Disconnect from a device | string | Promise |enable()
| | Enable Bluetooth (Android only) | None | Promise |onStateChange(listener)
| | Listen to state changes | EventListener | UnsubscribeFn |onDeviceFound(listener)
| | Listen to device discovery | EventListener | UnsubscribeFn |
#### Properties
| Property | Type | Description |
|----------|------|-------------|
| state | BleState | Current Bluetooth state |isScanning
| | boolean | Whether currently scanning |discoveredDevices
| | NativeDevice[] | List of discovered devices |
#### BleState
`typescript`
enum BleState {
Unknown = 'unknown',
Resetting = 'resetting',
Unsupported = 'unsupported',
Unauthorized = 'unauthorized',
PoweredOff = 'poweredOff',
PoweredOn = 'poweredOn'
}
#### ScanOptions
`typescript`
interface ScanOptions {
serviceUUIDs?: string[]; // Filter by service UUIDs
allowDuplicates?: boolean; // Allow duplicate advertisements
scanMode?: 'lowPower' | 'balanced' | 'lowLatency'; // Android only
timeout?: number; // Auto-stop timeout in ms
}
#### ConnectionOptions
`typescript`
interface ConnectionOptions {
autoConnect?: boolean; // Android only
timeout?: number; // Connection timeout in ms
requestMTU?: number; // Android only
priority?: 'high' | 'balanced' | 'lowPower';
}
This library uses expo-module-scripts for building. There are two build modes:
Development Build (watch mode):
`bash`
npm run build
This starts a watcher that automatically rebuilds when you make changes. Keep this running during development.
Production Build (one-time):
`bash`
npm run build:prod
This performs a one-time build without watch mode. Use this before publishing.
`bash`
npm run clean # Clean build artifacts
npm run lint # Lint the code
npm run test # Run tests
npm run prepare # Prepare module (runs after install)
Navigate to the example/ directory:`bash`
cd example
npm start # Start Expo dev server
npm run ios # Run on iOS simulator
npm run android # Run on Android emulator
Before publishing, ensure:
- All tests pass (npm run test)npm run lint
- Code is linted ()npm login
- You have npm publish rights to the package
- You're logged in to npm ()
1. Clean and build for production:
`bash`
npm run clean
npm run build:prod
2. Update version:
`bash`
# Increment version (patch, minor, or major)
npm version patch # 0.1.0 -> 0.1.1
npm version minor # 0.1.0 -> 0.2.0
npm version major # 0.1.0 -> 1.0.0
3. Publish to npm:
`bash`
npm publish
4. Push git tags:
`bash`
git push origin main --tags
The published npm package includes:
- /build/ - Compiled JavaScript and TypeScript definitions/plugin/
- - Expo config plugin/ios/
- - Native iOS source (Swift files and podspec)/android/
- - Native Android source (Kotlin files)package.json
- Configuration files (, expo-module.config.json, etc.)
The following are excluded (see .npmignore):/example/
- - Example app/node_modules/
- - Dependencies
- Test files and development tools
Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.
1. Fork the repository
2. Create your feature branch (git checkout -b feature/AmazingFeature)git commit -m 'Add some AmazingFeature'
3. Commit your changes ()git push origin feature/AmazingFeature
4. Push to the branch ()
5. Open a Pull Request
This project is licensed under the MIT License - see below for details:
`
MIT License
Copyright (c) 2024 Daniel Reiling
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
`
- Built with Expo Modules API
- Inspired by react-native-ble-plx and react-native-ble-manager
1. "Bluetooth permission not granted"
- Ensure you've called requestPermissions()` before scanning
- On iOS, check that NSBluetoothAlwaysUsageDescription is in Info.plist
- On Android, location permission may be required for older versions
2. Devices not appearing during scan
- Ensure Bluetooth is enabled on the device
- Check that the device is advertising and in range
- Try scanning without service UUID filters
3. Connection timeouts
- Increase the timeout in connection options
- Ensure the device is in range and not already connected
- Some devices may require specific connection parameters
- Expo Modules Documentation
- CoreBluetooth Documentation
- Android Bluetooth Documentation
---
Made with ☕️ by Daniel Reiling