Esp8266通过Mqtt协议连接阿里云平台
前言
提示:代码写的越急 ,程序跑得越慢—— Roy Carlson
本篇文章写的主要是通过Paho MQTT协议使ESP8266连接阿里云平台,如有需要创建阿里云平台和物模型的教程创建请参考以下文章:https://blog.csdn.net/chentuo2000/article/details/103559553
一、什么是MQTT协议?
MQTT(Message Queuing Telemetry Transport,消息队列遥测传输协议),是一种基于发布/订阅(publish/subscribe)模式的“轻量级”通讯协议,该协议构建于TCP/IP协议上,由IBM在1999年发布。
MQTT最大优点在于,用极少的代码和有限的带宽,为连接远程设备提供实时可靠的消息服务。
作为一种低开销、低带宽占用的即时通讯协议,使其在物联网、小型设备、移动应用等方面有较广泛的应用。
MQTT的特点
MQTT是一个基于客户端-服务器的消息发布/订阅传输协议。
MQTT协议是轻量、简单、开放和易于实现的,这些特点使它适用范围非常广泛。在很多情况下,包括受限的环境中,如:机器与机器(M2M)通信和物联网(IoT)。
其在,通过卫星链路通信传感器、偶尔拨号的医疗设备、智能家居、及一些小型化设备中已广泛使用。
MQTT协议当前版本为,2014年发布的MQTT v3.1.1。除标准版外,还有一个简化版MQTT-SN,该协议主要针对嵌入式设备,这些设备一般工作于TCP/IP网络,如:ZigBee。
MQTT 与 HTTP 一样,MQTT 运行在传输控制协议/互联网协议 (TCP/IP) 堆栈之上。
二、MQTT的基本工作原理
在MQTT协议通讯中,有两个最为重要的角色。它们分别是服务端和客户端
1.MQTT服务端
MQTT服务端通常是一台服务器。它是MQTT信息传输的枢纽,负责将MQTT客户端发送来的信息传递给MQTT客户端。MQTT服务端还负责管理MQTT客户端。确保客户端之间的通讯顺畅,保证MQTT消息得以正确接收和准确投递。
2.MQTT客户端
MQTT客户端可以向服务端发布信息,也可以从服务端收取信息。我们把客户端发送信息的行为成为“发布”信息。而客户端要想从服务端收取信息,则首先要向服务端“订阅”信息。“订阅”信息这一操作很像我们在视频网站订阅某一部电视剧。当这部电视剧上新后,视频网站会向订阅了该剧的用户发送信息,告诉他们有新剧上线了。
3.MQTT主题
刚刚我们在讲解MQTT客户端订阅信息时,使用了用户在视频网站订阅电视剧这个例子。在MQTT通讯中,客户端所订阅的肯定不是一部部电视剧,而是一个个“主题”。MQTT服务端在管理MQTT信息通讯时,就是使用“主题”来控制的。
为了便于您更好理解服务端是如何通过主题来控制客户端之间的信息通讯,我们来看看下图实例:
MQTT通讯实例-1
在以上图示中一共有三个MQTT客户端。它们分别是汽车,手机和电脑。MQTT服务端在管理MQTT通讯时使用了“主题”来对信息进行管理的。比如上图所示,假设我们需要利用手机和电脑获取汽车的速度,那么我们首先要利用电脑和手机向MQTT服务器订阅主题“汽车速度”。接下来,当汽车客户端向服务端的“汽车速度”主题发布信息后,服务端就会首先检查以下都有哪些客户端订阅了“汽车速度”这一主题的信息。当它发现订阅了该主题的客户端有一个手机和一个电脑,于是服务端就会将刚刚收到的“汽车速度”信息转发给订阅了该主题的手机和电脑客户端。
在以上实例中,汽车是“汽车速度”主题的发布者,而手机和电脑则是该主题的订阅者。
值得注意的是,MQTT客户端在通讯时,往往角色不是单一的。它既可以作为信息发布者也可以同时作为信息订阅者。如下图所示:
MQTT通讯实例-2
上图中的所有客户端都是围绕“空调温度”这一主题进行通讯的。对于“空调温度”这一主题,手机和电脑客户端成为了MQTT信息的发布者而汽车则成为了MQTT信息的订阅者(接收者)。
可以看到,针对不同的主题,MQTT客户端可以切换自己的角色。它们可能对主题A来说是信息发布者,但是对于主题B就成了信息订阅者。
参考:http://www.taichi-maker.com/homepage/esp8266-nodemcu-iot/iot-tuttorial/mqtt-tutorial/2-mqtt-basics/
三.Paho MQTT库
1.Paho MQTT库介绍
Paho是一个很小的开源项目,java文件只有96个,目标就是实现MqTT协议并封装接口提供给用户使用。项目很小,所以门槛就比较低,都能看的懂。功能虽然简单,但是很多共性的问题都会处理到,比如经典的生产者消费者问题、同步异步问题、多线程问题等,阅读源码的时候会有一定启发。另外,项目是eclipse大厂出品,可以学习大厂的代码设计。
Paho MQTT库下载地址:
链接: https://gitee.com/v587_lijing/paho.mqtt.embedded-c
2.Phao MQTT库的移植
1.访问连接下载paho.mqtt.embedded-c-master.zip包
Paho MQTT库下载地址: https://gitee.com/v587_lijing/paho.mqtt.embedded-c
2.将下载好的文件按照以下路径打开
路径:paho.mqtt.embedded-c-master.zip\paho.mqtt.embedded-c-master\MQTTPacket\src
3.将src文件夹复制到我们keil的工程文件里面,新建一个MQTT文件夹
4.路径下paho.mqtt.embedded-c-master\MQTTPacket\src的src文件夹内容简单介绍
文件名 | 介绍 |
---|---|
MQTTConnectClient.c | 包含了作为MQTT客户端的连接服务器,断开连接,发送心跳请求的函数 |
MQTTConnectServer.c | 包含了作为MQTT服务端处理连接请求所需要的函数 |
MQTTDeserializePublish.c | 包含了解析PUBLISH报文的函数,通俗说就是接收消息用的 |
MQTTFormat.c | 包含了报文构造函数,被其它文件中的报文构造函数调用,不直接调用里面的函数 |
MQTTPacket.c | 包含了供其他文件调用的一些解析报文用的函数 |
MQTTSerializePublish.c | 包含了构造PUBLISH,PUBACK,PUBREC,PUBREL报文的函数,通俗说就是发消息用的 |
MQTTSubscribeClient.c | 包含了构造SUBSCRIBE报文的函数,发送订阅主题的请求时使用的 |
MQTTSubscribeServer.c | 包含了解析SUBSCRIBE和构造SUBACK的函数,服务端使用的文件 |
MQTTUnsubscribeClient.c | 包含了构造UNSUBSCRIBE的函数,发送取消订阅主题的时使用 |
MQTTUnsubscribeServer.c | 包含了解析UNSUBSCRIBE和构造UNSUBACK报文的函数,服务端使用的文件 |
这里我们只做简单介绍,关于Paho MQTT库的详细讲解推荐以下文章
链接: Paho mqtt C语言库介绍
四.代码实现
1.先通过AT指令使esp8266连接阿里云服务器
u8 send_check_ack(u8* AT,u8*ack,u16 outtime)
{
//1.发送指令
//2.检查返回值里面有没有ACK
memset(wifi.rxdata,0,1024);
wifi.rxlen=0;
wifi.rxflag=0;
UART3_sendstr(AT);//发送AT指令
while(outtime)//通过while循环做一个超时等待,若模块未能在指定时间回应就退出循环。
{
if(wifi.rxflag == 1)//表示串口一次接收完成,通过串口空闲中断完成
{
//通过strstr函数去查找接收wifi.rxdata中有没有wifi模块回应OK
if(strstr((char *)wifi.rxdata,(char *)ack) !=NULL)
{
return 1;//表示指令成功发送,wifi模块也回应了你想得到的ACK
}
else
{
wifi.rxflag=0;//表示WiFi指令执行失败,将接收标志置0。为下一次接收准备
}
}
outtime--;
Delay_nms(1);
}
return 0;
}
//通过串口发送AT指令,连接阿里云
void wifi_cmd(void)
{
//通过usart发送AT指令,查询wifi模块是否正常工作
if(send_check_ack((u8*)"AT\r\n",(u8*)"OK",100))
{
//设置wifi模式为ST模式
if(send_check_ack((u8*)"AT+CWMODE=1\r\n",(u8*)"OK",100))
{
//wifi连接,注意热点要为2.4GHZ
if(send_check_ack((u8*)"AT+CWJAP=\"热点名称\",\"热点密码\"\r\n",(u8*)"OK",5000))
{
//连接阿里云平台,这里用了sprintf字符串拼接函数,MqttHostUr和Port可以在阿里云->物联网平台->设备->设备信息->mqtt连接参数中查看
sprintf((char*)wifi.txdata,"AT+CIPSTART=\"TCP\",\"%s\",%d\r\n",MqttHostUrl,Port);
//AT+CIPSTART指令将ESP8266作为TCP Client连接到阿里云服务器。TCP代表协议,MqttHostUr是服务器IP,Port是服务器端口
if(send_check_ack(wifi.txdata,(u8*)"OK",4000))
{
//启用透明传输模式
if(send_check_ack((u8*)"AT+CIPMODE=1\r\n",(u8*)"OK",100))
{
//设置wifi模块进入发送数据模式
if(send_check_ack((u8*)"AT+CIPSEND\r\n",(u8*)">",100))
{
mqtt_connect();//通过MQTT协议连接物联网平台
}
}
}
}
}
}
}
2.使用MQTT协议连接阿里云物联网平台
//MQTT协议-连接服务器
void mqtt_connect(void)
{
unsigned char buf[512] = {
0};//定义一个buf存放报文内容
int buflen = sizeof(buf);
int len = 0;
//这一部分可以参考mqtt官方库中samples(移植路径下的)文件夹中的例子写
MQTTPacket_connectData data = MQTTPacket_connectData_initializer;//使用MQTT库函数中的宏定义初始化 data
data.keepAliveInterval = 100;//心跳时间间隔,规定客户端每100s要向服务端发送信息,让服务端知道客户端还存在。
data.cleansession = 1;//当写1代表不是重要信息,服务端将信息发送出去后不会管客户端有没有收到信息,当写0代表为重要信息,当服务端信息发送后,客户端需要回一个ACK代表收到信息
//以下参数可以在阿里云->物联网平台->设备->设备信息->mqtt连接参数中查看
data.clientID.cstring = Clientid;//要连接服务端的客户端ID
data.username.cstring = Username;//你的名字
data.password.cstring = Passwd; //你的密码
len = MQTTSerialize_connect(buf, buflen, &data);//这个操作就是把data中的数据 构造成可以使用的CONNECT的报文 然后将这个报文 放到buff缓存起来 并返回报文的长度
UART3_sendbuff(buf,len);//通过串口发送给ESP8266,之后mqtt协议和物联网平台连接
}
}
1.mqtt_connect()函数主要内容讲解:
1.MQTTPacket_connectData结构体
/*MQTT连接服务器数据结构体*/
typedef struct
{
/** The eyecatcher for this structure. must be MQTC. */
char struct_id[4];//这个就是会用来存放“MQTC”这个字符串的,因为CONNECT报文中会包含这段识别码。
/** The version number of this structure. Must be 0 */
int struct_version;//就写0,照着注释来。
/** Version of MQTT to be used. 3 = 3.1 4 = 3.1.1
*/
unsigned char MQTTVersion;//这个是指明使用的MQTT协议的版本,注释说必须要3,我们遵照注释使用
MQTTString clientID;//用来设置clientID根据个人需要设置
unsigned short keepAliveInterval;//用来设置回话保持时长,默认是60s
unsigned char cleansession;//指定了会话状态的处理方式,控制会话状态生存时间,是继续会话还是重新开始
unsigned char willFlag;//遗嘱标志(Will Flag)被设置为 1,表示如果连接请求被接受了,遗嘱(Will Message)消息必须被存储在 服务端并且与这个网络连接关联 -- 此代码中默认0 没有使用遗嘱
MQTTPacket_willOptions will;//包含 WILL TOPIC 和WILL MESSAGE 字段 -- 遗嘱信息
MQTTString username;//设置连接用户名
MQTTString password;//设置连接密码
} MQTTPacket_connectData;
2.MQTTSerialize_connect()函数讲解
int MQTTSerialize_connect(unsigned char* buf, int buflen, MQTTPacket_connectData* options)
这个函数是用来构造MQTT协议中的CONNECT报文的,连接服务器就靠这个函数了,
buf这个参数就是用来存放构造好的CONNECT报文的缓冲区,
buflen就是指明缓冲区的大小,防止在构造报文时指针越界,
MQTTPacket_connectData是Paho库声明的一种结构体,
我们需要的就是声明一个这样的结构体,并为这个结构体成员赋值。
五.源码提供(仅供参考)
ESP8266.c文件
#include "esp8266.h"
#include "string.h"
#include "stdio.h"
#include "delay.h"
#include "MQTTPacket.h"
struct esp wifi={
0};
void esp_init(void)
{
//1.开启时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);//开启GPIOB口的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOE,ENABLE);//开启GPIOE口的时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART3,ENABLE);//开启USART3的时钟
//GPIO初始化
GPIO_InitTypeDef GPIO_InitStruct={
0};
GPIO_InitStruct.GPIO_Pin=GPIO_Pin_10;
GPIO_InitStruct.GPIO_Mode=GPIO_Mode_AF_PP;//复用推挽输出
GPIO_InitStruct.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOB,&GPIO_InitStruct);
GPIO_InitStruct.GPIO_Pin=GPIO_Pin_11;
GPIO_InitStruct.GPIO_Mode=GPIO_Mode_IN_FLOATING;//浮空输入
GPIO_Init(GPIOB,&GPIO_InitStruct);
GPIO_InitStruct.GPIO_Pin=GPIO_Pin_6;
GPIO_InitStruct.GPIO_Mode=GPIO_Mode_Out_PP;//推挽输出
GPIO_Init(GPIOE,&GPIO_InitStruct);
GPIO_SetBits(GPIOE,GPIO_Pin_6);//IO口电平拉高
//串口初始化
USART_InitTypeDef USART_InitStruct={
0};
USART_InitStruct.USART_BaudRate=115200;//波特率
USART_InitStruct.USART_HardwareFlowControl=USART_HardwareFlowControl_None;
USART_InitStruct.USART_WordLength=USART_WordLength_8b;//8位数据
USART_InitStruct.USART_StopBits=USART_StopBits_1;//停止位
USART_InitStruct.USART_Parity=USART_Parity_No;//没有奇偶校验
USART_InitStruct.USART_Mode=USART_Mode_Rx | USART_Mode_Tx;//收发使能
USART_Init(USART3,&USART_InitStruct);
USART_ITConfig(USART3,USART_IT_RXNE,ENABLE);//开启串口接收中断
USART_ITConfig(USART3,USART_IT_IDLE,ENABLE);//开启串口空闲中断
USART_Cmd(USART3, ENABLE);//对USRT3使能
//NVIC初始化
NVIC_InitTypeDef NVIC_InitStruct={
0};
NVIC_InitStruct.NVIC_IRQChannel=USART3_IRQn;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority=2;
NVIC_InitStruct.NVIC_IRQChannelSubPriority=2;
NVIC_InitStruct.NVIC_IRQChannelCmd=ENABLE;
NVIC_Init(&NVIC_InitStruct);
}
//发送函数
//unsigned char
void UART3_send(uint8_t data)
{
while(USART_GetFlagStatus(USART3,USART_FLAG_TC) == 0){
}//检查发送移位寄存器是否为空,判断一次是否发送完成
USART_SendData(USART3,data);//发送数据(一个字节)
}
//发送不定长的字符串
void UART3_sendstr(uint8_t *str)
{
while(*str !='\0')//直到遇到'\0'停止
{
UART3_send(*str);
str++;
}
}
//发送定长字符串
void UART3_sendbuff(uint8_t *buff,uint8_t len)
{
for(uint8_t i=0;i<len;i++)
{
UART3_send(buff[i]);
}
}
void USART3_IRQHandler(void)
{
/*
1.判断哪个中断触发
2.清理中断标志
3.执行代码
*/
uint8_t recvdata=0;
if(USART_GetITStatus(USART3,USART_IT_RXNE)==1)
{
recvdata=USART_ReceiveData(USART3);
wifi.rxdata[wifi.rxlen]=recvdata;
wifi.rxlen++;
//串口接收数据,没有办法判断数据是多少
//利用串口发送函数,将数据发送。
USART_SendData(USART1,recvdata);
USART_ClearITPendingBit(USART3,USART_IT_RXNE);
}
if(USART_GetITStatus(USART3,USART_IT_IDLE)==1)//判断是否为串口空闲中断
{
//空闲中断的清理,不不能用清理中断标志的函数
//需要读取(接收)数据,即可清零
//官方的手册 给的操作是先读状态寄存器 再度数据寄存器
recvdata = USART3->SR;
recvdata = USART3->DR;
wifi.rxflag=1;//数据接收完成标志位置1;
}
}
u8 send_check_ack(u8* AT,u8*ack,u16 outtime)
{
//1.发送指令
//2.检查返回值里面有没有ACK
memset(wifi.rxdata,0,1024);
wifi.rxlen=0;
wifi.rxflag=0;
//发送AT指令
UART3_sendstr(AT);
while(outtime)
{
if(wifi.rxflag == 1)
{
if(strstr((char *)wifi.rxdata,(char *)ack) !=NULL)
{
return 1;
}
else
{
wifi.rxflag=0;
}
}
outtime--;
Delay_nms(1);
}
return 0;
}
//wifi命令 STA模式
void wifi_cmd(void)
{
if(send_check_ack((u8*)"AT\r\n",(u8*)"OK",100))
{
if(send_check_ack((u8*)"AT+CWMODE=1\r\n",(u8*)"OK",100))
{
if(send_check_ack((u8*)"AT+CWJAP=\"热点名称\",\"热点密码\"\r\n",(u8*)"OK",5000))
{
sprintf((char*)wifi.txdata,"AT+CIPSTART=\"TCP\",\"%s\",%d\r\n",MqttHostUrl,Port);
if(send_check_ack(wifi.txdata,(u8*)"OK",4000))
{
if(send_check_ack((u8*)"AT+CIPMODE=1\r\n",(u8*)"OK",100))
{
if(send_check_ack((u8*)"AT+CIPSEND\r\n",(u8*)">",100))
{
mqtt_connect();
}
}
}
}
}
}
}
//MQTT协议-连接服务器
void mqtt_connect(void)
{
unsigned char buf[512] = {
0};//定义一个buf存放报文内容
int buflen = sizeof(buf);
int len = 0;
//这一部分可以参考mqtt官方库中samples(移植路径下的)文件夹中的例子写
MQTTPacket_connectData data = MQTTPacket_connectData_initializer;//使用MQTT库函数中的宏定义初始化 data
data.keepAliveInterval = 100;//心跳时间间隔,规定客户端每100s要向服务端发送信息,让服务端知道客户端还存在。
data.cleansession = 1;//当写1代表不是重要信息,服务端将信息发送出去后不会管客户端有没有收到信息,当写0代表为重要信息,当服务端信息发送后,客户端需要回一个ACK代表收到信息
//以下参数可以在阿里云->物联网平台->设备->设备信息->mqtt连接参数中查看
data.clientID.cstring = Clientid;//要连接服务端的客户端ID
data.username.cstring = Username;//你的名字
data.password.cstring = Passwd; //你的密码
len = MQTTSerialize_connect(buf, buflen, &data);//这个操作就是把data中的数据 构造成可以使用的CONNECT的报文 然后将这个报文 放到buff缓存起来 并返回报文的长度
UART3_sendbuff(buf,len);//通过串口发送给ESP8266,之后mqtt协议和物联网平台连接
}
ESP8266.h
#ifndef _ESP8266_H_
#define _ESP8266_H_
#include "stm32f10x.h"
void esp_init(void);
struct esp{
u8 rxdata[1024];
u16 rxlen;
u8 rxflag;
u8 txdata[1024];
u16 txlen;
};
extern struct esp wifi;
u8 send_check_ack(u8* AT,u8*ack,u16 outtime);
void wifi_cmd(void);
void UART3_send(uint8_t data);
void UART3_sendstr(uint8_t *str);
void UART3_sendbuff(uint8_t *buff,uint8_t len);
void mqtt_connect(void);
//以下参数可以在阿里云->物联网平台->设备->设备信息->mqtt连接参数中查看
#define Clientid "你的ID"
#define Username "你的名字"
#define MqttHostUrl "阿里云服务器"
#define Passwd "你的密码"
#define Port 端口号
#endif
总结
以上就是今天要讲的内容,本文仅仅简单介绍了ESP8266如何通过mqtt协议连接阿里云的使用,mqtt官方库为我们提供了能使用的函数接口实现连接。
链接: ESP8266通过MQTT协议连接阿里云实现数据的下发和上传,时间和天气的获取