微信小程序与ESP32-C3蓝牙通信实战
微信小程序与 ESP32 系列芯片蓝牙通信实战:从协议栈配置到双向数据交互
1. 工程背景与技术选型依据
在嵌入式智能小车等低功耗、短距、人机交互密集型场景中,蓝牙通信相较 Wi-Fi 具有显著优势:无需接入路由器、无 IP 地址管理开销、连接建立延迟低(典型值 <100ms)、功耗可控(BLE 模式下待机电流可低于 10μA),且天然适配手机端直连。ESP32-C3 作为 RISC-V 架构的低成本双模无线 SoC,其 Bluetooth LE 5.0 子系统已通过 SIG 认证,硬件支持链路层(LL)、主机控制器接口(HCI)及部分 GATT 协议栈功能,配合 ESP-IDF 中成熟的 bluedroid 协议栈,可实现完整 BLE 外设(Peripheral)角色开发。
本方案摒弃传统串口蓝牙模块(如 HC-05/HC-08)的 AT 指令模式,直接基于 ESP-IDF 的 esp_ble_gatts_api.h 和 esp_gap_ble_api.h 接口构建原生 BLE 服务,规避了指令解析、状态同步、波特率匹配等中间层问题。微信小程序端则利用 wx.openBluetoothAdapter 和 wx.createBLEConnection 等标准 API 实现跨平台 BLE 控制,避免开发独立 App 带来的分发与维护成本。
需明确的是:该方案不依赖任何第三方“蓝牙调试工具”APP,而是将 ESP32-C3 配置为标准 BLE 外设,小程序作为中央设备(Central)主动发现、连接并读写其 GATT 特征值。所有通信均遵循 Bluetooth SIG 定义的通用属性规范(GATT),确保协议兼容性与长期可维护性。
2. ESP32-C3 BLE 外设固件开发
2.1 开发环境与依赖配置
使用 ESP-IDF v5.1.2(LTS 版本)作为基础框架,该版本对 ESP32-C3 的 BLE 支持稳定,且 bluedroid 协议栈默认启用。项目根目录下的 sdkconfig 中需确认以下关键选项已启用:
CONFIG_BT_ENABLED=y
CONFIG_BT_BLUEDROID_ENABLED=y
CONFIG_BT_BLE_42_FEATURES=y
CONFIG_BT_BLE_50_FEATURES=y
CONFIG_BT_GATTS_ENABLE=y
CONFIG_BT_GATT_MAX_SRVC_INSTANCES=1
CONFIG_BT_GATT_MAX_CHAR_INSTANCES=4
CONFIG_BT_GATT_MAX_ATTR_LEN=512
特别注意 CONFIG_BT_GATT_MAX_ATTR_LEN 必须设为 ≥512,否则无法容纳后续定义的长特征值(如 256 字节控制指令缓冲区)。若未启用 CONFIG_BT_BLE_50_FEATURES ,则无法使用高吞吐量的 LE 2M PHY 模式,但本方案默认采用兼容性更强的 1M PHY。
2.2 BLE 服务与特征值设计
根据小车控制需求,定义一个精简但完备的 GATT 服务结构。服务 UUID 采用 128 位自定义值,避免与标准服务冲突;特征值按读/写/通知权限分离,符合 BLE 最佳实践:
| 服务 UUID | 00001111-0000-1000-8000-00805F9B34FB |
|---|---|
| 特征值 1:控制指令接收(Write Without Response) | |
| UUID | 00002222-0000-1000-8000-00805F9B34FB |
| 权限 | ESP_GATT_PERM_WRITE + ESP_GATT_CHAR_PROP_BIT_WRITE_NR |
| 描述 | 手机端向 ESP32 发送控制指令(如 ‘1’ 启动电机,‘2’ 停止,’F’ 前进) |
| 特征值 2:状态反馈通知(Notify) | |
| UUID | 00003333-0000-1000-8000-00805F9B34FB |
| 权限 | ESP_GATT_PERM_READ + ESP_GATT_CHAR_PROP_BIT_NOTIFY |
| 描述 | ESP32 主动向手机推送传感器状态、执行结果或错误码 |
| 特征值 3:设备信息读取(Read) | |
| UUID | 00004444-0000-1000-8000-00805F9B34FB |
| 权限 | ESP_GATT_PERM_READ + ESP_GATT_CHAR_PROP_BIT_READ |
| 描述 | 固件版本、MAC 地址、运行状态等只读信息 |
该设计严格遵循 GATT 分层模型:一个服务(Service)包含多个特征值(Characteristic),每个特征值拥有独立的值句柄(Handle)和客户端配置描述符(CCCD)。 Write Without Response 模式用于控制指令,因无需等待 ACK,降低通信延迟; Notify 模式用于状态推送,避免手机轮询,节省双方功耗。
2.3 GAP 广播参数配置
广播是 BLE 连接的第一步,直接影响发现成功率与功耗。在 app_main() 中初始化 GAP 参数:
esp_ble_adv_data_t adv_data = {
.set_scan_rsp = false,
.include_name = true,
.include_txpower = true,
.min_interval = 0x0020, // 32 * 0.625ms = 20ms
.max_interval = 0x0040, // 64 * 0.625ms = 40ms
.appearance = 0x0000,
.manufacturer_len = 0,
.p_manufacturer_data = NULL,
.service_data_len = 0,
.p_service_data = NULL,
.service_uuid_len = 0,
.p_service_uuid = NULL,
.flag = (ESP_BLE_ADV_FLAG_GEN_DISC | ESP_BLE_ADV_FLAG_BREDR_NOT_SPT)
};
esp_ble_adv_params_t adv_params = {
.adv_int_min = 0x0020,
.adv_int_max = 0x0040,
.adv_type = ADV_TYPE_IND,
.own_addr_type = BLE_ADDR_TYPE_PUBLIC,
.channel_map = ADV_CHNL_ALL,
.adv_filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY,
};
关键参数解析:
- min_interval / max_interval 设为 20–40ms:平衡发现速度与功耗。过短(如 10ms)导致射频占空比过高,加速电池消耗;过长(如 1s)则用户点击“连接”后需等待数秒才能被扫描到。
- flag 设置 ADV_FLAG_GEN_DISC :声明设备为可发现(Discoverable)且可连接(Connectable),这是微信小程序扫描到设备的前提。
- own_addr_type = BLE_ADDR_TYPE_PUBLIC :使用芯片烧录时写入的公有 MAC 地址,确保设备标识唯一性。若使用随机地址,需额外处理地址持久化,增加复杂度。
广播名称(Device Name)在 esp_ble_gap_set_device_name("BOE-THINK") 中设置,与小程序中预设名称严格一致,避免用户手动输入错误。
2.4 GATT 服务注册与事件处理
GATT 服务注册是核心步骤,需在 esp_ble_gatts_register_callback() 注册回调函数后调用 esp_ble_gatts_app_register() 。服务定义采用数组形式,清晰映射各特征值:
static const uint16_t gatt_srvc_id = 0x00;
static const uint16_t gatt_char_id = 0x01;
static const uint16_t gatt_desc_id = 0x02;
// GATT 数据库定义(简化版)
static const esp_gatts_attr_db_t gatt_db[HRS_IDX_NB] = {
// Service Declaration
[HRS_IDX_SVC] =
{{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&primary_service_uuid, ESP_GATT_PERM_READ}},
// Characteristic Declaration for Control
[HRS_IDX_CHAR_CTRL] =
{{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&character_declaration_uuid, ESP_GATT_PERM_READ}},
// Characteristic Value for Control (UUID: 00002222-...)
[HRS_IDX_CHAR_CTRL_VAL] =
{{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_128, (uint8_t *)ctrl_char_uuid, ESP_GATT_PERM_WRITE}},
// Characteristic Declaration for Notify
[HRS_IDX_CHAR_NOTIFY] =
{{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&character_declaration_uuid, ESP_GATT_PERM_READ}},
// Characteristic Value for Notify (UUID: 00003333-...)
[HRS_IDX_CHAR_NOTIFY_VAL] =
{{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_128, (uint8_t *)notify_char_uuid, ESP_GATT_PERM_READ}},
// Client Characteristic Configuration Descriptor (CCCD)
[HRS_IDX_CHAR_NOTIFY_CFG] =
{{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&character_client_config_uuid, ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE}},
};
事件处理函数 gatts_event_handler() 是 BLE 逻辑中枢,需响应三类关键事件:
- ESP_GATTS_REG_EVT :服务注册成功后,调用 esp_ble_gatts_create_service() 创建服务实例,并 esp_ble_gatts_start_service() 启动。
- ESP_GATTS_CONNECT_EVT :设备被连接时,记录连接句柄(conn_id),并启动 notify 功能(通过 esp_ble_gatts_send_response() 写入 CCCD)。
- ESP_GATTS_WRITE_EVT :当手机写入控制特征值时,提取 p_data->value 缓冲区内容,进行指令解析。
指令解析逻辑需健壮: p_data->value 是原始字节数组,长度由 p_data->value_len 给出。实际代码中不应简单判断 if (data[0] == '1') ,而应先校验长度(如 p_data->value_len >= 1 ),再转换为字符串处理,避免内存越界。例如:
if (p_data->value_len > 0 && p_data->value_len <= sizeof(ctrl_buffer)) {
memcpy(ctrl_buffer, p_data->value, p_data->value_len);
ctrl_buffer[p_data->value_len] = '\0'; // 确保 null-terminated
ESP_LOGI(TAG, "Received control: %s", ctrl_buffer);
// 后续解析 '1', '2', 'F' 等指令
}
2.5 双向数据通道实现
2.5.1 手机→ESP32:控制指令接收
ESP_GATTS_WRITE_EVT 事件中获取的数据即为手机发送的原始字节流。为提升用户体验,小程序端通常发送 ASCII 字符(如单字符 ‘1’ 或字符串 “MOTOR_ON”)。固件需设计轻量级解析器:
- 单字符指令 :适用于简单控制(‘1’=启动,‘0’=停止,’F’=前进,’B’=后退,’L’=左转,’R’=右转)。解析快,资源占用少。
- JSON 指令 :适用于复杂场景(如
{"cmd":"move","speed":80,"dir":"forward"})。需集成轻量 JSON 解析库(如 cJSON),增加约 12KB Flash 占用,但扩展性强。
本方案采用单字符+扩展字符串混合模式:前导字符标识指令类型,后续字节为参数。例如 'S80' 表示设置速度为 80, 'P1234' 表示播放预设动作序列 1234。解析函数如下:
typedef enum {
CMD_NONE,
CMD_START,
CMD_STOP,
CMD_SPEED,
CMD_PLAY_SEQ
} cmd_type_t;
cmd_type_t parse_control_cmd(const uint8_t* data, uint16_t len, int* param) {
if (len == 0) return CMD_NONE;
switch (data[0]) {
case '1': *param = 0; return CMD_START;
case '0': *param = 0; return CMD_STOP;
case 'S': // Speed: S80 -> speed=80
if (len > 1 && isdigit(data[1])) {
*param = atoi((char*)&data[1]);
return CMD_SPEED;
}
break;
case 'P': // Play Sequence: P1234
if (len > 1) {
*param = atoi((char*)&data[1]);
return CMD_PLAY_SEQ;
}
break;
default:
break;
}
return CMD_NONE;
}
2.5.2 ESP32→手机:状态反馈通知
状态推送通过 esp_ble_gatts_send_indicate() 或 esp_ble_gatts_send_notify() 实现。 notify 无需客户端确认,适合高频状态更新; indicate 需等待 ACK,适合关键事件(如错误告警)。本方案对传感器数据使用 notify ,对错误码使用 indicate 。
关键点在于 CCCD(Client Characteristic Configuration Descriptor)的处理:只有当手机端写入 0x0001 到 CCCD 时,才允许发送 notify。因此,在 ESP_GATTS_WRITE_EVT 中需监听 CCCD 写入:
if (p_data->handle == HRS_IDX_CHAR_NOTIFY_CFG) {
uint16_t value = 0;
if (p_data->value_len == 2) {
value = ((uint16_t)p_data->value[1] << 8) | p_data->value[0];
if (value == 0x0001) {
notify_enabled = true;
ESP_LOGI(TAG, "Notify enabled for connection %d", conn_id);
} else if (value == 0x0000) {
notify_enabled = false;
ESP_LOGI(TAG, "Notify disabled");
}
}
}
状态数据封装为固定格式字符串(如 "STAT:OK,VOLT:3.3,TEMP:25.5" ),长度不超过 GATT MTU(默认 23 字节)。若需发送长数据,必须分包并添加序号,或协商增大 MTU(通过 esp_ble_gattc_exchange_mtu() )。
2.6 FreeRTOS 任务协同机制
ESP32-C3 的双核架构(CPU0 为 PRO CPU,CPU1 为 APP CPU)要求任务调度谨慎。BLE 协议栈主要运行在 PRO CPU 上,用户任务宜绑定至 APP CPU,避免抢占 BLE 中断。创建控制任务时指定 CPU 核心:
xTaskCreatePinnedToCore(
control_task, // 任务函数
"control_task", // 任务名
4096, // 栈大小(字节)
NULL, // 参数
5, // 优先级
&control_task_handle,
1 // 绑定到 APP CPU (core ID 1)
);
控制任务主循环中,通过队列( xQueueReceive() )接收来自 GATT 事件处理器的指令,解耦协议栈与业务逻辑:
void control_task(void* pvParameters) {
cmd_msg_t cmd;
while(1) {
if (xQueueReceive(cmd_queue, &cmd, portMAX_DELAY) == pdTRUE) {
switch(cmd.type) {
case CMD_START:
motor_start();
break;
case CMD_SPEED:
motor_set_speed(cmd.param);
break;
// ... 其他指令
}
}
}
}
此设计使 gatts_event_handler() 仅做数据搬运,耗时操作(如电机 PWM 调制、PID 计算)在独立任务中执行,保障 BLE 通信实时性。
3. 微信小程序端 BLE 通信实现
3.1 小程序基础配置
在 app.json 中声明蓝牙权限:
{
"permission": {
"scope.bluetooth": {
"desc": "用于连接并控制智能小车"
}
}
}
在 app.js 的 onLaunch 生命周期中初始化蓝牙适配器,这是所有 BLE 操作的前提:
App({
onLaunch() {
wx.openBluetoothAdapter({
success: () => {
console.log('Bluetooth adapter opened');
this.getConnectedDevices(); // 获取已连接设备
},
fail: (err) => {
console.error('Failed to open adapter:', err);
wx.showToast({ title: '请开启手机蓝牙', icon: 'none' });
}
});
},
getConnectedDevices() {
wx.getConnectedBluetoothDevices({
services: ['00001111-0000-1000-8000-00805F9B34FB'],
success: (res) => {
if (res.devices.length > 0) {
this.deviceId = res.devices[0].deviceId;
this.connectToDevice();
}
}
});
}
});
wx.openBluetoothAdapter 是异步操作,必须等待 success 回调后才能调用其他蓝牙 API,否则返回 not init 错误。
3.2 设备发现与连接流程
连接流程需严格遵循微信小程序 BLE API 的时序约束:
- 开始搜索 :调用
wx.startBluetoothDevicesDiscovery(),指定services数组过滤目标服务 UUID。 - 监听发现事件 :使用
wx.onBluetoothDeviceFound()监听新设备。该事件可能触发多次,需去重(以deviceId为 key)。 - 连接设备 :对目标设备(
name === "BOE-THINK")调用wx.createBLEConnection()。 - 获取服务与特征值 :连接成功后,依次调用
wx.getBLEDeviceServices()和wx.getBLEDeviceCharacteristics()。
关键陷阱: wx.onBluetoothDeviceFound() 是监听器,必须在 wx.startBluetoothDevicesDiscovery() 之前注册,否则首次扫描可能丢失设备。完整流程代码:
startScan() {
wx.startBluetoothDevicesDiscovery({
services: ['00001111-0000-1000-8000-00805F9B34FB'],
success: () => {
console.log('Start discovery');
wx.onBluetoothDeviceFound((devices) => {
devices.forEach(device => {
if (device.name === 'BOE-THINK' && !this.connected) {
this.deviceId = device.deviceId;
this.connectToDevice();
}
});
});
}
});
},
connectToDevice() {
wx.createBLEConnection({
deviceId: this.deviceId,
success: () => {
console.log('Connected to BOE-THINK');
this.getServicesAndCharacteristics();
},
fail: (err) => {
console.error('Connect failed:', err);
wx.showToast({ title: '连接失败', icon: 'none' });
}
});
}
3.3 特征值读写与通知订阅
获取特征值后,需为通知特征值( 00003333-... )启用 notify,并为控制特征值( 00002222-... )准备写入。
3.3.1 启用 Notify
Notify 需两步:首先调用 wx.notifyBLECharacteristicValueChange() 启用通知,然后监听 wx.onBLECharacteristicValueChange() 事件:
enableNotify() {
wx.notifyBLECharacteristicValueChange({
state: true,
deviceId: this.deviceId,
serviceId: '00001111-0000-1000-8000-00805F9B34FB',
characteristicId: '00003333-0000-1000-8000-00805F9B34FB',
success: () => {
console.log('Notify enabled');
wx.onBLECharacteristicValueChange((res) => {
const value = new Uint8Array(res.value);
const decoded = String.fromCharCode.apply(null, value);
console.log('Received from ESP32:', decoded);
this.updateStatus(decoded); // 更新 UI
});
}
});
}
wx.onBLECharacteristicValueChange() 是全局监听器,一旦启用,所有 notify 事件都会触发,因此需在页面 onUnload 中调用 wx.offBLECharacteristicValueChange() 清理,防止内存泄漏。
3.3.2 写入控制指令
写入指令使用 wx.writeBLECharacteristicValue() ,数据需转换为 ArrayBuffer:
sendCommand(cmd) {
const buffer = new ArrayBuffer(1);
const dataView = new DataView(buffer);
dataView.setUint8(0, cmd.charCodeAt(0)); // 单字符
wx.writeBLECharacteristicValue({
deviceId: this.deviceId,
serviceId: '00001111-0000-1000-8000-00805F9B34FB',
characteristicId: '00002222-0000-1000-8000-00805F9B34FB',
value: buffer,
success: () => {
console.log(`Sent command: ${cmd}`);
this.addToLog(`Sent: ${cmd}`);
},
fail: (err) => {
console.error('Write failed:', err);
wx.showToast({ title: '发送失败', icon: 'none' });
}
});
}
对于多字节指令(如 "S80" ),需动态创建 ArrayBuffer 并填充:
const cmdStr = "S80";
const buffer = new ArrayBuffer(cmdStr.length);
const view = new Uint8Array(buffer);
for (let i = 0; i < cmdStr.length; i++) {
view[i] = cmdStr.charCodeAt(i);
}
// ... 传入 writeBLECharacteristicValue
3.4 用户界面与交互优化
小程序 UI 应直观反映设备状态。关键设计点:
- 连接状态指示 :使用
wx.getConnectedBluetoothDevices()定期检查连接状态,避免“假连接”(设备已断电但小程序未感知)。 - 指令按钮防抖 :连续点击同一按钮易导致指令重复发送。在
sendCommand前添加时间戳锁:javascript if (Date.now() - this.lastSendTime < 200) return; this.lastSendTime = Date.now(); - 输入框内容校验 :限制输入长度(如 ≤16 字符),过滤不可见字符(
\r,\n,\0),避免固件解析异常。 - 日志滚动显示 :使用
<scroll-view>显示收发日志,scroll-into-view属性自动滚动到底部。
4. 调试技巧与常见问题排查
4.1 固件端调试要点
- 日志输出重定向 :ESP-IDF 默认通过 UART0 输出日志。若使用 USB-JTAG 调试,可在
menuconfig中启用CONFIG_ESP_CONSOLE_UART_NUM=0并设置CONFIG_ESP_CONSOLE_UART_BAUDRATE=115200。关键日志使用ESP_LOGI(),错误日志用ESP_LOGE(),便于筛选。 - BLE 状态监控 :调用
esp_ble_gap_get_local_used_addr()获取当前广播地址,确认是否为预期的公有地址;用esp_ble_gap_config_adv_data_raw()自定义广播数据,嵌入调试信息(如固件版本号)。 - 内存泄漏检测 :在
app_main()结尾调用heap_caps_print_heap_info(MALLOC_CAP_DEFAULT),对比连接前后堆内存变化,排查未释放的 GATT 句柄。
4.2 小程序端调试技巧
- 真机调试必要性 :微信开发者工具的蓝牙模拟器功能有限,仅能测试 API 调用流程,无法验证真实信号强度、连接稳定性。必须在 iOS/Android 真机上测试。
- iOS 特殊限制 :iOS 对 BLE 扫描有后台限制。若小程序进入后台,
wx.onBluetoothDeviceFound()将停止触发。解决方案是引导用户保持小程序前台,或使用wx.startBeaconDiscovery()(需硬件支持 iBeacon)。 - 安卓权限细化 :Android 12+ 要求
BLUETOOTH_SCAN和BLUETOOTH_CONNECT权限,且需在AndroidManifest.xml中声明android:usesPermissionFlags="neverForLocation",否则扫描失败。
4.3 典型故障与解决方案
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 小程序扫描不到设备 | 广播未启动;广播间隔过长;设备名称不匹配 | 检查 esp_ble_gap_start_advertising() 返回值;用 nRF Connect App 验证设备是否广播;确认 esp_ble_gap_set_device_name() 名称与小程序一致 |
| 连接后无法读取特征值 | 服务未正确注册;GATT 数据库定义错误 | 在 gatts_event_handler() 中打印 ESP_GATTS_CREAT_EVT 和 ESP_GATTS_START_EVT 日志;用 nRF Connect 连接设备,查看服务树结构是否完整 |
| 写入指令无响应 | GATT 写事件未触发;指令解析逻辑错误 | 在 ESP_GATTS_WRITE_EVT 中添加日志,确认事件到达;检查 p_data->handle 是否匹配控制特征值句柄;用 nRF Connect 手动写入测试 |
| Notify 不触发 | CCCD 未启用; wx.notifyBLECharacteristicValueChange() 调用失败 |
检查 ESP_GATTS_WRITE_EVT 中是否收到 CCCD 写入;确认 characteristicId 与服务 UUID 匹配;用 nRF Connect 订阅通知验证 |
5. 性能优化与工程实践建议
5.1 通信效率优化
- MTU 协商 :默认 MTU 为 23 字节,频繁发送小包效率低。在连接建立后,调用
esp_ble_gattc_exchange_mtu()协商更大 MTU(如 247 字节),可减少包数量。小程序端需在wx.createBLEConnection()success 后立即调用wx.setBLEMTU()。 - 批量写入 :若需发送多条指令,可合并为一个长字符串(如
"1S80F"),固件端按分隔符解析,减少 GATT 事务次数。 - Notify 聚合 :避免每毫秒发送一次传感器数据。在固件中设置采样周期(如 100ms),将多个传感器读数打包成一条 notify 消息。
5.2 电源管理策略
ESP32-C3 的 BLE 通信功耗主要来自射频发射。优化措施:
- 广播阶段 :使用 ADV_TYPE_NONCONN_IND (非连接广播)替代 ADV_TYPE_IND ,若设备仅需被发现而不接受连接(如 Beacon 模式),可降低 30% 广播功耗。
- 连接阶段 :连接后,通过 esp_ble_gap_update_conn_params() 动态调整连接间隔( min_int / max_int )。空闲时设为 1s,活跃时降至 20ms,实现功耗与实时性平衡。
- 深度睡眠 :若小车长时间待机,可调用 esp_light_sleep_start() 进入 Light-sleep,RTC 仍工作,可配置定时器唤醒并广播。
5.3 安全性增强(可选)
当前方案为明文通信,适用于开发调试。量产需考虑:
- 配对加密 :调用 esp_ble_gap_set_security_param() 启用 Just Works 或 Numeric Comparison 配对,阻止未授权设备连接。
- 特征值权限 :将控制特征值权限设为 ESP_GATT_PERM_WRITE_ENCRYPTED ,强制要求加密连接。
- 指令签名 :在指令末尾添加 CRC16 校验,固件端验证通过才执行,防干扰误触发。
我在实际项目中曾遇到安卓手机连接后 notify 突然停止的问题,最终定位是 CCCD 写入后未及时调用 esp_ble_gatts_send_response() 返回成功,导致手机端认为配置失败而关闭 notify。因此,所有 ESP_GATTS_WRITE_EVT 处理完毕后,务必调用 esp_ble_gatts_send_response() ,即使只是返回 ESP_GATT_OK 。这个细节在官方文档中容易被忽略,却是稳定性的关键。
更多推荐


所有评论(0)