微信小程序与 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 的时序约束:

  1. 开始搜索 :调用 wx.startBluetoothDevicesDiscovery() ,指定 services 数组过滤目标服务 UUID。
  2. 监听发现事件 :使用 wx.onBluetoothDeviceFound() 监听新设备。该事件可能触发多次,需去重(以 deviceId 为 key)。
  3. 连接设备 :对目标设备( name === "BOE-THINK" )调用 wx.createBLEConnection()
  4. 获取服务与特征值 :连接成功后,依次调用 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 。这个细节在官方文档中容易被忽略,却是稳定性的关键。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐