Merge branch 'feature/btdm_add_bluetooth_examples_tutorials' into 'master'

component/bt: Add bluetooth example tutorials

See merge request !1592
This commit is contained in:
Jiang Jiang Jian 2018-01-03 13:04:16 +08:00
commit 1e141b5198
25 changed files with 2849 additions and 1 deletions

View file

@ -6,4 +6,4 @@ Run the gatt_server demo, the client demo will automatically connect to the gatt
Client demo will enable gatt_server's notify after connection. Then the two devices will exchange
data.
Please check the [tutorial](tutorial/gatt_client_example_walkthrough.md) for more information about this example.

View file

@ -0,0 +1,728 @@
# Gatt Client Example Walkthrough
## Introduction
In this example, the GATT Client example code for the ESP32 is reviewed. This example 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` directory. 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:
```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 programs 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) {
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_BTDM);
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) {
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 Dual Mode (BLE + BT Classic).
```c
ret = esp_bt_controller_enable(ESP_BT_MODE_BTDM);
```
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, 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 its 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 thats 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");
}
/* Erery char have 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:
```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.

View file

@ -6,3 +6,4 @@ This is the demo for user to use ESP BLE security API to connection with peer de
2.Used the esp_ble_set_encryption API to start encryption with peer device, if the peer device take the initiative encryption, should used the esp_ble_gap_security_rsp API to sent response to peer device when receive the ESP_GAP_BLE_SEC_REQ_EVT.
3.It will receive the ESP_GAP_BLE_AUTH_CMPL_EVT event when encryption finish will peer device.
Please check the [tutorial](tutorial/GATT_Security_Client_Example_Walkthrough.md) for more information about this example.

View file

@ -0,0 +1,119 @@
# GATT Security Client Example Walkthrough
## Introduction
This documents presents a review of the GATT Security Client code example for the ESP32. The GATT Client is capable of scanning for nearby devices and once it has found a device of interest, it requests a secure connection. The GATT client behaves as a master device that initiates a connection to a slave by sending a *Pairing Request* as specified by the [Bluetooth Core Specification Version 4.2](https://www.bluetooth.com/specifications/bluetooth-core-specification). The remote slave device is normally a GATT Server that exposes Services and Characteristics. The slave replies with a *Pairing Response* followed by authentication and exchange of keys. If the bonding process is also executed, the Long Term Keys are stored for subsequent connections. Finally an encrypted channel is established which can support protection against Man-In-The-Middle (MITM) attacks depending on the security configuration. The code is implemented using an Application Profile that upon registration, allows to set the local privacy configuration as events are triggered during the lifetime of the program.
This document only includes a description of the security aspects of the GATT Client implementation, for a review of how to define the functionality of the GATT client such as scanning parameters and opening connections please refer to **GATT Client Example Walkthrough**.
## Configuring Local Privacy of the Security Client
The example registers one Application Profile defined as:
```
#define PROFILE_NUM 1
#define PROFILE_A_APP_ID 0
```
The registration takes place in the ``app_main()`` function by using the ``esp_ble_gattc_app_register()`` function:
```
ret = esp_ble_gattc_app_register(PROFILE_A_APP_ID);
if (ret){
ESP_LOGE(GATTC_TAG, "%s gattc app register error, error code = %x\n", __func__, ret);
}
```
The Application Profile registration triggers an ``ESP_GATTC_REG_EVT`` event which is managed by the ``esp_gattc_cb()`` callback function and forwarded to the Profile A event handler ``gattc_profile_event_handler()``. Here, the event is used to configure the local privacy of the slave device by using the ``esp_ble_gap_config_local_privacy()`` function.
```
case ESP_GATTC_REG_EVT:
ESP_LOGI(GATTC_TAG, "REG_EVT");
esp_ble_gap_config_local_privacy(true);
break;
```
This function is a Bluedroid API call for configuring default privacy settings on the local device. Once the privacy is set, an ``ESP_GAP_BLE_SET_LOCAL_PRIVACY_COMPLETE_EVT`` is triggered which is used to set scan parameters and start scanning for nearby peripherals:
```
case ESP_GAP_BLE_SET_LOCAL_PRIVACY_COMPLETE_EVT:
if (param->local_privacy_cmpl.status != ESP_BT_STATUS_SUCCESS){
ESP_LOGE(GATTC_TAG, "config local privacy failed, error code =%x", param->local_privacy_cmpl.status);
break;
}
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;
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;
}
case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT:
//scan start complete event to indicate scan start successfully or failed
if (param->scan_start_cmpl.status != ESP_BT_STATUS_SUCCESS) {
ESP_LOGE(GATTC_TAG, "scan start failed, error status = %x", param->scan_start_cmpl.status);
break;
}
ESP_LOGI(GATTC_TAG, "Scan start success");
break;
```
## Configuring and Bonding to a Slave Device
The rest of the configuration for the GATT Client is performed normally in the same way as the regular GATT Client example. That is, the client finds a device of interest and opens a connection. At this point the GATT client, which is usually the master, initiates the pairing process by sending a Pairing Request to the slave device. This request should be acknowledged with a Pairing Response. The Pairing process is implemented automatically by the stack and no extra user configuration is needed. However, depending on the I/O capabilities of both devices, a passkey might be generated on the ESP32 which is presented to the user with the ``ESP_GAP_BLE_PASSKEY_NOTIF_EVT``:
```
case ESP_GAP_BLE_PASSKEY_NOTIF_EVT: ///the app will receive this evt when the IO has Output capability and the peer device IO has Input capability.
///show the passkey number to the user to input it in the peer deivce.
ESP_LOGE(GATTS_TABLE_TAG, "The passkey Notify number:%d", param->ble_security.key_notif.passkey);
break;
```
The combination of input and output capabilities that determine which algorithm is used are:
| Display Only | Display Yes/No|Keyboard Only|No Input No Output|Keyboard Display|
--------------------|------------------|-----------------------|--------------------|------------------|-----------------------|
Display Only | Just Works| Just Works|Passkey Entry|Just Works|Passkey Entry|
Display Yes/No |Just Works|Just Works|Passkey Entry|Just Works|Passkey Entry|
Keyboard Only |Passkey Entry |Passkey Entry|Passkey Entry|Just Works|Passkey Entry|
No Input No Output| Just Works|Just Works|Just Works|Just Works|Just Works|
Keyboard Display|Passkey Entry|Passkey Entry|Passkey Entry|Just Works|Passkey Entry|
In the Just Works method, the Temporary Key is set to 0. This is a practical way to authenticate devices when no display or keyboards are attached to them, so that there is no way to show or enter a passkey. However, if the ESP32 GATT Client has an LCD, it can present the passkey generated locally so that the user can input it on the other peer device, or if the GATT Client has a keyboard, it can input the passkey generated by the other peer device. Additionally, a numeric comparison can be performed if both devices have a display and yes/no confirm buttons and LE Secure Connections are used, that way an independently generated passkey is displayed on both devices and the user manually checks that both 6-digit confirmation values match.
## Exchanging Keys
When the client connects to a remote device and the pairing is done successfully, the initiator and responder keys are exchanged. For each key exchange message, an ``ESP_GAP_BLE_KEY_EVT`` event is triggered which can be used to print the type of key received:
```
case ESP_GAP_BLE_KEY_EVT:
//shows the ble key info share with peer device to the user.
ESP_LOGI(GATTS_TABLE_TAG, "key type = %s", esp_key_type_to_str(param->ble_security.ble_key.key_type));
break;
```
When the keys are exchanged successfully, the pairing process is completed and encryption of payload data can be started using the AES-128 engine. This triggers an ``ESP_GAP_BLE_AUTH_CMPL_EVT`` event which is used to print information:
```
case ESP_GAP_BLE_AUTH_CMPL_EVT: {
esp_bd_addr_t bd_addr;
memcpy(bd_addr, param->ble_security.auth_cmpl.bd_addr, sizeof(esp_bd_addr_t));
ESP_LOGI(GATTS_TABLE_TAG, "remote BD_ADDR: %08x%04x",\
(bd_addr[0] << 24) + (bd_addr[1] << 16) + (bd_addr[2] << 8) + bd_addr[3],
(bd_addr[4] << 8) + bd_addr[5]);
ESP_LOGI(GATTS_TABLE_TAG, "address type = %d", param->ble_security.auth_cmpl.addr_type);
ESP_LOGI(GATTS_TABLE_TAG, "pair status = %s",param->ble_security.auth_cmpl.success ? "success" : "fail");
break;
```
## Conclusion
In this document, a review of the security aspects of the GATT Client has been realized. BLE security encompasses Pairing, Bonding and Encryption. In order to establish a secure link between a master and a slave device, the local privacy of the GATT client is set, which allows the BLE stack to set necessary security parameters automatically without the need of additional user configuration. The combination of features and capabilities of the peer devices results in the selection of the appropriate pairing method which the BLE stack then executes. Immediately, the required keys are generated and exchanged and the encryption of subsequent messages is started using the AES-128 engine. These steps trigger different events that are managed by the GATT and GAP event handlers which can be used to print useful information such as the types of keys exchanged and the pairing status. The rest of the security GATT client functionality such as registering for notifications of characteristics is implemented in the same way as in **GATT Client Example Walkthrough**.

View file

@ -6,4 +6,5 @@ This is the demo for user to use ESP BLE security API to connection with peer de
2.Used the esp_ble_set_encryption API to start encryption with peer device, if the peer device take the initiative encryption, should used the esp_ble_gap_security_rsp API to sent response to peer device when receive the ESP_GAP_BLE_SEC_REQ_EVT.
3.It will receive the ESP_GAP_BLE_AUTH_CMPL_EVT event when encryption finish will peer device.
Please check the [tutorial](tutorial/GATT_SECURITY_SERVER_EXAMPLE_WALKTHROUGH.md) for more information about this example.

View file

@ -0,0 +1,229 @@
# Gatt Security Server Example Walkthrough
## Introduction
In this document, a description of the security GATT Server BLE example for the ESP32 is presented. The security configuration enables a GATT Server acting as a slave device to bond with a master and establish an encrypted link between them. This functionality is defined by the [Bluetooth Specification version 4.2](https://www.bluetooth.com/specifications/bluetooth-core-specification) and implemented on the ESP-IDF BLE stack, specifically on the Security Manager Protocol (SMP) API.
BLE security involves three interrelated concepts: pairing, bonding and encryption. Pairing concerns with the exchange of security features and types of keys needed. In addition, the pairing procedure takes care of the generation and exchange of shared keys. The core specification defines the legacy pairing and Secure Connections pairing (introduced in Bluetooth 4.2), which are both supported by ESP32. Once the exchange of shared keys is completed, a temporary encrypted link is established to exchange short term and long term keys. Bonding refers to storing the exchanged keys for subsequent connections so that they do not have to be transmitted again. Finally, encryption pertains to the ciphering of plain text data using the AES-128 engine and the shared keys. Server attributes may also be defined to allow only encrypted write and read messages. At any point of the communication, a slave device can always ask to start encryption by issuing a security request to the other peer device, which returns a security response by calling an API.
This document only describes the security configuration. The rest of the GATT server functionalities, such as defining the service table, are explained in the GATT Server example walkthrough documentation. For a better understanding of this example workflow, it is recommended that the reader is familiar with the pairing feature exchange and key generation defined in the section 3.5 of the [Bluetooth Specification Version 4.2](https://www.bluetooth.com/specifications/bluetooth-core-specification) [Vol 3, Part H].
## Setting Security Parameters
The ESP32 requires a series of security parameters in order to define how the pairing request and response are going to be built. The Pairing Response packet built by the GATT Server includes fields such as the input/output capabilities, Secure Connections pairing, authenticated Man-In-The-Middle (MITM) protection or no security requirements (see Section 2.3.1 of the [Bluetooth Specification Version 4.2](https://www.bluetooth.com/specifications/bluetooth-core-specification) [Vol 3, Part H]). In this example, this procedure is done in the `app_main()` function. The pairing request is sent by the initiator which in this case is a remote GATT client. The ESP32 server implemented in this example receives this request and replies with a pairing response, which contains the same security parameters in order for both devices to agree on the resources available and the applicable pairing algorithm (*Just Works* or *Passkey Entry*). Both the pairing request and response commands have the following parameters:
* *IO Capability*: describes if the device has input/output capabilities such as a display or a keyboard.
* *OOB Flag*: describes if the device supports Out of Band passkey exchange, for example using NFC or Wi-Fi to exchange keys as TKs. Currently, the ESP32 does not support this feature.
* *Authorization Request*: indicates the requested security properties such as Bonding, Secure Connections (SC), MITM protection or none that will be present in the Pairing Request and Response packets.
* *Maximum Encryption Key Size*: maximum encryption key size in octets.
* *Initiator Key Distribution/Generation*: indicates which keys the initiator is requesting to distribute/generate or use during the Transport Specific Key Distribution phase. In the pairing request, these keys are requested, while in the pairing response, these keys are confirmed to be distributed.
* *Responder Key Distribution/Generation*: indicates which keys the initiator is requesting the responder to distribute/generate or use during the Transport Specific Key Distribution phase. In the pairing request, these keys are requested, while in the pairing response, these keys are confirmed to be distributed.
In code, these parameters are defined as follows:
* *IO Capability*:
```c
esp_ble_io_cap_t iocap = ESP_IO_CAP_NONE;//set the IO capability to No Input No Output
```
The possible values for *IO Capabilities* are:
```c
ESP_IO_CAP_OUT 0 /*!< DisplayOnly */
ESP_IO_CAP_IO 1 /*!< DisplayYesNo */
ESP_IO_CAP_IN 2 /*!< KeyboardOnly */
ESP_IO_CAP_NONE 3 /*!< NoInputNoOutput */
ESP_IO_CAP_KBDISP 4 /*!< Keyboard display */
```
* *Authorization Request*:
```c
esp_ble_auth_req_t auth_req = ESP_LE_AUTH_BOND;
//bonding with peer device after authentication
```
The possible values for *Authorization Request* are a combination of Bonding, MITM protection and Secure Connections requests:
```c
ESP_LE_AUTH_NO_BOND: No bonding.
ESP_LE_AUTH_BOND: Bonding is performed.
ESP_LE_AUTH_REQ_MITM: MITM Protection is enabled.
ESP_LE_AUTH_REQ_SC_ONLY: Secure Connections without bonding enabled.
ESP_LE_AUTH_REQ_SC_BOND: Secure Connections with bonding enabled.
ESP_LE_AUTH_REQ_SC_MITM: Secure Connections with MITM Protection and no bonding enabled.
ESP_LE_AUTH_REQ_SC_MITM_BOND: Secure Connections with MITM Protection and bonding enabled.
```
* *Maximum Encryption Key Size*:
```c
uint8_t key_size = 16; //the key size should be 7~16 bytes
```
* *Initiator Key Distribution/Generation*:
```c
uint8_t init_key = ESP_BLE_ENC_KEY_MASK | ESP_BLE_ID_KEY_MASK;
```
The initiator distributes the LTK and IRK keys by the setting the EncKey and IdKey masks.
* *Responder Key Distribution/Generation*:
```c
uint8_t rsp_key = ESP_BLE_ENC_KEY_MASK | ESP_BLE_ID_KEY_MASK;
```
The responder distributes the LTK and IRK keys by the setting the *EncKey* and *IdKey* masks.
Once defined, the parameters are set using the `esp_ble_gap_set_security_param()` function. This function sets the parameter type, the parameter value and the parameter length:
```c
esp_ble_gap_set_security_param(ESP_BLE_SM_AUTHEN_REQ_MODE, &auth_req, sizeof(uint8_t));
esp_ble_gap_set_security_param(ESP_BLE_SM_IOCAP_MODE, &iocap, sizeof(uint8_t));
esp_ble_gap_set_security_param(ESP_BLE_SM_MAX_KEY_SIZE, &key_size, sizeof(uint8_t));
esp_ble_gap_set_security_param(ESP_BLE_SM_SET_INIT_KEY, &init_key, sizeof(uint8_t));
esp_ble_gap_set_security_param(ESP_BLE_SM_SET_RSP_KEY, &rsp_key, sizeof(uint8_t));
```
This information is enough for the BLE stack to perform the pairing process, including pairing confirmation and key generation. The procedure is invisible to the user and executed automatically by the stack.
## Connecting and Bonding to a Peer Device
The security parameters set previously are stored locally to be used later when the master device connects to the slave. Every time a remote device connects to the local GATT server, the connection event `ESP_GATTS_CONNECT_EVT` is triggered. This event is employed to perform the pairing and bonding process by invoking the `esp_ble_set_encryption()` function which takes as parameters the remote device address and the type of encryption that will be performed. The BLE stack then executes the actual pairing process in the background. In this example, the encryption includes MITM protection.
```c
case ESP_GATTS_CONNECT_EVT:
//start security connect with peer device when receive the connect event sent by the master.
esp_ble_set_encryption(param->connect.remote_bda, ESP_BLE_SEC_ENCRYPT_MITM);
break;
```
The types of encryptions available are:
* `ESP_BLE_SEC_NONE`
* `ESP_BLE_SEC_ENCRYPT`
* `ESP_BLE_SEC_ENCRYPT_NO_MITM`
* `ESP_BLE_SEC_ENCRYPT_MITM`
The difference between `ESP_BLE_SEC_ENCRYPT` and `ESP_BLE_SEC_ENCRYPT_NO_MITM` lies in the fact that a previous connection might have a security level that needs to be upgraded, therefore requires to exchange keys again.
In this example, the I/O capabilities are set to *No Input No Output*, therefore the *Just Works* pairing method, which doesn't not require the generation of a random 6-digit passkey, is used(For details, please refer to the the table below.) The user may modify the example to set the I/O capabilities to use other than *No Input No Output*. Therefore, depending on the I/O capabilities of the remote device, a passkey might be generated on the ESP32 which is presented to the user with the `ESP_GAP_BLE_PASSKEY_NOTIF_EVT`:
```c
case ESP_GAP_BLE_PASSKEY_NOTIF_EVT:
///the app will receive this evt when the IO has Output capability and the peer device IO has Input capability.
///show the passkey number to the user to input it in the peer deivce.
ESP_LOGE(GATTS_TABLE_TAG, "The passkey Notify number:%d", param->ble_security.key_notif.passkey);
break;
```
The combination of input and output capabilities that determine which algorithm is used are:
## Exchanging Keys
When the client connects to the server and the pairing is done successfully, the keys indicated by the `init_key` and `rsp_key` security parameters are exchanged. In this example the following keys are generated and distributed:
* Local device LTK
* Local device IRK
* Local device CSRK
* Peer device LTK
* Peer device IRK
Note that for this example only, the peer device CSRK is not exchanged. For each key exchange message, an `ESP_GAP_BLE_KEY_EVT` event is triggered, which can be used to print the type of key received:
```c
case ESP_GAP_BLE_KEY_EVT:
//shows the ble key info share with peer device to the user.
ESP_LOGI(GATTS_TABLE_TAG, "key type = %s", esp_key_type_to_str(param->ble_security.ble_key.key_type));
break;
```
When the keys are exchanged successfully, the pairing process is done. This triggers an `ESP_GAP_BLE_AUTH_CMPL_EVT` event, which is used to print information such as remote device, address type and pair status:
```c
case ESP_GAP_BLE_AUTH_CMPL_EVT: {
esp_bd_addr_t bd_addr;
memcpy(bd_addr, param->ble_security.auth_cmpl.bd_addr,
sizeof(esp_bd_addr_t));
ESP_LOGI(GATTS_TABLE_TAG, "remote BD_ADDR: %08x%04x",\
(bd_addr[0] << 24) + (bd_addr[1] << 16) + (bd_addr[2] << 8) +
bd_addr[3],
(bd_addr[4] << 8) + bd_addr[5]);
ESP_LOGI(GATTS_TABLE_TAG, "address type = %d",
param->ble_security.auth_cmpl.addr_type);
ESP_LOGI(GATTS_TABLE_TAG, "pair status = %s",
param->ble_security.auth_cmpl.success ? "success" : "fail");
break;
}
```
## Security Permission for Attributes
When defining the attributes of the server, different permissions can be set to the write and read events. Attributes permissions can be:
* Permission to read
* Permission to read with encryption
* Permission to read with encryption and MITM protection
* Permission to write
* Permission to write with encryption
* Permission to write with encryption and MITM protection
* Permission to signed write
* Permission to signed write with MITM protection
These permissions are defined in the API as:
* `ESP_GATT_PERM_READ`
* `ESP_GATT_PERM_READ_ENCRYPTED`
* `ESP_GATT_PERM_READ_ENC_MITM`
* `ESP_GATT_PERM_WRITE`
* `ESP_GATT_PERM_WRITE_ENCRYPTED`
* `ESP_GATT_PERM_WRITE_ENC_MITM`
* `ESP_GATT_PERM_WRITE_SIGNED`
* `ESP_GATT_PERM_WRITE_SIGNED_MITM`
When creating the services table, each attribute can have permissions to read or write, with or without encryption. When an attribute has encrypted permissions and a peer device that does not have the required security clearance tries to read or write to that attribute, the local host sends an Insufficient Authorization Error. In the example, the following attributes are defined with permissions with encryption:
```c
// 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_ENCRYPTED,
sizeof(uint8_t),
sizeof(body_sensor_loc_val),
(uint8_t *)body_sensor_loc_val}
},
// 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_ENCRYPTED|ESP_GATT_PERM_READ_ENCRYPTED,
sizeof(uint8_t),
sizeof(heart_ctrl_point),
(uint8_t *)heart_ctrl_point}
},
```
## Security Requests
During the communication between a master and a slave device, the slave might request to start encryption at any moment by issuing a security request command. This command will trigger an `ESP_GAP_BLE_SEC_REQ_EVT` event on the master, which will reply a positive (true) security response to the peer device to accept the request or a negative (false) one to reject the request. In this example, this event is used to reply a start encryption response by using the `esp_ble_gap_security_rsp()` API call.
```c
case ESP_GAP_BLE_SEC_REQ_EVT:
/* send the positive (true) security response to the peer device to accept the security request.
If not accept the security request, should sent the security response with negative(false) accept value*/
esp_ble_gap_security_rsp(param->ble_security.ble_req.bd_addr, true);
break;
```
## Conclusion
In this document, a review of the security aspects of the GATT Server has been realized. BLE security encompasses Pairing, Bonding and Encryption. In order to establish a secured link between a master and a slave device, security parameters that define the capabilities and features each device possess are set. The combination of features and capabilities of the peer devices results in the selection of the appropriate pairing method which the BLE stack then executes. Immediately, the required keys are generated and exchanged, and the encryption of subsequent messages is started using the AES-128 engine and these keys. These steps trigger different events that are managed by the GATT and GAP event handlers which can be used to print useful information such as the types of keys exchanged and the pairing status. In addition, attribute permissions are appointed to allow only encrypted read and write events when needed. The rest of the Security GATT server functionalities such as defining services and characteristics are implemented in the same way as presented in the GATT Server example.

View file

@ -6,3 +6,5 @@ be connected by client. Run the gatt_client demo, the client demo will automatic
to the gatt_server demo. The client demo will enable gatt_server's notify after connection.
Then the two devices will exchange data.
Please check the [tutorial] (./tutorial/GATT_SERVER_EXAMPLE_WALKTHROUGH.md) for more information about this example.

View file

@ -0,0 +1,948 @@
# Gatt Server Example Walkthrough
## Introduction
In this document, we review the GATT SERVER example code which implements a Bluetooth Low Energy (BLE) Generic Attribute Profile (GATT) Server on the ESP32. This example is designed around two Application Profiles and a series of events that are handled in order to execute a sequence of configuration steps, such as defining advertising parameters, updating connection parameters and creating services and characteristics. In addition, this example handles read and write events, including a Write Long Characteristic request, which divides the incoming data into chunks so that the data can fit in the Attribute Protocol (ATT) message. 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
First, lets take a look at the includes:
```c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#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 "bt.h"
#include "bta_api.h"
#include "esp_gap_ble_api.h"
#include "esp_gatts_api.h"
#include "esp_bt_defs.h"
#include "esp_bt_main.h"
#include "sdkconfig.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_gatts_api.h"`, which expose the BLE APIs required to implement this example.
* `bt.h`: implements BT controller and VHCI configuration procedures from the host side.
* `esp_bt_main.h`: implements initialization and enabling of the Bluedroid stack.
* `esp_gap_ble_api.h`: implements GAP configuration, such as advertising and connection parameters.
* `esp_gatts_api.h`: implements GATT configuration, such as creating services and characteristics.
## Main Entry Point
The entry point to this example is the app_main() function:
```c
void app_main()
{
esp_err_t ret;
// Initialize NVS.
ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES) {
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(GATTS_TAG, "%s initialize controller failed\n", __func__);
return;
}
ret = esp_bt_controller_enable(ESP_BT_MODE_BTDM);
if (ret) {
ESP_LOGE(GATTS_TAG, "%s enable controller failed\n", __func__);
return;
}
ret = esp_bluedroid_init();
if (ret) {
ESP_LOGE(GATTS_TAG, "%s init bluetooth failed\n", __func__);
return;
}
ret = esp_bluedroid_enable();
if (ret) {
ESP_LOGE(GATTS_TAG, "%s enable bluetooth failed\n", __func__);
return;
}
ret = esp_ble_gatts_register_callback(gatts_event_handler);
if (ret){
ESP_LOGE(GATTS_TAG, "gatts register error, error code = %x", ret);
return;
}
ret = esp_ble_gap_register_callback(gap_event_handler);
if (ret){
ESP_LOGE(GATTS_TAG, "gap register error, error code = %x", ret);
return;
}
ret = esp_ble_gatts_app_register(PROFILE_A_APP_ID);
if (ret){
ESP_LOGE(GATTS_TAG, "gatts app register error, error code = %x", ret);
return;
}
ret = esp_ble_gatts_app_register(PROFILE_B_APP_ID);
if (ret){
ESP_LOGE(GATTS_TAG, "gatts app register error, error code = %x", ret);
return;
}
esp_err_t local_mtu_ret = esp_ble_gatt_set_local_mtu(512);
if (local_mtu_ret){
ESP_LOGE(GATTS_TAG, "set local MTU failed, error code = %x", local_mtu_ret);
}
return;
}
```
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
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:
```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 Dual Mode (BLE + BT Classic).
```c
ret = esp_bt_controller_enable(ESP_BT_MODE_BTDM);
```
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 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:
```c
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
The GATT Server example application is organized by using Application Profiles as shown in Fig 1. Each Application Profile describes a way to group functionalities that are designed for one client application, such as a mobile app running on a smartphone or tablet. In this way, a single design, enabled by different Application Profiles, can behave differently when used by different smartphone apps, allowing the server to react differently according to the client app that is being used. In practice, each profile is seen by the client as an independent BLE service. It is up to the client to discriminate the services that it is interested in.
![](image/GATT SERVER FIGURE 1.png)
<p align="center">Fig.1. Application Profiles are used to organize a BLE application in order to implement different functionality for different clients.</p>
Each profile is defined as a struct where the struct members depend on the services and characteristics that are implemented in that Application Profile. The members also include a GATT interface, Application ID, Connection ID and a callback function to handle profile events. In this example, each profile is composed by:
* GATT interface
* Application ID
* Connection ID
* Service handle
* Service ID
* Characteristic handle
* Characteristic UUID
* Attribute permissions
* Characteristic properties
* Client Characteristic Configuration descriptor handle
* Client Characteristic Configuration descriptor UUID
It can be observed from this structure that this profile was designed to have one service and one characteristic, and that the characteristic has one descriptor. The service has a handle and an ID, in the same manner that each characteristic has a handle, an UUID, attribute permissions and properties. In addition, if the characteristic supports notifications or indications, it must implement a Client Characteristic Configuration descriptor (CCCD), which is an additional attribute that describes if the notifications or indications are enabled and defines how the characteristic may be configured by a specific client. This descriptor also has a handle and an UUID.
The structure implementation is:
```c
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;
};
```
The Application Profiles are stored in an array and corresponding callback functions `gatts_profile_a_event_handler()` and `gatts_profile_b_event_handler()` are assigned. Different applications on the GATT client use different interfaces, represented by the gatts_if parameter. For initialization, this parameter is set to `ESP_GATT_IF_NONE`, which means that the Application Profile is not linked to any client yet.
```c
static struct gatts_profile_inst gl_profile_tab[PROFILE_NUM] = {
[PROFILE_A_APP_ID] = {
.gatts_cb = gatts_profile_a_event_handler,
.gatts_if = ESP_GATT_IF_NONE,
[PROFILE_B_APP_ID] = {
.gatts_cb = gatts_profile_b_event_handler,
.gatts_if = ESP_GATT_IF_NONE,
},
};
```
Finally, the Application Profiles are registered using the Application ID, which is an user-assigned number to identify each profile. In this way, multiple Application Profiles can run in one server.
```c
esp_ble_gatts_app_register(PROFILE_A_APP_ID);
esp_ble_gatts_app_register(PROFILE_B_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 the Profile A GATT event handle to configure the advertising parameters upon registration. This example has the option to use both standard Bluetooth Core Specification advertising parameters or a customized raw buffer. The option can be selected with the `CONFIG_SET_RAW_ADV_DATA` define. The raw advertising data can be used to implement iBeacons, Eddystone or other proprietaries, and custom frame types such as the ones used for Indoor Location Services that are different from the standard specifications.
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:
```c
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 advertising min interval */
int max_interval; /*!< Advertising data show advertising 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:
```c
static esp_ble_adv_data_t adv_data = {
.set_scan_rsp = false,
.include_name = true,
.include_txpower = true,
.min_interval = 0x20,
.max_interval = 0x40,
.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 = 32,
.p_service_uuid = test_service_uuid128,
.flag = (ESP_BLE_ADV_FLAG_GEN_DISC | ESP_BLE_ADV_FLAG_BREDR_NOT_SPT),
};
```
The minimum and maximum advertisement intervals are set as multiples of 1.25 ms. In this example, the minimum advertisement interval is defined as 0x20 * 1.25 ms = 40 ms, and the maximum advertisement interval is initialized as 0x40 * 1.25 ms = 80 ms.
An advertising payload can be up to 31 bytes of data. It is possible the parameter data is large enough to surpass the 31-byte advertisement packet limit, which causes the stack to cut the advertisement packet and leave some of the parameters out. This behavior can be demonstrated in this example if the manufacturer length and data are uncommented, which makes the services to not be advertised after recompiling and testing.
It is possible to also advertise customized raw data using the `esp_ble_gap_config_adv_data_raw()`
and `esp_ble_gap_config_scan_rsp_data_raw()` functions, which require to create and pass a buffer for both advertising data and scanning response data. In this example, the raw data is represented by the `raw_adv_data[]` and `raw_scan_rsp_data[]` arrays.
Finally, to set the device name, the `esp_ble_gap_set_device_name()` function is used. The registering event handler is shown as follows:
```c
static void gatts_profile_a_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param) {
switch (event) {
case ESP_GATTS_REG_EVT:
ESP_LOGI(GATTS_TAG, "REGISTER_APP_EVT, status %d, app_id %d\n", param->reg.status, param->reg.app_id);
gl_profile_tab[PROFILE_A_APP_ID].service_id.is_primary = true;
gl_profile_tab[PROFILE_A_APP_ID].service_id.id.inst_id = 0x00;
gl_profile_tab[PROFILE_A_APP_ID].service_id.id.uuid.len = ESP_UUID_LEN_16;
gl_profile_tab[PROFILE_A_APP_ID].service_id.id.uuid.uuid.uuid16 = GATTS_SERVICE_UUID_TEST_A;
esp_ble_gap_set_device_name(TEST_DEVICE_NAME);
#ifdef CONFIG_SET_RAW_ADV_DATA
esp_err_t raw_adv_ret = esp_ble_gap_config_adv_data_raw(raw_adv_data, sizeof(raw_adv_data));
if (raw_adv_ret){
ESP_LOGE(GATTS_TAG, "config raw adv data failed, error code = %x ", raw_adv_ret);
}
adv_config_done |= adv_config_flag;
esp_err_t raw_scan_ret = esp_ble_gap_config_scan_rsp_data_raw(raw_scan_rsp_data, sizeof(raw_scan_rsp_data));
if (raw_scan_ret){
ESP_LOGE(GATTS_TAG, "config raw scan rsp data failed, error code = %x", raw_scan_ret);
}
adv_config_done |= scan_rsp_config_flag;
#else
//config adv data
esp_err_t ret = esp_ble_gap_config_adv_data(&adv_data);
if (ret){
ESP_LOGE(GATTS_TAG, "config adv data failed, error code = %x", ret);
}
adv_config_done |= adv_config_flag;
//config scan response data
ret = esp_ble_gap_config_adv_data(&scan_rsp_data);
if (ret){
ESP_LOGE(GATTS_TAG, "config scan response data failed, error code = %x", ret);
}
adv_config_done |= scan_rsp_config_flag;
#endif
```
## GAP Event Handler
Once the advertising data have been set, the GAP event `ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT` is triggered. For the case of raw advertising data set, the event triggered is `ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT`. Additionally when the raw scan response data is set, `ESP_GAP_BLE_SCAN_RSP_DATA_RAW_SET_COMPLETE_EVT` event is triggered.
```c
static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param)
{
switch (event) {
#ifdef CONFIG_SET_RAW_ADV_DATA
case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT:
adv_config_done &= (~adv_config_flag);
if (adv_config_done==0){
esp_ble_gap_start_advertising(&adv_params);
}
break;
case ESP_GAP_BLE_SCAN_RSP_DATA_RAW_SET_COMPLETE_EVT:
adv_config_done &= (~scan_rsp_config_flag);
if (adv_config_done==0){
esp_ble_gap_start_advertising(&adv_params);
}
break;
#else
case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT:
adv_config_done &= (~adv_config_flag);
if (adv_config_done == 0){
esp_ble_gap_start_advertising(&adv_params);
}
break;
case ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT:
adv_config_done &= (~scan_rsp_config_flag);
if (adv_config_done == 0){
esp_ble_gap_start_advertising(&adv_params);
}
break;
#endif
```
In any case, the server can start advertising using the `esp_ble_gap_start_advertising()` function, which takes a structure of type `esp_ble_adv_params_t` with the advertising parameters required by the stack to operate:
```c
/// 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 will be 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 GAP to execute.
For this example, the advertisement parameters are initialized as follows:
```c
static esp_ble_adv_params_t test_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 40 ms to 80 ms. The advertisement is `ADV_IND`, which is a generic, not directed to a particular central device and connectable type. 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. Otherwise, an error message is printed.
```c
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_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 used to store the generated interface in the profile table, and then the event is forwarded to the corresponding profile event handler.
```c
static void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param)
{
/* 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) {
gl_profile_tab[param->reg.app_id].gatts_if = gatts_if;
} else {
ESP_LOGI(GATTS_TAG, "Reg app failed, app_id %04x, status %d\n",
param->reg.app_id,
param->reg.status);
return;
}
}
/* If the gatts_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 (gatts_if == ESP_GATT_IF_NONE||gatts_if == gl_profile_tab[idx].gatts_if) {
if (gl_profile_tab[idx].gatts_cb) {
gl_profile_tab[idx].gatts_cb(event, gatts_if, param);
}
}
}
} while (0);
}
```
## Creating Services
The register event is also used to create a profile attributes table by using the `esp_ble_gatts_create_attr_tab()` function, which takes an `esp_gatts_attr_db_t` type structure that has all the information of the service table. The way to create this table is:
```c
esp_ble_gatts_create_service(gatts_if, &gl_profile_tab[PROFILE_A_APP_ID].service_id, GATTS_NUM_HANDLE_TEST_A);
break;
```
The number of handles is defined as 4:
```c
#define GATTS_NUM_HANDLE_TEST_A 4
```
The handles are:
1. Service handle
2. Characteristic handle
3. Characteristic value handle
4. Characteristic descriptor handle
The service is defined as a primary service with a UUID length of 16 bits. The service ID is initialized with instance ID = 0 and UUID defined by `GATTS_SERVICE_UUID_TEST_A`.
The service instance ID can be used to differentiate multiple services with the same UUID. In this example, since there is only one service for each Application Profiles and the services have different UUIDs, the service instance ID can be defined as 0 in both profile A and B. However if there was only one Application Profile with two services using the same UUID, then it would be necessary to use different instance IDs to refer to one service or the other.
Application Profile B creates the service in the same way as Profile A:
```c
static void gatts_profile_b_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param) {
switch (event) {
case ESP_GATTS_REG_EVT:
ESP_LOGI(GATTS_TAG, "REGISTER_APP_EVT, status %d, app_id %d\n", param->reg.status, param->reg.app_id);
gl_profile_tab[PROFILE_B_APP_ID].service_id.is_primary = true;
gl_profile_tab[PROFILE_B_APP_ID].service_id.id.inst_id = 0x00;
gl_profile_tab[PROFILE_B_APP_ID].service_id.id.uuid.len = ESP_UUID_LEN_16;
gl_profile_tab[PROFILE_B_APP_ID].service_id.id.uuid.uuid.uuid16 = GATTS_SERVICE_UUID_TEST_B;
esp_ble_gatts_create_service(gatts_if, &gl_profile_tab[PROFILE_B_APP_ID].service_id, GATTS_NUM_HANDLE_TEST_B);
break;
}
```
## Starting Services and Creating Characteristics
When a service is created successfully, an `ESP_GATTS_CREATE_EVT` event managed by the profile GATT handler is triggered, and can be used to start the service and add characteristics to the service. For the case of Profile A, the service is started and the characteristics are added as follows:
```c
case ESP_GATTS_CREATE_EVT:
ESP_LOGI(GATTS_TAG, "CREATE_SERVICE_EVT, status %d, service_handle %d\n", param->create.status, param->create.service_handle);
gl_profile_tab[PROFILE_A_APP_ID].service_handle = param->create.service_handle;
gl_profile_tab[PROFILE_A_APP_ID].char_uuid.len = ESP_UUID_LEN_16;
gl_profile_tab[PROFILE_A_APP_ID].char_uuid.uuid.uuid16 = GATTS_CHAR_UUID_TEST_A;
esp_ble_gatts_start_service(gl_profile_tab[PROFILE_A_APP_ID].service_handle);
a_property = ESP_GATT_CHAR_PROP_BIT_READ | ESP_GATT_CHAR_PROP_BIT_WRITE | ESP_GATT_CHAR_PROP_BIT_NOTIFY;
esp_err_t add_char_ret =
esp_ble_gatts_add_char(gl_profile_tab[PROFILE_A_APP_ID].service_handle,
&gl_profile_tab[PROFILE_A_APP_ID].char_uuid,
ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE,
a_property,
&gatts_demo_char1_val,
NULL);
if (add_char_ret){
ESP_LOGE(GATTS_TAG, "add char failed, error code =%x",add_char_ret);
}
break;
```
First, the service handle generated by the BLE stack is stored in the profile table, which will be used later by the application layer to refer to this service. Then, the UUID of the characteristic and its UUID length are set. The length of the characteristic UUID is again 16 bits. The service is started using the `esp_ble_gatts_start_service()` function with the service handle previously generated. An `ESP_GATTS_START_EVT` event, which is used to print information, is triggered. The characteristic is added to the service by the `esp_ble_gatts_start_service()` function in conjunction with the characteristics permissions and properties. In this example, the characteristics in both profiles are set up as follows:
Permissions:
* `ESP_GATT_PERM_READ`: To read characteristic value is permitted
* `ESP_GATT_PERM_WRITE`: To write characteristic value is permitted
Properties:
* `ESP_GATT_CHAR_PROP_BIT_READ`: Characteristic can be read
* `ESP_GATT_CHAR_PROP_BIT_WRITE`: Characteristic can be written
* `ESP_GATT_CHAR_PROP_BIT_NOTIFY`: Characteristic can notify value changes
It might seem redundant to have both permissions and properties for read and write. However, the read and write properties of an attribute are information that is shown to the client in order to let the client know if the server accepts read and write requests. In that sense, the properties serve as a hint to the client to properly access the server resources. On the other hand, permissions are the authorization that is given to the client to either read or write that attribute. For example, if a client tries to write an attribute for which it does not have the write permission, the server will reject that request, even if the write property is set.
In addition, this example gives an initial value to the characteristic represented by `gatts_demo_char1_val`. The initial value is defined as follows:
```c
esp_attr_value_t gatts_demo_char1_val =
{
.attr_max_len = GATTS_DEMO_CHAR_VAL_LEN_MAX,
.attr_len = sizeof(char1_str),
.attr_value = char1_str,
};
```
Where `char1_str` is dummy data:
```c
uint8_t char1_str[] = {0x11,0x22,0x33};
```
and the characteristic length is defined as:
```c
#define GATTS_DEMO_CHAR_VAL_LEN_MAX 0x40
```
The characteristic initial value must be a non-null object and the characteristic length must always be greater than zero, otherwise the stack will return an error.
Finally, the characteristic is configured in a way that it is required to send a response manually every time the characteristic is read or written, instead of letting the stack auto respond. This is configured by setting the last parameter of the `esp_ble_gatts_add_char()` function, representing the attribute response control parameter, to `ESP_GATT_RSP_BY_APP` or NULL.
## Creating the Characteristic Descriptor
Adding a characteristic to a service triggers an `ESP_GATTS_ADD_CHAR_EVT` event, which returns the handle generated by the stack for the characteristic just added. The event includes the following parameters:
* `esp_gatt_status_t status;     /*!< Operation status */`
* `uint16_t attr_handle;       /*!< Characteristic attribute handle */`
* `uint16_t service_handle;     /*!< Service attribute handle */`
* `esp_bt_uuid_t char_uuid;     /*!< Characteristic uuid */`
The attribute handle returned by the event is stored in the profile table and the characteristic descriptor length and UUID are set as well. The characteristic length and value are read using the `esp_ble_gatts_get_attr_value()` function, and then printed for information purposes. Finally, the characteristic description is added using the `esp_ble_gatts_add_char_descr()` function. The parameters used are the service handle, the descriptor UUID, write and read permissions, an initial value and the auto response setting. The initial value for the characteristic descriptor can be a NULL pointer and the auto response parameter is set to NULL as well, which means that requests that require responses have to be replied manually.
```c
case ESP_GATTS_ADD_CHAR_EVT: {
uint16_t length = 0;
const uint8_t *prf_char;
ESP_LOGI(GATTS_TAG, "ADD_CHAR_EVT, status %d, attr_handle %d, service_handle %d\n",
param->add_char.status, param->add_char.attr_handle, param->add_char.service_handle);
gl_profile_tab[PROFILE_A_APP_ID].char_handle = param->add_char.attr_handle;
gl_profile_tab[PROFILE_A_APP_ID].descr_uuid.len = ESP_UUID_LEN_16;
gl_profile_tab[PROFILE_A_APP_ID].descr_uuid.uuid.uuid16 = ESP_GATT_UUID_CHAR_CLIENT_CONFIG;
esp_err_t get_attr_ret = esp_ble_gatts_get_attr_value(param->add_char.attr_handle, &length, &prf_char);
if (get_attr_ret == ESP_FAIL){
ESP_LOGE(GATTS_TAG, "ILLEGAL HANDLE");
}
ESP_LOGI(GATTS_TAG, "the gatts demo char length = %x\n", length);
for(int i = 0; i < length; i++){
ESP_LOGI(GATTS_TAG, "prf_char[%x] = %x\n",i,prf_char[i]);
}
esp_err_t add_descr_ret = esp_ble_gatts_add_char_descr(
gl_profile_tab[PROFILE_A_APP_ID].service_handle,
&gl_profile_tab[PROFILE_A_APP_ID].descr_uuid,
ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE,
NULL,NULL);
if (add_descr_ret){
ESP_LOGE(GATTS_TAG, "add char descr failed, error code = %x", add_descr_ret);
}
break;
}
```
Once the descriptor is added, the `ESP_GATTS_ADD_CHAR_DESCR_EVT` event is triggered, which in this example is used to print an information message.
```c
case ESP_GATTS_ADD_CHAR_DESCR_EVT:
ESP_LOGI(GATTS_TAG, "ADD_DESCR_EVT, status %d, attr_handle %d, service_handle %d\n",
param->add_char.status, param->add_char.attr_handle,
param->add_char.service_handle);
break;
```
This procedure is repeated in the Profile B event handler, in order to create the services and characteristics for that profile.
## Connection Event
An `ESP_GATTS_CONNECT_EVT` is triggered when a client has connected to the GATT server. This event is used to update the connection parameters, such as latency, minimum connection interval, maximum connection interval and time out. The connection parameters are stored into an `esp_ble_conn_update_params_t` structure, and then passed to the `esp_ble_gap_update_conn_params()` function. The update connection parameters procedure needs to be done only once, therefore the Profile B connection event handler does not include the `esp_ble_gap_update_conn_params()` function. Finally, the connection ID returned by the event is stored in the profile table.
Profile A connection event:
```c
case ESP_GATTS_CONNECT_EVT: {
esp_ble_conn_update_params_t conn_params = {0};
memcpy(conn_params.bda, param->connect.remote_bda, sizeof(esp_bd_addr_t));
/* For the IOS system, please reference the apple official documents about the ble connection parameters restrictions. */
conn_params.latency = 0;
conn_params.max_int = 0x30; // max_int = 0x30*1.25ms = 40ms
conn_params.min_int = 0x10; // min_int = 0x10*1.25ms = 20ms
conn_params.timeout = 400; // timeout = 400*10ms = 4000ms
ESP_LOGI(GATTS_TAG, "ESP_GATTS_CONNECT_EVT, conn_id %d, remote %02x:%02x:%02x:%02x:%02x:%02x:, is_conn %d",
param->connect.conn_id,
param->connect.remote_bda[0],
param->connect.remote_bda[1],
param->connect.remote_bda[2],
param->connect.remote_bda[3],
param->connect.remote_bda[4],
param->connect.remote_bda[5],
param->connect.is_connected);
gl_profile_tab[PROFILE_A_APP_ID].conn_id = param->connect.conn_id;
//start sent the update connection parameters to the peer device.
esp_ble_gap_update_conn_params(&conn_params);
break;
}
```
Profile B connection event:
```c
case ESP_GATTS_CONNECT_EVT:
ESP_LOGI(GATTS_TAG, "CONNECT_EVT, conn_id %d, remote %02x:%02x:%02x:%02x:%02x:%02x:, is_conn %d\n",
param->connect.conn_id,
param->connect.remote_bda[0],
param->connect.remote_bda[1],
param->connect.remote_bda[2],
param->connect.remote_bda[3],
param->connect.remote_bda[4],
param->connect.remote_bda[5],
param->connect.is_connected);
gl_profile_tab[PROFILE_B_APP_ID].conn_id = param->connect.conn_id;
break;
```
The `esp_ble_gap_update_conn_params()` function triggers a GAP event `ESP_GAP_BLE_UPDATE_CONN_PARAMS_EVT`, which is used to print the connection information:
```c
case ESP_GAP_BLE_UPDATE_CONN_PARAMS_EVT:
ESP_LOGI(GATTS_TAG, "update connetion params status = %d, min_int = %d, max_int = %d,
conn_int = %d,latency = %d, timeout = %d",
param->update_conn_params.status,
param->update_conn_params.min_int,
param->update_conn_params.max_int,
param->update_conn_params.conn_int,
param->update_conn_params.latency,
param->update_conn_params.timeout);
break;
```
## Managing Read Events
Now that the services and characteristics are created and started, the program can receive read and write events. The read operations are represented by the `ESP_GATTS_READ_EVT` event, which has the following parameters:
* `uint16_t conn_id;     /*!< Connection id */`
* `uint32_t trans_id;     /*!< Transfer id */`
* `esp_bd_addr_t bda;     /*!< The bluetooth device address which been read */`
* `uint16_t handle;      /*!< The attribute handle */`
* `uint16_t offset;      /*!< Offset of the value, if the value is too long */`
* `bool is_long;        /*!< The value is too long or not */`
* `bool need_rsp;       /*!< The read operation need to do response */`
In this example, a response is constructed with dummy data and sent back to the host using the same handle given by the event. In addition to the response, the GATT interface, the connection ID and the transfer ID are also included as parameters in the `esp_ble_gatts_send_response()` function. This function is necessary if the auto response byte is set to NULL when creating the characteristic or descriptor.
```c
case ESP_GATTS_READ_EVT: {
ESP_LOGI(GATTS_TAG, "GATT_READ_EVT, conn_id %d, trans_id %d, handle %d\n",
param->read.conn_id, param->read.trans_id, param->read.handle);
esp_gatt_rsp_t rsp;
memset(&rsp, 0, sizeof(esp_gatt_rsp_t));
rsp.attr_value.handle = param->read.handle;
rsp.attr_value.len = 4;
rsp.attr_value.value[0] = 0xde;
rsp.attr_value.value[1] = 0xed;
rsp.attr_value.value[2] = 0xbe;
rsp.attr_value.value[3] = 0xef;
esp_ble_gatts_send_response(gatts_if,
param->read.conn_id,
param->read.trans_id,
ESP_GATT_OK, &rsp);
break;
}
```
## Managing Write Events
The write events are represented by the `ESP_GATTS_WRITE_EVT` event, which has the following parameters:
* `uint16_t conn_id;     /*!< Connection id */`
* `uint32_t trans_id;    /*!< Transfer id */`
* `esp_bd_addr_t bda;    /*!< The bluetooth device address which been written */`
* `uint16_t handle;     /*!< The attribute handle */`
* `uint16_t offset;     /*!< Offset of the value, if the value is too long */`
* `bool need_rsp;      /*!< The write operation need to do response */`
* `bool is_prep;       /*!< This write operation is prepare write */`
* `uint16_t len;       /*!< The write attribute value length */`
* `uint8_t *value;      /*!< The write attribute value */`
There are two types of write events implemented in this example, write characteristic value and write long characteristic value. The first type of write is used when the characteristic value can fit in one Attribute Protocol Maximum Transmission Unit (ATT MTU), which is usually 23 bytes long. The second type is used when the attribute to write is longer than what can be sent in one single ATT message by dividing the data into multiple chunks using Prepare Write Responses, after which an Executive Write Request is used to confirm or cancel the complete write request. This behavior is defined in the [Bluetooth Specification Version 4.2](https://www.bluetooth.com/specifications/bluetooth-core-specification), Vol 3, Part G, section 4.9. The write long characteristic message flow is shown in Fig. 2.
When a write event is triggered, this example prints logging messages, and then executes the `example_write_event_env()` function.
```c
case ESP_GATTS_WRITE_EVT: {
ESP_LOGI(GATTS_TAG, "GATT_WRITE_EVT, conn_id %d, trans_id %d, handle %d\n", param->write.conn_id, param->write.trans_id, param->write.handle);
if (!param->write.is_prep){
ESP_LOGI(GATTS_TAG, "GATT_WRITE_EVT, value len %d, value :", param->write.len);
esp_log_buffer_hex(GATTS_TAG, param->write.value, param->write.len);
if (gl_profile_tab[PROFILE_B_APP_ID].descr_handle == param->write.handle && param->write.len == 2){
uint16_t descr_value= param->write.value[1]<<8 | param->write.value[0];
if (descr_value == 0x0001){
if (b_property & ESP_GATT_CHAR_PROP_BIT_NOTIFY){
ESP_LOGI(GATTS_TAG, "notify enable");
uint8_t notify_data[15];
for (int i = 0; i < sizeof(notify_data); ++i)
{
notify_data[i] = i%0xff;
}
//the size of notify_data[] need less than MTU size
esp_ble_gatts_send_indicate(gatts_if, param->write.conn_id,
gl_profile_tab[PROFILE_B_APP_ID].char_handle,
sizeof(notify_data),
notify_data, false);
}
}else if (descr_value == 0x0002){
if (b_property & ESP_GATT_CHAR_PROP_BIT_INDICATE){
ESP_LOGI(GATTS_TAG, "indicate enable");
uint8_t indicate_data[15];
for (int i = 0; i < sizeof(indicate_data); ++i)
{
indicate_data[i] = i % 0xff;
}
//the size of indicate_data[] need less than MTU size
esp_ble_gatts_send_indicate(gatts_if, param->write.conn_id,
gl_profile_tab[PROFILE_B_APP_ID].char_handle,
sizeof(indicate_data),
indicate_data, true);
}
}
else if (descr_value == 0x0000){
ESP_LOGI(GATTS_TAG, "notify/indicate disable ");
}else{
ESP_LOGE(GATTS_TAG, "unknown value");
}
}
}
example_write_event_env(gatts_if, &a_prepare_write_env, param);
break;
}
```
![](image/GATT SERVER FIGURE 2.jpg)
<p align="center">Fig.2. Message flow for Write Long Characteristic</p>
The `example_write_event_env()` function contains the logic for the write long characteristic procedure:
```c
void example_write_event_env(esp_gatt_if_t gatts_if, prepare_type_env_t *prepare_write_env, esp_ble_gatts_cb_param_t *param){
esp_gatt_status_t status = ESP_GATT_OK;
if (param->write.need_rsp){
if (param->write.is_prep){
if (prepare_write_env->prepare_buf == NULL){
prepare_write_env->prepare_buf = (uint8_t *)malloc(PREPARE_BUF_MAX_SIZE*sizeof(uint8_t));
prepare_write_env->prepare_len = 0;
if (prepare_write_env->prepare_buf == NULL) {
ESP_LOGE(GATTS_TAG, "Gatt_server prep no mem\n");
status = ESP_GATT_NO_RESOURCES;
}
} else {
if(param->write.offset > PREPARE_BUF_MAX_SIZE) {
status = ESP_GATT_INVALID_OFFSET;
}
else if ((param->write.offset + param->write.len) > PREPARE_BUF_MAX_SIZE) {
status = ESP_GATT_INVALID_ATTR_LEN;
}
}
esp_gatt_rsp_t *gatt_rsp = (esp_gatt_rsp_t *)malloc(sizeof(esp_gatt_rsp_t));
gatt_rsp->attr_value.len = param->write.len;
gatt_rsp->attr_value.handle = param->write.handle;
gatt_rsp->attr_value.offset = param->write.offset;
gatt_rsp->attr_value.auth_req = ESP_GATT_AUTH_REQ_NONE;
memcpy(gatt_rsp->attr_value.value, param->write.value, param->write.len);
esp_err_t response_err = esp_ble_gatts_send_response(gatts_if, param->write.conn_id,
param->write.trans_id, status, gatt_rsp);
if (response_err != ESP_OK){
ESP_LOGE(GATTS_TAG, "Send response error\n");
}
free(gatt_rsp);
if (status != ESP_GATT_OK){
return;
}
memcpy(prepare_write_env->prepare_buf + param->write.offset,
param->write.value,
param->write.len);
prepare_write_env->prepare_len += param->write.len;
}else{
esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id, status, NULL);
}
}
}
```
When the client sends a Write Request or a Prepare Write Request, the server shall respond. However, if the client sends a Write Without Response command, the server does not need to reply back a response. This is checked in the write procedure by examining the value of the `write.need_rsp parameter`. If a response is needed, the procedure continues doing the response preparation, if not present, the client does not need a response and therefore the procedure is ended.
```c
void example_write_event_env(esp_gatt_if_t gatts_if, prepare_type_env_t *prepare_write_env,
esp_ble_gatts_cb_param_t *param){
esp_gatt_status_t status = ESP_GATT_OK;
if (param->write.need_rsp){
```
The function then checks if the Prepare Write Request parameter represented by the `write.is_prep` is set, which means that the client is requesting a Write Long Characteristic. If present, the procedure continues with the preparation of multiple write responses, if not present, then the server simply sends a single write response back.
```c
if (param->write.is_prep){
}else{
esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id, status, NULL);
}
```
To handle long characteristic write, a prepare buffer structure is defined and instantiated:
```c
typedef struct {
uint8_t *prepare_buf;
int prepare_len;
} prepare_type_env_t;
static prepare_type_env_t a_prepare_write_env;
static prepare_type_env_t b_prepare_write_env;
```
In order to use the prepare buffer, some memory space is allocated for it. In case the allocation fails due to a lack of memory, an error is printed:
```c
if (prepare_write_env->prepare_buf == NULL) {
prepare_write_env->prepare_buf =
(uint8_t*)malloc(PREPARE_BUF_MAX_SIZE*sizeof(uint8_t));
prepare_write_env->prepare_len = 0;
if (prepare_write_env->prepare_buf == NULL) {
ESP_LOGE(GATTS_TAG, "Gatt_server prep no mem\n");
status = ESP_GATT_NO_RESOURCES;
}
}
```
If the buffer is not NULL, which means initialization completed, the procedure then checks if the offset and message length of the incoming write fit the buffer.
```c
else {
if(param->write.offset > PREPARE_BUF_MAX_SIZE) {
status = ESP_GATT_INVALID_OFFSET;
}
else if ((param->write.offset + param->write.len) > PREPARE_BUF_MAX_SIZE) {
status = ESP_GATT_INVALID_ATTR_LEN;
}
}
```
The procedure now prepares the response of type `esp_gatt_rsp_t` to be sent back to the client. The response it constructed using the same parameters of the write request, such as length, handle and offset. In addition, the GATT authentication type needed to write to this characteristic is set with `ESP_GATT_AUTH_REQ_NONE`, which means that the client can write to this characteristic without needing to authenticate first. Once the response is sent, the memory allocated for its use is freed.
```c
esp_gatt_rsp_t *gatt_rsp = (esp_gatt_rsp_t *)malloc(sizeof(esp_gatt_rsp_t));
gatt_rsp->attr_value.len = param->write.len;
gatt_rsp->attr_value.handle = param->write.handle;
gatt_rsp->attr_value.offset = param->write.offset;
gatt_rsp->attr_value.auth_req = ESP_GATT_AUTH_REQ_NONE;
memcpy(gatt_rsp->attr_value.value, param->write.value, param->write.len);
esp_err_t response_err = esp_ble_gatts_send_response(gatts_if, param->write.conn_id,
param->write.trans_id, status, gatt_rsp);
if (response_err != ESP_OK){
ESP_LOGE(GATTS_TAG, "Send response error\n");
}
free(gatt_rsp);
if (status != ESP_GATT_OK){
return;
}
```
Finally, the incoming data is copied to the buffer created and its length is incremented by the offset:
```c
memcpy(prepare_write_env->prepare_buf + param->write.offset,
param->write.value,
param->write.len);
prepare_write_env->prepare_len += param->write.len;
```
The client finishes the long write sequence by sending an Executive Write Request. This command triggers an `ESP_GATTS_EXEC_WRITE_EVT` event. The server handles this event by sending a response and executing the `example_exec_write_event_env()` function:
```c
case ESP_GATTS_EXEC_WRITE_EVT:
ESP_LOGI(GATTS_TAG,"ESP_GATTS_EXEC_WRITE_EVT");
esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id, ESP_GATT_OK, NULL);
example_exec_write_event_env(&a_prepare_write_env, param);
break;
```
Lets take a look at the Executive Write function:
```c
void example_exec_write_event_env(prepare_type_env_t *prepare_write_env, esp_ble_gatts_cb_param_t *param){
if (param->exec_write.exec_write_flag == ESP_GATT_PREP_WRITE_EXEC){
esp_log_buffer_hex(GATTS_TAG, prepare_write_env->prepare_buf, prepare_write_env->prepare_len);
}
else{
ESP_LOGI(GATTS_TAG,"ESP_GATT_PREP_WRITE_CANCEL");
}
if (prepare_write_env->prepare_buf) {
free(prepare_write_env->prepare_buf);
prepare_write_env->prepare_buf = NULL;
}
prepare_write_env->prepare_len = 0;
}
```
The executive write is used to either confirm or cancel the write procedure done before, by the Long Characteristic Write procedure. In order to do this, the function checks for the `exec_write_flag` in the parameters received with the event. If the flag equals the execute flag represented by `exec_write_flag`, the write is confirmed and the buffer is printed in the log; if not, then it means the write is canceled and all the data that has been written is deleted.
```c
if (param->exec_write.exec_write_flag == ESP_GATT_PREP_WRITE_EXEC){
esp_log_buffer_hex(GATTS_TAG,
prepare_write_env->prepare_buf,
prepare_write_env->prepare_len);
}
else{
ESP_LOGI(GATTS_TAG,"ESP_GATT_PREP_WRITE_CANCEL");
}
```
Finally, the buffer structure that was created to store the chunks of data from the long write operation is freed and its pointer is set to NULL to leave it ready for the next long write procedure.
```c
if (prepare_write_env->prepare_buf) {
free(prepare_write_env->prepare_buf);
prepare_write_env->prepare_buf = NULL;
}
prepare_write_env->prepare_len = 0;
```
## Conclusion
In this document, we have gone through the GATT SERVER example code describing each section. The application is designed around the concept of Application Profiles. In addition, the procedures that this example uses to register event handlers are explained. The events follow a sequence of configuration steps, such as defining advertising parameters, updating connection parameters and creating services and characteristics. Finally, the way in which read and write events are handled, including Write Long characteristics by dividing the write into chunks that can fit the Attribute Protocol message is explained.

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

View file

@ -8,3 +8,4 @@ One is use the esp_gatts_create_service or esp_ble_gatts_add_char and etc.
The other way is use esp_ble_gatts_create_attr_tab.
The important things: the two ways cannot use in the same service, but can use in different service.
Please check the [tutorial](tutorial/GATT_Server_Service_Table_Example_Walkthrough.md) for more information about this example.

View file

@ -0,0 +1,492 @@
# GATT Server Service Table Example Walkthrough
## Introduction
This document presents a walkthrough of the GATT Server Service Table example code for the ESP32. This 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 Fig. 1. 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](https://www.bluetooth.com/specifications/profiles-overview).
![Table-like data structure representing the Heart Rate Service](image/Table-like data structure representing the Heart Rate Service.png)
## Includes
Lets start by taking a look at the included headers in the ``gatts_table_creat_demo.c`` file:
```
#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 "bt.h"
#include "bta_api.h"
#include "esp_gap_ble_api.h"
#include "esp_gatts_api.h"
#include "esp_bt_defs.h"
#include "esp_bt_main.h"
#include "esp_bt_main.h"
#include “gatts_table_creat_demo.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.
* ``bt.h``: implements BT controller and VHCI configuration procedures from the host side.
* ``esp_bt_main.h``: implements initialization and enabling of the Bluedroid stack.
* ``esp_gap_ble_api.h``: implements GAP configuration such as advertising and connection parameters.
* ``esp_gatts_api.h``: implements GATT Server configuration such as creating services and characteristics.
## Service Table
The header file ``gatts_table_creat_demo.h`` is where an enumeration of the services and characteristics is created:
```
enum
{
HRS_IDX_SVC,
HRS_IDX_HR_MEAS_CHAR,
HRS_IDX_HR_MEAS_VAL,
HRS_IDX_HR_MEAS_NTF_CFG,
HRS_IDX_BOBY_SENSOR_LOC_CHAR,
HRS_IDX_BOBY_SENSOR_LOC_VAL,
HRS_IDX_HR_CTNL_PT_CHAR,
HRS_IDX_HR_CTNL_PT_VAL,
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:
* HRS_IDX_SVC: Heart Rate Service index
* HRS_IDX_HR_MEAS_CHAR: Heart Rate Measurement characteristic index
* HRS_IDX_HR_MEAS_VAL: Heart Rate Measurement characteristic value index
* HRS_IDX_HR_MEAS_NTF_CFG: Heart Rate Measurement notifications configuration (CCC) index
* HRS_IDX_BOBY_SENSOR_LOC_CHAR: Heart Rate Body Sensor Location characteristic index
* HRS_IDX_BOBY_SENSOR_LOC_VAL: Heart Rate Body Sensor Location characteristic value index
* HRS_IDX_HR_CTNL_PT_CHAR: Heart Rate Control Point characteristic index
* HRS_IDX_HR_CTNL_PT_VAL: Heart Rate Control Point characteristic value index
* HRS_IDX_NB: Number of table elements.
## Main Entry Point
The entry point to this example is the ``app_main()`` function:
```
void app_main()
{
esp_err_t ret;
// Initialize NVS.
ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES) {
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(GATTS_TABLE_TAG, "%s enable controller failed\n", __func__);
return;
}
ret = esp_bt_controller_enable(ESP_BT_MODE_BTDM);
if (ret) {
ESP_LOGE(GATTS_TABLE_TAG, "%s enable controller failed\n", __func__);
return;
}
ESP_LOGI(GATTS_TABLE_TAG, "%s init bluetooth\n", __func__);
ret = esp_bluedroid_init();
if (ret) {
ESP_LOGE(GATTS_TABLE_TAG, "%s init bluetooth failed\n", __func__);
return;
}
ret = esp_bluedroid_enable();
if (ret) {
ESP_LOGE(GATTS_TABLE_TAG, "%s enable bluetooth failed\n", __func__);
return;
}
esp_ble_gatts_register_callback(gatts_event_handler);
esp_ble_gap_register_callback(gap_event_handler);
esp_ble_gatts_app_register(ESP_HEART_RATE_APP_ID);
return;
}
```
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
See this section in **GATT Server Example Walkthrough**.
## 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 HEART_PROFILE_NUM 1
#define HEART_PROFILE_APP_IDX 0
#define ESP_HEART_RATE_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 ``HEART_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[HEART_PROFILE_NUM] = {
[HEART_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:
* ``esp_ble_gap_set_device_name()``: used to set the advertised device name.
* ``esp_ble_gap_config_adv_data()``: used to configure standard advertising data.
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 advertising min interval */
int max_interval; /*!< Advertising data show advertising 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 = 0x20,
.max_interval = 0x40,
.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 advertisement intervals are set in units of 0.625 ms. In this example, the minimum advertisement interval is defined as 0x20 * 0.625 ms = 20 ms and the maximum advertisement interval is initialized as 0x40 * 0.625 ms = 40 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:
* ``[HRS_IDX_SVC]``: Named or designated initializer in the enum table.
* ``ESP_GATT_AUTO_RSP``: Auto respond configuration, set to respond automatically by the stack.
* ``ESP_UUID_LEN_16``: UUID length set to 16 bits.
* ``(uint8_t *)&primary_service_uuid``: UUID to identify the service as a primary one (0x2800).
* ``ESP_GATT_PERM_READ``: Read Permission for the service.
* ``sizeof(uint16_t)``: Maximum length of the service UUID (16 bits).
* ``sizeof(heart_rate_svc)``: Current service length set to the size of the variable *heart_rate_svc*, which is 16 bits.
* ``(uint8_t *)&heart_rate_svc``: Service attribute value set to the variable *heart_rate_svc* which contains the Heart Rate Service UUID (0x180D).
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;
};
```
## Conclusion
This document explains the work flow of the GATT Server Service Table example code that implements a Heart Rate Profile. This example begins by defining a table of attributes which include all the services and characteristics of the server, then it registers the Application Profile which triggers events that are used to configure GAP parameters and to create the service table. A service table is initialized with all the parameters required for each attribute and the service is started. This example shows a practical way of defining the server attributes by using a table instead of adding characteristic one by one.

View file

@ -7,3 +7,4 @@ Modify the name of gatt_server demo named ESP_GATTS_DEMO_a, ESP_GATTS_DEMO_b and
the gattc_multi_connect demo will connect the three gatt_server demos, and then exchange data.
Of course you can also modify the code to connect more devices, we default to connect up to 4 devices, more than 4 you need to modify menuconfig.
Please check the [tutorial](tutorial/GATT_Client_Multi-Connection_Example_Walkthrough.md) for more information about this example.

View file

@ -0,0 +1,326 @@
# GATT Client Multi-Connection Example Walkthrough
## Introduction
This document presents a description of the multi-connection BLE GATT client example for the ESP32. In this implementation, a single ESP32 working as a GATT client connects to three different GATT servers at the same time. This set up illustrates the use case of an ESP32 device acting in a way so that it receives data from different BLE sensors. The unique combination of ESP32s BLE + Wi-Fi capabilities in addition to connection to multiple peripherals makes it a great candidate to serve as an IoT gateway.
This examples workflow is similar to the **GATT Client Example Walkthrough** and is shown in Fig. 1. However, in the multi-connection implementation, a GATT client searches for three specific server names and once that it has found them it opens a connection to all three of them one after the other. In code, each connection is handled separately with one Application Profile.
Four ESP32 devices are needed in order to demonstrate this example, among which:
* one would be employed as a GATT Client flashed with the ``gattc_multi_connect`` demo, and,
* the rest run as GATT servers flashed with the ``gatt_server`` demo of the ESP-IDF Bluetooth examples folder.
![Multi-Connection GATT Client Flowchart](image/Multi-connection GATT Client Flowchart.png)
## Includes
The multi-connection examples main source file is ``gattc_multi_connect.c``. For details, see Section **Includes** in **GATT Client Example Walkthrough**.
## Main Entry Point
See Section **Main Entry Point** in **GATT Client Example Walkthrough**.
## Implementation Procedure
The GATT Client implementation includes the following steps:
* system initialization,
* scanning configuration,
* scanning of nearby devices,
* connection to devices of interest,
* registering for notifications.
### Initializing
See Section **Main Entry Point** in **GATT Client Example Walkthrough**.
#### Application Profiles
Application Profiles are a way to group functionality. They are designed so that each Application Profile connects to one peer device, that way the same ESP32 can connect to multiple devices by assigning one Application Profile to each one, as Fig. 2 shows. Each Application Profile creates a GATT interface to connect to other devices. The Application Profiles are defined by an ID number, there are three profiles in this example:
```
#define PROFILE_NUM 3
#define PROFILE_A_APP_ID 0
#define PROFILE_B_APP_ID 1
#define PROFILE_C_APP_ID 2
```
![ESP32 GATT Multi-Connect Client Application Profiles](image/ESP32 GATT Multi-connect Client Application Profiles.png)
The ``esp_ble_gattc_app_register()`` function is used to register each Application Profile to the BLE stack. The registration operation generates a GATT interface that is returned as a parameter in a registration event. In addition, each Application Profile is also defined by a structure that can be used to keep the state of the application and update its parameters when new data is propagated by the stack.
The Application Profiles in code are instances of a ``gattc_profile_inst`` structure. For details, see Section **Application Profiles** in **GATT Client Example Walkthrough**.
### Scanning
#### Setting Scan Parameters
See Section **Setting Scan Parameters** in **GATT Client Example Walkthrough**.
#### Starting to Scan
See Section **Start Scanning** in **GATT Client Example Walkthrough**.
#### Getting Scan Results
See Section **Getting Scan Results** in **GATT Client Example Walkthrough**.
#### Name Comparison
* First, the name of the device is extracted from the advertised data and stored in the ``adv_name`` variable:
```
adv_name = esp_ble_resolve_adv_data(scan_result->scan_rst.ble_adv,
ESP_BLE_AD_TYPE_NAME_CMPL, &adv_name_len);
```
* Then, the device name found is compared to the server names that the client wants to connect to. The server names are defined in the ``remote_device_name`` array:
```
static const char remote_device_name[3][20] = {"ESP_GATTS_DEMO_1", "ESP_GATTS_DEMO_2", “ESP_GATTS_DEMO_3"};
```
The name comparison takes places as follows:
```
if (strlen(remote_device_name[0]) == adv_name_len && strncmp((char *)adv_name, remote_device_name[0], adv_name_len) == 0) {
if (find_device_1 == false) {
find_device_1 = true;
ESP_LOGI(GATTC_TAG, "Searched device %s", remote_device_name[0]);
memcpy(gl_profile_tab[PROFILE_A_APP_ID].remote_bda, scan_result->scan_rst.bda, 6);
}
break;
}
else if (strlen(remote_device_name[1]) == adv_name_len && strncmp((char *)adv_name, remote_device_name[1], adv_name_len) == 0) {
if (find_device_2 == false) {
find_device_2 = true;
ESP_LOGI(GATTC_TAG, "Searched device %s", remote_device_name[1]);
memcpy(gl_profile_tab[PROFILE_B_APP_ID].remote_bda, scan_result->scan_rst.bda, 6);
}
}
else if (strlen(remote_device_name[2]) == adv_name_len && strncmp((char *)adv_name, remote_device_name[2], adv_name_len) == 0) {
if (find_device_3 == false) {
find_device_3 = true;
ESP_LOGI(GATTC_TAG, "Searched device %s", remote_device_name[2]);
memcpy(gl_profile_tab[PROFILE_C_APP_ID].remote_bda, scan_result->scan_rst.bda, 6);
}
break;
}
```
* If any of the device names found corresponds to a remote device name, the ``find_device_X`` flag is set and the address of the remote device is stored in the ``gl_profile_tab`` table. When all flags are set, the client stops scanning and connects to the remote devices.
### Connecting to Remote Devices
#### Connecting to the First Remote Device
Once all devices have been found, the client stops scanning:
```
if (find_device_1 && find_device_2 && find_device_3 && stop_scan == false)
{
stop_scan = true;
esp_ble_gap_stop_scanning();
}
```
The scan stop triggers an ``ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT`` event which is used to open a connection to the first remote device. The second and third devices get connected once the client searches for services, gets characteristics and registers for notifications on the first device. This workflow is designed to test that the communication between each remote device is working correctly before trying to connect to the next device or in case of error, skip to the next device.
```
case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT:
if (param->scan_stop_cmpl.status != ESP_BT_STATUS_SUCCESS){
ESP_LOGE(GATTC_TAG, "Scan stop failed");
break;
}
ESP_LOGI(GATTC_TAG, "Stop scan successfully");
if (!stop_scan){
ESP_LOGE(GATTC_TAG, "Did not find all devices");
}
if (find_device_1){
esp_ble_gattc_open(gl_profile_tab[PROFILE_A_APP_ID].gattc_if, gl_profile_tab[PROFILE_A_APP_ID].remote_bda, true);
}
break;
```
* The connection is opened with the ``esp_ble_gattc_open()`` function which takes the GATT interface, the remote device address and a boolean value set to true for direct connection or false for background auto connection. To disconnect the physical connection, the GAP API function ``esp_ble_gap_disconnect()`` is used.
When connecting to the first device, an ``ESP_GATTC_CONNECT_EVT`` event is generated which is forwarded to all profiles. It also triggers an ``ESP_GATTC_OPEN_EVT`` event that is forwarded to the Profile A event handler only, or ``gattc_profile_a_event_handler()`` function. The event checks that the connection is opened successfully, if not, the device is ignored and the client tries to open a connection to the second device:
```
case ESP_GATTC_OPEN_EVT:
if (p_data->open.status != ESP_GATT_OK){
//open failed, ignore the first device, connect the second device
ESP_LOGE(GATTC_TAG, "connect device failed, status %d", p_data->open.status);
if (find_device_2){
esp_ble_gattc_open(gl_profile_tab[PROFILE_B_APP_ID].gattc_if, gl_profile_tab[PROFILE_B_APP_ID].remote_bda, true);
}
break;
}
```
If the connection is successful the client saves the connection ID, prints the remote device information and configures the MTU size to 200 bytes.
```
gl_profile_tab[PROFILE_A_APP_ID].conn_id = p_data->open.conn_id;
ESP_LOGI(GATTC_TAG, "ESP_GATTC_OPEN_EVT conn_id %d, if %d, status %d, mtu %d", p_data->open.conn_id, gattc_if, p_data->open.status, p_data->open.mtu);
ESP_LOGI(GATTC_TAG, "REMOTE BDA:");
esp_log_buffer_hex(GATTC_TAG, p_data->open.remote_bda, sizeof(esp_bd_addr_t));
esp_err_t mtu_ret = esp_ble_gattc_config_mtu (gattc_if, p_data->open.conn_id, 200);
if (mtu_ret){
ESP_LOGE(GATTC_TAG, "config MTU error, error code = %x", mtu_ret);
}
break;
```
* After configuration of the MTU size, an ``ESP_GATTC_CFG_MTU_EVT`` is generated. This event is used to search for available known services on the remote device. The search is performed by using the ``esp_ble_gattc_search_service()`` function and a service ID defined by:
```
static esp_bt_uuid_t remote_filter_service_uuid = {
.len = ESP_UUID_LEN_16,
.uuid = {.uuid16 = REMOTE_SERVICE_UUID,},
};
```
* The handler then searches for the service:
```
case ESP_GATTC_CFG_MTU_EVT:
if (param->cfg_mtu.status != ESP_GATT_OK){
ESP_LOGE(GATTC_TAG,"Config mtu failed");
}
ESP_LOGI(GATTC_TAG, "Status %d, MTU %d, conn_id %d", param->cfg_mtu.status, param->cfg_mtu.mtu, param->cfg_mtu.conn_id);
esp_ble_gattc_search_service(gattc_if, param->cfg_mtu.conn_id, &remote_filter_service_uuid);
break;
```
If the service is found, an ``ESP_GATTC_SEARCH_RES_EVT`` event is triggered which allows to set the ``get_service_1 flag`` to true. This flag is used to print information and later get the characteristic that the client is interested in.
* Once the search for all services is completed, an ``ESP_GATTC_SEARCH_CMPL_EVT`` event is generated which is used to get the characteristics of the service just discovered. This is done with the ``esp_ble_gattc_get_characteristic()`` function:
```
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;
}
if (get_service_1){
esp_ble_gattc_get_characteristic(gattc_if, p_data->search_cmpl.conn_id, &remote_service_id, NULL);
}
break;
```
The ``esp_ble_gattc_get_characteristic()`` function takes the GATT interface, the connection ID and the remote service ID as parameters. In addition, a NULL value is passed to indicate that we want all the characteristics starting from the first one. If the client is interested in a specific characteristic it could pass the characteristic ID in this field to specify that.
An ``ESP_GATTC_GET_CHAR_EVT`` event is triggered when a characteristic is discovered. This event is used to print information about the characteristic.
* If the characteristic ID is the same as the one defined by ``REMOTE_NOTIFY_CHAR_UUID``, the client registers for notifications on that characteristic value.
* Finally, the next characteristic is requested using the same ``esp_ble_gattc_get_characteristic()`` function, this time, the last parameter is set to the current characteristic. This triggers another ``ESP_GATTC_GET_CHAR_EVT`` and the process is repeated until all characteristics are obtained.
```
case ESP_GATTC_GET_CHAR_EVT:
if (p_data->get_char.status != ESP_GATT_OK) {
break;
}
ESP_LOGI(GATTC_TAG, "GET CHAR: conn_id = %x, status %d", p_data->get_char.conn_id, p_data->get_char.status);
ESP_LOGI(GATTC_TAG, "GET CHAR: srvc_id = %04x, char_id = %04x", p_data->get_char.srvc_id.id.uuid.uuid.uuid16, p_data->get_char.char_id.uuid.uuid.uuid16);
if (p_data->get_char.char_id.uuid.uuid.uuid16 == REMOTE_NOTIFY_CHAR_UUID) {
ESP_LOGI(GATTC_TAG, "register notify");
esp_ble_gattc_register_for_notify(gattc_if, gl_profile_tab[PROFILE_A_APP_ID].remote_bda, &remote_service_id, &p_data->get_char.char_id);
}
esp_ble_gattc_get_characteristic(gattc_if, p_data->get_char.conn_id, &remote_service_id, &p_data->get_char.char_id);
break;
```
At this point the client has acquired all characteristics from the remote device and has subscribed for notifications on the characteristics of interest. Every time a client registers for notifications, an ``ESP_GATTC_REG_FOR_NOTIFY_EVT`` event is triggered. In this example, this event is set to write to the remote device Client Configuration Characteristic (CCC) using the ``esp_ble_gattc_write_char_descr()`` function. In turn, this function is used to write to characteristic descriptors. There are many characteristic descriptors defined by the Bluetooth specification, however, for this example, the descriptor of interest is the one that deals with enabling notifications, that is the Client Configuration descriptor.
#### Connecting to the Next Remote Device
* In order to pass this descriptor as a 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 as the UUID to identify the CCC:
```
#define ESP_GATT_UUID_CHAR_CLIENT_CONFIG 0x2902 /* Client Characteristic Configuration */
```
The value to write is “1” to enable notifications. The parameter ``ESP_GATT_WRITE_TYPE_RSP`` is also passed to request that the server responds to the write request, as well as the ``ESP_GATT_AUTH_REQ_NONE`` parameter to indicate that the write request does not need authorization:
```
case ESP_GATTC_REG_FOR_NOTIFY_EVT: {
if (p_data->reg_for_notify.status != ESP_GATT_OK){
ESP_LOGE(GATTC_TAG, "reg notify failed, error status =%x", p_data->reg_for_notify.status);
break;
}
uint16_t notify_en = 1;
ESP_LOGI(GATTC_TAG, "REG FOR NOTIFY: status %d, srvc_id = %04x, char_id = %04x",
p_data->reg_for_notify.status,
p_data->reg_for_notify.srvc_id.id.uuid.uuid.uuid16,
p_data->reg_for_notify.char_id.uuid.uuid.uuid16);
esp_ble_gattc_write_char_descr(gattc_if,
gl_profile_tab[PROFILE_A_APP_ID].conn_id,
&remote_service_id,
&p_data->reg_for_notify.char_id,
&notify_descr_id,
sizeof(notify_en),
(uint8_t *)&notify_en,
ESP_GATT_WRITE_TYPE_RSP,
ESP_GATT_AUTH_REQ_NONE);
break;
}
```
* Once notifications are enabled, the remote device sends a notification which triggers the ``ESP_GATTC_NOTIFY_EVT`` event on the client. This event is handled to write back to the characteristic using the ``esp_ble_gattc_write_char()`` function:
```
case ESP_GATTC_NOTIFY_EVT:
ESP_LOGI(GATTC_TAG, "ESP_GATTC_NOTIFY_EVT, Receive notify value:");
esp_log_buffer_hex(GATTC_TAG, p_data->notify.value, p_data->notify.value_len);
//write back
esp_ble_gattc_write_char(gattc_if,
gl_profile_tab[PROFILE_A_APP_ID].conn_id,
&remote_service_id,
&p_data->notify.char_id,
p_data->notify.value_len,
p_data->notify.value,
ESP_GATT_WRITE_TYPE_RSP,
ESP_GATT_AUTH_REQ_NONE);
break;
```
* If the writing procedure is acknowledged then the remote device has connected successfully and communication is established without error. Immediately, the write procedure generates an ``ESP_GATTC_WRITE_CHAR_EVT`` event which in this example is used to print information and connect to the second remote device:
```
case ESP_GATTC_WRITE_CHAR_EVT:
if (p_data->write.status != ESP_GATT_OK){
ESP_LOGE(GATTC_TAG, "write char failed, error status = %x", p_data->write.status);
}else{
ESP_LOGI(GATTC_TAG, "write char success");
}
//connect the second device
if (find_device_2){
esp_ble_gattc_open(gl_profile_tab[PROFILE_B_APP_ID].gattc_if, gl_profile_tab[PROFILE_B_APP_ID].remote_bda, true);
}
break;
```
* This triggers an open event which is handled by the Profile B event handler. This handler follows the same steps to search for services, get characteristics, register for notifications and write to the characteristic as the first device. The sequence for the second device also ends with an ``ESP_GATTC_WRITE_CHAR_EVT`` event which in turn is used to connect to the third device:
```
case ESP_GATTC_WRITE_CHAR_EVT:
if (p_data->write.status != ESP_GATT_OK){
ESP_LOGE(GATTC_TAG, "Write char failed, error status = %x", p_data->write.status);
}else{
ESP_LOGI(GATTC_TAG, "Write char success");
}
//connect the third device
if (find_device_3){
esp_ble_gattc_open(gl_profile_tab[PROFILE_C_APP_ID].gattc_if, gl_profile_tab[PROFILE_C_APP_ID].remote_bda, true);
}
break;
```
* The third devices also performs the same configuration and communication steps in identical form as the first and second devices. Upon successful completion, all three remote devices are simultaneously connected appropriately and receiving notifications without error.
## Conclusion
In this example we have reviewed the example code for the multi-connection GATT client. The client connects to three remote BLE peripherals and searches for services of interest. If the services are found, the characteristics of those services are discovered and subscribed to. The connections to the remote devices are done in order, starting from the first one and making sure that remote device has connected successfully and is notifying before trying to connect to the next device. This example shows a practical way to use the ESP32 as a central device that can read multiple BLE sensors at the same time.

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB