前言

  1. 该博客主要针对希望迅速上手 ESP32 蓝牙从机开发人员,因此,很多蓝牙技术细节知识并不会进行介绍,仅仅介绍我认为需要了解的 API 函数和回调内容。
  2. 本文主要是基于gatt_server demo来微调进行进行讲解。

代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <inttypes.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 "esp_bt.h"

#include "esp_gap_ble_api.h"
#include "esp_gatts_api.h"
#include "esp_bt_defs.h"
#include "esp_bt_main.h"
#include "esp_gatt_common_api.h"

#include "sdkconfig.h"

#define GATTS_TAG "GATTS_DEMO"

///Declare the static function
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);
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);

#define GATTS_SERVICE_UUID_TEST_A   0x00FF
#define GATTS_CHAR_UUID_TEST_A      0xFF01
#define GATTS_DESCR_UUID_TEST_A     0x3333
#define GATTS_NUM_HANDLE_TEST_A     4

#define GATTS_SERVICE_UUID_TEST_B   0x00EE
#define GATTS_CHAR_UUID_TEST_B      0xEE01
#define GATTS_DESCR_UUID_TEST_B     0x2222
#define GATTS_NUM_HANDLE_TEST_B     4

/* 广播设备名称,如果需要使用中文,则需要中文转 URL 编码。可以使用 https://tool.chinaz.com/tools/urlencode.aspx 进行转换 */
// #define TEST_DEVICE_NAME            "ESP_GATTS_DEMO"
const char TEST_DEVICE_NAME[] = {0xE9,0xA3,0x8E,0xE6,0xAD,0xA3,0xE8,0xB1,0xAA};
#define TEST_MANUFACTURER_DATA_LEN  17

#define GATTS_DEMO_CHAR_VAL_LEN_MAX 0x40

#define PREPARE_BUF_MAX_SIZE 1024

static uint8_t char1_str[] = {0x11,0x22,0x33};
static esp_gatt_char_prop_t a_property = 0;
static esp_gatt_char_prop_t b_property = 0;

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

#define adv_config_flag      (1 << 0)
#define scan_rsp_config_flag (1 << 1)

#ifdef CONFIG_SET_RAW_ADV_DATA
static uint8_t raw_adv_data[] = {
        0x02, 0x01, 0x06,                  // Length 2, Data Type 1 (Flags), Data 1 (LE General Discoverable Mode, BR/EDR Not Supported)
        0x02, 0x0a, 0xeb,                  // Length 2, Data Type 10 (TX power leve), Data 2 (-21)
        0x03, 0x03, 0xab, 0xcd,            // Length 3, Data Type 3 (Complete 16-bit Service UUIDs), Data 3 (UUID)
};
static uint8_t raw_scan_rsp_data[] = {     // Length 15, Data Type 9 (Complete Local Name), Data 1 (ESP_GATTS_DEMO)
        0x0f, 0x09, 0x45, 0x53, 0x50, 0x5f, 0x47, 0x41, 0x54, 0x54, 0x53, 0x5f, 0x44,
        0x45, 0x4d, 0x4f
};
#else

static uint8_t adv_service_uuid128[32] = {
    /* LSB <--------------------------------------------------------------------------------> MSB */
    //first uuid, 16bit, [12],[13] is the value
    0xfb, 0x34, 0x9b, 0x5f, 0x80, 0x00, 0x00, 0x80, 0x00, 0x10, 0x00, 0x00, 0xEE, 0x00, 0x00, 0x00,
    //second uuid, 32bit, [12], [13], [14], [15] is the value
    0xfb, 0x34, 0x9b, 0x5f, 0x80, 0x00, 0x00, 0x80, 0x00, 0x10, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00,
};

// 广播报数据长度应当小于 31 bytes
//static uint8_t test_manufacturer[TEST_MANUFACTURER_DATA_LEN] =  {0x12, 0x23, 0x45, 0x56};
//adv data
static esp_ble_adv_data_t adv_data = {
    .set_scan_rsp = false,
    .include_name = true,
    .include_txpower = false,
    .min_interval = 0x0006, //slave connection min interval, Time = min_interval * 1.25 msec
    .max_interval = 0x0010, //slave connection max interval, Time = max_interval * 1.25 msec
    .appearance = ESP_BLE_APPEARANCE_SPORTS_WATCH,
    .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(adv_service_uuid128),
    .p_service_uuid = adv_service_uuid128,
    .flag = (ESP_BLE_ADV_FLAG_GEN_DISC | ESP_BLE_ADV_FLAG_BREDR_NOT_SPT),
};
// scan response data
static esp_ble_adv_data_t scan_rsp_data = {
    .set_scan_rsp = true,
    .include_name = true,
    .include_txpower = true,
    //.min_interval = 0x0006,
    //.max_interval = 0x0010,
    // .appearance = 0x00,
    .manufacturer_len = 0, //TEST_MANUFACTURER_DATA_LEN,
    .p_manufacturer_data =  NULL, //&test_manufacturer[0],
    .service_data_len = 0,
    .p_service_data = NULL,
    .service_uuid_len = sizeof(adv_service_uuid128),
    .p_service_uuid = adv_service_uuid128,
    .flag = (ESP_BLE_ADV_FLAG_GEN_DISC | ESP_BLE_ADV_FLAG_BREDR_NOT_SPT),
};

#endif /* CONFIG_SET_RAW_ADV_DATA */

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

enum {
    PROFILE_A,
    PROFILE_B,

    PROFILE_NUM
};

/* 这个注册 APP ID 可为 0~65535 任意值,只要 APP ID 没有重复即可 */
#define PROFILE_A_APP_ID 0x55
#define PROFILE_B_APP_ID 0x66

struct gatts_profile_inst {
    esp_gatts_cb_t gatts_cb;         /* GATT 回调函数 */
    uint16_t gatts_if;               /* 协议栈分配的,用于指定服务的标识符 */
    uint16_t app_id;                 /* 注册的 APP ID,可为 0~65535 任意值,但不可重复 */
    uint16_t conn_id;                /* 用于表示一个设备连接,因为 ESP32 作为从机可以被多个主机同时连接,那么就需要利用这个参数指定是那个主机连接 */
    uint16_t service_handle;         /* 服务(Service)句柄 */
    esp_gatt_srvc_id_t service_id;   /* 首要服务的一些参数 */
    uint16_t char_handle;            /* 特征(Characteristic)句柄 */
    esp_bt_uuid_t char_uuid;         /* 特征(Characteristic) UUID 值 */
    esp_gatt_perm_t perm;
    esp_gatt_char_prop_t property;
    uint16_t descr_handle;           /* 特征配置描述符(descriptor)句柄 */
    esp_bt_uuid_t descr_uuid;        /* UUID 长度 */
};

/* 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 gl_profile_tab[PROFILE_NUM] = {
    [PROFILE_A] = {
        .app_id   = PROFILE_A_APP_ID,
        .gatts_cb = gatts_profile_a_event_handler,
        .gatts_if = ESP_GATT_IF_NONE,
    },
    [PROFILE_B] = {
        .app_id   = PROFILE_B_APP_ID,
        .gatts_cb = gatts_profile_b_event_handler,    /* 配置 B 服务的回调函数 */
        .gatts_if = ESP_GATT_IF_NONE,                 /* 如果协议栈没有分配 gatts_if ,那么就是 ESP_GATT_IF_NONE */
    },
};

typedef struct {
    uint8_t                 *prepare_buf;
    int                     prepare_len;
} prepare_type_env_t;

static prepare_type_env_t a_prepare_write_env; /* 存储 A 服务准备写操作内容 */
static prepare_type_env_t b_prepare_write_env; /* 存储 B 服务准备写操作内容 */

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);
void example_exec_write_event_env(prepare_type_env_t *prepare_write_env, esp_ble_gatts_cb_param_t *param);

static void advertise_init(void)
{
    esp_err_t set_dev_name_ret = esp_ble_gap_set_device_name(TEST_DEVICE_NAME);
    if (set_dev_name_ret){
        /* 设置设备名称失败 */
        ESP_LOGE(GATTS_TAG, "set device name failed, error code = %x", set_dev_name_ret);
    }
#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);
    }
    // 设置扫描响应数据
    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);
    }
#else
    // 设置广播数据
    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);
    }
    // 设置扫描响应数据
    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);
    }
#endif
}

static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param)
{
    ESP_LOGI(GATTS_TAG, "GAP Event:%d", event);
    switch (event) {
#ifdef CONFIG_SET_RAW_ADV_DATA
    case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT: {
        if (param->adv_data_raw_cmpl.status != ESP_BT_STATUS_SUCCESS) {
            ESP_LOGE(GATTS_TAG, "advertising data set failed");
        }
        break;
    }
    case ESP_GAP_BLE_SCAN_RSP_DATA_RAW_SET_COMPLETE_EVT: {
        if (param->scan_rsp_data_raw_cmpl.status != ESP_BT_STATUS_SUCCESS) {
            ESP_LOGE(GATTS_TAG, "advertising data set failed");
        }
        esp_ble_gap_start_advertising(&adv_params);
        break;
    }
#else
    case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT: {
        if (param->adv_data_cmpl.status != ESP_BT_STATUS_SUCCESS) {
            ESP_LOGE(GATTS_TAG, "advertising data set failed");
        }
        break;
    }
    case ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT: {
        if (param->scan_rsp_data_cmpl.status != ESP_BT_STATUS_SUCCESS) {
            ESP_LOGE(GATTS_TAG, "advertising data set failed");
        }
        esp_ble_gap_start_advertising(&adv_params);
        break;
    }
#endif
    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");
        }
        break;
    }
    case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT: {
        if (param->adv_stop_cmpl.status != ESP_BT_STATUS_SUCCESS) {
            ESP_LOGE(GATTS_TAG, "Advertising stop failed");
        } else {
            ESP_LOGI(GATTS_TAG, "Stop adv successfully");
        }
        break;
    }
    case ESP_GAP_BLE_UPDATE_CONN_PARAMS_EVT: {
         ESP_LOGI(GATTS_TAG, "update connection 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;
    }
    case ESP_GAP_BLE_SET_PKT_LENGTH_COMPLETE_EVT: {
        ESP_LOGI(GATTS_TAG, "packet length updated: rx = %d, tx = %d, status = %d",
                  param->pkt_data_length_cmpl.params.rx_len,
                  param->pkt_data_length_cmpl.params.tx_len,
                  param->pkt_data_length_cmpl.status);
        break;
    }
    default:
        break;
    }
}

/**
 * @brief 处理写事件的函数,用于处理普通写入和准备写入
 *
 * @param gatts_if GATT 接口,用于标识 GATT 服务端
 * @param prepare_write_env 指向准备写环境结构体的指针,保存准备写的数据
 * @param param GATT 回调参数,包括写事件的信息
 */
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) {
        /* 判断是否为准备写操作,注意 : 准备写入(Prepare Write)操作是必须有响应 */
        if (param->write.is_prep) {
            /* 检查写入偏移量是否超过缓冲区最大大小 */
            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;
            }
            /* 如果状态正常且准备缓冲区为空,则分配缓冲区内存 */
            if (status == ESP_GATT_OK && 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) {
                    /* malloc 申请分配内存失败 */
                    ESP_LOGE(GATTS_TAG, "Gatt_server prep no mem");
                    status = ESP_GATT_NO_RESOURCES;
                }
            }

            esp_gatt_rsp_t *gatt_rsp = (esp_gatt_rsp_t *)malloc(sizeof(esp_gatt_rsp_t));
            if (gatt_rsp) {
                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);
            } else {
                /* 申请 esp_gatt_rsp_t 结构体内存数据失败 */
                ESP_LOGE(GATTS_TAG, "malloc failed, no resource to send response error\n");
                status = ESP_GATT_NO_RESOURCES;
            }
            if (status != ESP_GATT_OK){
                return;
            }

            /* 将写入的数据复制到 a_prepare_write_env 或者 b_prepare_write_env 准备缓冲区 */
            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_LOGI(GATTS_TAG, "write event, but not prepare");
            /* 对于普通写入,直接发送响应。注意,该函数必须有,否则会引起客户端(client)断连 */
            esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id, status, NULL);
        }
    } else{
        /* 如果不需要响应,则直接返回 */
        ESP_LOGI(GATTS_TAG, "write event, but not need to response");
        return;
    }
}

/**
 * @brief 处理 GATT 服务端的执行写操作事件。
 * 
 * @param prepare_write_env 准备写操作的环境参数,包括缓冲区和长度。
 * @param param 事件回调参数,其中包含执行写操作的标志和其他信息。
 */
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;
}

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", param->reg.status, param->reg.app_id);

        /* 创造 GATTS 首要服务 */
        gl_profile_tab[PROFILE_A].service_id.is_primary = true;
        gl_profile_tab[PROFILE_A].service_id.id.inst_id = 0x00;
        gl_profile_tab[PROFILE_A].service_id.id.uuid.len = ESP_UUID_LEN_16;
        gl_profile_tab[PROFILE_A].service_id.id.uuid.uuid.uuid16 = GATTS_SERVICE_UUID_TEST_A;
        esp_ble_gatts_create_service(gatts_if, &gl_profile_tab[PROFILE_A].service_id, GATTS_NUM_HANDLE_TEST_A);
        break;
    }
    case ESP_GATTS_READ_EVT: {
        ESP_LOGI(GATTS_TAG, "GATT_READ_EVT, conn_id %d, trans_id %" PRIu32 ", handle %d", param->read.conn_id, param->read.trans_id, param->read.handle);
        /* 读取 esp_ble_gatts_add_char 初始化时候的 char_val */
        uint16_t length = 0;
        const uint8_t *prf_char;
        esp_gatt_rsp_t rsp;
        memset(&rsp, 0, sizeof(esp_gatt_rsp_t));
        /* 获取特征值  */
        esp_err_t get_attr_ret = esp_ble_gatts_get_attr_value(param->read.handle,  &length, &prf_char);
        if (get_attr_ret == ESP_FAIL){
            ESP_LOGE(GATTS_TAG, "ILLEGAL HANDLE");
        }
        rsp.attr_value.handle = param->read.handle;
        rsp.attr_value.len = length;
        memcpy(rsp.attr_value.value, prf_char, length);
        for(int i = 0; i < length; i++){
            ESP_LOGD(GATTS_TAG, "ESP_GATTS_READ_EVT : prf_char[%x] =%x",i,prf_char[i]);
            ESP_LOGD(GATTS_TAG, "ESP_GATTS_READ_EVT : i,rsp.attr_value.value[%x] =%x",i,rsp.attr_value.value[i]);
        }
        esp_ble_gatts_send_response(gatts_if, param->read.conn_id, param->read.trans_id,
                                    ESP_GATT_OK, &rsp);
        // 更新特征的值
        rsp.attr_value.value[0] = 0x55;
        rsp.attr_value.value[1] = 0x66;
        rsp.attr_value.value[2] = 0x77;
        esp_ble_gatts_set_attr_value(rsp.attr_value.handle, 3, rsp.attr_value.value);
        break;
    }
    case ESP_GATTS_WRITE_EVT: {
        ESP_LOGI(GATTS_TAG, "GATT_WRITE_EVT, conn_id %d, trans_id %" PRIu32 ", handle %d", param->write.conn_id, param->write.trans_id, param->write.handle);
        if (!param->write.is_prep) {
            /* 打印客户端(client)写入的数据长度和值 */
            ESP_LOGI(GATTS_TAG, "It's not prepare write, value len %d, value", param->write.len);
            esp_log_buffer_hex(GATTS_TAG, param->write.value, param->write.len);
            /* 如果是 特征配置描述符(descriptor)操作句柄,并且写入数据长度为2 */
            if (gl_profile_tab[PROFILE_A].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 (a_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_A].char_handle,
                                                sizeof(notify_data), notify_data, false);
                    }
                } else if (descr_value == 0x0002){
                    /*  */
                    if (a_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_A].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 descr value");
                    esp_log_buffer_hex(GATTS_TAG, param->write.value, param->write.len);
                }
            }
        }
        example_write_event_env(gatts_if, &a_prepare_write_env, param);
        break;
    }
    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;
    }
    case ESP_GATTS_MTU_EVT: {
        ESP_LOGI(GATTS_TAG, "ESP_GATTS_MTU_EVT, MTU %d", param->mtu.mtu);
        break;
    }
    case ESP_GATTS_CREATE_EVT: {
        ESP_LOGI(GATTS_TAG, "CREATE_SERVICE_EVT, status %d,  service_handle %d", param->create.status, param->create.service_handle);
        /* 添加 特征申明 和 特征值 */
        gl_profile_tab[PROFILE_A].service_handle = param->create.service_handle;
        gl_profile_tab[PROFILE_A].char_uuid.len = ESP_UUID_LEN_16;
        gl_profile_tab[PROFILE_A].char_uuid.uuid.uuid16 = GATTS_CHAR_UUID_TEST_A;
        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].service_handle, &gl_profile_tab[PROFILE_A].char_uuid,
                                                        ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE,
                                                        a_property,
                                                        &gatts_demo_char1_val, NULL);
        /* 启动服务 */
        esp_ble_gatts_start_service(gl_profile_tab[PROFILE_A].service_handle);
        if (add_char_ret){
            ESP_LOGE(GATTS_TAG, "add char failed, error code =%x",add_char_ret);
        }
        break;
    }
    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",
                param->add_char.status, param->add_char.attr_handle, param->add_char.service_handle);
        /* 添加 特征配置描述符 */
        gl_profile_tab[PROFILE_A].char_handle = param->add_char.attr_handle;
        gl_profile_tab[PROFILE_A].descr_uuid.len = ESP_UUID_LEN_16;
        gl_profile_tab[PROFILE_A].descr_uuid.uuid.uuid16 = ESP_GATT_UUID_CHAR_CLIENT_CONFIG;
        esp_err_t add_descr_ret = esp_ble_gatts_add_char_descr(gl_profile_tab[PROFILE_A].service_handle, &gl_profile_tab[PROFILE_A].descr_uuid,
                                                                ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE, NULL, NULL);

        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", length);
        for(int i = 0; i < length; i++){
            ESP_LOGI(GATTS_TAG, "prf_char[%x] =%x",i,prf_char[i]);
        }
        if (add_descr_ret){
            ESP_LOGE(GATTS_TAG, "add char descr failed, error code =%x", add_descr_ret);
        }
        break;
    }
    case ESP_GATTS_ADD_CHAR_DESCR_EVT: {
        gl_profile_tab[PROFILE_A].descr_handle = param->add_char_descr.attr_handle;
        ESP_LOGI(GATTS_TAG, "ADD_DESCR_EVT, status %d, attr_handle %d, service_handle %d",
                 param->add_char_descr.status, param->add_char_descr.attr_handle, param->add_char_descr.service_handle);
        break;
    }
    case ESP_GATTS_START_EVT: {
        ESP_LOGI(GATTS_TAG, "SERVICE_START_EVT, status %d, service_handle %d",
                 param->start.status, param->start.service_handle);
        break;
    }
    case ESP_GATTS_CONNECT_EVT: {
        ESP_LOGI(GATTS_TAG, "ESP_GATTS_CONNECT_EVT, conn_id %d, remote %02x:%02x:%02x:%02x:%02x:%02x:",
                 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]);
        gl_profile_tab[PROFILE_A].conn_id = param->connect.conn_id;
        //向对端设备发起连接参数更新
        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 = 0x20;    // max_int = 0x20*1.25ms = 40ms
        conn_params.min_int = 0x10;    // min_int = 0x10*1.25ms = 20ms
        conn_params.timeout = 400;    // timeout = 400*10ms = 4000ms
        esp_ble_gap_update_conn_params(&conn_params);
        break;
    }
    case ESP_GATTS_DISCONNECT_EVT: {
        ESP_LOGI(GATTS_TAG, "ESP_GATTS_DISCONNECT_EVT, disconnect reason 0x%x", param->disconnect.reason);
        break;
    }
    case ESP_GATTS_CONF_EVT: {
        ESP_LOGI(GATTS_TAG, "ESP_GATTS_CONF_EVT, status %d attr_handle %d", param->conf.status, param->conf.handle);
        if (param->conf.status != ESP_GATT_OK){
            esp_log_buffer_hex(GATTS_TAG, param->conf.value, param->conf.len);
        }
        break;
    }
    default:
        break;
    }
}

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", param->reg.status, param->reg.app_id);
        
        /* 创造 GATTS 首要服务 */
        gl_profile_tab[PROFILE_B].service_id.is_primary = true;
        gl_profile_tab[PROFILE_B].service_id.id.inst_id = 0x00;
        gl_profile_tab[PROFILE_B].service_id.id.uuid.len = ESP_UUID_LEN_16;
        gl_profile_tab[PROFILE_B].service_id.id.uuid.uuid.uuid16 = GATTS_SERVICE_UUID_TEST_B;
        esp_ble_gatts_create_service(gatts_if, &gl_profile_tab[PROFILE_B].service_id, GATTS_NUM_HANDLE_TEST_B);
        break;
    }
    case ESP_GATTS_READ_EVT: {
        ESP_LOGI(GATTS_TAG, "GATT_READ_EVT, conn_id %d, trans_id %" PRIu32 ", handle %d", 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;
    }
    case ESP_GATTS_WRITE_EVT: {
        ESP_LOGI(GATTS_TAG, "GATT_WRITE_EVT, conn_id %d, trans_id %" PRIu32 ", handle %d", param->write.conn_id, param->write.trans_id, param->write.handle);
        if (!param->write.is_prep) {
            ESP_LOGI(GATTS_TAG, "It's not prepare write, value len %d, value :", param->write.len);
            esp_log_buffer_hex(GATTS_TAG, param->write.value, param->write.len);
            /* 如果是 特征配置描述符(descriptor)操作句柄,并且写入数据长度为2 */
            if (gl_profile_tab[PROFILE_B].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, "profile b 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].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].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, &b_prepare_write_env, param);
        break;
    }
    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(&b_prepare_write_env, param);
        break;
    }
    case ESP_GATTS_MTU_EVT: {
        ESP_LOGI(GATTS_TAG, "ESP_GATTS_MTU_EVT, MTU %d", param->mtu.mtu);
        break;
    }
    case ESP_GATTS_CREATE_EVT: {
        ESP_LOGI(GATTS_TAG, "CREATE_SERVICE_EVT, status %d,  service_handle %d", param->create.status, param->create.service_handle);
        /* 创建特征 */
        gl_profile_tab[PROFILE_B].service_handle = param->create.service_handle;
        gl_profile_tab[PROFILE_B].char_uuid.len = ESP_UUID_LEN_16;
        gl_profile_tab[PROFILE_B].char_uuid.uuid.uuid16 = GATTS_CHAR_UUID_TEST_B;
        b_property = ESP_GATT_CHAR_PROP_BIT_READ | ESP_GATT_CHAR_PROP_BIT_WRITE | ESP_GATT_CHAR_PROP_BIT_NOTIFY | ESP_GATT_CHAR_PROP_BIT_INDICATE;
        esp_err_t add_char_ret =esp_ble_gatts_add_char( gl_profile_tab[PROFILE_B].service_handle, &gl_profile_tab[PROFILE_B].char_uuid,
                                                        ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE,
                                                        b_property,
                                                        NULL, NULL);
        esp_ble_gatts_start_service(gl_profile_tab[PROFILE_B].service_handle);
        if (add_char_ret){
            ESP_LOGE(GATTS_TAG, "add char failed, error code =%x",add_char_ret);
        }
        break;
    }
    case ESP_GATTS_ADD_CHAR_EVT: {
        ESP_LOGI(GATTS_TAG, "ADD_CHAR_EVT, status %d,  attr_handle %d, service_handle %d",
                 param->add_char.status, param->add_char.attr_handle, param->add_char.service_handle);

        /* 添加 特征配置描述符 */
        gl_profile_tab[PROFILE_B].char_handle = param->add_char.attr_handle;
        gl_profile_tab[PROFILE_B].descr_uuid.len = ESP_UUID_LEN_16;
        gl_profile_tab[PROFILE_B].descr_uuid.uuid.uuid16 = ESP_GATT_UUID_CHAR_CLIENT_CONFIG;
        esp_ble_gatts_add_char_descr(gl_profile_tab[PROFILE_B].service_handle, &gl_profile_tab[PROFILE_B].descr_uuid,
                                     ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE,
                                     NULL, NULL);
        break;
    }
    case ESP_GATTS_ADD_CHAR_DESCR_EVT: {
        gl_profile_tab[PROFILE_B].descr_handle = param->add_char_descr.attr_handle;
        ESP_LOGI(GATTS_TAG, "ADD_DESCR_EVT, status %d, attr_handle %d, service_handle %d",
                 param->add_char_descr.status, param->add_char_descr.attr_handle, param->add_char_descr.service_handle);
        break;
    }
    case ESP_GATTS_START_EVT: {
        ESP_LOGI(GATTS_TAG, "SERVICE_START_EVT, status %d, service_handle %d",
                 param->start.status, param->start.service_handle);
        break;
    }
    case ESP_GATTS_CONNECT_EVT: {
        ESP_LOGI(GATTS_TAG, "ESP_GATTS_CONNECT_EVT, conn_id %d, remote %02x:%02x:%02x:%02x:%02x:%02x:",
                 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]);
        gl_profile_tab[PROFILE_B].conn_id = param->connect.conn_id;
        break;
    }
    case ESP_GATTS_CONF_EVT: {
        ESP_LOGI(GATTS_TAG, "ESP_GATTS_CONF_EVT status %d attr_handle %d", param->conf.status, param->conf.handle);
        if (param->conf.status != ESP_GATT_OK){
            esp_log_buffer_hex(GATTS_TAG, param->conf.value, param->conf.len);
        }
        break;
    }
    case ESP_GATTS_DISCONNECT_EVT: {
        ESP_LOGI(GATTS_TAG, "ESP_GATTS_DISCONNECT_EVT, disconnect reason 0x%x", param->disconnect.reason);
    }
    default:
        break;
    }
}

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_TAG, "GATTS event = %d", event);
    int idx;
    /* 如果事件是注册事件,则存储每个概要文件的 gatts_if,该事件由 esp_ble_gatts_app_register() 函数触发 */
    if (event == ESP_GATTS_REG_EVT) {
        // 依次遍历 GATT profile
        for (idx = 0; idx < PROFILE_NUM; idx++) {
            // 找到对应的注册的 app_id ,如果 GATT 处于正常运行,那么将 GATT 接口 gatts_if 进行存储
            if (param->reg.app_id == gl_profile_tab[idx].app_id && param->reg.status == ESP_GATT_OK) {
                /* 将协议栈分配的 gatts_if 存储进对应的结构体中 */
                gl_profile_tab[idx].gatts_if = gatts_if;
                ESP_LOGI(GATTS_TAG,"gatts_if = %d,param->reg.app_id = 0x%x",gatts_if,param->reg.app_id);
            }
        }
    }
    // 触发 GATT 回调事件,如下处理为判断是那个 GATT profile 事件,并作出响应的处理
    for (idx = 0; idx < PROFILE_NUM; idx++) {
        /* 如果 gatts_if 为 ESP_GATT_IF_NONE 表示当前事件或操作不对应于任何特定的 GATT 应用程序接口,
         * 因此所有的 GATT profile都会被调用一次。如果指定了特点的 GATT 应用,则调用对应的应用程序。
         */
        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);
            }
        }
    }
}

void app_main(void)
{
    esp_err_t ret;

    // 初始化 NVS.
    ret = nvs_flash_init();
    /* 如果 NVS 分区没有剩余空间或者 NVS 分区包含新格式数据,当前版本无法识别。*/
    if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
        /* 清空 NVS 分区数据 */
        ESP_ERROR_CHECK(nvs_flash_erase());
        /* NVS 分区重新进行初始化 */
        ret = nvs_flash_init();
    }
    ESP_ERROR_CHECK( ret );

    /* 释放经典蓝牙协议栈空间 */
    ESP_ERROR_CHECK(esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT));

    /* 初始化 Control 层 */
    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: %s", __func__, esp_err_to_name(ret));
        return;
    }

    /* 使能 Control 层 */
    ret = esp_bt_controller_enable(ESP_BT_MODE_BLE);
    if (ret) {
        ESP_LOGE(GATTS_TAG, "%s enable controller failed: %s", __func__, esp_err_to_name(ret));
        return;
    }

    /* 初始化 HOST层,Bluedroid 协议栈 */
    ret = esp_bluedroid_init();
    if (ret) {
        ESP_LOGE(GATTS_TAG, "%s init bluetooth failed: %s", __func__, esp_err_to_name(ret));
        return;
    }
    /* 使能 HOST层,Bluedroid 协议栈 */
    ret = esp_bluedroid_enable();
    if (ret) {
        ESP_LOGE(GATTS_TAG, "%s enable bluetooth failed: %s", __func__, esp_err_to_name(ret));
        return;
    }

    /* 注册 GATTS 事件回调函数 */
    ret = esp_ble_gatts_register_callback(gatts_event_handler);
    if (ret){
        ESP_LOGE(GATTS_TAG, "gatts register error, error code = %x", ret);
        return;
    }
    /* 注册 GAP 事件回调函数 */
    ret = esp_ble_gap_register_callback(gap_event_handler);
    if (ret){
        ESP_LOGE(GATTS_TAG, "gap register error, error code = %x", ret);
        return;
    }
    /* 注册一个app_id, 协议栈将会分配一个对应的 gatts_if,用于标识一个 GATT 服务。
     * 调用这个函数就会触发 esp_ble_gatts_register_callback() 注册的回调函数中的 ESP_GATTS_REG_EVT 事件
     */
    ret = esp_ble_gatts_app_register(gl_profile_tab[PROFILE_A].app_id);
    if (ret){
        ESP_LOGE(GATTS_TAG, "gatts app register error, error code = %x", ret);
        return;
    }
    /* 注册一个app_id, 协议栈将会分配一个对应的 gatts_if,用于标识一个 GATT 服务。
     * 调用这个函数就会触发 esp_ble_gatts_register_callback() 注册的回调函数中的 ESP_GATTS_REG_EVT 事件
     */
    ret = esp_ble_gatts_app_register(gl_profile_tab[PROFILE_B].app_id);
    if (ret){
        ESP_LOGE(GATTS_TAG, "gatts app register error, error code = %x", ret);
        return;
    }
    /* 设置本地 MTU 为 23 字节,实际交换时刻的 MTU 要通过与客户端(client) 协商后得到。
     * MTU 大小范围为 23 ~ 517 字节之间,如果没有调用该函数,则默认为 23 字节。
     */
    esp_err_t local_mtu_ret = esp_ble_gatt_set_local_mtu(23);
    if (local_mtu_ret){
        ESP_LOGE(GATTS_TAG, "set local  MTU failed, error code = %x", local_mtu_ret);
    }
    /* 初始化广播参数 */
    advertise_init();
    return;
}

app_main

NVS 初始化

  1. 首先我们来看 app_main() 函数。在该函数中,我们先初始化了 NVS 分区。其实这个部分可以不要。他主要用于存储一些 RF(射频)校准数据,以确保无线通信的性能和稳定性
  2. 当 ESP32 第一次启动并运行无线功能(如 Wi-Fi 或蓝牙)时,它会进行 RF 校准,以确定在当前硬件和环境条件下的最佳射频参数。
  3. 校准过程的结果会被存储在 NVS 中,这样在后续启动时,设备可以直接使用这些校准数据,避免每次启动都需要重新校准。从而提高设备运行效率。
// 初始化 NVS.
    ret = nvs_flash_init();
    /* 如果 NVS 分区没有剩余空间或者 NVS 分区包含新格式数据,当前版本无法识别。*/
    if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
        /* 清空 NVS 分区数据 */
        ESP_ERROR_CHECK(nvs_flash_erase());
        /* NVS 分区重新进行初始化 */
        ret = nvs_flash_init();
    }
    ESP_ERROR_CHECK( ret );
  1. 如果是 NVS 分区没有 RF(射频)校准数据,那么就会出现如下日志信息,之后就开始 RF 校准,并将相关校准信息存放进 NVS 分区。
W (602) phy_init: failed to load RF calibration data (0x1102), falling back to full calibration
W (642) phy_init: saving new calibration data because of checksum failure, mode(2)
  1. 如果不初始化 NVS 分区,将会出现如下错误。虽然不影响程序的正常运行,但是这样每次芯片启动都需要进行 RF 校准,从而拖慢启动速度
// 将 NVS 初始化注释掉
    // // 初始化 NVS.
    // ret = nvs_flash_init();
    // /* 如果 NVS 分区没有剩余空间或者 NVS 分区包含新格式数据,当前版本无法识别。*/
    // if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
    //     /* 清空 NVS 分区数据 */
    //     ESP_ERROR_CHECK(nvs_flash_erase());
    //     /* NVS 分区重新进行初始化 */
    //     ret = nvs_flash_init();
    // }
    // ESP_ERROR_CHECK( ret );
E (599) phy_init: esp_phy_load_cal_data_from_nvs: NVS has not been initialized. Call nvs_flash_init before starting WiFi/BT.
W (619) phy_init: failed to load RF calibration data (0x1101), falling back to full calibration
W (659) phy_init: saving new calibration data because of checksum failure, mode(2)
I (659) phy: libbtbb version: b97859f, Jun  4 2024, 16:44:27
E (669) BT_OSI: config_new: NVS not initialized. Call nvs_flash_init before initializing bluetooth.
W (679) BT_BTC: btc_config_init unable to load config file; starting unconfigured.

E (689) BT_OSI: config_save: NVS not initialized. Call nvs_flash_init before initializing bluetooth.
E (709) BT_OSI: config_save, err_code: 0x2

E (729) BT_OSI: config_save: NVS not initialized. Call nvs_flash_init before initializing bluetooth.
E (729) BT_OSI: config_save, err_code: 0x2

E (739) BT_OSI: config_save: NVS not initialized. Call nvs_flash_init before initializing bluetooth.
E (749) BT_OSI: config_save, err_code: 0x2

E (759) BT_OSI: config_save: NVS not initialized. Call nvs_flash_init before initializing bluetooth.
E (779) BT_OSI: config_save, err_code: 0x2

E (779) BT_OSI: config_save: NVS not initialized. Call nvs_flash_init before initializing bluetooth.
E (799) BT_OSI: config_save, err_code: 0x2

协议栈初始化

  1. 我们需要知道,ESP32 是存在两个存在两个蓝牙协议栈的。
  • Bluedroid(默认) : 支持传统蓝牙(BR/EDR)和低功耗蓝牙(BLE)。同时涉及传统蓝牙(BR/EDR)低功耗蓝牙(BLE) 的用例应当使用该协议栈。
  • Apache NimBLE : 仅支持低功耗蓝牙(BLE)。仅涉及低功耗蓝牙(BLE),建议使用该协议栈,因为在代码占用和运行时,NimBLE 对内存的要求较低
  1. 这里我将介绍的是 Bluedroid 协议栈开发。但是需要注意,虽然我们用的是 Bluedroid 协议栈,但实际情况却仅仅用到了 低功耗蓝牙(BLE) 的功能。因为,我们需要释放 传统蓝牙(BR/EDR) 的协议栈。
  2. 对蓝牙开发结构不清楚的朋友,可以看看 BLE学习笔记(0.0) —— 基础概念(0)
/* 释放经典蓝牙协议栈空间 */
    ESP_ERROR_CHECK(esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT));

    /* 初始化 Control 层 */
    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: %s", __func__, esp_err_to_name(ret));
        return;
    }

    /* 使能 Control 层 */
    ret = esp_bt_controller_enable(ESP_BT_MODE_BLE);
    if (ret) {
        ESP_LOGE(GATTS_TAG, "%s enable controller failed: %s", __func__, esp_err_to_name(ret));
        return;
    }

    /* 初始化 HOST层,Bluedroid 协议栈 */
    ret = esp_bluedroid_init();
    if (ret) {
        ESP_LOGE(GATTS_TAG, "%s init bluetooth failed: %s", __func__, esp_err_to_name(ret));
        return;
    }
    /* 使能 HOST层,Bluedroid 协议栈 */
    ret = esp_bluedroid_enable();
    if (ret) {
        ESP_LOGE(GATTS_TAG, "%s enable bluetooth failed: %s", __func__, esp_err_to_name(ret));
        return;
    }
  1. 通过上述操作,我们成功的初始化了ESP32 的 bluedroid 协议栈。那么,此时就需要创建 GAP 和 GATT 任务。当我们触发 GAP 或者 GATT 事件时候,就会进入到如下的回调函数中。需要注意,如下的两个函数只能注册一次。
/* 注册 GATTS 事件回调函数 */
    ret = esp_ble_gatts_register_callback(gatts_event_handler);
    if (ret){
        ESP_LOGE(GATTS_TAG, "gatts register error, error code = %x", ret);
        return;
    }
    /* 注册 GAP 事件回调函数 */
    ret = esp_ble_gap_register_callback(gap_event_handler);
    if (ret){
        ESP_LOGE(GATTS_TAG, "gap register error, error code = %x", ret);
        return;
    }
  1. 上述回调函数注册完成后,我们就可以根据需求来创建服务信息。这里我后续回进一步讲解。
/* 注册一个app_id, 协议栈将会分配一个对应的 gatts_if,用于标识一个 GATT 服务。
     * 调用这个函数就会触发 esp_ble_gatts_register_callback() 注册的回调函数中的 ESP_GATTS_REG_EVT 事件
     */
    ret = esp_ble_gatts_app_register(gl_profile_tab[PROFILE_A].app_id);
    if (ret){
        ESP_LOGE(GATTS_TAG, "gatts app register error, error code = %x", ret);
        return;
    }
    /* 注册一个app_id, 协议栈将会分配一个对应的 gatts_if,用于标识一个 GATT 服务。
     * 调用这个函数就会触发 esp_ble_gatts_register_callback() 注册的回调函数中的 ESP_GATTS_REG_EVT 事件
     */
    ret = esp_ble_gatts_app_register(gl_profile_tab[PROFILE_B].app_id);
    if (ret){
        ESP_LOGE(GATTS_TAG, "gatts app register error, error code = %x", ret);
        return;
    }
  1. 当我们希望每一次传输的数据更多,能够拥有更大的吞吐量,那么就可以调用这个函数设置本地的 MTU。需要注意的一点是,这个实际的 MTU 大小是要根据与客户端(client) 协商后得到的。
/* 设置本地 MTU 为 500 字节,实际交换时刻的 MTU 要通过与**客户端(client)**  协商后得到。
     * MTU 大小范围为 23 ~ 517 字节之间,如果没有调用该函数,则默认为 23 字节。
     */
    esp_err_t local_mtu_ret = esp_ble_gatt_set_local_mtu(500);
    if (local_mtu_ret){
        ESP_LOGE(GATTS_TAG, "set local  MTU failed, error code = %x", local_mtu_ret);
    }
  1. 当进行完成上述操作后,我们可以初始化广播包了。需要注意的一点是,这里有一个宏定义 CONFIG_SET_RAW_ADV_DATA 。如果我们希望广播包的内容更加灵活,就可以定义 CONFIG_SET_RAW_ADV_DATA 宏。如果是新手,还是不建议打开这个宏。
  2. 如果不打开这个宏,那么我们广播数据就会被局限为发送名称发射功率连接间隔外观厂商自定义数据服务 UUID 和数据广播发现模式标志位。如果你希望发送一些其他格式的数据例如 BTHome,使用 esp_ble_gap_config_adv_data() 函数就不是合适的选择。那么我们就需要使用原始数据包函数。
static void advertise_init(void)
{
    esp_err_t set_dev_name_ret = esp_ble_gap_set_device_name(TEST_DEVICE_NAME);
    if (set_dev_name_ret){
        ESP_LOGE(GATTS_TAG, "set device name failed, error code = %x", set_dev_name_ret);
    }
#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
}

GATT 回调事件

GATT 服务启动流程

  1. 现在我们开始看程序执行流程。当我们调用 esp_ble_gatts_app_register() 注册一个 APP 任务,就会触发 ESP_GATTS_REG_EVT 事件。这里需要注意的一点是,APP ID 可以为 0 ~ 65535 的任意值,但是不能重复
  2. 之后我们需要在 ESP_GATTS_REG_EVT 事件中将协议栈分配的 gatts_if 进行存储。同时利用 esp_ble_gatts_create_service() 函数注册一个首要服务。
  3. esp_ble_gatts_create_service() 函数执行完成后,将会触发 ESP_GATTS_CREATE_EVT 事件。在该事件中,我们可以调用 esp_ble_gatts_add_char() 函数创建对应的特征信息,并且调用 esp_ble_gatts_start_service() 函数启动服务。
  4. 在调用 esp_ble_gatts_add_char() 函数之后,将会触发 ESP_GATTS_ADD_CHAR_EVT 事件。如果特征信息中,含有 ESP_GATT_CHAR_PROP_BIT_NOTIFY 或者 ESP_GATT_CHAR_PROP_BIT_INDICATE ,那么就必须调用 esp_ble_gatts_add_char_descr() 函数创建一个 特征配置描述符(characteristic descriptor)
  5. 在我们调用 esp_ble_gatts_start_service() 函数启动服务之后,将会触发 ESP_GATTS_START_EVT 事件,用于通知服务端(server)应用程序服务启动成功。
  6. 在我们调用 esp_ble_gatts_add_char_descr() 函数创建 特征配置描述符(characteristic descriptor) 后,将会触发 ESP_GATTS_ADD_CHAR_DESCR_EVT 事件。
  7. 例如下方,我们的日志打印信息可以看到任务执行流程。
I (741) GATTS_DEMO: GATTS event = 0
I (741) GATTS_DEMO: gatts_if = 4,param->reg.app_id = 0x66
I (751) GATTS_DEMO: REGISTER_APP_EVT, status 0, app_id 102
I (761) GATTS_DEMO: GATTS event = 7
I (771) GATTS_DEMO: CREATE_SERVICE_EVT, status 0,  service_handle 44
I (781) GATTS_DEMO: GATTS event = 9
I (791) GATTS_DEMO: ADD_CHAR_EVT, status 0,  attr_handle 46, service_handle 44
I (801) GATTS_DEMO: GATTS event = 12
I (801) GATTS_DEMO: SERVICE_START_EVT, status 0, service_handle 44
I (811) GATTS_DEMO: GATTS event = 10
  • ESP_GATTS_REG_EVT : APP 注册事件。当调用 esp_ble_gatts_app_register() 函数之后,会触发该。
  • ESP_GATTS_CREATE_EVT : 服务创建完成事件。当调用 esp_ble_gatts_create_service() 函数之后,会触发该事件。
  • ESP_GATTS_ADD_CHAR_EVT : 特征申明(characteristic declaration)特征值(characteristic value) 添加成功事件。 当调用esp_ble_gatts_add_char() 函数之后触发该事件。
  • ESP_GATTS_START_EVT : GATT 服务启动成功事件。当调用 esp_ble_gatts_start_service() 函数之后,会触发该事件。
  • ESP_GATTS_ADD_CHAR_DESCR_EVT : 特征配置描述符(characteristic descriptor) 添加成功事件。当调用 esp_ble_gatts_add_char_descr() 函数之后,触发该事件。

连接/断连事件

  1. 当成功与客户端(client) 建立连接,触发 ESP_GATTS_CONNECT_EVT 事件。我们在该事件中利用 esp_ble_gap_update_conn_params() 函数发起连接参数更新请求。
  2. 这里有两个注意的点,因为我们注册了两个 APP,因此我们会发现日志信息中有两次 ESP_GATTS_CONNECT_EVT 事件触发。
I (1619781) GATTS_DEMO: GATTS event = 14
I (1619791) GATTS_DEMO: ESP_GATTS_CONNECT_EVT, conn_id 0, remote 56:ae:b5:84:6f:16:
I (1619801) GATTS_DEMO: GATTS event = 14
I (1619811) GATTS_DEMO: CONNECT_EVT, conn_id 0, remote 56:ae:b5:84:6f:16:
  1. 正因为 ESP_GATTS_CONNECT_EVT 事件会触发两次,因此我们将连接参数更新的内容放在了 A 服务的回调函数中。如果放在公共的 GATT 回调函数中,那么连接参数更新将会被调用两次,而这两次的间隔事件太短,因此会出现如下警告。
W (22451) BT_L2CAP: l2cble_start_conn_update, the last connection update command still pending.
  1. 当与客户端(client) 连接断开,将会触发 ESP_GATTS_DISCONNECT_EVT 事件。
I (2275581) GATTS_DEMO: GATTS event = 15
I (2275581) GATTS_DEMO: ESP_GATTS_DISCONNECT_EVT, disconnect reason 0x13
  • ESP_GATTS_CONNECT_EVT : 当与客户端(client) 连接,触发该事件。
  • ESP_GATTS_DISCONNECT_EVT : 与客户端(client) 断开连接时候,触发该事件。

读事件触发流程

  1. 当我们客户端(client) 发送读请求时,将会触发 ESP_GATTS_READ_EVT 事件。
  2. 在该事件中,我们将要返回给客户端(client) 的内容,通过 esp_ble_gatts_send_response() 函数返回。需要注意,一定要调用esp_ble_gatts_send_response() 函数进行返回,否则客户端(client) 将会一直死等数据,直到断连。
I (2029071) GATTS_DEMO: GATTS event = 1
I (2029071) GATTS_DEMO: GATT_READ_EVT, conn_id 0, trans_id 1, handle 46
I (2029071) GATTS_DEMO: GATTS event = 21
  1. 当调用 esp_ble_gatts_send_response() 函数之后,将会触发 ESP_GATTS_RESPONSE_EVT 事件,表示回包数据发送成功。
  • ESP_GATTS_READ_EVT : 当客户端(client) 发起读请求,触发该事件。
  • ESP_GATTS_RESPONSE_EVT : GATT 服务回报完成。该事件由 esp_ble_gatts_send_response() 函数执行完成后触发。

写事件触发流程

Command 写

  1. 当我们连接上设备后,点击发送按键。

一文迅速上手 ESP32 bluedroid 蓝牙从机开发_#define


2. 按照如下步骤发送数据。

一文迅速上手 ESP32 bluedroid 蓝牙从机开发_#define_02


3. 此时 ESP32 将会触发 ESP_GATTS_WRITE_EVT 事件。

  • param->write.conn_id : 如果是多连接,我们可以利用这个参数判断是那个主机发送的操作数据。因为该实验是一台手机作为客户端(client) ,一个 ESP32 作为服务端(Server)的点对点通讯,因此这个参数用不上。
  • param->write.trans_id : 用于标识一次写操作。因为客户端(client) 可能在同一时间对服务端(server)存在大量的写操作,那么就可以利用这个参数来确定服务端(server)到底是在对哪一个写操作回复。
  • param->write.handle : 用于表示是哪一个目标特征(Characteristic)或描述符(descriptor)的写操作。
  • param->write.need_rsp : 是否需要服务端(server)进行回复。因为这里我们是 Command,因此该参数是 false。
  • param->write.is_prep : 是否为准备写操作。当写入的数据大于 MTU-3 时候,将进行分段写数据,那么该参数为 true。
  • param->write.len : 写入数据值长度。
  • param->write.value : 写入的数据值。
  1. 通过上面的分析,我们现在就可以知道了,手机作为客户端(client) 进行 Command 写操作能够触发 ESP32 的 ESP_GATTS_WRITE_EVT 事件,而且无需回应,因此打印信息如下。
I (12711) GATTS_DEMO: GATTS event = 2
I (12711) GATTS_DEMO: GATT_WRITE_EVT, conn_id 0, trans_id 1, handle 46
I (12711) GATTS_DEMO: It's not prepare write, value len 2, value :
I (12721) GATTS_DEMO: 55 66 
I (12731) GATTS_DEMO: write event, but not need to response
  • ESP_GATTS_WRITE_EVT : 当客户端(client) 发起写请求,触发该事件。

Request 写

  1. 同理,我们执行写操作,只不过这次我们需要服务端(server)进行数据回复。

一文迅速上手 ESP32 bluedroid 蓝牙从机开发_GAP_03


2. 当客户端(client) 执行如上操作,将会触发 ESP_GATTS_WRITE_EVT 事件,在该事件中,我们调用 example_write_event_env() 处理写操作。

3. 因为客户端(client) 是需要数据回应。并且写入的数据小于 MTU - 3(初始化的时候。调用 esp_ble_gatt_set_local_mtu(),函数设置的 MTU 为23),因此 param->write.need_rsp 为 false ,不是准备写操作,直接调用 esp_ble_gatts_send_response() 进行数据包的回复。

4. 在调用 esp_ble_gatts_send_response() 函数之后,将会触发 ESP_GATTS_RESPONSE_EVT 事件告诉应用层表示回包数据发送完成。

I (56091) GATTS_DEMO: GATTS event = 2
I (56091) GATTS_DEMO: GATT_WRITE_EVT, conn_id 0, trans_id 3, handle 46
I (56091) GATTS_DEMO: It's not prepare write, value len 2, value :
I (56101) GATTS_DEMO: 11 22 
I (56111) GATTS_DEMO: write event, but not prepare
I (56121) GATTS_DEMO: GATTS event = 21
  • ESP_GATTS_WRITE_EVT : 当客户端(client) 发起写请求,触发该事件。
  • ESP_GATTS_RESPONSE_EVT : GATT 服务回报完成。该事件由 esp_ble_gatts_send_response() 函数执行完成后触发。

prepare write

  1. 我们需要按照如下方法将双方的 MTU 设置为 23。

一文迅速上手 ESP32 bluedroid 蓝牙从机开发_#include_04


一文迅速上手 ESP32 bluedroid 蓝牙从机开发_#define_05

  1. 需要注意一点,prepare write 操作一定需要选择 Request。
  2. 之后我们可以看到如下日志信息,我们一步一步来进行分析。
  3. 首先我们触发 ESP_GATTS_WRITE_EVT 事件发现这是一个准备写请求。那么就直接进入 example_write_event_env() 函数操作。因为在 prepare wirte 请求中,那么需要进行一些堆空间,以及判断传入数据是否超出堆空间大小。最终将收到的数据利用 esp_ble_gatts_send_response() 函数发送回去,并且存储在 a_prepare_write_env 或者 b_prepare_write_env 准备缓冲区。
  4. esp_ble_gatts_send_response() 函数会触发 ESP_GATTS_RESPONSE_EVT 事件表示发送了回包数据。
  5. 客户端(client) 收到回包数据后,继续将剩余的数据发送出来。此时再次触发 ESP_GATTS_WRITE_EVT 事件,因为这是 prepare wirte,因此进入 example_write_event_env() 函数操作,继续将发送回包,并且将剩余数据存储进 a_prepare_write_env 或者 b_prepare_write_env 准备缓冲区。
  6. esp_ble_gatts_send_response() 函数会触发 ESP_GATTS_RESPONSE_EVT 事件表示发送了回包数据。
  7. 客户端(client) 收到回包之后,发现数据发完了,此时执行 Execute Write Request,然后 ESP32 触发 ESP_GATTS_EXEC_WRITE_EVT 事件。在该事件中,ESP32 需要调用 esp_ble_gatts_send_response() 函数回发响应数据。然后调用 example_exec_write_event_env() 函数将收到的数据进行打印处理,并且释放申请到的缓冲区。
I (45331) GATTS_DEMO: GATTS event = 2
I (45331) GATTS_DEMO: GATT_WRITE_EVT, conn_id 0, trans_id 1, handle 46
I (45331) GATTS_DEMO: GATTS event = 21
I (45391) GATTS_DEMO: GATTS event = 2
I (45391) GATTS_DEMO: GATT_WRITE_EVT, conn_id 0, trans_id 2, handle 46
I (45391) GATTS_DEMO: GATTS event = 21
I (45451) GATTS_DEMO: GATTS event = 3
I (45451) GATTS_DEMO: ESP_GATTS_EXEC_WRITE_EVT
I (45451) GATTS_DEMO: 00 11 22 33 44 55 66 77 88 99 00 11 22 33 44 55 
I (45451) GATTS_DEMO: 66 77 88 99 00 
I (45461) GATTS_DEMO: GATTS event = 21
  • ESP_GATTS_WRITE_EVT : 当客户端(client) 发起写请求,触发该事件。
  • ESP_GATTS_RESPONSE_EVT : GATT 服务回报完成。该事件由 esp_ble_gatts_send_response() 函数执行完成后触发。
  • ESP_GATTS_EXEC_WRITE_EVT : 客户端(client) 发送 Execute Write Request 触发该事件。

特征配置描述符(characteristic descriptor) 写

  1. 我们点击如下按键可以使能 notify 功能。
  2. 亦或者可以通过如下方法数据数据使能 notify。

一文迅速上手 ESP32 bluedroid 蓝牙从机开发_GAP_06


一文迅速上手 ESP32 bluedroid 蓝牙从机开发_单片机_07


3. 这里感觉有个 bug,理论上来说,notify 应该是不需要回包的,因此不会触发 ESP_GATTS_CONF_EVT 事件的,但是这里依旧有触发,这里建议抓包测试一下。

一文迅速上手 ESP32 bluedroid 蓝牙从机开发_#include_08


4. 除了 ESP_GATTS_CONF_EVT 事件,其他两个事件就很好理解了。客户端(client) 向 ESP32 特征配置描述 中写入数据是需要回包的,因此就会触发 ESP_GATTS_WRITE_EVT 和 ESP_GATTS_RESPONSE_EVT 事件。

一文迅速上手 ESP32 bluedroid 蓝牙从机开发_#define_09

I (14481) GATTS_DEMO: GATTS event = 2
I (14481) GATTS_DEMO: GATT_WRITE_EVT, conn_id 0, trans_id 1, handle 47
I (14481) GATTS_DEMO: It's not prepare write, value len 2, value :
I (14491) GATTS_DEMO: 01 00 
I (14501) GATTS_DEMO: profile b notify enable
I (14511) GATTS_DEMO: write event, but not prepare
I (14521) GATTS_DEMO: GATTS event = 21
I (14521) GATTS_DEMO: GATTS event = 5
I (14531) GATTS_DEMO: ESP_GATTS_CONF_EVT status 0 attr_handle 46
  1. 当我们启动 indicate 或者输入 02 00 时候,将会触发如下事件。需要注意的一点是,notify 和 indicate 两者只能存在一个。
I (94731) GATTS_DEMO: GATTS event = 2
I (94731) GATTS_DEMO: GATT_WRITE_EVT, conn_id 0, trans_id 3, handle 47
I (94731) GATTS_DEMO: It's not prepare write, value len 2, value :
I (94741) GATTS_DEMO: 02 00 
I (94751) GATTS_DEMO: indicate enable
I (94751) GATTS_DEMO: write event, but not prepare
I (94761) GATTS_DEMO: GATTS event = 21
I (94821) GATTS_DEMO: GATTS event = 5
I (94821) GATTS_DEMO: ESP_GATTS_CONF_EVT status 0 attr_handle 46
  1. 当关闭 indicate/notify 或者输入 00 00 时候,将触发如下事件。需要注意的是,如果要关,那么两个都会同时关闭。
I (42801) GATTS_DEMO: GATTS event = 2
I (42801) GATTS_DEMO: GATT_WRITE_EVT, conn_id 0, trans_id 2, handle 47
I (42801) GATTS_DEMO: It's not prepare write, value len 2, value :
I (42811) GATTS_DEMO: 00 00 
I (42821) GATTS_DEMO: notify/indicate disable 
I (42831) GATTS_DEMO: write event, but not prepare
I (42831) GATTS_DEMO: GATTS event = 21
  • ESP_GATTS_WRITE_EVT : 当客户端(client) 发起写请求,触发该事件。
  • ESP_GATTS_RESPONSE_EVT : GATT 服务回报完成。该事件由 esp_ble_gatts_send_response() 函数执行完成后触发。
  • ESP_GATTS_CONF_EVT : 调用 esp_ble_gatts_send_indicate() 函数触发。

GAP 回调事件

GAP 服务初始化

  1. 在注册完 APP 之后,我们调用 advertise_init() 函数初始化广播数据。当我们调用 esp_ble_gap_config_adv_data() 函数时候,将会触发 ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT 事件。
  2. 之后调用 esp_ble_gap_config_adv_data() 函数,触发 ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT 事件。在该事件里面,我们调用 esp_ble_gap_start_advertising() 函数启动广播。
  3. esp_ble_gap_start_advertising() 函数将会触发 ESP_GAP_BLE_ADV_START_COMPLETE_EVT 事件。我们可以在该事件中知道广播是否成功启动。
I (841) GATTS_DEMO: GAP Event:0
I (841) GATTS_DEMO: GAP Event:1
I (851) GATTS_DEMO: GAP Event:6
  • ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT : 设置广播数据完成事件。当调用 esp_ble_gap_config_adv_data() 函数,其中 set_scan_rsp 设置为 false 时触发该事件。
  • ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT : 设置广播回包数据完成事件。当调用 esp_ble_gap_config_adv_data() 函数,其中 set_scan_rsp 设置为 true 时触发该事件。
  • ESP_GAP_BLE_ADV_START_COMPLETE_EVT : 当调用 esp_ble_gap_start_advertising() 函数触发。

连接事件

  1. 在连接过程中需要协商双方的收发数据,因此会触发 ESP_GAP_BLE_SET_PKT_LENGTH_COMPLETE_EVT 事件。
  2. 在连接成功后,ESP_GATTS_CONNECT_EVT 事件中调用的 esp_ble_gap_update_conn_params() 函数,将会触发连接参数更新,因此将会触发 ESP_GAP_BLE_UPDATE_CONN_PARAMS_EVT 事件。我们可以在该事件中看到最终的从机延迟连接间隔监管超时等信息。
I (1619781) GATTS_DEMO: GAP Event:21
I (1619781) GATTS_DEMO: packet length updated: rx = 27, tx = 251, status = 0
# ... 这里省略 GATT 回调
I (1620171) GATTS_DEMO: GAP Event:20
I (1620171) GATTS_DEMO: update connection params status = 0, min_int = 16, max_int = 32,conn_int = 6,latency = 0, timeout = 500
I (1620501) GATTS_DEMO: GAP Event:20
I (1620501) GATTS_DEMO: update connection params status = 0, min_int = 0, max_int = 0,conn_int = 24,latency = 0, timeout = 400

参考

  1. ESP32 蓝牙 API
  2. BLE学习笔记(0.0) —— 基础概念(0)
  3. 乐鑫论坛 : Write a string to characteristic