物联网在在最近今年非常的火,尤其是在5G概念席卷整个社会之后,物联网,大数据,人工智能的概念更是成为了人们茶余饭后的谈资。
物联网(Internet of Things,IoT)是指通过信息传感设备,按约定的协议,将任何物体与网络相连接,物体通过信息传播媒介进行信息交换和通信。物联网的目的是实现不受地点、时间限制,长期快速的连接如智能家庭、智慧城市等场景的移动设备。
在物联网高速发展的时期,物联网设备终端和物联网云平台都得到了大量的投入研发。从根本上说,物联网云平台充当的是网关,也可以充当管理工具,对现场设备进行管理。云平台允许设备端采集的大量数据上传到平台,也能够通过平台发送数据到设备端,是一个双向的通信。基本的功能,如设备管理、OTA远程升级、数据管理等典型功能。
目前,市面上有很多知名的云平台,比如中移物联网OneNet、阿里云、华为云、机智云等。这些云平台都有较为简单的接入方式,并且有大量的资料供我们学习。
MQTT(Message Queuing Telemetry Transport,消息队列遥测传输协议),是一种基于发布/订阅(publish/subscribe)模式的"轻量级"通讯协议,该协议构建于TCP/IP协议上,由IBM在1999年发布。MQTT最大优点在于,可以以极少的代码和有限的带宽,为连接远程设备提供实时可靠的消息服务。作为一种低开销、低带宽占用的即时通讯协议,使其在物联网、小型设备、移动应用等方面有较广泛的应用。
MQTT是一个基于客户端-服务器的消息发布/订阅传输协议。MQTT协议是轻量、简单、开放和易于实现的,这些特点使它适用范围非常广泛。在很多情况下,包括受限的环境中,如:机器与机器(M2M)通信和物联网(IoT)。其在,通过卫星链路通信传感器、偶尔拨号的医疗设备、智能家居、及一些小型化设备中已广泛使用。
MQTT 协议提供一对多的消息发布,可以降低应用程序的耦合性,用户只需要编写极少量的应用代码就能完成一对多的消息发布与订阅,该协议是基于<客户端-服务器>模型,在协议中主要有三种身份:发布者(Publisher)、服务器(Broker)以及订阅者(Subscriber)。其中,MQTT 消息的发布者和订阅者都是客户端,服务器只是作为一个中转的存在,将发布者发布的消息进行转发给所有订阅该主题的订阅者;发布者可以发布在其权限之内的所有主题,并且消息发布者可以同时是订阅者,实现了生产者与消费者的脱耦,发布的消息可以同时被多个订阅者订阅。
中移物联网OneNet平台,支持多协议接入,还具有多种增值服务。注册认证后,可以免费创建几个产品,对于学生来说,是一个学习物联网十分不错的平台,当然,阿里云,百度云也很不错。
硬件上,我使用的是野火的STM32F103VE指南者和WiFi模组ESP8266,硬件设备上还是比较大众化,便宜又好用,对于ESP8266直接使用AT指令控制就可以了。然后数据采集使用的是一个DHT11温湿度传感器,控制设备使用的是一个继电器。
软件上,使用串口1进行答疑调试,使用串口2来驱动ESP8266 WiFi模块。串口接收使用的是DMA+串口空闲中断来接收数据。具体可以参考我之前写的博客:
DMA+串口空闲中断
我使用的是OneNet的官方文档和资料移植的代码。
1)首先要做的是初始化ESP8266。
.c文件如下
UsartPrintf(USART_DEBUG, "\r\n1. AT\r\n");
while(ESP8266_SendCmd(AT, "OK"))
delay_ms(100);
UsartPrintf(USART_DEBUG, "2. CWMODE\r\n");
while(ESP8266_SendCmd(CWMODE, "OK"))
delay_ms(200);
UsartPrintf(USART_DEBUG, "3. 复位模块\r\n");
while(ESP8266_SendCmd(RST, "OK"))
delay_ms(200);
UsartPrintf(USART_DEBUG, "4. 连接WiFi\r\n");
while(ESP8266_SendCmd(CWJAP, "GOT IP"))
delay_ms(300);
UsartPrintf(USART_DEBUG, "5. 连接TCP服务器\r\n");
while(ESP8266_SendCmd(CIPSTART, "CONNECT"))
delay_ms(300);
UsartPrintf(USART_DEBUG, "8. ESP8266 初始化成功\r\n");
.h文件
#define AT "AT\r\n"
#define CWMODE "AT+CWMODE=3\r\n" //STA+AP模式
#define CWDHCP "AT+CWDHCP=1,1\r\n" //STA+AP模式
#define RST "AT+RST\r\n"
#define CIFSR "AT+CIFSR\r\n"
#define CWJAP "AT+CWJAP=\"你家的WiFi名字\",\"你家的WiFi密码\"\r\n" //ssid: onenet 密码:空
#define CIPSTART "AT+CIPSTART=\"TCP\",\"183.230.40.39\",6002\r\n" //MQTT服务器 183.230.40.39/876
#define CIPMODE0 "AT+CIPMODE=0\r\n" //非透传模式
#define CIPMODE1 "AT+CIPMODE=1\r\n" //透传模式
#define CIPSEND "AT+CIPSEND\r\n"
#define Out_CIPSEND "+++"
#define CIPSTATUS "AT+CIPSTATUS\r\n" //网络状态查询
发送了这些AT指令之后,WiFi模组就初始化成功并且成功连接上OneNet的MQTT服务器了。
2)然后连接到OneNet平台
这个函数的作用就是将产品号、鉴权信息、设备ID组包,通过WiFi模组上传到服务器,并对服务器返回的数据进行判断,是否连接到平台。
#define PROID "产品号"
#define AUTH_INFO "鉴权信息"
#define DEVID "设备ID"
_Bool OneNet_DevLink(void)
{
MQTT_PACKET_STRUCTURE mqttPacket = {NULL, 0, 0, 0}; //协议包
unsigned char *dataPtr;
_Bool status = 1;
UsartPrintf(USART_DEBUG,"OneNET_DevLink\r\n");
UsartPrintf(USART_DEBUG,"PROID: %s, AUIF: %s, DEVID:%s\r\n", PROID, AUTH_INFO, DEVID);
if(MQTT_PacketConnect(PROID, AUTH_INFO, DEVID, 512, 0, MQTT_QOS_LEVEL0, NULL, NULL, 0, &mqttPacket) == 0)
{
ESP8266_SendData(mqttPacket._data, mqttPacket._len); //上传平台
dataPtr = ESP8266_GetIPD(250); //等待平台响应
if(dataPtr != NULL)
{
if(MQTT_UnPacketRecv(dataPtr) == MQTT_PKT_CONNACK)
{
switch(MQTT_UnPacketConnectAck(dataPtr))
{
case 0:UsartPrintf(USART_DEBUG, "Tips: 连接成功\r\n");status = 0;break;
case 1:UsartPrintf(USART_DEBUG, "WARN: 连接失败:协议错误\r\n");break;
case 2:UsartPrintf(USART_DEBUG, "WARN: 连接失败:非法的clientid\r\n");break;
case 3:UsartPrintf(USART_DEBUG, "WARN: 连接失败:服务器失败\r\n");break;
case 4:UsartPrintf(USART_DEBUG, "WARN: 连接失败:用户名或密码错误\r\n");break;
case 5:UsartPrintf(USART_DEBUG, "WARN: 连接失败:非法链接(比如token非法)\r\n");break;
default:UsartPrintf(USART_DEBUG, "ERR: 连接失败:未知错误\r\n");break;
}
}
}
MQTT_DeleteBuffer(&mqttPacket); //删包
ESP8266_Clear();
}
else
UsartPrintf(USART_DEBUG, "WARN: MQTT_PacketConnect Failed\r\n");
return status;
}
3)封装上传的数据,我们通过字符串拼接函数把数据封装成cJSON格式的。
unsigned char OneNet_FillBuf(char *buf)//Type3 CJson格式
{
char text[24];
memset(text, 0, sizeof(text));
strcpy(buf, "{");
memset(text, 0, sizeof(text));//将text清零
sprintf(text, "\"key\":%d,", key);//控制精度并打印到text中
strcat(buf, text);//将字符串text添加到buf结尾处并添加'\0'
memset(text, 0, sizeof(text));//将text清零
sprintf(text, "\"temp\":%.1f,", temp);//控制精度并打印到text中
strcat(buf, text);//将字符串text添加到buf结尾处并添加'\0'
memset(text, 0, sizeof(text));
sprintf(text, "\"humi\":%.1f", humi);//湿度
strcat(buf, text);
strcat(buf, "}");
return strlen(buf);
}
4)我们上传数据的频率,通过一个定时器来控制,具体的频率可以自己调整。
void Tim1_Init(int arr,int psc)//500ms进入一次
{
TIM_TimeBaseInitTypeDef TIM_Structure; //定义定时器结构体变量
NVIC_InitTypeDef NVIC_TIM; //定义中断嵌套结构体变量
RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1,ENABLE); //打开定时器时钟
TIM_Structure.TIM_Period = (arr-1) ; //设置自动重装载寄存器周期值 溢出时间TimeOut= (arr)*(psc)/Tic 单位为us
TIM_Structure.TIM_Prescaler = (psc-1); //设置预分频值
TIM_Structure.TIM_CounterMode = TIM_CounterMode_Up ; //计数模式 上升计数
TIM_Structure.TIM_ClockDivision = TIM_CKD_DIV1; //时钟分频 Tic=72M/(TIM_ClockDivision+1)
TIM_Structure.TIM_RepetitionCounter = 0; //重复计数的次数
TIM_TimeBaseInit(TIM1,&TIM_Structure); //初始化定时器1
NVIC_TIM.NVIC_IRQChannel = TIM1_UP_IRQn; //定时器1的向上计算通道
NVIC_TIM.NVIC_IRQChannelCmd = ENABLE ; //使能
NVIC_TIM.NVIC_IRQChannelPreemptionPriority = 0 ; //抢占优先级
NVIC_TIM.NVIC_IRQChannelSubPriority = 0; //响应优先级
NVIC_Init(&NVIC_TIM); //初始化结构体
TIM_ClearFlag(TIM1,TIM_FLAG_Update); //清空所有标志位 保证工作状态初始化
TIM_ITConfig(TIM1,TIM_IT_Update,ENABLE); //打开计时器
TIM_Cmd(TIM1,ENABLE); //打开TIM1
}
void TIM1_UP_IRQHandler (void)
{
static unsigned int temp_count,ping_count;
if(TIM_GetITStatus(TIM1,TIM_IT_Update) != RESET) //如果中断标志被置1 证明有中断
{
TIM_ClearITPendingBit(TIM1,TIM_IT_Update); // 清空标志位,为下一次进入中断做准备
temp_count++;
ping_count++;
if(temp_count == 2000)//20s
{
temp_flag=1;
temp_count=0;
}
if(ping_count == 8000)//40s
{
ping_flag=1;
ping_count=0;
}
}
}
在main函数里面:我们间隔一定时间上报数据并上传心跳包。
if(temp_flag) //发送间隔5s
{
temp_flag=0;
DHT11_Read_Data();
UsartPrintf(USART_DEBUG, "OneNet_SendData\r\n");
OneNet_SendData(); //发送数据
ESP8266_Clear();
}
if(ping_flag)//发送心跳包 每40s/次
{
ping_flag=0;
MQTT_Ping();
ESP8266_Clear();
}
5)数据接收,通过封装一个GET_Cmd函数来获取。
void GET_Cmd(void)//获取指令
{
unsigned char *dataPtr = NULL;
dataPtr = ESP8266_GetIPD(0);
if(dataPtr != NULL)
OneNet_RevPro(dataPtr);
}
并且在OneNet_RevPro函数里面,添加判断接收数据的语句,判断是开还是关。
switch(type)
{
case MQTT_PKT_CMD: //命令下发
result = MQTT_UnPacketCmd(cmd, &cmdid_topic, &req_payload, &req_len); //解出topic和消息体
if(result == 0)
{
UsartPrintf(USART_DEBUG,"cmdid: %s, req: %s, req_len: %d\r\n", cmdid_topic, req_payload, req_len);
if(strstr((const char*)req_payload,"open")) key=1;
else if(strstr((const char*)req_payload,"close")) key=0;
.....
}
其他关键的函数就是,封装一个心跳包,保持长连接。
void MQTT_Ping(void)//发送心跳包
{
MQTT_PACKET_STRUCTURE mqttPacket = {NULL, 0, 0, 0};
if(MQTT_PacketPing(&mqttPacket)==0)//心跳包组包
{
ESP8266_SendData(mqttPacket._data, mqttPacket._len);//发送心跳包
UsartPrintf(USART_DEBUG, "Ping data:%x\r\n",mqttPacket._data);
MQTT_DeleteBuffer(&mqttPacket); //删包释放内存
UsartPrintf(USART_DEBUG, "Ping succeed\r\n");
// dataPtr = ESP8266_GetIPD(100); //等待平台响应
//
// if(dataPtr != NULL)
// {
// if(MQTT_UnPacketRecv(dataPtr) == MQTT_PKT_PINGRESP)//确认是心跳响应
// UsartPrintf(USART_DEBUG, "Ping succeed\r\n");
// }
// UsartPrintf(USART_DEBUG, "Ping succeed\r\n");
}
}
WiFi模组发送数据,没有采用透传模式。
void ESP8266_SendData(unsigned char *data, unsigned short len)
{
char cmdBuf[32];
ESP8266_Clear(); //清空接收缓存
sprintf(cmdBuf, "AT+CIPSEND=%d\r\n", len); //发送命令
if(!ESP8266_SendCmd(cmdBuf, ">")) //收到‘>’时可以发送数据
{
Usart_SendString(USART2, data, len); //发送设备连接请求数据
}
}
OneNet提供了可视化控件,将数据流通过可视化控件模型显示出来。但是我们上传的主题一定要是系统默认的主题$dp,控件才能接收到数据流。
当然,我们可以自己搭建一个自己的MQTT物联网平台。我使用的是阿里云的服务器,学生认证之后,只需要98块钱一年,十分划算。然后使用EMQ作为MQTT消息代理,node-red作为一个后台,使用dashboard搭建了一个物联网平台支持接入。
MQTT代码连接