1. 项目概述:从“能连上”到“能干活”的跨越
玩过一阵子ESP32-C3蓝牙的朋友,估计都跑过官方的GATT Server例程,看着手机上的蓝牙调试助手能连上设备,能发现一堆服务(Service)和特征值(Characteristic),感觉挺酷。但当你真正想用它做点自己的事情,比如让ESP32-C3上报一个温度值,或者接收一个指令控制LED时,很多人就卡住了。这个卡点,往往就出在“添加Service”这一步。
为什么?因为官方的例程通常是一个“大而全”的演示,它把蓝牙协议栈里能展示的都给你列出来了,UUID(通用唯一识别码)也是预设好的。但到了实际项目,你需要的是定义自己的服务,实现自己的业务逻辑。这就像给你一套精装修的样板房,看着挺好,但你想把书房改成电竞房,把客厅的墙刷成自己喜欢的颜色,却发现不知道水管电线怎么走,承重墙在哪。添加自定义Service,就是学习如何在这个“蓝牙协议栈”的毛坯房里,按照你的图纸进行水电改造和隔断。
简单来说,之前的测试可能让你实现了“设备可见和连接”,而本篇要解决的,是如何让设备具备“独特的功能”。我们将聚焦于使用ESP-IDF框架,从头开始创建一个全新的、自定义的GATT服务,并为其添加可读、可写、可通知的特征值。这不仅是ESP32-C3蓝牙开发的核心技能,也是将任何蓝牙低功耗(BLE)设备从原型推向实用产品的必经之路。
2. 理解蓝牙GATT架构:服务与特征值的角色
在动手写代码之前,我们必须把几个核心概念掰扯清楚。很多开发者照着例程能跑通,但一出错就懵,根本原因是对底层模型理解不透。蓝牙低功耗(BLE)的设备间通信,主要基于GATT(通用属性协议)架构,这个架构非常清晰,可以类比为一个提供特定服务的公司。
GATT Server(服务器): 就是我们的ESP32-C3设备,它像一个服务提供商(比如一家“智能家居数据服务公司”)。它对外提供一系列具体的服务。
Service(服务): 这是公司里的一个独立部门,每个部门提供一项独特的业务。例如,“环境监测部”专门负责上报温湿度,“设备控制部”专门负责接收开关指令。在蓝牙中,一个Service就是一组相关数据(称为特征值)的集合,用一个128位的UUID来唯一标识。为了简化,我们常用16位的短UUID(由蓝牙技术联盟SIG定义)或自定义的128位UUID。
Characteristic(特征值): 这是部门里具体的一项业务数据或一个操作接口。它是实际承载数据的最小单元。每个Characteristic也拥有自己的UUID,并且包含三个核心属性:
- Value(值): 数据本身,比如当前的温度值“25.5”。
- Properties(属性): 定义了客户端(如手机)可以对这个值进行什么操作。最常见的有:
READ: 客户端可以读取这个值。WRITE/WRITE_NR: 客户端可以写入这个值(后者无需服务器回复确认)。NOTIFY: 服务器可以主动向已订阅的客户端“通知”值的变化(这是实现实时数据推送的关键)。INDICATE: 类似NOTIFY,但需要客户端确认,更可靠。
- Descriptor(描述符): 最常用的是
CCCD(客户端特征配置描述符),当Characteristic具有NOTIFY或INDICATE属性时,必须包含它。客户端通过向这个描述符写入0x0001来开启通知,写入0x0000来关闭。
GATT Client(客户端): 就是我们的手机APP或者另一个ESP32设备,它像客户,来连接服务器,发现其提供的服务(部门),然后与具体的特征值(业务接口)进行交互,读取数据或发送指令。
所以,我们“添加Service”的实质,就是在ESP32-C3这个“公司”里,新建一个“部门”(Service),并为这个部门配置好具体的“业务接口”(Characteristic),规定好每个接口是只能看(READ)、只能改(WRITE),还是可以订阅最新动态(NOTIFY)。
3. 实战:从头定义并实现一个自定义环境监测服务
理论说再多不如一行代码。我们现在就来创建一个名为“环境监测服务”的自定义服务,它包含两个特征值:一个可读、可通知的“温度”特征,和一个可写的“LED控制”特征。
3.1 第一步:定义服务的UUID
UUID是服务的身份证。对于非标准服务,我们必须使用自定义的128位UUID,以避免与蓝牙标准服务冲突。我们可以使用在线UUID生成器,或者自己定义一个。在代码中,我们通常这样定义:
// 自定义环境监测服务的UUID (可以自己定义,这里是一个示例) #define ESP_CUSTOM_SERVICE_UUID 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01 // 温度特征值的UUID #define ESP_CUSTOM_CHAR_TEMP_UUID 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02 // LED控制特征值的UUID #define ESP_CUSTOM_CHAR_LED_UUID 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03 // 将上面的数组转换为esp_bt_uuid_t类型 static esp_bt_uuid_t custom_service_uuid = { .len = ESP_UUID_LEN_128, .uuid = {ESP_CUSTOM_SERVICE_UUID}, }; static esp_bt_uuid_t temp_char_uuid = { .len = ESP_UUID_LEN_128, .uuid = {ESP_CUSTOM_CHAR_TEMP_UUID}, }; static esp_bt_uuid_t led_char_uuid = { .len = ESP_UUID_LEN_128, .uuid = {ESP_CUSTOM_CHAR_LED_UUID}, };注意:UUID数组是大端字节序(Most Significant Byte First),即你在定义时写的第一个字节(如0xFF)是UUID的最高有效位。这在某些调试工具里显示时需要注意顺序。
3.2 第二步:创建GATT服务表
这是ESP-IDF中定义服务和特征值的核心数据结构。它是一个esp_gatts_attr_db_t类型的数组,按顺序描述了从服务声明到特征值描述符的所有属性。
// 定义GATT属性数据库 static const esp_gatts_attr_db_t custom_gatt_db[] = { // 服务声明 (Service Declaration) [IDX_CUSTOM_SVC] = {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&primary_service_uuid, ESP_GATT_PERM_READ, sizeof(custom_service_uuid.uuid), sizeof(custom_service_uuid.uuid), (uint8_t *)&custom_service_uuid.uuid}}, // 温度特征值声明 (Characteristic Declaration) [IDX_CUSTOM_CHAR_TEMP] = {{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_read_notify}}, // 温度特征值数值 (Characteristic Value) [IDX_CUSTOM_VAL_TEMP] = {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_128, (uint8_t *)&temp_char_uuid.uuid, ESP_GATT_PERM_READ, TEMP_VAL_LEN_MAX, sizeof(temp_value), (uint8_t *)temp_value}}, // 温度特征值的CCCD描述符 (Client Characteristic Configuration Descriptor) [IDX_CUSTOM_CFG_TEMP] = {{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(temp_cccd), (uint8_t *)temp_cccd}}, // LED控制特征值声明 [IDX_CUSTOM_CHAR_LED] = {{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}}, // LED控制特征值数值 [IDX_CUSTOM_VAL_LED] = {{ESP_GATT_RSP_BY_APP}, {ESP_UUID_LEN_128, (uint8_t *)&led_char_uuid.uuid, ESP_GATT_PERM_WRITE, LED_VAL_LEN_MAX, sizeof(led_value), (uint8_t *)led_value}}, // 注意这里不是AUTO_RSP };关键点解析:
- 索引管理:
IDX_CUSTOM_SVC、IDX_CUSTOM_CHAR_TEMP等是自定义的枚举或宏,用于在数组中定位每个属性。这比直接用数字更清晰,也便于后续在事件回调中通过attr_handle(属性句柄)来识别是哪个特征值被访问了。 - 属性类型:每个属性第一个参数是
{ESP_GATT_AUTO_RSP}或{ESP_GATT_RSP_BY_APP}。AUTO_RSP表示协议栈自动处理该属性的读写请求并回复。对于简单的、值固定的属性(如服务声明、特征声明),可以用这个。但对于需要执行我们自定义逻辑的特征值(比如写入LED控制指令后需要实际控制GPIO),我们必须使用RSP_BY_APP,这样读写请求会通过事件传递给我们自己的回调函数,由我们处理后再手动发送响应。 - 权限与长度:
ESP_GATT_PERM_READ和ESP_GATT_PERM_WRITE定义了权限。TEMP_VAL_LEN_MAX是客户端能读取的最大长度,sizeof(temp_value)是当前值的实际长度。对于可写的特征值,最大长度需要根据你预期接收的数据来合理设置,比如LED控制指令可能只是一个字节(0x00关,0x01开)。 - 特征值属性:
char_prop_read_notify和char_prop_write是esp_gatt_char_prop_t类型的变量,需要在别处定义,例如:static esp_gatt_char_prop_t char_prop_read_notify = ESP_GATT_CHAR_PROP_BIT_READ | ESP_GATT_CHAR_PROP_BIT_NOTIFY; static esp_gatt_char_prop_t char_prop_write = ESP_GATT_CHAR_PROP_BIT_WRITE;
3.3 第三步:实现GATT事件回调函数
这是整个GATT Server的“大脑”。所有客户端的连接、断开、读、写、订阅等操作,都会触发事件,并在这个回调函数中被处理。我们需要根据event类型和传递的参数来执行相应的操作。
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: // GATT Server注册成功事件 esp_ble_gatts_create_service(gatts_if, &custom_service_uuid, CUSTOM_SERVICE_HANDLE_START, CUSTOM_SERVICE_NUM_ATTRIBUTES); break; case ESP_GATTS_CREATE_EVT: // 服务创建成功事件 if (param->create.status == ESP_GATT_OK) { custom_service_handle = param->create.service_handle; // 保存服务句柄 esp_ble_gatts_start_service(custom_service_handle); // 启动服务 // 将之前定义的属性表添加到服务中 esp_ble_gatts_add_char_descr(custom_service_handle, &custom_gatt_db[0], IDX_CUSTOM_NUM, CUSTOM_SERVICE_HANDLE_START); } break; case ESP_GATTS_READ_EVT: // 读请求事件 handle_read_event(gatts_if, param); break; case ESP_GATTS_WRITE_EVT: // 写请求事件 handle_write_event(gatts_if, param); break; case ESP_GATTS_CONNECT_EVT: // 客户端连接事件 ESP_LOGI(GATTS_TAG, "Client connected, conn_id = %d", param->connect.conn_id); break; case ESP_GATTS_DISCONNECT_EVT: // 客户端断开事件 ESP_LOGI(GATTS_TAG, "Client disconnected"); // 断开后,需要重新开启广播以便其他设备连接 esp_ble_gap_start_advertising(&adv_params); break; // ... 处理其他必要事件,如MTU交换事件(ESP_GATTS_MTU_EVT)等 default: break; } }3.4 第四步:处理读/写请求的核心逻辑
读和写是交互的核心。我们需要在handle_read_event和handle_write_event函数中实现业务逻辑。
处理读请求 (handle_read_event): 对于温度特征值,当客户端发起读操作时,我们需要提供最新的温度数据。这里演示如何动态更新读取的值。
static void handle_read_event(esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param) { uint16_t handle = param->read.handle; // 获取被读的属性句柄 esp_gatt_rsp_t rsp; // 定义响应结构体 memset(&rsp, 0, sizeof(esp_gatt_rsp_t)); // 判断是哪个特征值被读取 if (handle == temp_char_handle) { // temp_char_handle 需要在属性添加成功后保存 // 假设我们从某个传感器(如DS18B20)读取了温度,这里用模拟值 float current_temp = read_temperature_sensor(); // 你的传感器读取函数 // 将float转换为字节数组(例如,转换为整数放大100倍后传输) int16_t temp_int = (int16_t)(current_temp * 100); rsp.attr_value.len = 2; rsp.attr_value.value[0] = temp_int & 0xFF; rsp.attr_value.value[1] = (temp_int >> 8) & 0xFF; // 设置响应状态并发送 rsp.attr_value.handle = handle; esp_ble_gatts_send_response(gatts_if, param->read.conn_id, param->read.trans_id, ESP_GATT_OK, &rsp); } else { // 对于其他非RSP_BY_APP的属性,或者未知句柄,可以返回错误 esp_ble_gatts_send_response(gatts_if, param->read.conn_id, param->read.trans_id, ESP_GATT_READ_NOT_PERMITTED, NULL); } }处理写请求 (handle_write_event): 对于LED控制特征值,当客户端写入一个值(比如0x01)时,我们需要解析这个值并控制GPIO。
static void handle_write_event(esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param) { uint16_t handle = param->write.handle; if (handle == led_char_handle && param->write.is_prep == false) { // 处理非“准备写入” uint8_t *data = param->write.value; uint16_t len = param->write.len; if (len == 1) { if (data[0] == 0x01) { gpio_set_level(LED_GPIO, 1); // 开LED ESP_LOGI(GATTS_TAG, "LED ON command received"); } else if (data[0] == 0x00) { gpio_set_level(LED_GPIO, 0); // 关LED ESP_LOGI(GATTS_TAG, "LED OFF command received"); } else { ESP_LOGW(GATTS_TAG, "Invalid LED command: 0x%02X", data[0]); } } else { ESP_LOGW(GATTS_TAG, "Invalid write length for LED: %d", len); } // 发送写响应(对于WRITE请求需要响应,WRITE_NR则不需要) if (param->write.need_rsp) { esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id, ESP_GATT_OK, NULL); } // 可选:更新特征值的本地值,以便后续读取能反映最新状态 esp_ble_gatts_set_attr_value(led_char_handle, len, data); } }3.5 第五步:实现数据主动通知(NOTIFY)
这是BLE中服务器主动向客户端推送数据的机制,对于传感器数据上报至关重要。当温度变化时,我们不需要等客户端来轮询读取,而是可以直接“通知”已订阅的客户端。
首先,在客户端通过写入CCCD开启通知后,我们需要在ESP_GATTS_WRITE_EVT事件中处理CCCD的写入:
// 在handle_write_event函数中添加对CCCD写的处理 if (handle == temp_cccd_handle) { // temp_cccd_handle 是温度特征值CCCD的描述符句柄 uint16_t descr_value = param->write.value[0] | (param->write.value[1] << 8); if (descr_value == 0x0001) { ESP_LOGI(GATTS_TAG, "Temperature Notify enabled for conn_id %d", param->write.conn_id); // 记录这个连接已经开启了通知,可以保存在一个连接信息结构体中 enable_notification_for_conn(param->write.conn_id, true); } else if (descr_value == 0x0000) { ESP_LOGI(GATTS_TAG, "Temperature Notify disabled for conn_id %d", param->write.conn_id); enable_notification_for_conn(param->write.conn_id, false); } }然后,在需要上报数据的地方(例如定时器中断、传感器数据就绪时),遍历所有已连接且开启了通知的客户端,发送通知:
void temperature_sensor_task(void *arg) { while (1) { float temp = read_temperature_sensor(); int16_t temp_int = (int16_t)(temp * 100); uint8_t notify_data[2] = {temp_int & 0xFF, (temp_int >> 8) & 0xFF}; // 假设我们有一个数组记录了所有开启通知的连接 for (int i = 0; i < MAX_CONNECTIONS; i++) { if (conn_info[i].connected && conn_info[i].notify_enabled) { esp_ble_gatts_send_indicate(gatts_if, conn_info[i].conn_id, temp_char_handle, sizeof(notify_data), notify_data, false); // false表示NOTIFY, true表示INDICATE } } vTaskDelay(pdMS_TO_TICKS(2000)); // 每2秒上报一次 } }4. 关键细节、避坑指南与调试技巧
按照上面的步骤,一个基本的自定义服务框架就搭起来了。但在实际烧录和调试过程中,你会遇到各种各样的问题。下面是我在多个项目中总结出的关键细节和常见坑点。
4.1 属性句柄(Attribute Handle)的管理
属性句柄是GATT Server内部用来唯一标识每个属性(服务、特征值、描述符)的16位数字。它在服务创建和属性添加时由协议栈分配。你必须妥善保存这些句柄,因为在回调事件中,你只能拿到handle,需要用它来判断是哪个特征值被访问了。
最佳实践:在ESP_GATTS_ADD_CHAR_EVT和ESP_GATTS_ADD_CHAR_DESCR_EVT事件中,保存特征值和描述符的句柄。
case ESP_GATTS_ADD_CHAR_EVT: if (param->add_char.service_handle == custom_service_handle) { if (param->add_char.char_uuid.uuid.uuid128 == temp_char_uuid.uuid.uuid128) { temp_char_handle = param->add_char.attr_handle; ESP_LOGI(GATTS_TAG, "Temperature Characteristic handle = 0x%04X", temp_char_handle); } else if (...) { // 保存其他特征值句柄 } } break; case ESP_GATTS_ADD_CHAR_DESCR_EVT: if (param->add_char_descr.service_handle == custom_service_handle) { // 通常通过特征值句柄+1来推断CCCD句柄,但最好在事件中保存 // 可以比较父句柄(param->add_char_descr.attr_handle - 1)来判断是哪个特征的CCCD if ((param->add_char_descr.attr_handle - 1) == temp_char_handle) { temp_cccd_handle = param->add_char_descr.attr_handle; } } break;4.2 MTU(最大传输单元)协商问题
MTU决定了单次蓝牙数据传输的最大字节数。默认是23字节,ATT头占3字节,实际有效数据只有20字节。如果你需要传输更长的数据(比如一张图片的片段、一段较长的配置信息),就必须协商一个更大的MTU。
坑点:如果你尝试发送超过当前MTU的数据,esp_ble_gatts_send_indicate会返回ESP_GATT_INVALID_ATTR_LEN错误,并且数据发不出去。
解决方案:
- 在连接事件
ESP_GATTS_CONNECT_EVT中,主动发起MTU交换:esp_ble_gattc_send_mtu_req(gattc_if, conn_id);(注意:对于Server端,通常等待Client发起,但Server也可以发起)。 - 在
ESP_GATTS_MTU_EVT事件中,获取协商后的MTU值:uint16_t mtu = param->mtu.mtu;。 - 确保你通过NOTIFY/INDICATE发送的数据长度
<= (mtu - 3)。
4.3 连接参数更新
BLE连接参数(连接间隔、从机延迟、监督超时)直接影响功耗和吞吐量。对于需要频繁上报数据的设备(如心率带),需要较短的连接间隔;对于电池供电的传感器,可能需要较长的连接间隔以省电。
操作:在连接建立后,Server可以调用esp_ble_gap_update_conn_params(&conn_params)来向Client建议新的连接参数。但最终决定权在Client(通常是手机)手中。你可以在ESP_GATTS_CONNECT_EVT事件后稍作延迟(例如1秒)再发起更新请求,以提高成功率。
4.4 广播数据(Advertising Data)与服务UUID
为了让手机能发现你的设备并识别出它支持的自定义服务,你需要在广播数据包中包含服务的UUID。
static esp_ble_adv_data_t adv_data = { .set_scan_rsp = false, .include_name = true, .include_txpower = false, .min_interval = 0x20, // 最小广播间隔 .max_interval = 0x40, // 最大广播间隔 .appearance = 0x00, .manufacturer_len = 0, .p_manufacturer_data = NULL, .service_data_len = 0, .p_service_data = NULL, .service_uuid_len = sizeof(custom_service_uuid.uuid), .p_service_uuid = custom_service_uuid.uuid, // 关键:包含自定义服务UUID .flag = (ESP_BLE_ADV_FLAG_GEN_DISC | ESP_BLE_ADV_FLAG_BREDR_NOT_SPT), };注意:广播包有31字节的长度限制。如果你包含了长设备名、厂商数据等多个字段,再加上128位的UUID(16字节),很容易超限。超限后广播会失败。务必使用esp_ble_gap_config_adv_data的返回值检查错误,或使用ESP_LOGI打印配置结果。
4.5 使用手机APP进行真机调试
不要只依赖ESP-IDF的日志。手机蓝牙调试APP(如nRF Connect,LightBlue)是必不可少的调试工具。
调试流程:
- 扫描与连接:在APP中扫描,确认你的设备名和广播UUID是否正确出现。
- 服务发现:连接后,查看“Discover Services”或类似选项,确认你的自定义服务(以你定义的UUID显示)是否被正确列出。
- 特征值操作:
- 读:点击具有
READ属性的特征值,查看返回的数据格式和值是否正确。 - 写:在具有
WRITE属性的特征值处,输入十六进制或ASCII值(如01),点击“Write”,观察ESP32的串口日志是否收到WRITE_EVT,以及GPIO是否动作。 - 通知:找到具有
NOTIFY属性的特征值,你会看到一个“订阅”或“启用通知”的按钮(这背后就是向CCCD写入0x0001)。点击启用后,观察APP是否开始自动接收数据,以及接收到的数据是否正确。
- 读:点击具有
- 错误排查:如果任何操作失败,APP通常会显示一个错误码(如
0x80等)。结合ESP32的串口日志(ESP_LOGE),可以快速定位问题。常见的错误有权限不足、句柄无效、数据过长等。
4.6 内存与资源管理
ESP32-C3内存有限。如果创建多个服务或特征值,注意esp_gatts_attr_db_t数组的大小。每个动态分配的特征值(RSP_BY_APP)都会占用一些内存。在项目开发后期,如果遇到奇怪的崩溃或连接不稳定,可以检查堆内存剩余量esp_get_free_heap_size()。
另外,确保你的GATT事件回调函数gatts_event_handler执行效率要高,不要在里面进行长时间阻塞的操作(如vTaskDelay)。复杂的业务逻辑应放到独立的FreeRTOS任务中,通过队列与回调函数通信。
5. 进阶:构建更健壮、可维护的GATT服务框架
当你的项目需要多个服务、十几个特征值时,用上面那种全局变量和巨型switch-case的写法会变得难以维护。下面分享一些架构上的优化思路。
5.1 面向对象的结构化设计
为每个“服务”定义一个结构体,封装其所有资源。
typedef struct { uint16_t service_handle; esp_bt_uuid_t service_uuid; // 特征值数组 struct { uint16_t char_handle; uint16_t cccd_handle; esp_bt_uuid_t uuid; esp_gatt_char_prop_t properties; uint8_t value[MAX_VAL_LEN]; uint16_t value_len; // 回调函数指针 esp_err_t (*on_read)(uint8_t *out_val, uint16_t *out_len); esp_err_t (*on_write)(uint8_t *in_val, uint16_t in_len); } characteristics[MAX_CHARS_PER_SERVICE]; uint8_t char_count; } ble_service_t; static ble_service_t env_monitor_service; static ble_service_t device_ctrl_service;然后在事件回调中,通过遍历服务数组和特征值数组,根据句柄找到对应的服务实例和特征值实例,再调用其注册的回调函数on_read或on_write。这样,每个服务的逻辑就高度内聚,代码清晰很多。
5.2 使用ESP-IDF的NVS(非易失性存储)保存配置
对于一些需要持久化的特征值,比如设备的名称、某些工作模式参数,可以在on_write回调中将值保存到NVS中,并在设备重启后从NVS读取并恢复特征值的初始值。
esp_err_t on_write_device_name(uint8_t *in_val, uint16_t in_len) { // 1. 校验数据... // 2. 更新本地变量 memcpy(device_name, in_val, in_len); device_name_len = in_len; // 3. 保存到NVS nvs_handle_t handle; ESP_ERROR_CHECK(nvs_open("storage", NVS_READWRITE, &handle)); ESP_ERROR_CHECK(nvs_set_blob(handle, "dev_name", device_name, device_name_len)); ESP_ERROR_CHECK(nvs_commit(handle)); nvs_close(handle); // 4. 更新广播数据(如果需要) update_adv_data(); return ESP_OK; }5.3 安全性与配对绑定
对于需要控制智能门锁、调节医疗设备参数等敏感操作,必须启用BLE安全功能。这涉及到配对、绑定和加密。
- 在广播数据中设置标志:
adv_data.flag可以包含ESP_BLE_ADV_FLAG_SEC_CON等。 - 配置IO能力:调用
esp_ble_gap_set_security_param(ESP_BLE_SM_IOCAP_MODE, &iocap, sizeof(uint8_t));设置设备的输入输出能力(如是否支持显示、键盘等)。 - 设置安全参数:调用
esp_ble_gap_set_security_param设置认证需求、加密密钥大小等。 - 处理安全事件:在GAP事件回调
gap_event_handler中处理ESP_GAP_BLE_SEC_REQ_EVT、ESP_GAP_BLE_AUTH_CMPL_EVT等事件。
启用安全后,只有配对绑定的客户端才能对具有ESP_GATT_PERM_READ_ENC或ESP_GATT_PERM_WRITE_ENC权限的特征值进行读写,大大提升了安全性。
添加自定义Service是ESP32-C3蓝牙开发从入门到精通的标志性一步。它意味着你不再只是协议栈的调用者,而是开始按照蓝牙规范的逻辑,设计和实现自己的通信协议。这个过程必然会遇到各种问题,从UUID定义错误、句柄管理混乱,到MTU协商失败、通知发送阻塞。但每一次问题的排查和解决,都会让你对BLE的理解更深一层。我建议你在实现基础功能后,尝试用手机APP连接并交互,然后有意识地制造一些“错误”(比如写一个超长的数据,或者不开启通知就尝试发送indicate),观察系统的反应和日志,这比单纯看文档学得更快。当你能够流畅地定义服务、处理读写、管理连接,并构建出清晰的服务层代码时,ESP32-C3在你手中就真正成为一个可靠、灵活的无线通信节点了。