有机会做这次项目很开心,学到了很多东西,但刚开始的时候是真的从零开始,网上找的资料都大相径庭,很多代码都有问题,而且把别人做的模块一个个移植实在是很麻烦,为了不让后来需要做类似这个物联网项目的大家走歪路,我尽可能的把我这次做项目的核心事务说清楚,个人能力有限,有错的地方麻烦轻喷和指教,谢谢各位。
正题首先先把要做什么搞清楚,顾名思义,本次项目内容即:
①把MQTT协议移植到stm32上;
②通过W5500以太网芯片,用网线连接至本地路由;
③由stm32本地网口连接到阿里云
使用到的工具:Keil5、pahoMqtt包、stm32f103RCT6、W5500、网线*1
调试工具:485通信usart口串口通信、串口调试助手、通信猫、阿里云、MQTTfx、MQTTBOX(当然如果不使用串口,单步Debug也是可以,我当时就这样,搞的我快崩溃了,最后还是弄了串口方便很多,也很简单)
学习资料:这边安利和推广一下B站上一个叫橙子的up主,他做的视频挺好的,很适合萌新上手,视频看看能明白很多东西;
友情链接:stm32 mqtt移植
如果有违权的地方麻烦友情提醒一下,方便我删除。
进入正题,MQTT协议,硬件部分不多说。
首先是下载Mqtt包,可以上github上下载,很多其他mqtt移植的博文都有链接,不过这里也贴一下;
友情链接:PahoMqtt-Github下载
下载后解压,把\paho.mqtt.embedded-c-master\MQTTPacket\src下的所有源文件导入到你的工程项目中,然后导入src所有的头文件路径,最后再把/sample下的transport.c、.h也导入;
这样首先第一步移植就成功了,很多人说transport.c里面有很多函数是Linux下编译的,所以用不了,那必然是用不了的2333,我们要做的事情就是重写transport.c和transport.h文件里的函数。主要有
transport_sendPacketBuffer //发送数据包到服务器
transport_getdata //从服务器获取数据
transport_open // 创建socket,绑定、连接等
transport_close // 关闭socket
这四个函数;
后续要Connect、Pub、Sub、Disconnect,都是基于这四个函数,以及一些MQTT_Package里头自带的一些不需要你写的函数;
最最重要的是搞清楚MQTT协议的工作方式,才能把功能实现,无论你是用一些常用的比如SIM800C、8266、Lora、W5500之类的通信途径,都是大同小异的;
工作方式:具体的几个名词不细讲。本地作为客户端连接到服务器,服务器作为Broker,你可以向Broker订阅话题,也可以向Broker发布话题,同样Broker作为服务器也可以对你Pub和Sub,宏观来看,是对等的;
首先你要有服务器的域名或者IP,只有域名就得做DNS解析,有IP直接IP连,端口号视你要连的服务器规定,阿里云就是固定的1883,通信猫18831之类的,在连接阿里云之前,我强烈建议先把连接通信猫作为实验调通了作为练手;
连接时,有以下几个参数:
data.clientID.cstring // 客户端ID
data.keepAliveInterval //心跳时长,所谓心跳就是等待时间,阿里云规定要//大于60,否则拒绝连接
data.cleansession// 清除位,默认1
data.username.cstring//用户名
data.password.cstring//密码
data.MQTTVersion //规定版本号,比如Viersion4,连接阿里云,透传
关于Qos不细说;
连接阿里云的具体过程:发出连接报文>收到CONNACK(云确认到你的连接的回执)>订阅>收到SUBACK,返回Qos>不断Read做轮询,该干什么干什么,比如Pub、心跳以及等待云端Pub;
阿里云对于C的JDK手册中说,规定Pub和Sub的报文格式为Json格式,因此要注意你在Pub和在Sub解析的时候都要注意Json格式,是否符合Json格式,网上有很多在线检测,请自行搜索;
很重要的一点,一般来说,自带的Socket库里头的read都是阻塞的,如果你需要同时(近似)Pub和Sub,一定要自己修改成非阻塞的,不难;
下面贴上核心代码。
/**
*@Todo:通过tcp发数据到服务器
*/
int transport_sendPacketBuffer(unsigned char* buf, int buflen)
{
return send(SOCK_TCPS,buf,buflen);
}
/**
*@Todo:阻塞方式接收tcp服务器发送之数据
*/
int transport_getdata(unsigned char* buf, int count)
{
return recv(SOCK_TCPS,buf,count);
}
/**
*@Todo:打开一个socket并连接到服务器
*@retval 小于表示fail
*/
int transport_open(void){
int32_t ret;
//新建socket
ret = socket(1, Sn_MR_TCP,2001, 0x00);
if(ret != SOCK_TCPS){
printf("%d:Socket Error\r\n",SOCK_TCPS);
while(1);
}else{
printf("%d:Opened\r\n",SOCK_TCPS);
}
//连接TCP服务器
ret = connect(SOCK_TCPS,domain_ip,1883);
if(ret != SOCK_OK){
printf("%d:Socket Connect Error\r\n",SOCK_TCPS);
//while(1);
}else{
printf("%d:Connected\r\n",SOCK_TCPS);
}
return 0;
}
/**
*@Todo:关闭Socket
*/
int transport_close(void)
{
disconnect(SOCK_TCPS);
close(SOCK_TCPS);
return 0;
}
/***************************以上为基本四函数************************/
while(1){
timeh++;
if(connect_flag == 1&&timeh==1000)
{ // Json格式,根据Ailiyun中定义变量进行数据发布,payload为负载
Ua = 56.7 -rand()%10+1;
Ub = 34.8 -rand()%10+1;
Uc = 69.2 -rand()%10+1;
sprintf((char*)payload_out,"{\"params\":{\"nUa\":+%0.1f,\"Ub\":+%0.1f,\"Uc\":+%0.1f},\"method\":\"thing.event.property.post\"}",Ua,Ub,Uc);
payload_out_len = strlen((char*)payload_out);
topicString.cstring = topic_pub_set_info; //发布推送主题
len = MQTTSerialize_publish(buf, buflen, 0, req_qos, retained, msgid, topicString, payload_out, payload_out_len);
rc = transport_sendPacketBuffer(buf, len);
if(rc == len) //
printf("send PUBLISH Successfully\r\n"); // 发出去和收回来的长度一样,代表发布ok
}
if(timeh>=1000)
timeh = 0;
switch(msgtypes)
{
case CONNECT: len = MQTTSerialize_connect(buf, buflen, &data); //发送连接信息
rc = transport_sendPacketBuffer(buf, len); //发送连接信息
if(rc == len){ //
printf("send CONNECT Successfully\r\n");
}
else{
msgtypes = CONNECT;
continue;
}
break;
case CONNACK: if(MQTTDeserialize_connack(&sessionPresent, &connack_rc,buf, buflen)) //收回执
{
printf("MQTT is conncet OK!\r\n"); //收到回执,连接成功
connect_flag = 1;
msgtypes = SUBSCRIBE; //订阅操作
}
else
{
printf("Unable to connect, return code %d\r\n", connack_rc); //失败
}
break;
case SUBSCRIBE: topicString.cstring = topic_sub_set_info;
len = MQTTSerialize_subscribe(buf, buflen, 0, msgid, 1, &topicString, &req_qos);
rc = transport_sendPacketBuffer(buf, len);
if(rc == len){
goto tt;
}
else
{
continue;
}
break;
case SUBACK: rc = MQTTDeserialize_suback(&submsgid, 1, &subcount, &granted_qos,buf, buflen); //订阅成功 QoS
printf("granted qos is %d\r\n", granted_qos);
if (granted_qos != 0)
//goto exit;
printf("Get qos");
break;
case PUBLISH: rc = MQTTDeserialize_publish(&dup, &qos, &retained, &msgid, &receivedTopic,&payload_in, &payloadlen_in, (unsigned char*)buf, buflen); //收到服务器消息
printf("message arrived : %s\r\n", payload_in);
json = cJSON_Parse((char *)payload_in); //解析数据包
if (!json)
{
printf("Error before: [%s]\r\n",cJSON_GetErrorPtr());
}
else
{
json_id = cJSON_GetObjectItem(json , "id");
if(json_id->type == cJSON_String)
{
printf("id:%s\r\n", json_id->valuestring);
}
json_params = cJSON_GetObjectItem(json , "params");
if(json_params)
{
if(cJSON_GetObjectItem(json_params, "LEDSwitch"))
{
}
if(cJSON_GetObjectItem(json_params, "p2"))
{
}
}
}
cJSON_Delete(json); //释放空间
if(qos == 1)
{
printf("publish qos is 1,send publish ack.\r\n"); //
memset(buf,0,buflen);
len = MQTTSerialize_ack(buf,buflen,PUBACK,dup,msgid); //publish ack
rc = transport_sendPacketBuffer(buf, len);
if(rc == len)
printf("send PUBACK Successfully\r\n");
else
printf("send PUBACK failed\r\n");
}
break;
case PUBACK: printf("PUBACK!\r\n");
break;
case PUBREC: printf("PUBREC!\r\n"); //just for qos2
USART2_sendbyte(12);
break;
case PUBREL: printf("PUBREL!\r\n"); //just for qos2
break;
case PUBCOMP: printf("PUBCOMP!\r\n"); //just for qos2
break;
case PINGREQ: len = MQTTSerialize_pingreq(buf, buflen);
rc = transport_sendPacketBuffer(buf, len);
break;
case PINGRESP: printf("mqtt server Pong\r\n");
msgtypes = PINGREQ;
break;
default:
break;
}
if(msgtypes!=SUBSCRIBE){
tt:
rc=MQTTPacket_read(buf, buflen, transport_getdata); 轮询,根据收到的回执进行相应的处理
if(rc >0) //如果收到正确的,存在的回执,则进到相应状态
{
msgtypes = rc;
}else msgtypes=PINGREQ;
}
}
exit:
transport_close();
MQTT通信部分使用了以上的状态机,其实大部分代码网上都有,但是都不是完全正确,而且根据每个人情况不同,代码的理解也不同。
姑且算是原创吧(? 毕竟我也是自己搞的,如果违反了,请提醒我修改。
对域名的DNS解析,大家可以网上自己移植,我尝试过自己写,写了一天硬是没写出来,直接移植比较方便;
结论写着写着感觉文字有点多…不太会写文章,大家有问题还是问吧,如果有看见会回答的,我个人认为这个项目大家没必要去花钱买资料看,网上可以白嫖的很多的,从0开始确实很艰辛…我也是花了几天时间去搞明白,最重要的是要理解,盲目的肝代码莫得用处(对自己说的…)