物联网实战--入门篇之(七)嵌入式-MQTT

目录

一、MQTT简介

二、MQTT使用方法

三、MQTT驱动设计

四、代码解析

五、使用过程

六、总结


一、MQTT简介

        MQTT因为其轻量、高效和稳定的特点,特别适合作为物联网系统的数据传输协议,已经成为物联网事实上的通信标准了。关于协议的具体内容看看这篇文章和官方文档MQTT协议详解(完整版)-CSDN博客,在这里我们主要讲解使用方法。

        作为嵌入式设备,设备资源比较紧张,我们这里选用开源库paho mqtt,开源地址在这儿GitHub - eclipse/paho.mqtt.embedded-c: Paho MQTT C client library for embedded systems. Paho is an Eclipse IoT project (https://iot.eclipse.org/)

        我们项目里已经都整理好了,直接用就行了,具体如下图所示,从映射文件可以看出,mqtt开源库大概占用2KB的 ROM,已经很轻量化了。这个开源库的核心作用就是可以帮我们根据协议要求组合要发送的数据,或者拆解接收到的数据,而应用层不用去太关心协议本身的内容。

二、MQTT使用方法

        MQTT是以服务器为中心,客户端对为对象,话题为关系纽带的一种通讯协议,在这个体系里,净化器设备是客户端,用户手机也是客户端,手机订阅净化器发布的话题,服务器就会把净化器发布的消息推送给手机;同样的道理,手机根据设备订阅的话题来发布消息,就可以对净化器设备进行控制了。

        下图是净化器项目的话题,其中11223344是设备的序列号,对于所有净化器的数据手机都能收的到,手机针对某个净化器的数据也只有某个净化器能接收,其它序列号的设备收不到。这里面的核心逻辑都是服务器根据话题来区分运行的。

三、MQTT驱动设计

        MQTT的驱动应该算是比较难的,首先要确定它的地位和作用,如下图所示,drv_mqtt是作为设备端mqtt的核心,整合了底层的开源库、物理层的收发接口和应用层的参数配置功能,以及自身的连接、收发、订阅/取消订阅等功能。

        下面进入代码进行解析,从头文件开始,MQTTPacket.h主要包含了mqtt开源库的功能文件,这个应该没什么问题,下面的ringbuffer.h需要强调下,它是RT-Thread的功能,叫环形缓冲区,就是数据按顺序环形保存,取出的时候按照先进先出的原则,MQTT开源库需要按顺序取出数据解析,有这个ringbuffer作为缓存媒介在操作上非常便捷,这也是使用RT-Thread的另一个重要原因了。

     接下来是宏定义的内容,没什么特殊情况默认即可,有需要改变的在user_opt.h中重定义即可,具体的内容都有注释,就不赘述了。

   

        订阅话题是个重要组成部分,在这里定义了话题的三个状态,空闲、订阅和取消订阅,取消订阅一般用不到,特殊情况下会有一些临时话题,为了缓解资源,可以取消订阅。结构体里的base_msg_id主要是为了标记 订阅/取消订阅 时返回的话题,这样程序才能区分。

        

        最后是最重要的客户端连接信息了,具体都有注释,其中用户名、密码和客户端ID都是指针,在应用层定义这些信息需要用全局变量或者静态变量,才能保证信息的完整性;同样的,收发函数也是采用回调的方式,在应用程根据不同的物理接口进行注册,这里我们采用的自然是esp8266的收发函数了。

四、代码解析

        先从初始化开始,主要就是对用户名、密码和客户端ID进行赋值。

/*		
================================================================================
描述 : 初始化指定MQTT连接
输入 : 
输出 : 
================================================================================
*/
void drv_mqtt_init(u8 index, char *usr_name, char *passwd, char *client_id)
{if(index<MQTT_CONN_NUM){MqttClientStruct *pClient=&g_sMqttWork.client_list[index];MQTTPacket_connectData connect_init = MQTTPacket_connectData_initializer;if((pClient->rb=rt_ringbuffer_create(MQTT_RING_BUFF_SIZE))!=NULL )   {memcpy(&pClient->condata, &connect_init, sizeof(connect_init));//复制连接初始化信息pClient->condata.keepAliveInterval=MQTT_KEEP_TIME;			pClient->condata.username.cstring=usr_name;//用户名pClient->condata.password.cstring=passwd;//密码pClient->condata.clientID.cstring=client_id;//客户ID      pClient->is_enable=true;}      }    
}

        接下来就是连接和订阅了,在这里就可以很清晰的看到mqtt开源库的作用了,就是组合连接、订阅和取消订阅的报文。MQTT里也有保活功能,这是协议层的,如果指定时间内没有没有收到数据,那么会自己发个ping请求包来保持连接。


/*		
================================================================================
描述 : 连接和订阅
输入 : 
输出 : 
================================================================================
*/
void drv_mqtt_connect(void)
{static u32 last_sec_time=0;static u8 make_buff[80]={0};const int make_size=sizeof(make_buff);int make_len;	u32 now_sec_time=drv_get_sec_counter();if(now_sec_time-last_sec_time>=2){static u8 conn_ptr=0;if(conn_ptr>=MQTT_CONN_NUM)conn_ptr=0;MqttClientStruct *pClient=&g_sMqttWork.client_list[conn_ptr];if(pClient->is_enable){if(pClient->is_connected==false){memset(make_buff, 0, make_size);make_len=MQTTSerialize_connect(make_buff, make_size, &pClient->condata);//组合连接请求包		if(pClient->mqtt_send!=NULL){
//          printf("client=%d, mqtt send connect!  make_len=%d\n",conn_ptr, make_len); pClient->mqtt_send(make_buff, make_len);//发送}		          }else{//订阅话题for(u8 i=0; i<MQTT_SUB_NUM; i++){SubPackStruct *pSub=&pClient->sub_list[i];if(strlen(pSub->sub_topic)>0 && pSub->curr_state!=pSub->dst_state){if(pSub->dst_state==TopicStateSub)//需要订阅{MQTTString topicString = MQTTString_initializer;int req_qos=1;					topicString.cstring=pSub->sub_topic;memset(make_buff, 0, make_size);make_len = MQTTSerialize_subscribe(make_buff, make_size, 0, pSub->base_msg_id, 1, &topicString, &req_qos);//组合订阅报文if(pClient->mqtt_send!=NULL){printf("sub topic=%s\n", pSub->sub_topic);pClient->mqtt_send(make_buff, make_len);//发送}		              }else if(pSub->dst_state==TopicStateUnSub)//需要取消订阅{MQTTString topicString = MQTTString_initializer;			topicString.cstring=pSub->sub_topic;memset(make_buff, 0, make_size);make_len = MQTTSerialize_unsubscribe(make_buff, make_size, 0, pSub->base_msg_id, 1, &topicString);//组合取消订阅报文if(pClient->mqtt_send!=NULL){printf("unsub topic=%s\n", pSub->sub_topic);pClient->mqtt_send(make_buff, make_len);//发送}		              }break;//每次只订阅一个,避免堵塞}}//超时检测u32 det_time=now_sec_time-pClient->keep_time;if(det_time>=MQTT_KEEP_TIME){printf("mqtt sock_id=%d timeout, close!\n", conn_ptr);drv_mqtt_close(pClient);//超时关闭	          }else if(det_time>=MQTT_KEEP_TIME-10){//发送ping请求,保活memset(make_buff, 0, make_size);make_len=MQTTSerialize_pingreq(make_buff, make_size);//组合ping包		if(pClient->mqtt_send!=NULL){
//            printf("sock=%d, mqtt send ping req! make_len=%d\n",conn_ptr,make_len); pClient->mqtt_send(make_buff, make_len);//发送}		          }          }}conn_ptr++;last_sec_time=drv_get_sec_counter();}  
}  

        接收部分的逻辑是MQTTPacket_read函数调用回调函数pClient->mqtt_recv获取环形缓冲区内的数据并按照协议解析,最后根据解析结果执行相应动作,消息类型如下图所示,常用的是连接回复、收到发布数据、订阅回复、取消订阅回复、ping回复和断开连接。


/*		
================================================================================
描述 : 接收检查
输入 : 
输出 : 
================================================================================
*/
void drv_mqtt_recv_check(void)
{static u8 make_buff[MQTT_SUB_BUFF_SIZE];const int make_size=sizeof(make_buff);int rc;u8 dup;int qos;u8 retained;u16 msgid;int payloadlen_in;u8 *payload_in;	MQTTString receivedTopic;	for(u8 i=0; i<MQTT_CONN_NUM; i++){MqttClientStruct *pClient=&g_sMqttWork.client_list[i];if(pClient->is_enable==true)//启用{rc=MQTTPacket_read(make_buff, make_size, pClient->mqtt_recv);switch(rc){case CONNACK://连接回复{printf("mqtt_id=%d CONNACK!\n", i);u8 sessionPresent, connack_rc;if (MQTTDeserialize_connack(&sessionPresent, &connack_rc, make_buff, make_size) != 1 || connack_rc != 0)//解析收到的回复报文{drv_mqtt_close(pClient);printf("mqtt sock_id=%d  Unable to connect, return code %d\n",i, connack_rc); }else{pClient->is_connected=true;pClient->keep_time=drv_get_sec_counter();//更新时间printf("mqtt sock_id=%d connect ok!\n", i);}						break;}		case PUBREC:case PUBACK: //发布回复{
//					debug("sock_id=%d PUBACK!\n", i);break;}			case PUBLISH://收到发布的消息{pClient->keep_time=drv_get_sec_counter();//更新时间printf("sock_id=%d PUBLISH!\n", i);rc = MQTTDeserialize_publish(&dup, &qos, &retained, &msgid, &receivedTopic, &payload_in, &payloadlen_in, make_buff, make_size);	char *pTopic=receivedTopic.lenstring.data;if(g_sMqttWork.mqtt_recv_parse!=NULL){char topic[30]={0};int len=(char*)payload_in-pTopic;//topic 长度if(len>sizeof(topic)){len=sizeof(topic)-1;}memcpy(topic, pTopic, len);g_sMqttWork.mqtt_recv_parse(i, topic, payload_in, payloadlen_in);//应用层数据解析}break;}	case SUBACK://订阅回复{
//					debug("sock_id=%d SUBACK!\n", i);
//					printf_hex("sub buff=", make_buff, 30);int count, requestedQoSs[1];MQTTDeserialize_suback(&msgid, 1, &count, requestedQoSs, make_buff, make_size);
//					debug("$$$ msgid=0x%04X\n", msgid);for(u8 k=0; k<MQTT_SUB_NUM; k++){SubPackStruct *pSub=&pClient->sub_list[k];if(pSub->base_msg_id==msgid){printf("topic=%s sub ok!\n", pSub->sub_topic);pSub->curr_state=TopicStateSub;
//              pSub->subed_time=drv_get_sec_counter();}}break;}	              case UNSUBACK://取消订阅回复{
//					debug("sock_id=%d UNSUBACK!\n", i);MQTTDeserialize_unsuback(&msgid, make_buff, make_size);
//					debug("$$$ msgid=0x%04X\n", msgid); for(u8 k=0; k<MQTT_SUB_NUM; k++){SubPackStruct *pSub=&pClient->sub_list[k];if(pSub->base_msg_id==msgid){printf("topic=%s unsub ok!\n", pSub->sub_topic);pSub->curr_state=TopicStateUnSub;
//              pSub->subed_time=drv_get_sec_counter();}}          break;}case PINGRESP://ping回复{pClient->keep_time=drv_get_sec_counter();//更新时间
//					debug("sock_id=%d PINGRESP!\n", i);break;}	case DISCONNECT://断开连接{printf("mqtt_id=%d DISCONNECT!\n", i);drv_mqtt_close(pClient);					break;}					}}	}
}

        剩下的就是一些简单的功能了,比如设置话题、发布消息,关闭连接等等,较为简单。

/*		
================================================================================
描述 : 设置话题信息
输入 : 
输出 :  
================================================================================
*/
void drv_mqtt_set_topic_info(u8 client_id, u8 sub_id, char *topic, u32 base_msg_id, u8 dst_state)
{if(client_id<MQTT_CONN_NUM) { MqttClientStruct *pClient=&g_sMqttWork.client_list[client_id];if(sub_id<MQTT_SUB_NUM){SubPackStruct *pSub=&pClient->sub_list[sub_id];if(strlen(topic)<sizeof(pSub->sub_topic)){pSub->curr_state=TopicStateIdel;  pSub->dst_state=dst_state;pSub->base_msg_id=base_msg_id;strcpy(pSub->sub_topic, topic);        }}}
}/*		
================================================================================
描述 : 设置话题订阅状态
输入 : 
输出 :  
================================================================================
*/
void drv_mqtt_set_topic_state(u8 client_id, u8 sub_id, u8 dst_state)
{if(client_id<MQTT_CONN_NUM) { MqttClientStruct *pClient=&g_sMqttWork.client_list[client_id];if(sub_id<MQTT_SUB_NUM){SubPackStruct *pSub=&pClient->sub_list[sub_id];pSub->dst_state=dst_state;}}  
}
/*		
================================================================================
描述 : MQTT发布数据
输入 : 
输出 : 
================================================================================
*/
void drv_mqtt_publish(u8 index, u8 *msg_buff, u16 msg_len, char *topic)
{static u8 make_buff[MQTT_PUB_BUFF_SIZE]={0};static const int make_size=sizeof(make_buff);	u16 make_len=0;	  if(index<MQTT_CONN_NUM){MqttClientStruct *pClient=&g_sMqttWork.client_list[index];if(pClient->is_connected==true)//已经连接{ pClient->msg_id++;MQTTString topicString = MQTTString_initializer;topicString.cstring=topic; make_len = MQTTSerialize_publish(make_buff, make_size, 0, 1,0, pClient->msg_id, topicString, msg_buff, msg_len);//组合发布报文if(pClient->mqtt_send!=NULL && make_len>0){int ret=pClient->mqtt_send(make_buff, make_len);//发送}			}		    }
}
/*		
================================================================================
描述 : 关闭连接
输入 : 
输出 : 
================================================================================
*/
void drv_mqtt_close(MqttClientStruct *pClient)
{pClient->is_connected=false;for(u8 i=0; i<MQTT_SUB_NUM; i++){SubPackStruct *pSub=&pClient->sub_list[i];pSub->curr_state=TopicStateIdel;
//    pSub->subed_time=0;}pClient->msg_id=0;pClient->keep_time=0;
}

五、使用过程

        应用层的使用主要就是根据要求配置信息,首先物理通讯接口先设置,这里使用esp8266的连接3作为网络链路,同时注册接收函数把数据缓存进ringbuffer;然后就是MQTT用户名、密码、客户端ID的设置了;接下来有三个回调函数注册,两个是物理层的MQTT收发,还有一个是应用层的数据解析,这里已经来到了最后的净化器项目本身了,由此可以看出,要想代码好维护,写代码之前就要分层设计,这样出问题了才好分级排查,再后期自己阅读时逻辑也更走得通;最后一步就是话题订阅了,这样才能收到用户的控制数据,每个设备订阅话题都不一样,最后都带上了自己序列号,这样用户端才能针对性控制设备。

        下面代码是净化器应用层的数据解析。


/*		
================================================================================
描述 : 设备解析服务器下发的数据
输入 : 
输出 : 
================================================================================
*/
void app_air_recv_parse(u8 *buff, u16 len)
{u8 head[2]={0xAA, 0x55};u8 *pData=memstr(buff, len, head, 2);if(pData!=NULL){u16 total_len=pData[2]<<8 | pData[3];u16 crcValue=pData[total_len]<<8 | pData[total_len+1];if(crcValue==drv_crc16(pData, total_len)){pData+=4;u32 device_sn=pData[0]<<24|pData[1]<<16|pData[2]<<8|pData[3];pData+=4;if(device_sn!=g_sAirWork.device_sn)//识别码确认return;u8 cmd_type=pData[0];pData++;switch(cmd_type){case AIR_CMD_HEART://心跳包{break;}case AIR_CMD_DATA://数据包{break;}case AIR_CMD_SET_SPEED://设置风速{u8 speed=pData[0];pData+=1;app_motor_set_speed(speed);break;}        case AIR_CMD_SET_SWITCH://设置开关{u8 state=pData[0];pData+=1;g_sAirWork.switch_state=state;if(state>0){app_motor_set_speed(100);//启动风扇}else{app_motor_set_speed(0);//停止风扇}app_air_send_status();break;}}}}
}
六、总结

        MQTT协议本身较为繁琐,现在应用阶段暂时不用太深入,先学会使用就行,用熟了再去查阅文档,这样理解起来更透彻。mqtt的驱动设计相较于其他驱动文件更为复杂,因为它所牵涉的内容更广,有开源库、网络链路、应用层参数配置等等,完整的工程在第二篇文章里有的下载,自行查阅。

本项目的交流QQ群:701889554

   写于2024-4-1

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/296708.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

校园局域网钓鱼实例

Hello &#xff01; 我是"我是小恒不会java" 本文仅作为针对普通同学眼中的网络安全&#xff0c;设计的钓鱼案例也是怎么简陋怎么来 注&#xff1a;本文不会外传代码&#xff0c;后端已停止使用&#xff0c;仅作为学习使用 基本原理 内网主机扫描DNS劫持前端模拟后端…

图论基础(python蓝桥杯)

图的基本概念 图的种类 怎么存放图呢&#xff1f; 优化 DFS 不是最快/最好的路&#xff0c;但是能找到一条连通的道路。&#xff08;判断两点之间是不是连通的&#xff09; 蓝桥3891 import os import sys sys.setrecursionlimit(100000) # 请在此输入您的代码 n, m map(int,…

【Frida】【Android】 07_爬虫之网络通信库HttpURLConnection

&#x1f6eb; 系列文章导航 【Frida】【Android】01_手把手教你环境搭建 https://blog.csdn.net/kinghzking/article/details/136986950【Frida】【Android】02_JAVA层HOOK https://blog.csdn.net/kinghzking/article/details/137008446【Frida】【Android】03_RPC https://bl…

SQL server 查询数据库中所有的表名及行数

SQL server 查询数据库中所有的表名及行数 select a.name,b.rows from sysobjects as ainner join sysindexes as bon a.id b.id where (a.type u)and (b.indid in (0, 1)) and b.rows<50 and b.rows>20 order by a.name, b.rows desc;

图像处理_积分图

目录 1. 积分图算法介绍 2. 基本原理 2.1 构建积分图 2.2 使用积分图 3. 举个例子 1. 积分图算法介绍 积分图算法是图像处理中的经典算法之一&#xff0c;由Crow在1984年首次提出&#xff0c;它是为了在多尺度透视投影中提高渲染速度。 积分图算法是一种快速计算图像区域和…

Ceph分布式存储系统以及高可用原理

Ceph分布式存储系统以及高可用原理 1. Ceph原理和架构1.1 分布式存储系统抽象1.2 Ceph基本组件 2 Ceph中的策略层2.1 CRUSH进行数据分发和定位2.2 PG(Placement Group): 集群管理的基本单元2.3 PG的代理primary OSD2.4 轻量级的集群元数据ClusterMap2.5 对PG的罗辑分组&#xf…

观察和配置MAC地址表

目录 原理概述 实验目的 实验内容 实验拓扑 ​编辑1&#xff0e;基本配置 2.观察正常状态时的MAC地址表 4.配置静态MAC地址表项 原理概述 MAC 地址表是交换机的一个核心组成部分&#xff0c;交换机主要是根据 MAC 地址表来进行帧的转发的。交换机对帧的转发操作行为一共有…

车道线检测_Canny算子边缘检测_1

Canny算子边缘检测&#xff08;原理&#xff09; Canny算子边缘检测是一种经典的图像处理算法&#xff0c;由John F. Canny于1986年提出&#xff0c;用于精确、可靠地检测数字图像中的边缘特征。该算法设计时考虑了三个关键目标&#xff1a;低错误率&#xff08;即尽可能多地检…

【漏洞复现】WordPress Plugin LearnDash LMS 敏感信息暴漏

漏洞描述 WordPress和WordPress plugin都是WordPress基金会的产品。WordPress是一套使用PHP语言开发的博客平台。该平台支持在PHP和MySQL的服务器上架设个人博客网站。WordPress plugin是一个应用插件。 WordPress Plugin LearnDash LMS 4.10.2及之前版本存在安全漏洞&#x…

从汇编看函数调用

文章目录 函数调用流程栈相关寄存器及的作用简介寄存器功能指令功能 栈函数的括号{}正括号反括号 参数传递传值&#xff0c;变量不可改传指针&#xff0c;变量可改C 传引用 函数调用实例 函数调用流程 目标&#xff1a;函数调用前后栈保持不变 保存main函数的寄存器上下文移…

使用MySQL和PHP创建一个公告板

目录 一、创建表 二、制作首页&#xff08;创建主题以及显示列表&#xff09; 三、制作各个主题的页面&#xff08;输入回帖和显示列表&#xff09; 四、制作消息的查询界面 五、制作读取数据库信息的原始文件 六、制作数据重置页面 七、效果图 八、问题 1、目前无法处…

商务电子邮件: 在WorkPlace中高效且安全

高效和安全的沟通是任何组织成功的核心。在我们关于电子邮件类型的系列文章的第二期中&#xff0c;我们将重点关注商业电子邮件在促进无缝交互中的关键作用。当你身处重要的工作场环境时&#xff0c;本系列的每篇文章都提供了电子邮件的不同维度的视角。 “2024年&#xff0c;全…

基于springboot实现房屋租赁管理系统项目【项目源码+论文说明】计算机毕业设计

基于springboot实现房屋租赁系统演示 摘要 房屋是人类生活栖息的重要场所&#xff0c;随着城市中的流动人口的增多&#xff0c;人们对房屋租赁需求越来越高&#xff0c;为满足用户查询房屋、预约看房、房屋租赁的需求&#xff0c;特开发了本基于Spring Boot的房屋租赁系统。 …

保健品wordpress外贸模板

保健品wordpress外贸模板 健康保养保健品wordpress外贸模板&#xff0c;做大健康行业的企业官方网站模板。 https://www.jianzhanpress.com/?p3514

防火墙状态检测和会话机制

FW对TCP&#xff0c;UDP和ICMP协议的报文创建会话

【如何解决一些常见的 Composer 错误的保姆级讲解】

&#x1f308;个人主页:程序员不想敲代码啊&#x1f308; &#x1f3c6;CSDN优质创作者&#xff0c;CSDN实力新星&#xff0c;CSDN博客专家&#x1f3c6; &#x1f44d;点赞⭐评论⭐收藏 &#x1f91d;希望本文对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提…

如何使用免费的ChatGpt3.5

如何使用免费的ChatGpt 最近免费的gpt3.5很多都不怎么行了实在是太给力了尾声 最近免费的gpt3.5很多都不怎么行了 原因是什么呢&#xff1f;因为openai已经取消了免费的5刀赠送&#xff0c;那么这些人手上的免费的sses-key 用完后&#xff0c;就基本上全军覆没了&#xff0c;再…

在 Amazon Timestream 上通过时序数据机器学习进行预测分析

由于不断变化的需求和现代化基础设施的动态性质&#xff0c;为大型应用程序规划容量可能会非常困难。例如&#xff0c;传统的反应式方法依赖于某些 DevOps 指标&#xff08;如 CPU 和内存&#xff09;的静态阈值&#xff0c;而这些指标在这样的环境中并不足以解决问题。在这篇文…

vscode + wsl1 搭建远程C/C++开发环境

记录第一次搭建环境过程。 搭建C/C开发环境有很多种方式&#xff0c;如 MinGW vscode&#xff08;MinGW 是GCC的Windows版本&#xff0c;本地编译环境&#xff09;SSH隧道连接 vscode&#xff08;远程Linux主机&#xff09;wsl vscode&#xff08;远程Linux环境&#xff09…

flink1.18源码本地调试环境

01 源码本地调试环境搭建 1. 从github拉取源码创建本地项⽬ https://github.com/apache/flink.git 可以拉取github上官⽅代码 https://github.com/apache/flink.git GitHub - apache/flink: Apache Flink 2. 配置编译环境 ctrlaltshifts &#xff08;或菜单&#xff09;打…