Aprende a establecer una conexión BLE (Bluetooth Low Energy) entre dos placas ESP32. Una de ellas hará de servidor y la otra de cliente. El servidor BLE anuncia características que contienen lecturas de sensores que el cliente puede leer. El cliente BLE del ESP32 lee los valores de esas características (temperatura y humedad) y los muestra en una pantalla OLED.
Lectura recomendada: si es tu primer contacto con BLE, te recomendamos leer antes la Guía de introducción a Bluetooth Low Energy en el ESP32.
¿Qué es Bluetooth Low Energy?
Antes de ir directos al proyecto, es importante repasar algunos conceptos esenciales de BLE para que puedas entender mejor el proyecto después. Si ya estás familiarizado con BLE, puedes saltar directamente a la sección de Resumen del proyecto.
Bluetooth Low Energy, o BLE para abreviar, es una variante de bajo consumo del Bluetooth clásico. La aplicación principal de BLE es la transmisión de corta distancia de pequeñas cantidades de datos (ancho de banda bajo). A diferencia del Bluetooth clásico, que está siempre encendido, BLE permanece en modo sleep constantemente excepto cuando se inicia una conexión.
Esto hace que consuma muy poca energía. BLE consume aproximadamente 100 veces menos energía que el Bluetooth clásico (dependiendo del caso de uso).
Servidor y cliente BLE
Con Bluetooth Low Energy, existen dos tipos de dispositivos: el servidor y el cliente. El ESP32 puede actuar tanto como cliente como servidor.
El servidor anuncia su existencia, para que pueda ser encontrado por otros dispositivos, y contiene los datos que el cliente puede leer. El cliente escanea los dispositivos cercanos, y cuando encuentra el servidor que busca, establece una conexión y escucha los datos entrantes. Esto se llama comunicación punto a punto.
Existen otros modos de comunicación posibles, como el modo broadcast y la red mesh (no cubiertos en este tutorial).
GATT: la estructura jerárquica de datos
GATT significa Generic Attributes (Atributos Genéricos) y define una estructura de datos jerárquica que se expone a los dispositivos BLE conectados. Esto significa que GATT define la forma en que dos dispositivos BLE envían y reciben mensajes estándar. Entender esta jerarquía es importante porque te hará más fácil entender cómo usar BLE con el ESP32.
La jerarquía GATT se compone de estos niveles:
Profile (Perfil): colección estándar de servicios para un caso de uso específico. Service (Servicio): colección de información relacionada, como lecturas de sensores, nivel de batería, ritmo cardíaco, etc. Characteristic (Característica): es donde los datos reales se guardan en la jerarquía (el valor). Descriptor (Descriptor): metadatos sobre los datos. Properties (Propiedades): describen cómo se puede interactuar con el valor de la característica. Por ejemplo: read, write, notify, broadcast, indicate, etc.
En nuestro ejemplo, crearemos un servicio con dos características: una para la temperatura y otra para la humedad. Las lecturas reales de temperatura y humedad se guardan en el valor bajo sus características. Cada característica tiene la propiedad notify, para que notifique al cliente cada vez que los valores cambien.
UUID: el identificador único
Cada servicio, característica y descriptor tiene un UUID (Universally Unique Identifier). Un UUID es un número único de 128 bits (16 bytes). Por ejemplo:
55072829-bc9e-4c53-938a-74a6d4c78776
Existen UUIDs abreviados para todos los tipos, servicios y perfiles especificados en el SIG (Bluetooth Special Interest Group). Pero si tu aplicación necesita su propio UUID, puedes generarlo usando un generador de UUID online.
En resumen, el UUID se usa para identificar información de forma única. Por ejemplo, puede identificar un servicio particular proporcionado por un dispositivo Bluetooth.
Resumen del proyecto
En este tutorial vas a aprender cómo hacer una conexión BLE entre dos placas ESP32. Una ESP32 será el servidor BLE y la otra será el cliente BLE.
El servidor BLE del ESP32 está conectado a un sensor BME280 y actualiza sus valores de temperatura y humedad cada 30 segundos. El cliente BLE del ESP32 se conecta al servidor BLE y recibe notificaciones de sus valores de temperatura y humedad. Este ESP32 está conectado a una pantalla OLED y muestra las últimas lecturas.
Este proyecto está dividido en dos partes: Parte 1 – Servidor BLE del ESP32 y Parte 2 – Cliente BLE del ESP32.
Componentes necesarios
Para el servidor BLE del ESP32: una placa ESP32 DOIT DEVKIT V1, un sensor BME280, cables jumper, protoboard y un smartphone con Bluetooth (opcional).
Para el cliente BLE del ESP32: una placa ESP32 DOIT DEVKIT V1, una pantalla OLED, cables jumper y protoboard.
Parte 1: Servidor BLE con el ESP32
En esta parte, configuraremos el servidor BLE que anuncia un servicio que contiene dos características: una para la temperatura y otra para la humedad. Esas características tienen la propiedad Notify para notificar nuevos valores al cliente.
Esquema de conexión
El servidor BLE del ESP32 anunciará características con temperatura y humedad procedentes de un sensor BME280. Puedes usar cualquier otro sensor siempre que añadas las líneas necesarias en el código.
Vamos a usar comunicación I2C con el módulo sensor BME280. Para ello, conecta el sensor a los pines SCL (GPIO 22) y SDA (GPIO 21) por defecto del ESP32.
Instalar las librerías del BME280
Como mencionamos antes, anunciaremos lecturas de un sensor BME280. Así que necesitas instalar las librerías para interactuar con el sensor BME280: la librería Adafruit_BME280 y la librería Adafruit_Sensor.
Puedes instalar las librerías usando el Gestor de Librerías de Arduino. Ve a Programa > Incluir Librería > Gestionar Librerías y busca el nombre de la librería.
Nota para VS Code + PlatformIO: si estás usando VS Code con la extensión PlatformIO, copia las siguientes dependencias en el archivo
platformio.ini:lib_deps = adafruit/Adafruit Unified Sensor @ ^1.1.4 adafruit/Adafruit BME280 Library @ ^2.1.2
Código del servidor BLE del ESP32
Con el circuito listo y las librerías necesarias instaladas, copia el siguiente código en Arduino IDE, o en el archivo main.cpp si usas VS Code:
/*
Rui Santos
Instrucciones completas en https://RandomNerdTutorials.com/esp32-ble-server-client/
*/
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>
#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>
// Por defecto, la temperatura está en Celsius
// Comenta la siguiente línea para temperatura en Fahrenheit
#define temperatureCelsius
// Nombre del servidor BLE
#define bleServerName "BME280_ESP32"
Adafruit_BME280 bme; // I2C
float temp;
float tempF;
float hum;
// Variables del temporizador
unsigned long lastTime = 0;
unsigned long timerDelay = 30000;
bool deviceConnected = false;
// Consulta lo siguiente para generar UUIDs:
// https://www.uuidgenerator.net/
#define SERVICE_UUID "91bad492-b950-4226-aa2b-4ede9fa42f59"
// Característica y descriptor de temperatura
#ifdef temperatureCelsius
BLECharacteristic bmeTemperatureCelsiusCharacteristics("cba1d466-344c-4be3-ab3f-189f80dd7518", BLECharacteristic::PROPERTY_NOTIFY);
BLEDescriptor bmeTemperatureCelsiusDescriptor(BLEUUID((uint16_t)0x2902));
#else
BLECharacteristic bmeTemperatureFahrenheitCharacteristics("f78ebbff-c8b7-4107-93de-889a6a06d408", BLECharacteristic::PROPERTY_NOTIFY);
BLEDescriptor bmeTemperatureFahrenheitDescriptor(BLEUUID((uint16_t)0x2902));
#endif
// Característica y descriptor de humedad
BLECharacteristic bmeHumidityCharacteristics("ca73b3ba-39f6-4ab3-91ae-186dc9577d99", BLECharacteristic::PROPERTY_NOTIFY);
BLEDescriptor bmeHumidityDescriptor(BLEUUID((uint16_t)0x2903));
// Configura las funciones de callback onConnect y onDisconnect
class MyServerCallbacks: public BLEServerCallbacks {
void onConnect(BLEServer* pServer) {
deviceConnected = true;
};
void onDisconnect(BLEServer* pServer) {
deviceConnected = false;
}
};
void initBME(){
if (!bme.begin(0x76)) {
Serial.println("No se pudo encontrar un BME280 válido, ¡comprueba el cableado!");
while (1);
}
}
void setup() {
// Inicia la comunicación serie
Serial.begin(115200);
// Inicia el sensor BME
initBME();
// Crea el dispositivo BLE
BLEDevice::init(bleServerName);
// Crea el servidor BLE
BLEServer *pServer = BLEDevice::createServer();
pServer->setCallbacks(new MyServerCallbacks());
// Crea el servicio BLE
BLEService *bmeService = pServer->createService(SERVICE_UUID);
// Crea las características BLE y los descriptores
// Temperatura
#ifdef temperatureCelsius
bmeService->addCharacteristic(&bmeTemperatureCelsiusCharacteristics);
bmeTemperatureCelsiusDescriptor.setValue("BME temperature Celsius");
bmeTemperatureCelsiusCharacteristics.addDescriptor(&bmeTemperatureCelsiusDescriptor);
#else
bmeService->addCharacteristic(&bmeTemperatureFahrenheitCharacteristics);
bmeTemperatureFahrenheitDescriptor.setValue("BME temperature Fahrenheit");
bmeTemperatureFahrenheitCharacteristics.addDescriptor(&bmeTemperatureFahrenheitDescriptor);
#endif
// Humedad
bmeService->addCharacteristic(&bmeHumidityCharacteristics);
bmeHumidityDescriptor.setValue("BME humidity");
bmeHumidityCharacteristics.addDescriptor(new BLE2902());
// Inicia el servicio
bmeService->start();
// Inicia el advertising
BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
pAdvertising->addServiceUUID(SERVICE_UUID);
pServer->getAdvertising()->start();
Serial.println("Esperando conexión de un cliente para notificar...");
}
void loop() {
if (deviceConnected) {
if ((millis() - lastTime) > timerDelay) {
// Lee la temperatura en Celsius (por defecto)
temp = bme.readTemperature();
// Fahrenheit
tempF = 1.8*temp +32;
// Lee la humedad
hum = bme.readHumidity();
// Notifica la lectura de temperatura del sensor BME
#ifdef temperatureCelsius
static char temperatureCTemp[6];
dtostrf(temp, 6, 2, temperatureCTemp);
// Establece el valor de la característica de temperatura y notifica al cliente conectado
bmeTemperatureCelsiusCharacteristics.setValue(temperatureCTemp);
bmeTemperatureCelsiusCharacteristics.notify();
Serial.print("Temperature Celsius: ");
Serial.print(temp);
Serial.print(" ºC");
#else
static char temperatureFTemp[6];
dtostrf(tempF, 6, 2, temperatureFTemp);
bmeTemperatureFahrenheitCharacteristics.setValue(temperatureFTemp);
bmeTemperatureFahrenheitCharacteristics.notify();
Serial.print("Temperature Fahrenheit: ");
Serial.print(tempF);
Serial.print(" ºF");
#endif
// Notifica la lectura de humedad del sensor BME
static char humidityTemp[6];
dtostrf(hum, 6, 2, humidityTemp);
bmeHumidityCharacteristics.setValue(humidityTemp);
bmeHumidityCharacteristics.notify();
Serial.print(" - Humidity: ");
Serial.print(hum);
Serial.println(" %");
lastTime = millis();
}
}
}
Puedes subir el código y funcionará inmediatamente, anunciando su servicio con las características de temperatura y humedad.
Cómo funciona el código del servidor paso a paso
Importar librerías
El código empieza importando las librerías necesarias:
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>
#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>
Elegir la unidad de temperatura
Por defecto, el ESP32 envía la temperatura en grados Celsius. Puedes comentar la siguiente línea o borrarla para enviar la temperatura en grados Fahrenheit:
#define temperatureCelsius
Nombre del servidor BLE
La siguiente línea define un nombre para nuestro servidor BLE. Deja el nombre de servidor BLE por defecto. De lo contrario, el nombre del servidor en el código del cliente también tendrá que cambiarse (porque tienen que coincidir):
#define bleServerName "BME280_ESP32"
Sensor BME280
Se crea un objeto Adafruit_BME280 llamado bme en los pines I2C por defecto del ESP32. Las variables temp, tempF y hum almacenarán la temperatura en grados Celsius, la temperatura en grados Fahrenheit y la humedad leídas del sensor BME280.
Variables del temporizador y estado de conexión
Las siguientes variables del temporizador definen con qué frecuencia queremos escribir en las características de temperatura y humedad. Establecemos timerDelay en 30000 milisegundos (30 segundos), pero puedes cambiarlo. La variable booleana deviceConnected nos permite saber si un cliente está conectado al servidor:
unsigned long lastTime = 0;
unsigned long timerDelay = 30000;
bool deviceConnected = false;
UUIDs de BLE
En las siguientes líneas definimos UUIDs para el servicio, para la característica de temperatura en Celsius, para la característica de temperatura en Fahrenheit y para la humedad. Recomiendo dejar todos los UUIDs por defecto. De lo contrario, también tendrás que cambiar el código del lado del cliente para que pueda encontrar el servicio y recuperar los valores de las características.
La función setup()
En el setup(), se inicializa el Monitor Serie y el sensor BME280. Después se crea un nuevo dispositivo BLE con el nombre del servidor BLE que definiste antes. Se establece el dispositivo BLE como servidor y se asigna una función de callback:
BLEServer *pServer = BLEDevice::createServer();
pServer->setCallbacks(new MyServerCallbacks());
La función de callback MyServerCallbacks() cambia la variable booleana deviceConnected a true o false según el estado actual del dispositivo BLE. Esto significa que si un cliente está conectado al servidor, el estado es true. Si el cliente se desconecta, la variable booleana cambia a false.
Después, se inicia un servicio BLE con el UUID del servicio definido antes. Después, se crea la característica BLE de temperatura (Celsius o Fahrenheit según la definición). Después, se establece la característica de humedad. Finalmente, se inicia el servicio y el servidor inicia el advertising para que otros dispositivos puedan encontrarlo.
La función loop()
La función loop() es bastante directa. Compruebas constantemente si el dispositivo está conectado a un cliente o no. Si está conectado y ha pasado el timerDelay, lee la temperatura y humedad actuales.
Si estás usando temperatura en Celsius, se ejecuta la sección correspondiente. Primero, convierte la temperatura a una variable char (variable temperatureCTemp). Debemos convertir la temperatura a una variable de tipo char para usar el método setValue(). Después, establece el valor de la característica a la nueva temperatura usando setValue(). Después de establecer el nuevo valor, podemos notificar al cliente conectado usando la función notify().
Seguimos un procedimiento similar para la temperatura en Fahrenheit y para la humedad.
Probar el servidor BLE del ESP32
Sube el código a tu placa y luego abre el Monitor Serie. Mostrará un mensaje indicando que está esperando conexiones. Después, puedes probar si el servidor BLE funciona como esperas usando una aplicación de escaneo BLE en tu smartphone como nRF Connect. Esta aplicación está disponible para Android y iOS.
Después de instalar la aplicación, habilita Bluetooth en tu smartphone. Abre la app nRF Connect y pulsa el botón de escanear. Encontrará todos los dispositivos Bluetooth cercanos, incluyendo tu dispositivo BME280_ESP32 (el nombre del servidor BLE que definiste en el código).
Conéctate a tu dispositivo BME280_ESP32 y luego selecciona la pestaña del cliente. Puedes comprobar que anuncia el servicio con el UUID que definimos en el código, así como las características de temperatura y humedad. Fíjate en que esas características tienen la propiedad Notify.
¡Tu servidor BLE del ESP32 está listo! Ve a la siguiente sección para crear un cliente BLE del ESP32 que se conecte al servidor para acceder a las características de temperatura y humedad y mostrar las lecturas en una pantalla OLED.
Parte 2: Cliente BLE con el ESP32
En esta sección, crearemos el cliente BLE del ESP32 que establecerá una conexión con el servidor BLE del ESP32 y mostrará las lecturas en una pantalla OLED.
Esquema de conexión
El cliente BLE del ESP32 está conectado a una pantalla OLED. La pantalla muestra las lecturas recibidas vía Bluetooth. Conecta tu pantalla OLED al ESP32: el pin SCL se conecta al GPIO 22 y el pin SDA al GPIO 21.
Instalar las librerías SSD1306, GFX y BusIO
Necesitas instalar las siguientes librerías para interactuar con la pantalla OLED: la librería Adafruit_SSD1306, la librería Adafruit GFX y la librería Adafruit BusIO.
Para instalar las librerías, ve a Programa > Incluir Librería > Gestionar Librerías y busca los nombres de las librerías.
Nota para VS Code + PlatformIO: si estás usando VS Code con la extensión PlatformIO, copia las siguientes dependencias en el archivo
platformio.ini:lib_deps = adafruit/Adafruit GFX Library@^1.10.12 adafruit/Adafruit SSD1306@^2.4.6
Código del cliente BLE del ESP32
Copia el sketch del cliente BLE en tu Arduino IDE o en el archivo main.cpp si usas VS Code con PlatformIO:
/*
Rui Santos
Instrucciones completas en https://RandomNerdTutorials.com/esp32-ble-server-client/
*/
#include "BLEDevice.h"
#include <Wire.h>
#include <Adafruit_SSD1306.h>
#include <Adafruit_GFX.h>
// Por defecto, la temperatura está en Celsius
// Comenta la siguiente línea para temperatura en Fahrenheit
#define temperatureCelsius
// Nombre del servidor BLE (el nombre del otro ESP32 que ejecuta el sketch del servidor)
#define bleServerName "BME280_ESP32"
/* UUIDs del servicio y características que queremos leer */
// Servicio BLE
static BLEUUID bmeServiceUUID("91bad492-b950-4226-aa2b-4ede9fa42f59");
// Características BLE
#ifdef temperatureCelsius
// Característica de temperatura Celsius
static BLEUUID temperatureCharacteristicUUID("cba1d466-344c-4be3-ab3f-189f80dd7518");
#else
// Característica de temperatura Fahrenheit
static BLEUUID temperatureCharacteristicUUID("f78ebbff-c8b7-4107-93de-889a6a06d408");
#endif
// Característica de humedad
static BLEUUID humidityCharacteristicUUID("ca73b3ba-39f6-4ab3-91ae-186dc9577d99");
// Banderas que indican si debemos empezar a conectar y si la conexión está activa
static boolean doConnect = false;
static boolean connected = false;
// Dirección del dispositivo periférico. La dirección se encontrará durante el escaneo...
static BLEAddress *pServerAddress;
// Características que queremos leer
static BLERemoteCharacteristic* temperatureCharacteristic;
static BLERemoteCharacteristic* humidityCharacteristic;
// Activar notify
const uint8_t notificationOn[] = {0x1, 0x0};
const uint8_t notificationOff[] = {0x0, 0x0};
#define SCREEN_WIDTH 128 // Ancho de la pantalla OLED, en píxeles
#define SCREEN_HEIGHT 64 // Alto de la pantalla OLED, en píxeles
// Declaración para una pantalla SSD1306 conectada a I2C (pines SDA, SCL)
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);
// Variables para almacenar temperatura y humedad
char* temperatureChar;
char* humidityChar;
// Banderas para comprobar si hay nuevas lecturas de temperatura y humedad disponibles
boolean newTemperature = false;
boolean newHumidity = false;
// Conecta al servidor BLE que tiene el nombre, Servicio y Características
bool connectToServer(BLEAddress pAddress) {
BLEClient* pClient = BLEDevice::createClient();
// Conecta al servidor BLE remoto.
pClient->connect(pAddress);
Serial.println(" - Conectado al servidor");
// Obtiene una referencia al servicio que buscamos en el servidor BLE remoto.
BLERemoteService* pRemoteService = pClient->getService(bmeServiceUUID);
if (pRemoteService == nullptr) {
Serial.print("No se encontró el UUID de nuestro servicio: ");
Serial.println(bmeServiceUUID.toString().c_str());
return (false);
}
// Obtiene una referencia a las características en el servicio del servidor BLE remoto.
temperatureCharacteristic = pRemoteService->getCharacteristic(temperatureCharacteristicUUID);
humidityCharacteristic = pRemoteService->getCharacteristic(humidityCharacteristicUUID);
if (temperatureCharacteristic == nullptr || humidityCharacteristic == nullptr) {
Serial.print("No se encontró el UUID de nuestra característica");
return false;
}
Serial.println(" - Encontradas nuestras características");
// Asigna funciones de callback para las características
temperatureCharacteristic->registerForNotify(temperatureNotifyCallback);
humidityCharacteristic->registerForNotify(humidityNotifyCallback);
return true;
}
// Función de callback que se llama cuando se recibe un anuncio de otro dispositivo
class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks {
void onResult(BLEAdvertisedDevice advertisedDevice) {
if (advertisedDevice.getName() == bleServerName) { // Comprueba si el nombre del anunciante coincide
advertisedDevice.getScan()->stop(); // El escaneo se puede detener, hemos encontrado lo que buscábamos
pServerAddress = new BLEAddress(advertisedDevice.getAddress()); // La dirección del anunciante es la que necesitamos
doConnect = true; // Establece el indicador, indicando que estamos listos para conectar
Serial.println("Dispositivo encontrado. ¡Conectando!");
}
}
};
// Cuando el servidor BLE envía una nueva lectura de temperatura con la propiedad notify
static void temperatureNotifyCallback(BLERemoteCharacteristic* pBLERemoteCharacteristic,
uint8_t* pData, size_t length, bool isNotify) {
// Almacena el valor de temperatura
temperatureChar = (char*)pData;
newTemperature = true;
}
// Cuando el servidor BLE envía una nueva lectura de humedad con la propiedad notify
static void humidityNotifyCallback(BLERemoteCharacteristic* pBLERemoteCharacteristic,
uint8_t* pData, size_t length, bool isNotify) {
// Almacena el valor de humedad
humidityChar = (char*)pData;
newHumidity = true;
Serial.print(newHumidity);
}
// Función que imprime las últimas lecturas de los sensores en la pantalla OLED
void printReadings(){
display.clearDisplay();
// Muestra la temperatura
display.setTextSize(1);
display.setCursor(0,0);
display.print("Temperature: ");
display.setTextSize(2);
display.setCursor(0,10);
display.print(temperatureChar);
display.setTextSize(1);
display.cp437(true);
display.write(167);
display.setTextSize(2);
Serial.print("Temperature:");
Serial.print(temperatureChar);
#ifdef temperatureCelsius
// Temperatura Celsius
display.print("C");
Serial.print("C");
#else
// Temperatura Fahrenheit
display.print("F");
Serial.print("F");
#endif
// Muestra la humedad
display.setTextSize(1);
display.setCursor(0, 35);
display.print("Humidity: ");
display.setTextSize(2);
display.setCursor(0, 45);
display.print(humidityChar);
display.print("%");
display.display();
Serial.print(" Humidity:");
Serial.print(humidityChar);
Serial.println("%");
}
void setup() {
// Configuración de la pantalla OLED
// SSD1306_SWITCHCAPVCC = genera el voltaje de la pantalla desde 3.3V internamente
if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { // Dirección 0x3C para 128x32
Serial.println(F("¡Fallo en la asignación de SSD1306!"));
for(;;); // No continuar, bucle infinito
}
display.clearDisplay();
display.setTextSize(2);
display.setTextColor(WHITE,0);
display.setCursor(0,25);
display.print("BLE Client");
display.display();
// Inicia la comunicación serie
Serial.begin(115200);
Serial.println("Iniciando aplicación cliente BLE de Arduino...");
// Inicia el dispositivo BLE
BLEDevice::init("");
// Recupera un Scanner y establece el callback que queremos usar para ser informados cuando
// hayamos detectado un nuevo dispositivo. Especifica que queremos escaneo activo y comienza
// el escaneo para que se ejecute durante 30 segundos.
BLEScan* pBLEScan = BLEDevice::getScan();
pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks());
pBLEScan->setActiveScan(true);
pBLEScan->start(30);
}
void loop() {
// Si la bandera "doConnect" es true, hemos escaneado y encontrado el servidor BLE
// deseado al que queremos conectarnos. Ahora nos conectamos a él. Una vez
// conectados, establecemos la bandera connected a true.
if (doConnect == true) {
if (connectToServer(*pServerAddress)) {
Serial.println("Ahora estamos conectados al servidor BLE.");
// Activa la propiedad Notify de cada característica
temperatureCharacteristic->getDescriptor(BLEUUID((uint16_t)0x2902))->writeValue((uint8_t*)notificationOn, 2, true);
humidityCharacteristic->getDescriptor(BLEUUID((uint16_t)0x2902))->writeValue((uint8_t*)notificationOn, 2, true);
connected = true;
} else {
Serial.println("Hemos fallado al conectar al servidor; Reinicia tu dispositivo para escanear servidores BLE cercanos de nuevo.");
}
doConnect = false;
}
// Si hay nuevas lecturas de temperatura disponibles, se imprimen en la OLED
if (newTemperature && newHumidity){
newTemperature = false;
newHumidity = false;
printReadings();
}
delay(1000); // Retardo de un segundo entre bucles.
}
Cómo funciona el código del cliente paso a paso
Importar librerías
Se empieza importando las librerías necesarias:
#include "BLEDevice.h"
#include <Wire.h>
#include <Adafruit_SSD1306.h>
#include <Adafruit_GFX.h>
Elegir la unidad de temperatura
Por defecto, el cliente recibirá la temperatura en grados Celsius. Si comentas la siguiente línea o la borras, empezará a recibir la temperatura en grados Fahrenheit:
#define temperatureCelsius
Nombre del servidor BLE y UUIDs
Después, se define el nombre del servidor BLE al que queremos conectarnos y los UUIDs del servicio y las características que queremos leer. Deja el nombre del servidor BLE y los UUIDs por defecto para que coincidan con los definidos en el sketch del servidor.
Declarar variables
Después, necesitas declarar algunas variables que se usarán con Bluetooth para comprobar si estamos conectados al servidor o no. La variable doConnect indica si debemos empezar a conectar. La variable connected indica si la conexión está activa. Después, se crea una variable de tipo BLEAddress que hace referencia a la dirección del servidor al que queremos conectar. Esta dirección se encontrará durante el escaneo. Después, se establecen las características que queremos leer (temperatura y humedad).
Pantalla OLED
También necesitas declarar algunas variables para trabajar con la OLED. Se define el ancho y alto de la OLED (128 x 64 píxeles) y se instancia la pantalla OLED.
Variables de temperatura y humedad
Se definen variables char para almacenar los valores de temperatura y humedad recibidos por el servidor. Las siguientes variables se usan para comprobar si hay nuevas lecturas disponibles y si es momento de actualizar la pantalla OLED.
La función printReadings()
Se crea una función llamada printReadings() que muestra las lecturas de temperatura y humedad en la pantalla OLED. La función limpia la pantalla, muestra el texto “Temperature: ” y el valor recibido, después “Humidity: ” y su valor, y finalmente llama a display.display() para volcar el contenido al hardware.
La función setup()
En el setup(), se inicia la pantalla OLED. Después, se imprime un mensaje en la primera línea diciendo “BLE Client”. Después, se inicia la comunicación serie a 115200 baudios y se inicializa el dispositivo BLE.
Escanear dispositivos cercanos
Los siguientes métodos escanean dispositivos cercanos. Nota que la función MyAdvertisedDeviceCallbacks(), al encontrar un dispositivo BLE, comprueba si el dispositivo encontrado tiene el nombre correcto del servidor BLE. Si lo tiene, detiene el escaneo y cambia la variable booleana doConnect a true. De esta forma sabemos que hemos encontrado el servidor que buscamos y podemos empezar a establecer una conexión.
Conectar al servidor
Si la variable doConnect es true, se intenta conectar al servidor BLE. La función connectToServer() maneja la conexión entre el cliente y el servidor. También asigna funciones de callback responsables de manejar qué ocurre cuando se recibe un nuevo valor:
temperatureCharacteristic->registerForNotify(temperatureNotifyCallback);
humidityCharacteristic->registerForNotify(humidityNotifyCallback);
Después de que el cliente BLE se conecta al servidor, necesitas activar la propiedad notify para cada característica. Para eso, usa el método writeValue() sobre el descriptor:
temperatureCharacteristic->getDescriptor(BLEUUID((uint16_t)0x2902))->writeValue((uint8_t*)notificationOn, 2, true);
humidityCharacteristic->getDescriptor(BLEUUID((uint16_t)0x2902))->writeValue((uint8_t*)notificationOn, 2, true);
Notificar nuevos valores
Cuando el cliente recibe un nuevo valor notify, llamará a estas dos funciones: temperatureNotifyCallback() y humidityNotifyCallback(), que son responsables de recuperar el nuevo valor, actualizar la OLED con las nuevas lecturas e imprimirlas en el Monitor Serie.
Estas dos funciones se ejecutan cada vez que el servidor BLE notifica al cliente con un nuevo valor, lo que ocurre cada 30 segundos. Estas funciones guardan los valores recibidos en las variables temperatureChar y humidityChar. También cambian las variables newTemperature y newHumidity a true, para que sepamos que hemos recibido nuevas lecturas.
Mostrar las nuevas lecturas de temperatura y humedad
En el loop(), hay una sentencia if que comprueba si hay nuevas lecturas disponibles. Si hay nuevas lecturas, se ponen las variables newTemperature y newHumidity a false para poder recibir nuevas lecturas más adelante. Después, se llama a la función printReadings() para mostrar las lecturas en la OLED.
Probar el proyecto
Eso es todo para el código. Puedes subirlo a tu placa ESP32. Una vez subido el código, enciende el servidor BLE del ESP32, luego enciende el ESP32 con el sketch del cliente. El cliente empieza a escanear dispositivos cercanos, y cuando encuentra el otro ESP32, establece una conexión Bluetooth. Cada 30 segundos, actualiza la pantalla con las últimas lecturas.
Importante: no olvides desconectar tu smartphone del servidor BLE. De lo contrario, el cliente BLE del ESP32 no podrá conectarse al servidor.
Conclusión: lo que has aprendido
En este tutorial, has aprendido cómo crear un servidor BLE y un cliente BLE con el ESP32. Has aprendido cómo establecer nuevos valores de temperatura y humedad en las características del servidor BLE. Después, otros dispositivos BLE (clientes) pueden conectarse a ese servidor y leer esos valores de las características para obtener los últimos valores de temperatura y humedad. Esas características tienen la propiedad notify, para que el cliente sea notificado cada vez que hay un nuevo valor.
Usar BLE es otro protocolo de comunicación que puedes usar con las placas ESP32 además del Wi-Fi. Combinando el bajo consumo de BLE con la potencia del ESP32, puedes construir sistemas de monitorización inalámbrica que funcionen durante meses con una batería, comunicando datos entre dispositivos sin necesidad de una red Wi-Fi ni de un router.
Este patrón de servidor + cliente con notify es uno de los más útiles en proyectos IoT: un nodo sensor remoto (servidor) publica datos, y un nodo concentrador (cliente) los recoge y los muestra o reenvía. Dominar este patrón te abre las puertas a proyectos de domótica, wearables, monitorización industrial y sistemas de bajo consumo en general.
Descubre más desde CIBERED
Suscríbete y recibe las últimas entradas en tu correo electrónico.
