解锁华为云新技能-AIOT开发全流程(2)【设备接入-ESP端侧数据收集[MQTT]-实时数据分析】(步步截图较详细)

举报
柯子翼 发表于 2022/01/29 19:33:10 2022/01/29
【摘要】 华为云-ESP8266-STM32-传感器结合以实现端侧数据收集[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);
【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。