We’re going to look at how to create a React Native application for Adnroid that enables BLE (Bluetooth Low Energy) communication with an ESP32. We’ll be using React Native to develop a BLE terminal on Android for communication with an ESP32 NodeMCU or other compatible devices.
Hardware
- A computer with React Native and Node.js installed
- An Android device with BLE
- A USB cable to connect the computer to the device
- A BLE device (ESP32)
BLE management code for ESP32
To test the React Native application, we’re going to use the BLE management code for ESP32.
/* Based on Neil Kolban example for IDF: https://github.com/nkolban/esp32-snippets/blob/master/cpp_utils/tests/BLE%20Tests/SampleWrite.cpp Ported to Arduino ESP32 by Evandro Copercini */ #include <BLEDevice.h> #include <BLEUtils.h> #include <BLEServer.h> // See the following for generating UUIDs: // https://www.uuidgenerator.net/ #define SERVICE_UUID "4fafc201-1fb5-459e-8fcc-c5c9c331914b" #define CHARACTERISTIC_UUID "beb5483e-36e1-4688-b7f5-ea07361b26a8" BLECharacteristic *pCharacteristic = NULL; std::string msg; class MyCallbacks: public BLECharacteristicCallbacks { void onWrite(BLECharacteristic *pCharacteristic) { std::string value = pCharacteristic->getValue(); if (value.length() > 0) { Serial.println("*********"); Serial.print("New value: "); for (int i = 0; i < value.length(); i++) Serial.print(value[i]); Serial.println(); Serial.println("*********"); } } }; class MyServerCallbacks: public BLEServerCallbacks { void onConnect(BLEServer* pServer) { Serial.println("Client connected"); } void onDisconnect(BLEServer* pServer) { Serial.println("Client disconnected"); BLEDevice::startAdvertising(); // needed for reconnection } }; void setup() { Serial.begin(115200); Serial.println("1- Download and install an BLE Terminal Free"); Serial.println("2- Scan for BLE devices in the app"); Serial.println("3- Connect to ESP32BLE"); Serial.println("4- Go to CUSTOM CHARACTERISTIC in CUSTOM SERVICE and write something"); BLEDevice::init("ESP32BLE"); BLEServer *pServer = BLEDevice::createServer(); pServer->setCallbacks(new MyServerCallbacks()); BLEService *pService = pServer->createService(SERVICE_UUID); pCharacteristic = pService->createCharacteristic( CHARACTERISTIC_UUID, BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_WRITE ); pCharacteristic->setCallbacks(new MyCallbacks()); pCharacteristic->setValue("Hello World"); pService->start(); BLEAdvertising *pAdvertising = pServer->getAdvertising(); pAdvertising->addServiceUUID(SERVICE_UUID); pAdvertising->setScanResponse(true); pAdvertising->setMinPreferred(0x06); // functions that help with iPhone connections issue pAdvertising->setMinPreferred(0x12); pAdvertising->start(); Serial.print("Server address:"); Serial.println(BLEDevice::getAddress().toString().c_str()); } void loop() { readSerialPort(); //Send data to slave if(msg!=""){ pCharacteristic->setValue(msg); msg=""; } delay(2000); } void readSerialPort(){ while (Serial.available()) { delay(10); if (Serial.available() >0) { char c = Serial.read(); //gets one byte from serial buffer msg += c; //add to String } } Serial.flush(); //clean buffer }
We add the BLEServerCallbacks function to the BLE Server management to detect the disconnection and start advertising to reconnect the ESP32.
pServer->setCallbacks(new MyServerCallbacks());
React Native application for BLE management
To manage BLE (Bluetooth Low Energy) communication on the Android device, we use the react-native-ble-manager library
npm install react-native-ble-manager --save
To set up the application project, follow the previous tutorial.
In the App.tsx file, to use the library we import it using the command
import BleManager from 'react-native-ble-manager';
We create a functional component that will contain the elements needed to manage BLE communication.
let serviceid="4fafc201-1fb5-459e-8fcc-c5c9c331914b"; let caracid="beb5483e-36e1-4688-b7f5-ea07361b26a8"; const BluetoothBLETerminal = () => { const [devices, setDevices] = useState<any[]>([]); const [paired, setPaired] = useState<any[]>([]); const [selectedDevice, setSelectedDevice] = useState<Peripheral>(); const [messageToSend, setMessageToSend] = useState(""); const [receivedMessage, setReceivedMessage] = useState(""); const [isConnected, setIsConnected] = useState(false); const [intervalId, setIntervalId] = useState<NodeJS.Timer>(); const [isScanning, setIsScanning] = useState(false);
N.B.: it is possible to create a component derived from ReactNative.Components
Permission management
To discover and connect to Bluetooth devices, you need at least 3 permissions:
- BLUETOOTH_SCAN
- BLUETOOTH_CONNECT
- ACCESS_FINE_LOCATION
N.B.: these permissions depend on the version and OS used.
Here are the tags to add to the AndroidManifest.xml file
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.BLUETOOTH" /> <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" /> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" /> <uses-permission android:name="android.permission.BLUETOOTH_SCAN" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
In the App.tsx file, we create the requestBluetoothPermission() function
if (Platform.OS === 'android' && Platform.Version >= 23) { PermissionsAndroid.requestMultiple([ PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN, PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT, PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION, ]).then(result => { if ( (result['android.permission.BLUETOOTH_SCAN'] && result['android.permission.BLUETOOTH_CONNECT'] && result['android.permission.ACCESS_FINE_LOCATION'] === 'granted') || (result['android.permission.BLUETOOTH_SCAN'] && result['android.permission.BLUETOOTH_CONNECT'] && result['android.permission.ACCESS_FINE_LOCATION'] === 'never_ask_again') ) { console.log('User accepted'); } else { console.log('User refused'); } }); }
BLE management function
Bluetooth LE management functions are as follows:
- discover bluetooth devices startDeviceDiscovery() (I use paired devices)
- connect to device connectToDevice()
- disconnectFromDevice()
- send messages sendMessage()
- read messages from the readData() communication
N.B.: In this example, we write to and read from the same characteristic. We therefore read the value recorded by pressing the
const checkBluetoothEnabled = async () => { try { // turn on bluetooth if it is not on BleManager.enableBluetooth().then(() => { console.log('Bluetooth is turned on!'); }); } catch (error) { console.error('BLE is not available on this device.'); } } const startScan = () => { if (!isScanning) { BleManager.scan([], 5, true) .then(() => { console.log('Scanning...'); setIsScanning(true); }) .catch(error => { console.error(error); }); } }; const startDeviceDiscovery = async () => { BleManager.getBondedPeripherals().then((bondedPeripheralsArray) => { // Each peripheral in returned array will have id and name properties console.log("Bonded peripherals: " + bondedPeripheralsArray.length); setPaired(bondedPeripheralsArray); }); /*BleManager.getDiscoveredPeripherals().then((peripheralsArray) => { // Success code console.log("Discovered peripherals: " + peripheralsArray.length); });*/ } const connectToDevice = async (device: Peripheral) => { BleManager.connect(device.id) .then(() => { // Success code console.log("Connected"); setSelectedDevice(device); setIsConnected(true); BleManager.retrieveServices(device.id).then( (deviceInfo) => { // Success code console.log("Device info:", deviceInfo); } ); }) .catch((error) => { // Failure code console.log(error); }); } const sendMessage = async () => { if(selectedDevice && isConnected){ try { const buffer = Buffer.from(messageToSend); BleManager.write( selectedDevice.id, serviceid, caracid, buffer.toJSON().data ).then(() => { // Success code console.log("Write: " + buffer.toJSON().data); }) .catch((error) => { // Failure code console.log(error); }); } catch (error) { console.error('Error sending message:', error); } } } const readData = async () => { if (selectedDevice && isConnected) { BleManager.read( selectedDevice.id, serviceid, caracid ) .then((readData) => { // Success code console.log("Read: " + readData); const message = Buffer.from(readData); //const sensorData = buffer.readUInt8(1, true); if(receivedMessage.length>300){ setReceivedMessage(""); //reset received message if length higher than 300 } setReceivedMessage(receivedMessage => receivedMessage + message +"\n" ); console.log("receivedMessage length",receivedMessage.length) }) .catch((error) => { // Failure code console.log("Error reading message:",error); }); } } // disconnect from device const disconnectFromDevice = (device: Peripheral) => { BleManager.disconnect(device.id) .then(() => { setSelectedDevice(undefined); setIsConnected(false); setReceivedMessage(""); clearInterval(intervalId); console.log("Disconnected from device"); }) .catch((error) => { // Failure code console.log("Error disconnecting:",error); }); };
The screen rendering function
For the display, we choose to put everything on the same screen. There will be :
- A title
- La liste des appareils qui n’apparait que si on n’est pas connecté (!isConnected &&)
- Un encart type terminal de communication qui n’apparait que si on est connecté (selectedDevice && isConnected &&)
- TextInput to write the message to be sent messageToSend
- A send button
- A play button
- A text box to display receivedMessage
- A disconnect button
return ( <View> <Text style={{ fontSize: 30, textAlign: 'center', borderBottomWidth: 1, }}> AC Bluetooth Terminal </Text> <ScrollView> {!isConnected && ( <> {/* <Text>Available Devices:</Text> {devices.map((device) => ( <Button key={device.id} title={device.name || 'Unnamed Device'} onPress={() => this.connectToDevice(device)} /> ))} */} <Text>Paired Devices:</Text> {paired.map((pair: BluetoothDevice) => ( <View style={{ flexDirection: 'row', justifyContent: 'space-between', marginBottom: 2, }}> <View style={styles.deviceItem}> <Text style={styles.deviceName}>{pair.name}</Text> <Text style={styles.deviceInfo}>{pair.id}</Text> </View> <TouchableOpacity onPress={() => isConnected ? disconnect() : connectToDevice(pair) } style={styles.deviceButton}> <Text style={[ styles.scanButtonText, {fontWeight: 'bold', fontSize: 12}, ]}> {isConnected ? 'Disconnect' : 'Connect'} </Text> </TouchableOpacity> </View> ))} </> )} {selectedDevice && isConnected && ( <> <View style={{ flexDirection: 'row', justifyContent: 'space-between', margin: 5, }}> <View style={styles.deviceItem}> <Text style={styles.deviceName}>{selectedDevice.name}</Text> <Text style={styles.deviceInfo}>{selectedDevice.id}</Text> </View> <TouchableOpacity onPress={() => isConnected ? disconnect() : connectToDevice(selectedDevice) } style={styles.deviceButton}> <Text style={styles.scanButtonText}> {isConnected ? 'Disconnect' : 'Connect'} </Text> </TouchableOpacity> </View> <View style={{ flexDirection: 'row', justifyContent: 'space-between', margin: 5, }}> <TextInput style={{ backgroundColor: '#888888', margin: 2, borderRadius: 15, flex:3, }} placeholder="Enter a message" value={messageToSend} onChangeText={(text) => setMessageToSend(text )} /> <TouchableOpacity onPress={() => sendMessage() } style={[styles.sendButton]}> <Text style={[ styles.scanButtonText, ]}> SEND </Text> </TouchableOpacity> </View> <Text>Received Message:</Text> <TextInput editable = {false} multiline numberOfLines={20} maxLength={100} style={{ backgroundColor: '#333333', margin: 10, borderRadius: 2, borderWidth: 1, borderColor: '#EEEEEE', textAlignVertical: 'top', }} > {receivedMessage} </TextInput> </> )} </ScrollView> </View> );
Results
As pairing is not handled by the application, the ESP32 must be paired before using the application. Once the code has been loaded onto the ESP32, you can launch the application on the phone using the command
npx react-native start
Complete code for the React Native application
/** * Sample React Native App * https://github.com/facebook/react-native * * @format * https://github.com/innoveit/react-native-ble-manager * https://blog.logrocket.com/using-react-native-ble-manager-mobile-app/ */ import React, {useState, useEffect} from 'react'; import { StyleSheet, Dimensions, View, ScrollView, Text, TextInput, PermissionsAndroid, TouchableOpacity, Platform} from 'react-native'; import BleManager,{Peripheral} from 'react-native-ble-manager'; import { Buffer } from 'buffer'; let serviceid="4fafc201-1fb5-459e-8fcc-c5c9c331914b"; let caracid="beb5483e-36e1-4688-b7f5-ea07361b26a8"; const BluetoothBLETerminal = () => { const [devices, setDevices] = useState<any[]>([]); const [paired, setPaired] = useState<any[]>([]); const [selectedDevice, setSelectedDevice] = useState<Peripheral>(); const [messageToSend, setMessageToSend] = useState(""); const [receivedMessage, setReceivedMessage] = useState(""); const [isConnected, setIsConnected] = useState(false); const [intervalId, setIntervalId] = useState<NodeJS.Timer>(); const [isScanning, setIsScanning] = useState(false); const checkBluetoothEnabled = async () => { try { // turn on bluetooth if it is not on BleManager.enableBluetooth().then(() => { console.log('Bluetooth is turned on!'); }); } catch (error) { console.error('BLE is not available on this device.'); } } const startScan = () => { if (!isScanning) { BleManager.scan([], 5, true) .then(() => { console.log('Scanning...'); setIsScanning(true); }) .catch(error => { console.error(error); }); } }; const startDeviceDiscovery = async () => { BleManager.getBondedPeripherals().then((bondedPeripheralsArray) => { // Each peripheral in returned array will have id and name properties console.log("Bonded peripherals: " + bondedPeripheralsArray.length); setPaired(bondedPeripheralsArray); }); /*BleManager.getDiscoveredPeripherals().then((peripheralsArray) => { // Success code console.log("Discovered peripherals: " + peripheralsArray.length); });*/ } const connectToDevice = async (device: Peripheral) => { BleManager.connect(device.id) .then(() => { // Success code console.log("Connected"); setSelectedDevice(device); setIsConnected(true); BleManager.retrieveServices(device.id).then( (deviceInfo) => { // Success code console.log("Device info:", deviceInfo); } ); }) .catch((error) => { // Failure code console.log(error); }); } const sendMessage = async () => { if(selectedDevice && isConnected){ try { const buffer = Buffer.from(messageToSend); BleManager.write( selectedDevice.id, serviceid, caracid, buffer.toJSON().data ).then(() => { // Success code console.log("Write: " + buffer.toJSON().data); }) .catch((error) => { // Failure code console.log(error); }); } catch (error) { console.error('Error sending message:', error); } } } const readData = async () => { if (selectedDevice && isConnected) { BleManager.read( selectedDevice.id, serviceid, caracid ) .then((readData) => { // Success code console.log("Read: " + readData); const message = Buffer.from(readData); //const sensorData = buffer.readUInt8(1, true); if(receivedMessage.length>300){ setReceivedMessage(""); } setReceivedMessage(receivedMessage => receivedMessage + message +"\n" ); console.log("receivedMessage length",receivedMessage.length) }) .catch((error) => { // Failure code console.log("Error reading message:",error); }); } } /*useEffect(() => { let intervalId: string | number | NodeJS.Timer | undefined; if (selectedDevice && isConnected) { intervalId = setInterval(() => readData(), 100); setIntervalId(intervalId); } return () => { clearInterval(intervalId); }; }, [isConnected,selectedDevice]);*/ // disconnect from device const disconnectFromDevice = (device: Peripheral) => { BleManager.disconnect(device.id) .then(() => { setSelectedDevice(undefined); setIsConnected(false); setReceivedMessage(""); clearInterval(intervalId); console.log("Disconnected from device"); }) .catch((error) => { // Failure code console.log("Error disconnecting:",error); }); /*BleManager.removeBond(peripheral.id) .then(() => { peripheral.connected = false; peripherals.set(peripheral.id, peripheral); setConnectedDevices(Array.from(peripherals.values())); setDiscoveredDevices(Array.from(peripherals.values())); Alert.alert(`Disconnected from ${peripheral.name}`); }) .catch(() => { console.log('fail to remove the bond'); });*/ }; useEffect(() => { checkBluetoothEnabled(); if (Platform.OS === 'android' && Platform.Version >= 23) { PermissionsAndroid.requestMultiple([ PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN, PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT, PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION, ]).then(result => { if ( (result['android.permission.BLUETOOTH_SCAN'] && result['android.permission.BLUETOOTH_CONNECT'] && result['android.permission.ACCESS_FINE_LOCATION'] === 'granted') || (result['android.permission.BLUETOOTH_SCAN'] && result['android.permission.BLUETOOTH_CONNECT'] && result['android.permission.ACCESS_FINE_LOCATION'] === 'never_ask_again') ) { console.log('User accepted'); } else { console.log('User refused'); } }); } BleManager.start({showAlert: false}).then(() => { console.log('BleManager initialized'); startDeviceDiscovery(); }).catch((error) => { // Failure code console.log("Error requesting permission:",error); }); BleManager.checkState().then((state) => console.log(`current BLE state = '${state}'.`) ); BleManager.getConnectedPeripherals([]).then((peripheralsArray) => { // Success code console.log("Connected peripherals: " + peripheralsArray.length); }); BleManager.getBondedPeripherals().then((bondedPeripheralsArray) => { // Each peripheral in returned array will have id and name properties console.log("Bonded peripherals: " + bondedPeripheralsArray.length); //setBoundedDevices(bondedPeripheralsArray); }); BleManager.getDiscoveredPeripherals().then((peripheralsArray) => { // Success code console.log("Discovered peripherals: " + peripheralsArray.length); }); /*let stopDiscoverListener = BleManagerEmitter.addListener( 'BleManagerDiscoverPeripheral', peripheral => { peripherals.set(peripheral.id, peripheral); }, );*/ /*let stopConnectListener = BleManagerEmitter.addListener( 'BleManagerConnectPeripheral', peripheral => { console.log('BleManagerConnectPeripheral:', peripheral); peripherals.set(peripheral.id, peripheral); setConnectedDevices(Array.from(peripherals.values())); }, );*/ /*let stopScanListener = BleManagerEmitter.addListener( 'BleManagerStopScan', () => { setIsScanning(false); console.log('scan stopped'); BleManager.getDiscoveredPeripherals().then((peripheralsArray) => { // Success code console.log("Discovered peripherals: " + peripheralsArray.length); for (let i = 0; i < peripheralsArray.length; i++) { let peripheral = peripheralsArray[i]; console.log("item:", peripheral); //peripheral.connected = true; peripherals.set(peripheral.id, peripheral); setDiscoveredDevices(peripheralsArray); } }); }, );*/ return () => { /*stopDiscoverListener.remove(); stopConnectListener.remove(); stopScanListener.remove();*/ }; }, []) return ( <View style={[styles.mainBody]}> <Text style={{ fontSize: 30, textAlign: 'center', borderBottomWidth: 1, }}> AC BLE Terminal </Text> <ScrollView> {!isConnected && ( <> <TouchableOpacity onPress={() => startDeviceDiscovery() } style={[styles.deviceButton]}> <Text style={[ styles.scanButtonText, ]}> SCAN </Text> </TouchableOpacity> {/* <Text>Available Devices:</Text> {devices.map((device) => ( <Button key={device.id} title={device.name || 'Unnamed Device'} onPress={() => this.connectToDevice(device)} /> ))} */} <Text>Paired Devices:</Text> {paired.map((pair,i) => ( <View key={i} style={{ flexDirection: 'row', justifyContent: 'space-between', marginBottom: 2, }}> <View style={styles.deviceItem}> <Text style={styles.deviceName}>{pair.name}</Text> <Text style={styles.deviceInfo}>{pair.id}, rssi: {pair.rssi}</Text> </View> <TouchableOpacity onPress={() => isConnected ? disconnectFromDevice(pair) : connectToDevice(pair) } style={styles.deviceButton}> <Text style={[ styles.scanButtonText, {fontWeight: 'bold', fontSize: 12}, ]}> {isConnected ? 'Disconnect' : 'Connect'} </Text> </TouchableOpacity> </View> ))} </> )} {selectedDevice && isConnected && ( <> <View style={{ flexDirection: 'row', justifyContent: 'space-between', margin: 5, }}> <View style={styles.deviceItem}> <Text style={styles.deviceName}>{selectedDevice.name}</Text> <Text style={styles.deviceInfo}>{selectedDevice.id}, rssi: {selectedDevice.rssi}</Text> </View> <TouchableOpacity onPress={() => isConnected ? disconnectFromDevice(selectedDevice) : connectToDevice(selectedDevice) } style={styles.deviceButton}> <Text style={styles.scanButtonText}> {isConnected ? 'Disconnect' : 'Connect'} </Text> </TouchableOpacity> </View> <View style={{ flexDirection: 'row', justifyContent: 'space-between', margin: 5, }}> <TextInput style={{ backgroundColor: '#888888', margin: 2, borderRadius: 15, flex:3, }} placeholder="Enter a message" value={messageToSend} onChangeText={(text) => setMessageToSend(text)} /> <TouchableOpacity onPress={() => sendMessage() } style={[styles.sendButton]}> <Text style={[ styles.scanButtonText, ]}> SEND </Text> </TouchableOpacity> </View> <View style={{ flexDirection: 'row', justifyContent: 'space-between', margin: 5, }}> <Text style={{textAlignVertical: 'center'}}>Received Message:</Text> <TouchableOpacity onPress={() => readData() } style={[styles.deviceButton]}> <Text style={[ styles.scanButtonText, ]}> READ </Text> </TouchableOpacity> </View> <TextInput editable = {false} multiline numberOfLines={20} maxLength={300} style={{ backgroundColor: '#333333', margin: 10, borderRadius: 2, borderWidth: 1, borderColor: '#EEEEEE', textAlignVertical: 'top', }} > {receivedMessage} </TextInput> </> )} </ScrollView> </View> ); };//end of component //https://medium.com/supercharges-mobile-product-guide/reactive-styles-in-react-native-79a41fbdc404 export const theme = { smallPhone: 0, phone: 290, tablet: 750, } const windowHeight = Dimensions.get('window').height; const styles = StyleSheet.create({ mainBody: { flex: 1, justifyContent: 'center', height: windowHeight, ...Platform.select ({ ios: { fontFamily: "Arial", }, android: { fontFamily: "Roboto", }, }), }, scanButtonText: { color: 'white', fontWeight: 'bold', fontSize: 12, textAlign: 'center', }, noDevicesText: { textAlign: 'center', marginTop: 10, fontStyle: 'italic', }, deviceItem: { marginBottom: 2, }, deviceName: { fontSize: 14, fontWeight: 'bold', }, deviceInfo: { fontSize: 8, }, deviceButton: { backgroundColor: '#2196F3', padding: 10, borderRadius: 10, margin: 2, paddingHorizontal: 20, }, sendButton: { backgroundColor: '#2196F3', padding: 15, borderRadius: 15, margin: 2, paddingHorizontal: 20, }, }); export default BluetoothBLETerminal;