Skip to content

Latest commit

 

History

History
723 lines (595 loc) · 38.4 KB

Gatt_Client_Example_Walkthrough.md

File metadata and controls

723 lines (595 loc) · 38.4 KB

Gatt Client Example Walkthrough

Introduction

In this tutorial, the GATT client example code for the ESP32 is reviewed. The code implements a Bluetooth Low Energy (BLE) Generic Attribute (GATT) client, which scans for nearby peripheral servers and connects to a predefined service. The client then searches for available characteristics and subscribes to a known characteristic in order to receive notifications or indications. The example can register an Application Profile and initializes a sequence of events, which can be used to configure Generic Access Profile (GAP) parameters and to handle events such as scanning, connecting to peripherals and reading and writing characteristics.

Includes

This example is located in the examples folder of the ESP-IDF under the bluetooth/bluedroid/ble/gatt_client/main. The gattc_demo.c file located in the main folder contains all the functionality that we are going to review. The header files contained in gattc_demo.c are:

#include <stdint.h>
#include <string.h>
#include <stdbool.h>
#include <stdio.h>
#include "nvs.h"
#include "nvs_flash.h"
#include "controller.h"

#include "bt.h"
#include "esp_gap_ble_api.h"
#include "esp_gattc_api.h"
#include "esp_gatt_defs.h"
#include "esp_bt_main.h"
#include "esp_gatt_common_api.h"

These includes are required for the FreeRTOS and underlaying system components to run, including the 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_gattc_api.h”, which expose the BLE APIs required to implement this example.

  • bt.h: configures the BT controller and VHCI from the host side.
  • esp_bt_main.h: initializes and enables the Bluedroid stack.
  • esp_gap_ble_api.h: implements the GAP configuration, such as advertising and connection parameters.
  • esp_gattc_api.h: implements the GATT Client configuration, such as connecting to peripherals and searching for services.

Main Entry Point

The program’s entry point is the app_main() function:

void app_main()
{
    // Initialize NVS.
    esp_err_t 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_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
    ret = esp_bt_controller_init(&bt_cfg);
    if (ret) {
        ESP_LOGE(GATTC_TAG, "%s initialize controller failed, error code = %x\n", __func__, ret);
        return;
    }

    ret = esp_bt_controller_enable(ESP_BT_MODE_BLE);
    if (ret) {
        ESP_LOGE(GATTC_TAG, "%s enable controller failed, error code = %x\n", __func__, ret);
        return;
    }

    ret = esp_bluedroid_init();
    if (ret) {
        ESP_LOGE(GATTC_TAG, "%s init bluetooth failed, error code = %x\n", __func__, ret);
        return;
    }

    ret = esp_bluedroid_enable();
    if (ret) {
        ESP_LOGE(GATTC_TAG, "%s enable bluetooth failed, error code = %x\n", __func__, ret);
        return;
    }

    //register the  callback function to the gap module
    ret = esp_ble_gap_register_callback(esp_gap_cb);
    if (ret){
        ESP_LOGE(GATTC_TAG, "%s gap register failed, error code = %x\n", __func__, ret);
        return;
    }

    //register the callback function to the gattc module
    ret = esp_ble_gattc_register_callback(esp_gattc_cb);
    if(ret){
        ESP_LOGE(GATTC_TAG, "%s gattc register failed, error code = %x\n", __func__, ret);
        return;
    }

    ret = esp_ble_gattc_app_register(PROFILE_A_APP_ID);
    if (ret){
        ESP_LOGE(GATTC_TAG, "%s gattc app register failed, error code = %x\n", __func__, ret);
    }

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

}

The main function starts by initializing the non-volatile storage library. This library allows to save key-value pairs in flash memory and is used by some components such as the Wi-Fi library to save the SSID and password:

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

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

The controller should be enabled in ESP_BT_MODE_BTDM, if you want to use the dual mode (BLE + BT).

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 main function ends by registering the GAP and GATT event handlers, as well as the Application Profile and set the maximum supported MTU size.

    //register the  callback function to the gap module
    ret = esp_ble_gap_register_callback(esp_gap_cb);

    //register the callback function to the gattc module
    ret = esp_ble_gattc_register_callback(esp_gattc_cb);

    ret = esp_ble_gattc_app_register(PROFILE_A_APP_ID);

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

The GAP and GATT event handlers are the functions used to catch the events generated by the BLE stack and execute functions to configure parameters of the application. Moreover, the event handlers are also used to handle read and write events coming from the central. The GAP event handler takes care of scanning and connecting to servers and the GATT handler manages events that happen after the client has connected to a server, such as searching for services and writing and reading data. The GAP and GATT event handlers are registered by using:

esp_ble_gap_register_callback();
esp_ble_gattc_register_callback();

The functions esp_gap_cb() and esp_gattc_cb() handle all the events generated by the BLE stack.

Application Profiles

The Application Profiles are a way to group functionalities that are designed for one or more server applications. For example, you can have an Application Profile connected to the Heart Rate Sensors, and another one connected to the Temperature Sensors. Each Application Profile creates a GATT interface to connect to other devices. The Application Profiles in the code are instances of the gattc_profile_inst structure, which is defined as:

struct gattc_profile_inst {
    esp_gattc_cb_t gattc_cb;
    uint16_t gattc_if;
    uint16_t app_id;
    uint16_t conn_id;
    uint16_t service_start_handle;
    uint16_t service_end_handle;
    uint16_t char_handle;
    esp_bd_addr_t remote_bda;
};

The Application Profile structure contains:

  • gattc_cb: GATT client callback function
  • gattc_if: GATT client interface number for this profile
  • app_id: Application Profile ID number
  • conn_id: Connection ID
  • service_start_handle: Service start handle
  • service_end_handle: Service end handle
  • char_handle: Char handle
  • remote_bda: Remote device address connected to this client.

In this example there is one Application Profile and its ID is defined as:

#define PROFILE_NUM 1
#define PROFILE_A_APP_ID 0

The Application Profile are stored in the gl_profile_tab array, which is initialized as:

/* One gatt-based profile one app_id and one gattc_if, this array will store the gattc_if returned by ESP_GATTS_REG_EVT */
static struct gattc_profile_inst gl_profile_tab[PROFILE_NUM] = {
		[PROFILE_A_APP_ID] = {.gattc_cb = gattc_profile_event_handler,
								  .gattc_if = ESP_GATT_IF_NONE, /* Not get the gatt_if, so initial is ESP_GATT_IF_NONE */
    },
};

The initialization of the Application Profile table array includes defining the callback functions for each Profile. These are gattc_profile_a_event_handler() and gattc_profile_a_event_handler() respectively. In addition, the GATT interface is initialized to the default value of ESP_GATT_IF_NONE. Later on, when the Application Profile is registered, the BLE stack returns a GATT interface instance to use with that Application Profile.

The profile registration triggers an ESP_GATTC_REG_EVT event, which is handled by the esp_gattc_cb() event handler. The handler takes the GATT interface returned by the event and stores it in the profile table:

static void esp_gattc_cb(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param)
{
    ESP_LOGI(GATTC_TAG, "EVT %d, gattc if %d", event, gattc_if);

    /* If event is register event, store the gattc_if for each profile */
    if (event == ESP_GATTC_REG_EVT) {
        if (param->reg.status == ESP_GATT_OK) {
            gl_profile_tab[param->reg.app_id].gattc_if = gattc_if;
        } else {
            ESP_LOGI(GATTC_TAG, "reg app failed, app_id %04x, status %d",
                    param->reg.app_id,
                    param->reg.status);
            return;
        }
    }
…

Finally, the callback function invokes the corresponding event handler for each profile in the gl_profile_tab table.

/* If the gattc_if equal to profile A, call profile A cb handler,
     * so here call each profile's callback */
    do {
        int idx;
        for (idx = 0; idx < PROFILE_NUM; idx++) {
            if (gattc_if == ESP_GATT_IF_NONE || /* ESP_GATT_IF_NONE, not specify a certain gatt_if, need to call every profile cb function */
                    gattc_if == gl_profile_tab[idx].gattc_if) {
                if (gl_profile_tab[idx].gattc_cb) {
                    gl_profile_tab[idx].gattc_cb(event, gattc_if, param);
                }
            }
        }
    } while (0);
}

Setting Scan Parameters

The GATT client normally scans for nearby servers and tries connect to them, if interested. However, in order to perform the scanning, first the configuration parameters need to be set. This is done after the registration of the Application Profiles, because the registration, once completed, triggers an ESP_GATTC_REG_EVT event. The first time this event is triggered, the GATT event handler captures it and assigns a GATT interface to Profile A, then the event is forwarded to the GATT event handler of Profile A. One in this event handler, the event is used to call the esp_ble_gap_set_scan_params() function, which takes a ble_scan_params structure instance as parameter. This structure is defined as:

/// Ble scan parameters
typedef struct {
    esp_ble_scan_type_t     scan_type;              /*!< Scan type */
    esp_ble_addr_type_t     own_addr_type;          /*!< Owner address type */
    esp_ble_scan_filter_t   scan_filter_policy;     /*!< Scan filter policy */
    uint16_t                scan_interval;          /*!< Scan interval. This is defined as the time interval from when the Controller started its last LE scan until it begins the subsequent LE scan.*/ 
    //Range: 0x0004 to 0x4000 
    //Default: 0x0010 (10 ms)
    //Time = N * 0.625 msec
    //Time Range: 2.5 msec to 10.24	seconds
    uint16_t                scan_window;            /*!< Scan window. The duration of the LE scan. LE_Scan_Window shall be less than or equal to LE_Scan_Interval*/
    //Range: 0x0004 to 0x4000                                                   	 //Default: 0x0010 (10 ms)
    //Time = N * 0.625 msec
    //Time Range: 2.5 msec to 10240 msec
} esp_ble_scan_params_t;

An it is initialized as:

static esp_ble_scan_params_t ble_scan_params = {
    .scan_type              = BLE_SCAN_TYPE_ACTIVE,
    .own_addr_type          = BLE_ADDR_TYPE_PUBLIC,
    .scan_filter_policy     = BLE_SCAN_FILTER_ALLOW_ALL,
    .scan_interval          = 0x50,
    .scan_window            = 0x30
};

The BLE scan parameters are configured so that the type of scanning is active (includes reading the scanning response), it is of public type, allows any advertising device to be read and has a scanning interval of 100 ms (1.25 ms * 0x50) and a scanning window of 60 ms (1.25 ms * 0x30).

The scan values are set using the esp_ble_gap_set_scan_params() function:

case ESP_GATTC_REG_EVT:
        ESP_LOGI(GATTC_TAG, "REG_EVT");
        esp_err_t scan_ret = esp_ble_gap_set_scan_params(&ble_scan_params);
        if (scan_ret){
            ESP_LOGE(GATTC_TAG, "set scan params error, error code = %x", scan_ret);
        }
        break;

Start Scanning

Once the scanning parameters are set, an ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT event is triggered, which is handled by the GAP event handler esp_gap_cb(). This event is used to start the scanning of nearby GATT servers:

    case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: {
        //the unit of the duration is second
        uint32_t duration = 30;
        esp_ble_gap_start_scanning(duration);
        break;
        }

The scanning is started using the esp_ble_gap_start_scanning() function which takes a parameter representing the duration of the continuous scanning (in seconds). Once the scanning period is ended, an ESP_GAP_SEARCH_INQ_CMPL_EVT event is triggered.

Getting Scan Results

The results of the scanning are displayed as soon as they arrive with the ESP_GAP_BLE_SCAN_RESULT_EVT event, which includes the following parameters:

    /**
     * @brief ESP_GAP_BLE_SCAN_RESULT_EVT
     */
    struct ble_scan_result_evt_param {
        esp_gap_search_evt_t search_evt;            /*!< Search event type */
        esp_bd_addr_t bda;                          /*!< Bluetooth device address which has been searched */
        esp_bt_dev_type_t dev_type;                 /*!< Device type */
        esp_ble_addr_type_t ble_addr_type;          /*!< Ble device address type */
        esp_ble_evt_type_t ble_evt_type;            /*!< Ble scan result event type */
        int rssi;                                   /*!< Searched device's RSSI */
        uint8_t  ble_adv[ESP_BLE_ADV_DATA_LEN_MAX + ESP_BLE_SCAN_RSP_DATA_LEN_MAX]; /*!< Received EIR */
        int flag;                                   /*!< Advertising data flag bit */
        int num_resps;                              /*!< Scan result number */
        uint8_t adv_data_len;                       /*!< Adv data length */
        uint8_t scan_rsp_len;                       /*!< Scan response length */
    } scan_rst;                                     /*!< Event parameter of ESP_GAP_BLE_SCAN_RESULT_EVT */

This event also includes a list of sub events as shown below:

/// Sub Event of ESP_GAP_BLE_SCAN_RESULT_EVT
typedef enum {
    ESP_GAP_SEARCH_INQ_RES_EVT             = 0,      /*!< Inquiry result for a peer device. */
    ESP_GAP_SEARCH_INQ_CMPL_EVT            = 1,      /*!< Inquiry complete. */
    ESP_GAP_SEARCH_DISC_RES_EVT            = 2,      /*!< Discovery result for a peer device. */
    ESP_GAP_SEARCH_DISC_BLE_RES_EVT        = 3,      /*!< Discovery result for BLE GATT based service on a peer device. */
    ESP_GAP_SEARCH_DISC_CMPL_EVT           = 4,      /*!< Discovery complete. */
    ESP_GAP_SEARCH_DI_DISC_CMPL_EVT        = 5,      /*!< Discovery complete. */
    ESP_GAP_SEARCH_SEARCH_CANCEL_CMPL_EVT  = 6,      /*!< Search cancelled */
} esp_gap_search_evt_t;

We are interested in the ESP_GAP_SEARCH_INQ_RES_EVT event, which is called every time a new device is found. We are also interested in the ESP_GAP_SEARCH_INQ_CMPL_EVT, which is triggered when the duration of the scanning is completed and can be used to restart the scanning procedure:

      case ESP_GAP_BLE_SCAN_RESULT_EVT: {
        esp_ble_gap_cb_param_t *scan_result = (esp_ble_gap_cb_param_t *)param;
        switch (scan_result->scan_rst.search_evt) {
	        case ESP_GAP_SEARCH_INQ_RES_EVT:
		        esp_log_buffer_hex(GATTC_TAG, scan_result->scan_rst.bda, 6);
		        ESP_LOGI(GATTC_TAG, "searched Adv Data Len %d, Scan Response Len %d", scan_result->scan_rst.adv_data_len, scan_result->scan_rst.scan_rsp_len);
		        adv_name = esp_ble_resolve_adv_data(scan_result->scan_rst.ble_adv, ESP_BLE_AD_TYPE_NAME_CMPL, &adv_name_len);
		        ESP_LOGI(GATTC_TAG, "searched Device Name Len %d", adv_name_len);
		        esp_log_buffer_char(GATTC_TAG, adv_name, adv_name_len);
		        ESP_LOGI(GATTC_TAG, "\n");
		        if (adv_name != NULL) {
			        if (strlen(remote_device_name) == adv_name_len && strncmp((char *)adv_name, remote_device_name, adv_name_len) == 0) {
                    ESP_LOGI(GATTC_TAG, "searched device %s\n", remote_device_name);
                    if (connect == false) {
                        connect = true;
                        ESP_LOGI(GATTC_TAG, "connect to the remote device.");
                        esp_ble_gap_stop_scanning();
                        esp_ble_gattc_open(gl_profile_tab[PROFILE_A_APP_ID].gattc_if, scan_result->scan_rst.bda, scan_result->scan_rst.ble_addr_type, true);
                    }
                }
            }
            break;

First the device name is resolved and compared to the one defined in remote_device_name. If it equals to the device name of the GATT Server we are interested in, then the scanning is stopped.

Connecting to A GATT Server

Every time we receive a result from the ESP_GAP_SEARCH_INQ_RES_EVT event, the code first prints the address of the remote device:

case ESP_GAP_SEARCH_INQ_RES_EVT:
     esp_log_buffer_hex(GATTC_TAG, scan_result->scan_rst.bda, 6);

The client then prints the advertised data length and the scan response length:

ESP_LOGI(GATTC_TAG, "searched Adv Data Len %d, Scan Response Len %d", scan_result->scan_rst.adv_data_len, scan_result->scan_rst.scan_rsp_len);

In order to get the device name, we use the esp_ble_resolve_adv_data() function, which takes the advertised data stored in scan_result->scan_rst.ble_adv, the type of advertising data and the length, in order to extract the value from the advertising packet frame. Then the device name is printed.

adv_name = esp_ble_resolve_adv_data(scan_result->scan_rst.ble_adv, ESP_BLE_AD_TYPE_NAME_CMPL, &adv_name_len);
ESP_LOGI(GATTC_TAG, "searched Device Name Len %d", adv_name_len);
esp_log_buffer_char(GATTC_TAG, adv_name, adv_name_len);

Finally if the remote device name is the same as we have defined above, the local device stops scanning and tries to open a connection to the remote device using the esp_ble_gattc_open() function. This function takes as parameters the Application Profile GATT interface, the remote server address and a boolean value. The boolean value is used to indicate if the connection is done directly or if it’s done in the background (auto-connection), at the moment this boolean value must be set to true in order to establish the connection. Notice that the client opens a virtual connection to the server. The virtual connection returns a connection ID. The virtual connection is the connection between the Application Profile and the remote server. Since many Application Profiles can run on one ESP32, there could be many virtual connection opened to the same remote server. There is also the physical connection which is the actual BLE link between the client and the server. Therefore, if the physical connection is disconnected with the esp_ble_gap_disconnect() function, all other virtual connections are closed as well. In this example, each Application Profile creates a virtual connection to the same server with the esp_ble_gattc_open() function, so when the close function is called, only that connection from the Application Profile is closed, while if the gap disconnect function is called, both connections will be closed. In addition, connect events are propagated to all profiles because it relates to the physical connection, while open events are propagated only to the profile that creates the virtual connection.

Configuring the MTU Size

ATT_MTU is defined as the maximum size of any packet sent between a client and a server. When the client connects to the server, it informs the server which MTU size to use by exchanging MTU Request and Response protocol data units (PDUs). This is done after the opening of a connection. After opening the connection, an ESP_GATTC_CONNECT_EVT event is triggered:

     case ESP_GATTC_CONNECT_EVT:
        //p_data->connect.status always be ESP_GATT_OK
        ESP_LOGI(GATTC_TAG, "ESP_GATTC_CONNECT_EVT conn_id %d, if %d, status %d", conn_id, gattc_if, p_data->connect.status);
        conn_id = p_data->connect.conn_id;
        gl_profile_tab[PROFILE_A_APP_ID].conn_id = p_data->connect.conn_id;
        memcpy(gl_profile_tab[PROFILE_A_APP_ID].remote_bda, p_data->connect.remote_bda, sizeof(esp_bd_addr_t));
        ESP_LOGI(GATTC_TAG, "REMOTE BDA:");
        esp_log_buffer_hex(GATTC_TAG, gl_profile_tab[PROFILE_A_APP_ID].remote_bda, sizeof(esp_bd_addr_t));
        esp_err_t mtu_ret = esp_ble_gattc_send_mtu_req (gattc_if, conn_id);
        if (mtu_ret){
            ESP_LOGE(GATTC_TAG, "config MTU error, error code = %x", mtu_ret);
        }
        break;

The connection ID and the address of the remote device (server) are stored in the Application Profile table and printed to the console:

conn_id = p_data->connect.conn_id;
gl_profile_tab[PROFILE_A_APP_ID].conn_id = p_data->connect.conn_id;
memcpy(gl_profile_tab[PROFILE_A_APP_ID].remote_bda, p_data->connect.remote_bda, 
		sizeof(esp_bd_addr_t));
ESP_LOGI(GATTC_TAG, "REMOTE BDA:");
esp_log_buffer_hex(GATTC_TAG, gl_profile_tab[PROFILE_A_APP_ID].remote_bda, 
		sizeof(esp_bd_addr_t));

The typical MTU size for a Bluetooth 4.0 connection is 23 bytes. A client can change the size of MUT, using esp_ble_gattc_send_mtu_req() function, which takes the GATT interface and the connection ID. The size of the requested MTU is defined by esp_ble_gatt_set_local_mtu(). The server can then accept or reject the request. The ESP32 supports a MTU size of up to 517 bytes, which is defined by the ESP_GATT_MAX_MTU_SIZE in esp_gattc_api.h. In this example, the MTU size is set to 500 bytes. In case the configuration fails, the returned error is printed:

esp_err_t mtu_ret = esp_ble_gattc_send_mtu_req (gattc_if, conn_id);
if (mtu_ret){
	ESP_LOGE(GATTC_TAG, "config MTU error, error code = %x", mtu_ret);
}
break;

The connection opening also triggers an ESP_GATTC_OPEN_EVT, which is used to check that the opening of the connection was done successfully, otherwise print an error and exit.

case ESP_GATTC_OPEN_EVT:
        if (param->open.status != ESP_GATT_OK){
            ESP_LOGE(GATTC_TAG, "open failed, status %d", p_data->open.status);
            break;
        }
ESP_LOGI(GATTC_TAG, "open success");

When the MTU is exchanged, an ESP_GATTC_CFG_MTU_EVT is triggered, which in this example is used to print the new MTU size.

case ESP_GATTC_CFG_MTU_EVT:
        if (param->cfg_mtu.status != ESP_GATT_OK){
            ESP_LOGE(GATTC_TAG,"config mtu failed, error status = %x", param->cfg_mtu.status);
        }
        ESP_LOGI(GATTC_TAG, "ESP_GATTC_CFG_MTU_EVT, Status %d, MTU %d, conn_id %d", param->cfg_mtu.status, param->cfg_mtu.mtu, param->cfg_mtu.conn_id);
…

Discovering Services

The MTU configuration event is also used to start discovering the services available in the server that the client just connected to. To discover the services, the function esp_ble_gattc_search_service() is used. The parameters of the function are the GATT interface, the Application Profile connection ID and the UUID of the service application that the client is interested in. The service we are looking for is defined as:

static esp_bt_uuid_t remote_filter_service_uuid = {
    .len = ESP_UUID_LEN_16,
    .uuid = {.uuid16 = REMOTE_SERVICE_UUID,},
};

Where,

#define REMOTE_SERVICE_UUID        0x00FF

If UUID of the service application the user is interested in is 128-bit, then there is one note below for the user which is relevant with the little-endian storage mode of the processor architecture. The struct of UUID is defined as:

typedef struct {
#define ESP_UUID_LEN_16     2
#define ESP_UUID_LEN_32     4
#define ESP_UUID_LEN_128    16
    uint16_t len;							/*!< UUID length, 16bit, 32bit or 128bit */
    union {
        uint16_t    uuid16;                 /*!< 16bit UUID */
        uint32_t    uuid32;                 /*!< 32bit UUID */
        uint8_t     uuid128[ESP_UUID_LEN_128]; /*!< 128bit UUID */
    } uuid;									/*!< UUID */
} __attribute__((packed)) esp_bt_uuid_t;

Note: In little-endian storage mode, you can define service UUID directly in the normal order if it's a 16-bit or a 32-bit UUID, but if service UUID is 128-bit, there is minor difference. For example, if the UUID of the service application that the user is interested in is 12345678-a1b2-c3d4-e5f6-9fafd205e457, REMOTE_SERVICE_UUID should be defined as {0x57,0xE4,0x05,0xD2,0xAF,0x9F,0xF6,0xE5,0xD4,0xC3,0xB2,0xA1,0x78,0x56,0x34,0x12}.

The services are then discovered as follows:

esp_ble_gattc_search_service(gattc_if, param->cfg_mtu.conn_id, &remote_filter_service_uuid);
        break;

The resulting service found, if there is any, will be returned from an ESP_GATTC_SEARCH_RES_EVT. For each service found, the event is triggered to print information about the service discovered, depending on the size of the UUID:

 case ESP_GATTC_SEARCH_RES_EVT: {
        esp_gatt_srvc_id_t *srvc_id = &p_data->search_res.srvc_id;
        conn_id = p_data->search_res.conn_id;
        if (srvc_id->id.uuid.len == ESP_UUID_LEN_16 && srvc_id->id.uuid.uuid.uuid16 == 
REMOTE_SERVICE_UUID) {
        get_server = true;
        gl_profile_tab[PROFILE_A_APP_ID].service_start_handle = p_data->search_res.start_handle;
        gl_profile_tab[PROFILE_A_APP_ID].service_end_handle = p_data->search_res.end_handle;
        ESP_LOGI(GATTC_TAG, "UUID16: %x", srvc_id->id.uuid.uuid.uuid16);
        }
        break;

In case that the client finds the service that it is looking for, the flag get_server is set to true, and the start handle value and end handle value, which will be used later to get all the characteristics of that service, are saved. After all service results are returned, the search is completed and an ESP_GATTC_SEARCH_CMPL_EVT event is triggered.

Getting Characteristics

This example implements getting characteristic data from a predefined service. The service that we want the characteristics from has an UUID of 0x00FF, and the characteristic we are interested in has an UUID of 0xFF01:

#define REMOTE_NOTIFY_CHAR_UUID    0xFF01

A service is defined using the esp_gatt_srvc_id_t structure as:

/**
 * @brief Gatt id, include uuid and instance id
 */
typedef struct {
    esp_bt_uuid_t   uuid;                   /*!< UUID */
    uint8_t         inst_id;                /*!< Instance id */
} __attribute__((packed)) esp_gatt_id_t;

In this example, we define the service that we want to get the characteristics from as:

static esp_gatt_srvc_id_t remote_service_id = {
    .id = {
        .uuid = {
            .len = ESP_UUID_LEN_16,
            .uuid = {.uuid16 = REMOTE_SERVICE_UUID,},
        },
        .inst_id = 0,
    },
    .is_primary = true,
};

Once defined, we can get the characteristics from that service using the esp_ble_gattc_get_characteristic() function, which is called in the ESP_GATTC_SEARCH_CMPL_EVT event after the search for services is completed and the client has found the service that it was looking for.

case ESP_GATTC_SEARCH_CMPL_EVT:
    if (p_data->search_cmpl.status != ESP_GATT_OK){
        ESP_LOGE(GATTC_TAG, "search service failed, error status = %x", p_data->search_cmpl.status);
        break;
    }
    conn_id = p_data->search_cmpl.conn_id;
    if (get_server){
        uint16_t count = 0;
        esp_gatt_status_t status = esp_ble_gattc_get_attr_count( gattc_if,
                          p_data->search_cmpl.conn_id,ESP_GATT_DB_CHARACTERISTIC,                                                                                                                 		                    gl_profile_tab[PROFILE_A_APP_ID].service_start_handle,                                                                   		                    gl_profile_tab[PROFILE_A_APP_ID].service_end_handle,        
                                                                INVALID_HANDLE,      	                  
                                                                     &count);
        if (status != ESP_GATT_OK){
            ESP_LOGE(GATTC_TAG, "esp_ble_gattc_get_attr_count error");
        }

        if (count > 0){
            char_elem_result = (esp_gattc_char_elem_t*)malloc
                                          (sizeof(esp_gattc_char_elem_t) * count);
            if (!char_elem_result){
                ESP_LOGE(GATTC_TAG, "gattc no mem");
            }else{
                status = esp_ble_gattc_get_char_by_uuid( gattc_if,
                                                       p_data->search_cmpl.conn_id,                                                                      
                              gl_profile_tab[PROFILE_A_APP_ID].service_start_handle,                                                            
                              gl_profile_tab[PROFILE_A_APP_ID].service_end_handle,
                                                         remote_filter_char_uuid,
                                                         char_elem_result,
                                                         &count);
                if (status != ESP_GATT_OK){
                    ESP_LOGE(GATTC_TAG, "esp_ble_gattc_get_char_by_uuid error");
                }

                /*  Every service have only one char in our 'ESP_GATTS_DEMO' demo,     
                    so we used first 'char_elem_result' */
                if (count > 0 && (char_elem_result[0].properties                       
                                 &ESP_GATT_CHAR_PROP_BIT_NOTIFY)){
                    gl_profile_tab[PROFILE_A_APP_ID].char_handle =  
                    char_elem_result[0].char_handle;
                    esp_ble_gattc_register_for_notify (gattc_if,   
                                   gl_profile_tab[PROFILE_A_APP_ID].remote_bda, 
                                   char_elem_result[0].char_handle);
                }
            }
            /* free char_elem_result */
            free(char_elem_result);
        }else{
            ESP_LOGE(GATTC_TAG, "no char found");
        }        }
        break;

esp_ble_gattc_get_attr_count() gets the attribute count with the given service or characteristic in the gattc cache. The parameters of esp_ble_gattc_get_attr_count() function are the GATT interface, the connection ID, the attribute type defined in esp_gatt_db_attr_type_t, the attribute start handle, the attribute end handle, the characteristic handle (this parameter is only valid when the type is set to ESP_GATT_DB_DESCRIPTOR.) and output the number of attribute has been found in the gattc cache with the given attribute type. Then we allocate a buffer to save the char information for esp_ble_gattc_get_char_by_uuid() function. The function finds the characteristic with the given characteristic UUID in the gattc cache. It just gets characteristic from local cache, instead of the remote devices. In a server, there might be more than one chars sharing the same UUID. However, in our gatt_server demo, every char has an unique UUID and that’s why we only use the first char in char_elem_result, which is the pointer to the characteristic of the service. Count initially stores the number of the characteristics that the client wants to find, and will be updated with the number of the characteristics that have been actually found in the gattc cache with esp_ble_gattc_get_char_by_uuid.

Registering for Notifications

The client can register to receive notifications from the server every time the characteristic value changes. In this example, we want to register for notifications of the characteristic identified with an UUID of 0xff01. After getting all the characteristics, we check the properties of the received characteristic, then use the esp_ble_gattc_register_for_notify() function to register notifications. The function arguments are the GATT interface, the address of the remote server, and the handle we want to register for notifications.

/*  Every service have only one char in our 'ESP_GATTS_DEMO' demo, so we used first 'char_elem_result' */
                    if(count > 0 && (char_elem_result[0].properties & ESP_GATT_CHAR_PROP_BIT_NOTIFY)){
                        gl_profile_tab[PROFILE_A_APP_ID].char_handle = char_elem_result[0].char_handle;
                        esp_ble_gattc_register_for_notify (gattc_if, gl_profile_tab[PROFILE_A_APP_ID].remote_bda,
                        char_elem_result[0].char_handle);
                        }
…

This procedure registers notifications to the BLE stack, and triggers an ESP_GATTC_REG_FOR_NOTIFY_EVT. This event is used to write to the server Client Configuration Descriptor:

    case ESP_GATTC_REG_FOR_NOTIFY_EVT: {
        ESP_LOGI(GATTC_TAG, "ESP_GATTC_REG_FOR_NOTIFY_EVT");
        if (p_data->reg_for_notify.status != ESP_GATT_OK){
            ESP_LOGE(GATTC_TAG, "REG FOR NOTIFY failed: error status = %d", p_data->reg_for_notify.status);
        }else{
            uint16_t count = 0;
            uint16_t notify_en = 1;
            esp_gatt_status_t ret_status = esp_ble_gattc_get_attr_count( gattc_if, gl_profile_tab[PROFILE_A_APP_ID].conn_id,
											            ESP_GATT_DB_DESCRIPTOR,
											            gl_profile_tab[PROFILE_A_APP_ID].service_start_handle,
											            gl_profile_tab[PROFILE_A_APP_ID].service_end_handle,
											            gl_profile_tab[PROFILE_A_APP_ID].char_handle, &count);
            if (ret_status != ESP_GATT_OK){
                ESP_LOGE(GATTC_TAG, "esp_ble_gattc_get_attr_count error");
            }
            if (count > 0){
                descr_elem_result = malloc(sizeof(esp_gattc_descr_elem_t) * count);
                if (!descr_elem_result){
                    ESP_LOGE(GATTC_TAG, "malloc error, gattc no mem");
                }else{
                    ret_status = esp_ble_gattc_get_descr_by_char_handle( 
                    gattc_if, 
                    gl_profile_tab[PROFILE_A_APP_ID].conn_id, 
                    p_data->reg_for_notify.handle, 
                    notify_descr_uuid, 
                    descr_elem_result,&count);
                    
                    if (ret_status != ESP_GATT_OK){
                        ESP_LOGE(GATTC_TAG, "esp_ble_gattc_get_descr_by_char_handle   
                                                                            error");
                    }

                    /* Every char has only one descriptor in our 'ESP_GATTS_DEMO' demo, so we used first 'descr_elem_result' */
                    if (count > 0 && descr_elem_result[0].uuid.len == ESP_UUID_LEN_16 && descr_elem_result[0].uuid.uuid.uuid16 == ESP_GATT_UUID_CHAR_CLIENT_CONFIG){
                        ret_status = esp_ble_gattc_write_char_descr( gattc_if, 
								                        gl_profile_tab[PROFILE_A_APP_ID].conn_id,
								                        descr_elem_result[0].handle,
								                        sizeof(notify_en),
								                        (Uint8 *)&notify_en,
								                        ESP_GATT_WRITE_TYPE_RSP,
								                        ESP_GATT_AUTH_REQ_NONE);
                    }

                    if (ret_status != ESP_GATT_OK){
                        ESP_LOGE(GATTC_TAG, "esp_ble_gattc_write_char_descr error");
                    }

                    /* free descr_elem_result */
                    free(descr_elem_result);
                }
            }
            else{
                ESP_LOGE(GATTC_TAG, "decsr not found");
            }

        }
        break;
    }

The event is used to first print the notification register status and the service and characteristic UUIDs of the just registered notifications. The client then writes to the Client Configuration Descriptor by using the esp_ble_gattc_write_char_descr() function. There are many characteristic descriptors defined in the Bluetooth specification. However, in this case we are interested in writing to the descriptor that deals with enabling notifications, which is the Client Configuration descriptor. In order to pass this descriptor as parameter, we first define it as:

static esp_gatt_id_t notify_descr_id = {
    .uuid = {
        .len = ESP_UUID_LEN_16,
        .uuid = {.uuid16 = ESP_GATT_UUID_CHAR_CLIENT_CONFIG,},
    },
    .inst_id = 0,
};

Where ESP_GATT_UUID_CHAR_CLIENT_CONFIG is defined with the UUID to identify the Characteristic Client Configuration:

#define ESP_GATT_UUID_CHAR_CLIENT_CONFIG            0x2902          /*  Client Characteristic Configuration */

The value to write is “1” to enable notifications. We also pass ESP_GATT_WRITE_TYPE_RSP to request that the server responds to the request of enabling notifications and ESP_GATT_AUTH_REQ_NONE to indicate that the Write request does not need authorization.

Conclusion

We have reviewed the GATT Client example code for the ESP32. This example scans for nearby devices and searches for services and characteristics of servers of interest. When the server of interest is found, a connection is made with that server and a search for services is performed. Finally, the client looks for a specific characteristic in the services found, if found, gets the characteristic value and registers for notifications to that characteristic. This is done by registering one Application Profile and following a sequence of events to configure the GAP and GATT parameters required.