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
gatttoolpara 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:
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 RateIDX_CHAR_A: índice de la definición de la característica Heart Rate MeasurementIDX_CHAR_VAL_A: índice del valor de la característica Heart Rate MeasurementIDX_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 LocationIDX_CHAR_VAL_B: índice del valor de la característica Heart Rate Body Sensor LocationIDX_CHAR_C: índice de la declaración de característica Heart Rate Control PointIDX_CHAR_VAL_C: índice del valor de la característica Heart Rate Control PointIDX_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:
ESP_BT_MODE_IDLE: Bluetooth no funcionalESP_BT_MODE_BLE: Modo BLEESP_BT_MODE_CLASSIC_BT: Modo BT ClásicoESP_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_controles el parámetro de autorespuesta, típicamente fijado aESP_GATT_AUTO_RSPpara permitir que la pila BLE reponda automáticamente a los mensajes de lectura o escritura cuando dichos eventos son recibidos. Una opción alternativa esESP_GATT_RSP_BY_APPque permite respuestas manuales utilizando la funciónesp_ble_gatts_send_response(). -
att_desces 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.