Vamos ver como criar uma aplicação React Native que permite a comunicação Bluetooth entre um dispositivo Android e um ESP32. Usamos o React Native para desenvolver um terminal Bluetooth no Android que se comunica com um ESP32 NodeMCU. O NodeMCU é utilizado para testar a nossa aplicação com um objeto ligado, mas a aplicação pode funcionar com qualquer dispositivo Bluetooth.
Hardware
- Um computador com o React Native e o Node.js instalados
- Um dispositivo Android com Bluetooth
- Um cabo USB para ligar o computador ao dispositivo
- Um dispositivo Bluetooth (ESP32)
Código de gestão Bluetooth para ESP32
Para testar a aplicação React Native, vamos utilizar o código de gestão Bluetooth para o ESP32.
#include "BluetoothSerial.h"
#if !defined(CONFIG_BT_ENABLED) || !defined(CONFIG_BLUEDROID_ENABLED)
#error Bluetooth is not enabled! Please run `make menuconfig` to and enable it
#endif
BluetoothSerial SerialBT;
void callback(esp_spp_cb_event_t event, esp_spp_cb_param_t *param) {
if (event == ESP_SPP_SRV_OPEN_EVT) {
Serial.println("Client Connected");
}
if (event == ESP_SPP_CLOSE_EVT ) {
Serial.println("Client disconnected");
//SerialBT.flush();
//SerialBT.disconnect();
//SerialBT.end();
//SerialBT.begin("ESP32BT");
ESP.restart(); // needed to be able to reconnect
}
}
void setup() {
Serial.begin(115200);
SerialBT.register_callback(callback);
SerialBT.begin("ESP32BT"); //Bluetooth device name
Serial.println("The device started, now you can pair it with bluetooth!");
}
String msg = "";
void loop() {
/*if (Serial.available()) {
SerialBT.write(Serial.read());
}*/
readSerialPort();
//Send data to slave
if (msg != "") {
Serial.println(msg);
SerialBT.println(msg);
msg = "";
}
if (SerialBT.available()) {
Serial.write(SerialBT.read());
}
delay(20);
}
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
}
Adicionamos a função de retorno de chamada à gestão Bluetooth para detetar a desconexão e reiniciar o ESP32
SerialBT.register_callback(callback);
N.B.: é necessário reiniciar o ESP32 para voltar a ligar o Bluetooth porque, obviamente, o BluetoothSocket não se fecha quando se desliga.
Aplicação React Native para gestão de Bluetooth
Para gerir a comunicação Bluetooth (clássica) no dispositivo Android, utilizamos a biblioteca react-native-bluetooth-classic
npm install react-native-bluetooth-classic --save
Para configurar o projeto de aplicação, siga o tutorial anterior.
No ficheiro App.tsx, para utilizar a biblioteca, importamo-la utilizando o comando
import RNBluetoothClassic, {BluetoothDevice,} from 'react-native-bluetooth-classic';
Estamos a criar um componente funcional que conterá os elementos de que necessitamos para gerir a comunicação Bluetooth
const BluetoothClassicTerminal = () => {
const [devices, setDevices] = useState<any[]>([]);
const [paired, setPaired] = useState<any[]>([]);
const [selectedDevice, setSelectedDevice] = useState<BluetoothDevice>();
const [messageToSend, setMessageToSend] = useState("");
const [receivedMessage, setReceivedMessage] = useState("");
const [isConnected, setIsConnected] = useState(false);
const [intervalId, setIntervalId] = useState<NodeJS.Timer>();
Nota: é possível criar um componente derivado de ReactNative.Components
Gestão de autorizações
Para descobrir e estabelecer ligação a dispositivos Bluetooth, são necessárias pelo menos 3 permissões:
- BLUETOOTH_SCAN
- LIGAÇÃO_DE_DISTRIBUIÇÃO
- LOCALIZAÇÃO_DE_ACESSO
N.B.: estas permissões dependem da versão e do sistema operativo utilizado
Eis as etiquetas a adicionar ao ficheiro AndroidManifest.xml
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.BLUETOOTH" /> <uses-permission android:name="android.permission.LIGAÇÃO_DE_DISTRIBUIÇÃO" /> <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.LOCALIZAÇÃO_DE_ACESSO" />
No ficheiro App.tsx, criamos a função requestBluetoothPermission()
async function requestBluetoothPermission(){
try {
const grantedScan = await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN,
{
title: 'Bluetooth Scan Permission',
message: 'This app needs Bluetooth Scan permission to discover devices.',
buttonPositive: 'OK',
buttonNegative: 'Cancel',
}
);
const grantedConnect = await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.LIGAÇÃO_DE_DISTRIBUIÇÃO,
{
title: 'Bluetooth Connect Permission',
message: 'This app needs Bluetooth Connect permission to connect to devices.',
buttonPositive: 'OK',
buttonNegative: 'Cancel',
}
);
const grantedLocation = await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.LOCALIZAÇÃO_DE_ACESSO,
{
title: 'Fine Location Permission',
message: 'This app needs to know location of device.',
buttonPositive: 'OK',
buttonNegative: 'Cancel',
}
);
if (
grantedScan === PermissionsAndroid.RESULTS.GRANTED &&
grantedConnect === PermissionsAndroid.RESULTS.GRANTED &&
grantedLocation === PermissionsAndroid.RESULTS.GRANTED
) {
console.log('Bluetooth permissions granted');
// Vous pouvez maintenant commencer la découverte et la connexion Bluetooth ici.
} else {
console.log('Bluetooth permissions denied');
}
} catch (err) {
console.warn(err);
}
}
Função de gestão Bluetooth
As funções de gestão do Bluetooth são as seguintes
- descobrir dispositivos bluetooth startDeviceDiscovery() (utilizo dispositivos emparelhados)
- ligar ao dispositivo connectToDevice()
- desconectar desconectar()
- enviar mensagens sendMessage()
- ler mensagens da comunicação readData()
N.B.: a documentação da biblioteca menciona a utilização de um ouvinte onDataReceived, que não consegui utilizar. Por isso, introduzi a função readData e um Intervalo para obter os dados.
const checkBluetoothEnabled = async () => {
try {
const enabled = await RNBluetoothClassic.isBluetoothEnabled();
if (!enabled) {
await RNBluetoothClassic.requestBluetoothEnabled();
}
} catch (error) {
console.error('Bluetooth Classic is not available on this device.');
}
}
const startDeviceDiscovery = async () => {
console.log("searching for devices...");
try {
const paired = await RNBluetoothClassic.getBondedDevices();
console.log("Bonded peripherals: " + paired.length);
setPaired(paired);
} catch (error) {
console.error('Error bonded devices:', error);
}
}
const connectToDevice = async (device: BluetoothDevice) => {
try {
console.log("Connecting to device");
let connection = await device.isConnected();
if (!connection) {
console.log("Connecting to device");
await device.connect({
connectorType: "rfcomm",
DELIMITER: "\n",
DEVICE_CHARSET: Platform.OS === "ios" ? 1536 : "utf-8",
});
}
setSelectedDevice(device);
setIsConnected(true);
console.log("is connected : ",isConnected);
//device.onDataReceived((data) => this.readData());
//const intervalId = setInterval(() => {readData();}, 100);
//setIntervalId(intervalId);
} catch (error) {
console.error('Error connecting to device:', error);
}
}
const sendMessage = async () => {
if(selectedDevice && isConnected){
console.log("isConnected in message",isConnected);
try {
await selectedDevice.write(messageToSend);
} catch (error) {
console.error('Error sending message:', error);
}
}
}
const readData = async () => {
if (selectedDevice && isConnected) {
try {
let message = await selectedDevice.read();
if(message){
message = message.trim();
if (message !== "" && message !== " "){
if(receivedMessage.length>300){
setReceivedMessage("");
}
setReceivedMessage(receivedMessage => receivedMessage + message +"\n" );
}
}
} catch (error) {
console.error('Error reading message:', error);
}
}
}
useEffect(() => {
let intervalId: string | number | NodeJS.Timer | undefined;
if (selectedDevice && isConnected) {
intervalId = setInterval(() => readData(), 100);
}
return () => {
clearInterval(intervalId);
};
}, [isConnected,selectedDevice]);
const disconnect = () => {
//need to reset esp32 at disconnect
if(selectedDevice && isConnected){
try {
clearInterval(intervalId);
setIntervalId(undefined);
selectedDevice.clear().then( () => {
console.log("BT buffer cleared");
});
selectedDevice.disconnect().then( () => {
setSelectedDevice(undefined);
setIsConnected(false);
setReceivedMessage("");
console.log("Disconnected from device");
});
} catch (error) {
console.error('Error disconnecting:', error);
}
}
}
A função de renderização do ecrã
Para a apresentação, decidimos colocar tudo no mesmo ecrã. Haverá :
- Um título
- A lista de dispositivos que só aparece se não estiver ligado (!isConnected &&)
- Inserção de um tipo de terminal de comunicação que só aparece se o utilizador estiver ligado (selectedDevice && isConnected &&)
- TextInput para escrever a mensagem a enviar messageToSend
- Um botão de envio
- Uma caixa de texto para apresentar receivedMessage
- Um botão para desligar
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,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}</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={300}
style={{
backgroundColor: '#333333',
margin: 10,
borderRadius: 2,
borderWidth: 1,
borderColor: '#EEEEEE',
textAlignVertical: 'top',
}} >
{receivedMessage}
</TextInput>
</>
)}
</ScrollView>
</View>
);
Resultados
Como o emparelhamento não é gerido pela aplicação, o ESP32 tem de estar emparelhado antes de utilizar a aplicação. Uma vez carregado o código no ESP32, é possível lançar a aplicação no telemóvel utilizando o comando
npx react-native start



Código completo para a aplicação React Native
/**
* https://kenjdavidson.com/react-native-bluetooth-classic/
*/
import React, {useState, useEffect} from 'react';
import {
StyleSheet,
Dimensions,
View, ScrollView, Text,
Button, TextInput,PermissionsAndroid,Platform, TouchableOpacity } from 'react-native';
import RNBluetoothClassic, {BluetoothDevice,} from 'react-native-bluetooth-classic';
const BluetoothClassicTerminal = () => {
const [devices, setDevices] = useState<any[]>([]);
const [paired, setPaired] = useState<any[]>([]);
const [selectedDevice, setSelectedDevice] = useState<BluetoothDevice>();
const [messageToSend, setMessageToSend] = useState("");
const [receivedMessage, setReceivedMessage] = useState("");
const [isConnected, setIsConnected] = useState(false);
const [intervalId, setIntervalId] = useState<NodeJS.Timer>();
/*const [state, setState] = useState({
devices: [],
paired: [],
selectedDevice: null,
messageToSend: "",
receivedMessage: "",
isConnected: false,
intervalId: null,
})*/
const checkBluetoothEnabled = async () => {
try {
const enabled = await RNBluetoothClassic.isBluetoothEnabled();
if (!enabled) {
await RNBluetoothClassic.requestBluetoothEnabled();
}
} catch (error) {
console.error('Bluetooth Classic is not available on this device.');
}
}
const startDeviceDiscovery = async () => {
console.log("searching for devices...");
try {
const paired = await RNBluetoothClassic.getBondedDevices();
console.log("Bonded peripherals: " + paired.length);
setPaired(paired);
} catch (error) {
console.error('Error bonded devices:', error);
}
/*try {
const devices = await RNBluetoothClassic.startDiscovery();
this.setState({ devices });
console.log("Discovered peripherals: " + devices.length);
} catch (error) {
console.error('Error discovering devices:', error);
}*/
}
const connectToDevice = async (device: BluetoothDevice) => {
try {
console.log("Connecting to device");
let connection = await device.isConnected();
if (!connection) {
console.log("Connecting to device");
await device.connect({
connectorType: "rfcomm",
DELIMITER: "\n",
DEVICE_CHARSET: Platform.OS === "ios" ? 1536 : "utf-8",
});
}
setSelectedDevice(device);
setIsConnected(true);
console.log("is connected : ",isConnected);
//device.onDataReceived((data) => this.readData());
//const intervalId = setInterval(() => {readData();}, 100);
//setIntervalId(intervalId);
} catch (error) {
console.error('Error connecting to device:', error);
}
}
/*async onReceivedData() {
const { selectedDevice, receivedMessage } = this.state;
//console.log("event : recived message", event);
try{
//const message = await selectedDevice.read();
console.log("reieved msg from", selectedDevice.name);
const messages = await selectedDevice.available();
if (messages.length > 0) {
console.log("msg waiting : ", messages.length);
}
//this.setState({ receivedMessage: message.data });
} catch (error) {
console.error('Error receiving data:', error);
}
}*/
const sendMessage = async () => {
if(selectedDevice && isConnected){
console.log("isConnected in message",isConnected);
try {
await selectedDevice.write(messageToSend);
} catch (error) {
console.error('Error sending message:', error);
}
}
}
/*const readData = async () => {
console.log("reading data connected", isConnected);
if(selectedDevice && isConnected){
try {
console.log("reading data from", selectedDevice.name);
//const available = await selectedDevice.available();
//if (available>1){
let message = await selectedDevice.read();
if(message){
message = message.trim();
if (message !== "" && message !== " "){
console.log("reading data from", selectedDevice.name);
//console.log(" available : ", available);
//console.log("available", selectedDevice.available());
//console.log("read", selectedDevice.read());
setReceivedMessage(receivedMessage + message +"\n" );
console.log('message', message);
console.log('message', receivedMessage);
}
}
// }
} catch (error) {
//console.log("isConnected",isConnected);
//console.log("selectedDevice",selectedDevice);
console.error('Error reading message:', error);
}
}
}*/
const readData = async () => {
if (selectedDevice && isConnected) {
try {
//const available = await selectedDevice.available();
//if (available>1){
let message = await selectedDevice.read();
if(message){
message = message.trim();
if (message !== "" && message !== " "){
if(receivedMessage.length>300){
setReceivedMessage("");
}
setReceivedMessage(receivedMessage => receivedMessage + message +"\n" );
}
}
// }
} catch (error) {
//console.log("isConnected",isConnected);
//console.log("selectedDevice",selectedDevice);
console.error('Error reading message:', error);
}
}
}
useEffect(() => {
let intervalId: string | number | NodeJS.Timer | undefined;
if (selectedDevice && isConnected) {
intervalId = setInterval(() => readData(), 100);
}
return () => {
clearInterval(intervalId);
};
}, [isConnected,selectedDevice]);
const disconnect = () => {
//need to reset esp32 at disconnect
if(selectedDevice && isConnected){
try {
clearInterval(intervalId);
setIntervalId(undefined);
selectedDevice.clear().then( () => {
console.log("BT buffer cleared");
});
selectedDevice.disconnect().then( () => {
setSelectedDevice(undefined);
setIsConnected(false);
setReceivedMessage("");
console.log("Disconnected from device");
});
/*RNBluetoothClassic.unpairDevice(uuid).then( () => {
console.log("Unpaired from device");
});
RNBluetoothClassic.pairDevice(uuid).then( () => {
console.log("paired from device");
});*/
} catch (error) {
console.error('Error disconnecting:', error);
}
}
}
useEffect(() => {
async function requestBluetoothPermission(){
try {
const grantedScan = await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN,
{
title: 'Bluetooth Scan Permission',
message: 'This app needs Bluetooth Scan permission to discover devices.',
buttonPositive: 'OK',
buttonNegative: 'Cancel',
}
);
const grantedConnect = await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.LIGAÇÃO_DE_DISTRIBUIÇÃO,
{
title: 'Bluetooth Connect Permission',
message: 'This app needs Bluetooth Connect permission to connect to devices.',
buttonPositive: 'OK',
buttonNegative: 'Cancel',
}
);
const grantedLocation = await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.LOCALIZAÇÃO_DE_ACESSO,
{
title: 'Fine Location Permission',
message: 'This app needs to know location of device.',
buttonPositive: 'OK',
buttonNegative: 'Cancel',
}
);
if (
grantedScan === PermissionsAndroid.RESULTS.GRANTED &&
grantedConnect === PermissionsAndroid.RESULTS.GRANTED &&
grantedLocation === PermissionsAndroid.RESULTS.GRANTED
) {
console.log('Bluetooth permissions granted');
// Vous pouvez maintenant commencer la découverte et la connexion Bluetooth ici.
} else {
console.log('Bluetooth permissions denied');
}
} catch (err) {
console.warn(err);
}
}
checkBluetoothEnabled();
requestBluetoothPermission().then( () => {
startDeviceDiscovery();
});
}, [])
return (
<View>
<Text
style={{
fontSize: 30,
textAlign: 'center',
borderBottomWidth: 1,
}}>
AC Bluetooth 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: BluetoothDevice,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}</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={300}
style={{
backgroundColor: '#333333',
margin: 10,
borderRadius: 2,
borderWidth: 1,
borderColor: '#EEEEEE',
textAlignVertical: 'top',
}} >
{receivedMessage}
</TextInput>
</>
)}
</ScrollView>
</View>
);
};//end of component
const windowHeight = Dimensions.get('window').height;
const styles = StyleSheet.create({
mainBody: {
flex: 1,
justifyContent: 'center',
height: windowHeight,
},
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 BluetoothClassicTerminal;