问题现象
ESP32-S3 BLE 打印机桥接项目中,以下操作流程必然复现:
- 打开蓝牙开关 → 扫描正常,设备列表填充
- 选择设备点击”连接” → 连接超时失败 → 按钮恢复
- 关闭蓝牙再打开 → MAC 列表全空,扫描彻底停止,再无新设备出现
根因分析
一句话:ble_gap_connect() 使用 BLE_HS_FOREVER 无超时连接,软件层 10 秒判定失败后未取消底层连接过程,NimBLE 协议栈被永久占用,后续所有 ble_gap_disc() 扫描请求均被拒绝。
详细时序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| ① 点击"连接" → ble_task 调用 ble_connect_device() → ble_gap_connect(..., BLE_HS_FOREVER, ...) → NimBLE 开始无限时长的连接尝试
② 10 秒软件超时 → ble_task 里 connected = false → 按钮恢复,但扫描未重启 → ble_scan_enabled 仍为 false
③ 关闭蓝牙 → ble_stop_scan() 发现 ble_scan_enabled 已是 false,直接空返回 → 未向 NimBLE 发任何取消命令 → 底层连接尝试仍在继续 → ble_clear_device_list() 清空 UI 列表
④ 再次打开蓝牙 → ble_start_scan() → ble_gap_disc() → NimBLE 返回错误(BLE_HS_EBUSY) → 扫描启动失败,设备列表永远为空
|
三个关联 Bug
| # |
位置 |
问题 |
| 1 |
ble_task() 连接超时分支 |
未调用 ble_start_scan() 重启扫描 |
| 2 |
全局 |
无任何地方调用 ble_gap_conn_cancel(),pending 连接泄漏 |
| 3 |
BLE_GAP_EVENT_CONNECT 失败事件 |
无恢复逻辑,纯打日志 |
修复方案
1. ble_start_scan() 增加防御性清理
1 2 3 4 5 6 7 8 9 10 11
| void ble_start_scan(void) { ble_gap_conn_cancel(); vTaskDelay(pdMS_TO_TICKS(50));
if (ble_scan_enabled) { } }
|
2. 连接失败事件中恢复扫描
1 2 3 4 5 6 7 8 9 10
| case BLE_GAP_EVENT_CONNECT: if (event->connect.status == 0) { } else { ESP_LOGE(TAG, "连接打印机失败: %d", event->connect.status); if (sys_status.bluetooth_open) { ble_start_scan(); } } break;
|
3. ble_task() 连接超时后取消并重启扫描
1 2 3 4 5 6 7 8 9 10 11
| if (connected) { } else { ble_reset_screen_button(); ble_gap_conn_cancel(); vTaskDelay(pdMS_TO_TICKS(100)); if (sys_status.bluetooth_open) { ble_start_scan(); } ESP_LOGI(TAG, "连接超时,已取消并恢复扫描"); }
|
4. 关闭蓝牙时彻底清理
1 2 3 4 5 6 7 8 9 10
| } else { ble_gap_conn_cancel(); vTaskDelay(pdMS_TO_TICKS(50)); ble_disconnect(); ble_stop_scan(); vTaskDelay(pdMS_TO_TICKS(100)); ble_clear_device_list(); ble_page_set_device_name(NULL, NULL); }
|
5. GAP 断连事件中直接重置 UI
1 2 3 4 5 6
| case BLE_GAP_EVENT_DISCONNECT: printer_conn_handle = BLE_HS_CONN_HANDLE_NONE; printer_ready = false; printer_attr_handle = 0; ble_reset_screen_button(); break;
|
代码重构:移除 bluetooth_connecting 冗余标志
bluetooth_connecting 本质上是 ble_is_connected() && printer_ready 的手动副本,完全冗余。涉及 5 个文件的清理:
| 文件 |
改动 |
data.h |
删除 bool bluetooth_connecting 字段 |
hello_world_main.c |
删除初始化 |
printer_scan.c |
删除所有赋值/判断;心跳改为 printer_ready && ble_is_connected();断连检测移到 GAP 事件 |
tjc_uart2.c |
按钮切换改为 ble_is_connected() |
key.c |
导航限制改为 ble_is_connected() |
遇到的编译问题
- 花括号不配对 — 替换
if(connected) 块时新代码缺少关闭 if(ble_connect_flag) 外层的 }
- 隐式声明 —
ble_reset_screen_button() 在 GAP 事件中被提前调用,需加前置声明;ble_gap_conn_cancel() 需 #include "host/ble_gap.h"
- 缓冲区溢出 —
sample_mode_name[20] 装不下编码损坏后的中文字符串,改为 [32]
关键认知
ble_gap_disc_cancel() 只能取消扫描,取消不了 ble_gap_connect() 发起的连接过程,必须用 ble_gap_conn_cancel()
ble_scan_enabled 是软件维护的布尔量,与 NimBLE 协议栈真实状态不同步,不能作为唯一判断依据
- 串口屏双态按钮的
value=1/0 是按下/弹起信号而非连接/断开语义,每次点击依次发 1 和 0,意图判断必须基于 ble_is_connected() 真实状态
架构要点
1
| 串口屏按钮 → UART2 解析 → 设置标志位 → ble_task 50ms 轮询 → 执行 BLE 操作
|
ble_connect_flag / ble_disconnect_flag 是 UART 线程到 BLE 线程的消息队列
- NimBLE GAP 操作不能跨线程直接调用,必须集中在
ble_task 一个线程
- 决策信源唯一性原则:依据 MCU 的
ble_is_connected()(物理真相),不依赖屏幕维护的状态副本