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;
    }
}
小技巧:在CONNECTED状态里,我习惯用一个独立的定时器来触发心跳,而不是在状态机循环里轮询。这样可以避免状态机被心跳阻塞,影响消息处理的实时性。

OTA升级状态机

OTA升级,是物联网设备绕不开的话题。设备卖出去之后,你不可能一个个去刷固件。OTA升级状态机,就是用来管理这个过程的。

你想想看,升级过程中如果断电了怎么办?下载了一半网络断了怎么办?新固件有问题怎么办?这些都得靠状态机来兜底。

状态定义

状态 说明
IDLE 空闲状态,等待升级指令
CHECKING 检查服务器是否有新版本
DOWNLOADING 正在下载固件包
VERIFYING 校验下载的固件完整性
READY_TO_UPDATE 校验通过,准备更新
UPDATING 正在写入固件到Flash
ROLLBACK 升级失败,回滚到旧版本
ERROR 不可恢复的错误

核心逻辑

OTA升级最怕什么?最怕升级到一半挂了,设备变砖。所以我设计了一个“双备份”机制:固件分A区和B区,当前运行在A区,新固件下载到B区。校验通过后,标记B区为启动区,然后重启。如果重启后起不来,看门狗会触发回滚。

我曾经在一个项目里,因为没做回滚机制,升级后新固件有个bug导致设备不断重启。最后只能派人去现场刷机,那叫一个惨。所以,回滚状态不是可选项,是必选项。

状态转换图

下面我用SVG画了一张OTA升级状态机的流程图,方便你理解整个跳转逻辑。

IDLE CHECKING DOWNLOADING VERIFYING READY_TO_UPDATE UPDATING ROLLBACK ERROR 收到升级指令 有新版本 下载完成 校验通过 开始更新 更新成功,重启 更新失败 回滚完成 无新版本 下载失败 校验失败 图例说明 正常跳转 异常跳转(失败/错误) 正常状态 执行状态 回滚状态 错误状态

代码骨架

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;
    }
}
注意:在UPDATING状态写入Flash时,一定要关中断。我曾经因为没关中断,写入过程中被高优先级任务打断,导致Flash写入时序错乱,整个分区都废了。写入完成后,记得做一次读回校验,确保数据写进去了。

两个状态机的协作

MQTT状态机和OTA状态机不是孤立的。它们之间需要协作。举个例子:

  • OTA升级过程中,MQTT客户端应该保持连接,以便上报升级进度
  • 如果MQTT断线了,OTA下载应该暂停,等重连后再继续
  • 升级完成后,MQTT客户端需要重新订阅Topic,因为新固件可能改了订阅规则

我一般会在系统层设计一个事件总线,让两个状态机通过事件来通信。比如OTA下载完成时,发送一个OTA_DONE事件,MQTT状态机收到后,重新订阅Topic。这样耦合度低,也容易扩展。

核心总结:

  • MQTT客户端状态机:管通信,核心是重连和心跳
  • OTA升级状态机:管固件更新,核心是校验和回滚
  • 两个状态机通过事件总线协作,不要直接调用对方接口

好了,这一章的内容就到这里。状态机在物联网里的应用远不止这些,比如传感器采集、设备配网、远程配置,都可以用状态机来管理。你想想看,你手头的项目里,还有哪些逻辑可以用状态机来重构?


公众号:蓝海资料掘金营,微信deep3321