WellAlly Logo
WellAlly康心伴
Development

Real-Time Health Data: Connecting React Native to a BLE Heart Rate Monitor

An advanced tutorial on connecting a React Native app to a BLE heart rate monitor. Learn to scan for devices, handle permissions, subscribe to characteristics, and parse raw binary data for real-time fitness tracking.

W
2025-12-21
9 min read

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

code
npx react-native init BLEHeartRateApp
cd BLEHeartRateApp
npm install react-native-ble-plx
Code collapsed

For iOS:

Navigate to your ios directory and run pod install. Then, open Info.plist and add the following keys to request Bluetooth permissions:

code
<!-- 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>
Code collapsed

For Android:

Open your android/app/src/main/AndroidManifest.xml and add the following permissions:

code
<!-- 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" />
Code collapsed

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:

code
// 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;
Code collapsed

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:

code
// ... (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>}
};
Code collapsed

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:

code
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);
  }
};
Code collapsed

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:

code
// 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;
Code collapsed

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.

Resources

#

Article Tags

reactnativehealthtechiotble
W

WellAlly's core development team, comprised of healthcare professionals, software engineers, and UX designers committed to revolutionizing digital health management.

Expertise

Healthcare TechnologySoftware DevelopmentUser ExperienceAI & Machine Learning

Found this article helpful?

Try KangXinBan and start your health management journey

© 2024 康心伴 WellAlly · Professional Health Management