目录
- 一、前言
- 二、开发板需要做什么
- [三、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 等) */
解析 :
buf和readbuf是给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、循环执行
test1。test1内部若遇到 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 继续带你从原理到实践,掌握嵌入式上位机开发的核心技能,敬请关注~