解锁华为云新技能-AIOT开发全流程(2)【设备接入-ESP端侧数据收集[MQTT]-实时数据分析】(步步截图较详细)
ESP8266-STM32-传感器结合以实现端侧数据收集[MQTT]
前面这部分为了使的演示简单明了,同时也考虑到大部分的小伙伴看这篇博客是没有硬件在手,所以采用了虚拟设备进行云链接,现在我们开始展示硬件的云通信。
在这里主要的通讯模块是ESP8266,因为笔者实在是太菜,对NB-IOT或者4/5G模块或者LoRa的理解还不够深入,所以用这个例子来讲解MQTT的传输。
MQTT简介
MQTT可以为一种网络协议,是通讯的语言,比如说大家熟知的java、python、C/C+都是编程语言。而物联网设备间沟通的语言,就是网络协议。设备间想相互交流,通信双方必须使用同一种“语言”。比如说你和中国人问好说’你好‘、日本人问好要说‘こんにちは’、和英国人问好要说‘hello’.
我以前在郭朝斌“物联网开发实战“中曾经听到是这么解释MQTT的重要性的,这里例子非常生动:
物联网的网络通信特点是物联网设备很大可能工作在不可靠、高延迟的网络环境中。 并且物联网系统中,设备数量多,而且交互非常复杂。比如家里的环境监测,温度、湿度、光照、二氧化碳、甲醛含量……这些都需要不同的设备测量,而且每个房间用到的设备也不同。如果让云平台的服务对每个设备分别做权限控制和数据阈值设置,这会非常麻烦。
因为当数据的“生产者”和“消费者”直接交互时,要是没有中间角色基于共同的目标协调,双方的耦合度会很大,导致系统很难实现。 这时候,需要把家为一个整体来处理,交互逻辑就会变得简单多了。设备经常需要根据实际使用环境做增加、减少等调整。
所以基于上述的这些特点,物联网系统在选择网络通信的协议时,一般采用一种发布-订阅式的结构。
发布 - 订阅模式
发布 - 订阅模式包含三个角色,分别是发布者、经纪人和订阅者,它们的关系如下图所示
消息传递的过程可以分为三步:
1.发布者(设备)负责生产数据。发布者发送某个主题的数据给经纪人,发布者不知道订阅者。
2.订阅经纪人管理的某个或者某几个主题。
3.当经纪人接收到某个主题的数据时,将数据发送给这个主题的所有订阅者。
比如说当使用美团外卖点一分午餐,这时候发布订单给外卖订单中心服务器时,外卖订单中心收到订单之后,再把订单发送给店家
发布 - 订阅模式之所以适合物联网系统因为在物联网场景中,一个传感器数据需要触发多个服务或者终端执行动作。
比如红外传感器,当它检测到有人体靠近时,就需要触发一系列动作:通知摄像头拍照,声光报警器执行报警,推送消息给主人的手机等。
怎么满足这种需求呢?我们最好让摄像头、声光报警器和手机都订阅“人体靠近”这个主题消息。当红外传感器被触发时,它发送人体靠近的消息,然后这些设备就能同时收到这个消息,接着完成系统定义的那些动作。这就是发布 - 订阅模式的工作方式。
MQTT 协议就是发布 - 订阅模式中的大佬,所以MQTT在物联网使用的范围非常广泛。
MQTT
MQTT它有三个主要特点:
1.采用二进制的消息内容编码格式,所以二进制数据、JSON 和图片等负载内容都可以方便传输。(华为云也是仅仅支持JSON和二进制)
2.协议头很紧凑,协议交互也简单,保证了网络传输流量很小。
3.支持 3 种 QoS(Quality of Service,服务质量)级别,便于应用根据不同的场景需求灵活选择。
这三个特点,让 MQTT 协议非常适合计算能力有限、网络带宽低、信号不稳定的远程设备,所以它成为了物联网系统事实上的网络协议标准。
请求 - 响应模式
请求 - 响应模式有两个角色,一个是客户端,另一个是服务器。
客户端是请求数据或者服务的一方。服务器则用来接收客户端的请求,并提供相应的数据或者服务。服务器在收到请求后,会获取数据,对资源数据(比如数据库)进行加工处理,准备好响应,然后返回给客户端。
请求 - 响应模式是无状态的通信方式,每个完整的请求 - 响应都是相互独立的。进一步细分的话,它还可以分为同步和异步两种。你可以看下这张图片。
如果我们使用华为云的话,华为云就是我们的服务器,而我们的设备就是我们的客户端。之所以要采用云的工作方式,就是为了提高我们对数据加工处理的能力。
好,一般来说,ESP8266都是通过WI-FI来通信,但是方式和硬件设备条件有一些不同,有以下两种产品:
第一种是独立的开发版,有独立的处理器与充足的I/O管脚的,像下图
第二种是通讯模块,需要搭配板子使用,期间是信息传递的桥梁,将信息传输的平台,然后把平台传输会设备。烧录器上面那个芯片就是我们的WI-FI模块
接下来我们主要详细讲将STM32和ESP8266模块这种搭配为例子
STM32+ESP8266+MQTT+华为云
这里参考一位大佬的博客,大家感兴趣可以去看下
https://blog.csdn.net/hao1__/article/details/121050775
首先我来说下这里完成的工作:
华为云
1、获取端口、域名、WI-FI、ClientId、Username、Password
2、自定义操作topic
STM32端
1、ST-Link仿真器、STM32、ESP8266硬件连接
2、配置、填写和写入topic
最后云端获取温湿度数据以及控制继电器开关
获取端口、域名、WI-FI、ClientId、Username、Password
1、WI-FI
这个只需要准备好你家的WI-FI名字和密码就可以
2、端口-域名
你根据下图的提示,在总览这里找到MQTT华为这端的域名和端口
3、ClientId、Username、Password
这三个需要进入到点击进入生产工具网址,进行三元组信息生产。
生成工具网址:
https://iot-tool.obs-website.cn-north-4.myhuaweicloud.com/
将我们在华为云的ID地址和密钥复制进去,将生成之后生成ClientId、Username、Password复制保存
自定义操作topic
我们上面已经接触过用topic进行数据的传输,这里我们自定义一个topic,跟着下面三张图的讲解就可以完成,但是还是要提醒一下,topic不可以直接用,而是把{ deviceId_ID}需要替换成自己的ID
ST-Link仿真器、STM32、ESP8266硬件连接
这个设备用的是stm32l,需要加装一个ST-Link,同时将ESP8266模块直接装入板子进行配置。一起来看下吧
一、材料准备
准备以下材料
将ESP8266模块插进去
将串口线一端插入到STM32L的USB口,另一端插入到电脑的USB口
连接ST-Link仿真器
用3条杜邦线接入STM32L的 DIO、GND、CLK中
另一头的杜邦线接入仿真器,仿真器USB接口接入电脑。
配置、填写和烧录
代码在QQ讨论群下载
131322621
配置ST-Link仿真器
打开代码,点击配置仿真器
填写
现在需要通讯的的那几个要素(端口、域名、WI-FI、ClientId、Username、Password )依次填入
填入自定义topic,来触发特定的指令,比如说传输温湿度、打开关闭
校验结果
写两个数据上传到华为云
你可以在特定的指令中下达我们特定的topic,然后开关继电器
数据格式问题
这里要强调数据格式问题,一个是JSON格式、一个是二进制数据流格式,在上面接入设备的时候你就会发现一个问题,如果不同格式的发送到华为云平台,虽然不会报错,但是只可以接收到一段完整的代码,而不能提取出其中的数据,所以正确的格式在多设备的数据处理中尤其重要。
这里有篇博客写的非常好,解决了ESP8266和STM32两个设备中的格式转化问题,下面它的链接:
https://blog.csdn.net/qq_44857700/article/details/112388324?utm_source=app&app_version=5.0.1&code=app_1562916241&uLinkId=usr1mkqgl919blen
实现方式
首先以这两个作为例子,剩下的不论是什么设备都是这流程:
1、安装当下环境的JSON解析库
2、串口接收的实现
3、JSON库对数据进行解析
4、JSON数据再处理打包发送
在这里ESP8266按装的Arduino的环境(ArduinoJson库),STM32按装的是Keil5的环境(Jansson库),这和上面那个例子是一样的,都是Keil环境。
串口接收的实现
在ESP8266程序中需要将usartEvent();函数放到loop()函数中。
/*串口数据接收*/
void usartEvent(){
comdata = "";
while (Serial.available())//时刻读取硬件串口数据
{
comdata = Serial.readStringUntil('\n');//从串口缓存区读取字符到一个字符串型变量,直至读完或遇到某终止字符。
UserData(comdata);//进行JOSN数据解析
}
while (Serial.read() >= 0){}//清除串口缓存
}
ArduinoJson库实现数据解析
/*数据解析{status:true}*/
void UserData(String content){
StaticJsonDocument<200> doc;//申请JSON解析空间
DeserializationError error = deserializeJson(doc,content);
if (error) {//解析错误
Serial.print(F("deserializeJson() failed: "));
return;
}
status= doc["status"];
}
ArduinoJson库实现数据的打包发送
参照ArduinoJson库的JsonGeneratorExample工程即可得到以下代码,当然也可采用serial.println()函数进行格式化输出。
StaticJsonDocument<200> doc;
doc["sensor"] = "gps";
doc["time"] = 1351824120;
serializeJsonPretty(doc, Serial);
STM32部分
STM32同ESP8266一样,我会从以下四部分介绍:①JSON解析库的安装②串口接收的实现③JSON库实现数据解析④JSON数据的打包发送
JSON解析库的安装
关于这个库还没有下载的小伙伴可以参照这个博客安装:
【STM32上使用JSON - 羊羊得亿 - 博客园】https://www.cnblogs.com/yangxuli/p/7885225.html
串口接收的实现
STM32的串口中断我才用的是串口空闲中断,空闲中断是接受数据后出现一个byte的高电平(空闲)状态,就会触发空闲中断。
代码实现如下:
需要定义的全局变量:u8 buf1_size = 0;//串口数据接收数量标记 bool data_change = 0;//串口接收完成/变化标志位
#include <stdarg.h>
#include <string.h>
void USART1_printf (char *fmt, ...){
char buffer[USART1_REC_LEN+1]; // 数据长度
u8 i = 0;
va_list arg_ptr;
va_start(arg_ptr, fmt);
vsnprintf(buffer, USART1_REC_LEN+1, fmt, arg_ptr);
while ((i < USART1_REC_LEN) && (i < strlen(buffer))){
USART_SendData(USART1, (u8) buffer[i++]);
while (USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET);
}
va_end(arg_ptr);
}
void USART1_Init(u32 bound){ //串口1初始化
//GPIO端口设置
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1|RCC_APB2Periph_GPIOA, ENABLE); //使能USART1,GPIOA时钟
//USART1_TX PA.9
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; //PA.9
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出
GPIO_Init(GPIOA, &GPIO_InitStructure);
//USART1_RX PA.10
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;//浮空输入
GPIO_Init(GPIOA, &GPIO_InitStructure);
//Usart1 NVIC 配置
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=3 ;//抢占优先级3
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3; //子优先级3
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道使能
NVIC_Init(&NVIC_InitStructure); //根据指定的参数初始化VIC寄存器
//USART 初始化设置
USART_InitStructure.USART_BaudRate = bound;//一般设置为9600;
USART_InitStructure.USART_WordLength = USART_WordLength_8b;//字长为8位数据格式
USART_InitStructure.USART_StopBits = USART_StopBits_1;//一个停止位
USART_InitStructure.USART_Parity = USART_Parity_No;//无奇偶校验位
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;//无硬件数据流控制
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; //收发模式
USART_Init(USART1, &USART_InitStructure); //初始化串口
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);//开启ENABLE/关闭DISABLE串口接收中断
USART_ITConfig(USART1, USART_IT_IDLE, ENABLE);//开启串口空闲中断
USART_Cmd(USART1, ENABLE); //使能串口
}
void USART1_IRQHandler(void){ //串口1中断服务程序(固定的函数名不能修改)
u8 clear = clear;
USART_ClearFlag(USART1,USART_FLAG_TC);
if(USART_GetITStatus(USART1,USART_IT_RXNE)!=Bit_RESET)//串口中断发生
{
if(data_change == 0)//重新接收
{
memset(USART1_RX_BUF,0,sizeof(USART1_RX_BUF));//清空整个接收数组
data_change = 1;//标志位拉高
}
USART1_RX_BUF[buf1_size++]=USART1->DR;
}
else if(USART_GetFlagStatus(USART1,USART_FLAG_IDLE)!=Bit_RESET)//空闲中断发生
{
buf1_size = 0;
data_change = 0;//标志位拉低,下次数据改变进入
data_sys = 1;//允许解析
clear = USART1->SR;//空闲中断要读这两个寄存器
clear = USART1->DR;
USART_ClearITPendingBit(USART1,USART_IT_IDLE);//清除空闲中断标志位
}
}
JSON库实现数据解析
JSON库解析需要调用头文件#include <jansson.h>,bool类型变量使用需要#include "stdbool.h"头文件。
并且非常重要的是,在对大量数据进行解析的时候,需要将startup_stm32f10x_md.s33的Stack_Size EQU 0x00000200修改为Stack_Size EQU 0x00000C00,这一步骤是将STM32的堆栈增加,防止在解析的时候出现堆栈不够用的情况。
#include <jansson.h>
bool led_status;
//开关灯JSON函数{"status":true}
//存在问题!!使用本函数解析后串口printf无法使用,建议使用USART1_printf函数实现发送
/**
* @brief Json解析函数
* @param none
* @return
* - 0 转换成功
* - 1 转换失败
* @details
*/
uint8_t Jansson_Analysis(char *text)
{
json_error_t error;
json_t *root;
root = json_loads((const char*)text, 0, &error);
if(json_is_object(root))
{
status = json_object_get(root, "status");
if(json_is_true(status))
led_status = 1;
else if(json_is_false(status))
led_status = 0;
}
else
{
USART1_printf("root format error:%d-%s\r\n", error.line, error.text);
return 1;
}
json_decref(root);//释放JSON空间
return 0;
}
/*
// string 类型的解析
name = (char *)json_string_value(json_object_get(root, "name"));
// int 类型的解析
age = json_integer_value(json_object_get(root, "age"));
// double 类型的解析
score = json_real_value(json_object_get(root, "score"));
// bool 类型的解析
status = json_object_get(root, "status");
if(json_is_true(status))
else if(json_is_false(status))
*/
JSON数据的打包发送
Jansson包提供了一个json数据打包的函数,但此函数在打包过程中会占用极大的片内空间,因此在这里仅介绍给大家,不推荐大家使用,推荐大家使用的方法还是使用printf函数进行格式化输出。
/**
* @brief 将数据打包为Json格式
* @param [in] status
* @return none
* @details
*/
void jansson_pack(bool state)
{
json_t *root;
char *out;
/* Build the JSON object */
root = json_pack("{sb}","status",status);
out = json_dumps(root, JSON_ENCODE_ANY);
printf("%s",out);
json_decref(root);//释放JSON空间
free(out);//释放JSON空间
}
/*
s 代表string类型
b 代表bool类型
d 代表int类型
f 代表float、double类型
*/
使用printf格式化输出只需要:
printf("{\"status\":%d}",status);
- 点赞
- 收藏
- 关注作者
评论(0)