问题现象

ESP32-S3 BLE 打印机桥接项目中,以下操作流程必然复现:

  1. 打开蓝牙开关 → 扫描正常,设备列表填充
  2. 选择设备点击”连接” → 连接超时失败 → 按钮恢复
  3. 关闭蓝牙再打开 → 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)
{
/* 防御性清理:取消可能残留的 connect 过程 */
ble_gap_conn_cancel();
vTaskDelay(pdMS_TO_TICKS(50));

if (ble_scan_enabled) {
// ... 原有 guard 逻辑
}
// ...
}

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(); // ★ 关键:释放 NimBLE GAP 资源
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 {
/* 取消 pending 连接 + 断开已建立的连接 */
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(); // ★ 替代 ble_task 的轮询检测
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()

遇到的编译问题

  1. 花括号不配对 — 替换 if(connected) 块时新代码缺少关闭 if(ble_connect_flag) 外层的 }
  2. 隐式声明ble_reset_screen_button() 在 GAP 事件中被提前调用,需加前置声明;ble_gap_conn_cancel()#include "host/ble_gap.h"
  3. 缓冲区溢出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()(物理真相),不依赖屏幕维护的状态副本