<<< Go back

Bluetooth Low Energy (BLE) with React Native and Arduino

May 10, 2024

Introduction

I have just built my first bluetooth low energy application (BLE) for a client and there are a few gotchas I would like to go over. As well as explain what exactly is Bluetooth Low Energy and how it is different from Bluetooth classic. Furthermore, discuss how you can program it on an ESP32 and in React Native for your next application. Then lastly go over some optimization for the data you are sending back and forth via BLE communication to allow for the sending the maximum amount of data possible.

Introduction into Bluetooth

There are two types of bluetooth connection modes. Bluetooth Classic vs Bluetooth Low Energy.

Bluetooth Classic

Bluetooth Classic is the original form of bluetooth. Its ideal application is when an application needs to stream a large amount of data from one device to another. Such as a pair of headphones streaming music from a smartphone, or printing a file from a mobile phone to a Bluetooth enabled printer.

When a device is using Bluetooth Classic, it can only send data via point-to-point communication. Meaning it can only talk to one device at a time while in a data exchange. Its what results in a higher data transfer speed overall.

Bluetooth Low Energy (BLE)

With a max transfer size of 512 bytes per transaction. BLE is definitely not made to handle big data transfers. But more ideal for connected IoT devices such as sensors or smart toggles.

In addition to data transfer via point-to-point communication like Bluetooth Classic, BLE also offers connecting via mesh networking. Which will allow a bluetooth application to pass on messages from one device to another until it reaches the correct destination.

BLE also offers data communication via broadcast. In which a bluetooth device will announce the data it holds to any device that will listen.

In the Arduino IDE, there is currently only support for point-to-point and broadcast communication modes. Mesh networking is not on any kind of roadmap for implementation but can be implemented via the ESP-IDF toolchain if needed for the ESP32 or you can also look at this example here.

Going deeper on BLE

A BLE service has three main parts being: profile, service, and characteristic. As shown in this diagram below:

bluetooth stack

Profile

A profile is the top level of the hierarchy in the bluetooth service stack. It can hold multiple services. An then a service can consist of multiple characteristics.

Service

A service is a collection of data and associated behaviors to accomplish a particular goal. really a grouper for the characteristics that hold and send the actual data in the application.

I wondered why someone might need multiple services in an application, but according to this stack overflow answer you can have private services that are private and only visible on specific devices. Or grouping related characteristics together such as device information, battery health and heart data.

characteristic

A characteristic is a unique identifier used to send data between devices.

Setting up Bluetooth Low Energy on a ESP32

In this article, I am going to assume that you have use the Arduino IDE a little bit and have setup your ESP32 to work with the Arduino IDE to work correctly.

Bluetooth Client vs Bluetooth Server

The client is a device that starts commands and requests towards the server and can receive different types of data from the server.

The server is the device that accepts incoming commands and requests from the client.

In this example our server will be the ESP32 device.

Initializing the Bluetooth server in Arduino IDE

After creating a sketch, we will want to add the following headers to our sketch:

1#include <BLEDevice.h>
2#include <BLEUtils.h>
3#include <BLEServer.h>
4#include <BLE2902.h>

Then we will have to create an instance of "BLEServer", as well as define a string for device name and service UUID at top of our file.

The device name is what shows up in the bluetooth menu of other devices when they try to connect to our ESP32.

The service UUID is what wholes our characteristics for our application and we can also use to connect to the device without knowing the device name. Due to memory restrictions on the ESP32 we can only have one service running.

You can generate UUIDs using this tool here: https://www.uuidgenerator.net/ and we will be using it to generate IDs for our service and characteristics.

1#include <BLEDevice.h>
2#include <BLEUtils.h>
3#include <BLEServer.h>
4#include <BLE2902.h>
5
6BLEServer *pServer;
7#define DEVICE_NAME "MyESP32"
8#define SERVICE_UUID "ab49b033-1163-48db-931c-9c2a3002ee1d"
9
10
11void setup() {
12// ...
13}

After we have defined our strings. we will move into the setup function of our code to initialize our BLEServer called "pServer" using the device name and service UUID we choose.

1#include <BLEDevice.h>
2#include <BLEUtils.h>
3#include <BLEServer.h>
4#include <BLE2902.h>
5
6BLEServer *pServer;
7#define DEVICE_NAME "MyESP32"
8#define SERVICE_UUID "ab49b033-1163-48db-931c-9c2a3002ee1d"
9
10void setup() {
11  // put your setup code here, to run once:
12  BLEDevice::init(DEVICE_NAME);
13  pServer = BLEDevice::createServer();
14  BLEService *pService = pServer->createService(SERVICE_UUID);
15}

We then can start advertising our service and our BLE device with the following code:

1#include <BLEDevice.h>
2#include <BLEUtils.h>
3#include <BLEServer.h>
4#include <BLE2902.h>
5
6BLEServer *pServer;
7#define DEVICE_NAME "MyESP32"
8#define SERVICE_UUID "ab49b033-1163-48db-931c-9c2a3002ee1d"
9
10void setup() {
11  // put your setup code here, to run once:
12  BLEDevice::init(DEVICE_NAME);
13  pServer = BLEDevice::createServer();
14  BLEService *pService = pServer->createService(SERVICE_UUID);
15
16    //start the service
17  pService->start();
18  //start advertising service
19  BLEAdvertising *pAdvertising = pServer->getAdvertising();
20  pAdvertising->addServiceUUID(SERVICE_UUID);
21// helps with IPhone pairing
22  pAdvertising->setScanResponse(true);
23  pAdvertising->setMinPreferred(0x06);
24  pAdvertising->setMinPreferred(0x12);
25  BLEDevice::startAdvertising();
26  Serial.println("Ready! For your bits!");
27}
28
29void loop() {
30  // put your main code here, to run repeatedly:
31
32}

Afterwards, we should download the BLE Scanner app. So we can look for our newly created BLE device:

We can see the device in the list of bluetooth devices in the BLE Scanner app

list of bluetooth devices

and after connecting to our ESP32 we can see the service UUID we define in the source code (ab49b033-1163-48db-931c-9c2a3002ee1d)

Showing details of the ESP 32

Cool now that we can see our service is working correctly, lets setup some characteristics to send data back and forth between our ESP32 and the React native app we are going to build.

Configuring Characteristics in the Arduino IDE

Using the UUID tool I mention before ( https://www.uuidgenerator.net) lets generate a UUID for our first characteristic and putt it at the top of our file. As well as an new variable for BLECharacteristic

We will create one to track step count for now.

1BLECharacteristic *pStepCountCharacteristic;
2#define STEPCOUNT_CHARACTERISTIC_UUID "fbb6411e-26a7-44fb-b7a3-a343e2b011fe"

Then for initializing a characteristic you need to define a callback class that will provide actions when your characteristic is used. The one I am going to show below is empty but you can create additional functionality if need for on value read or write.

1class MyCallbacks: public BLECharacteristicCallbacks {
2    void onWrite(BLECharacteristic *pCharacteristic) {
3    }
4};

I created a helper function to create characteristics more compactly and it is useful if you have a few characteristics that have the same requirements. It looks like the following:

1BLECharacteristic* createCharacteristic(std::string characteristicUuid, BLEService *pService)
2{
3  BLECharacteristic *characteristic = pService->createCharacteristic( 
4                                         characteristicUuid,
5                                         BLECharacteristic::PROPERTY_READ |
6                                         BLECharacteristic::PROPERTY_NOTIFY |
7                                         BLECharacteristic::PROPERTY_WRITE
8                                       );
9  characteristic->addDescriptor(new BLE2902());
10  characteristic->setCallbacks(new MyCallbacks());
11  return characteristic;
12}

This function takes a string that is a uuid used to define a characteristic, and a service to tie the characteristic to.

The function assumes that you want to read, write and be able to tell a device of any changes.

it adds a generic description to the characteristic and adds a instance of the callback we have defined.

We can now use this function in our setup function to define characteristics like the following:

1pStepCountCharacteristic = createCharacteristic(STEPCOUNT_CHARACTERISTIC_UUID, pService);

Now we have added this code for creating characteristics, lets get caught up on all the code we have written so far:

1#include <BLEDevice.h>
2#include <BLEUtils.h>
3#include <BLEServer.h>
4#include <BLE2902.h>
5
6BLEServer *pServer;
7#define DEVICE_NAME "MyESP32"
8#define SERVICE_UUID "ab49b033-1163-48db-931c-9c2a3002ee1d"
9
10BLECharacteristic *pStepCountCharacteristic;
11#define STEPCOUNT_CHARACTERISTIC_UUID "fbb6411e-26a7-44fb-b7a3-a343e2b011fe"
12
13class MyCallbacks: public BLECharacteristicCallbacks {
14    void onWrite(BLECharacteristic *pCharacteristic) {
15    }
16    
17    void onConnect(BLEServer* pServer) {
18      }
19
20    void onDisconnect(BLEServer* pServer){
21      }
22};
23
24BLECharacteristic* createCharacteristic(std::string characteristicUuid, BLEService *pService)
25{
26  BLECharacteristic *characteristic = pService->createCharacteristic( 
27                                         characteristicUuid,
28                                         BLECharacteristic::PROPERTY_READ |
29                                         BLECharacteristic::PROPERTY_NOTIFY |
30                                         BLECharacteristic::PROPERTY_WRITE
31                                       );
32  characteristic->addDescriptor(new BLE2902());
33  characteristic->setCallbacks(new MyCallbacks());
34  return characteristic;
35}
36
37void setup() {
38  // put your setup code here, to run once:
39  Serial.begin(115200);
40  while(!Serial);
41  BLEDevice::init(DEVICE_NAME);
42  pServer = BLEDevice::createServer();
43  BLEService *pService = pServer->createService(SERVICE_UUID);
44
45    //start the service
46  pService->start();
47  //start advertising service
48  BLEAdvertising *pAdvertising = pServer->getAdvertising();
49  pAdvertising->addServiceUUID(SERVICE_UUID);
50  // helps with IPhone pairing
51  pAdvertising->setScanResponse(true);
52  pAdvertising->setMinPreferred(0x06);
53  pAdvertising->setMinPreferred(0x12);
54  BLEDevice::startAdvertising();
55  Serial.println("Ready! For your bits!");
56}
57
58void loop() {
59  // put your main code here, to run repeatedly:
60}


Now that we are caught up on what we are rwThen we can use it to send random data from our loop function for now as the following shows:

1void loop() {
2  // put your main code here, to run repeatedly:
3  stepCount = random(30);
4  String stepData = String(stepCount);
5  pStepDataCharacteristic->setValue(stepData.c_str());
6  pStepDataCharacteristic->notify();
7
8  Serial.print("Value is: ");
9  Serial.println(stepCount);
10}

Setting up BLE in a React Native application

For handling the Bluetooth communication we will use dotintent/react-native-ble-plx. But first this might be your first React Native project, so lets cover all the bases and show you how to configure react native on your computer.

Preparing for React Native

I like using Android to develop applications for react native because it can be easier to generate builds for testing and I have a few android phones just laying around my apartment. So you can follow this guide provided by the react native wiki and we will all be on the same page: https://reactnative.dev/docs/environment-setup

Then you will need to put your android phone into developer mode. Usually, by going to the "about phone" section in your settings menu and clicking on the "build number" until your developer mode setting enables.

Under "Developer options" I would enable the following settings:

  • "Stay awake"
  • "USB debugging"

Now you can plug your android phone into your computer and start coding your first bluetooth application.

Additionally, I found this 3D model on printables useful for holding my phone why writing code for my react native application: https://www.printables.com/model/52407-no-supports-phone-holderstand/files

You can see the phone in the holder below in the image

Phone holder

Creating the React Native application

Moving on to creating the application. Run the following command to create an expo project:

1npx create-expo-app -t expo-template-blank-typescript BLETest

It will use the name you have provided and start generating a folder for you to use. I named my 'BLETest' so just 'cd' into that folder after it is done generating.

Run the following command so we can use expo to install our dependencies

1npm i -g expo

We also have to go to our package.json and change the expo version to be less then expo 49 because some dependency conflicts occur due to the bluetooth package being installed.

Screenshot 2024-05-16 at 5.13.59 PM

Then run the following command to remove the old node modules installed when creating the package and install the new versions:

1rm -rf node_modules && npm i

Now we can start writing some code to handle permissions for bluetooth before we install our bluetooth package. To the best of my knowledge this is not necessary for IOS, but we only need to handle the permissions for android. Luckily, I have found a react hook that can handle this for us. By visiting this gist by ivanstnsk on Github and copying it into our codebase and updating the PERMISSIONS_REQUEST to the following:

1const PERMISSIONS_REQUEST = [
2  PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,
3  PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT,
4  PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN,
5];

We can now request the correct permissions for bluetooth BLE on our android phone. Additionally, you can look at all the permissions you can request here at the following link if you need to request additional permissions for your application.

permission dialog on android

Android Bluetooth permissions

Then we can use it in the body of our App component like so:

1import { StatusBar } from 'expo-status-bar';
2import { useEffect, useState } from 'react';
3import { Button, Platform, Text, View } from 'react-native';
4import { useAndroidPermissions } from './useAndroidPermissions';
5
6export default function App() {
7  const [hasPermissions, setHasPermissions] = useState<boolean>(Platform.OS == 'ios');
8  const [waitingPerm, grantedPerm] = useAndroidPermissions();
9
10  useEffect(() => {
11    if (!(Platform.OS == 'ios')){
12      setHasPermissions(grantedPerm);
13    }
14  }, [grantedPerm])
15
16  return (
17    <View
18    style={{flex: 1, alignItems: 'center', justifyContent:'center'}}
19    >
20    {
21      !hasPermissions && (
22        <View> 
23          <Text>Looks like you have not enabled Permission for BLE</Text>
24        </View>
25      )
26    }
27    {hasPermissions &&(
28      <Text>BLE Premissions enabled!</Text>
29    )
30    }
31      <StatusBar style="auto" />
32    </View>
33  );
34}

This will skip the permission check on IOS and only check the permissions on android, as well we can wait for the permissions to become available and react to the change.

Now we can move on and we will install the bluetooth module with the following command:

1expo install react-native-ble-plx

After which we should have the module installed to develop our Bluetooth application.

Going into our "App.tsx" file we can start writing our bluetooth code. By declaring an instance of our BleManager like this

1import { BleManager } from 'react-native-ble-plx';
2
3const bleManager = new BleManager();

We then can grab the strings of our Device Name and service UUID from the Arduino code and put them as strings in our React Native code:

1import { BleManager } from 'react-native-ble-plx';
2const bleManager = new BleManager();
3
4const DEVICE_NAME = "MyESP32";
5const SERVICE_UUID = "ab49b033-1163-48db-931c-9c2a3002ee1d";

Then we want to create some status state variables to track connections of the bluetooth devices, so that we know if a device has successfully connected or when we have lost connection:

1const [connectionStatus, setConnectionStatus] = useState("Searching...");
2const [isConnected, setIsConnected] = useState<boolean>(false);

Then in our application we can create a function for searching for our esp32 device. Then we can call this function when we know the device has the permissions for Bluetooth.

1const [waitingPerm, grantedPerm] = useAndroidPermissions();
2 const [hasPermissions, setHasPermissions] = useState<boolean>(Platform.OS == 'ios');
3
4  useEffect(() => {
5    if (!(Platform.OS == 'ios')){
6      setHasPermissions(grantedPerm);
7    }
8  }, [grantedPerm])
9
10  useEffect(() => {
11    if(hasPermissions){
12      searchAndConnectToDevice();
13    }
14  }, [hasPermissions]);
15
16const searchAndConnectToDevice = () =>
17    bleManager.startDeviceScan(null, null, (error, device) => {
18      if (error) {
19        console.error(error);
20        setIsConnected(false);
21        setConnectionStatus("Error searching for devices");
22        return;
23      }
24      if (device?.name === DEVICE_NAME) {
25        bleManager.stopDeviceScan();
26        setConnectionStatus("Connecting...");
27        connectToDevice(device);
28      }
29    });

Then we can create a function for handling the connection to our ESP32 once our phone has found the device, and create the supporting state variables to hold the data once we have made a connection

1const [device, setDevice] = useState<Device | null>(null);
2
3    const connectToDevice = async (device: Device) => {
4      try {
5      const _device = await device.connect(); 
6       // require to make all services and Characteristics accessable
7      await _device.discoverAllServicesAndCharacteristics();
8      setConnectionStatus("Connected");
9      setIsConnected(true);
10      setDevice(_device);
11      } catch (error){
12          setConnectionStatus("Error in Connection");
13          setIsConnected(false);
14      }
15    };

Lastly we need to handle disconnection and update the body of our App.tsx to show the status of our BLE connection.

1useEffect(() => {
2      if (!device) {
3        return;
4      }
5
6      const subscription = bleManager.onDeviceDisconnected(
7        device.id,
8        (error, device) => {
9          if (error) {
10            console.log("Disconnected with error:", error);
11          }
12          setConnectionStatus("Disconnected");
13          setIsConnected(false);
14          console.log("Disconnected device");
15          if (device) {
16            setConnectionStatus("Reconnecting...");
17            connectToDevice(device)
18              .then(() => {
19                setConnectionStatus("Connected");
20                setIsConnected(true);
21              })
22              .catch((error) => {
23                console.log("Reconnection failed: ", error);
24                setConnectionStatus("Reconnection failed");
25                setIsConnected(false);
26                setDevice(null);
27              });
28          }
29        }
30      );
31
32      return () => subscription.remove();
33    }, [device]);
1return (
2    <View
3    style={{flex: 1, alignItems: 'center', justifyContent:'center'}}
4    >
5    {
6      !hasPermissions && (
7        <View> 
8          <Text>Looks like you have not enabled Permission for BLE</Text>
9        </View>
10      )
11    }
12    {hasPermissions &&(
13      <View>
14      <Text>BLE Premissions enabled!</Text>
15      <Text>The connection status is: {connectionStatus}</Text>
16      <Button 
17      disabled={!isConnected} 
18      onPress={() => {}}
19      title={`The button is ${isConnected ? "enabled" : "disabled"}`}
20      />
21      </View>
22    )
23    }
24      <StatusBar style="auto" />
25    </View>
26  );

Before making a connection:

Screenshot_20240517-174438

After making a the connection to the ESP32:

Screenshot_20240517-190255

Wow! that was a lot of code! so before we move on lets review the whole code snippet we have just written:

1import { StatusBar } from 'expo-status-bar';
2import { useEffect, useState } from 'react';
3import { Button, Platform, Text, View } from 'react-native';
4import { BleManager, Device } from 'react-native-ble-plx';
5import { useAndroidPermissions } from './useAndroidPermissions';
6
7const bleManager = new BleManager();
8
9const DEVICE_NAME = "MyESP32";
10const SERVICE_UUID = "ab49b033-1163-48db-931c-9c2a3002ee1d";
11
12export default function App() {
13  const [hasPermissions, setHasPermissions] = useState<boolean>(Platform.OS == 'ios');
14  const [waitingPerm, grantedPerm] = useAndroidPermissions();
15
16  const [connectionStatus, setConnectionStatus] = useState("Searching...");
17  const [isConnected, setIsConnected] = useState<boolean>(false);
18
19  useEffect(() => {
20    if (!(Platform.OS == 'ios')){
21      setHasPermissions(grantedPerm);
22    }
23  }, [grantedPerm])
24
25  useEffect(() => {
26    if(hasPermissions){
27      searchAndConnectToDevice();
28    }
29  }, [hasPermissions]);
30
31  const searchAndConnectToDevice = () =>
32    bleManager.startDeviceScan(null, null, (error, device) => {
33      if (error) {
34        console.error(error);
35        setIsConnected(false);
36        setConnectionStatus("Error searching for devices");
37        return;
38      }
39      if (device?.name === DEVICE_NAME) {
40        bleManager.stopDeviceScan();
41        setConnectionStatus("Connecting...");
42        connectToDevice(device);
43      }
44    });
45
46
47    const [device, setDevice] = useState<Device | null>(null);
48
49    const connectToDevice = async (device: Device) => {
50      try {
51      const _device = await device.connect(); 
52       // require to make all services and Characteristics accessable
53      await _device.discoverAllServicesAndCharacteristics();
54      setConnectionStatus("Connected");
55      setIsConnected(true);
56      setDevice(_device);
57      } catch (error){
58          setConnectionStatus("Error in Connection");
59          setIsConnected(false);
60      }
61    };
62
63    useEffect(() => {
64      if (!device) {
65        return;
66      }
67
68      const subscription = bleManager.onDeviceDisconnected(
69        device.id,
70        (error, device) => {
71          if (error) {
72            console.log("Disconnected with error:", error);
73          }
74          setConnectionStatus("Disconnected");
75          setIsConnected(false);
76          console.log("Disconnected device");
77          if (device) {
78            setConnectionStatus("Reconnecting...");
79            connectToDevice(device)
80              .then(() => {
81                setConnectionStatus("Connected");
82                setIsConnected(true);
83              })
84              .catch((error) => {
85                console.log("Reconnection failed: ", error);
86                setConnectionStatus("Reconnection failed");
87                setIsConnected(false);
88                setDevice(null);
89              });
90          }
91        }
92      );
93
94      return () => subscription.remove();
95    }, [device]);
96  
97
98  return (
99    <View
100    style={{flex: 1, alignItems: 'center', justifyContent:'center'}}
101    >
102    {
103      !hasPermissions && (
104        <View> 
105          <Text>Looks like you have not enabled Permission for BLE</Text>
106        </View>
107      )
108    }
109    {hasPermissions &&(
110      <View>
111      <Text>BLE Premissions enabled!</Text>
112      <Text>The connection status is: {connectionStatus}</Text>
113      <Button 
114      disabled={!isConnected} 
115      onPress={() => {}}
116      title={`The button is ${isConnected ? "enabled" : "disabled"}`}
117      />
118      </View>
119    )
120    }
121      <StatusBar style="auto" />
122    </View>
123  );
124}

Setting up characteristics

Moving on to setting up a BLE characteristic, which is the piece that holds the actual data to be transmitted from our BLE device to our smart phone.

We can now grab the step count characteristic uuid from our Arduino code and put it into our React native code. Like the following:

1const STEPCOUNT_CHARACTERISTIC_UUID = "fbb6411e-26a7-44fb-b7a3-a343e2b011fe";

And we will also need to install a package that can un-encode the values from base64 to be integer values for further processing later on:

1expo install react-native-quick-base64

Then we can use a useEffect to watch for changes to the device and add a listener for if there is an update to the step characteristic

1import {  atob } from "react-native-quick-base64";
2//...
3
4 const [stepCount, setStepCount] = useState(-1);
5      useEffect(() => {
6      if(!device || !device.isConnected) {
7        return
8      }
9      const sub = device.monitorCharacteristicForService(
10        SERVICE_UUID,
11        STEPCOUNT_CHARACTERISTIC_UUID,
12        (error, char) => {
13          if (error || !char) {
14            return;
15          }
16
17          const rawValue = parseInt(atob(char?.value ?? ""));
18          setStepCount(rawValue);
19        }
20      )
21      return () => sub.remove()
22    }, [device])

Now we can update the body of our app to use this new data and looking at the overall code changes from adding this characteristic:

1import { StatusBar } from 'expo-status-bar';
2import { useEffect, useState } from 'react';
3import { Button, Platform, Text, View } from 'react-native';
4import { BleManager, Device } from 'react-native-ble-plx';
5import { useAndroidPermissions } from './useAndroidPermissions';
6import {  atob } from "react-native-quick-base64";
7
8const bleManager = new BleManager();
9
10const DEVICE_NAME = "MyESP32";
11const SERVICE_UUID = "ab49b033-1163-48db-931c-9c2a3002ee1d";
12const STEPCOUNT_CHARACTERISTIC_UUID = "fbb6411e-26a7-44fb-b7a3-a343e2b011fe";
13
14export default function App() {
15  const [hasPermissions, setHasPermissions] = useState<boolean>(Platform.OS == 'ios');
16  const [waitingPerm, grantedPerm] = useAndroidPermissions();
17
18  const [connectionStatus, setConnectionStatus] = useState("Searching...");
19  const [isConnected, setIsConnected] = useState<boolean>(false);
20
21  const [device, setDevice] = useState<Device | null>(null);
22
23  const [stepCount, setStepCount] = useState(-1);
24
25  useEffect(() => {
26    if (!(Platform.OS == 'ios')){
27      setHasPermissions(grantedPerm);
28    }
29  }, [grantedPerm])
30
31  useEffect(() => {
32    if(hasPermissions){
33      searchAndConnectToDevice();
34    }
35  }, [hasPermissions]);
36
37  const searchAndConnectToDevice = () =>
38    bleManager.startDeviceScan(null, null, (error, device) => {
39      if (error) {
40        console.error(error);
41        setIsConnected(false);
42        setConnectionStatus("Error searching for devices");
43        return;
44      }
45      if (device?.name === DEVICE_NAME) {
46        bleManager.stopDeviceScan();
47        setConnectionStatus("Connecting...");
48        connectToDevice(device);
49      }
50    });
51
52    const connectToDevice = async (device: Device) => {
53      try {
54      const _device = await device.connect(); 
55       // require to make all services and Characteristics accessable
56      await _device.discoverAllServicesAndCharacteristics();
57      setConnectionStatus("Connected");
58      setIsConnected(true);
59      setDevice(_device);
60      } catch (error){
61          setConnectionStatus("Error in Connection");
62          setIsConnected(false);
63      }
64    };
65
66    useEffect(() => {
67      if (!device) {
68        return;
69      }
70
71      const subscription = bleManager.onDeviceDisconnected(
72        device.id,
73        (error, device) => {
74          if (error) {
75            console.log("Disconnected with error:", error);
76          }
77          setConnectionStatus("Disconnected");
78          setIsConnected(false);
79          console.log("Disconnected device");
80          if (device) {
81            setConnectionStatus("Reconnecting...");
82            connectToDevice(device)
83              .then(() => {
84                setConnectionStatus("Connected");
85                setIsConnected(true);
86              })
87              .catch((error) => {
88                console.log("Reconnection failed: ", error);
89                setConnectionStatus("Reconnection failed");
90                setIsConnected(false);
91                setDevice(null);
92              });
93          }
94        }
95      );
96
97      return () => subscription.remove();
98    }, [device]);
99  
100    useEffect(() => {
101      if(!device || !device.isConnected) {
102        return
103      }
104      const sub = device.monitorCharacteristicForService(
105        SERVICE_UUID,
106        STEPCOUNT_CHARACTERISTIC_UUID,
107        (error, char) => {
108          if (error || !char) {
109            return;
110          }
111
112          const rawValue = parseInt(atob(char?.value ?? ""));
113          setStepCount(rawValue);
114        }
115      )
116      return () => sub.remove()
117    }, [device])
118
119
120  return (
121    <View
122    style={{flex: 1, alignItems: 'center', justifyContent:'center'}}
123    >
124    {
125      !hasPermissions && (
126        <View> 
127          <Text>Looks like you have not enabled Permission for BLE</Text>
128        </View>
129      )
130    }
131    {hasPermissions &&(
132      <View>
133      <Text>BLE Premissions enabled!</Text>
134      <Text>The connection status is: {connectionStatus}</Text>
135      <Button 
136      disabled={!isConnected} 
137      onPress={() => {}}
138      title={`The button is ${isConnected ? "enabled" : "disabled"}`}
139      />
140      <Text>The current Step count is: {stepCount}</Text>
141      </View>
142    )
143    }
144      <StatusBar style="auto" />
145    </View>
146  );
147}

Ta'da! We are now receiving data from ESP32 via BLE on our smart phone! 🎉

well done

You can look at the full code we have written here: https://github.com/cjoshmartin/BLE-example-code/commit/b2aebcb78c89d38be871d402a22c8a52706e6139

Optimizations

Okay, Like all the other applications you can find on the internet we have created our basic BLE application. Lets go over some additional optimizations and use cases to increase the performance of our application using the following bulleted list:

  • Sending data to the BLE device from the mobile Application
  • Optimizing Connecting and reconnecting of devices
  • Searching for BLE Devices rather then a static device
  • Characteristics Optimization to allow the transmission of lots of data via BLE

Sending data to the BLE device from the mobile Application

Okay in addition to step count say there is some data on the phone that we want to send or sync with the ESP32. Maybe its heart rate or some other type of data. How we would we go about syncing that data with the ESP32.

We would first need to generate a new UUID ID using the tool I mention before: https://www.uuidgenerator.net/

After generating this ID we will use it for a new characteristic for sending heart rate from the phone to the ESP32.

1int heartRate = 0;
2BLECharacteristic *pHeartRateCharacteristic;
3# define HEARTRATE_CHARACTERISTIC_UUID "c58b67c8-f685-40d2-af4c-84bcdaf3b22e"

Then we initialize the characteristic in our setup function

1void setup() {
2// ...
3pHeartRateCharacteristic = createCharacteristic(HEARTRATE_CHARACTERISTIC_UUID, pService);
4//...
5}

Then we can create a function for reading in values and converting them to integer values

1int readValue(BLECharacteristic *characteristic) {
2  if (characteristic == NULL) {
3    return -1;
4  }
5  std::string value = std::string(
6      (
7        characteristic->getValue()
8      ).c_str()
9    );
10
11  try{
12    return stoi(value);
13  } catch(...) {
14    return -1;
15  }
16}

Then we can use this in our loop function to read the values from the phone as follows:

1void loop() {
2// ...
3  heartRate = readValue(pHeartRateCharacteristic);
4  Serial.print("heart rate value is: ");
5  if (heartRate > 0){
6    Serial.println(heartRate);
7  }
8  else {
9    Serial.println("No Heart rate value received");
10  }
11// ...
12}

And that's it! for the Arduino code. If you would to review the code for later you can look at the the commit here: https://github.com/cjoshmartin/BLE-example-code/commit/c1a4632c933d3bc2ca9f0e72e5d750e2c79aa8a7

Moving on to the React Native code to send the data to the ESP32 device. we can paste in the UUID we set for the characteristic for heart rate

1const  HEARTRATE_CHARACTERISTIC_UUID = "c58b67c8-f685-40d2-af4c-84bcdaf3b22e";

Then add a new state variable for the heart rate and a use effect that will react to the change in value of the heart rate and send that off to the ESP32 device

1useEffect(() => {
2    if(!device || !device.isConnected){
3      return;
4    }
5    device.writeCharacteristicWithResponseForService(
6      SERVICE_UUID,
7      HEARTRATE_CHARACTERISTIC_UUID,
8      btoa(String(heartRate))
9    ).catch(e => {})
10  }, [heartRate])

We now should be able to send data to the ESP32 anytime the heart rate variable changes if a device is connected. So let's update the body of the app so that we can cause the heart rate variable to change and verify that the data is being received on the ESP32.

1<View style={{margin: 10}}>
2            <Text style={{ fontWeight: "500", margin: 5 }}>
3              Heart Rate value is: {heartRate}
4            </Text>
5            <Button
6              onPress={() => setHeartRate(heartRate + 1)}
7              title="Increase User's heart rate"
8              color="red"
9            />
10          </View>

Afterwards, here is a video of testing the code on the devices:

It's a little hard to hold a mobile phone for recording as well as click on another one. But anyways Ta-da we are now sending data from the phone to the ESP32!

The code for the React code can be found at this commit: https://github.com/cjoshmartin/BLE-example-code/commit/d5fd11c496189c7ae0bd71933360f36687ce2a08

Optimizing Connecting and reconnecting of devices

Moving on to optimizing the connection process for BLE. Often the problem I have when it comes to BLE from the examples I see online is that you can only connect to the ESP32 if the device has first booted up. I need to restart the ESP32 once the device has lost connection. Which is not always a practical solution. Luckily, I have found a way to make the ESP32 look for a device to connect once it has lost connection regardless if the device has just booted or not.

By adding service callbacks that will update a boolean with the state of the device's connection as in the following example.

1bool deviceConnected = false;
2bool oldDeviceConnected = false;
3
4class MyServerCallbacks: public BLEServerCallbacks {
5    void onConnect(BLEServer* pServer) {
6      deviceConnected = true;
7      Serial.println("Connected from device");
8    };
9
10    void onDisconnect(BLEServer* pServer) {
11      deviceConnected = false;
12      Serial.println("Disconnected from device");
13    }
14};

And then attaching them to our BLE server on device setup. We can track the change in our connect status.

1void setup() {
2//...
3pServer->setCallbacks(new MyServerCallbacks());
4//...
5}

Then in our loop function we can validate the connection status and start advertising if a device loses connection.

1void loop() {
2// ...   
3// disconnecting
4  if (!deviceConnected && oldDeviceConnected) {
5    delay(500); // give the bluetooth stack the chance to get things ready
6    pServer->startAdvertising(); // restart advertising
7    Serial.println("start advertising");
8    oldDeviceConnected = deviceConnected;
9  }
10  // connecting
11  if (deviceConnected && !oldDeviceConnected) {
12    // do stuff here on connecting
13    oldDeviceConnected = deviceConnected;
14  }
15
16     if(!deviceConnected){
17    return;
18  }
19// ...
20}

And Cool! Now we can generate a more reliable connection from the ESP32 via BLE! Which is pretty cool! And you can view the full code changes at this commit: https://github.com/cjoshmartin/BLE-example-code/commit/6a3ba65e21252e72184e25a01c42023a294983e2

Searching for BLE Devices rather then a static device

So moving once again to searching for a BLE device rather then using a static service UUID and Device name. Because your application might require to connect to different devices or you have multiple of the same devices deployed into the field.

We will update our searchAndConnectToDevice function so that it no longer actually connects to a fixed device name rather it will add a listener that will scan the BLE devices its sees and keep track off them in a list. Using a ref for this said list will insure that the data persist between renders in our list but because of that we will manually have to update the screen with the changes in the list using an setInterval function.

You can see that function below:

1const devicesRef = useRef({});
2  const devicesRefreshIntervalRef = useRef(0);
3
4  const searchAndConnectToDevice = () => {
5    bleManager.startDeviceScan([], {allowDuplicates: false}, (error, device) => {
6
7      if (error) {
8        console.error(error);
9        setIsConnected(false);
10        setConnectionStatus("Error searching for devices");
11        return;
12      }
13      if (device?.name){
14        devicesRef.current
15        if(!devicesRef.current) {
16          devicesRef.current = {};
17        }
18        else {
19          //@ts-ignore
20          devicesRef.current[device.name] = device
21        }
22      }
23    });
24    
25    const id = setInterval(() => {
26          console.log('refreshing list to be: ', Object.keys(devicesRef?.current))
27          setDevices(devicesRef?.current);
28        }, 3000)
29    //@ts-ignore
30      devicesRefreshIntervalRef.current = id;
31  }

As a further note; you can restrict the devices that appear in list to be only device with the correct service UUID devices by update the first array param to bleManager.startDeviceScan. Like the following:

1bleManager.startDeviceScan([SERVICE_UUID], {allowDuplicates: false}, (error, device) => {})

Then we can update our actual connectToDevice function to stop the scanning for BLE devices as well as stop manual updating state. As the follows:

1const connectToDevice = async (device: Device) => {
2      bleManager.stopDeviceScan();
3      clearInterval(devicesRefreshIntervalRef.current);
4      devicesRefreshIntervalRef.current = 0;
5      try {
6      const _device = await bleManager.connectToDevice(device.id, undefined); 
7// ...
8}

Next we will create a compouent that will allow us to connect to a BLE device

1function Item (props: Device){
2      return (
3        <Pressable key={props.id}
4          onPress={() =>{
5                setConnectionStatus("Connecting...");
6                connectToDevice(props);
7          }}
8          style={{
9            backgroundColor: '#2596be',
10            padding: 16,
11            marginBottom: 10,
12            borderRadius: 20
13          }}
14        >
15          <Text
16            style={{
17              color: 'white',
18              fontWeight: '500',
19              fontSize: 16
20            }}
21          >{props.name} // {props.rssi}</Text>
22        </Pressable>
23      );
24    }

And finally, We will render out the list of the device we have found that can be connected to:

1{hasPermissions && !isConnected && (
2        <SafeAreaView
3          style={{
4            flex: 1,
5            //@ts-ignore
6            marginTop: 50,
7          }}
8        >
9          <Text style={{ fontWeight: "600", fontSize: 24, marginBottom: 14 }}>
10            Device connection list
11          </Text>
12          <ScrollView>
13            {Object.values(devices)
14              //@ts-ignore
15              .sort((a, b) => Number(b.rssi) - Number(a.rssi))
16              .filter((value, i) => i < 20)
17              .map((item: any) => (
18                <Item key={item.id + (item.rssi ?? "")} {...item} />
19              ))}
20          </ScrollView>
21        </SafeAreaView>
22      )}

And then once again Ta-da! We have a select menu for finding our bluetooth device by device name. Like all the other sections you can look at the code here:

https://github.com/cjoshmartin/BLE-example-code/commit/e1d338960211c53d245212dcd167c11da776b24b

Characteristics Optimization to allow the transmission of lots of data via BLE

Okay Our final and last section of this blog post. Finally, I have been writing this for weeks ha. But anyways let's make this app a little more useful. Lets turn this app into a Lighting app with two kinds of lights:

  1. Lights that just turn on and off
  2. Lights that can vary in brightness from 0-100 percent brightness.

In this application, I want to have four lights that just turn on and off. As well, four lights that vary in brightness. So that is eight pieces of data that will be communicated between the ESP32 and the React Native mobile application. However, characteristics that could be used to transmit each piece of these eight pieces of data would use too much memory on the ESP32. In my experience I have only been able to have four characteristics running at a time before the ESP32 runs out of memory and starts boot looping.

So we will have to compress the data down somehow... We can compress all this data down to two characteristics using the following methods.

For values that toggle on and off, we can compress those values into one integer value and run bitwise operations with bitwise mask to run operations such as toggling and checking if enabled.

for our example we can store all four of our lights in a four bit number like this:

'0000' - all lights are off

the mask would look as follows:

'1000' - light 1 mask

'0100' - light 2 mask

'0010' - light 3 mask

'0001' - light 4 mask

And to check if the light is enabled we can "(bitwise) AND (&)" the current state of all the lights with the mask (e.g. light 1):

(0000 & 1000)

Then compare that result with the mask to see if that light is enabled:
(0000 & 1000) & 1000

We can see that in code here:

1setIsOn(Boolean((props.value & props.mask)  == props.mask))

For toggling a state for off to on, or on to off. We can use a bitwise exclusive or (^) between the current value and the bitwise mask to change the value around this is what it looks like in code:

1const value = props.value ^ props.mask;
2  setIsOn(Boolean(value == props.mask))

Now we can transmit up to 20 bytes of toggles equaling 20bytes * 8bits = 160 toggles in this one characteristic. Which is quite a few lights to have in an apartment at once!

Now if we want control likes that vary in brightness, we can not pack all their values in to one integer value because the data is not that simple but we can compress them into an array and sending them back and forth using JSON.

It's actually quite simple to setup. We just install this ArduinoJSON library and we can unpack JSON data as the following in our ESP32 code:

1std::string value = readValue(pAllLotCharacteristic, true);
2  JsonDocument doc;
3  DeserializationError error = deserializeJson(doc, value);
4
5  // Test if parsing succeeds.
6  if (error) {
7    Serial.print(F("deserializeJson() failed: "));
8    Serial.println(error.f_str());
9    return;
10  }
11
12  int v1 = doc[0];
13  int v2 = doc[1];
14  int v3 = doc[2];
15  int v4 = doc[3];
16
17  Serial.print("JSON Values: [");
18  Serial.print(v1);
19  Serial.print(",");
20  Serial.print(v2);
21  Serial.print(",");
22  Serial.print(v3);
23  Serial.print(",");
24  Serial.print(v4);
25  Serial.println("]");

And We can send it over the wire from the react Native code using the following:

1const testData = JSON.stringify([
2        adj1,
3        adj2,
4        adj3,
5        adj4,
6      ]);
7      console.log("Sending array of test data...");
8      device
9        .writeCharacteristicWithResponseForService(
10          SERVICE_UUID,
11          ALL_LOT_CHARACTERISTIC_UUID,
12          btoa(testData)
13        )
14        .catch((e) => {});

And that's it! We are now compressing the data down and can send way more data back and forth. To look at the full code check it out here: https://github.com/cjoshmartin/BLE-example-code/commit/65a3cd052de31de91770ff820cc8775d31697ed0


Josh Martin

Full Stack Engineer and Maker

contact@cjoshmartin.com

Hello, I am Josh, I am a full-stack developer specializing in developing for Web and Mobile in React, React Native, Node.js, and Django. I used to work at Amazon in the advertising sales Performance department as a Frontend Engineer...

I have a degree from Purdue University in Computer Engineering with the use of my degree and passions I can offer support more than just Web and Mobile development. I can assist with any need related to hardware integration with internet-enabled devices or design needs in CAD and manufacturing.