本文主要涉及,c语言基础知识,嵌入式技术,STM32开发,Java全栈,Maven,小程序等技术的应用。
物联网(英文:Internet of Things,缩写:IoT)起源于传媒领域,是信息科技产业的第三次革命。物联网是指通过信息传感设备,按约定的协议,将任何物体与网络相连接,物体通过信息传播媒介进行信息交换和通信,以实现智能化识别、定位、跟踪、监管等功能。
物联网技术简单来说就是上传各类数据到应用层加以运用的技术。物联网项目根据物联网的四层架构可分为:物理层、传输层、平台层、应用层。三层结构分为:感知层、网络层和应用层。本文介绍各层的解决方案。本文从使用的角度介绍了各层技术,并且完成了一个简单的温湿度监管系统。
根据四层模型,各层描述了一些采用应用技术。架构图如1.1所示。
图1.1 物联网四层模型
物理层主要作用是数据获取,然后通过传输层将数据上传给物联网平台,供应用层使用。即物理层是数据的主要来源。
物理层通常由MCU、感知节点、执行节点构成。MCU相当于大脑,负责与各个节点交互,MCU可以是STM32、Arduino、ESP8266、ESP32、51单片机等控制器。感知节点及一些传感器,用于获取需要的数据,感知节点根据需求来确定。执行节点一般来说是继电器、风扇等设备。
这里只对STM32和Arduino进行介绍。
STM32是一款拥有ADC、PWM、中断、UART等功能的微控制器,主要使用C语言进行开发,入门推荐观看野火的教程,也视频版、也有图书。视频资源在B站可以观看【单片机】野火STM32F103教学视频(配套霸道/指南者/MINI)【全】(刘火良老师出品)(无字幕)_哔哩哔哩_bilibili】。书籍《零死角玩转STM32—F103指南者》可以配合视频使用。入门主要掌握对各个引脚的配置,例如配置引脚的输入输出模式,输出高低电平等,这些是比较重要的东西。要获取传感器的数据可以到网上自行查阅,大部的传感器都是有现成的程序的,需要做的是移植到对应的开发板。当然也可以根据芯片手册自己进行程序的编写,这需要一定的基础,初期可以不用到这个成都。然后便是学一些外设了,PWM、UART等等。
Arduino与STM32外设差不多,也拥有ADC、PWM、UART这些外设,不过Arduino相对来说比较简单,他是使用C++进行开发,也就是使用了面向对象的开发思想,很多传感器都有写好的库,使用非常方便。
下面使用代码对比一下两种MCU获取DHT11温湿度传感器值的差异。
STM32:
这是DHT11的头文件,定义了一些函数、常量
#ifndef __DHT11_H
#define __DHT11_H
#include "system.h"
#define BOOL unsigned char
#ifndef TRUE
#define TRUE 1
#endif
#ifndef FALSE
#define FALSE 0
#endif
//定义DHT11 GPIOD 0
#define DHT11_PORT_RCC RCC_APB2Periph_GPIOD
#define DHT11_PIN GPIO_Pin_0
#define DHT11_PORT GPIOD
static void DHT11_DataPin_Configure_Output(void);
static void DHT11_DataPin_Configure_Input(void);
BOOL DHT11_get_databit(void);
void DHT11_set_databit(BOOL level);
void mdelay(u16 ms);
void udelay(u16 us);
static uint8_t DHT11_read_byte(void);
static uint8_t DHT11_start_sampling(void);
void DHT11_get_data(u32 *buf);
#endif
这里是获取DHT11数据的具体的实现:
#include "dht11.h"
#include "SysTick.h"
/*数据定义:
---以下变量均为全局变量--------
//----温度高8位== U8T_data_H------
//----温度低8位== U8T_data_L------
//----湿度高8位== U8RH_data_H-----
//----湿度低8位== U8RH_data_L-----
//-----校验 8位 == U8checkdata-----
*/
u8 U8T_data_H,U8T_data_L,U8RH_data_H,U8RH_data_L,U8checkdata;
u8 U8T_data_H_temp,U8T_data_L_temp,U8RH_data_H_temp,U8RH_data_L_temp,U8checkdata_temp;
static void DHT11_DataPin_Configure_Output(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(DHT11_PORT_RCC, ENABLE); //使能PD端口时钟
GPIO_InitStructure.GPIO_Pin = DHT11_PIN; //PD.0 端口配置
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(DHT11_PORT, &GPIO_InitStructure);
}
static void DHT11_DataPin_Configure_Input(void)
{
GPIO_InitTypeDef DataPin;
DataPin.GPIO_Pin = DHT11_PIN;
DataPin.GPIO_Mode = GPIO_Mode_IN_FLOATING; //悬空 输入
DataPin.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(DHT11_PORT, &DataPin);
}
BOOL DHT11_get_databit(void)
{
uint8_t val;
val = GPIO_ReadInputDataBit(DHT11_PORT, DHT11_PIN);
if(val == Bit_RESET)
{
return FALSE;
}
else
{
return TRUE;
}
}
void DHT11_set_databit(BOOL level)
{
if(level == TRUE)
{
GPIO_SetBits(DHT11_PORT, DHT11_PIN);
}
else
{
GPIO_ResetBits(DHT11_PORT, DHT11_PIN);
}
}
void mdelay(u16 ms)
{
if(ms != 0)
{
delay_ms(ms);
}
}
void udelay(u16 us)
{
if(us != 0)
{
delay_us(us);
}
}
static uint8_t DHT11_read_byte(void)
{
uint8_t i;
uint8_t data = 0;
for(i = 0; i < 8; i++)
{
data <<= 1;
while((!DHT11_get_databit()));
udelay(10);
udelay(10);
udelay(10);
if(DHT11_get_databit())
{
data |= 0x1;
while(DHT11_get_databit());
}
else
{
}
}
return data;
}
static uint8_t DHT11_start_sampling(void)
{
DHT11_DataPin_Configure_Output();
//主机拉低18ms
DHT11_set_databit(FALSE);
mdelay(18);
DHT11_set_databit(TRUE);
//总线由上拉电阻拉高 主机延时20us
udelay(10);
udelay(10);
//主机设为输入 判断从机响应信号
DHT11_set_databit(TRUE);
DHT11_DataPin_Configure_Input();
//判断从机是否有低电平响应信号 如不响应则跳出,响应则向下运行
if(!DHT11_get_databit()) //T !
{
//判断从机是否发出 80us 的低电平响应信号是否结束
while((!DHT11_get_databit()));
// printf("DHT11 answers.\r\n");
//判断从机是否发出 80us 的高电平,如发出则进入数据接收状态
while((DHT11_get_databit()))
{}
return 1;
}
return 0;
}
void DHT11_get_data(u32 *buf)
{
u8 temp;
if(DHT11_start_sampling())
{
//printf("DHT11 is ready to transmit data\r\n");
//数据接收状态
U8RH_data_H_temp = DHT11_read_byte();
U8RH_data_L_temp = DHT11_read_byte();
U8T_data_H_temp = DHT11_read_byte();
U8T_data_L_temp = DHT11_read_byte();
U8checkdata_temp = DHT11_read_byte();
/* Data transmission finishes, pull the bus high */
DHT11_DataPin_Configure_Output();
DHT11_set_databit(TRUE);
//数据校验
temp=(U8T_data_H_temp+U8T_data_L_temp+U8RH_data_H_temp+U8RH_data_L_temp);
if(temp==U8checkdata_temp)
{
U8RH_data_H=U8RH_data_H_temp; //湿度
U8RH_data_L=U8RH_data_L_temp;
U8T_data_H=U8T_data_H_temp; //温湿
U8T_data_L=U8T_data_L_temp;
U8checkdata=U8checkdata_temp;
buf[0] = U8T_data_H; //温度的高低
buf[1] = U8T_data_L;
buf[2] = U8RH_data_H; //湿度高低
buf[3] = U8RH_data_L;
}
}
}
主函数调用:
#include "system.h"
#include "SysTick.h" //延时
#include "oled.h"
#include "dht11.h"
void dht11_show(u32 *buf); //oled显示
int main()
{
//显示温度和湿度的数组 元素0 为温度高 元素1 为温度低
// 元素2 为湿度高 元素3 为湿度的低
u32 DTH11_data[4]={0};
//时钟源获取 配置
SysTick_Init(72); //精确延时
OLED_Init();
OLED_ColorTurn(0);//0正常显示,1 反色显示
OLED_DisplayTurn(0);//0正常显示 1 屏幕翻转显示
while(1)
{
DHT11_get_data(DTH11_data); //获取到温湿度
dht11_show(DTH11_data);
delay_ms(5000);
OLED_ShowString(70,20," ",16,1);
OLED_ShowString(70,40," ",16,1);
OLED_Refresh();
}
}
再看Arduino获取DTH11的值。
#include //调用DHT库
DHT dht(D1,DHT11); //设置Data引脚所接IO口和传感器类型
void setup(){ //初始化函数,只在程序开始时运行一次
Serial.begin(115200); //设置串口波特率
dht.begin();
}
//https://blog.zeruns.tech
void loop() {
delay(1000); //延时1000毫秒
float RH = dht.readHumidity(); //读取湿度数据
float T = dht.readTemperature();//读取温度数据
Serial.print("Humidity:"); //向串口打印 Humidity:
Serial.print(RH); //向串口打印湿度数据
Serial.print("%");
Serial.print(" Temperature:");
Serial.print(T); //向串口打印温度数据
Serial.println("C");
Serial.println("https://blog.zeruns.tech");
}
可见Arduino使用库后是多么的简介。其实STM32使用DHT11.C和DHT11.H也相当于是库了,所以从使用的角度他们差距也不大。
感知节点是获取数据的传感器,要使传感器与MCU通信并且获取到数据,一般有两种做法,一是自己根据对应传感器的数据手册直接实现,二是在网上获取。一般来说,常见的传感器都是可直接找到资源的,这里推荐【GITEE】搜索,以DHT11为例,STM32版如图1.1所示。
图1.1 STM32获取DHT11搜索
使用GITEE搜索Arduino DTH11的结果如图1.2所示。
图1.2 Arduino获取DHT11搜索
执行节点通常是一些继电器,通过简单的输出高低电平来控制继电器的开关。也就是MCU通过控制引脚输出来实现控制。
传输层主要负责将数据传送至平台层,一般可将数据通过网络传输的模块都可以算作传输层的设备。数据通信方式如图3.1所示。
图3.1 物联网通信方式
使用移动网络技术的模块为NB-IOT,使用Wi-Fi技术的模块有ESP8266、ESP32等,使用以太网的模块W5500。这里只做简单介绍,根据不同的应用场景可自行了解相关技术。
在众多通信模块中,下面对使用Wi-Fi技术的ESP8266作简要介绍。
ESP8266是一个完整且自成体系的Wi-Fi网络解决方案,能够搭载软件应用,或通过另一个应用处理器卸载所有Wi-Fi网络功能。ESP8266在搭载应用并作为设备中唯一的应用处理器时,能够直接从外接闪存中启动。内置的高速缓冲存储器有利于提高系统性能,并减少内存需求
ESP8266 系列的特性:
32-bit MCU & 2.4 GHz Wi-Fi
单核 CPU 时钟频率高达 160 MHz
+19.5 dBm 天线端输出功率,确保良好的覆盖范围
睡眠电流小于 20 μA,适用于电池供电的可穿戴电子设备
外设包括 UART,GPIO,I2S,I2C,SDIO,PWM,ADC 和 SPI
ESP8266也有不同版本,这里选择安信可的ESP8266NODEMCU做介绍,NODEMCU版相当于物理层加传输层,适用于小型项目,这里只使用他的通信功能。
ESP8266作为一个MCU也有很多的开发方式,如基于官方SDK二次开发、AT固件、Lua、ArduinoIDE等等。从只使用其通信功能来讲,推荐使用AT固件方式开发。
AT指令是应用于终端设备与PC应用之间的连接与通信的指令。AT
即Attention。每个AT命令行中只能包含一条AT指令;对于AT指令的发送,除AT两个字符外,最多可以接收1056个字符的长度(包括最后的空字符)。
既然选择了AT固件方式开发,则需要让ESP8266支持AT功能,下面讲解一些烧录AT固件至ESP8266。
要使用AT固件则需要先获取,AT固件获取地址【ESP8266 SDK发布 | 安信可科技
(ai-thinker.com)】。推荐使用第4个,它支持MQTT。固件下载如图3.2所示。
在写本文时,在实际测试中发现现在该固件支持的是ESP-12S,而如果你是安信可NODEMCU版本则是ESP-12F,它们不兼容。所固件获取需要换个思路,不使用安信可提供的而是直接使用该芯片的设计商,乐鑫【提供 Wi-Fi、蓝牙芯片和 AIoT 解决方案 I 乐鑫科技
(espressif.com)】。以下内容可以略读,成功操作在本节重新烧录固件处,点击跳转->[1]。
3.2 AT固件下载
固件有了得需要烧录工具将它烧录到ESP8266,固件烧录工具下载地址【开发工具清单 |安信可科技(ai-thinker.com),同时也可将串口调试工具下载下来。烧录工具及串口调试工具下载如图3.3所示。
3.3 烧录工具及串口调试工具下载
固件和烧录工具都准备好了,接下来就是烧录了。首先准备好ESP8266、CH340模块、母对母杜邦线若干。根据ESP8266用户手册,查询下载模式接线方式。启动模式如图3.4所示。
图3.4 ESP8266启动模式
下载模式实际连线方式如3.5所示。
图3.5 实际连线图
连接好后将CH340与电脑相连接,可能会识别出来,需要安装CH340驱动,安装驱动可以看到如3.6图所示。
图3.6 识别到CH340
万事俱备了,打开烧录工具,打开如图3.7所示。
图3.7 打开烧录工具
打开烧录工具后,选择固件,如图3.8所示。
图3.8 选择固件
然后填写填上烧录地址填0,选中固件,SPI更改为DIO,FLASHSIZE改为32mbit,选择对应的串口,选择波特率为115200,点击开始。如果连线正确就可以会开始,等待完成即可,如果没有识别,则大概率是连线错误,请检查连线。烧录完成后如图3.9所示。
3.9 烧录完成
烧录完成后将启动模式更改为运行模式。拔掉IO0的杜邦线即可。如图3.10所示。
图3.10 运行模式连线图
断开杜邦线后,拔下CH340重新连接。接下来测试固件是否烧录成功。打开串口调试工具,选中对应串口,波特率115200,输入AT,选中自动换行,点击发送。如果成功会返回OK,如果失败会不显示,或显示乱码,按复位键也会显示乱码,失败如图3.11所示。
图3.11 烧录失败
第一遍烧录失败是较为正常的,解决方法是按照刚刚的步骤重新烧录一遍,特别注意IO0的杜邦线连接方式,并且重新烧录前需要关闭串口调试助手,不然会占用串口。
在几次实验后发现固件与芯片不符,不符合是因为安信可所提供的固件是他们个性化开发的,所以适配性不好。所以需要换个思路,直接找该芯片的提供商,也就是乐鑫,下面开始介绍。
首先是获取固件,获取地址【https://www.espressif.com/zh-hans/support/download/at】,选择ESP8266,点击即会跳转,如图3.12所示。
图3.12 乐鑫ESP8266 AT固件
选择ESP8266 AT binaries,如图3.13所示。
图3.13 ESP8266 AT Binaries
然后点击推荐的固件,点击下载,如图3.14所示。
图3.14 下载AT固件
解压后可以看到两个文件,其中ESP8266-IDF-AT Release Note
是新版本的一些说明,文件夹中factory的bin文件就是固件。解压后如图3.15所示。
图3.15 解压
下面进行烧录,CH340与ESP8266的连接方式可完全按上面来,这里还有一种简便方式,简单来说就是如果给ESP8266通电后大部分引脚默认就是高电平,所以根据下载模式只需要将IO0拉低就行了,这样三根杜邦线就可以完成。连接示意图如图3.16所示。
图3.16 连接示意图
连接好后打开下载工具,选择factory中的bin文件,地址填0,SPI模式选DIO,FLASH
SIZE根据实际芯片大小选择,ESP-12F选择32Mbit,具体选择如图3.17所示。
图3.17 烧录配置
点击Start后正常就会开始烧录了,如果没有可以按一下ESP8266的RST键。烧录完成后拔掉IO0,根据刚刚下载的压缩包中的手册如图3.18所示,可以知道得通过IO13和1O15来通讯。
图3.18 使用手册
运行模式连接示意图如图3.19所示。
图3.19 运行模式连接示意图
按以上连接方式连接后,打开串口调试工具,波特率选择115200,按下复位键,如果收到ready,这表示固件烧录完成。可以发送一些测试指令来测试固件是否能正常使用。正常运行如图3.20所示。
图3.20 正常运行效果
完成以上操作后,说明固件已经成功烧录完成。下面进行云平台连接,云平台有很多,目前有阿里云物联网平台,ONENET,百度物联网平台,还有开源物联网平台,还可以自己搭建平台,他们共性是都支持以MQTT协议为通信协议的平台,所以也可以自己搭建MQTT服务器。
对于MQTT协议可以自行查阅文档,它是一种发布/订阅模型,典型的应用有两种模式,一是,物理层控制传输层向平台的某个MQTT主题发布消息,这个消息可以是任意内容,一般是发送JSON格式串,这样平台层就获取到了物理层的数据了,然后可交由上层使用;二是,物联网层控制传输层订阅平台的某个主题,上层应用向这个主题发送指令,那么物理层就可解析这个指令做出相应的操作。这样就完成了数据的上传和指令的接受了。
这里选择阿里云物联网平台,阿里云物联网平台相对来说是更加麻烦的,不过学会它的连接后,使用其他平台也就没有问题了。
首先登陆阿里云物理平台,登录网址【https://iot.console.aliyun.com/】,登录后点击【公共实例】,点击产品,点击【创建产品】,名称随意,选择自定义产品,选择透传,选择免校验,选择设备密钥。然后点击完成。产品创建配置如图3.21所示。
图3.21 产品创建配置图
创建完成后点击查看产品详请,可以自定义一些topic。然后在该产品下创建设备,点击设备,添加设备,产品选择刚刚创建的产品,名称随意,备注随意,点击确定。配置如图3.22所示。
图3.22 设备配置
确定后,点击查看设备详情,可以看到如图3.23所示的信息。
图3.23 设备详情
点击如图DeviceSecret,可以获取到设备的三元组如图3.24所示。保持好三元组的信息。
图3.24 三元组
图3.25 官方文档
查看MQTT接入方式,网址如下,如果过期了可以自行按如图3.26所示查找。
【https://help.aliyun.com/document_detail/73742.html?spm=a2c4g.11186623.6.614.77e53d57dlMcmh】
图3.26 MQTT方式接入文档
公共实例的接入域名:
${YourProductKey}.iot-as-mqtt.${YourRegionId}.aliyuncs.com:1883。
其中:
${YourProductKey}:请替换为设备所属产品的ProductKey。
按照刚刚保存的三元组中的ProductKey替换字符串中的内容,地域查询如图3.27所示。
图3.27 地域查询
所以接入域名为:a1Wp5KdjrwG.iot-as-mqtt.cn-shanghai.aliyuncs.com:1883
接下来要拼接以下内容:
mqttClientId:
clientId+"|securemode=3,signmethod=hmacsha1,timestamp=132323232|"
mqttUsername: deviceName+"&"+productKey
mqttPassword: sign_hmac(deviceSecret,content)
mqttClientId:格式中| |内为扩展参数。
clientId:表示客户端ID,可自定义,长度不可超过64个字符。建议使用设备的MAC地址或SN码,方便您识别区分不同的客户端。
securemode:表示目前安全模式,可选值有2(TLS直连模式)和3(TCP直连模式)。
signmethod:表示签名算法类型。支持hmacmd5,hmacsha1和hmacsha256,默认为hmacmd5。
timestamp:表示当前时间毫秒值,可以不传递。
mqttPassword:sign签名需把提交给服务器的参数按字典排序后,根据signmethod加签。
content的值为提交给服务器的参数(ProductKey、DeviceName、timestamp和clientId),按照字母顺序排序,
然后将参数值依次拼接。通过源码分析得知,他是按clientId, deviceName,
productKey,timestamp这个顺序拼接的,并且是key+value的形式,具体实例见下文。
该设备的信息如下:
clientId = 2017519216(按推荐方式提供,也可自定义)
deviceName = the_device(来自于三元组),
productKey = a1Wp5KdjrwG(来自于三元组)
timestamp (不用)
signmethod=hmacsha1,(加密方式)
deviceSecret= a0634c8e595544fa38652287cc981bf9,(来自于三元组)
使用TCP方式提交
则对应的信息为:
mqttClientId: ”2017519216|securemode=3,signmethod=hmacsha1|”
mqttUsername: ”the_device& a1Wp5KdjrwG”
这里对于密码获取简单介绍一下,密码应该使用一个加密函数通过
deviceSecert与Content进行加密,加密函数有很多种(支持hmacmd5,hmacsha1和hmacsha256,),可以使用在线工具的方式进行加密,也可以使用单片机实现加密算法,阿里云提供了加密函数的C语言实现。这里先使用在线加密的方式进行加密。
拼接Content:
”clientId2017519216deviceNamethe_deviceproductKeya1Wp5KdjrwG”,没有时间戳不写,这是要加密的内容,加密过程如图3.28所示。
图3.28 在线加密
加密结果:1c1ea606f217a89fa1cc1ac9f97802b009a993db
至此需要的数据都准备好了。汇总一下:
接入域名 | a1Wp5KdjrwG.iot-as-mqtt.cn-shanghai.aliyuncs.com:1883 |
---|---|
mqttClientId | 2017519216|securemode=3,signmethod=hmacsha1| |
mqttUsername | the_device&a1Wp5KdjrwG |
mqttPassword | 1c1ea606f217a89fa1cc1ac9f97802b009a993db |
经过上面数据准备,可以进行连接云平台了,连接平台大致为以下步骤,连接wifi,配置MQTT信息,连接云平台,发送就收消息,具体流程如图3.29所示。
图3.29 SP8266使用流程图
根据流程图的描述,首先是连接Wi-Fi,所以需要查阅与连接Wi-Fi有关的AT指令,AT指令文档在乐鑫官网文档中可以看到。【https://docs.espressif.com/projects/esp-at/zh_CN/latest/AT_Command_Set/index.html】。也可在提供的文件夹中找到。
与Wi-Fi相关的AT指令
• AT+CWMODE:查询/设置 Wi-Fi 模式 (Station/SoftAP/Station+SoftAP)
• AT+CWJAP:连接 AP
• AT+CWRECONNCFG:查询/设置 Wi-Fi 重连配置
• AT+CWLAPOPT:设置AT+CWLAP 命令扫描结果的属性
• AT+CWLAP:扫描当前可用的 AP
• AT+CWQAP:断开与 AP 的连接
• AT+CWSAP:配置 ESP32 SoftAP 参数
• AT+CWLIF:查询连接到 ESP SoftAP 的 station 信息
• AT+CWQIF:断开 station 与 ESP SoftAP 的连接
• AT+CWDHCP:启用/禁用 DHCP
• AT+CWDHCPS:查询/设置 ESP SoftAP DHCP 分配的 IP 地址范围
• AT+CWAUTOCONN:上电是否自动连接 AP
…
连接Wi-Fi步骤:
AT+CWMODE=1 |
---|
AT+CWJAP=“FAST”,“12345789rg” |
---|
Wi-Fi连接成功如图3.30所示。
图3.30 连接Wi-Fi成功
与MQTT相关的AT指令:
• AT+MQTTUSERCFG:设置 MQTT 用户属性
•AT+MQTTCONNCFG:设置 MQTT 连接属性
•AT+MQTTPUB:发布 MQTT 消息(字符串)
•AT+MQTTSUB:订阅 MQTT Topic
•AT+MQTTCONN:连接 MQTT Broker
•AT+MQTTUNSUB:取消订阅 MQTT Topic
•AT+MQTTCLEAN:断开 MQTT 连接
命令说明请查阅文档。
MQTT使用:
AT+MQTTUSERCFG=0,1,"2017519216\|securemode=3\\,signmethod=hmacsha1\|","the_device&a1Wp5KdjrwG","1c1ea606f217a89fa1cc1ac9f97802b009a993db",0,0,""
AT+MQTTCONNCFG=0,300,0,"","",0,0
连接服务器
AT+MQTTCONN=0,"a1Wp5KdjrwG.iot-as-mqtt.cn-shanghai.aliyuncs.com",1883,0
连接成功如图3.31,3.32所示。
图3.31 连接服务器成功
图3.32 连接服务器成功
如果没有连接成功,就是参数有错误,所有的符号都应该为英文符号。以上过程,在做下一步前每一步都是成功的,即返回的是OK。
发送数据需要对某个topic发送,产品中可以创建topic,设备中可以查看有哪些topic。这里找一个topic。topic如图3.33所示。
图3.33 topic列表
Topic:/a1Wp5KdjrwG/the_device/user/update
Data:”{
\\“temperature\\”:36.5\\,
\\“humidity\\”:50
}”
AT+MQTTPUB=0,"/a1Wp5KdjrwG/the_device/user/update","{\\"temperature\\":36.5\\,\\"humidity\\":50}",0,0
发送成功串口助手打印ok,如图3.34所示。在阿里云物联网平台的日志服务中可以查看该条数据。
图3.34 串口发布消息成功
图3.35 云平台收到数据
接收数据前得订阅一个有订阅权限的topic。
Topic:a1Wp5KdjrwG/the_device/user/get
AT+MQTTSUB=0,"a1Wp5KdjrwG/the\_device/user/get",0
订阅成功串口助手显示OK,在平台设备中可以看到订阅的Topic,向该Topic发送消息,ESP8266可接收到数据,如图3.36所示。
图3.36 订阅主题成功
点击发布消息发布{“operation”:“close”},ESP8266收到后使用串口打印该条消息,如图3.37所示。
图3.37 ESP8266 接收到消息
至此使用串口连接平台就完成了,下面将介绍STM32控制ESP8266连接平台。
有了上面通过串口连接平台的经验,使用STM32连接就较为容易了,与使用串口连接的区别就在于,发送各种指令的不再是我们手动发送了,而是使用STM32来依次发送。根据这个原理,找一个STM32
UART工程,在它的基础上就可实现使用STM32连接云平台,安信可提供了现成的AT
STM32工程【https://docs.ai-thinker.com/esp8266/examples/at_demo】,当然也可以自己手动封装。下载工程项目如图3.38所示。
图3.38 STM32 AT工程
打开ESP8266->Project
中的项目文件,打开工程后,需要与自己的开发板做一个适配,由于本项目使用的是STM32F103ZET6,所以得删除原先的启动文件然后添加新的启动文件如图3.39所示,修改芯片型号如图3.40所示。
图3.39 修改启动文件
图3.40 修改芯片型号
修改宏定义STM32F10X_MD为STM32F10X_HD,具体操作如图3.41所示。
图3.41 修改宏定义
简单分析工程可知,该工程使用UART2与ESP8266交互,使用UART3作为串口打印,UART2收到的数据都会转发给UART3。也可根据需求自己修改对应的UART。STM32与ESP8266按照工程默认的连接方式如表3.1所示。
表3.1 STM32与ESP8266连接表
STM32引脚 | ESP8266引脚 |
---|---|
A2 | IO13 |
A3 | IO15 |
STM32与CH340默认连接方式如表3.2所示。
表3.2 STM32与CH340连接表
STM32引脚 | CH340引脚 |
---|---|
B10 | RXD |
B11 | TXD |
连接完成后需要对连接信息进行一个配置,主要修改mqtt.h这个文件,根据上节的串口连接云平台生成的连接信息配置。配置如图3.42所示。
图3.42 连接信息配置
需要注意的是User_ESP8266_client_id中“,”需要在前面加两个反斜杠,这是通过测试得到的结论。Main函数做如图3.43所示的修改。
图3.43 main函数修改
完成上述修改后即可编译代码,将程序下载到开发板了。打开串口调试助手发现卡在了配置MQTT信息,如图3.44所示
图3.44 配置MQTT信息
经过分析发现是因为安信可所提供代码有点问题,在esp8266.c中cmd数组给得太小了,所以出了问题,解决方法就是增大数组,将120改为300,不止该函数,下面的几个函数中的cmd都得增加。如图3.45所示。
图3.45 增加cmd数组大小
其中ESP8266_SendString()函数中数组大小只有20,应该它是问题所在,改它的大小为200.如图3.46所示。
图3.46 修改数组大小
为了能发送和接收消息,需要提供两个topic。在mqtt.h中添加User_ESP8266_MQTTServer_Topic_Send和User_ESP8266_MQTTServer_Topic_Recv两个topic,如图3.47所示。
图3.47 添加TOPIC
修改完后还需要修改mqtt.c中订阅和发布的主题,修改如图3.48所示。
图3.48 修改发布和订阅的主题
重新下载重新后,通过串口可以看到正常发送了,在物联网平台日志服务中可以看到上传的消息,如图3.49所示。
图3.49 STM32上传数据
至此就完成了STM32连接物联网平台,当然后续还有一些需要完善的地方,比如接收到的数据解析,如何给ESP8266配网等问题。
云平台种类十分多;仅国内就有如下平台:
中国电信的天翼物联
https://www.ctwing.cn/
中国联通的物联网平台
https://www.10646.cn/
中国移动的ONENet
https://open.iot.10086.cn/
百度天工
https://cloud.baidu.com/solution/iot/index.html
阿里云物联网
https://iot.aliyun.com/
腾讯IoT Explorer
https://cloud.tencent.com/product/iotexplorer
腾讯QQ物联
https://iot.open.qq.com/
京东微联
https://smartcloud.jd.com/
京东小京鱼
https://jdwhale.jd.com/
京东智联云
https://www.jdcloud.com/cn/iot/all
小米IoT开发者平台
https://iot.mi.com/
华为云IoT
https://www.huaweicloud.com/product/iot.html
浪潮云IoT
https://cloud.inspur.com/product/iotdm/
新华三绿洲平台
https://iot-developer.h3c.com/
庆科云FogCloud
https://v2.fogcloud.io/
中消云
http://www.zxycloud.com/
涂鸦智能
https://www.tuya.com/cn/
机智云
https://www.gizwits.com/
云智易
http://www.xlink.cn/
青云
https://sw.qingcloud.com/internet_of_things
智城云
https://www.zcyun.cn/
氦氪云
https://www.hekr.me/
Ablecloud
http://www.ablecloud.com/
Yeelink
http://www.yeelink.net/
艾拉物联
https://www.ayla.com.cn/
云平台作为一个数据中转站,本节主要介绍应用层如何与平台层交互,主要基于阿里云物联网平台。阿里云物联网平台可以直接通过平台生成web页面,移动应用等服务,可自行了解。作为后台服,本文将使用SpringBoot框架来开发。
在创建SpringBoot工程前,在阿里云物联网平台的产品下再创建一个设备,具体步骤见上一章。创建完成后保存三元组。创建完成如图4.1所示。
图4.1 创建应用层设备
连接阿里云的参数需要进行加密,官方有提供加密工具。
官方文档【https://help.aliyun.com/document_detail/146631.html?spm=a2c4g.11186623.6.1037.5ea825930TNG2r】。最终版工程见提供的文件。
首先创建一个maven工程,pom.xml:
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<groupId>org.examplegroupId>
<artifactId>DHT11_SupportartifactId>
<version>1.0-SNAPSHOTversion>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.3.5.RELEASEversion>
parent>
<properties>
<maven.compiler.source>8maven.compiler.source>
<maven.compiler.target>8maven.compiler.target>
<java.version>1.8java.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-jdbcartifactId>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
<version>2.0.1version>
dependency>
<dependency>
<groupId>tk.mybatisgroupId>
<artifactId>mapper-spring-boot-starterartifactId>
<version>2.1.5version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-integrationartifactId>
dependency>
<dependency>
<groupId>org.springframework.integrationgroupId>
<artifactId>spring-integration-streamartifactId>
dependency>
<dependency>
<groupId>org.springframework.integrationgroupId>
<artifactId>spring-integration-mqttartifactId>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>fastjsonartifactId>
<version>1.2.58version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
<version>2.2.6.RELEASEversion>
plugin>
plugins>
build>
<description/>
<developers>
<developer/>
developers>
<licenses>
<license/>
licenses>
<scm>
<url/>
scm>
<url/>
project>
下载好依赖后,创建启动类。
package com.dht11;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class,args);
}
}
创建完启动类后,在resource下创建配置文件application.yml。
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/dht11?characterEncoding=utf8&useSSL=false&serverTimezone=UTC&rewriteBatchedStatements=true
username: root
password: root
Aliyun:
#阿里云三元组
productKey: a1Wp5KdjrwG
deviceName: application
deviceSecret: 75cb6106327998fc73d80a49d890e427
#连接host
host: tcp://a1Wp5KdjrwG.iot-as-mqtt.cn-shanghai.aliyuncs.com:1883
default:
topic_send: /a1Wp5KdjrwG/application/user/update
topic_recv: /a1Wp5KdjrwG/application/user/get
配置mqtt,仍有bug,应该是发送和接受不能使用同一个clientId,不过不影响使用。
package com.dht11.config;
import com.dht11.util.MqttSign;
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.integration.annotation.ServiceActivator;
import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.core.MessageProducer;
import org.springframework.integration.mqtt.core.DefaultMqttPahoClientFactory;
import org.springframework.integration.mqtt.core.MqttPahoClientFactory;
import org.springframework.integration.mqtt.inbound.MqttPahoMessageDrivenChannelAdapter;
import org.springframework.integration.mqtt.outbound.MqttPahoMessageHandler;
import org.springframework.integration.mqtt.support.DefaultPahoMessageConverter;
import org.springframework.integration.mqtt.support.MqttHeaders;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageHandler;
import org.springframework.messaging.MessagingException;
@Configuration
public class MqttConfig {
@Value("${Aliyun.host}")
private String mqttHost;
@Value("${Aliyun.productKey}")
private String productKey;
@Value("${Aliyun.deviceName}")
private String deviceName;
@Value("${Aliyun.deviceSecret}")
private String deviceSecret;
@Value("${Aliyun.default.topic_send}")
private String sendTopic;
@Value("${Aliyun.default.topic_recv}")
private String recvTopic;
MqttSign signSend = new MqttSign();
MqttSign signRecv = new MqttSign();
private static final Logger logger = LoggerFactory.getLogger(MqttConfig.class);
private MqttConfig mqttClientConfig;
/**
* 订阅的bean名称
*/
public static final String CHANNEL_NAME_IN = "mqttInboundChannel";
/**
* 发布的bean名称
*/
public static final String CHANNEL_NAME_OUT = "mqttOutboundChannel";
/**
* 构造一个默认的mqtt客户端操作bean
*
* @return
*/
public MqttPahoClientFactory mqttClientFactory(MqttSign mqttSign,String id) {
mqttSign.calculate(productKey,deviceName,deviceSecret,id);
DefaultMqttPahoClientFactory factory = new DefaultMqttPahoClientFactory();
MqttConnectOptions options = new MqttConnectOptions();
options.setCleanSession(true);
options.setKeepAliveInterval(180);
options.setUserName(mqttSign.getUsername());
options.setPassword(mqttSign.getPassword().toCharArray());
options.setServerURIs(new String[]{this.mqttHost});
factory.setConnectionOptions(options);
return factory;
}
/**
* 构造一个默认的mqtt客户端操作bean
*
* @return
*/
//发送通道
@Bean(name = CHANNEL_NAME_OUT)
public MessageChannel mqttOutboundChannel() {
return new DirectChannel();
}
@Bean
@ServiceActivator(inputChannel = CHANNEL_NAME_OUT)
public MessageHandler MqttOutbound(){
MqttPahoClientFactory send = mqttClientFactory(signSend,"send");
MqttPahoMessageHandler messageHandler = new MqttPahoMessageHandler(signSend.getClientId(),send);
messageHandler.setAsync(true);
messageHandler.setDefaultTopic(this.sendTopic);
return messageHandler;
}
//接收通道
@Bean(name = CHANNEL_NAME_IN)
public MessageChannel mqttInboundChannel() {
return new DirectChannel();
}
//配置client,监听的topic
@Bean
public MessageProducer inbound() {
MqttPahoClientFactory recv = mqttClientFactory(signRecv,"recv");
MqttPahoMessageDrivenChannelAdapter adapter =
new MqttPahoMessageDrivenChannelAdapter( signRecv.getClientId(),recv,this.recvTopic);
adapter.setCompletionTimeout(5000);
adapter.setConverter(new DefaultPahoMessageConverter());
adapter.setQos(1);
adapter.setOutputChannel(mqttInboundChannel());
return adapter;
}
@Bean
@ServiceActivator(inputChannel =CHANNEL_NAME_IN)
public MessageHandler handler() {
return new MessageHandler() {
//消息消费
@Override
public void handleMessage(Message<?> message) throws MessagingException {
String topic = String.valueOf(message.getHeaders().get(MqttHeaders.RECEIVED_TOPIC));
String payload = String.valueOf(message.getPayload());
logger.info("接收到 mqtt消息,主题:{} 消息:{}", topic, payload);
}
};
}
}
配置发送消息的网关。
package com.dht11.gateway;
import com.dht11.config.MqttConfig;
import org.springframework.integration.annotation.MessagingGateway;
import org.springframework.integration.mqtt.support.MqttHeaders;
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.stereotype.Component;
@Component
@MessagingGateway(defaultRequestChannel = MqttConfig.CHANNEL_NAME_OUT)
public interface MqttGateway {
/**
* 发送信息到MQTT服务器
*
* @param data 发送的文本
*/
void sendToMqtt(String data);
/**
* 发送信息到MQTT服务器
*
* @param topic 主题
* @param payload 消息主体
*/
void sendToMqtt(@Header(MqttHeaders.TOPIC) String topic,
String payload);
/**
* 发送信息到MQTT服务器
*
* @param topic 主题
* @param qos 对消息处理的几种机制。
0 表示的是订阅者没收到消息不会再次发送,消息会丢失。
* 1 表示的是会尝试重试,一直到接收到消息,但这种情况可能导致订阅者收到多次重复消息。
* 2 多了一次去重的动作,确保订阅者收到的消息有一次。
* @param payload 消息主体
*/
void sendToMqtt(@Header(MqttHeaders.TOPIC) String topic,
@Header(MqttHeaders.QOS) int qos,
String payload);
}
编写发送消息的Controller。
package com.dht11.controller;
import com.dht11.gateway.MqttGateway;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class PublishMessage {
@Autowired
MqttGateway mqttGateway;
@RequestMapping("/send/{msg}")
public String send(@PathVariable("msg") String msg ){
mqttGateway.sendToMqtt(msg);
return "send "+ msg + " success";
}
}
向平台发送消息,如图4.2所示。
图4.2 发送消息到云平台
使用云平台向应用层发送数据,如图4.3所示。
图4.3 云平台发送数据到应用层
以上为平台层与应用层数据交互的介绍。
应用层负责数据存储、分析,指令下发等等操作,同时应该能为各种服务提供接口,由于物理层的设备上传的数据是实时的,可能会有大量数据,使用可能会使用大数据技术。本文为了能获取实时数据,将使用Redis来储存数据。同时可能需要对历史数据进行存储,需要使用MySQL数据库。并且需要为不同对象提供服务,如小程序、手机APP、web等等。
以物理层为DHT11上传数据为例,数据库表的设计如表5.1所示。
表5.1 DHT11实体表
列名 | 数据类型 | 主键 | 必填字段 | 备注 |
---|---|---|---|---|
Id | 整型 | 是 | 是 | 自增 |
Temperature | 浮点型 | 否 | 否 | |
Humidity | 浮点型 | 否 | 否 | |
Time | 长整型 | 否 | 否 |
以下操作以第4章的SpringBoot项目基础,创建完数据库实体表后,创建对应的pojo,注意得安装lombok插件。
import lombok.Data;
import javax.persistence.Column;
import javax.persistence.Id;
@Data
@Table(name = "dht11_msg")
public class DHT11 {
@Id
@Column(name = "`id`")
private Long id;
private Float temperature;
private Float humidity;
private Long time;
}
Dao编写
package com.dht11.dao;
import com.dht11.pojo.DHT11;
import org.springframework.stereotype.Repository;
import tk.mybatis.mapper.common.Mapper;
@Repository
public interface DHT11Dao extends Mapper<DHT11> {
}
service编写
import com.dht11.pojo.DHT11;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class DHT11Service {
@Autowired
private DHT11Dao dht11Dao;
public List<DHT11> getAll(){
return dht11Dao.selectAll();
}
}
Controller编写
package com.dht11.controller;
import com.dht11.pojo.DHT11;
import com.dht11.service.DHT11Service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import java.util.List;
@RestController
public class DHT11Controller {
@Autowired
private DHT11Service dht11Service;
@RequestMapping("/getAll")
public List<DHT11> getAll(){
return dht11Service.getAll();
}
}
运行测试如图5.3所示。
图5.3 运行测试图
较为关键的是redis的整合,首先导入依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
在application.yml中配置redis信息
spring:
redis:
host: 127.0.0.1
port: 6379
创建redis配置文件
package top.raogang.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
@Bean
@SuppressWarnings("all")
public RedisTemplate<String,Object> getRedisTemplate(RedisConnectionFactory factory){
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
//json序列化配置
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
//string 序列化配置
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
//序列化方式配置
template.setKeySerializer(stringRedisSerializer);
template.setHashKeySerializer(stringRedisSerializer);
template.setValueSerializer(jackson2JsonRedisSerializer);
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();;
return template;
}
}
编写redis utils
package com.dht11.util;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
@Component
public final class RedisUtil {
@Autowired
@Qualifier("getRedisTemplate")
private RedisTemplate<String, Object> redisTemplate;
// =============================common============================
/**
* 指定缓存失效时间
* @param key 键
* @param time 时间(秒)
* @return
*/
public boolean expire(String key, long time) {
try {
if (time > 0) {
redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据key 获取过期时间
* @param key 键 不能为null
* @return 时间(秒) 返回0代表为永久有效
*/
public long getExpire(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
/**
* 判断key是否存在
* @param key 键
* @return true 存在 false不存在
*/
public boolean hasKey(String key) {
try {
return redisTemplate.hasKey(key);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除缓存
* @param key 可以传一个值 或多个
*/
@SuppressWarnings("unchecked")
public void del(String... key) {
if (key != null && key.length > 0) {
if (key.length == 1) {
redisTemplate.delete(key[0]);
} else {
redisTemplate.delete(CollectionUtils.arrayToList(key));
}
}
}
// ============================String=============================
/**
* 普通缓存获取
* @param key 键
* @return 值
*/
public Object get(String key) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}
/**
* 普通缓存放入
* @param key 键
* @param value 值
* @return true成功 false失败
*/
public boolean set(String key, Object value) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 普通缓存放入并设置时间
* @param key 键
* @param value 值
* @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
* @return true成功 false 失败
*/
public boolean set(String key, Object value, long time) {
try {
if (time > 0) {
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
} else {
set(key, value);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 递增
* @param key 键
* @param delta 要增加几(大于0)
* @return
*/
public long incr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递增因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, delta);
}
/**
* 递减
* @param key 键
* @param delta 要减少几(小于0)
* @return
*/
public long decr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递减因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, -delta);
}
// ================================Map=================================
/**
* HashGet
* @param key 键 不能为null
* @param item 项 不能为null
* @return 值
*/
public Object hget(String key, String item) {
return redisTemplate.opsForHash().get(key, item);
}
/**
* 获取hashKey对应的所有键值
* @param key 键
* @return 对应的多个键值
*/
public Map<Object, Object> hmget(String key) {
return redisTemplate.opsForHash().entries(key);
}
/**
* HashSet
* @param key 键
* @param map 对应多个键值
* @return true 成功 false 失败
*/
public boolean hmset(String key, Map<String, Object> map) {
try {
redisTemplate.opsForHash().putAll(key, map);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* HashSet 并设置时间
* @param key 键
* @param map 对应多个键值
* @param time 时间(秒)
* @return true成功 false失败
*/
public boolean hmset(String key, Map<String, Object> map, long time) {
try {
redisTemplate.opsForHash().putAll(key, map);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
* @param key 键
* @param item 项
* @param value 值
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value) {
try {
redisTemplate.opsForHash().put(key, item, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
* @param key 键
* @param item 项
* @param value 值
* @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value, long time) {
try {
redisTemplate.opsForHash().put(key, item, value);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除hash表中的值
* @param key 键 不能为null
* @param item 项 可以使多个 不能为null
*/
public void hdel(String key, Object... item) {
redisTemplate.opsForHash().delete(key, item);
}
/**
* 判断hash表中是否有该项的值
* @param key 键 不能为null
* @param item 项 不能为null
* @return true 存在 false不存在
*/
public boolean hHasKey(String key, String item) {
return redisTemplate.opsForHash().hasKey(key, item);
}
/**
* hash递增 如果不存在,就会创建一个 并把新增后的值返回
* @param key 键
* @param item 项
* @param by 要增加几(大于0)
* @return
*/
public double hincr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, by);
}
/**
* hash递减
* @param key 键
* @param item 项
* @param by 要减少记(小于0)
* @return
*/
public double hdecr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, -by);
}
// ============================set=============================
/**
* 根据key获取Set中的所有值
* @param key 键
* @return
*/
public Set<Object> sGet(String key) {
try {
return redisTemplate.opsForSet().members(key);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 根据value从一个set中查询,是否存在
* @param key 键
* @param value 值
* @return true 存在 false不存在
*/
public boolean sHasKey(String key, Object value) {
try {
return redisTemplate.opsForSet().isMember(key, value);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将数据放入set缓存
* @param key 键
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSet(String key, Object... values) {
try {
return redisTemplate.opsForSet().add(key, values);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 将set数据放入缓存
* @param key 键
* @param time 时间(秒)
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSetAndTime(String key, long time, Object... values) {
try {
Long count = redisTemplate.opsForSet().add(key, values);
if (time > 0)
expire(key, time);
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 获取set缓存的长度
* @param key 键
* @return
*/
public long sGetSetSize(String key) {
try {
return redisTemplate.opsForSet().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 移除值为value的
* @param key 键
* @param values 值 可以是多个
* @return 移除的个数
*/
public long setRemove(String key, Object... values) {
try {
Long count = redisTemplate.opsForSet().remove(key, values);
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
// ===============================list=================================
/**
* 获取list缓存的内容
* @param key 键
* @param start 开始
* @param end 结束 0 到 -1代表所有值
* @return
*/
public List<Object> lGet(String key, long start, long end) {
try {
return redisTemplate.opsForList().range(key, start, end);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 获取list缓存的长度
* @param key 键
* @return
*/
public long lGetListSize(String key) {
try {
return redisTemplate.opsForList().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 通过索引 获取list中的值
* @param key 键
* @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
* @return
*/
public Object lGetIndex(String key, long index) {
try {
return redisTemplate.opsForList().index(key, index);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 将list放入缓存
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
*/
public boolean lSet(String key, Object value) {
try {
redisTemplate.opsForList().rightPush(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
*/
public boolean lSet(String key, Object value, long time) {
try {
redisTemplate.opsForList().rightPush(key, value);
if (time > 0)
expire(key, time);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
*/
public boolean lSet(String key, List<Object> value) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
*/
public boolean lSet(String key, List<Object> value, long time) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
if (time > 0)
expire(key, time);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据索引修改list中的某条数据
* @param key 键
* @param index 索引
* @param value 值
* @return
*/
public boolean lUpdateIndex(String key, long index, Object value) {
try {
redisTemplate.opsForList().set(key, index, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 移除N个值为value
* @param key 键
* @param count 移除多少个
* @param value 值
* @return 移除的个数
*/
public long lRemove(String key, long count, Object value) {
try {
Long remove = redisTemplate.opsForList().remove(key, count, value);
return remove;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
}
编写controller
@RestController
public class DHT11Controller {
@Autowired
private DHT11Service dht11Service;
@Autowired
private RedisUtil redisUtil;
@RequestMapping("/getAll")
public List<DHT11> getAll(){
return dht11Service.getAll();
}
@RequestMapping("/getRealtimeData")
public Data getRealtimeData(){
Object temperature = redisUtil.get("temperature");
Object humidity = redisUtil.get("humidity");
Object beep = redisUtil.get("beep");
Data data = new Data();
data.setTemperature((Double) temperature);
data.setHumidity((Double) humidity);
data.setBeep((Integer) beep);
return data;
}
}
接口设计完成,接下来是连通物理层到应用层。首先物理层需要按指定格式上传数据,以便于上传解析,物理层将DHT11作为感知节点,蜂鸣器作为执行节点,上传数据做如下定义:
DHT11及beep移植可自行查阅。
{
”temperature”:23.9,
”humidity”:46.6,
”beep”:0
}
关键代码
typedef struct data{
float temperature;
float humidity;
int beep;
}Data;
char * toDataString(float temp,float humi, int beep){
char str[100];
memset(str,0,sizeof(str));
sprintf(str,"{\\'temperature\\':%.2f\\,\\'humidity\\':%.2f\\,\\'beep\\':%d}",temp,humi,beep);
return str;
}
int main(void){
DHT11_Data_TypeDef DHT11_Data;
Data data = {0};
delay_init();
uart3_Init(115200);
ESP8266_Init(115200);
BEEP_GPIO_Config();
DHT11_Init();
ESP8266_STA_MQTTClient_Test();//测试MQTT通讯
BEEP(ON);
delay_ms(1000);
BEEP(OFF);
delay_ms(500);
while(1)
{
/*调用DHT11_Read_TempAndHumidity读取温湿度,若成功则输出该信息*/
if( DHT11_Read_TempAndHumidity ( & DHT11_Data ) == SUCCESS)
{
printf("\r\n读取DHT11成功!\r\n\r\n湿度为%d.%d %RH ,温度为 %d.%d℃ \r\n",\
DHT11_Data.humi_int,DHT11_Data.humi_deci,DHT11_Data.temp_int,DHT11_Data.temp_deci);
data.humidity = DHT11_Data.humi_int + DHT11_Data.humi_deci*1.0/10;
data.temperature = DHT11_Data.temp_int + DHT11_Data.temp_deci*1.0/10;
}
else
{
printf("Read DHT11 ERROR!\r\n");
}
data.beep = readBeepStatus();
printf("temp:%.2f, humi:%.2f, beep:%d\n",data.temperature,data.humidity,data.beep);
delay_ms(1000);
MQTT_SendString (User_ESP8266_MQTTServer_Topic_Send,toDataString(data.temperature,data.humidity,data.beep));//发送数据到MQTT服务器
}
}
物理层的数据上传至平台后通过规则引擎可以将物理设备的数据转发给应用层使用,在阿里物联网平台的规则引擎中选择云产品流转,点击创建规则,创建完成如图5.4所示。
图5.4 创建规则引擎
点击编写SQL语句,通过SQL语句获取到temperature,humidity,beep这几个数据,如图5.5所示。
图5.5 编写SQL语句
点击添加操作,将数据转发给应用层topic,注意是物理层的update到应用层的get,如图5.6所示。
图5.6 添加转发操作
同样的再创建一个用于转发应用层的update到物理层的get,应用层发往物理层的key统一为operation,如图5.8所示。
图5.8 应用层转发给物理层
创建完成,点击启动,如图5.9.
图5.9 启动规则引擎
启动SpringBoot项目,打开开发板,测试是否连通,连通效果成功,控制台收到物理层数据如图5.10,使用web发送数据串口打印如图5.11所示。
图5.10 操作台输出
图5.11 串口输出
为了能获取实时数据将数据存入redis,在消息解析处做如下更改:
@Bean
@ServiceActivator(inputChannel =CHANNEL_NAME_IN)
public MessageHandler handler() {
return new MessageHandler() {
//消息消费
@Override
public void handleMessage(Message<?> message) throws MessagingException {
String topic = String.valueOf(message.getHeaders().get(MqttHeaders.RECEIVED_TOPIC));
String payload = String.valueOf(message.getPayload());
logger.info("接收到 mqtt消息,主题:{} 消息:{}", topic, payload);
Data data = JSON.parseObject(payload, Data.class);//转化为data object
redisUtil .set("temperature",data.getTemperature());
redisUtil.set("humidity",data.getHumidity());
redisUtil.set("beep",data.getBeep());
}
};
}
这样通过getRealtimeData就能实时获取到数据了,如图5.12所示。
图5.12 实时数据
关于操作指令下发的接口如下:
@PostMapping("/operation/{instruct}")
public void sendInstruction(@PathVariable("instruct") Integer instruct){
Command command = new Command();
command.setOperation(instruct);
mqttGateway.sendToMqtt(JSON.toJSONString(command));
}
物理层处理下发的指令,使用Keil提供的库来处理,导入方式如5.13所示.
图5.13 导入库文件
针对应用层下发的数据,封装了数据处理文件.
代码如下:
Command.h
#ifndef __COMMAND_H
#define __COMMAND_H
#include "stdlib.h"
#include "bsp_beep.h"
#include "cJSON.h"
#include "string.h"
#include "jansson.h"
// 由于频繁的分配内存容易死机,所以抽离出来命令处理文件
//处理命令
void handleCMD(int operation);
//获取命令
int getCommand(char * json,json_type commandType,char* key,void** value);
//获取json字符串
char* jsonHandler(char * ret);
#endif
Command.c
#include "command.h"
char jsonResultString[100];
void handleCMD(int operation){
if(operation == 0){
BEEP(OFF);
}else if(operation== 1){
BEEP(ON);
}else{
BEEP(OFF);
}
}
//获取命令
// json : json字符串
// commandType : 命令的类型 JSON_OBJECT,
// JSON_STRING,
// JSON_INTEGER,
// key : 获取command的key
// value : command
// 成功返回1 ,失败返回0
int getCommand(char * json,json_type commandType,char* key,void** value){
json_error_t error;
json_t *root;
int operationInt;
root = json_loads((const char*)json, 0, &error);
if(commandType == JSON_INTEGER){
if(json_is_object(root)) // 如果读取失败,自动置为空指针
{
operationInt = json_integer_value(json_object_get(root,key));
*value = (void*)&operationInt;
return 1;
}else{
*value = NULL;
return 0;
}
}else if(commandType == JSON_STRING){
if(json_is_object(root)) // 如果读取失败,自动置为空指针
{
*value = (void*)json_string_value(json_object_get(root,key));
return 1;
}else{
*value = NULL;
return 0;
}
}
*value = NULL;
return 0;
}
//获取json字符串
char* jsonHandler(char * ret)
{
memset(jsonResultString,0,sizeof(jsonResultString));
strcpy(jsonResultString,"{\"");//用于拼接的字符串
ret = strcat(jsonResultString, ret);//拼接
int len = strlen(ret);
//ret复制到res
for (int i = 0; i < len; i++) {
if (*(ret + i) != '}') {
jsonResultString[i] = *(ret + i);
}
else
{
jsonResultString[i] = '}';
jsonResultString[i + 1] = '\0';
break;
}
}
//获取到指令json
return jsonResultString;
}
在uart.c的中断处理函数中,当一个数据帧完成后处理:
if(USART_GetITStatus( USART2, USART_IT_IDLE ) != RESET ) //如果总线空闲
{
ESP8266_Fram_Record_Struct .InfBit .FramFinishFlag = 1;
ucCh = USART_ReceiveData( USART2 ); //由软件序列清除中断标志位(先读USART_SR,然后读USART_DR)
USART_SendData(USART3,ucCh);
TcpClosedFlag = strstr ( ESP8266_Fram_Record_Struct .Data_RX_BUF, "CLOSED\r\n" ) ? 1 : 0;
ESP8266_Fram_Record_Struct.Data_RX_BUF[ ESP8266_Fram_Record_Struct.InfBit.FramLength] = '\0'; //判断是否是服务器发下来的指令
char* ret = strstr(ESP8266_Fram_Record_Struct.Data_RX_BUF, "operation");//获取包含operation的字串,
//判断ret是否为空,为空说明不是指令
if(ret != NULL){
//若ret不为空,则获取到 operation":0} ,不是完整的字符串
//拼接为完整的json字符串
char* json = jsonHandler(ret);
//获取json串中的指令, 指令的key为operation
void* command;
getCommand(json,JSON_INTEGER,"operation",&command);
handleCMD(*(int*)command);
strcpy(ESP8266_Fram_Record_Struct.Data_RX_BUF,"");
ESP8266_Fram_Record_Struct .InfBit .FramLength = 0 ;
}
USART_ClearITPendingBit(USART1, USART_IT_IDLE); // 清除空闲中断
}
完成以上操作就能够使用应用层控制物理层了。
移动app用Android
Studio开发,关键点在于发送http请求,本文使用了OKHTTP库。页面设计如图5.14所示。
图5.14 app页面设计
MainActivity主要获取到了各个页面对象,定时的发送http请求获取实时数据,关键代码如下:
public class MainActivity extends AppCompatActivity {
private TextView temperatureText;
private TextView humidityText;
private TextView serverStatusText;
private Switch beepSwitch;
private final static String TAG = "MainActivity";
private final String serverHost = "http://192.168.1.105:8080";
private NetworkHandler networkHandler = new NetworkHandler();
private static final long TIMEOUT_MILLS = 10*1000L;//1s
Timer timer = new Timer();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
temperatureText = findViewById(R.id.temperatureText);
humidityText = findViewById(R.id.humidityText);
serverStatusText = findViewById(R.id.serverStatusText);
beepSwitch = findViewById(R.id.beepSwitch);
beepSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
Log.i(TAG,"switchStatus: "+ isChecked);
Callback callback = new Callback() {
@Override
public void onFailure(@NotNull Call call, @NotNull IOException e) {
serverStatusText.setText(R.string.serverOff);
}
@Override
public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
Log.i(TAG,response.body().string());
}
};
Command command = new Command();
command.setOperation(isChecked?1:0);
networkHandler.nonSyncPost(serverHost+"/sendCommand",command,callback);
}
});
timer.schedule(timeoutTask,0,TIMEOUT_MILLS);
}
@Override
protected void onPostResume() {
super.onPostResume();
Callback callback = new Callback() {
@Override
public void onFailure(@NotNull Call call, @NotNull IOException e) {
serverStatusText.setText(R.string.serverOff);
}
@Override
public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
serverStatusText.setText(R.string.serverOn);
Data data = JSON.parseObject(response.body().string(), Data.class);
Log.i(TAG,data.toString());
humidityText.setText(data.getHumidity() + " %");
temperatureText.setText(data.getTemperature()+" ℃");
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
beepSwitch.setChecked(data.getBeep() == 1);
}
});
}
};
networkHandler.nonSyncGet(serverHost+"/getRealtimeData",callback);
}
private TimerTask timeoutTask = new TimerTask() {
@Override
public void run() {
//timeout logic
Callback callback = new Callback() {
@Override
public void onFailure(@NotNull Call call, @NotNull IOException e) {
serverStatusText.setText(R.string.serverOff);
}
@Override
public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
serverStatusText.setText(R.string.serverOn);
Data data = JSON.parseObject(response.body().string(), Data.class);
Log.i(TAG,data.toString());
humidityText.setText(data.getHumidity() + " %");
temperatureText.setText(data.getTemperature()+" ℃");
}
};
networkHandler.nonSyncGet(serverHost+"/getRealtimeData",callback);
}
};
}
具体代码可以看工程文件。App运行如图5.15所示。
图5.15 APP运行
小程序使用uniapp开发,会一点vue就可以使用。首先创建一个uniapp项目,使用的是HBuilder-x【https://www.dcloud.io/hbuilderx.html】,如图5.16所示。
图5.16 创建uni-app项目
在微信开发者工具中开启服务端口,如图5.17所示。
图5.17开启服务端口
运行到微信开发费者工具,如图5.18所示。
图5.18 运行小程序
安装了uVIEW,页面主要显示了数据,主要代码:
主要逻辑是定时发送数据请求,触发开关发送指令,主要代码:
<script>
export default {
data() {
return {
title: 'Hello',
checked: false,
temperature: 0,
humidity: 0
}
},
mounted(){
setInterval(()=>{
this.getData(this)
},2000)
},
methods: {
getData:(_this)=>{
uni.request({
url: 'http://192.168.1.105:8080/getRealtimeData', //仅为示例,并非真实接口地址。
success: (res) => {
console.log(res.data);
_this.temperature = res.data.temperature
_this.humidity = res.data.humidity
}
});
},
sendCommand:function(cmd){
uni.request({
url: 'http://192.168.1.105:8080/operation/'+cmd, //仅为示例,并非真实接口地址。
data: dataSend,
method: "POST",
dataType: "JSON",
success: (res) => {
console.log(res);
},
fail: (res) =>{
console.log(res);
}
});
},
switch1Change: function (e) {
var cmd = this.checked == true?1:0;
this.sendCommand(cmd)
},
}
}
</script>
小程序运行结果如图5.19所示。
图5.19 小程序运行图
public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
serverStatusText.setText(R.string.serverOn);
Data data = JSON.parseObject(response.body().string(), Data.class);
Log.i(TAG,data.toString());
humidityText.setText(data.getHumidity() + " %");
temperatureText.setText(data.getTemperature()+" ℃");
}
};
networkHandler.nonSyncGet(serverHost+"/getRealtimeData",callback);
}
};
}
具体代码可以看工程文件。App运行如图5.15所示。
[外链图片转存中…(img-82aLvfze-1632898026312)]
图5.15 APP运行
小程序使用uniapp开发,会一点vue就可以使用。首先创建一个uniapp项目,使用的是HBuilder-x【https://www.dcloud.io/hbuilderx.html】,如图5.16所示。
[外链图片转存中…(img-Ca018QQH-1632898026313)]
图5.16 创建uni-app项目
在微信开发者工具中开启服务端口,如图5.17所示。
[外链图片转存中…(img-PFEEVfwC-1632898026314)]
图5.17开启服务端口
运行到微信开发费者工具,如图5.18所示。
[外链图片转存中…(img-X0CdBcvK-1632898026315)]
图5.18 运行小程序
安装了uVIEW,页面主要显示了数据,主要代码:
主要逻辑是定时发送数据请求,触发开关发送指令,主要代码:
<script>
export default {
data() {
return {
title: 'Hello',
checked: false,
temperature: 0,
humidity: 0
}
},
mounted(){
setInterval(()=>{
this.getData(this)
},2000)
},
methods: {
getData:(_this)=>{
uni.request({
url: 'http://192.168.1.105:8080/getRealtimeData', //仅为示例,并非真实接口地址。
success: (res) => {
console.log(res.data);
_this.temperature = res.data.temperature
_this.humidity = res.data.humidity
}
});
},
sendCommand:function(cmd){
uni.request({
url: 'http://192.168.1.105:8080/operation/'+cmd, //仅为示例,并非真实接口地址。
data: dataSend,
method: "POST",
dataType: "JSON",
success: (res) => {
console.log(res);
},
fail: (res) =>{
console.log(res);
}
});
},
switch1Change: function (e) {
var cmd = this.checked == true?1:0;
this.sendCommand(cmd)
},
}
}
</script>
小程序运行结果如图5.19所示。
[外链图片转存中…(img-zPrkcTsf-1632898026316)]
图5.19 小程序运行图
如果想获取全部资料和word版教程,到资源下载