LAB5. BLE: servidor GATT

Objetivos

  • Diseccionar en detalle un firmware de construcción de tabla GATT (servidor GATT) utilizando la API de ESP-IDF.
  • Aprender a utilizar la herramienta gatttool para interactuar con el servidor GATT.
  • Modificar el servidor GATT para que acepte peticiones de notificación por parte del cliente, y para que publique bajo demanda valores actualizados para una determinada característica.

Implementación de un servidor GATT basado en tablas

Introducción

En esta práctica, desplegaremos un servidor GATT utilizando la API de ESP-IDF para tal fin. Dicha API expone las funcionalidades de Bluedroid, la pila Bluetooth (incluyendo BLE) que proporciona ESP-IDF para el desarrollo de aplicaciones Bluetooth.

El ejemplo con el que trabajaremos reside en el directorio examples/bluetooth/bluedroid/ble/gatt_server_service_table. Debido a la complejidad del ejemplo (al menos en su parte inicial), la presente práctica procede, en primer lugar, con un recorrido por la preparación y construcción del servidor siguiendo una estructura de tabla que define los servicios y características que se implementarán en el mismo.

El ejemplo implementa el perfil Heart Rate Profile definido en la especificación Bluetooth, que sigue la siguiente estructura:

Table-like data structure representing the Heart Rate Service

Desplegaremos, por tanto, tres características. De ellas, la más importante para nosotros será el valor de medición de ritmo cardíaco, con su valor (Heart Rate Measurement Value) y su configuración de notificaciones (Heart Rate Measurement Notification Configuration).

Inclusión de encabezados

Los siguientes ficheros de cabecera son necesarios para dotar de funcionalidad BLE a nuestro firmware:

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"
#include "esp_system.h"
#include "esp_log.h"
#include "nvs_flash.h"
#include "esp_bt.h"
#include "esp_gap_ble_api.h"
#include "esp_gatts_api.h"
#include "esp_bt_main.h"
#include "gatts_table_creat_demo.h"
#include "esp_gatt_common_api.h"

Estos encabezados son necesarios para un correcto funcionamiento de FreeRTOS y de sus componentes, incluyendo funcionalidad relativa a logging y almacenamiento no volátil. Son especialmente interesantes los ficheros esp_bt.h, esp_bt_main.h, esp_gap_ble_api.h y esp_gatts_api.h, ya que exponen la API BLE necesaria para la implementación del firmware:

  • esp_bt.h: implementa el controlador BT y los procedimientos VHCI del lado del host.
  • esp_bt_main.h: implementa las rutinas de inicialización y activación de la pila Bluedroid.
  • esp_gap_ble_api.h: implementa la configuración GAP (parámetros de anuncios y conexión).
  • esp_gatts_api.h: implementa la configuración del servidor GATT (por ejemplo, la creación de servicios y características).

La tabla de servicios

El fichero de cabecera gatts_table_creat_demo.h contiene una enumeración de los servicios y características deseadas:

enum
{
    IDX_SVC,
    IDX_CHAR_A,
    IDX_CHAR_VAL_A,
    IDX_CHAR_CFG_A,

    IDX_CHAR_B,
    IDX_CHAR_VAL_B,

    IDX_CHAR_C,
    IDX_CHAR_VAL_C,

    HRS_IDX_NB,
};

Los elementos de la anterior estructura se han incluido en el mismo orden que los atributos del Heart Rate Profile, comenzando con el servicio, seguido por las características del mismo. Además, la característica Heart Rate Measurement dispone de configuración propia (Client Characteristic Configuration, o CCC), un descriptor que describe si la característica tiene las notificaciones activas. Todos estos índices pueden utilizarse para identificar a cada elemento a la hora de crear la tabla de atributos:

  • IDX_SVC: índice del servicio Heart Rate
  • IDX_CHAR_A: índice de la definición de la característica Heart Rate Measurement
  • IDX_CHAR_VAL_A: índice del valor de la característica Heart Rate Measurement
  • IDX_CHAR_CFG_A: índice del descriptor de característica Client Configuration Characteristic (CCC) de la característica Heart Rate Measurement (permite configurar notificaciones por cambio en el valor de la característica)
  • IDX_CHAR_B: ínidce de la declaración de característica Heart Rate Body Sensor Location
  • IDX_CHAR_VAL_B: índice del valor de la característica Heart Rate Body Sensor Location
  • IDX_CHAR_C: índice de la declaración de característica Heart Rate Control Point
  • IDX_CHAR_VAL_C: índice del valor de la característica Heart Rate Control Point
  • IDX_NB: Número de elementos en la tabla

Punto de entrada

El punto de entrada de la aplicación (app_main()) se implementa como sigue:

void app_main(void)
{
    esp_err_t ret;

    /* Initialize NVS. */
    ret = nvs_flash_init();
    if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
        ESP_ERROR_CHECK(nvs_flash_erase());
        ret = nvs_flash_init();
    }
    ESP_ERROR_CHECK( ret );

    ESP_ERROR_CHECK(esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT));

    esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
    ret = esp_bt_controller_init(&bt_cfg);
    if (ret) {
        ESP_LOGE(GATTS_TABLE_TAG, "%s enable controller failed: %s", __func__, esp_err_to_name(ret));
        return;
    }

    ret = esp_bt_controller_enable(ESP_BT_MODE_BLE);
    if (ret) {
        ESP_LOGE(GATTS_TABLE_TAG, "%s enable controller failed: %s", __func__, esp_err_to_name(ret));
        return;
    }

    ret = esp_bluedroid_init();
    if (ret) {
        ESP_LOGE(GATTS_TABLE_TAG, "%s init bluetooth failed: %s", __func__, esp_err_to_name(ret));
        return;
    }

    ret = esp_bluedroid_enable();
    if (ret) {
        ESP_LOGE(GATTS_TABLE_TAG, "%s enable bluetooth failed: %s", __func__, esp_err_to_name(ret));
        return;
    }

    ret = esp_ble_gatts_register_callback(gatts_event_handler);
    if (ret){
        ESP_LOGE(GATTS_TABLE_TAG, "gatts register error, error code = %x", ret);
        return;
    }

    ret = esp_ble_gap_register_callback(gap_event_handler);
    if (ret){
        ESP_LOGE(GATTS_TABLE_TAG, "gap register error, error code = %x", ret);
        return;
    }

    ret = esp_ble_gatts_app_register(ESP_APP_ID);
    if (ret){
        ESP_LOGE(GATTS_TABLE_TAG, "gatts app register error, error code = %x", ret);
        return;
    }

    esp_err_t local_mtu_ret = esp_ble_gatt_set_local_mtu(500);
    if (local_mtu_ret){
        ESP_LOGE(GATTS_TABLE_TAG, "set local  MTU failed, error code = %x", local_mtu_ret);
    }
}

Las acciones realizadas por esta función de entrada se describen en los siguientes apartados.

Inicialización de la flash para almacenamiento no volátil

La función principal procede inicializando el almacenamiento no volátil, para almacenar los parámetros necesarios en memoria flash:

ret = nvs_flash_init();

Inicialización y configuración de BLE

A continuación la función principal inicializa el controlador Bluetooth, creando en primer lugar una estructura de configuración para tal fin de tipo esp_bt_controller_config_t con valores por defecto dictados por la macro BT_CONTROLLER_INIT_CONFIG_DEFAULT().

El controlador Bluetooth implementa el Host Controller Interface (HCI), la capa de enlace y la capa física BLE; es, por tanto, transparente para el programador. La configuración incluye el tamaño de pila reservado al controlador, prioridad y baudios para la transmisión. Con estas configuraciones, el controlador puede ser inicializado y activado con la función esp_bt_controller_init():

esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
ret = esp_bt_controller_init(&bt_cfg);

Una vez inicializado el controlador se activa el modo BLE:

ret = esp_bt_controller_enable(ESP_BT_MODE_BLE);

Existen cuatro modos de funcioinamiento del controlador Bluetooth:

  1. ESP_BT_MODE_IDLE: Bluetooth no funcional
  2. ESP_BT_MODE_BLE: Modo BLE
  3. ESP_BT_MODE_CLASSIC_BT: Modo BT Clásico
  4. ESP_BT_MODE_BTDM: Modo Dual (BLE + BT Clásico)

Tras la inicialización del controlador Bluetooth, se inicializa y activa la pila Bluedroid (que incluye APIs tanto para BLE como para Bluetooth Clásico):

ret = esp_bluedroid_init();
ret = esp_bluedroid_enable();

La pila Bluetooth está, a partir de este punto, lista para funcionar, pero todavía no se ha implementado ninguna lógica de aplicación. Dicha funcionalidad se define con el clásico mecanismo basado en eventos, que pueden ser emitidos, por ejemplo, cuando otro dispositivo intenta leer o escribir parámetros o establecer una conexión.

Existen dos gestores de eventos para BLE: los manejadores (handlers) GAP y GATT. La aplicación necesita registrar una función de callback para cada uno de ellos, que será la encargada de tratar los eventos correspondientes a estos servicios.

esp_ble_gatts_register_callback(gatts_event_handler);
esp_ble_gap_register_callback(gap_event_handler);

En la aplicación de ejemplo estos callbacks son las funciones gatts_event_handler() y gap_event_handler(), que manejarán los eventos emitidos por la pila BLE hacia la aplicación.

Perfiles de aplicación (Application profiles)

Como se ha dicho, el objetivo es implementar un perfil de aplicación para el servicio Heart Rate. Un perfil de aplicación es un mecanismo que permite agrupar funcionalidad diseñada para ser utilizada por un cliente de la aplicación, por ejemplo, una aplicación móvil. Un mismo servidor puede implementar uno o más perfiles de aplicación (aplicaciones en el mundo BLE).

El Identificador de Perfil de Aplicación (Application Profile ID) es un valor seleccionable por el usuario para identificar cada perfil; su uso se reduce al registro del perfil en la pila Bluetooth. En el ejemplo, el ID es 0x55:

#define PROFILE_NUM                  1
#define PROFILE_APP_IDX              0
#define ESP_APP_ID                   0x55

Los perfiles se almacenan en el array heart_rate_profile_tab. Al haber un único perfil en el ejemplo, sólo se almacena un elemento en el array, con índice 0 (tal y como se define en PROFILE_APP_IDX). Además, es necesario inicializar la función de callback manejadora de los eventos del perfil. Cada aplicación en el servidor GATT utiliza una interfaz diferenciada, representada por el parámetro gatts_if. Para la inicialización, este parámetro se iguala a ESP_GATT_IF_NONE; cuando más adelante se registre la aplicación, el parámetro gatts_if se actualizará con la interfaz generada automáticamente por la pila Bluetooth.

/* One gatt-based profile one app_id and one gatts_if, this array will store the gatts_if returned by ESP_GATTS_REG_EVT */
static struct gatts_profile_inst heart_rate_profile_tab[PROFILE_NUM] = {
    [PROFILE_APP_IDX] = {
        .gatts_cb = gatts_profile_event_handler,
        .gatts_if = ESP_GATT_IF_NONE,       /* Not get the gatt_if, so initial is ESP_GATT_IF_NONE */
    },
};

La estructura gatts_profile_inst completa presenta los siguientes campos (no todos se usan en el ejemplo):

struct gatts_profile_inst {
    esp_gatts_cb_t gatts_cb;
    uint16_t gatts_if;
    uint16_t app_id;
    uint16_t conn_id;
    uint16_t service_handle;
    esp_gatt_srvc_id_t service_id;
    uint16_t char_handle;
    esp_bt_uuid_t char_uuid;
    esp_gatt_perm_t perm;
    esp_gatt_char_prop_t property;
    uint16_t descr_handle;
    esp_bt_uuid_t descr_uuid;
};

El registro de la aplicación tiene lugar en la función app_main(), utilizando la función esp_ble_gatts_app_register():

esp_ble_gatts_app_register(ESP_APP_ID);

Registro de aplicación

El evento de registro de aplicación ESP_GATTS_REG_EVT es el primero que se generará en la vida del programa. Este evento es tratado por el callback registrado como manejador de eventos del servidor GATT, que en nuestro ejemplo lo termina delegando en el callback asociado al perfil de aplicación (gatts_profile_event_handler):

static void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param)
{
    ESP_LOGI(GATTS_TABLE_TAG, "EVT %d, gatts if %d\n", event, gatts_if);

    /* If event is register event, store the gatts_if for each profile */
    if (event == ESP_GATTS_REG_EVT) {
        if (param->reg.status == ESP_GATT_OK) {
            heart_rate_profile_tab[HEART_PROFILE_APP_IDX].gatts_if = gatts_if;
        } else {
            ESP_LOGI(GATTS_TABLE_TAG, "Reg app failed, app_id %04x, status %d\n",
                    param->reg.app_id,
                    param->reg.status);
            return;
        }
    }

    do {
        int idx;
        for (idx = 0; idx < HEART_PROFILE_NUM; idx++) {
            if (gatts_if == ESP_GATT_IF_NONE || /* ESP_GATT_IF_NONE, not specify a certain gatt_if, need to call every profile cb function */
            gatts_if == heart_rate_profile_tab[idx].gatts_if) {
                if (heart_rate_profile_tab[idx].gatts_cb) {
                    heart_rate_profile_tab[idx].gatts_cb(event, gatts_if, param);
                }
            }
        }
    } while (0);
}

Los parámetros asociados al evento son:

esp_gatt_status_t status;    /* Operation status */
uint16_t app_id;             /* Application ID */
esp_gatt_if_t gatts_if;      /* Interfaz GATTS asignada por la pila BLE */

A partir de ese momento deberá usarse la interfaz gatts_if para operar, por lo que la función registra esta interfaz en la tabla de descripción del perfil de aplicación (heart_rate_profile_tab).

Finalmente, la función delega el evento en el callback registrado para tratar los eventos asociados al perfil (gatts_profile_event_handler).

Parámetros GAP

Como hemos visto arriba, el evento ESP_GATTS_REG_EVT es delegado en la función gatss_profile_event_handler por el manejador de eventos del servidor GATT para completar su procesamiento:

static void gatts_profile_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param)
{
    switch (event) {
        case ESP_GATTS_REG_EVT:{
            esp_err_t set_dev_name_ret = esp_ble_gap_set_device_name(SAMPLE_DEVICE_NAME);
            if (set_dev_name_ret){
                ESP_LOGE(GATTS_TABLE_TAG, "set device name failed, error code = %x", set_dev_name_ret);
            }
    #ifdef CONFIG_SET_RAW_ADV_DATA
            esp_err_t raw_adv_ret = esp_ble_gap_config_adv_data_raw(raw_adv_data, sizeof(raw_adv_data));
            if (raw_adv_ret){
                ESP_LOGE(GATTS_TABLE_TAG, "config raw adv data failed, error code = %x ", raw_adv_ret);
            }
            adv_config_done |= ADV_CONFIG_FLAG;
            esp_err_t raw_scan_ret = esp_ble_gap_config_scan_rsp_data_raw(raw_scan_rsp_data, sizeof(raw_scan_rsp_data));
            if (raw_scan_ret){
                ESP_LOGE(GATTS_TABLE_TAG, "config raw scan rsp data failed, error code = %x", raw_scan_ret);
            }
            adv_config_done |= SCAN_RSP_CONFIG_FLAG;

            ...
    #endif
            esp_err_t create_attr_ret = esp_ble_gatts_create_attr_tab(gatt_db, gatts_if, HRS_IDX_NB, SVC_INST_ID);
            if (create_attr_ret){
                ESP_LOGE(GATTS_TABLE_TAG, "create attr table failed, error code = %x", create_attr_ret);
            }
        }
            break;

            ...
}

Esta función utiliza el evento para configurar parámetros GAP (de anuncio). Las funciones asociadas son:

  • esp_ble_gap_set_device_name(): utilizada para establecer el nombre del dispositivo anunciado.
  • esp_ble_gap_config_adv_data_raw(): usada para configurar datos estándar de anuncio.

Como podemos ver en el código de arriba, se comienza estableciendo el nombre del dispositivo:

    case ESP_GATTS_REG_EVT:{
        esp_err_t set_dev_name_ret = esp_ble_gap_set_device_name(SAMPLE_DEVICE_NAME);
        if (set_dev_name_ret){
            ESP_LOGE(GATTS_TABLE_TAG, "set device name failed, error code = %x", set_dev_name_ret);
        }

A continuación se configuran los datos de anuncio. La función esp_ble_gap_config_adv_data_raw() toma un puntero a un array de bytes con los datos de anuncio. Cada dato anunciado se compone de:

  • Campo longitud: indica el número de bytes que ocupa el dato, sin contar el campo longitud (es decir, cuantos bytes vienen después del campo longitud)
  • Campo tipo: indica el tipo de dato según el documento Bluetooth Assigned Numbers Document
  • Campo datos: los bytes del dato anunciado

Podemos ver que en nuestro ejemplo el anuncio envía:

  • Flags: LE General Discoverable Mode y BR/EDR not supported
  • TX Power Level: 1 byte en complemento a 2, en dBm (-127 a 127 dBm). Se puede usar para determinar la potencia perdida en la transmisión, calculando la diferencia entre la potencia transmitida anunciada y el RSSI en la recepción. En nuestro ejemplo -21 dBm.
  • Complete 16-bit Service UUIDs: lista de UUIDs de 16 bits de los servicios que ofrece el dispositivo.
  • Complete Local Name: el nombre local completo asignado al dispositivo.

La carga total del paquete de anuncio (payload) puede ser como máximo de 31 bytes (en nuestro ejemplo el anuncio ocupa 26 bytes). Para poder enviar más datos (otros 31 bytes) se puede configurar un scan response, lo que hará que los anuncios enviados sean de tipo scannable. El scan response se puede configurar usando la función esp_ble_gap_config_scan_rsp_data_raw().

    //config scan response data
    ret = esp_ble_gap_config_adv_data(&scan_rsp_data);
    if (ret){
        ESP_LOGE(GATTS_TABLE_TAG, "config scan response data failed, error code = %x", ret);
    }
    adv_config_done |= SCAN_RSP_CONFIG_FLAG;

Al final del tratamiento del evento de registro se inicializa la tabla del servidor GATT. En nuestro ejemplo, los atributos de la tabla se pasan a través del array gatt_db, indicándose la interfaz BLE a utilizar (gatts_if), el número de entradas en el array (HRS_IDX_NB) y la instancia de ese servicio (SVC_INST_ID).

    esp_err_t create_attr_ret = esp_ble_gatts_create_attr_tab(gatt_db, gatts_if, HRS_IDX_NB, SVC_INST_ID);
    if (create_attr_ret){
        ESP_LOGE(GATTS_TABLE_TAG, "create attr table failed, error code = %x", create_attr_ret);
    }

Estructura de la tabla GATT

Como hemos visto arriba, la tabla GATT se inicializa en el evento de registro de aplicación, usando la función esp_ble_gatts_create_attr_tab(). Esta función toma como argumento un array de estructuras de tipo esp_gatts_attr_db_t, indexable con los valores del enumerado definido en el fichero gatts_table_creat_demo.h.

Las estructuras esp_gatts_attr_db_t tienen dos miembros:

esp_attr_control_t    attr_control;       /* The attribute control type */
esp_attr_desc_t       att_desc;           /* The attribute type */
  • attr_control es el parámetro de autorespuesta, típicamente fijado a ESP_GATT_AUTO_RSP para permitir que la pila BLE reponda automáticamente a los mensajes de lectura o escritura cuando dichos eventos son recibidos. Una opción alternativa es ESP_GATT_RSP_BY_APP que permite respuestas manuales utilizando la función esp_ble_gatts_send_response().

  • att_desc es la descripción del atributo, una estructura con los siguientes campos:

uint16_t uuid_length;      /* UUID length */
uint8_t  *uuid_p;          /* UUID value */
uint16_t perm;             /* Attribute permission */
uint16_t max_length;       /* Maximum length of the element */
uint16_t length;           /* Current length of the element */
uint8_t  *value;           /* Element value array */

Por ejemplo, el primer elemento de la tabla en el ejemplo es el atributo de servicio:

    // Service Declaration
    [IDX_SVC] = {
        {ESP_GATT_AUTO_RSP},
        {
            ESP_UUID_LEN_16,
            (uint8_t *)&primary_service_uuid,
            ESP_GATT_PERM_READ,
            sizeof(uint16_t),
            sizeof(GATTS_SERVICE_UUID_TEST),
            (uint8_t *)&GATTS_SERVICE_UUID_TEST
        }
    },

Los valores de inicialización son:

  • [IDX_SVC]: index del atributo dentro de la tabla GATT.
  • ESP_GATT_AUTO_RSP: configuración de respuesta automática, fijada en este caso a respuesta automática por parte de la pila BLE.
  • ESP_UUID_LEN_16: longitud del UUID del tipo de atributo (16 bits).
  • (uint8_t *)&primary_service_uuid: UUID para identificar al atributo como servicio primario (0x2800).
  • ESP_GATT_PERM_READ: permisos del atributo, en este caso de solo lectura.
  • sizeof(uint16_t): longitud máxima del valor del atributo (16 bits).
  • sizeof(GATTS_SERVICE_UUID_TEST): longitud del valor del atributo, en este caso 16 bits (fijado por el tamaño de la variable GATTS_SERVICE_UUID_TEST).
  • (uint8_t *)&GATTS_SERVICE_UUID_TEST: valor del atributo, en este caso el UUID real del servicio (fijado en GATTS_SERVICE_UUID_TEST).

El resto de atributos se inicializan de forma similar. Algunos atributos también tienen activa la propiedad NOTIFY, que se establece vía &char_prop_read_write_notify. La tabla completa se inicializa como sigue:

/* Full Database Description - Used to add attributes into the database */
static const esp_gatts_attr_db_t gatt_db[HRS_IDX_NB] =
{
    // Service Declaration
    [IDX_SVC]        =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&primary_service_uuid, ESP_GATT_PERM_READ,
      sizeof(uint16_t), sizeof(GATTS_SERVICE_UUID_TEST), (uint8_t *)&GATTS_SERVICE_UUID_TEST}},

    /* Characteristic Declaration */
    [IDX_CHAR_A]     =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&character_declaration_uuid, ESP_GATT_PERM_READ,
      CHAR_DECLARATION_SIZE, CHAR_DECLARATION_SIZE, (uint8_t *)&char_prop_read_write_notify}},

    /* Characteristic Value */
    [IDX_CHAR_VAL_A] =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&GATTS_CHAR_UUID_TEST_A, ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE,
      GATTS_DEMO_CHAR_VAL_LEN_MAX, sizeof(char_value), (uint8_t *)char_value}},

    /* Client Characteristic Configuration Descriptor */
    [IDX_CHAR_CFG_A]  =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&character_client_config_uuid, ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE,
      sizeof(uint16_t), sizeof(heart_measurement_ccc), (uint8_t *)heart_measurement_ccc}},

    /* Characteristic Declaration */
    [IDX_CHAR_B]      =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&character_declaration_uuid, ESP_GATT_PERM_READ,
      CHAR_DECLARATION_SIZE, CHAR_DECLARATION_SIZE, (uint8_t *)&char_prop_read}},

    /* Characteristic Value */
    [IDX_CHAR_VAL_B]  =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&GATTS_CHAR_UUID_TEST_B, ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE,
      GATTS_DEMO_CHAR_VAL_LEN_MAX, sizeof(char_value), (uint8_t *)char_value}},

    /* Characteristic Declaration */
    [IDX_CHAR_C]      =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&character_declaration_uuid, ESP_GATT_PERM_READ,
      CHAR_DECLARATION_SIZE, CHAR_DECLARATION_SIZE, (uint8_t *)&char_prop_write}},

    /* Characteristic Value */
    [IDX_CHAR_VAL_C]  =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&GATTS_CHAR_UUID_TEST_C, ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE,
      GATTS_DEMO_CHAR_VAL_LEN_MAX, sizeof(char_value), (uint8_t *)char_value}},

};

Inicialización del servicio

Cuando la tabla se crea, se emite un evento de tipo ESP_GATTS_CREAT_ATTR_TAB_EVT, tratado por el manejador de eventos del perfil. Este evento tiene los siguientes parámetros asociados:

esp_gatt_status_t status;    /* Operation status */
esp_bt_uuid_t svc_uuid;      /* Service UUID type */
uint16_t num_handle;         /* The number of the attribute handles which have been added to the GATT Service table */
uint16_t *handles;           /* The handles which have been added to the table */

Nuestro código de ejemplo utiliza este evento para comprobar que el tamaño de la tabla creada es igual al número de elementos en la enumeración (HRS_IDX_NB). Si la tabla se creó correctamente, se copian en la tabla heart_rate_handle_table los handles (IDs internos) asignados por el servidor GATT a los atributos, y finalmente se inicializa el servicio utilizando la función esp_ble_gatts_start_service():

case ESP_GATTS_CREAT_ATTR_TAB_EVT:
    if (param->add_attr_tab.status != ESP_GATT_OK){
        ESP_LOGE(GATTS_TABLE_TAG, "create attribute table failed, error code=0x%x", param->add_attr_tab.status);
    }
    else if (param->add_attr_tab.num_handle != HRS_IDX_NB){
        ESP_LOGE(GATTS_TABLE_TAG, "create attribute table abnormally, num_handle (%d) \
                doesn't equal to HRS_IDX_NB(%d)", param->add_attr_tab.num_handle, HRS_IDX_NB);
    }
    else {
        ESP_LOGI(GATTS_TABLE_TAG, "create attribute table successfully, the number handle = %d\n",param->add_attr_tab.num_handle);
        memcpy(heart_rate_handle_table, param->add_attr_tab.handles, sizeof(heart_rate_handle_table));
        esp_ble_gatts_start_service(heart_rate_handle_table[IDX_SVC]);
    }
    break;

Estos handles pueden usarse en cualquier punto de la aplicación para operar sobre los atributos.

El manejador de eventos GAP

Como hemos visto antes, en el procesamiento del evento de registro de aplicación se establecen los datos de anuncio. Cuando se complete esta inicialización, la pila BLE emitirá un evento de tipo ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT, que será manejado por el callback registrado como GAP handler. Si se ha configurado un anuncio scannable y una respuesta al escaneado (en nuestro ejemplo se ha configurado), la pila BLE emitirá también un evento de tipo ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT. El manejador del código de ejemplo espera a que se produzcan estos dos eventos para comenzar con el proceso de anuncio, utilizando la función esp_ble_gap_start_advertising():

static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param)
{
    ESP_LOGE(GATTS_TABLE_TAG, "GAP_EVT, event %d\n", event);

    switch (event) {
    #ifdef CONFIG_SET_RAW_ADV_DATA
        case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT:
            adv_config_done &= (~ADV_CONFIG_FLAG);
            if (adv_config_done == 0){
                esp_ble_gap_start_advertising(&adv_params);
            }
            break;
        case ESP_GAP_BLE_SCAN_RSP_DATA_RAW_SET_COMPLETE_EVT:
            adv_config_done &= (~SCAN_RSP_CONFIG_FLAG);
            if (adv_config_done == 0){
                esp_ble_gap_start_advertising(&adv_params);
            }
            break;
    #else
        ...
    #endif
    }
}

La función de inicio de anuncios toma una estructura de tipo esp_ble_adv_params_t con los parámetros de anuncio requeridos.

/// Advertising parameters
typedef struct {
    uint16_t adv_int_min; /* Minimum advertising interval for undirected and low
                           * duty cycle directed advertising.
                           * Range: 0x0020 to 0x4000
                           * Default: N = 0x0800 (1.28 second)
                           * Time = N * 0.625 msec
                           * Time Range: 20 ms to 10.24 sec */
    uint16_t adv_int_max; /* Maximum advertising interval for undirected and low
                           * duty cycle directed advertising.
                           * Range: 0x0020 to 0x4000
                           * Default: N = 0x0800 (1.28 second)
                           * Time = N * 0.625 msec
                           * Time Range: 20 ms to 10.24 sec */
    esp_ble_adv_type_t adv_type;            /* Advertising type */
    esp_ble_addr_type_t own_addr_type;      /* Owner bluetooth device address type */
    esp_bd_addr_t peer_addr;                /* Peer device bluetooth device address */
    esp_ble_addr_type_t peer_addr_type;     /* Peer device bluetooth device address type */
    esp_ble_adv_channel_t channel_map;      /* Advertising channel map */
    esp_ble_adv_filter_t adv_filter_policy; /* Advertising filter policy */
} esp_ble_adv_params_t;

Nótese como esp_ble_gap_config_adv_data_raw() (en gatts_profile_event_handler) configura los datos enviados en el anuncio, mientras que esp_ble_gap_start_advertising() pone al dispositivo en modo advertising, haciendo que el servidor comience a enviar los anuncios. Los parámetros de anuncio configuran cómo y cuándo enviarlos. En nuestro ejemplo:

static esp_ble_adv_params_t adv_params = {
    .adv_int_min         = 0x20,
    .adv_int_max         = 0x40,
    .adv_type            = ADV_TYPE_IND,
    .own_addr_type       = BLE_ADDR_TYPE_PUBLIC,
    .channel_map         = ADV_CHNL_ALL,
    .adv_filter_policy   = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY,
};

Estos parámetros configuran el intervalo de anuncio entre 20 ms y 40 ms. El anuncio es de tipo ADV_TYPE_IND (tipo genérico), destinado a cualquier dispositivo central y anuncia que el servidor GATT es conectable. A su vez, el tipo de dirección utilizada por el servidor GATT es su MAC Bluetooth pública, utiliza todos los canales de anuncio y, finalmente, permite peticiones de escaneo y conexión por parte de cualquier dispositivo central.

Si el proceso de anuncio se inició correctamente, se emitirá un evento de tipo ESP_GAP_BLE_ADV_START_COMPLETE_EVT, que en este ejemplo se utiliza para comprobar si el estado es realmente anunciando u otro, en cuyo caso se emitirá un mensaje de error:

...
    case ESP_GAP_BLE_ADV_START_COMPLETE_EVT:
        /* advertising start complete event to indicate advertising start successfully or failed */
        if (param->adv_start_cmpl.status != ESP_BT_STATUS_SUCCESS) {
            ESP_LOGE(GATTS_TABLE_TAG, "advertising start failed");
        }else{
            ESP_LOGI(GATTS_TABLE_TAG, "advertising start successfully");
        }
        break;
...

Interacción a través de un cliente GATT

Existen multitud de herramientas que permiten gestionar la conexión al servidor GATT. En Linux, utilizaremos hcitool y gatttool; en Windows, puedes utilizar una herramienta llamada Bluetooth LE Explorer que implementa, aunque de forma gráfica, la misma funcionalidad. De la misma forma, en macOS puedes usar LightBlue.

Nota (en caso de trabajar con máquina virtual)

Para desarrollar la práctica, deberás hacer visible en la máquina virtual el controlador Bluetooth del equipo que la hospeda (tu portátil o el equipo de laboratorio).

Uso de hcitool y gatttool en modo cliente

Escaneando dispositivos disponibles: hcitool

hcitool es una herramienta de línea de comandos que permite gestionar la interfaz Bluetooth del equipo en el que se ejecuta. En nuestro caso, necesitaremos determinar la dirección MAC Bluetooth de nuestro servidor. Para ello, en primer lugar, realizaremos un escaneado de los dispositivos BLE disponibles en el entorno con:

sudo hcitool lescan

Si todo ha ido bien, se mostrará una línea por dispositivo BLE disponible y en fase de anuncio. Entre ellos, deberemos encontrar nuestro dispositivo. Apuntaremos su dirección MAC para poder usarla de aquí en adelante.

Ejercicio 1

Edita el fichero main/gatts_table_creat_demo.c y modifica el nombre de dispositivo que se enviará en cada anuncio emitido en la fase de advertising. A continuación, compila y flashea el ejemplo, y comienza una sesión de escaneado de dispositivos BLE. Deberás observar tu dispositivo en una de las líneas. Anota su dirección MAC.

Interactuando con el servidor GATT: gatttool

Una vez obtenida la dirección MAC Bluetooth del dispositivo, deberemos proceder en dos fases. La primera de ellas es el emparejado al dispostivo desde tu consola. La segunda, la interacción con la tabla GATT. En ambos casos, se utilizará la herramienta gatttool desde la línea de comandos.

Para comenzar una sesión gatttool, invocaremos a la herramienta en modo interactivo, utilizando la orden:

gatttool -b MAC -I

Esto abrirá una consola interactiva a la espera de las órdenes correspondientes.

Para realizar el emparejamiento, y considerando que conocemos la MAC Bluetooth, utilizaremos la orden connect. Si todo ha ido bien, deberemos observar un cambio en el color del prompt y un mensaje Connection successful. En este punto, observa como en la salida de depuración del ESP32 se muestran los mensajes correspondientes al proceso de emparejamiento.

Ejercicio 2

Documenta el proceso de conexión con gatttool en informe de la práctica.

Desde la terminal de gatttool, puedes ejecutar en cualquier momento la orden help para obtener ayuda (en forma de lista de comandos disponibles):

gatttool -b 24:6F:28:36:60:B2 -I
[24:6F:28:36:60:B2][LE]> connect
Attempting to connect to 24:6F:28:36:60:B2
Connection successful
[24:6F:28:36:60:B2][LE]> help
help                                           Show this help
exit                                           Exit interactive mode
quit                                           Exit interactive mode
connect         [address [address type]]       Connect to a remote device
disconnect                                     Disconnect from a remote device
primary         [UUID]                         Primary Service Discovery
included        [start hnd [end hnd]]          Find Included Services
characteristics [start hnd [end hnd [UUID]]]   Characteristics Discovery
char-desc       [start hnd] [end hnd]          Characteristics Descriptor Discovery
char-read-hnd   <handle>                       Characteristics Value/Descriptor Read by handle
char-read-uuid  <UUID> [start hnd] [end hnd]   Characteristics Value/Descriptor Read by UUID
char-write-req  <handle> <new value>           Characteristic Value Write (Write Request)
char-write-cmd  <handle> <new value>           Characteristic Value Write (No response)
sec-level       [low | medium | high]          Set security level. Default: low
mtu             <value>                        Exchange MTU for GATT/ATT

Continuamos consultando la lista de características del servidor GATT.

Ejercicio 3

Mediante el comando correspondiente, consulta y anota las características disponibles en tu servidor GATT.

Una de estas características será de crucial interés, ya que nos permitirá acceder a la medición del ritmo cardíaco, así como a la configuración de notificaciones sobre dicho valor. Para determinar cuál de las líneas es la que nos interesa, observa el valor de UUID devuelto para cada una de ellas y compara con los UUIDs utilizados en el código.

Para interactuar con el valor de dicha característica, necesitamos conocer su ID interno (handle). Dicho handle se muestra, para cada línea, en el campo char value handle.

Ejercicio 4

Encuentra el handle que nos permitirá leer el Heart Rate Measurement Value. ¿Con qué comando podríamos leer su valor? Documenta el proceso en tu informe explicando cómo saber el handle que se debe usar.

Ejercicio 5

Obtén los datos de lectura de la característica de medición del valor de monitorización de ritmo cardíaco. ¿Cuáles son? Modifica el código para almacenar otros datos diferentes, recompila y vuelve a flashear. ¿Han variado?

Ejercicio 6

Escribe ahora en el valor de la característica usando gatttool. Documenta el proceso en tu informe y muestra una captura del valor actualizado.

Escribiremos a continuación en la característica de configuración (CCC) del servicio de montorización. Para ello, utilizaremos el handle siguiente al utilizado anteriormente. Esto es, si se nos devolvió, por ejemplo, 0x0001 para el valor de monitorización, el valor de configuración será 0x0002.

Ejercicio 7

Intenta ahora escribir en la característica de configuración el valor 0x0100 (valor especial para activar notificaciones). Muestra una captura del valor actualizado.

Como habrás observado, es posible leer desde el valor de monitorización y escribir en el valor de configuración. Utilizaremos esta última característica para configurar las notificaciones sobre el valor de monitorización. De este modo, cada vez que el valor cambie, el servidor enviará automáticamente el nuevo valor al cliente.

Para ello, necesitamos modificar algunas partes del código. Específicamente, necesitaremos:

1) Crear una nueva tarea que, periódicamente, modifique el valor de monitorización del ritmo cardíaco (leyéndolo desde un sensor si está disponible, o, en nuestro caso, generando un valor aleatorio). Dicha tarea consistirá en un bucle infinito que, en cualquier caso, sólo enviará datos al cliente si la notificación está activa, con un intervalo de envío de un segundo:

static void publish_data_task(void *pvParameters)
{
    while (1) {
        ESP_LOGI("APP", "Sending data..."); 

        // Paso 1: actualizo valor...

        // Paso 2: si las notificaciones están activas envío datos...

        // Paso 3: duermo un segundo...
    }
}

Esta rutina deberá crearse en respuesta al evento de conexión por parte de un cliente (ESP_GATTS_CONNECT_EVT), utilizando, por ejemplo, la invocación a:

xTaskCreate(&publish_data_task, "publish_data_task", 4096, NULL, 5, NULL);

2) La actualización del valor, realizada periódicamente y de forma aleatoria, modificará el segundo byte de la variable char_value, tomando un valor aleatorio entre 0 y 255.

3) La comprobación de la activación o no de la notificación se puede realizar especificando una nueva variable global que mantenga si se han activado las notificaciones (estudiar el manejo del evento ESP_GATTS_WRITE_EVT).

4) Para enviar la notificación, utilizaremos la siguiente función:

esp_ble_gatts_send_indicate(heart_rate_profile_tab[0].gatts_if, 
                                      heart_rate_profile_tab[0].conn_id,
                                      heart_rate_handle_table[IDX_CHAR_VAL_A],
                                      sizeof(char_value), char_value, false);

Tras modificar el código, recompilar y flashear, recuerda volver a activar las notificaciones (ejercicio 7).

Ejercicio 8

Modifica el firmware original para que, periódicamente (cada segundo) notifique el valor de ritmo cardíaco a los clientes conectados.

Entrega el código modificado e incluye en tu informe capturas de pantalla que demuestren que un cliente gatttool suscrito a notificaciones recibe cada segundo la actualización del ritmo cardíaco por parte del sensor.