简介
在上一篇文章中,我已经对蓝牙和蓝牙广播的相关知识进行了介绍。那么在本章节中我们将详细的了解蓝牙传输数据的几种方式,和使用ESP-IDF来基于官方Demo的修改里实现一个GATTServer实现手机端对ESP32设备的订阅(notice)
对于一个蓝牙 GATT(Generic Attribute Profile)服务而言,其主要的功能是有,Write, Read,Write with no response, Notify, 和 indicate其主要的区别如下所示

ACK的意思是否进行确认,比如写的情况,当我写成功之后要求从机给我一个相应来确认是否写入成功。这个就是ACK。需要ACK的交互方式复杂度较高。一般被用于需要回调的敏感数据。
蓝牙 GATT(Generic Attribute Profile)服务简介
蓝牙 GATT(Generic Attribute Profile)服务 是 BLE 通信的核心,它定义了设备间数据的组织和交互方式。一个 GATT 服务由若干特征(Characteristic)组成,特征是具体的数据点,可以支持 读(Read)、写(Write/Write Without Response)、通知(Notify)、指示(Indicate) 等操作。通常,外围设备(GATT Server)保存数据,中心设备(GATT Client)通过这些操作来读取或修改数据,从而实现如传感器数据采集、智能家居控制、可穿戴设备同步等功能。
在ESP-IDF的环境下来实现GATT服务非常简单。官方有已经创建好的Demo示例。
首先我们需要创建GATT 项目

这份示例主要演示了 ESP32 作为 GATT Server 的典型用法。其主要流程如下:
设备广播(Advertising):ESP32 会定期广播自己的名称、UUID 等信息。
客户端连接:比如手机 App 或另一台 ESP32(运行 GATT Client 示例)可以扫描并连接该 Server。
特征读写:客户端可以读取 ESP32 提供的特征值(如本文例子中模拟的湿度数据),也可以向其写入数据。
通知功能(Notify):客户端启用 Notify 后,ESP32 会定时主动向客户端推送数据 (我这里进行了修改,数据是逐渐改变的。 采用的indicate模式,因此在接受到数据之后ESP32会收到来自手机的ACK确认)。
GATT Server 程序的执行流程大概是(建议配合Demo进行阅读):
1- 系统初始化(NVS、BLE 控制器、协议栈,已经在main方法中进行包含)。
esp_err_t ret;
// Initialize NVS.
ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND)
{
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
}
ESP_ERROR_CHECK(ret);
#if CONFIG_EXAMPLE_CI_PIPELINE_ID
memcpy(test_device_name, esp_bluedroid_get_example_name(), ESP_BLE_ADV_NAME_LEN_MAX);
#endif
ESP_ERROR_CHECK(esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT));
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;
}
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;
}
ret = esp_bluedroid_init();
if (ret)
{
ESP_LOGE(GATTS_TAG, "%s init bluetooth failed: %s", __func__, esp_err_to_name(ret));
return;
}
ret = esp_bluedroid_enable();
if (ret)
{
ESP_LOGE(GATTS_TAG, "%s enable bluetooth failed: %s", __func__, esp_err_to_name(ret));
return;
}2- 注册 GAP 和 GATTS 回调 (已经在main方法中进行包含)。
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;
}
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);
}3- 设置广播包并启动广播。
static esp_ble_adv_data_t adv_data = {
.set_scan_rsp = false,
.include_name = true, // 说明蓝牙广播中包含name
.include_txpower = false,
.service_uuid_len = sizeof(adv_service_uuid128),
.p_service_uuid = adv_service_uuid128, //指定的128位自定义服务ID
.flag = (ESP_BLE_ADV_FLAG_GEN_DISC | ESP_BLE_ADV_FLAG_BREDR_NOT_SPT), # 说明这是一个BLE设备,不支持传统蓝牙
};之后便是设置广播数据,并且启动广播(这些数据代码都在官方代码的对应event处理中,实际开发的时候不需要手动去操作每一个event,只需要在对应的event里来增加自己的代码就可以了。)
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);然后就是注册GATT服务
esp_ble_gatts_create_service(gatts_if, &gl_profile_tab[PROFILE_A_APP_ID].service_id, GATTS_NUM_HANDLE_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);
由于在上方我已经定义了原本的服务ID为 0x1809
#define GATTS_SERVICE_UUID_TEST_A 0x1809
所以当前的服务会被识别为Health Thermometer Service服务
4- 客户端连接 ESP32,建立 GATT 会话。
5- 处理客户端请求(读、写、订阅 Notify)。
然后在这里我对官方的代码进行了一点修改,使其连接上之后,如果进行读的话,那么我会发送给他一个固定的数据。
case ESP_GATTS_READ_EVT:
{
ESP_LOGI(GATTS_TAG, "Characteristic read, conn_id %d, trans_id %" PRIu32 ", handle %d",
param->read.conn_id, param->read.trans_id, param->read.handle);
uint8_t humi = 56; // 湿度%
esp_gatt_rsp_t rsp;
memset(&rsp, 0, sizeof(rsp));
rsp.attr_value.handle = param->read.handle;
rsp.attr_value.len = 1; // 只发一个字节
rsp.attr_value.value[0] = humi;
esp_ble_gatts_send_response(gatts_if, param->read.conn_id, param->read.trans_id, ESP_GATT_OK, &rsp);
break;
}6- 若开启 Notify,ESP32 定时向客户端推送数据。
void notify_task(void *param) {
while (notify_enabled) {
uint8_t data[15];
for (int i = 0; i < sizeof(data); ++i) {
data[i] = notify_counter + i;
}
notify_counter++;
esp_ble_gatts_send_indicate(gatts_if, conn_id, char_handle,
sizeof(data), data, false);
vTaskDelay(pdMS_TO_TICKS(1000)); // 每秒推送一次
}
}这里我也进行了修改,原本的订阅只是一个固定的数据,我在GATT的写事件里增加了一个任务的判断,使其如果当前的用户开启了通知的话,那么会判断当前的任务是否存在,如果不存在的话,启动一个task定期向手机端发送数据。如果用户点击关闭notify的话。那么则删除掉这个任务。这样的话就可以实现ESP32一直向手机端发送数据的功能了。
case ESP_GATTS_WRITE_EVT:
{
ESP_LOGI(GATTS_TAG, "Characteristic write, 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, "value len %d, value ", param->write.len);
ESP_LOG_BUFFER_HEX(GATTS_TAG, param->write.value, param->write.len);
if (gl_profile_tab[PROFILE_A_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 (a_property & ESP_GATT_CHAR_PROP_BIT_NOTIFY)
{
ESP_LOGI(GATTS_TAG, "Notification enable");
notify_enabled = true;
if (notify_task_handle == NULL)
{
uint16_t *conn_id_copy = malloc(sizeof(uint16_t));
*conn_id_copy = param->write.conn_id;
xTaskCreate(notify_task,
"notify_task",
4096,
conn_id_copy,
5,
¬ify_task_handle);
}
}
}
else if (descr_value == 0x0002)
{
if (a_property & ESP_GATT_CHAR_PROP_BIT_INDICATE)
{
ESP_LOGI(GATTS_TAG, "Indication 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_APP_ID].char_handle,
sizeof(indicate_data), indicate_data, true);
}
}
else if (descr_value == 0x0000)
{
ESP_LOGI(GATTS_TAG, "Notification/Indication disable");
notify_enabled = false;
}
else
{
ESP_LOGE(GATTS_TAG, "Unknown descriptor 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;
}效果演示
1- 手机搜索BLE服务

2- 查看其广播数据

3- 连接后查看具有的服务(已经成功的识别到我们定义的服务信息)

4- 读数据,成功读取到我们自定义的56,转换成Hex为0x38

5- 通知数据,自动刷新。点击notify后,数据自动发送,取消之后,数据停止发送


上图为不同的value值,从ESP32发送到手机。
附件
我要赚赏金
