嵌入式上位机开发入门(二十六):将 MQTT 测试程序加入 APP 任务

目录

  • 一、前言
  • 二、开发板需要做什么
  • [三、MQTT 测试函数 test1](#三、MQTT 测试函数 test1)
    • [3.1 变量声明与初始化](#3.1 变量声明与初始化)
    • [3.2 连接 TCP/MQTT Broker](#3.2 连接 TCP/MQTT Broker)
    • [3.3 订阅消息](#3.3 订阅消息)
    • [3.4 循环发布消息](#3.4 循环发布消息)
  • [四、消息接收回调 messageArrived](#四、消息接收回调 messageArrived)
  • [五、APP 任务:MQTTClientTask](#五、APP 任务:MQTTClientTask)
  • 六、总结
  • 七、结尾

一、前言

大家好,这里是 Hello_Embed。上篇完成了 Paho MQTT 的 Socket 层适配------DNS 解析、读写函数替换、接收超时改造,已经能让 W800 跑通 TCP+MQTT 的底层通信。

本篇在此基础上,仿照 Paho MQTT 官方 test1 的写法,把 MQTT 收发逻辑封装成一个完整的 APP 任务,正式加入工程。


二、开发板需要做什么

开发板作为 MQTT 客户端,完整的启动流程如下:

步骤 操作 说明
1 连接 WiFi 通过 W800 的 AT 指令接入热点
2 TCP 连接 Broker 建立与 EMQX Broker 的 TCP 连接(IP + Port)
3 MQTT 连接 Broker 发送 CONNECT 报文,完成协议层握手
4 订阅主题 发送 SUBSCRIBE 报文,注册消息回调
5 循环发布消息 定时发布数据,并调用 MQTTYield 处理收到的消息

解析:步骤 1-2 属于传输层,步骤 3-5 属于协议层。MQTT 任务只关心协议层的收发逻辑,不需要关心 Modbus 等其他总线的操作------不同功能通过 FreeRTOS 任务隔离,互不干扰。


三、MQTT 测试函数 test1

整个 MQTT 收发逻辑封装在 test1 函数中,仿照 Paho MQTT 源码 test/ 目录下的用例写法。

3.1 变量声明

c 复制代码
/* Broker 地址(PC 本机 IP,通过 ipconfig 获取;MQTTX 查看端口默认为 1883) */
#define PC_MQTT_BROKER_IP   "192.168.1.29"
#define PC_MQTT_BROKER_PORT 1883

static void test1(void)
{
    int subsqos = 2;                    /* 订阅 QoS 等级 */
    Network n;                          /* 网络层结构体(封装 socket 及读写函数指针) */
    MQTTClient c;                       /* MQTT 客户端实例 */
    int rc = 0;                         /* 返回值,0=SUCCESS */

    char *sub_topic = "/topic/humiture"; /* 订阅的主题(接收温湿度数据) */
    char *pub_topic = "/topic/temp";     /* 发布的主题 */

    MQTTPacket_willOptions wopts;        /* 遗愿消息选项(连接异常断开时自动发布) */

    unsigned char buf[100];             /* MQTT 协议层发送缓冲区 */
    unsigned char readbuf[100];         /* MQTT 协议层接收缓冲区 */
    char pubbuf[100];                   /* 发布消息内容缓冲区 */
    int cnt = 0;                        /* 发布消息计数器 */
    int wait_seconds;                   /* Yield 等待轮次 */

    MQTTMessage pubmsg;                 /* 发布的消息结构体(payload、QoS、retained 等) */

解析bufreadbuf 是给 MQTTClientInit 使用的协议层缓冲区,与业务层的 pubbuf 分开,避免混淆。wopts 这里声明了但由后面的 data.will 字段直接配置,代码与上篇源码保持一致。


3.2 连接 TCP/MQTT Broker

c 复制代码
    /* 第一步:初始化 Network 结构体,绑定 W800 socket 的读写函数指针 */
    NetworkInit(&n);

    /* 第二步:建立 TCP 连接,失败则不断重试(对应屏幕提示 "Re-Connect TCP/Port ...") */
    while (0 != NetworkConnect(&n, PC_MQTT_BROKER_IP, PC_MQTT_BROKER_PORT))
    {
        Draw_String(0, 64, "Re-Connect TCP/Port ...", 0xff0000, 0);
        vTaskDelay(100); /* 延时 100ms 再重试,避免频繁占用 AT 通道 */
    }

    /* 第三步:初始化 MQTT 客户端,绑定 Network 层,设置命令超时 1000ms */
    MQTTClientInit(&c, &n, 1000, buf, 100, readbuf, 100);

    /* 第四步:填写 CONNECT 报文参数 */
    MQTTPacket_connectData data = MQTTPacket_connectData_initializer;

    data.willFlag            = 1;                       /* 使能遗愿消息 */
    data.clientID.cstring    = "hello_embed_mqtt_test"; /* 客户端 ID,Broker 用于区分连接 */
    data.username.cstring    = "testuser";              /* Broker 鉴权用户名 */
    data.password.cstring    = "testpassword";          /* Broker 鉴权密码 */
    data.keepAliveInterval   = 20;                      /* 心跳间隔 20s,超时无响应则断开 */
    data.cleansession        = 1;                       /* 每次连接清除历史会话 */

    /* 遗愿消息配置:连接异常断开时,Broker 自动向 "will topic" 发布 "will message" */
    data.will.message.cstring   = "will message";
    data.will.qos               = 1;
    data.will.retained          = 0;
    data.will.topicName.cstring = "will topic";

    /* 第五步:发送 CONNECT 报文,失败则不断重试 */
    Draw_String(0, 80, "Connect MQTT Broker ...", 0xff0000, 0);
    while (SUCCESS != MQTTConnect(&c, &data))
    {
        Draw_String(0, 96, "Re-Connect MQTT Broker ...", 0xff0000, 0);
    }

解析 :TCP 连接和 MQTT 连接分两步,这与协议分层一致------NetworkConnect 负责传输层,MQTTConnect 负责应用协议层。两处都加了重试循环,保证弱网环境下最终能连上,不会因一次失败直接卡死。


3.3 订阅消息

c 复制代码
    /* 订阅主题,注册消息回调 messageArrived */
    Draw_String(0, 96, "MQTTSubscribe ...", 0xff0000, 0);
    rc = MQTTSubscribe(&c, sub_topic, subsqos, messageArrived);

解析MQTTSubscribe 发送 SUBSCRIBE 报文并等待 SUBACK。第三个参数是本地请求的 QoS ,Broker 在 SUBACK 中可能降级(比如 Broker 不支持 QoS2 就返回 QoS1);第四个参数 messageArrived 是消息到达时的回调,由 MQTTYield 内部的 deliverMessage 触发。


3.4 循环发布消息

c 复制代码
    while (1)
    {
        /* 清空消息结构体,防止上次数据残留 */
        memset(&pubmsg, '\0', sizeof(pubmsg));

        /* 构造发布内容,格式:"msg from H5, <cnt>" */
        sprintf(pubbuf, "msg from H5, %d", cnt++);

        pubmsg.payload    = pubbuf;           /* 消息内容指针 */
        pubmsg.payloadlen = strlen(pubbuf);   /* 消息长度(字节) */
        pubmsg.qos        = 0;                /* QoS 0:最多发一次,不确认 */
        pubmsg.retained   = 0;                /* 不保留:新订阅者不会收到这条历史消息 */
        pubmsg.dup        = 0;                /* dup=0:首次发送,非重传 */

        /* 在屏幕上显示即将发布的消息内容 */
        Draw_String(0, 112, pubbuf, 0xff0000, 0);

        /* 发布消息到 pub_topic */
        rc = MQTTPublish(&c, pub_topic, &pubmsg);

        /* Yield 循环:每次等待 100ms,共循环 10 次(总计约 1s)
         * MQTTYield 内部会处理接收到的消息,触发 messageArrived 回调
         * 同时维护 keepalive 心跳,防止 Broker 认为客户端超时断开 */
        wait_seconds = 10;
        while (wait_seconds-- > 0)
        {
            MQTTYield(&c, 100);
        }
    }
}

解析 :这里每发布一条消息就 Yield 约 1 秒,并非"发完就走"。原因是 Paho MQTT Embedded-C 是单线程设计------发布和接收共用同一个 socket,必须调用 MQTTYield 才能处理收到的消息(包括 PUBACK、SUBACK 和业务消息),同时也在这段时间内维护 keepalive 心跳包。


四、消息接收回调 messageArrived

c 复制代码
static void messageArrived(MessageData* md)
{
    static int cnt = 0;             /* 记录收到消息的次数(静态局部变量,不随函数返回清零) */
    MQTTMessage* m = md->message;   /* 取出消息结构体指针 */
    char buf[100];

    /* 将收到的消息内容格式化为 "get msg <cnt>: <payload>" */
    snprintf(buf, 100, "get msg %d: %s", cnt++, (char *)m->payload);
    buf[99] = '\0'; /* 强制加结束符,防止 snprintf 截断后无 '\0' */

    /* 在屏幕第 200 行显示接收到的消息 */
    Draw_String(0, 200, buf, 0xff0000, 0);
}

解析messageArrived 是由 MQTTYield 内部调用的回调,上层不直接调用它。static int cnt 用静态局部变量保存计数,每次进入函数时不会重置,相当于一个轻量的接收计数器。buf[99] = '\0' 是防御性写法------snprintf 在截断时会保证末尾有 \0,但这里出于安全再加一道保障。


五、APP 任务:MQTTClientTask

test1 只是业务逻辑函数,还需要一个 FreeRTOS 任务来承载它,并在 test1 异常退出后自动重启:

c 复制代码
void MQTTClientTask(void *pvParameters)
{
    int err;

    /* 根据启动模式在屏幕左上角显示标识 */
    if (isBootloader())
        Draw_String(150, 0, "Bootloader",   0xff0000, 0);
    else
        Draw_String(150, 0, "Application", 0xff0000, 0);

    /* 初始化 AT 串口(连接 W800 模组的 UART) */
    at_init("uart1");

    /* 等待 WiFi 连接成功;失败则每隔 1s 重试 */
    while (1)
    {
        err = at_connect_ap("Programmers", "Hello_Embed"); /* SSID, Password */
        if (!err)
            break; /* 连接成功,退出重试循环 */
        else
            vTaskDelay(1000); /* 延时 1s 后重试 */
    }

    Draw_String(0, 48, "Connect TCP/Port ...", 0xff0000, 0);

    /* 外层循环:test1 因连接断开返回后,重新执行完整流程 */
    while (1)
    {
        test1(); /* TCP连接→MQTT连接→订阅→循环发布 */
    }

    vTaskDelete(NULL); /* 理论上不会执行到这里 */
}

解析 :整个任务只做三件事------初始化 AT 串口、连接 WiFi、循环执行 test1test1 内部若遇到 TCP/MQTT 断开会一直重试,MQTTClientTask 的外层 while(1) 则保证即使 test1 意外退出,整个流程也能重新来过,无需人工干预。


六、总结

知识点 要点
NetworkConnect 重试逻辑 TCP 连接失败时循环重试,保证弱网环境可用
MQTTClientInit 绑定 Network 层,分配协议层缓冲区,设置命令超时
MQTTConnect + 遗愿消息 keepalive=20s;willFlag=1,连接异常断开时 Broker 自动发布遗愿
MQTTSubscribe + 回调 订阅时注册 messageArrived,由 MQTTYield 内部触发
MQTTPublish + Yield 每次发布后 Yield 约 1s,处理 ACK 和业务消息,维护心跳
MQTTClientTask 外层循环 保证 test1 意外退出后自动重启完整流程

七、结尾

本篇完成了 MQTT APP 任务的搭建:仿照 Paho MQTT 官方 test 格式,把 WiFi 连接、TCP/MQTT 建连、订阅、循环发布整合成一个可自动重连的 FreeRTOS 任务,并对各关键步骤加了详细注释。

下一篇将进行 MQTT 上机测试,敬请期待~

Hello_Embed 继续带你从原理到实践,掌握嵌入式上位机开发的核心技能,敬请关注~

相关推荐
2023自学中2 小时前
i.MX6ULL 板子的完整启动流程图(从上电 → 用户空间)
linux·嵌入式
不会编程的懒洋洋2 小时前
C# Task async/await CancellationToken
笔记·c#·线程·面向对象·task·同步异步
亚空间仓鼠3 小时前
网络学习实例:网络理论知识
网络·学习·智能路由器
上海合宙LuatOS3 小时前
LuatOS扩展库API——【libfota2】远程升级
网络·物联网·junit·luatos
pengyi8710153 小时前
动态IP池快速更换实操方案,5分钟完成IP替换
服务器·网络·tcp/ip
平升电子DATA864 小时前
地下管网(污水/雨水管网)流量怎么监测?
网络
被摘下的星星4 小时前
以太网技术
服务器·网络
24zhgjx-lxq5 小时前
OSPF的网络类型:NBMA和P2MP
网络·智能路由器·hcip·ensp·ospf
zhangrelay5 小时前
蓝桥云课五分钟-通关自动控制-octave
笔记·学习