Lab 4. Bluetooth Low Energy (BLE)

Goals

GATT Sever Implementation

Introduction

In this lab assignmet, we will deploy a GATT server using the ESP-IDF API. This API exposes the functionalities of Bluedroid, the Bluetooth stack (including BLE) that provides ESP-IDF for the development of Bluetooth applications.

We will use the example from examples/bluetooth/bluedroid/ble/gatt_server_service_table, which example implements a Bluetooth Low Energy (BLE) Generic Attribute (GATT) Server using a table-like data structure to define the server services and characteristics such as the one shown in the figure below Therefore, it demonstrates a practical way to define the server functionality in one place instead of adding services and characteristics one by one.

This example implements the Heart Rate Profile as defined by the Traditional Profile Specifications.

Table-like data structure representing the Heart Rate Service

We will therefore display three characteristics. Of them, the most important for us will be the heart rate measurement value, with its value (Heart Rate Measurement Value) and its notification settings (Heart Rate Measurement Notification Configuration).

Due to the complexity of the code (at least in its initial part), this document follows the program workflow and breaks down the code in order to make sense of every section and reasoning behind the implementation.

Includes

Let’s start by taking a look at the included headers:

#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"

These includes are required for the FreeRTOS and underlaying system components to run, including logging functionality and a library to store data in non-volatile flash memory. We are interested in bt.h, esp_bt_main.h, esp_gap_ble_api.h and esp_gatts_api.h which expose the BLE APIs required to implement this example.

Service Table

The header file gatts_table_creat_demo.h is where an enumeration of the services and characteristics is created:

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,
};

The enumeration elements are set up in the same order as the Heart Rate Profile attributes, starting with the service followed by the characteristics of that service. In addition, the Heart Rate Measurement characteristic has a Client Characteristic Configuration (CCC) descriptor which is an additional attribute that describes if the characteristic has notifications enabled. The enumeration index can be used to identify each element later when creating the actual attributes table. In summary, the elements are described as follows:

Main Entry Point

The entry point to this example is the app_main() function:

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);
    }
}

The main function starts by initializing the non-volatile storage library in order to be able to save parameters in flash memory.

ret = nvs_flash_init();

BT Controller and Stack Initialization

The main function also initializes the BT controller by first creating a BT controller configuration structure named esp_bt_controller_config_t with default settings generated by the BT_CONTROLLER_INIT_CONFIG_DEFAULT() macro.

The BT controller implements the Host Controller Interface (HCI) on the controller side, the Link Layer (LL) and the Physical Layer (PHY). The BT Controller is invisible to the user applications and deals with the lower layers of the BLE stack. The controller configuration includes setting the BT controller stack size, priority and HCI baud rate. With the settings created, the BT controller is initialized and enabled with the esp_bt_controller_init() function:

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

Next, the controller is enabled in BLE Mode.

ret = esp_bt_controller_enable(ESP_BT_MODE_BLE);

There are four Bluetooth modes supported:

  1. ESP_BT_MODE_IDLE: Bluetooth not running
  2. ESP_BT_MODE_BLE: BLE mode
  3. ESP_BT_MODE_CLASSIC_BT: BT Classic mode
  4. ESP_BT_MODE_BTDM: Dual mode (BLE + BT Classic)

After the initialization of the BT controller, the Bluedroid stack, which includes the common definitions and APIs for both BT Classic and BLE, is initialized and enabled by using:

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

The Bluetooth stack is up and running at this point in the program flow, however the functionality of the application has not been defined yet. The functionality is defined by reacting to events such as what happens when another device tries to read or write parameters and establish a connection.

The two main managers of events are the GAP and GATT event handlers. The application needs to register a callback function for each event handler in order to let the application know which functions are going to handle the GAP and GATT events:

esp_ble_gatts_register_callback(gatts_event_handler);
esp_ble_gap_register_callback(gap_event_handler);

The functions gatts_event_handler() and gap_event_handler() handle all the events that are pushed to the application from the BLE stack.

Application Profiles

This example implements one Application Profile for the Heart Rate Service. An Application Profile is a way to group functionality which is designed to be used by one client application, for example one smartphone mobile app. In this way, different types of profiles can be accommodated in one server.

The Application Profile ID, which is an user-assigned number to identify each profile, is used to register the profile in the stack, in this example the ID is 0x55.

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

The profiles are stored in the heart_rate_profile_tab array. Since there is only one profile in this example, one element is stored in the array with index zero as defined by the PROFILE_APP_IDX. Additionally, the profile event handler callback function is initialized. Each application on the GATT server uses a different interface, represented by the gatts_if parameter. For initialization, this parameter is set to ESP_GATT_IF_NONE, later when the application is registered, the gatts_if parameter is updated with the corresponding interface generated by the stack.

/* 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 */
    },
};

The application registration takes place inside app_main() using the esp_ble_gatts_app_register() function:

esp_ble_gatts_app_register(ESP_HEART_RATE_APP_ID);

Setting GAP Parameters

The register application event is the first one that is triggered during the lifetime of the program. This example uses this event to configure advertising parameters upon registration in the profile event handler. The functions used to achieve this are:

The function used to configure standard Bluetooth Specification advertisement parameters is esp_ble_gap_config_adv_data() which takes a pointer to an esp_ble_adv_data_t structure. The esp_ble_adv_data_t data structure for advertising data has the following definition:

typedef struct {
    bool set_scan_rsp;    /*!< Set this advertising data as scan response or not*/
    bool include_name;    /*!< Advertising data include device name or not */
    bool include_txpower; /*!< Advertising data include TX power */
    int min_interval;     /*!< Advertising data show slave preferred connection min interval */
    int max_interval;     /*!< Advertising data show slave preferred connection max interval */
    int appearance;       /*!< External appearance of device */
    uint16_t manufacturer_len; /*!< Manufacturer data length */
    uint8_t *p_manufacturer_data; /*!< Manufacturer data point */
    uint16_t service_data_len;    /*!< Service data length */
    uint8_t *p_service_data;      /*!< Service data point */
    uint16_t service_uuid_len;    /*!< Service uuid length */
    uint8_t *p_service_uuid;      /*!< Service uuid array point */
    uint8_t flag;         /*!< Advertising flag of discovery mode, see BLE_ADV_DATA_FLAG detail */
} esp_ble_adv_data_t;

In this example, the structure is initialized as follows:

static esp_ble_adv_data_t heart_rate_adv_config = {
    .set_scan_rsp = false,
    .include_name = true,
    .include_txpower = true,
    .min_interval = 0x0006,
    .max_interval = 0x0010,
    .appearance = 0x00,
    .manufacturer_len = 0, //TEST_MANUFACTURER_DATA_LEN,
    .p_manufacturer_data =  NULL, //&test_manufacturer[0],
    .service_data_len = 0,
    .p_service_data = NULL,
    .service_uuid_len = sizeof(heart_rate_service_uuid),
    .p_service_uuid = heart_rate_service_uuid,
    .flag = (ESP_BLE_ADV_FLAG_GEN_DISC | ESP_BLE_ADV_FLAG_BREDR_NOT_SPT),
};

The minimum and maximum slave preferred connection intervals are set in units of 1.25 ms. In this example, the minimum slave preferred connection interval is defined as 0x0006 * 1.25 ms = 7.5 ms and the maximum slave preferred connection interval is initialized as 0x0010 * 1.25 ms = 20 ms.

An advertising payload can be up to 31 bytes of data. It is possible that some of the parameters surpass the 31-byte advertisement packet limit which causes the stack to cut the message and leave some of the parameters out. To solve this, usually the longer parameters are stored in the scan response, which can be configured using the same esp_ble_gap_config_adv_data() function and an additional esp_ble_adv_data_t type structure with the .set_scan_rsp parameter is set to true. Finally, to set the device name the esp_ble_gap_set_device_name() function is used. The registering event handler is shown as follows:

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)
{
    ESP_LOGE(GATTS_TABLE_TAG, "event = %x\n",event);
    switch (event) {
        case ESP_GATTS_REG_EVT:
            ESP_LOGI(GATTS_TABLE_TAG, "%s %d\n", __func__, __LINE__);
            esp_ble_gap_set_device_name(SAMPLE_DEVICE_NAME);
            ESP_LOGI(GATTS_TABLE_TAG, "%s %d\n", __func__, __LINE__);
            esp_ble_gap_config_adv_data(&heart_rate_adv_config);
            ESP_LOGI(GATTS_TABLE_TAG, "%s %d\n", __func__, __LINE__);
...

GAP Event Handler

Once the advertising data have been set, the ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT is triggered and managed by the GAP event handler. Moreover, an ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT is triggered as well if the scan response is also set. Once the configuration of the advertising and scan response data has been set, the handler can use any of these events to start advertising, which is done using the esp_ble_gap_start_advertising() function:

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) {
    case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT:
        esp_ble_gap_start_advertising(&heart_rate_adv_params);
        break;
    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\n");
        }
        break;
    default:
        break;
    }
}

The function to start advertising takes a structure of type esp_ble_adv_params_t with the advertising parameters required.

/// 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;

Note that esp_ble_gap_config_adv_data() configures the data that is advertised to the client and takes an esp_ble_adv_data_t structure, while esp_ble_gap_start_advertising() makes the server to actually start advertising and takes an esp_ble_adv_params_t structure. The advertising data is the information that is shown to the client, while the advertising parameters are the configuration required by the BLE stack to execute.

For this example, the advertisement parameters are initialized as follows:

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

These parameters configure the advertising interval between 20 ms to 40 ms. The advertisement is of type ADV_IND, which is generic, not directed to a particular central device and advertises the server as connectable. The address type is public, uses all channels and allows both scan and connection requests from any central.

If the advertising started successfully, an ESP_GAP_BLE_ADV_START_COMPLETE_EVT event is generated which in this example is used to check if the advertising status is indeed advertising or otherwise print an error message.

...
    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\n");
        }
        break;
...

GATT Event Handlers

When an Application Profile is registered, an ESP_GATTS_REG_EVT event is triggered. The parameters of the ESP_GATTS_REG_EVT are:

esp_gatt_status_t status;    /*!< Operation status */
uint16_t app_id;             /*!< Application id which input in register API */

In addition to the previous parameters, the event also contains the GATT interface assigned by the BLE stack. The event is captured by the gatts_event_handler() which stores the generated interface in the profile table and then forwards it to the corresponding 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);
}

Creating Services and Characteristics with the Attribute Table

The register event is used to create a table of profile attributes by employing the esp_ble_gatts_create_attr_tab() function. This function takes an argument of type esp_gatts_attr_db_t which corresponds to a look up table keyed by the enumeration values defined in the header file.

The esp_gatts_attr_db_t structure has two members:

esp_attr_control_t    attr_control;       /*!< The attribute control type*/
esp_attr_desc_t       att_desc;           /*!< The attribute type*/

The attr_control is the auto-respond parameter which can be set as ESP_GATT_AUTO_RSP to allow the BLE stack to take care of responding messages when read or write events arrive. The other option is ESP_GATT_RSP_BY_APP which allows to manually respond to messages using the esp_ble_gatts_send_response() function.

The att_desc is the attribute description which is made of:

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*/

For example, the first element of the table in this example is the service attribute:

[HRS_IDX_SVC]                       =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&primary_service_uuid, ESP_GATT_PERM_READ,
      sizeof(uint16_t), sizeof(heart_rate_svc), (uint8_t *)&heart_rate_svc}},

The initialization values are:

The rest of the attributes is initialized in the same way. Some attributes also have the NOTIFY property which is set by &char_prop_notify. The complete table structure is initialized as follows:

/// Full HRS Database Description - Used to add attributes into the database
static const esp_gatts_attr_db_t heart_rate_gatt_db[HRS_IDX_NB] =
{
    // Heart Rate Service Declaration
    [HRS_IDX_SVC]                       =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&primary_service_uuid, ESP_GATT_PERM_READ,
      sizeof(uint16_t), sizeof(heart_rate_svc), (uint8_t *)&heart_rate_svc}},

    // Heart Rate Measurement Characteristic Declaration
    [HRS_IDX_HR_MEAS_CHAR]            =
    {{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_notify}},

    // Heart Rate Measurement Characteristic Value
    [HRS_IDX_HR_MEAS_VAL]               =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&heart_rate_meas_uuid, ESP_GATT_PERM_READ,
      HRPS_HT_MEAS_MAX_LEN,0, NULL}},

    // Heart Rate Measurement Characteristic - Client Characteristic Configuration Descriptor
    [HRS_IDX_HR_MEAS_NTF_CFG]           =
    {{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}},

    // Body Sensor Location Characteristic Declaration
    [HRS_IDX_BOBY_SENSOR_LOC_CHAR]  =
    {{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}},

    // Body Sensor Location Characteristic Value
    [HRS_IDX_BOBY_SENSOR_LOC_VAL]   =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&body_sensor_location_uuid, ESP_GATT_PERM_READ,
      sizeof(uint8_t), sizeof(body_sensor_loc_val), (uint8_t *)body_sensor_loc_val}},

    // Heart Rate Control Point Characteristic Declaration
    [HRS_IDX_HR_CTNL_PT_CHAR]          =
    {{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}},

    // Heart Rate Control Point Characteristic Value
    [HRS_IDX_HR_CTNL_PT_VAL]             =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&heart_rate_ctrl_point, ESP_GATT_PERM_WRITE|ESP_GATT_PERM_READ,
      sizeof(uint8_t), sizeof(heart_ctrl_point), (uint8_t *)heart_ctrl_point}},
};

Starting the Service

When the attribute table is created, an ESP_GATTS_CREAT_ATTR_TAB_EVT event is triggered. This event has the following parameters:

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 handle to be added to the gatts database */
uint16_t *handles;           /*!< The number to the handles */

This example uses this event to print information and to check that the size of the created table equals the number of elements in the enumeration HRS_IDX_NB. If the table is correctly created, the attribute handles are copied into the handle table heart_rate_handle_table and the service is started using the esp_ble_gatts_start_service() function:

case ESP_GATTS_CREAT_ATTR_TAB_EVT:{
        ESP_LOGI(GATTS_TABLE_TAG, "The number handle =%x\n",param->add_attr_tab.num_handle);
        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 {
            memcpy(heart_rate_handle_table, param->add_attr_tab.handles, sizeof(heart_rate_handle_table));
            esp_ble_gatts_start_service(heart_rate_handle_table[HRS_IDX_SVC]);
        }
        break;

The handles stored in the handles pointer of the event parameters are numbers that identify each attribute. The handles can be used to know which characteristic is being read or written to, therefore they can be passed around and to upper layers of the application to handle different actions.

Finally, the heart_rate_handle_table contains the Application Profile in the form of a structure with information about the attribute parameters as well as GATT interface, connection ID, permissions and application ID. The profile structure is shown as follows, note that not all members are used in this example:

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;
};

Interaction with the GATT Server

There are many tools that allow you to manage the connection to the GATT server. On Linux, we will use hcitool andgatttool. In Windows, you can use a tool called Bluetooth LE Explorer, that implements, albeit graphically, the same functionality.

For this part of the lab assignment, you have to make visible the bluetooth controller of your host machine (laptop/pc) to the virtual machine used for the course.

Using hcitool and gatttool in client mode

Scanning available devices: hcitool

hcitool is a command line tool that allows you to manage the Bluetooth interface of the computer on which it is running. In our case, We will need to determine the Bluetooth MAC address of our server. To do this, first of all, we will perform a scan of the devices BLE available in the environment using the command:

sudo hcitool lescan

Note

This command will not work if you did not make the bluetooth controller available to the virtual machine.

If all went well, one line per available BLE device in the announcement phase will be displayed. Among them, we must find our device, and annotate its MAC address.

Task

Edit the file main/gatts_table_creat_demo.c and modify the name of your device, which will be announced in each emmited advertisment packet in the advertising phase. You can achieve this by modifying the corresponding field of the structure raw_adv_data. Next, compile and flash the example, and start a session of scanning of BLE devices using the command:

sudo hcitool lescan

You should see your device on one of the output lines. Write down or remember its MAC address.

Interacting with the GATT server: gatttool.

Task 4.1

Write a small pdf report documenting all the steps and Tasks in this section.

Once the Bluetooth MAC address of the device is obtained, we must proceed in two phases. The first one is to pair it to the ESP device. The second, the interaction with the GATT table. In both cases, you will use the gatttool tool from the command line.

To start a gatttool session, we'll invoke the tool in interactive mode, using the command:

gatttool -b MAC -I

This will open an interactive console, waiting for the corresponding commands.

To perform the pairing, and considering that the Bluetooth MAC is already known, we will use the connect command. If everything went well, we should observe a change in the color of the prompt, and the Connection successful message. At this point, see how the debugging output of ESP32 shows the messages corresponding to the pairing process.

From the gatttool terminal, you can run the command help to get help (in the form of a list of available commands):

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

We'll start by looking at the list of GATT server features.

Task

Using the corresponding command (characteristics), consult and write down the features available on your GATT server.

One of these characteristics will be of crucial interest, since it will allow us access, through its UUID, the instant heart rate measurement value, as well as as well as the notification settings on that value. To determine which of the lines is the one that interests us, look at the returned UUID value for each of them, and determine, based on the macro GATTS_CHAR_UUID_TEST_A which one is it.

To interact with this feature, we will need to know its handler, to use it as a parameter in the gatttool commands. This handler is shown, for each line, after the string char value handle.

Task

The handler that allows reading the Heart Rate Value has associated a handler of type character. Write its value down.

To read the value of the characteristic we can use the read command, using the annotated handler as argument (char-read-hnd handler).

Task

Read the heart rate monitoring value characteristic. What do you obtain? You should observe a four-byte return value with value 0x00. These values correspond to those of the char_value variable in your code. Modify them, rebuild the project and flash it on the ESP32. Repeat the read. Did you read the new value?

Task

Now try to write to the characteristic. Use the command char-write-cmd handler value, where value is, for example, 11223344. It's possible? Why?

We will now write to its client configuration characteristic descriptor. Its hancler is the handler of the characteristic's value plus one. For instance, if the handle for the value is 0x0001, its cliente configuration characteristic would have the handle 0x0002.

Task

Try now to write to the client configuration characteristic. Use the command char-write-cmd handler value, where value is, for example, 0100. It's possible? Why?

Task 4.2

As you may have noticed, it is possible to read from the monitoring value, and write to config value. We will use this last feature to configure notifications about the monitoring value. This way, each time the value changes, the clients that have activated the notifications will receive the new value.

To achieve this we need to modify some parts of our code. Specifically, we will need:

  1. Create a new task that periodically modifies the heart rate value (in our case generating a new random value). This task will consist of an infinite loop that, generates a new value for the characteristic and correctly updates the gatt table. Then, if notifications have been activated, it sends the new value to the clients:
static void publish_data_task(void *pvParameters)
{
    while (1) {
        ESP_LOGI("APP", "Sending data...");

        // Step 1: Update characteristic value ...

        // Step 2: If indications are active send the indication...

        // Step 3: Sleep for one second...
        vTaskDelay( 1000. / portTICK_PERIOD_MS);
    }
}

This routine should be created in response to the connection event by a client, using, for example, the invocation to:

xTaskCreate(&publish_data_task, "publish_data_task", 4096, NULL, 5, NULL);
  1. The update of the value, carried out periodically and randomly, will modify byte 1 of the heart rate value, taking a random value between 0 and 255 (as an additional note, current heart rate monitors support values higher for heart rate, although the configuration of this functionality is outside the scope of practice), and then update the internal gatt table using the esp_ble_gatts_set_attr_value function.

  2. The verification of the activation or not of the notification is done by consulting the two bytes of the corresponding client configuration characteristic. If these values are 0x01 and0x00 (positions 0 and 1, respectively), the notifications are active, and therefore, the notification shall be sent. You will need to use the function esp_ble_gatts_get_attr_value to read this descriptor.

  3. To send the notification, we will use the following function:

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);

The activation of notifications from gatttool will be done by writing the value 0x0100 in the client configuration characteristic, this is:

char-write-cmd HANDLER 0100

If you also modify the UUIDs by those provided in the specification of Bluetooth for the Heart Rate Service, and everything has been configured correctly, your ESP32 should be able to interface with any heart rate monitor to, for example, Android. To do this, use the following UUIDs:

Deliver the modified code with a small pdf report showing how you activate the notifications with gatttool, and how the node then sends a new value every second.