四川省住房与城乡建设厅网站官网wordpress收集
四川省住房与城乡建设厅网站官网,wordpress收集,公司装修孕妇怎么办,网站建设策划表ESP32蓝牙通信实战#xff1a;如何用gatts services实现手机与设备双向消息交互#xff08;附完整代码#xff09;
最近在做一个智能家居的控制器项目#xff0c;核心需求是让手机App能够实时控制ESP32设备#xff0c;并且设备也能主动向手机推送状态更新。这听起来像是典…ESP32蓝牙通信实战如何用gatts services实现手机与设备双向消息交互附完整代码最近在做一个智能家居的控制器项目核心需求是让手机App能够实时控制ESP32设备并且设备也能主动向手机推送状态更新。这听起来像是典型的蓝牙双向通信场景对吧但当我真正动手用ESP32的GATT Servergatts来实现时才发现“双向”这两个字背后藏着不少门道。网上能找到的教程要么是单向通知要么是手机主动读取真正像聊天一样你一句我一句的交互资料反而零散。今天我就把自己从搭建服务到调试成功的完整流程连同踩过的坑和最终可用的代码系统地梳理出来。如果你也卡在如何让手机发指令、设备能回复或者设备如何主动“说话”上这篇文章或许能给你一条清晰的路径。1. 理解双向通信的核心GATT角色与事件流在开始写代码之前我们必须摆脱一个常见的误解蓝牙BLE通信不是简单的“发送”和“接收”两个独立动作。对于ESP32作为GATT服务器设备端而言所有的数据交互都围绕着“客户端操作”所触发的事件。手机作为GATT客户端其每一个“写”或“读”的请求都会在ESP32端触发一个特定的事件。我们的代码逻辑本质上是对这些事件进行响应。这里最容易混淆的是“发送”的概念。设备端并没有一个随心所欲的send_to_phone()函数。它的“发送”行为通常绑定在两种机制上通知 (Notify)和指示 (Indicate) 这是设备主动向手机推送数据的主要方式。但前提是手机必须先订阅Enable该特性Characteristic的通知功能。对“读”请求的响应 当手机发起读操作时设备在对应的回调事件中返回数据。对“写”请求的响应 这是我们实现手机发、设备回模式的关键。设备可以在处理完手机写入的数据后立刻通过“指示”向手机回复数据。为了实现流畅的双向对话我们通常会设计两个特性特性A (e.g., 0xFF01) 属性为WRITE或WRITE_NR。手机向这里写数据触发设备的接收逻辑。特性B (e.g., 0xFF02) 属性为NOTIFY和/或INDICATE。设备通过这里向手机发送通知或指示。下面的表格对比了这三种数据流动方式数据流向触发方ESP32端关键事件常用API特点手机 → 设备手机ESP_GATTS_WRITE_EVT(无被动接收)设备被动接收在写事件中处理param-write.value。设备 → 手机 (响应读)手机ESP_GATTS_READ_EVTesp_ble_gatts_send_response一问一答由手机的读请求触发。设备 → 手机 (主动推)设备(无固定事件由应用逻辑触发)esp_ble_gatts_send_indicateesp_ble_gatts_send_response真正意义上的主动推送需手机先订阅。Indicate需确认更可靠。提示INDICATE比NOTIFY更可靠因为客户端必须回复一个确认包。对于关键的状态确认或指令回复建议使用INDICATE。理解了这张蓝图我们就能明白代码的核心结构是一个事件处理回调函数它像是一个路由器根据不同的事件类型将程序流导向不同的处理分支。2. 构建GATT服务与特性从蓝图到代码理论清晰后我们开始动手搭建。首先需要在ESP32上定义我们的服务“大厦”和里面的数据“房间”特性。2.1 定义UUID与属性表一切始于UUID。我们需要为自定义服务和特性生成唯一的标识符。这里我们使用标准的16位短UUID格式并拓展为128位。// 自定义服务与特性的UUID基于蓝牙基础UUID #define GATTS_SERVICE_UUID 0xFFF0 #define GATTS_CHAR_UUID_RX 0xFFF1 // 用于接收手机数据 (WRITE) #define GATTS_CHAR_UUID_TX 0xFFF2 // 用于向手机发送数据 (NOTIFY/INDICATE) // 将16位UUID扩展为128位 static uint16_t primary_service_uuid ESP_GATT_UUID_PRI_SERVICE; static uint16_t character_declaration_uuid ESP_GATT_UUID_CHAR_DECLARE; static uint16_t character_client_config_uuid ESP_GATT_UUID_CHAR_CLIENT_CONFIG; static esp_bt_uuid_t service_uuid { .len ESP_UUID_LEN_16, .uuid {.uuid16 GATTS_SERVICE_UUID,}, }; static esp_bt_uuid_t char_rx_uuid { .len ESP_UUID_LEN_16, .uuid {.uuid16 GATTS_CHAR_UUID_RX,}, }; static esp_bt_uuid_t char_tx_uuid { .len ESP_UUID_LEN_16, .uuid {.uuid16 GATTS_CHAR_UUID_TX,}, };接下来是最关键的一步定义属性表。这是一个数组它按顺序描述了GATT数据库中的所有属性包括服务声明、特性声明、特性值、描述符等。它的结构决定了蓝牙协议栈如何管理数据。// 属性表简化版展示核心结构 static const esp_gatts_attr_db_t gatts_db[] { // 服务声明 [IDX_SVC] {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)primary_service_uuid, ESP_GATT_PERM_READ, sizeof(service_uuid), sizeof(service_uuid), (uint8_t *)service_uuid}}, // 发送特性(TX)声明声明这是一个特性并指定其属性NOTIFY [IDX_CHAR_TX] {{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}}, // 发送特性(TX)的值这是实际存储要发送数据的地方 [IDX_CHAR_VAL_TX] {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)char_tx_uuid, ESP_GATT_PERM_READ, GATTS_DEMO_CHAR_VAL_LEN_MAX, sizeof(char_tx_value), (uint8_t *)char_tx_value}}, // 发送特性(TX)的客户端特性配置描述符(CCCD)手机通过写这个描述符来订阅/取消订阅通知 [IDX_CHAR_CFG_TX] {{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(char_tx_ccc_value), (uint8_t *)char_tx_ccc_value}}, // 接收特性(RX)声明属性为WRITE [IDX_CHAR_RX] {{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_write}}, // 接收特性(RX)的值手机写入的数据会存到这里 [IDX_CHAR_VAL_RX] {{ESP_GATT_RSP_BY_APP}, {ESP_UUID_LEN_16, (uint8_t *)char_rx_uuid, ESP_GATT_PERM_WRITE, GATTS_DEMO_CHAR_VAL_LEN_MAX, sizeof(char_rx_value), (uint8_t *)char_rx_value}}, // 注意这里响应模式是 BY_APP };注意IDX_CHAR_VAL_RX的响应模式设置为ESP_GATT_RSP_BY_APP而非ESP_GATT_AUTO_RSP。这至关重要它允许我们在应用层代码事件回调函数中接收到写请求从而有机会在处理完数据后立即进行回复。如果设为AUTO_RSP协议栈会自动回复我们就失去了在写事件中插入回复逻辑的机会。2.2 初始化与启动服务定义了属性表后我们需要在app_main()或类似的初始化函数中依次完成蓝牙控制器、蓝牙协议栈的初始化并注册应用Profile和事件回调函数。void app_main(void) { esp_err_t ret; // 1. 初始化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); // 2. 初始化蓝牙控制器 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); ESP_ERROR_CHECK(ret); ret esp_bt_controller_enable(ESP_BT_MODE_BLE); ESP_ERROR_CHECK(ret); // 3. 初始化蓝牙协议栈 ret esp_bluedroid_init(); ESP_ERROR_CHECK(ret); ret esp_bluedroid_enable(); ESP_ERROR_CHECK(ret); // 4. 注册GATT事件回调函数 ret esp_ble_gatts_register_callback(gatts_event_handler); ESP_ERROR_CHECK(ret); // 5. 注册应用Profile ret esp_ble_gatts_app_register(PROFILE_A_APP_ID); ESP_ERROR_CHECK(ret); // 6. 设置设备名称和广播参数 esp_ble_gap_set_device_name(ESP32_DUAL_COMM); esp_ble_gap_config_adv_data(adv_data); // ... 启动广播 }3. 事件处理回调双向通信的中枢所有交互逻辑都汇聚在gatts_event_handler这个函数中。它是整个GATT服务器的“大脑”。我们需要重点处理以下几个事件3.1 连接与断连事件static void gatts_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_ble_gatts_create_service esp_ble_gatts_create_service(gatts_if, service_uuid, GATTS_NUM_HANDLE); break; case ESP_GATTS_CREATE_EVT: // 服务创建成功在此事件中启动服务并添加特性调用esp_ble_gatts_add_char esp_ble_gatts_start_service(param-create.service_handle); // 添加TX特性NOTIFY和RX特性WRITE... break; case ESP_GATTS_CONNECT_EVT: // 手机连接成功 memcpy(gl_profile_tab[PROFILE_A_APP_ID].conn_id, param-connect.conn_id, sizeof(uint16_t)); gl_profile_tab[PROFILE_A_APP_ID].conn_id param-connect.conn_id; ESP_LOGI(GATTS_TAG, Device connected, conn_id %d, gl_profile_tab[PROFILE_A_APP_ID].conn_id); break; case ESP_GATTS_DISCONNECT_EVT: // 手机断开连接 ESP_LOGI(GATTS_TAG, Device disconnected); gl_profile_tab[PROFILE_A_APP_ID].conn_id 0xFFFF; // 重置连接ID // 重新开始广播等待下次连接 esp_ble_gap_start_advertising(adv_params); break;3.2 核心交互写事件与发送回复这是实现手机发、设备回的关键所在。当手机向RX特性属性为WRITE写入数据时会触发ESP_GATTS_WRITE_EVT。case ESP_GATTS_WRITE_EVT: { // 1. 判断写入的是哪个句柄特性 if (param-write.handle gl_profile_tab[PROFILE_A_APP_ID].char_handle_rx) { ESP_LOGI(GATTS_TAG, Received data on RX char, handle 0x%x, param-write.handle); // 2. 获取手机发送的数据 uint16_t data_len param-write.len; uint8_t *data param-write.value; ESP_LOGI(GATTS_TAG, Data length: %d, data_len); ESP_LOG_BUFFER_HEX(GATTS_TAG, data, data_len); // 3. 处理数据例如解析指令、控制GPIO、更新状态等 // 这里只是一个示例将收到的数据原样转为大写后回复 uint8_t reply_data[data_len]; for (int i 0; i data_len; i) { reply_data[i] toupper(data[i]); } // 4. 关键步骤通过TX特性向手机发送回复使用INDICATE // 注意这里使用的是TX特性的句柄而不是RX特性的句柄。 esp_err_t send_ret esp_ble_gatts_send_indicate( gatts_if, // GATT接口 param-write.conn_id, // 使用写事件中的连接ID gl_profile_tab[PROFILE_A_APP_ID].char_handle_tx, // **TX特性的句柄** data_len, // 回复数据长度 reply_data, // 回复数据 true // 需要确认 (INDICATE) ); if (send_ret ESP_OK) { ESP_LOGI(GATTS_TAG, Send indicate reply success.); } else { ESP_LOGE(GATTS_TAG, Send indicate reply failed, err %d, send_ret); } } // 5. 判断写入的是否是CCCD描述符手机订阅/取消订阅通知 else if (param-write.handle gl_profile_tab[PROFILE_A_APP_ID].char_handle_cccd) { // 处理订阅状态更新... } // 6. 必须手动发送对写操作的响应因为RX特性值设置为ESP_GATT_RSP_BY_APP esp_ble_gatts_send_response(gatts_if, param-write.conn_id, param-write.trans_id, ESP_GATT_OK, NULL); break; }这段代码有几个极易出错但至关重要的细节句柄 (Handle) 每个属性服务、特性、描述符在创建时都会被分配一个唯一的句柄。param-write.handle是手机写入的那个特性的句柄这里是RX特性。而esp_ble_gatts_send_indicate的第三个参数attr_handle是你要通过哪个特性发送数据的句柄这里是TX特性。务必分清“收”和“发”用的是两个不同的句柄。混淆它们会导致发送失败或数据发到错误的特性上。连接ID (conn_id) 每个连接都有一个ID。发送数据时必须使用当前连接的ID即param-write.conn_id。响应模式 由于RX特性值设置为ESP_GATT_RSP_BY_APP我们必须在事件处理末尾调用esp_ble_gatts_send_response来告知客户端写操作已完成。忘记这一步手机端可能会认为写操作超时失败。3.3 处理订阅与主动推送设备要能主动推送消息如传感器数据前提是手机已经订阅了TX特性的通知/指示。这发生在手机向CCCD描述符写入0x0001启用通知或0x0002启用指示时。// 在 ESP_GATTS_WRITE_EVT 事件中补充对CCCD写的处理 else if (param-write.handle gl_profile_tab[PROFILE_A_APP_ID].char_handle_cccd) { uint16_t cccd_value param-write.value[0] | (param-write.value[1] 8); if (cccd_value 0x0001) { ESP_LOGI(GATTS_TAG, Notification enabled by client.); gl_profile_tab[PROFILE_A_APP_ID].notify_enable true; } else if (cccd_value 0x0002) { ESP_LOGI(GATTS_TAG, Indication enabled by client.); gl_profile_tab[PROFILE_A_APP_ID].indicate_enable true; } else if (cccd_value 0x0000) { ESP_LOGI(GATTS_TAG, Notifications/Indications disabled by client.); gl_profile_tab[PROFILE_A_APP_ID].notify_enable false; gl_profile_tab[PROFILE_A_APP_ID].indicate_enable false; } }一旦订阅成功你就可以在应用的任何地方例如定时器中断、传感器数据就绪时主动调用esp_ble_gatts_send_indicate或esp_ble_gatts_send_response用于Notify来推送数据了。记得先检查conn_id是否有效已连接以及订阅是否已启用。4. 手机端交互要点与完整代码整合设备端准备好了手机端客户端也需要正确操作。以一款通用的蓝牙调试助手如 nRF Connect为例操作流程如下扫描并连接名为 “ESP32_DUAL_COMM” 的设备。发现服务后你会看到我们自定义的服务UUID: FFF0。展开服务找到两个特性TX特性 (UUID: FFF2) 点击其右侧的“通知”或“指示”图标三个小点进行订阅。成功订阅后图标会高亮或显示“已监听”。RX特性 (UUID: FFF1) 这是一个可写的特性。测试双向通信手机发 → 设备回在RX特性的写入框输入数据如“hello”点击发送。稍等片刻你应该能在日志或TX特性的值显示区看到设备回复的数据如“HELLO”。设备主动推在设备端代码中设置一个定时器每隔几秒通过TX特性发送一个数据包如当前时间。在手机端只要已订阅就能自动收到这些数据包。下面是一个整合了以上所有关键部分的简化版完整代码框架。由于篇幅限制它省略了完整的属性表定义、全局变量声明和一些错误处理但清晰地展示了核心逻辑的骨架。/* ESP32 GATT Server 双向通信核心代码框架 */ #include stdio.h #include string.h #include esp_log.h #include nvs_flash.h #include esp_bt.h #include esp_bt_main.h #include esp_gap_ble_api.h #include esp_gatts_api.h // ... (UUID定义、全局变量、属性表声明等) /* 主事件处理回调 */ static void gatts_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_ble_gatts_create_service(gatts_if, service_uuid, GATTS_NUM_HANDLE); break; case ESP_GATTS_CREATE_EVT: // 服务创建成功启动并添加特性 esp_ble_gatts_start_service(param-create.service_handle); // 添加TX、RX特性... break; case ESP_GATTS_ADD_CHAR_EVT: // 特性添加成功保存句柄 if (param-add_char.attr_handle ...) { /* 保存TX句柄 */ } if (param-add_char.attr_handle ...) { /* 保存RX句柄 */ } break; case ESP_GATTS_CONNECT_EVT: // 保存连接ID gl_profile_tab[PROFILE_A_APP_ID].conn_id param-connect.conn_id; break; case ESP_GATTS_DISCONNECT_EVT: // 重置连接状态 gl_profile_tab[PROFILE_A_APP_ID].conn_id 0xFFFF; esp_ble_gap_start_advertising(adv_params); break; case ESP_GATTS_WRITE_EVT: handle_write_event(gatts_if, param); // 将复杂的写事件处理封装成函数 break; default: break; } } /* 处理写事件的函数 */ static void handle_write_event(esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param) { // 1. 检查是否是写入RX特性手机发来数据 if (param-write.handle gl_profile_tab[PROFILE_A_APP_ID].char_handle_rx) { uint16_t len param-write.len; uint8_t *data param-write.value; ESP_LOGI(TAG, RX Write: len%d, data:, len); ESP_LOG_BUFFER_HEX(TAG, data, len); // 2. 处理业务逻辑示例回显并转为大写 uint8_t reply_data[len]; process_received_data(data, len, reply_data); // 你的业务处理函数 // 3. 通过TX特性发送回复Indicate if (gl_profile_tab[PROFILE_A_APP_ID].conn_id ! 0xFFFF) { esp_ble_gatts_send_indicate(gatts_if, param-write.conn_id, gl_profile_tab[PROFILE_A_APP_ID].char_handle_tx, len, reply_data, true); // 使用Indicate确保送达 } } // 4. 检查是否是写入CCCD订阅通知 else if (param-write.handle gl_profile_tab[PROFILE_A_APP_ID].char_handle_cccd) { uint16_t cccd_value param-write.value[0] | (param-write.value[1] 8); update_notify_status(cccd_value); // 更新订阅状态 } // 5. 发送写响应针对RX特性 if (param-write.handle gl_profile_tab[PROFILE_A_APP_ID].char_handle_rx) { esp_ble_gatts_send_response(gatts_if, param-write.conn_id, param-write.trans_id, ESP_GATT_OK, NULL); } } /* 应用主函数 */ void app_main(void) { // 初始化NVS、蓝牙控制器、协议栈... initialize_bluetooth_stack(); // 注册GATT事件回调 esp_ble_gatts_register_callback(gatts_event_handler); // 注册应用Profile esp_ble_gatts_app_register(PROFILE_A_APP_ID); // 设置设备名、广播数据并开始广播... start_advertising(); }把这个框架填充完整烧录到ESP32再配合手机端的蓝牙调试工具一个可靠的双向通信通道就搭建起来了。我最初调试时最耗时间的就是搞清句柄的对应关系以及忘记发送写响应导致的手机端超时。一旦理顺了“事件驱动”这个核心剩下的就是往这个框架里填充你自己的业务逻辑了。