简介
在上一篇文章中,我已经对蓝牙和蓝牙广播的相关知识进行了介绍。那么在本章节中我们将详细的了解蓝牙传输数据的几种方式,和使用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发送到手机。
附件