ESP32 learning notes (32) -- BLE GAP host connection

Posted by AliasZero on Tue, 18 Jan 2022 03:29:33 +0100

1, Background

1.1 Low Power Bluetooth (BLE) protocol stack


The link layer (LL) controls the RF state of the equipment, which has five equipment states: standby, broadcast, scanning, initialization and connection.

The broadcast is a broadcast packet, while the scan is to monitor the broadcast.

In GAP communication, the central device (Central - host) is used to scan and connect Peripheral devices (Peripheral - slave).

1.2 BLE process from initialization to connection establishment

  1. The peripheral device starts broadcasting. After sending a broadcast packet, T_IFS, open the RF Rx window to receive data packets from the central equipment
  2. The central equipment scans the broadcast and receives the broadcast t_ After ifs, if the scanning Reply of the central device is enabled, the central device will send a reply to the external device
  3. The peripheral receives the reply from the central equipment, prepares for receiving, and returns the ACK packet
  4. If the ACK packet is not received by the central device, the central device will send a reply until it times out. During this period, as long as the peripheral returns the ACK packet once, the connection is successful
  5. After establishing communication, the subsequent central equipment will send data packets to the external equipment based on the time of receiving the peripheral broadcast and the Connection Interval. The data packets will have two functions: synchronizing the clocks of the two equipment and establishing master-slave communication mode

Every time the peripheral receives a packet from the central device, it will reset its timing origin to synchronize with the central device (Service to Client)

After the BLE communication is successfully established, it will change to the Master-slave mode, the Central device will change to the Master, and the Peripheral peripheral will change to slave. Slave can only send its own data back to the Master within the specified time after the Master sends it a packet

  1. Connection established successfully
  2. The peripheral automatically stops broadcasting, and other devices can no longer find the peripheral
  3. The communication is carried out according to the following sequence, and the peripheral can send multiple packets within the interval when the central equipment sends packets

  1. When the connection needs to be disconnected, only the central equipment needs to stop connecting (stop sending packets)
  2. The central equipment can write the addr of the peripheral into Flash or SRAM and other storage devices, keep listening to the addr, and establish communication when the peripheral broadcast is received again. In order to save power, the BLE Server device can no longer send packets when there is no data to send for a period of time, and both parties will be disconnected due to connection timeout. At this time, the central device needs to start listening. In this way, when the BLE Server device needs to send data, it can connect again

1.3 ESP32 Bluetooth application structure

Bluetooth is a short-range communication system. Its key characteristics include robustness, low power consumption, low cost and so on. Bluetooth system is divided into two different technologies: Classic Bluetooth and Bluetooth low energy.
ESP32 supports dual-mode Bluetooth, that is, it supports both classic Bluetooth and Bluetooth low power consumption.

In terms of overall structure, Bluetooth can be divided into two parts: controller and host: the controller includes PHY, Baseband, Link Controller, Link Manager, Device Manager, HCI and other modules for hardware connection management, link management, etc; The host includes L2CAP, SMP, SDP, ATT, GATT, GAP and various specifications, which constructs the basis of providing interfaces to the application layer to facilitate the application layer's access to the Bluetooth system. The host can run on the same host as the controller, or it can be distributed on different hosts. ESP32 can support the above two methods.

1.4 Bluedroid host architecture

In ESP-IDF, the Bluetooth host (Classic BT + BLE) uses a large number of modified BLUEDROID. BLUEDROID has relatively perfect functions, holds common specifications and architecture design, and is also relatively complex. After a large number of modifications, BLUEDROID retains most of the codes below the BTA layer, almost completely deletes the codes of the BTIF layer, and uses the relatively simplified BTC layer as the built-in specification and Misc control layer. The modified BLUEDROID and its relationship with the controller are shown in the figure below:

2, API description

The following controller and virtual HCI interfaces are located at bt/include/esp32/include/esp_bt.h.

2.1 esp_bt_controller_mem_release

2.2 esp_bt_controller_init

2.3 esp_bt_controller_enable


The following GAP interfaces are located at bt/host/bluedroid/api/include/api/esp_bt_main.h and bt/host/bluedroid/api/include/api/esp_gap_ble_api.h.

2.4 esp_bluedroid_init

2.5 esp_bluedroid_enable

2.6 esp_ble_gap_register_callback

2.7 esp_ble_gap_set_scan_params

2.8 esp_ble_gap_start_scanning

2.9 esp_ble_gap_stop_scanning

2.10 esp_ble_resolve_adv_data

2.11 esp_ble_gap_disconnect


The following GATT interfaces are located at bt/host/bluedroid/api/include/api/esp_gattc_api.h

2.12 esp_ble_gattc_open

2.13 esp_ble_gattc_close

3, BT controller and protocol stack initialization

Use ESP IDF \ examples \ Bluetooth \ bluedroid \ ble \ GATT_ Routines in client

.........
//esp_bt_controller_config_t is the Bluetooth controller configuration structure. A default parameter is used here
    esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
    //Initializing Bluetooth controller, this function can only be called once and must be invoked before other Bluetooth functions are invoked.
    ret = esp_bt_controller_init(&bt_cfg);
    if (ret) {
        ESP_LOGE(GATTC_TAG, "%s initialize controller failed: %s\n", __func__, esp_err_to_name(ret));
        return;
    }

    //Enable the Bluetooth controller. Mode is the Bluetooth mode. If you want to change the Bluetooth mode dynamically, you can't call this function directly,
    //You should turn off Bluetooth with disable first, and then use this API to change the Bluetooth mode
    ret = esp_bt_controller_enable(ESP_BT_MODE_BLE);
    if (ret) {
        ESP_LOGE(GATTC_TAG, "%s enable controller failed: %s\n", __func__, esp_err_to_name(ret));
        return;
    }
    //Initialize Bluetooth and allocate system resources. It should be called first
    /*
    Bluetooth stack bluedroid stack includes the basic definition and API used by BT and BLE
    After initializing the Bluetooth stack, you cannot directly use the Bluetooth function,
    FSM is also required to manage Bluetooth connection
    */
    ret = esp_bluedroid_init();
    if (ret) {
        ESP_LOGE(GATTC_TAG, "%s init bluetooth failed: %s\n", __func__, esp_err_to_name(ret));
        return;
    }
    //Enable Bluetooth stack
    ret = esp_bluedroid_enable();
    if (ret) {
        ESP_LOGE(GATTC_TAG, "%s enable bluetooth failed: %s\n", __func__, esp_err_to_name(ret));
        return;
    }

    //Establish Bluetooth FSM (finite state machine)
    //Here, the callback function is used to control the response in each state. It needs to be registered in the callback function of GATT and GAP layer
    /*esp_gattc_cb And esp_gap_cb handles all possible situations of Bluetooth stack to achieve the effect of FSM*/
    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;
    }
    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;
    }

    //The BLE GATT service A is created below, which is equivalent to an independent application
    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);
    }
    /*
    Set the value of MTU (through MTU exchange, set the maximum amount of data that can be exchanged in a PDU).
    For example, the master device sends a 1000 byte MTU request, but the slave device responds with 500 bytes of MTU. In the future, both parties should use the smaller value of 500 bytes as the subsequent MTU.
    That is, the master and slave do not exceed this maximum data unit every time they transmit data.
    */
    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);
    }
.......

4, Application profile

An application profile is a way to group features designed for one or more server applications. For example, you can connect an application profile to a heart rate sensor and another application profile to a temperature sensor. Each application profile creates a GATT interface to connect to other devices. The application configuration file in the code is gattc_ profile_ The instance of Inst structure is defined as follows:

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 structure includes:

  • gattc_cb: GATT client callback function
  • gattc_if: GATT client interface number of this configuration file
  • app_id: application profile ID number
  • conn_id: connection ID number
  • service_start_handle: service header handle
  • service_end_handle: service end handle
  • char_handle: feature handle
  • remote_bda: the address of the remote device connected to this client

In this example, there is an application configuration file whose ID is defined as:

#define PROFILE_NUM 1
#define PROFILE_A_APP_ID 0

Application configuration files are stored in gl_profile_tab array and allocate the corresponding callback function gattc_profile_a_event_handler(). Different applications on the GATT client use different interfaces, which are controlled by gattc_if parameter. For initialization, this parameter is set to ESP_GATT_IF_NONE, which means that the application configuration file has not been linked to any server.

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

ESP triggered by application profile registration_ GATTC_ REG_ EVT event, which is controlled by esp_gattc_cb() event handler. The handler obtains the GATT interface returned by the event and stores it in the profile table:

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

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

Finally, the callback function calls GL_ profile_ The corresponding event handler for each profile in the tab table.

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

5, Get scan results

Start scanning, and the scanning results will arrive at esp_ GAP_ BLE_ SCAN_ RESULT_ Displayed immediately when the evt event, which includes the following parameters:

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

The event also includes a list of sub events as follows:

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

ESP_GAP_SEARCH_INQ_RES_EVT event, which is called every time a new device is found. ESP_ GAP_ SEARCH_ INQ_ CMPL_ The evt event is triggered when the scan is complete and can be used to restart the scan process:

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

First, resolve the device name and compare it with remote_device_name. If the device names match, the scan stops and the connection is initiated.

6, Initiate connection

Use the ` ` function to open a connection to a remote device. This function takes the application configuration file, GATT interface, remote device address and a Boolean value as parameters. Boolean value is used to indicate whether the connection is completed directly or in the background (automatic connection). In this case, the Boolean value must be set to true to establish the connection. Note that the client opens a virtual connection to the server. The virtual connection returns a connection ID. A virtual connection is a connection between an application profile and a remote server. Since many application profiles can run on an ESP32, many virtual connections may be opened to the same remote server. There is also a physical connection, that is, the actual BLE connection between the client and the server. Therefore, if the physical connection is esp_ BLE_ gap_ If the disconnect () function is disconnected, all other virtual connections will be closed. In this example, ESP is used for each application configuration file_ BLE_ gattc_ The open () function creates a virtual connection to the same server. Therefore, when the close function is called, only the connection from the application configuration file is closed, and if gap is called_ Disconnect function, both connections will be closed. In addition, the connection event is propagated to all application profiles because it is related to the physical connection, while the open event is propagated only to the application profile that creates the virtual connection.

7, Connection events, configuring MTU size

ATT_MTU is defined as the maximum size of any packet sent between the client and the server. When the client connects to the server, it notifies the server which MTU size to use by exchanging MTU request and response protocol data unit (PDU). This is done after the connection is opened. After opening the connection, ESP_ GATTC_ CONNECT_ The evt event is triggered.

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

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

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. Clients can use esp_ ble_ gattc_ send_ mtu_ The req() function changes the size of the MTU, which accepts the GATT interface and connection ID. The requested MTU size is determined by esp_ble_gatt_set_local_mtu() definition. Then the server can accept or reject the request. ESP32 supports an MTU size of up to 517 bytes, which is determined by esp_ gattc_ api. Esp in H_ GATT_ MAX_ MTU_ Size defined. In this example, the size of the MTU is set to 500 bytes. If the configuration fails, the returned error is printed:

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

Opening the connection triggers ESP_GATTC_OPEN_EVT event, which is used to check whether the connection is opened successfully. Otherwise, it prints an error and exits.

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

Esp when exchanging MTU_ GATTC_ CFG_ MTU_ The evt is triggered and, in this case, it is used to print the new MTU size.

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

8, Disconnect

Since many application profiles can run on an ESP32, many virtual connections may be opened to the same remote server. There is also a physical connection, that is, the actual BLE connection between the client and the server. Therefore, if the physical connection is esp_ BLE_ gap_ If the disconnect () function is disconnected, all other virtual connections will be closed. In this example, ESP is used for each application configuration file_ BLE_ gattc_ The open () function creates a virtual connection to the same server, so when calling ESP_ BLE_ gattc_ When the close () function is used, only the connection from the application configuration file is closed, and if ESP is called_ BLE_ gap_ Disconnect() function, both connections will be closed.

• by Leung Written on July 9, 2021

• reference: ESPIDF development ESP32 learning notes [classic Bluetooth and BLE]
    GATT client example walkthrough

Topics: ESP32 BLE