This change adds a check for compatibility between the nvs version found on nvs flash and the one assumed by running code during nvs initialization. Any mismatch is reported to the user using new error code ESP_ERR_NVS_NEW_VERSION_FOUND.
704 lines
37 KiB
Markdown
704 lines
37 KiB
Markdown
# 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/gatt_client/main](../main). The [gattc_demo.c](../main/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](../main/gattc_demo.c) are:
|
||
|
||
```c
|
||
#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:
|
||
|
||
```c
|
||
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:
|
||
|
||
```c
|
||
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:
|
||
|
||
```c
|
||
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.
|
||
|
||
```c
|
||
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:
|
||
|
||
```c
|
||
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.
|
||
|
||
```c
|
||
//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:
|
||
|
||
```c
|
||
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:
|
||
|
||
```c
|
||
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:
|
||
|
||
```c
|
||
#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:
|
||
|
||
```c
|
||
/* 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:
|
||
|
||
```c
|
||
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.
|
||
|
||
```c
|
||
…
|
||
/* 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:
|
||
|
||
```c
|
||
/// 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:
|
||
|
||
```c
|
||
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:
|
||
|
||
```c
|
||
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:
|
||
|
||
```c
|
||
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:
|
||
|
||
```c
|
||
/**
|
||
* @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:
|
||
|
||
```c
|
||
/// 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:
|
||
|
||
```c
|
||
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:
|
||
|
||
```c
|
||
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:
|
||
|
||
```c
|
||
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.
|
||
|
||
```c
|
||
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:
|
||
|
||
```c
|
||
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:
|
||
|
||
```c
|
||
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:
|
||
|
||
```c
|
||
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.
|
||
|
||
```c
|
||
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.
|
||
|
||
```c
|
||
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:
|
||
|
||
```c
|
||
static esp_bt_uuid_t remote_filter_service_uuid = {
|
||
.len = ESP_UUID_LEN_16,
|
||
.uuid = {.uuid16 = REMOTE_SERVICE_UUID,},
|
||
};
|
||
```
|
||
Where,
|
||
|
||
```c
|
||
#define REMOTE_SERVICE_UUID 0x00FF
|
||
```
|
||
The services are then discovered as follows:
|
||
|
||
```c
|
||
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:
|
||
|
||
```c
|
||
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 examples 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:
|
||
|
||
```c
|
||
#define REMOTE_NOTIFY_CHAR_UUID 0xFF01
|
||
```
|
||
A service is defined using the `esp_gatt_srvc_id_t` structure as:
|
||
|
||
```c
|
||
/**
|
||
* @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:
|
||
|
||
```c
|
||
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.
|
||
|
||
```c
|
||
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;h
|
||
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.
|
||
|
||
```c
|
||
…
|
||
/* 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:
|
||
|
||
```c
|
||
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 *)¬ify_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:
|
||
|
||
```c
|
||
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:
|
||
|
||
```c
|
||
#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.
|
||
|