一、环境介绍
单片机采用:STM32F103C8T6
上网方式:采用ESP8266,也可以使用其他设备代替,只要支持TCP协议即可。比如:GSM模块、有线网卡等。
开发软件:keil5
硬件连接功能:ESP8266接在STM32的串口3上。通过AT指令与ESP8266进行通信。
注意:本篇文章没有贴ESP8266的底层编程代码,如果不会ESP8266底层编程,请看这里:
https://blog.csdn.net/xiaolong1126626497/article/details/107379554
如果需要了解ESP8266 MQTT协议连接阿里云物联网服务器请看这里:https://blog.csdn.net/xiaolong1126626497/article/details/107311897
工程完整源码下载: https://download.csdn.net/download/xiaolong1126626497/15803518
二、功能介绍
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下代码:
代码语言:javascript复制#include <stdio.h>
#include <time.h>
#include <time.h>
int main()
{
time_t time_sec;
time_sec=time(NULL); //当前的秒单位时间--UTC时间
printf("当前时间(秒):%ldn",time_sec);
printf("加30天的时间(秒):%ldn",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代码
代码语言:javascript复制#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 00
const 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代码
代码语言:javascript复制#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 主函数代码
代码语言:javascript复制#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 +
2 空格
3 / /
4 ? ?
5 % %
6 # #
7 & &
8 = =
*/
//OneNet物联网服务器的设备信息
#define MQTT_ClientID "mq2"
#define MQTT_UserName "361594"
#define MQTT_PassWord "version=2018-10-31&res=products/361594/devices/mq2&et=1597492895&method=sha1&sign=uqvA0KkjXw0FlN01aT6fWrGBLGw="
//订阅与发布的主题
//格式:$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:%dn",ESP8266_STA_TCP_Client_Mode("OnePlus5T","1126626497","183.230.40.16",8883,1));
//非加密端口
USART1_Printf("WIFI:%dn",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("发送状态1rn");
}
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("发送状态0rn");
}
if(USART3_RX_FLAG)
{
USART3_RX_BUFFER[USART3_RX_CNT]='