Nous allons voir comment créer une application React Native pour Adnroid permettant la communication BLE (Bluetooth Low Energy) avec un ESP32. Nous utilisons React Native pour développer un terminal BLE sur Android permettant la communication avec un NodeMCU ESP32 ou tout autres appareils compatibles.
Matériel
- Un ordinateur avec installation de React Native et Node.js
- Un appareil Android avec BLE
- Un câble USB pour relier l’ordinateur à l’appareil
- Un appareil BLE (ESP32)
Code de gestion du BLE pour ESP32
Pour tester l’application React Native, nous allons utiliser le code de gestion du BLE pour 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 }
Nous rajoutons la fonction BLEServerCallbacks à la gestion du Serveur BLE pour détecter la déconnexion et démarrer l’advertising pour pouvoir reconnecter l’ESP32
pServer->setCallbacks(new MyServerCallbacks());
Application React Native pour la gestion du BLE
Pour gérer la communciation BLE (Bluetooth Low Energy) sur l’appareil Android, nous utilisons la librairie react-native-ble-manager
npm install react-native-ble-manager --save
Pour mettre en place le projet de l’application, suivez le tutoriel précédent.
Dans le fichier App.tsx, pour utiliser la bibliothèque nous l’importons à l’aide de la commande
import BleManager from 'react-native-ble-manager';
Nous créons un composant fonctionnel qui contiendra les éléments nous permettant de gérer la communication BLE
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.: il est possible de créer un composant qui dérive de ReactNative.Components
Gestion des permissions
Pour pouvoir découvrir et se connecter à des appareils Bluetooth, il faut 3 permissions au minimum:
- BLUETOOTH_SCAN
- BLUETOOTH_CONNECT
- ACCESS_FINE_LOCATION
N.B.: ces permissions dépendent de la version et de l’OS utilisé
Voici les balises à ajouter dans le fichier AndroidManifest.xml
<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" />
Dans le fichier App.tsx, nous créons la fonction requestBluetoothPermission()
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'); } }); }
Fonction de gestion du BLE
Les fonctions permettant la gestion du Bluetooth LE sont les suivantes:
- découvrir des appareils bluetooth startDeviceDiscovery() (j’utilise les appareils apairés)
- se connecter à l’appareil connectToDevice()
- se déconnecter disconnectFromDevice()
- envoyer des messages sendMessage()
- lire les messages provenant de la communication readData()
N.B.: Dans cet exemple, nous écrivons et lisons sur la même caractéristique. Nous lisons donc la valeur enregistré avec un appui bouton
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); }); };
La fonction de rendu de l’écran
Pour l’affichage, nous choisissons de tous mettre sur un même écran. Il y aura :
- Un titre
- 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 pour écrire le message à envoyer messageToSend
- Un bouton d’envoi
- Un bouton de lecture
- Une zone de texte pour afficher receivedMessage
- Un bouton de déconnexion
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> );
Résultat
Comme l’appairage n’est pas gérer par l’application, il faut appairer l’ESP32 avant l’utilisation de l’application. Une fois le code chargé sur l’ESP32, vous pouvez lancer l’application sur le téléphone à l’aide de la commande
npx react-native start
Code complet de l’application React Native
/** * 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;