In the rapidly growing world of IoT and wearable technology, Bluetooth Low Energy (BLE) has become the go-to protocol for short-range, low-power communication. For mobile developers, this opens up a world of possibilities, from creating fitness apps that track your heart rate to building smart home control centers. If you've ever wondered how your favorite fitness app magically syncs with your heart rate monitor, you're in the right place.
In this tutorial, we'll build a React Native application that connects to a BLE heart rate monitor, subscribes to its data stream, and displays your heart rate in real-time. We'll demystify the complexities of BLE, from handling permissions to parsing binary data packets.
This guide is for developers who have a basic understanding of React Native and want to dive into the exciting world of hardware integration. You'll need a physical BLE heart rate monitor to follow along with the practical steps.
Understanding the Problem
Connecting to BLE devices can be tricky. Unlike simple API calls, it involves a multi-step, asynchronous process: scanning, connecting, discovering services, and subscribing to data streams (characteristics). Each step comes with its own set of challenges, especially when dealing with the nuances of both Android and iOS permissions.
The data from sensors like heart rate monitors is often not sent in a human-readable format like JSON. Instead, it's a stream of bytes that needs to be parsed according to the official Bluetooth GATT (Generic Attribute Profile) specifications. For instance, the standard Heart Rate Measurement characteristic packs multiple pieces of information into just a few bytes, and we need to know how to unpack them.
Prerequisites
- Node.js and npm/yarn installed.
- React Native development environment set up for both iOS and Android.
- A physical BLE heart rate monitor (most modern fitness trackers will work).
- Familiarity with React Hooks.
We'll be using the react-native-ble-plx library, a popular and powerful tool for interacting with BLE devices in React Native.
Step 1: Setting Up the Project and Permissions
First, let's create a new React Native project and install the necessary dependencies.
What we're doing
We'll initialize a new React Native project and add the react-native-ble-plx library. Then, we'll configure the necessary permissions for both Android and iOS to allow our app to use Bluetooth.
Implementation
npx react-native init BLEHeartRateApp
cd BLEHeartRateApp
npm install react-native-ble-plx
For iOS:
Navigate to your ios directory and run pod install. Then, open Info.plist and add the following keys to request Bluetooth permissions:
<!-- ios/BLEHeartRateApp/Info.plist -->
<key>NSBluetoothAlwaysUsageDescription</key>
<string>Our app uses Bluetooth to connect to heart rate monitors.</string>
<key>NSBluetoothPeripheralUsageDescription</key>
<string>Our app uses Bluetooth to connect to heart rate monitors.</string>
For Android:
Open your android/app/src/main/AndroidManifest.xml and add the following permissions:
<!-- android/app/src/main/AndroidManifest.xml -->
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
For Android, we also need to request these permissions at runtime. We'll handle this in our application logic.
How it works
These permissions are required by the operating systems to ensure that apps don't access a user's Bluetooth functionalities without their consent. react-native-ble-plx will use these underlying native permissions to interact with the device's Bluetooth module.
Step 2: Scanning for BLE Devices
Now that our project is set up, let's start scanning for nearby BLE devices.
What we're doing
We'll create a simple UI with a button to start scanning for BLE devices. We'll use react-native-ble-plx to initiate the scan and display a list of discovered devices. We'll specifically look for devices advertising the standard Heart Rate Service UUID (0x180D).
Implementation
Here's a basic component to handle scanning:
// src/components/DeviceScanner.js
import React, 'useState', useEffect } from 'react';
import { View, Text, Button, FlatList, PermissionsAndroid, Platform } from 'react-native';
import { BleManager } from 'react-native-ble-plx';
const manager = new BleManager();
// Standard Bluetooth Service UUID for Heart Rate
const HEART_RATE_SERVICE_UUID = '0000180d-0000-1000-8000-00805f9b34fb';
const DeviceScanner = () => {
const [devices, setDevices] = useState([]);
const [isScanning, setIsScanning] = useState(false);
const requestBluetoothPermission = async () => {
if (Platform.OS === 'android') {
const granted = await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,
{
title: 'Bluetooth Permission',
message: 'This app needs access to your location to scan for BLE devices.',
buttonNeutral: 'Ask Me Later',
buttonNegative: 'Cancel',
buttonPositive: 'OK',
},
);
return granted === PermissionsAndroid.RESULTS.GRANTED;
}
return true;
};
const startScan = async () => {
const hasPermission = await requestBluetoothPermission();
if (!hasPermission) {
console.log('Permission denied');
return;
}
setIsScanning(true);
setDevices([]);
manager.startDeviceScan([HEART_RATE_SERVICE_UUID], null, (error, device) => {
if (error) {
console.error(error);
setIsScanning(false);
return;
}
if (device) {
setDevices(prevDevices => {
if (!prevDevices.some(d => d.id === device.id)) {
return [...prevDevices, device];
}
return prevDevices;
});
}
});
// Stop scanning after 10 seconds
setTimeout(() => {
manager.stopDeviceScan();
setIsScanning(false);
}, 10000);
};
return (
<View>
<Button title={isScanning ? 'Scanning...' : 'Scan for Devices'} onPress={startScan} disabled={isScanning} />
<FlatList
data={devices}
keyExtractor={item => item.id}
renderItem={({ item }) => <Text>{item.name || 'Unknown Device'} ({item.id})</Text>}
/>
</View>
);
};
export default DeviceScanner;
How it works
When the "Scan for Devices" button is pressed, we first request the necessary location permission for Android. Then, manager.startDeviceScan() begins searching for nearby devices. We've provided the HEART_RATE_SERVICE_UUID to filter our scan and only find devices that are advertising this specific service. Discovered devices are added to our devices state and displayed in a FlatList.
Step 3: Connecting and Subscribing to Data
Once we've found our heart rate monitor, the next step is to connect to it and subscribe to the heart rate measurement characteristic.
What we're doing
We'll extend our component to handle connecting to a selected device. After connecting, we'll discover its services and characteristics. Finally, we'll find the Heart Rate Measurement characteristic (0x2A37) and subscribe to its notifications to receive real-time data.
Implementation
Let's add connection and subscription logic:
// ... (imports and existing code)
// Standard Bluetooth Characteristic UUID for Heart Rate Measurement
const HEART_RATE_MEASUREMENT_CHARACTERISTIC_UUID = '00002a37-0000-1000-8000-00805f9b34fb';
const HeartRateMonitor = () => {
// ... (existing state)
const [connectedDevice, setConnectedDevice] = useState(null);
const [heartRate, setHeartRate] = useState(0);
// ... (permission and scan logic)
const connectToDevice = async (device) => {
try {
manager.stopDeviceScan();
const connected = await device.connect();
setConnectedDevice(connected);
await connected.discoverAllServicesAndCharacteristics();
// Subscribe to heart rate notifications
monitorHeartRate(connected);
} catch (error) {
console.error('Connection error:', error);
}
};
const monitorHeartRate = (device) => {
device.monitorCharacteristicForService(
HEART_RATE_SERVICE_UUID,
HEART_RATE_MEASUREMENT_CHARACTERISTIC_UUID,
(error, characteristic) => {
if (error) {
console.error('Monitoring error:', error);
return;
}
if (characteristic?.value) {
const buffer = Buffer.from(characteristic.value, 'base64');
const heartRateValue = parseHeartRate(buffer);
setHeartRate(heartRateValue);
}
}
);
};
// ... (return statement with FlatList and Button)
// Modify FlatList to be pressable
renderItem={({ item }) => (
<Button title={`Connect to ${item.name || 'Unknown'}`} onPress={() => connectToDevice(item)} />
)}
// Display heart rate
{connectedDevice && <Text>Heart Rate: {heartRate} BPM</Text>}
};
How it works
When a device from the list is tapped, connectToDevice is called. It first stops the scan, then connects to the device using device.connect(). After a successful connection, discoverAllServicesAndCharacteristics() retrieves all the services and characteristics the device offers. Finally, monitorCharacteristicForService sets up a listener for the Heart Rate Measurement characteristic. Whenever the heart rate monitor has a new reading, this listener will fire with the new data.
Step 4: Parsing the Heart Rate Data
The data we receive from the characteristic is in a raw binary format. We need to parse it to extract the actual heart rate value.
What we're doing
We'll create a function to interpret the byte array received from the heart rate monitor according to the Bluetooth specification. The first byte of the data packet contains flags that tell us the format of the heart rate value.
Implementation
Here is the data parsing function:
import { Buffer } from 'buffer'; // Need to import buffer
const parseHeartRate = (buffer) => {
const flags = buffer.readUInt8(0);
// Check if the heart rate is in 16-bit format (bit 0 of flags)
const is16Bit = (flags & 0x01) !== 0;
if (is16Bit) {
return buffer.readUInt16LE(1);
} else {
return buffer.readUInt8(1);
}
};
How it works
According to the official Bluetooth specification for the Heart Rate Measurement characteristic, the first byte is a "flags" field.
- Bit 0: If this bit is 0, the heart rate value is a single byte (UINT8). If it's 1, the value is two bytes (UINT16).
- Other bits: Indicate other data like sensor contact status and energy expended, which we are ignoring for this tutorial.
Our parseHeartRate function reads the first byte to check this flag and then reads the subsequent byte(s) accordingly to get the heart rate value.
Putting It All Together
Here is the complete App.js with all the components integrated:
// App.js
import React, { useState, useEffect } from 'react';
import {
SafeAreaView,
View,
Text,
Button,
FlatList,
PermissionsAndroid,
Platform,
StyleSheet,
} from 'react-native';
import { BleManager } from 'react-native-ble-plx';
import { Buffer } from 'buffer';
const manager = new BleManager();
const HEART_RATE_SERVICE_UUID = '0000180d-0000-1000-8000-00805f9b34fb';
const HEART_RATE_MEASUREMENT_CHARACTERISTIC_UUID = '00002a37-0000-1000-8000-00805f9b34fb';
const App = () => {
const [devices, setDevices] = useState([]);
const [isScanning, setIsScanning] = useState(false);
const [connectedDevice, setConnectedDevice] = useState(null);
const [heartRate, setHeartRate] = useState(0);
const requestBluetoothPermission = async () => {
// Permission logic remains the same...
};
const startScan = async () => {
// Scan logic remains the same...
};
const connectToDevice = async (device) => {
try {
manager.stopDeviceScan();
const connected = await device.connect();
setConnectedDevice(connected);
await connected.discoverAllServicesAndCharacteristics();
monitorHeartRate(connected);
} catch (error) {
console.error('Connection error:', error);
}
};
const parseHeartRate = (buffer) => {
const flags = buffer.readUInt8(0);
const is16Bit = (flags & 0x01) !== 0;
return is16Bit ? buffer.readUInt16LE(1) : buffer.readUInt8(1);
};
const monitorHeartRate = (device) => {
device.monitorCharacteristicForService(
HEART_RATE_SERVICE_UUID,
HEART_RATE_MEASUREMENT_CHARACTERISTIC_UUID,
(error, characteristic) => {
if (error) {
console.error('Monitoring error:', error);
return;
}
if (characteristic?.value) {
const buffer = Buffer.from(characteristic.value, 'base64');
const heartRateValue = parseHeartRate(buffer);
setHeartRate(heartRateValue);
}
},
);
};
const disconnectDevice = () => {
if (connectedDevice) {
connectedDevice.cancelConnection();
setConnectedDevice(null);
setHeartRate(0);
}
};
return (
<SafeAreaView style={styles.container}>
<View style={styles.header}>
<Text style={styles.title}>BLE Heart Rate Monitor</Text>
</View>
{connectedDevice ? (
<View style={styles.heartRateContainer}>
<Text style={styles.heartRateText}>{heartRate} BPM</Text>
<Button title="Disconnect" onPress={disconnectDevice} />
</View>
) : (
<>
<Button title={isScanning ? 'Scanning...' : 'Scan for Devices'} onPress={startScan} disabled={isScanning} />
<FlatList
data={devices}
keyExtractor={item => item.id}
renderItem={({ item }) => (
<View style={styles.deviceItem}>
<Text>{item.name || 'Unknown Device'}</Text>
<Button title="Connect" onPress={() => connectToDevice(item)} />
</View>
)}
/>
</>
)}
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: { flex: 1, margin: 20 },
header: { marginBottom: 20 },
title: { fontSize: 24, fontWeight: 'bold' },
heartRateContainer: { flex: 1, justifyContent: 'center', alignItems: 'center' },
heartRateText: { fontSize: 48, fontWeight: 'bold', marginBottom: 20 },
deviceItem: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', padding: 10, borderBottomWidth: 1, borderBottomColor: '#ccc' },
});
export default App;
Security Best Practices
- Always request permissions at the appropriate time. Don't ask for Bluetooth permissions the moment the app launches; wait until the user intends to use a feature that requires it.
- Handle connection errors gracefully. BLE connections can be unstable. Implement logic to handle unexpected disconnections and provide clear feedback to the user.
- Validate incoming data. While we trust the heart rate monitor, in other applications, you should always validate the data received from a BLE peripheral to prevent crashes or unexpected behavior.
Conclusion
You've now built a fully functional React Native app that can connect to a BLE heart rate monitor and display real-time data. You've tackled some of the most challenging aspects of BLE development, including permissions, scanning, connecting, and parsing binary data. This foundation opens the door to countless IoT projects, from fitness trackers to smart home controllers.
The next steps could be to add features like charting the heart rate over time, saving sessions, or connecting to other types of BLE sensors.