项目背景

一个基于 ESP32-S3 的空气质量采样设备,搭载 TJC 串口屏作为人机交互界面。蓝牙模块承担双重角色:

  • BLE 外设(Peripheral):通过 Nordic UART Service 与手机 App 通信
  • BLE 中心设备(Central):连接蓝牙热敏打印机,打印检测记录

蓝牙模块的核心文件为 printer_scan.c(约 1000 行)和 printer_scan.h


问题发现:标志位混乱

项目运行中出现以下异常现象:

  1. 蓝牙关闭时仍能触发连接 —— 串口屏的 connecting 按钮在蓝牙关闭状态下仍可被物理按键触发
  2. 连接状态显示不同步 —— 屏幕显示”已连接”但实际连接尚未建立
  3. MAC 设备列表间歇性空 —— 开启蓝牙后扫描正常,但设备列表不显示任何设备
  4. 标志位多源写入 —— 同一个连接状态标志被 UI 处理函数和 BLE 任务同时写入

原始标志位全景

1
2
3
4
5
6
7
8
"蓝牙开关" → sys_status.bluetooth_open ──┐
ble_switch_enabled ─────────┤ (3个变量表达同一概念)
ble_enabled (僵尸声明) ──────┘ (未定义!)

"蓝牙连接" → sys_status.bluetooth_connecting ──┐
ble_page_connected ───────────────┤ (4个变量)
printer_ready ────────────────────┘
ble_connect_flag / ble_disconnect_flag

总计 7 个标志变量分布在 3 个文件中,存在严重的职责重叠和多源写入问题。


问题 1:sys_status.bluetooth_connecting 被两处争抢写入

根因

tjc_uart2.c 中 UI 按键处理直接写入了本应只反映实际连接状态的标志:

1
2
3
4
5
6
7
8
9
10
11
12
13
// tjc_uart2.c — UI 按键处理
key = "connecting";
if (strcmp(New_key, key) == 0) {
sys_status.bluetooth_connecting = value; // ❌ UI 直写状态标志
if (value == 1) {
if (ble_page_connected) {
ble_disconnect_flag = true;
ble_page_connected = false; // ❌ 提前清零
} else if (sys_status.bluetooth_open == 1) {
ble_connect_flag = true;
}
}
}

同时 ble_task 也在写同一个标志:

1
2
3
4
5
6
7
// printer_scan.c — ble_task
if (connected) {
ble_page_connected = true;
sys_status.bluetooth_connecting = true; // ble_task 也写
} else {
sys_status.bluetooth_connecting = false; // 争抢写入
}

4 个执行上下文同时读写同一组 bool 标志,且零互斥保护:

上下文 载体
GAP 事件回调 NimBLE Host Task
ble_task 独立 FreeRTOS Task
触摸屏处理 tjc_uart2(主 Task)
物理按键 key.c(主 Task)

修复

原则:每个标志只能有一个写入方。

1
2
3
4
5
6
7
8
9
// tjc_uart2.c — 修改后:只设动作标志,不越权写状态
key = "connecting";
if (strcmp(New_key, key) == 0 && value == 1) {
if (sys_status.bluetooth_connecting) {
ble_disconnect_flag = true;
} else if (sys_status.bluetooth_open == 1) {
ble_connect_flag = true;
}
}

sys_status.bluetooth_connecting 仅由 ble_task 根据实际连接结果写入


问题 2:标志位冗余——ble_page_connected 合并

ble_page_connectedsys_status.bluetooth_connecting 的生命周期完全一致:

1
2
3
4
5
6
7
// 同时设 true
sys_status.bluetooth_connecting = true;
ble_page_connected = true;

// 同时设 false
sys_status.bluetooth_connecting = false;
ble_page_connected = false;

全局替换 ble_page_connectedsys_status.bluetooth_connecting,删除冗余标志。


问题 3:ble_scan_enabled 卡死导致列表为空

根因

1
2
3
4
5
6
7
8
9
10
void ble_stop_scan(void) {
if (!ble_scan_enabled) return;
int rc = ble_gap_disc_cancel();
if (rc != 0 && rc != BLE_HS_EALREADY) {
ESP_LOGW(TAG, "stop scan fail: %d", rc);
// ❌ ble_scan_enabled 没有被清零!
} else {
ble_scan_enabled = false;
}
}

ble_gap_disc_cancel() 返回非预期的错误码时,ble_scan_enabled 保持 true。下次开启蓝牙时,ble_start_scan() 检测到 true 直接返回,扫描根本不启动。

修复

1
2
3
4
5
6
7
8
void ble_stop_scan(void) {
if (!ble_scan_enabled) return;
int rc = ble_gap_disc_cancel();
if (rc != 0 && rc != BLE_HS_EALREADY) {
ESP_LOGW(TAG, "stop scan fail: %d", rc);
}
ble_scan_enabled = false; // 无条件清零
}

问题 4:扫描设备列表填充过慢

根因

1
disc_params.filter_duplicates = 1;  // 过滤重复广播包

同一设备在扫描窗口内只上报一次,新设备需要等待下一个广播周期才能出现。

修复

1
disc_params.filter_duplicates = 0;  // 不滤重

ble_update_device_list() 内部已有去重逻辑(memcmp 比较 MAC 地址),不会导致重复条目。


精简:删除 12 个废弃符号

printer_scan.h 删除

声明 原因
extern bool ble_enabled 僵尸声明,从未定义
void printer_scan_deinit(void) 从未调用
void ble_clear_selected_device(void) 从未外部调用
const char* ble_get_device_display_name(...) 从未调用
ble_set_scan_enabled / ble_get_scan_enabled 从未调用
void ble_set_current_mac_index(...) 从未调用
const char* ble_get_current_mac_str(void) 从未调用
bool ble_get_switch_state(void) 从未调用

printer_scan.c 删除

符号 原因
ble_switch_enabled sys_status.bluetooth_open 完全冗余
selected_device_addr[6] 只写不读
has_selected_device 只写不读
对应 8 个废弃函数的实现 从未调用

.h 从 165 行缩减到 75 行,.c 从 28 个函数缩减到 27 个。


连接失败的屏幕回传

蓝牙异步连接失败后,需要恢复串口屏上 connecting 按钮的视觉状态。

方案

ble_task 中新增 ble_reset_screen_button(),向屏幕发送 click connecting,1/0 指令:

1
2
3
4
5
6
7
static void ble_reset_screen_button(void) {
char cmd[32];
snprintf(cmd, sizeof(cmd), "click connecting,1");
send_to_screen(cmd);
snprintf(cmd, sizeof(cmd), "click connecting,0");
send_to_screen(cmd);
}

在三处调用:连接失败、主动断开、被动断线。

设计要点:用 click 而非直接写 connecting.val=0,因为 click 会触发按钮的 press/release 动画和图片切换。由于按钮当前处于 val=1 状态,click 翻转后输出 connecting=0 给 ESP32,而 ESP32 端只对 value==1 做处理,因此 echo 天然安全。


物理按键防御

key.c 中增加:蓝牙关闭时,OK 键只允许触发 index 0(bluetooth_open 按钮),不能触发 connecting 按钮:

1
2
3
4
5
case bluetooth:
if (sys_status.bluetooth_open == 0 && focus_ctrl.focus_index != 0) {
break; // 拦截
}
// ... 正常 click

配合屏幕端脚本 tsw connecting,0 禁用触摸,形成三层防御。


最终架构

标志位写权限

1
2
3
4
串口屏按钮 ──→ sys_status.bluetooth_open       (开关, 仅一处写入)
串口屏按钮 ──→ ble_connect_flag (连接触发)
串口屏按钮 ──→ ble_disconnect_flag (断开触发)
ble_task ────→ sys_status.bluetooth_connecting (连接状态, 仅一处写入)

模块划分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
打印机模块
├── printer_send_data() 向打印机发送数据 (MTU 分片)
├── printer_print_query_record() 组装 ESC/POS 指令

手机 UART 模块 (ESP32 作为外设)
├── ble_uart_advertise() BLE 广播
├── ble_uart_gap_event() 手机 GAP 事件
├── gatt_svr_chr_access() GATT 特征读写
└── send_uart_notification() 推送通知到手机

打印机客户端模块 (ESP32 作为中心设备)
├── ble_start_scan() / ble_stop_scan() 扫描控制
├── printer_gap_event() 打印机 GAP 事件
├── printer_discover_service() GATT 服务发现
├── ble_connect_device() 发起连接
├── ble_disconnect() 断开连接
└── ble_is_connected() 连接状态查询

设备列表
├── ble_update_device_list() 添加设备 (去重 + RSSI 过滤)
├── ble_get_scanned_device() 按索引获取
├── ble_next_mac() / ble_prev_mac() 导航

任务与初始化
├── ble_task() 50ms 轮询: 连接/断开/心跳/断线检测
├── ble_reset_screen_button() 连接失败回传屏幕
├── ble_on_sync() BLE 栈就绪回调
└── printer_scan_init() 总初始化

开关与 UI
├── ble_set_switch_state() 设置蓝牙开关
└── ble_page_set_device_name() 更新屏幕设备名

总结

指标 修改前 修改后
标志位数量 7 个 4 个
多源写入标志 2 个 0 个
.h 外部声明 20 个 12 个
.c 函数 28 个 27 个
红线问题 4 个 0 个

核心经验:

  1. 单写原则 —— 每个标志位只有一个写入方,从源头消灭竞态
  2. 串口屏只管发指令 —— 状态由 BLE 任务根据实际结果维护,UI 不越权
  3. 异步操作用标志位桥接 —— ble_task 和 UI 线程通过 ble_connect_flag 解耦
  4. 无条件清理 —— ble_stop_scan()ble_scan_enabled 不管取消失败都清零