expo-ble
v0.5.0
Published
BLE support for expo apps
Maintainers
Readme
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!
🚀 Features
- 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)
📋 Requirements
- Expo SDK 50+
- React Native 0.71+
- iOS 13.0+
- Android 5.0+ (API level 21+)
📦 Installation
npx expo install expo-bleAutomatic Configuration with Expo Config Plugin
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:
{
"expo": {
"plugins": ["expo-ble"]
}
}Config Plugin Options
You can customize the plugin behavior by passing options:
{
"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)
Manual Configuration
If you're not using the config plugin, you can manually configure your project:
iOS Configuration
Add the following to your Info.plist:
<key>NSBluetoothAlwaysUsageDescription</key>
<string>This app uses Bluetooth to connect to BLE devices</string>Android Configuration
The library automatically adds the required permissions to your Android manifest via the config plugin:
BLUETOOTHandBLUETOOTH_ADMIN(Android 11 and below)BLUETOOTH_SCANandBLUETOOTH_CONNECT(Android 12+)ACCESS_FINE_LOCATION(for BLE scanning on Android 11 and below)
For bare React Native projects, run:
npx expo prebuild🎯 Basic Usage
Import the library
import bleManager, { BleState, NativeDevice } from 'expo-ble';Initialize and check Bluetooth state
// 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
}
});Request permissions
// Request necessary permissions
const result = await bleManager.requestPermissions();
if (result.granted) {
console.log('Permissions granted!');
} else {
console.log('Permissions denied');
}Scan for devices
// 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();Connect to a device
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);
}📖 Complete Example
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<NativeDevice[]>([]);
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 (
<View style={{ flex: 1, padding: 20 }}>
<Button
title={isScanning ? "Stop Scan" : "Start Scan"}
onPress={isScanning ? stopScan : startScan}
/>
<FlatList
data={devices}
keyExtractor={item => item.id}
renderItem={({ item }) => (
<View style={{ padding: 10 }}>
<Text>{item.name || 'Unknown Device'}</Text>
<Text style={{ fontSize: 12, color: 'gray' }}>
{item.id} • RSSI: {item.rssi}
</Text>
</View>
)}
/>
</View>
);
}🔧 API Reference
BleManager
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<void> |
| requestPermissions() | Request BLE permissions | None | Promise<PermissionResponse> |
| checkPermissions() | Check current permission status | None | Promise<PermissionStatus> |
| startScan(options?) | Start scanning for devices | ScanOptions | Promise<void> |
| stopScan() | Stop scanning | None | Promise<void> |
| connect(deviceId, options?) | Connect to a device | string, ConnectionOptions | Promise<NativeDevice> |
| disconnect(deviceId) | Disconnect from a device | string | Promise<void> |
| enable() | Enable Bluetooth (Android only) | None | Promise<void> |
| onStateChange(listener) | Listen to state changes | EventListener<BleState> | UnsubscribeFn |
| onDeviceFound(listener) | Listen to device discovery | EventListener<NativeDevice> | UnsubscribeFn |
Properties
| Property | Type | Description |
|----------|------|-------------|
| state | BleState | Current Bluetooth state |
| isScanning | boolean | Whether currently scanning |
| discoveredDevices | NativeDevice[] | List of discovered devices |
Types
BleState
enum BleState {
Unknown = 'unknown',
Resetting = 'resetting',
Unsupported = 'unsupported',
Unauthorized = 'unauthorized',
PoweredOff = 'poweredOff',
PoweredOn = 'poweredOn'
}ScanOptions
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
interface ConnectionOptions {
autoConnect?: boolean; // Android only
timeout?: number; // Connection timeout in ms
requestMTU?: number; // Android only
priority?: 'high' | 'balanced' | 'lowPower';
}🛠️ Development
Building the Module
This library uses expo-module-scripts for building. There are two build modes:
Development Build (watch mode):
npm run buildThis starts a watcher that automatically rebuilds when you make changes. Keep this running during development.
Production Build (one-time):
npm run build:prodThis performs a one-time build without watch mode. Use this before publishing.
Other Development Commands
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)Working with the Example App
Navigate to the example/ directory:
cd example
npm start # Start Expo dev server
npm run ios # Run on iOS simulator
npm run android # Run on Android emulator📦 Publishing
Prerequisites
Before publishing, ensure:
- All tests pass (
npm run test) - Code is linted (
npm run lint) - You have npm publish rights to the package
- You're logged in to npm (
npm login)
Publishing Steps
Clean and build for production:
npm run clean npm run build:prodUpdate version:
# 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.0Publish to npm:
npm publishPush git tags:
git push origin main --tags
What Gets Published
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)- Configuration files (
package.json,expo-module.config.json, etc.)
The following are excluded (see .npmignore):
/example/- Example app/node_modules/- Dependencies- Test files and development tools
🤝 Contributing
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.
- Fork the repository
- Create your feature branch (
git checkout -b feature/AmazingFeature) - Commit your changes (
git commit -m 'Add some AmazingFeature') - Push to the branch (
git push origin feature/AmazingFeature) - Open a Pull Request
📄 License
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.🙏 Acknowledgments
- Built with Expo Modules API
- Inspired by react-native-ble-plx and react-native-ble-manager
📱 Platform-Specific Notes
iOS
- Bluetooth permission is required and will be requested automatically
- Background modes are not currently supported
- Minimum iOS version: 13.0
Android
- Location permission is required for BLE scanning on Android 11 and below
- Different permissions are used for Android 12+ (BLUETOOTH_SCAN, BLUETOOTH_CONNECT)
- The library supports programmatic Bluetooth enabling via system dialog
🐛 Troubleshooting
Common Issues
"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
- Ensure you've called
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
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
📚 Resources
Made with ☕️ by Daniel Reiling
