写点什么

STM32+ESP8266+MQTT 协议连接 OneNet 物联网平台

作者:DS小龙哥
  • 2022 年 7 月 22 日
  • 本文字数:7625 字

    阅读完需:约 25 分钟

一、环境介绍

单片机采用: STM32F103C8T6


上网方式: 采用 ESP8266,也可以使用其他设备代替,只要支持 TCP 协议即可。比如:GSM 模块、有线网卡等。


开发软件: keil5


硬件连接功能: ESP8266 接在 STM32 的串口 3 上。通过 AT 指令与 ESP8266 进行通信。

二、功能介绍

2.1 功能说明

通过 OneNet 物联网服务器实现设备数据远程上传、下发,实现数据交互(不清楚 OneNet 物联网服务器功能的可以百度一下进入官网看简介)。之前的 OneNet 服务器不支持标准 MQTT 协议登录的,现在官网更新之后支持标准的 MQTT 协议,本篇文章介绍使用 STM32+ESP8266 使用标准 MQTT 协议登录 Onenet 服务器,实现数据交互。实现步骤 OneNet 官方提供了很详细的文档,可以参考一下。


文档地址:https://open.iot.10086.cn/devdoc/




2.2 硬件资源

在当前使用的开发板上有 4 盏 LED 灯、一个蜂鸣器、4 个按键,ESP8266 型号是 ESP-12F,STM32 型号是:STM32F103C8T6。



三、OneNet 支持的 MQTT 协议版本

目前 OneNet 服务器支持 MQTT 3.1.1 版本,MQTT 协议官网: http://mqtt.org/?spm=a2c4g.11186623.2.11.19083f86gxhJ7h


报文支持情况: 支持 connect、subscribe、publish、ping、unsubscribe、disconnect 等报文,不支持 pubrec、pubrel、pubcomp 报文。



四、登录 OneNet 服务器创建物联网产品

没有注册账号的,需要提前登录官网注册账号,再进入下面步骤:





这里根据自己产品情况填写。





产品创建成功之后,点击产品名称,跳转页面,继续添加设备。

















下面选择仪表盘的数据来源,根据自己创建的数据点选择。



创建一个文本控件,显示数据点更新的时间,方便调试。






OneNte 有手机版本的 APP,登录之后也可以看到该页面。


下载地址:https://open.iot.10086.cn/doc/book/device-develop/multpro/sdk-doc-tool/APP.html




下面是手机上登录 APP 看到的界面效果:




五、OneNet 服务器 MQTT 登录地址与订阅主题相关格式介绍

官网介绍文档地址: https://open.iot.10086.cn/doc/mqtt/book/get-start/connect.html



5.1 MQTT 服务器登录地址


目前 MQTT 协议支持两个 IP 地址和端口号,一个需要加密、一个不需要加密。


注意:单片机上移植加密算法很麻烦,这里采用不需要加密的端口。(IP 地址: 183.230.40.96 端口: 1883)



5.2 MQTT 登录的:设备 ID、用户名称、密码 格式参数



上面图片里说明了,OneNet 的设备参数与标准 MQTT 协议的登录参数对应关系。 OneNet 的设备参数,在设备页面可以去查看。


登录密码生成看下面步骤:





注意:该工具在 win10 系统运行可能会提示非信任程序,点击任要运行即可。


下面是生成 MQTT 登录密匙的工具使用示例。



注意:工具中填的参数说明请看文档介绍。


res 选项参数的格式: products/{产品 ID}/devices/{设备名称}


et 是设置 token 过期时间:算出 1970-1-1 到你想要设置的到期时间,单位是秒,填入即可。


比如: 超时时间设置为 2020-07-20 ,那么,这里填入的秒就是:1970-1-1 到 2020-07-20 之间的秒单位时间。


Linux 下代码:


#include <stdio.h>#include <time.h> #include <time.h>
int main(){ time_t time_sec; time_sec=time(NULL); //当前的秒单位时间--UTC时间 printf("当前时间(秒):%ld\n",time_sec); printf("加30天的时间(秒):%ld\n",time_sec+30*24*60*60); return 0;}
复制代码


key 的参数格式: 就是设备创建之后,在设备详情页的 key


工具生成的结果值,直接当做 MQTT 登录的密码。

5.3 主题订阅格式

文档地址:https://open.iot.10086.cn/doc/mqtt/book/device-develop/protocol.html




5.4 设备保活时间



5.5 向服务器传数据点




六、核心代码

6.1 matt.c 代码

#include "mqtt.h"
u8 *mqtt_rxbuf;u8 *mqtt_txbuf;u16 mqtt_rxlen;u16 mqtt_txlen;u8 _mqtt_txbuf[256];//发送数据缓存区u8 _mqtt_rxbuf[256];//接收数据缓存区
typedef enum{ //名字 值 报文流动方向 描述 M_RESERVED1 =0 , // 禁止 保留 M_CONNECT , // 客户端到服务端 客户端请求连接服务端 M_CONNACK , // 服务端到客户端 连接报文确认 M_PUBLISH , // 两个方向都允许 发布消息 M_PUBACK , // 两个方向都允许 QoS 1消息发布收到确认 M_PUBREC , // 两个方向都允许 发布收到(保证交付第一步) M_PUBREL , // 两个方向都允许 发布释放(保证交付第二步) M_PUBCOMP , // 两个方向都允许 QoS 2消息发布完成(保证交互第三步) M_SUBSCRIBE , // 客户端到服务端 客户端订阅请求 M_SUBACK , // 服务端到客户端 订阅请求报文确认 M_UNSUBSCRIBE , // 客户端到服务端 客户端取消订阅请求 M_UNSUBACK , // 服务端到客户端 取消订阅报文确认 M_PINGREQ , // 客户端到服务端 心跳请求 M_PINGRESP , // 服务端到客户端 心跳响应 M_DISCONNECT , // 客户端到服务端 客户端断开连接 M_RESERVED2 , // 禁止 保留}_typdef_mqtt_message;
//连接成功服务器回应 20 02 00 00//客户端主动断开连接 e0 00const u8 parket_connetAck[] = {0x20,0x02,0x00,0x00};const u8 parket_disconnet[] = {0xe0,0x00};const u8 parket_heart[] = {0xc0,0x00};const u8 parket_heart_reply[] = {0xc0,0x00};const u8 parket_subAck[] = {0x90,0x03};
void MQTT_Init(void){ //缓冲区赋值 mqtt_rxbuf = _mqtt_rxbuf; mqtt_rxlen = sizeof(_mqtt_rxbuf); mqtt_txbuf = _mqtt_txbuf; mqtt_txlen = sizeof(_mqtt_txbuf); memset(mqtt_rxbuf,0,mqtt_rxlen); memset(mqtt_txbuf,0,mqtt_txlen); //无条件先主动断开 MQTT_Disconnect(); delay_ms(100); MQTT_Disconnect(); delay_ms(100);}
/*函数功能: 登录服务器函数返回值: 0表示成功 1表示失败*/u8 MQTT_Connect(char *ClientID,char *Username,char *Password){ u8 i,j; int ClientIDLen = strlen(ClientID); int UsernameLen = strlen(Username); int PasswordLen = strlen(Password); int DataLen; mqtt_txlen=0; //可变报头+Payload 每个字段包含两个字节的长度标识 DataLen = 10 + (ClientIDLen+2) + (UsernameLen+2) + (PasswordLen+2); //固定报头 //控制报文类型 mqtt_txbuf[mqtt_txlen++] = 0x10; //MQTT Message Type CONNECT //剩余长度(不包括固定头部) do { u8 encodedByte = DataLen % 128; DataLen = DataLen / 128; // if there are more data to encode, set the top bit of this byte if ( DataLen > 0 ) encodedByte = encodedByte | 128; mqtt_txbuf[mqtt_txlen++] = encodedByte; }while ( DataLen > 0 ); //可变报头 //协议名 mqtt_txbuf[mqtt_txlen++] = 0; // Protocol Name Length MSB mqtt_txbuf[mqtt_txlen++] = 4; // Protocol Name Length LSB mqtt_txbuf[mqtt_txlen++] = 'M'; // ASCII Code for M mqtt_txbuf[mqtt_txlen++] = 'Q'; // ASCII Code for Q mqtt_txbuf[mqtt_txlen++] = 'T'; // ASCII Code for T mqtt_txbuf[mqtt_txlen++] = 'T'; // ASCII Code for T //协议级别 mqtt_txbuf[mqtt_txlen++] = 4; // MQTT Protocol version = 4 对于 3.1.1 版协议,协议级别字段的值是 4(0x04) //连接标志 mqtt_txbuf[mqtt_txlen++] = 0xc2; // conn flags mqtt_txbuf[mqtt_txlen++] = 0; // Keep-alive Time Length MSB mqtt_txbuf[mqtt_txlen++] = 100; // Keep-alive Time Length LSB 100S心跳包 保活时间 mqtt_txbuf[mqtt_txlen++] = BYTE1(ClientIDLen);// Client ID length MSB mqtt_txbuf[mqtt_txlen++] = BYTE0(ClientIDLen);// Client ID length LSB memcpy(&mqtt_txbuf[mqtt_txlen],ClientID,ClientIDLen); mqtt_txlen += ClientIDLen; if(UsernameLen > 0) { mqtt_txbuf[mqtt_txlen++] = BYTE1(UsernameLen); //username length MSB mqtt_txbuf[mqtt_txlen++] = BYTE0(UsernameLen); //username length LSB memcpy(&mqtt_txbuf[mqtt_txlen],Username,UsernameLen); mqtt_txlen += UsernameLen; } if(PasswordLen > 0) { mqtt_txbuf[mqtt_txlen++] = BYTE1(PasswordLen); //password length MSB mqtt_txbuf[mqtt_txlen++] = BYTE0(PasswordLen); //password length LSB memcpy(&mqtt_txbuf[mqtt_txlen],Password,PasswordLen); mqtt_txlen += PasswordLen; } memset(mqtt_rxbuf,0,mqtt_rxlen); MQTT_SendBuf(mqtt_txbuf,mqtt_txlen); for(j=0;j<10;j++) { delay_ms(50); if(USART3_RX_FLAG) { memcpy((char *)mqtt_rxbuf,USART3_RX_BUFFER,USART3_RX_CNT); //memcpy for(i=0;i<USART3_RX_CNT;i++)USART1_Printf("%#x ",USART3_RX_BUFFER[i]); USART3_RX_FLAG=0; USART3_RX_CNT=0; } //CONNECT if(mqtt_rxbuf[0]==parket_connetAck[0] && mqtt_rxbuf[1]==parket_connetAck[1]) //连接成功 { return 0;//连接成功 } } return 1;}
/*函数功能: MQTT订阅/取消订阅数据打包函数函数参数: topic 主题 qos 消息等级 0:最多分发一次 1: 至少分发一次 2: 仅分发一次 whether 订阅/取消订阅请求包 (1表示订阅,0表示取消订阅)返回值: 0表示成功 1表示失败*/u8 MQTT_SubscribeTopic(char *topic,u8 qos,u8 whether){ u8 i,j; mqtt_txlen=0; int topiclen = strlen(topic); int DataLen = 2 + (topiclen+2) + (whether?1:0);//可变报头的长度(2字节)加上有效载荷的长度 //固定报头 //控制报文类型 if(whether)mqtt_txbuf[mqtt_txlen++] = 0x82; //消息类型和标志订阅 else mqtt_txbuf[mqtt_txlen++] = 0xA2; //取消订阅
//剩余长度 do { u8 encodedByte = DataLen % 128; DataLen = DataLen / 128; // if there are more data to encode, set the top bit of this byte if ( DataLen > 0 ) encodedByte = encodedByte | 128; mqtt_txbuf[mqtt_txlen++] = encodedByte; }while ( DataLen > 0 ); //可变报头 mqtt_txbuf[mqtt_txlen++] = 0; //消息标识符 MSB mqtt_txbuf[mqtt_txlen++] = 0x0A; //消息标识符 LSB //有效载荷 mqtt_txbuf[mqtt_txlen++] = BYTE1(topiclen);//主题长度 MSB mqtt_txbuf[mqtt_txlen++] = BYTE0(topiclen);//主题长度 LSB memcpy(&mqtt_txbuf[mqtt_txlen],topic,topiclen); mqtt_txlen += topiclen; if(whether) { mqtt_txbuf[mqtt_txlen++] = qos;//QoS级别 } for(i=0;i<10;i++) { memset(mqtt_rxbuf,0,mqtt_rxlen); MQTT_SendBuf(mqtt_txbuf,mqtt_txlen); for(j=0;j<10;j++) { delay_ms(50); if(USART3_RX_FLAG) { memcpy((char *)mqtt_rxbuf,(char*)USART3_RX_BUFFER,USART3_RX_CNT); USART3_RX_FLAG=0; USART3_RX_CNT=0; } if(mqtt_rxbuf[0]==parket_subAck[0] && mqtt_rxbuf[1]==parket_subAck[1]) //订阅成功 { return 0;//订阅成功 } } } return 1; //失败}
//MQTT发布数据打包函数//topic 主题 //message 消息//qos 消息等级 u8 MQTT_PublishData(char *topic, char *message, u8 qos){ int topicLength = strlen(topic); int messageLength = strlen(message); static u16 id=0; int DataLen; mqtt_txlen=0; //有效载荷的长度这样计算:用固定报头中的剩余长度字段的值减去可变报头的长度 //QOS为0时没有标识符 //数据长度 主题名 报文标识符 有效载荷 if(qos) DataLen = (2+topicLength) + 2 + messageLength; else DataLen = (2+topicLength) + messageLength;
//固定报头 //控制报文类型 mqtt_txbuf[mqtt_txlen++] = 0x30; // MQTT Message Type PUBLISH
//剩余长度 do { u8 encodedByte = DataLen % 128; DataLen = DataLen / 128; // if there are more data to encode, set the top bit of this byte if ( DataLen > 0 ) encodedByte = encodedByte | 128; mqtt_txbuf[mqtt_txlen++] = encodedByte; }while ( DataLen > 0 ); mqtt_txbuf[mqtt_txlen++] = BYTE1(topicLength);//主题长度MSB mqtt_txbuf[mqtt_txlen++] = BYTE0(topicLength);//主题长度LSB memcpy(&mqtt_txbuf[mqtt_txlen],topic,topicLength);//拷贝主题 mqtt_txlen += topicLength; //报文标识符 if(qos) { mqtt_txbuf[mqtt_txlen++] = BYTE1(id); mqtt_txbuf[mqtt_txlen++] = BYTE0(id); id++; } memcpy(&mqtt_txbuf[mqtt_txlen],message,messageLength); mqtt_txlen += messageLength; MQTT_SendBuf(mqtt_txbuf,mqtt_txlen); return mqtt_txlen;}
void MQTT_SentHeart(void){ MQTT_SendBuf((u8 *)parket_heart,sizeof(parket_heart));}
void MQTT_Disconnect(void){ MQTT_SendBuf((u8 *)parket_disconnet,sizeof(parket_disconnet));}
void MQTT_SendBuf(u8 *buf,u16 len){ USARTx_DataSend(USART3,buf,len);}
复制代码

6.2 mqtt.h 代码

#ifndef __FY_MQTT_H_#define __FY_MQTT_H_
#include "stm32f10x.h"#include "string.h"#include "stdio.h"#include "stdlib.h"#include "stdarg.h"#include "delay.h"#include "usart.h"
#define BYTE0(dwTemp) (*( char *)(&dwTemp))#define BYTE1(dwTemp) (*((char *)(&dwTemp) + 1))#define BYTE2(dwTemp) (*((char *)(&dwTemp) + 2))#define BYTE3(dwTemp) (*((char *)(&dwTemp) + 3)) //用户名初始化void OneNet_LoginInit(char *ProductKey,char *DeviceName,char *DeviceSecret);//MQTT协议相关函数声明u8 MQTT_PublishData(char *topic, char *message, u8 qos);u8 MQTT_SubscribeTopic(char *topic,u8 qos,u8 whether);void MQTT_Init(void);u8 MQTT_Connect(char *ClientID,char *Username,char *Password);void MQTT_SentHeart(void);void MQTT_Disconnect(void);void MQTT_SendBuf(u8 *buf,u16 len);#endif
复制代码

6.3 main.c 主函数代码

#include "stm32f10x.h"#include "led.h"#include "delay.h"#include "key.h"#include "usart.h"#include <string.h>#include "timer.h"#include "esp8266.h"#include "mqtt.h"
/*序号 符号 编码1 + %2B2 空格%203 / %2F4 ? %3F5 % %256 # %237 & %268 = %3D*/
//OneNet物联网服务器的设备信息#define MQTT_ClientID "mq2"#define MQTT_UserName "361594"
#define MQTT_PassWord "version=2018-10-31&res=products%2F361594%2Fdevices%2Fmq2&et=1597492895&method=sha1&sign=uqvA0KkjXw0FlN01aT6fWrGBLGw%3D"
//订阅与发布的主题//格式:$sys/{产品ID}/{设备名称}/##define SET_TOPIC "$sys/361594/mq2/#" //订阅设备所有信息
//格式: $sys/{产品ID}/{设备名称}/dp/post/json#define POST_TOPIC "$sys/361594/mq2/dp/post/json" //发布
char mqtt_message[200];//上报数据缓存区
int main(){ u32 time_cnt=0; u32 i; u8 key; LED_Init(); BEEP_Init(); KEY_Init(); USART1_Init(115200); TIMER1_Init(72,20000); //超时时间20ms USART3_Init(115200);//串口-WIFI TIMER3_Init(72,20000); //超时时间20ms USART1_Printf("正在初始化WIFI请稍等.\n"); if(ESP8266_Init()) { USART1_Printf("ESP8266硬件检测错误.\n"); } else { //加密端口 //USART1_Printf("WIFI:%d\n",ESP8266_STA_TCP_Client_Mode("OnePlus5T","1126626497","183.230.40.16",8883,1)); //非加密端口 USART1_Printf("WIFI:%d\n",ESP8266_STA_TCP_Client_Mode("OnePlus5T","1126626497","183.230.40.96",1883,1)); } //2. MQTT协议初始化 MQTT_Init(); //3. 连接OneNet服务器 while(MQTT_Connect(MQTT_ClientID,MQTT_UserName,MQTT_PassWord)) { USART1_Printf("OneNet服务器连接失败,正在重试...\n"); delay_ms(500); } USART1_Printf("OneNet服务器连接成功.\n"); //3. 订阅主题 if(MQTT_SubscribeTopic(SET_TOPIC,0,1)) { USART1_Printf("主题订阅失败.\n"); } else { USART1_Printf("主题订阅成功.\n"); } while(1) { key=KEY_Scan(0); if(key==2) { time_cnt=0; sprintf(mqtt_message,"{\"id\":1,\"dp\":{\"mq2\":[{\"v\":50}]}}"); MQTT_PublishData(POST_TOPIC,mqtt_message,0); USART1_Printf("发送状态1\r\n"); } else if(key==3) { time_cnt=0; sprintf(mqtt_message,"{\"id\":1,\"dp\":{\"mq2\":[{\"v\":80}]}}"); MQTT_PublishData(POST_TOPIC,mqtt_message,0); USART1_Printf("发送状态0\r\n"); }
if(USART3_RX_FLAG) { USART3_RX_BUFFER[USART3_RX_CNT]='\0'; for(i=0;i<USART3_RX_CNT;i++) { USART1_Printf("%c",USART3_RX_BUFFER[i]); } USART3_RX_CNT=0; USART3_RX_FLAG=0; }
//定时发送心跳包,保持连接 delay_ms(10); time_cnt++; if(time_cnt==500) { MQTT_SentHeart();//发送心跳包 time_cnt=0; } }}
复制代码

七、设备登录运行效果

登录成功之后,网页会显示在线状态。

按下开发按键上传烟雾数据到服务器效果:




发布于: 2022 年 07 月 22 日阅读数: 66
用户头像

DS小龙哥

关注

之所以觉得累,是因为说的比做的多。 2022.01.06 加入

熟悉C/C++、51单片机、STM32、Linux应用开发、Linux驱动开发、音视频开发、QT开发. 目前已经完成的项目涉及音视频、物联网、智能家居、工业控制领域

评论

发布
暂无评论
STM32+ESP8266+MQTT协议连接OneNet物联网平台_7月月更_DS小龙哥_InfoQ写作社区