BLE en ESP32: Conexión Bluetooth Low Energy

En esta ocasión os voy a hablar acerca de cómo utilizar la conexión bluetooth BLE (Bluetooth Low Energy) en nuestro ESP32. A nivel de consumo puede que no nos aporte mucho ya que el consumo del procesador del ESP32 no es bajo, pero lo que si nos proporcionará es un interfaz avanzado de comunicación que nos permitirá añadir seguridad, tener varios servicios,… Por poner un ejemplo, podrías crear un sensor para una estación metereológica y crear un servicio para la temperatura, otro para la humedad,… A la hora de leer los datos podrías elegir cuál quieres sin tener que leer todos y luego separarlos.

Lo primero que debemos saber es qué es BLE: Es una tecnología bluetooth que permite la comunicación entre dos dispositivos con un consumo de energía muy bajo. Esto permite que haya dispositivos como balizas bluetooth con una duración de varios años con una simple pila de botón. Además de esto también incluye cifrado y seguridad configurable, por lo que podremos proteger nuestras conexiones de escuchas externas.

En esta guía os enseñaré cómo configurar el ESP32 en modo host y cliente, para así intercambiar datos entre dispositivos compatibles. También te recomiendo que eches un vistazo a mi guía de iniciación, en la que enseño a realizar una conexión serial entre dispositivos.

Cómo se organizan los datos

Antes de lanzarnos a realizar nuestras comunicaciones utilizando BLE, es muy importante que sepamos cómo se organizan los datos. La estructura de los datos se realiza de forma jerárquica y se divide en secciones. Estas secciones son perfiles, servicios y características.

Perfiles

Un perfil es un conjunto de servicios que están definidos por la Bluetooth SIG (Bluetooth Special Interest Group). Por ejemplo, te puedes descargar el perfil de un sensor de presión arterial (en inglés), desde aqui. Puedes ver una lista extensa de los perfiles oficiales en la página oficial de bluetooth.

Si te has asustado al ver el documento de 44 páginas sobre el perfil, no te preocupes, sólo quería que lo tuvieseis en cuenta ;). No es necesario cumplirlo a rajatabla para una conexión entre dos ESP32 por ejemplo, ya que nos podemos inventar nuestro perfil propio. Este tipo información es útil que la sepas si vas a crear un dispositivo y pretendes que sea compatible con otros, pero si el uso va a ser privado no es importante.

Servicios

Los servicios son agrupamientos de datos simples, como por ejemplo la información de un sensor. Se dividen en características, y sirven para agruparlas.

El uso de los servicios sirve para por ejemplo, que tengas un dispositivo que dispone de varios sensores. En lugar de crear un sólo servicio con todos los datos, podrías crear un servicio por sensor y así tener los datos organizados.

Los servicios tienen un identificador único llamado UUID, el cual es de 16 bits para servicios oficiales, y de 128 bits para servicios personalizados. Algunos servicios como el nivel de batería, presión sanguínea… al igual que los perfiles, están definidos por la Bluetooth SIG. Por ejemplo, el UUID de un servicio de un sensor de presión arterial es el 0x1810, y el de pulso es el 0x180D.

Puedes ver todos los datos también en la página oficial de Bluetooth. En este documento puedes encontrar las distintas UUID dependiendo del tipo de servicio que estemos implementando. Al igual que con los perfiles, esta información sólo es útil si vamos a crear un dispositivo que queramos que sea compatible con otros. Si el uso es privado, podemos generar nuestro propio UUID, para lo cual podemos usar cualquier herramienta como por ejemplo esta página.

Características

Una característica es cada uno de los valores que queremos incluir en los servicios. Por ejemplo, un servicio para un sensor DHT11/22 podría contener dos características, las cuales serían la temperatura y la humedad. Un servicio de un acelerómetro podría contener tres características para cada uno de los ejes (X, Y, y Z).

Al igual que con los servicios, tiene un identificador único UUID de 16 bits para una característica oficial, y 128 bits para una característica personalizada. Puedes encontrar una tabla con las características oficiales también en la web de bluetooth, y al igual que con los otros UUID, podemos utilizar uno aleatorio para un uso personalizado.

BLE en modo host

Lo primero que vamos a aprender es cómo hacer nuestro propio dispositivo BLE para poder leer y modificar datos desde el móvil. Para ello necesitaremos instalarnos una aplicación que nos permita dicha comunicación, como por ejemplo la que yo uso y que se llama nRF Connect.

Empezaremos con un ejemplo básico, en el que controlaremos un diodo LED a través de una característica personalizada. El circuito que usaremos será uno básico para encender un led:

Y el código para controlarlo será el siguiente:

/*
    Simple sketch to control a led with BLE protocol by
    Daniel Carrasco (https://www.electrosoftcloud.com)
*/

#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEServer.h>

// See the following for generating UUIDs:
// https://www.uuidgenerator.net/

#define SERVICE_UUID        "3feb1e8a-3981-4045-ad39-b225135013a0"
#define CONTROL_CHARACTERISTIC_UUID "beb5483e-36e1-4688-b7f5-ea07361b26a8"
#define LED 23

char ledStatus = 48; // 0 in ASCII

// New characteristic with object to manage it
BLECharacteristic controlCharacteristic(
  CONTROL_CHARACTERISTIC_UUID,
  BLECharacteristic::PROPERTY_READ |
  BLECharacteristic::PROPERTY_WRITE
);

void setup() {
  Serial.begin(115200);
  Serial.println("Starting BLE!");

  Serial.println("Initializing device");
  BLEDevice::init("Led control electrosoftcloud"); // Initializing the device with its name
  Serial.println("Creating server");
  BLEServer *pServer = BLEDevice::createServer(); // Create the server
  Serial.println("Adding service UUID");
  BLEService *pService = pServer->createService(SERVICE_UUID); // Creating a new service into server
  
  // Adding a characteristic with the object name (official UUID), without object (this characteristic will not change)
  Serial.println("Adding name characteristic");
  BLECharacteristic *nameCharacteristic = pService->createCharacteristic(
                                         BLEUUID((uint16_t)0x2A00),
                                         BLECharacteristic::PROPERTY_READ
                                       );
  nameCharacteristic->setValue("Led");

  // Adding a characteristic to control the led with 0 and 1
  Serial.println("Adding control characteristic");
  pService->addCharacteristic(&controlCharacteristic);
  controlCharacteristic.setValue(&ledStatus); // Value uint8_t with length 1

  Serial.println("Starting...");
  pService->start();
  Serial.println("Creating advertising");
  // BLEAdvertising *pAdvertising = pServer->getAdvertising();  // this still is working for backward compatibility
  BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
  pAdvertising->addServiceUUID(SERVICE_UUID);
  pAdvertising->setScanResponse(true);
  pAdvertising->setMinPreferred(0x06);  // functions that help with iPhone connections issue
  pAdvertising->setMinPreferred(0x12);
  BLEDevice::startAdvertising();
  Serial.println("Characteristic defined! Now you can read it in your phone!");

  pinMode (LED, OUTPUT); // Set the LED pin as OUTPUT
}

void loop() {
  std::string controlValue = controlCharacteristic.getValue();
  if (controlValue[0] != ledStatus) {
    Serial.print("Value changed... new value: ");
    Serial.println(controlValue[0]);
    ledStatus = controlValue[0];
    if (ledStatus == 48) {
      digitalWrite(LED, LOW); // LED Off
    }
    else if (ledStatus == 49) {
      digitalWrite(LED, HIGH); // LED On
    }
  }
}

Explicación del código

Para la explicación del código me centraré en las partes más complejas, obviando las más básicas como los includes, macros, variables… Lo primero que nos encontraremos en el código será la definición de una característica BLE.

// New characteristic with object to manage it
BLECharacteristic controlCharacteristic(
  CONTROL_CHARACTERISTIC_UUID,
  BLECharacteristic::PROPERTY_READ |
  BLECharacteristic::PROPERTY_WRITE
);

Esta característica se define fuera de la función setup y de esta forma para que sea accesible de forma global y podamos usarla en la función loop también. En esta definición simplemente creamos un objeto del tipo BLECharacteristic, con el nombre controlCharacteristic, el UUID definido al principio del sketch y las funciones que tendrá esta característica (en este caso READ y WRITE). Los funciones disponibles para las características son:

PROPERTY_READ: El cliente podrá leer el valor de esta característica
PROPERTY_WRITE: El cliente podrá modificar el valor de esta característica
PROPERTY_NOTIFY: El cliente será notificado cuando el valor de la característica cambie sin necesidad de tener que estar verificándolo continuamente. Se usa en combinación con PROPERTY_READ.
PROPERTY_BROADCAST: La característica es emisible (por ejemplo un stream de audio)
PROPERTY_INDICATE: Similar a la propiedad NOTIFY, con la diferencia de que se espera respuesta del cliente.
PROPERTY_WRITE_NR: Similar a la propiedad WRITE, con la diferencia de que esta no espera respuesta del servidor.

BLEDevice::init("Led control electrosoftcloud"); // Initializing the device with its name
Serial.println("Creating server");
BLEServer *pServer = BLEDevice::createServer(); // Create the server
Serial.println("Adding service UUID");
BLEService *pService = pServer->createService(SERVICE_UUID);

En esta líneas, el init se encarga de inicializar el bluetooth con el nombre que se le indique. Luego se un objeto de servidor y dentro de ese objeto se crea un servicio nuevo con un UUID único indicado anteriormente.

  BLECharacteristic *nameCharacteristic = pService->createCharacteristic(
                                         BLEUUID((uint16_t)0x2A00),
                                         BLECharacteristic::PROPERTY_READ
                                       );
  nameCharacteristic->setValue("Led");

  // Adding a characteristic to control the led with 0 and 1
  Serial.println("Adding control characteristic");
  pService->addCharacteristic(&controlCharacteristic);
  controlCharacteristic.setValue(&ledStatus); // Value uint8_t with length 1

Aquí añadimos las dos características que queremos tener en nuestro dispositivo. Podemos observar que una de ellas la añadimos directamente con createCharacteristic y guardamos un puntero al objeto que se crea (por eso se usan flechas para acceder a sus métodos). La otra característica como la creamos anteriormente para que fuera accesible desde más partes del programa, usaremos addCharacteristic para añadir una ya existente. Fíjate también en que en lugar de usar un UUID de 128 bits, usamos uno de 16 bits. Eso es porque he utilizado el UUID oficial para «Nombre de dispositivo» en lugar de generar uno personalizado.

Sé que algunos diréis que se podría haber creado el puntero fuera de setup y luego crear las dos características del mismo modo, asignando el puntero en el momento de crearla. Sí, es cierto, pero en parte quería enseñaros las dos formas en las que podemos crear las características.

 pService->start();

La verdad es que esta línea no requiere explicación, ya que simplemente inicia el servicio.

  BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
  pAdvertising->addServiceUUID(SERVICE_UUID);
  pAdvertising->setScanResponse(true);

Por último, estas líneas crean un servicio de «propaganda» (advertising), sin el cual nuestro dispositivo no será detectable. En la primera línea generamos el objeto, guardándolo de igual forma en un puntero. En la segunda línea añadimos el servicio que hemos creado anteriormente, y en la tercera indicamos que queremos que responda cuando se escanea.

Conectando con nuestro ESP32 a través de BLE

Una vez que ya tenemos nuestro ESP32 en modo servidor, nos toca conectarnos para leer y modificar sus características. Para ello tal y como indiqué más arriba, usaremos el programa nRF Connect.

Según lo abramos, lo primero que tendremos que hacer es pulsar el botón scan para que detecte los dispositivos bluetooth cercanos. Nuestro dispositivo aparecerá en la lista y tan sólo le tendremos que dar a conectar.

Tras conectarnos veremos que nos aparecen dos servicios genéricos, los cuales son creados por el dispositivo, y un tercero que es el personalizado que creamos nosotros.

Pinchamos sobre él y veremos que se despliegan las características de las que dispone este servicio, en donde puedes observar la que creamos con un UUID oficial y que es por lo tanto conocida (Device Name), y una característica desconocida que es la que creamos personalizada.

Con los botones de la derecha podemos decirle si queremos leer o escribir los datos de la característica dependiendo de si la configuramos como READ o WRITE.

Si le das a leer en ambas verás que contienen los valores que les dimos en el sketch, siendo Led para el nombre del dispositivo, y 0x30 en hexadecimal para el servicio personalizado, que corresponde a 48 en decimal y «0» en ASCII.

¡Ahora vamos a encender el LED! que es para lo que creamos la característica personalizada. Para ello simplemente damos a la flechita de envío de nuevo valor.

Cambiamos el tipo de valor a text que es el que estamos manejando con el desplegable de la derecha.

Pondremos el valor a 1 que es el carácter que indicamos el programa que encendería el led (49 en ASCII), y observaremos cómo se enciendo el LED.

Para apagarlo, simplemente repetimos el proceso y ponemos un 0 como valor de la característica.

Callbacks en BLE

No puedo terminar el apartado del host BLE sin hablar de los callbacks. Esta funcionalidad nos permitirá responder a eventos como que alguien lea o escriba datos en una característica o un cliente se conecte, y así evitarnos el tener que estar revisando en la función loop si hay cambios. En este caso no se usarán funciones como cuando usamos el bluetooth en modo Serial, sino que crearemos una clase con las funciones que nos interesen y la asignaremos. En las conexiones BLE de nuestro bluetooth disponemos de varios tipos de callbacks, entre los que están el callback de servidor y el callback de característica.

Callback de servidor

Los callbacks de servidor se lanzan cuando un cliente interactúa con el host. En este caso están limitados a cuando un cliente se conecta o se desconecta.

class ServerCallbacks: public BLEServerCallbacks {
    void onConnect(BLEServer* MyServer) {
      Serial.println("A client has connected to the host");
    };

    void onDisconnect(BLEServer* MyServer) {
      Serial.println("A client has disconnected from the host");
    }
};

Para asignar dicho callback a nuestro servidor bluetooth simplemente añadiremos la siguiente línea en la función setup:

pServer->setCallbacks(new ServerCallbacks());

Callback de característica

Este tipo de callback como su nombre indica, son lanzados cuando ocurren eventos en una característica como por ejemplo que se lea o modifique un valor.

La clase para un callback de característica en el ejemplo del led que hicimos arriba, sería algo así.

class LedControlCallback: public BLECharacteristicCallbacks {
    void onWrite(BLECharacteristic *pCharacteristic) {
      std::string rxValue = pCharacteristic->getValue();

      if (rxValue.length() > 0) {
        Serial.print("Value changed... new value: ");
        Serial.println(rxValue[0]);
        if (rxValue[0] == 48) {
          digitalWrite(LED, LOW); // LED Off
        }
        else if (rxValue[0] == 49) {
          digitalWrite(LED, HIGH); // LED On
        }
      }
    }
};

Y asignaremos dicho callback a la característica de control del led que creamos.

controlCharacteristic.setCallbacks(new LedControlCallback());

Recuerda que si creamos la variable de la característica como puntero hacia esta, tendremos que usar flechas para indicar que queremos ejecutar los métodos.

Con esto nuestro código del led quedaría así.

/*
    Simple sketch to control a led with BLE protocol by
    Daniel Carrasco (https://www.electrosoftcloud.com)
*/

#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEServer.h>

// See the following for generating UUIDs:
// https://www.uuidgenerator.net/

#define SERVICE_UUID        "3feb1e8a-3981-4045-ad39-b225135013a0"
#define CONTROL_CHARACTERISTIC_UUID "beb5483e-36e1-4688-b7f5-ea07361b26a8"
#define LED 23

// Callback function for led switch characteristic
class LedControlCallback: public BLECharacteristicCallbacks {
    void onWrite(BLECharacteristic *pCharacteristic) {
      std::string rxValue = pCharacteristic->getValue();

      if (rxValue.length() > 0) {
        Serial.print("Value changed... new value: ");
        Serial.println(rxValue[0]);
        if (rxValue[0] == 48) {
          digitalWrite(LED, LOW); // LED Off
        }
        else if (rxValue[0] == 49) {
          digitalWrite(LED, HIGH); // LED On
        }
      }
    }
};

// New characteristic with object to manage it
BLECharacteristic controlCharacteristic(
  CONTROL_CHARACTERISTIC_UUID,
  BLECharacteristic::PROPERTY_READ |
  BLECharacteristic::PROPERTY_WRITE
);

void setup() {
  Serial.begin(115200);
  Serial.println("Starting BLE!");

  Serial.println("Initializing device");
  BLEDevice::init("Led control electrosoftcloud"); // Initializing the device with its name
  Serial.println("Creating server");
  BLEServer *pServer = BLEDevice::createServer(); // Create the server
  Serial.println("Adding service UUID");
  BLEService *pService = pServer->createService(SERVICE_UUID); // Creating a new service into server
  
  // Adding a characteristic with the object name (official UUID), without object (this characteristic will not change)
  Serial.println("Adding name characteristic");
  BLECharacteristic *nameCharacteristic = pService->createCharacteristic(
                                         BLEUUID((uint16_t)0x2A00),
                                         BLECharacteristic::PROPERTY_READ
                                       );
  nameCharacteristic->setValue("Led");

  // Adding a characteristic to control the led with 0 and 1
  Serial.println("Adding control characteristic");
  pService->addCharacteristic(&controlCharacteristic);
  controlCharacteristic.setValue("0"); // Value uint8_t with length 1
  controlCharacteristic.setCallbacks(new LedControlCallback());

  Serial.println("Starting...");
  pService->start();
  Serial.println("Creating advertising");
  // BLEAdvertising *pAdvertising = pServer->getAdvertising();  // this still is working for backward compatibility
  BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
  pAdvertising->addServiceUUID(SERVICE_UUID);
  pAdvertising->setScanResponse(true);
  pAdvertising->setMinPreferred(0x06);  // functions that help with iPhone connections issue
  pAdvertising->setMinPreferred(0x12);
  BLEDevice::startAdvertising();
  Serial.println("Characteristic defined! Now you can read it in your phone!");

  pinMode (LED, OUTPUT); // Set the LED pin as OUTPUT
}

void loop() {
}

Como puedes observar hemos liberado la función loop para todo lo demás, y no necesitamos ni de la variable de estado del led para verificar si ha cambiado, ya que el callback sólo se ejecutará cuando los haya. Además, los callbacks se ejecutarán de forma instantánea aunque la ejecución de la función loop sea muy larga.

Entre los métodos que podemos utilizar en el callback están:

  • onRead: Este método será llamado cuando un cliente lea el valor de una característica
  • onWrite: Este método será llamado cuando un cliente cambie el valor de una característica
  • onNotify: Este método será llamado cuando ocurra un evento de notificación, por ejemplo cuando el servidor cambie el valor de la característica.
  • onStatus: Este método será llamado en distintos cambios de estado, por ejemplo cuando se lanza una notificación de cambio y no hay clientes conectados, timeouts… El listado de eventos es el siguiente:
    • SUCCESS_INDICATE
    • SUCCESS_NOTIFY
    • ERROR_INDICATE_DISABLED
    • ERROR_NOTIFY_DISABLED
    • ERROR_GATT
    • ERROR_NO_CLIENT
    • ERROR_INDICATE_TIMEOUT
    • ERROR_INDICATE_FAILURE

BLE en modo cliente

Por último una cosa que tenemos que saber, es como conectarnos a un dispositivo BLE. Principalmente porque esto nos permitirá conectarnos a distintos dispositivos BLE como pueden ser los relojes deportivos, cintas de correr… y recuperar los datos que nos dan. Además de en conjunción con nuestro host creado arriba. En este ejemplo te enseñaré cómo conectarte al host que hemos creado anteriormente y encender el LED directamente desde otro ESP32. Para ello es necesario que tengas un dispositivo ESP32 con el sketch de host que hicimos anteriormente, y vamos a proceder a programar otro como cliente.

Código cliente BLE

Para nuestro cliente «blink» usaremos el siguiente código:

/**
 * A simple BLE client that connects to led host and make it blink
 * by Daniel Carrasco from https://www.electrosoftcloud.com
 */

#include "BLEDevice.h"


// The remote service we wish to connect to.
static BLEUUID serviceUUID("3feb1e8a-3981-4045-ad39-b225135013a0");
// The characteristic of the remote service we are interested in.
static BLEUUID    charUUID("beb5483e-36e1-4688-b7f5-ea07361b26a8");

static boolean doConnect = false;
static boolean connected = false;
static boolean doScan = false;
static BLERemoteCharacteristic* pRemoteCharacteristic;
static BLEAdvertisedDevice* myDevice;
bool ledStatus = false;

class MyClientCallback : public BLEClientCallbacks {
  void onConnect(BLEClient* pclient) {
  }

  void onDisconnect(BLEClient* pclient) {
    connected = false;
    Serial.println("onDisconnect");
  }
};

bool connectToServer() {
    Serial.print("Forming a connection to ");
    Serial.println(myDevice->getAddress().toString().c_str());
    
    BLEClient*  pClient  = BLEDevice::createClient();
    pClient->setClientCallbacks(new MyClientCallback());
    Serial.println(" - Created client");

    // Connect to the remove BLE Server.
    pClient->connect(myDevice);  // if you pass BLEAdvertisedDevice instead of address, it will be recognized type of peer device address (public or private)
    Serial.println(" - Connected to server");

    // Obtain a reference to the service we are after in the remote BLE server.
    BLERemoteService* pRemoteService = pClient->getService(serviceUUID);
    if (pRemoteService == nullptr) {
      Serial.print("Failed to find our service UUID: ");
      Serial.println(serviceUUID.toString().c_str());
      pClient->disconnect();
      return false;
    }
    Serial.println(" - Found our service");


    // Obtain a reference to the characteristic in the service of the remote BLE server.
    pRemoteCharacteristic = pRemoteService->getCharacteristic(charUUID);
    if (pRemoteCharacteristic == nullptr) {
      Serial.print("Failed to find our characteristic UUID: ");
      Serial.println(charUUID.toString().c_str());
      pClient->disconnect();
      return false;
    }
    Serial.println(" - Found our characteristic");

    // Read the value of the characteristic.
    if(pRemoteCharacteristic->canRead()) {
      std::string value = pRemoteCharacteristic->readValue();
      Serial.print("The characteristic value was: ");
      Serial.println(value.c_str());
    }

    connected = true;
    return true;
}
/**
 * Scan for BLE servers and find the first one that advertises the service we are looking for.
 */
class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks {
 /**
   * Called for each advertising BLE server.
   */
  void onResult(BLEAdvertisedDevice advertisedDevice) {
    Serial.print("BLE Advertised Device found: ");
    Serial.println(advertisedDevice.toString().c_str());

    // We have found a device, let us now see if it contains the service we are looking for.
    if (advertisedDevice.haveServiceUUID() && advertisedDevice.isAdvertisingService(serviceUUID)) {

      BLEDevice::getScan()->stop();
      myDevice = new BLEAdvertisedDevice(advertisedDevice);
      doConnect = true;
      doScan = true;

    } // Found our server
  } // onResult
}; // MyAdvertisedDeviceCallbacks


void setup() {
  Serial.begin(115200);
  Serial.println("Starting Arduino BLE Client application...");
  BLEDevice::init("");

  // Retrieve a Scanner and set the callback we want to use to be informed when we
  // have detected a new device.  Specify that we want active scanning and start the
  // scan to run for 5 seconds.
  BLEScan* pBLEScan = BLEDevice::getScan();
  pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks());
  pBLEScan->setInterval(1349);
  pBLEScan->setWindow(449);
  pBLEScan->setActiveScan(true);
  pBLEScan->start(5, false);
} // End of setup.


// This is the Arduino main loop function.
void loop() {

  // If the flag "doConnect" is true then we have scanned for and found the desired
  // BLE Server with which we wish to connect.  Now we connect to it.  Once we are 
  // connected we set the connected flag to be true.
  if (doConnect == true) {
    if (connectToServer()) {
      Serial.println("We are now connected to the BLE Server.");
    } else {
      Serial.println("We have failed to connect to the server; there is nothin more we will do.");
    }
    doConnect = false;
  }

  // If we are connected to a peer BLE Server, update the characteristic each time we are reached
  // with the current time since boot.
  if (connected) {
    ledStatus = !ledStatus;
    Serial.print("Setting new led characteristic value to \"");
    Serial.print(ledStatus);
    Serial.println("\"");
    
    // Set the characteristic's value to be the array of bytes that is actually a string.
    if (ledStatus) {
      pRemoteCharacteristic->writeValue("1", 1); // Value and length
    }
    else {
      pRemoteCharacteristic->writeValue("0", 1); // Value and length
    }
  }else if(doScan){
    BLEDevice::getScan()->start(0);  // this is just example to start scan after disconnect, most likely there is better way to do it in arduino
  }
  
  delay(500); // Delay a second between loops.
} // End of loop

Este código es quizá un poco complejo, pero tiene la ventaja de que es capaz de descubrir automáticamente los dispositivos que haya con el servicio y la característica que creamos anteriormente. También se podría escanear los dispositivos BLE y conectar a través del nombre, pero este método me pareció mejor.

En este caso no haré la explicación del código, ya que tiene buenos comentarios en él y además muchas de las cosas son similares a un tipo servidor.

Callbacks

Al igual que el modo host, el modo cliente también dispone de callbacks que se ejecutarán tras ciertos eventos como conectarse y desconectarse a un host. En el ejemplo de arriba podremos aprovechar los callbacks de cliente, además de los de anuncio. Estos últimos son aprovechados en el código anterior para buscar la característica que necesitamos en el dispositivo y así conectarnos a él sin necesidad de usar el nombre.

Callbacks de cliente

Para los callbacks de cliente, dispondremos de dos callbacks para detectar cuando se ha conectado o desconectado:

  • onConnect
  • onDisconnect

Callbacks de anuncio

En este caso sólo disponemos de un callback, el cual es onResult. Este callback recibe como argumento un objeto con una serie de métodos disponibles con el dispositivo que ha descubierto, los cuales son:

  • BLEAddress getAddress(): Devuelve la dirección MAC del dispositivo
  • uint16_t getApperance(): Devuelve el tipo de dispositivo
  • std::string getManufacturerData(): Devuelve los datos del fabricante del dispositivo
  • std::string getName(): Devuelve el nombre del dispositivo
  • int getRSSI(): Devuelve la potencia de la señal recibida
  • BLEUUID getServiceUUID(): Devuelve el UUID del servicio
  • int8_t getTXPower(); Devuelve la potencia de transmisión
  • bool haveAppearance(): Verifica si tiene un tipo de dispositivo
  • bool haveManufacturerData(): Verifica si tiene los datos del fabricante
  • bool haveName(): Verifica si coincide su nombre
  • bool haveRSSI(): Verifica si la señal recibida tiene como mínimo la potencia indicada
  • bool haveServiceUUID(): Verifica si el bluetooth tiene el UUID de servicio
  • bool haveTXPower(): Verifica si la potencia de transmisión es la menos la indicada
  • std::string toString(): Convierte a texto los datos del dispositivo

Espero que os haya gustado y no dudéis en comentar

¡Un saludo!

2 comentarios en «BLE en ESP32: Conexión Bluetooth Low Energy»

    • Hola,

      El poder conectar más de un dispositivo al mismo tiempo al host dependerá más bien del propio host. Algunos dispositivos permiten múltiples conexiones, pero otros no. En el caso del ESP32 me temo que no he experimentado lo suficiente como para probarlo y no te puedo decir. Una buena alternativa es conectarse, sacar el dato que se necesita y desconectarse, así el host queda libre.
      Respecto a lo de crear más de un servicio, tendrías que crear un UUID para este servicio, y utilizar las líneas para crearlo que muestro en el ejemplo:

      Serial.println("Adding service UUID");
      BLEService *pService = pServer->createService(NEW_SERVICE_UUID); // Creating a new service into server

      // Adding a characteristic with the object name (official UUID), without object (this characteristic will not change)
      Serial.println("Adding name characteristic");
      BLECharacteristic *nameCharacteristic = pService->createCharacteristic(
      BLEUUID((uint16_t)0x2A00),
      BLECharacteristic::PROPERTY_READ
      );
      nameCharacteristic->setValue("Led");

      Responder

Deja un comentario

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.