蓝牙上位机的状态机设计:连接只是第一步
第一版上位机只有“扫描”和“连接”两个按钮。设备少、网络好时一切正常;一旦用户连续点击、系统蓝牙关闭或固件升级中途断开,界面就出现按钮仍可点击、进度条不动、后台线程继续运行的组合故障。问题不在某个回调,而在应用从未定义自己处于什么状态。
用状态描述事实
我把生命周期拆成:Idle、Scanning、Connecting、Handshaking、Ready、Updating、Recovering 和 Error。所有外部事件先进入单一事件队列,再由状态机决定动作。界面只渲染当前状态,不直接改变连接对象。
Idle -> Scanning -> Connecting -> Handshaking -> Ready
| | |
+----------> Error <------+
|
Recovering
“已连接”不是 Ready。物理链路建立后还要读取设备标识、协议版本、能力位和时钟,只有握手成功才开放业务操作。这个区分解决了许多刚连接就发送命令导致的竞态。
每个异步动作都有身份
扫描和连接会返回操作编号。回调到达时先核对编号与当前状态,旧操作的迟到结果直接丢弃。取消操作不是简单隐藏对话框,而是发出取消令牌、等待资源释放,然后进入目标状态。
超时也作为事件处理。连接超时只负责终止这一次尝试,不把任何异常都转换成“设备离线”。错误对象包含阶段、底层码、是否可重试和面向用户的下一步,因此日志与界面不再各说各话。
重连不是无限循环
Recovering 使用有上限的指数退避:一秒、两秒、四秒,达到次数后停在可解释的 Error。用户主动断开会设置意图标志,不触发自动重连。固件升级期间断线则保存进度和设备身份,重新握手后先查询设备端状态,再决定续传或重新开始。
列表里的设备也有生命周期。扫描结果按稳定身份合并,RSSI 做平滑显示,长时间未出现的设备标记为离线而不是瞬间消失,避免用户追着跳动的列表点击。
界面派生规则
Idle:允许开始扫描,隐藏设备操作Scanning:允许停止,禁用重复扫描Connecting:显示目标和取消入口Handshaking:展示“正在确认设备信息”,不伪装成可用Ready:根据能力位显示功能,而非按型号硬编码Updating:锁定冲突操作,保留日志和安全退出说明Error:给出原因、建议和可执行的重试动作
回归清单
- [x] 在每个状态关闭系统蓝牙并观察收敛结果
- [x] 连续点击扫描、连接、取消不会产生两个会话
- [x] 迟到回调不能覆盖新的连接
- [x] 应用退出时所有任务和句柄均被释放
- [x] 升级断线后根据设备真实状态恢复
- [x] 日志能从一次会话编号串起完整时间线
状态机最大的收益不是代码“更优雅”,而是产品行为可以被列举和测试。用户看到的每个按钮都对应一个明确事实,后台每个任务都有开始、结束和取消路径。连接只是链路事件;让设备在复杂环境中始终给出一致反馈,才是上位机真正的工作。
评论区