3. 架构设计:自动重连模块的总体架构与状态机设计
好,咱们进入正题。上一章我们把断连的根因和场景摸透了,这一章就要开始搭架子了。说白了,自动重连模块到底长什么样?它的核心骨架是什么?
我个人习惯,在设计任何嵌入式模块之前,先画状态机。为什么?因为状态机能把所有可能的行为路径都锁死,不会出现「哎呀这个情况我没考虑到」的尴尬。我在项目中遇到过好几次,代码写到一半发现少了一个状态,结果整个重连逻辑要推倒重来。嗯,那滋味可不好受。
3.1 模块总体架构
先看整体。自动重连模块不是独立存在的,它寄生在 GATT 客户端(Central)的协议栈之上。我把它拆成三个层次:
- 策略层(Policy Layer):决定「要不要重连」以及「怎么重连」。比如重连次数上限、退避算法参数、是否允许在特定 RSSI 下重连。
- 状态机层(State Machine Layer):管理连接生命周期。从空闲到扫描,从扫描到发起连接,从连接到断连,每一步都有明确的状态转移。
- 执行层(Execution Layer):调用底层协议栈 API。比如启动扫描、停止扫描、发起连接、取消连接、注册回调。这一层只干活,不做决策。
核心原则:策略层和状态机层绝对不能直接调用协议栈 API。中间必须隔一层执行层。这样换协议栈(比如从 BlueZ 换到 NimBLE)时,你只需要重写执行层,上层逻辑纹丝不动。
你想想看,如果策略层里直接写了 hci_le_create_conn(),哪天换了芯片厂商,你不得把所有代码翻一遍?我吃过这个亏,后来就学乖了。
3.2 状态机设计
好,重点来了。自动重连状态机,我把它设计成 6 个状态。不多不少,刚好覆盖所有场景。
| 状态 | 含义 | 触发条件 | 典型停留时间 |
|---|---|---|---|
| IDLE | 空闲态。模块未启动或已停止 | 系统初始化 / 用户停止重连 | 无限 |
| WAITING | 等待态。断连后等待退避时间 | 收到断连事件 | 1s ~ 60s(动态) |
| SCANNING | 扫描态。搜索目标设备广播 | 退避计时器到期 | 5s ~ 30s(可配置) |
| CONNECTING | 连接态。正在发起连接请求 | 扫描到目标设备 | 2s ~ 10s(超时) |
| CONNECTED | 已连接态。正常通信中 | 连接成功 | 无限(直到断连) |
| SUSPENDED | 挂起态。重连次数耗尽或用户暂停 | 达到最大重连次数 / 用户指令 | 可配置(如 5 分钟后再试) |
这 6 个状态,我画了无数遍才定下来。一开始我只有 4 个状态,结果发现断连后立刻扫描太粗暴,容易把电池耗光。后来加了 WAITING 态,用退避算法控制节奏,效果立竿见影。
我的经验:WAITING 态的退避时间不要用固定值。我一般用指数退避 + 随机抖动。比如第一次等 1 秒,第二次等 2 秒,第三次等 4 秒……最大不超过 60 秒。每次加一个 ±20% 的随机值,防止多个设备同时重连造成冲突。
3.3 状态转移图
光说不够直观,我画了一张状态机图。你仔细看,箭头上的条件就是触发转移的事件。
3.4 关键设计决策
状态机画出来了,但有几个细节我得单独拎出来说。这些是我在实际项目中踩过的坑。
3.4.1 为什么断连后不直接扫描?
你可能会想:断连了,赶紧扫描重连啊,等什么?
我曾经也这么干过。结果呢?设备在信号不稳定的区域(比如电梯口),频繁断连、频繁扫描,电池半天就耗光了。更糟糕的是,扫描本身会占用射频资源,影响同板上其他 BLE 设备的通信。
所以,WAITING 态是必须的。它像一个缓冲器,让系统喘口气。退避时间可以根据断连原因动态调整:
- 连接超时断连:退避 1 秒(可能只是瞬时干扰)
- 链路层断连:退避 5 秒(可能是设备移动)
- GATT 服务变更断连:退避 30 秒(需要重新发现服务)
3.4.2 扫描超时 vs 连接超时
这两个超时容易搞混。我简单区分一下:
| 超时类型 | 含义 | 典型值 | 超时后动作 |
|---|---|---|---|
| 扫描超时 | 在 SCANNING 态停留太久,没找到目标设备 | 10s | 回到 WAITING,下次退避时间加倍 |
| 连接超时 | 在 CONNECTING 态发起连接后,对方没响应 | 5s | 回到 WAITING,下次退避时间不变 |
为什么连接超时后不退避加倍?因为连接超时往往意味着设备不在范围内,加倍退避只会让重连周期拉得太长。我一般保持退避时间不变,尽快再次尝试。
注意:连接超时时间不要设得太短。BLE 连接请求需要经过 3 个空口包交互(CONNECT_REQ、LL_CONNECTION_PARAM_REQ、LL_CONNECTION_PARAM_RSP),再加上射频干扰,2 秒以内很容易误判。我建议至少设 5 秒。
3.5 状态机实现骨架
最后,给一个 C 语言的状态机骨架。这不是完整代码,但核心逻辑都在了。
typedef enum {
STATE_IDLE,
STATE_WAITING,
STATE_SCANNING,
STATE_CONNECTING,
STATE_CONNECTED,
STATE_SUSPENDED
} reconn_state_t;
typedef struct {
reconn_state_t state;
uint8_t retry_count;
uint16_t wait_time_ms; // 当前退避时间
uint16_t scan_timeout_ms;
uint16_t conn_timeout_ms;
// 回调函数指针
void (*on_scan_start)(void);
void (*on_scan_stop)(void);
void (*on_connect_req)(bd_addr_t *addr);
void (*on_disconnect)(void);
} reconn_module_t;
void reconn_event_handler(reconn_module_t *mod, reconn_event_t event) {
switch (mod->state) {
case STATE_WAITING:
if (event == EVENT_TIMEOUT) {
// 退避到期,进入扫描
mod->state = STATE_SCANNING;
mod->on_scan_start();
} else if (event == EVENT_RETRY_EXCEEDED) {
// 重连次数耗尽,进入挂起
mod->state = STATE_SUSPENDED;
// 启动挂起定时器(比如 5 分钟)
}
break;
case STATE_SCANNING:
if (event == EVENT_DEVICE_FOUND) {
mod->state = STATE_CONNECTING;
mod->on_scan_stop();
mod->on_connect_req(&found_addr);
} else if (event == EVENT_SCAN_TIMEOUT) {
mod->state = STATE_WAITING;
mod->wait_time_ms *= 2; // 退避加倍
if (mod->wait_time_ms > 60000) mod->wait_time_ms = 60000;
// 启动退避定时器
}
break;
case STATE_CONNECTING:
if (event == EVENT_CONNECTED) {
mod->state = STATE_CONNECTED;
mod->retry_count = 0; // 重置重连计数
} else if (event == EVENT_CONN_TIMEOUT) {
mod->state = STATE_WAITING;
// 退避时间不变,尽快重试
}
break;
case STATE_CONNECTED:
if (event == EVENT_DISCONNECTED) {
mod->state = STATE_WAITING;
mod->retry_count++;
if (mod->retry_count >= MAX_RETRY) {
// 触发重连次数耗尽事件
reconn_event_handler(mod, EVENT_RETRY_EXCEEDED);
} else {
// 启动退避定时器
}
}
break;
// 其他状态类似处理...
}
}
这个骨架我用了好几年,换过三款蓝牙芯片,上层逻辑基本没动。你拿去用的时候,只需要把 on_scan_start、on_connect_req 这些回调换成你的协议栈 API 就行。
小技巧:状态机的 event 不要用简单的枚举值。我习惯把 event 定义成 16 位整数,高 8 位表示事件类别(定时器事件、协议栈事件、用户事件),低 8 位表示具体事件。这样调试时看日志,一眼就能知道事件来源。
嗯,架构和状态机就讲到这里。下一块内容,我们会深入每个状态的实现细节,包括退避算法怎么算、扫描参数怎么配、连接参数怎么协商。但那是后面的事了。先把状态机跑通,你就成功了一半。