21、状态机在物联网中的应用:MQTT客户端状态机、OTA升级状态机
物联网开发,说白了就是和设备打交道。设备不像手机,你不能指望用户天天盯着它。它得自己知道什么时候该连网,什么时候该断线,什么时候该升级。这些逻辑,用状态机来管理最合适不过了。
我个人习惯,在物联网项目里,至少会设计两个核心状态机:一个是MQTT客户端状态机,管通信;一个是OTA升级状态机,管固件更新。今天我们就来聊聊这两个东西。
MQTT客户端状态机
MQTT是物联网里最常用的协议之一。但设备联网不是一锤子买卖,网络会断,服务器会重启,设备会休眠。MQTT客户端的状态机,就是用来处理这些“意外”的。
我见过不少新手,直接把MQTT的连接、发布、订阅写在一个大循环里。结果呢?网络一抖,整个系统就卡死了。嗯,这里要注意,MQTT客户端必须是有状态的。
状态定义
一个典型的MQTT客户端状态机,包含以下几个状态:
| 状态 | 说明 |
|---|---|
| INIT | 初始化,尚未配置网络参数 |
| DISCONNECTED | 已配置,但未连接或已断开 |
| CONNECTING | 正在尝试建立TCP和MQTT连接 |
| CONNECTED | 已连接,可以收发消息 |
| RECONNECTING | 连接丢失,正在重连 |
| SLEEP | 低功耗模式,定时唤醒检查 |
状态转换逻辑
状态之间的跳转,不是随便来的。我总结了几条核心规则:
- INIT → DISCONNECTED:配置完服务器地址和端口后自动跳转
- DISCONNECTED → CONNECTING:主动发起连接请求
- CONNECTING → CONNECTED:MQTT CONNACK收到成功响应
- CONNECTING → DISCONNECTED:连接超时或失败
- CONNECTED → RECONNECTING:心跳超时或TCP断开
- RECONNECTING → CONNECTING:等待退避时间后重试
- CONNECTED → SLEEP:进入低功耗模式
- SLEEP → DISCONNECTED:唤醒后准备重新连接
这里有个坑,我曾经在RECONNECTING状态里直接做重连,没有加退避时间。结果服务器一重启,几十台设备同时疯狂重连,直接把服务器打挂了。后来我加了一个指数退避,每次重连间隔翻倍,最大不超过5分钟。这才稳下来。
代码骨架
下面是一个简化版的MQTT状态机实现。注意,我只展示了核心逻辑,实际项目中还要考虑内存管理和错误处理。
typedef enum {
MQTT_STATE_INIT,
MQTT_STATE_DISCONNECTED,
MQTT_STATE_CONNECTING,
MQTT_STATE_CONNECTED,
MQTT_STATE_RECONNECTING,
MQTT_STATE_SLEEP
} mqtt_state_t;
typedef struct {
mqtt_state_t state;
uint32_t reconnect_interval; // 重连间隔,单位秒
uint32_t heartbeat_count; // 心跳计数
} mqtt_client_t;
void mqtt_state_machine(mqtt_client_t *client) {
switch (client->state) {
case MQTT_STATE_INIT:
// 配置网络参数
mqtt_config();
client->state = MQTT_STATE_DISCONNECTED;
break;
case MQTT_STATE_DISCONNECTED:
// 检查是否需要连接
if (mqtt_should_connect()) {
mqtt_start_connect();
client->state = MQTT_STATE_CONNECTING;
}
break;
case MQTT_STATE_CONNECTING:
if (mqtt_is_connected()) {
client->reconnect_interval = 1; // 重置退避时间
client->state = MQTT_STATE_CONNECTED;
} else if (mqtt_timeout()) {
client->state = MQTT_STATE_DISCONNECTED;
}
break;
case MQTT_STATE_CONNECTED:
// 处理心跳
if (mqtt_heartbeat_timeout()) {
mqtt_send_pingreq();
client->heartbeat_count++;
if (client->heartbeat_count > 3) {
// 连续心跳失败,进入重连
client->state = MQTT_STATE_RECONNECTING;
}
}
// 处理正常消息收发
mqtt_process_messages();
break;
case MQTT_STATE_RECONNECTING:
// 指数退避
mqtt_delay(client->reconnect_interval);
client->reconnect_interval *= 2;
if (client->reconnect_interval > 300) {
client->reconnect_interval = 300;
}
client->state = MQTT_STATE_CONNECTING;
break;
case MQTT_STATE_SLEEP:
// 低功耗模式,定时唤醒
mqtt_enter_sleep();
break;
}
}
OTA升级状态机
OTA升级,是物联网设备绕不开的话题。设备卖出去之后,你不可能一个个去刷固件。OTA升级状态机,就是用来管理这个过程的。
你想想看,升级过程中如果断电了怎么办?下载了一半网络断了怎么办?新固件有问题怎么办?这些都得靠状态机来兜底。
状态定义
| 状态 | 说明 |
|---|---|
| IDLE | 空闲状态,等待升级指令 |
| CHECKING | 检查服务器是否有新版本 |
| DOWNLOADING | 正在下载固件包 |
| VERIFYING | 校验下载的固件完整性 |
| READY_TO_UPDATE | 校验通过,准备更新 |
| UPDATING | 正在写入固件到Flash |
| ROLLBACK | 升级失败,回滚到旧版本 |
| ERROR | 不可恢复的错误 |
核心逻辑
OTA升级最怕什么?最怕升级到一半挂了,设备变砖。所以我设计了一个“双备份”机制:固件分A区和B区,当前运行在A区,新固件下载到B区。校验通过后,标记B区为启动区,然后重启。如果重启后起不来,看门狗会触发回滚。
我曾经在一个项目里,因为没做回滚机制,升级后新固件有个bug导致设备不断重启。最后只能派人去现场刷机,那叫一个惨。所以,回滚状态不是可选项,是必选项。
状态转换图
下面我用SVG画了一张OTA升级状态机的流程图,方便你理解整个跳转逻辑。
代码骨架
typedef enum {
OTA_STATE_IDLE,
OTA_STATE_CHECKING,
OTA_STATE_DOWNLOADING,
OTA_STATE_VERIFYING,
OTA_STATE_READY_TO_UPDATE,
OTA_STATE_UPDATING,
OTA_STATE_ROLLBACK,
OTA_STATE_ERROR
} ota_state_t;
typedef struct {
ota_state_t state;
uint32_t download_progress; // 下载进度 0-100
uint32_t retry_count; // 重试次数
uint8_t firmware_buffer[1024]; // 简化示例,实际用Flash分区
} ota_manager_t;
void ota_state_machine(ota_manager_t *ota) {
switch (ota->state) {
case OTA_STATE_IDLE:
if (ota_check_update_command()) {
ota->state = OTA_STATE_CHECKING;
}
break;
case OTA_STATE_CHECKING:
if (ota_query_server_version()) {
ota->state = OTA_STATE_DOWNLOADING;
} else {
// 无新版本,回到空闲
ota->state = OTA_STATE_IDLE;
}
break;
case OTA_STATE_DOWNLOADING:
if (ota_download_firmware()) {
ota->state = OTA_STATE_VERIFYING;
} else if (ota->retry_count < 3) {
ota->retry_count++;
// 继续下载,不改变状态
} else {
ota->state = OTA_STATE_ERROR;
}
break;
case OTA_STATE_VERIFYING:
if (ota_verify_checksum()) {
ota->state = OTA_STATE_READY_TO_UPDATE;
} else {
ota->state = OTA_STATE_ERROR;
}
break;
case OTA_STATE_READY_TO_UPDATE:
// 等待用户确认或自动触发
if (ota_should_update()) {
ota->state = OTA_STATE_UPDATING;
}
break;
case OTA_STATE_UPDATING:
if (ota_write_to_flash()) {
// 写入成功,重启设备
system_reset();
} else {
ota->state = OTA_STATE_ROLLBACK;
}
break;
case OTA_STATE_ROLLBACK:
ota_restore_previous_version();
ota->state = OTA_STATE_IDLE;
break;
case OTA_STATE_ERROR:
// 记录错误日志,等待人工干预
ota_log_error();
break;
}
}
两个状态机的协作
MQTT状态机和OTA状态机不是孤立的。它们之间需要协作。举个例子:
- OTA升级过程中,MQTT客户端应该保持连接,以便上报升级进度
- 如果MQTT断线了,OTA下载应该暂停,等重连后再继续
- 升级完成后,MQTT客户端需要重新订阅Topic,因为新固件可能改了订阅规则
我一般会在系统层设计一个事件总线,让两个状态机通过事件来通信。比如OTA下载完成时,发送一个OTA_DONE事件,MQTT状态机收到后,重新订阅Topic。这样耦合度低,也容易扩展。
核心总结:
- MQTT客户端状态机:管通信,核心是重连和心跳
- OTA升级状态机:管固件更新,核心是校验和回滚
- 两个状态机通过事件总线协作,不要直接调用对方接口
好了,这一章的内容就到这里。状态机在物联网里的应用远不止这些,比如传感器采集、设备配网、远程配置,都可以用状态机来管理。你想想看,你手头的项目里,还有哪些逻辑可以用状态机来重构?
公众号:蓝海资料掘金营,微信deep3321