小项目:使用MQTT上传温湿度到Onenet服务器
前言
我们之前分别编写了 DHT11、ESP8266 和 MQTT 的代码,现在我们将它们仨整合在一起,来做一个温湿度检测小项目。这个项目可以实时地将 DHT11 传感器获取到的温湿度数据上传到 OneNET 平台。通过登录 OneNET,我们随时随地可以查看温湿度数据。
这种环境监测项目的应用场景有很多,其中特别适用于温室环境监测,园丁可以随时随地了解温室中空气情况,以确保温室环境适合娇贵的花草树木生长。
1. 源码下载及前置阅读
- STM32F103C8T6模板工程
链接:https://pan.baidu.com/s/1n7XHCaMYtASWdJH2uA5yDA?pwd=lw59 提取码:lw59
- 本文的源码
链接:https://pan.baidu.com/s/1skNMQTdgoWllgL-fWl2mqw?pwd=p8qw 提取码:p8qw
- token生成工具
链接:https://pan.baidu.com/s/1w73nfagxNC5Xw6dnsagixQ?pwd=dafw 提取码:dafw
如果你是嵌入式开发小白,那么建议你先读读下面几篇文章。
前期教程,没看过的小伙伴可以先看下。
2. 整体系统设计
使用 STM32F103C8T6 作主控芯片,配合 DHT11 温湿度传感器,实时监测周围环境的温湿度变化。通过 ESP8266 模块以 MQTT 协议将获取到的温湿度数据通过无线网络连接上传至 OneNET 平台,以便用户可以随时随地通过手机或电脑查看数据。
3. 硬件选型
本教程使用的硬件如下:
- 单片机:STM32F103C8T6
这款单片机具有 64K flash,20K RAM,4 个定时器,3 个串口,网络上资料好几吨,非常适合初学者入门,强烈推荐。
- WiFi模块:ESP-01S(ESP8266)
ESP8266 可以利用串口与单片机进行通讯,从而编程实现控制。
- 温湿度传感器:DHT11
DHT11 有 3 脚和 4 脚两款,在使用上没有差别,接线都一样,主要接三根,四脚的款式有一脚悬空。四脚款接杜邦线会有点不稳,适合插面包板或开发板上。
DHT11 工作参数:
- 湿度测量范围:20~90%RH
- 湿度测量精度:±5%RH
- 温度测量范围:0~50℃
- 温度测量精度:±2℃
- 工作电压:DC 3.3V/5V
- 串口:USB 转 TTL
这种设备主要作用是用来调试或下载程序,本文用于串口输出作调试。价格也很便宜,普遍 5~8 元。
- 烧录器:ST-LINK V2
ST-Link 是一种用于 STM32 微控制器的调试和编程工具,它可以通过 SWD 或 JTAG 接口与开发板进行通信。本文用做烧录。一般也很便宜,七八元左右。
4. OneNET 物联网平台
MQTT 服务端可以是云平台,OneNET、阿里云、华为云、腾讯云等;也可以自己搭建服务端,用 EMQ 或 Mosquitto 。
本次我们使用 OneNET,接下来我们来配置一下 OneNET。
点开 OneNET 官方网址:中移坤灵 - 中国移动物联网开放平台 (10086.cn)
有账号的登录,没账号的注册一下。
登录好后点击「开发者中心」。
4.1 创建产品
接下来我们先创建一个产品,之后再创建具体的设备。
可按照下图参数创建产品。
4.2 创建物模型
通过物模型我们可以定义设备的属性、服务和事件功能。我们需要创建几个物模型,用于上传数据和事件告警。
创建两个物模型:
- 当前湿度,用于存储实时湿度数据。
- 当前温度,用于存储实时温度数据。
本教程只用到「当前湿度」和「当前温度」,剩下的物模型是下篇教程使用的。
4.3 创建设备
接下来就开始创建产品下的具体设备。
4.4 生成MQTT三元组
「MQTT 三元组」是 MQTT 协议中至关重要的,就像去考试的时候,一定要带上准考证、身份证才能进考场,要有「MQTT 三元组」才能连接 MQTT 服务端。
得到初步「MQTT 三元组」:
- 设备 ID:temp01
- 产品 ID:P2k4KV0low
- 设备密钥:REhWUEhWbDlIOTdRUFEzU1dGQXk4TlZKZ25oQ0N4S3M=
设备密钥需要经过加密,加密需要用 OneNET 官方的 token 生成工具。
官网下载地址:OneNET - 中国移动物联网开放平台 (10086.cn)
也可以拿文章开头提供的,也是官网下载的。
下载好 token 生成工具,打开界面如下,我来告诉大家每个空填啥。
各个参数介绍如下表:
名称 | 类型 | 参数说明 | 参数示例 |
---|---|---|---|
res | string | 访问资源 resource 格式为:products/{产品id}/devices/{设备名字} | products/P2k4KV0low/devices/temp01 |
et | int | 访问过期时间,单位秒,unix 时间。当一次访问参数中的 et 时间小于当前时间时,平台会认为访问参数过期从而拒绝该访问 | 2017881776 表示:北京时间 2033-12-11 10:42:56 |
key | string | MQTT 三元组的设备密钥 | REhWUEhWbDlIOTdRUFEzU1dGQXk4TlZKZ25oQ0N4S3M= |
method | string | 加密方式,支持 hmacmd5、hmacsha1、hmacsha256 | md5(代表使用hmacmd5算法) sha1(代表使用hmacsha1算法) sha256(代表使用hmacsha256 算法) |
version | string | 参数组版本号,日期格式,目前仅支持"2018-10-31" | 2018-10-31 |
et 的时间戳可以用这个在线工具转换,网页地址:时间戳(Unix timestamp)转换工具 - 在线工具 (tool.lu)
根据介绍,填好各个参数的空,我们选择 sha1 的加密方式,大家可以选择自己喜欢的。填好如下操作:
得到最终「MQTT 三元组」:
- 设备 ID:temp01
- 产品 ID:P2k4KV0low
- token:version=2018-10-31&res=products%2FP2k4KV0low%2Fdevices%2Ftemp01&et=2017881776&method=sha1&sign=M3jVJvfeFLnggMrUPhYm5uRirXs%3D
4.5 主题订阅格式
4.5.1 OneNET地址
OneNET 服务器地址是 mqtts.heclouds.com : 1883,地址是从 OneNET 文档中心得到的。
4.5.2 订阅主题
选择设置直连设备属性:$sys/P2k4KV0low/{device-name}/thing/property/set
{device-name} 是设备ID,比如我们的就是 temp01。
4.5.3 上报主题
选择直连设备上报属性:$sys/P2k4KV0low/{device-name}/thing/property/post
{device-name} 是设备ID,比如我们的就是 temp01。
5. STM32 设备端开发
5.1 硬件接线
接线可参照下表:
ESP8266 | DHT11 | STM32 | USB 转 TTL |
---|---|---|---|
3V3 | 3.3 | ||
TX | A3 | ||
RX | A2 | ||
GND | G | ||
VCC | 3.3 | ||
DATA | A5 | ||
GND | G | ||
A10 | TX | ||
A9 | RX | ||
G | GND |
烧录的时候接线如下表,如果不会烧录的话可以看我之前的文章【STM32下载程序的五种方法】。
ST-Link V2 | STM32 |
---|---|
SWCLK | SWCLK |
SWDIO | SWDIO |
GND | GND |
3.3V | 3V3 |
5.2 设备实物图
接好如下图。
5.3 DHT11温湿度传感器代码
详细代码解析可以看手把手教你玩转DHT11(原理+驱动)。
5.3.1 读取1字节数据
将 DHT11 发来的二进制数据存储到 ReadData 变量中,读取一位后,左移一位,循环8次,最终得到 1 byte 数据。
那么如何判断我们读到的数据是 0 还是 1 呢?
通过 3.2.3 的分析可以知道,0 和 1 的时序只是高电平持续时间不同,所以我们只需要在 DHT11 拉低电平之后延时 40~60 微秒(代码中使用 50 微秒),再读取电平状态就可以了,如果是高电平则为 1,低电平则为 0 。
uint8_t DHT_Read_Byte(void) //从DHT11读取一位(8字节)信号
{
uint8_t i;
uint8_t ReadData = 0; //ReadData用于存放8bit数据,即8个单次读取的1bit数据的组合
uint8_t temp; //临时存放信号电平(0或1)
for(i=0;i<8;i++){
while(!HAL_GPIO_ReadPin(DHT11_IO, DHT11_PIN));
Delay_us(50);
if(HAL_GPIO_ReadPin(DHT11_IO, DHT11_PIN) == 1){
temp = 1;
while(HAL_GPIO_ReadPin(DHT11_IO, DHT11_PIN));
}else{
temp = 0;
}
ReadData = ReadData << 1;
ReadData |= temp;
}
return ReadData;
}
5.3.2 一次数据读取及显示
根据 DHT11 的时序,我们就可以使用代码实现 DHT11 一次读取数据过程。
注意:DHT11 读取数据间隔至少为 2 秒,否则读取到的数据可能不稳定,所以在最后可以延时 2 秒。
void DHT_Read()
{
uint8_t i;
DHT11_Start();
DHT_GPIO_INPUT();
for(i= 0;i < 5;i++){
Data[i] = DHT_Read_Byte();
}
if((Data[0]+Data[1]+Data[2]+Data[3])==Data[4])
{
printf("湿度: %d.%dRH ,", Data[0], Data[1]);
printf("温度: %d.%d℃\r\n", Data[2], Data[3]);
}else{
printf("ERROR DATA\r\n");
}
HAL_Delay(2000);
}
5.4 ESP8266模块代码
详细代码解析可以看手把手教你玩转ESP8266(原理+驱动)。
5.4.1 ESP8266 初始化
按照项目需求编写 ESP8266 初始化代码,我们需要将 ESP8266 设置工作模式为STA、单路连接模式、连接 WIFI 等操作。
uint8_t esp8266_init(uint32_t baudrate)
{
char ip_buf[16];
esp8266_uart_init(baudrate); /* ESP8266 UART初始化 */
/* 让WIFI退出透传模式 */
printf("\r\n 退出透传模式\r\n");
esp8266_exit_unvarnished();
printf(" 1.测试ESP8266是否存在(AT)\r\n");
while(esp8266_at_test())
delay_ms(500);
printf(" 2.重启ESP8266(AT+RST)\r\n");
while(esp8266_sw_reset())
delay_ms(500);
while(esp8266_disconnect_tcp_server())
delay_ms(500);
printf(" 3.设置工作模式为STA(AT+CWMODE=1)\r\n");
while(esp8266_set_mode(ESP8266_STA_MODE))
delay_ms(500);
printf(" 4.设置单路连接模式(AT+CIPMUX)\r\n"); //设置单路连接模式,透传只能使用此模式
while(esp8266_single_connection())
delay_ms(500);
printf(" 5.连接WiFi,SSID:%s,PWD:%s\r\n", WIFI_SSID, WIFI_PWD); //连接WIFI
while(esp8266_join_ap(WIFI_SSID, WIFI_PWD))
delay_ms(1000);
printf(" 6.获取IP地址(AT+CIFSR):");
while(esp8266_get_ip(ip_buf))
delay_ms(500);
printf("%s\r\n\r\n", ip_buf);
printf("ESP8266初始化完成\r\n");
return ESP8266_EOK;
}
初始化结束后连接 OneNET 服务器并进入透传模式。
void esp8266_connect_server(char *server_ip, char *server_port)
{
esp8266_disconnect_tcp_server();
printf(" 7.连接云服务器(AT+CIPSTART),server_ip:%s,server_port:%s\r\n", server_ip, server_port);
while(esp8266_connect_tcp_server(server_ip, server_port))
delay_ms(500);
printf(" 8.进入透传模式(AT+CIPMODE)\r\n");
while(esp8266_enter_unvarnished())
delay_ms(500);
printf("已连接上云服务器并进入透传模式。\r\n");
}
5.5 MQTT代码
5.5.1 CONNECT 报文
CONNECT 报文的编写及发送代码,报文编写就按照我们7.2节的理论编写即可,报文内容:10+剩余长度+00 04 4D 51 54 54 04 C2+保持连接时间+L+设备 ID+L+产品 ID+L+token。
uint8_t mqtt_connect(char *ClientID,char *Username,char *Password)
{
uint8_t i,j;
int ClientIDLen = strlen(ClientID);
int UsernameLen = strlen(Username);
int PasswordLen = strlen(Password);
int DataLen;
mqtt_txlen=0;
//可变报头+Payload 每个字段包含两个字节的长度标识
DataLen = 10 + (ClientIDLen+2) + (UsernameLen+2) + (PasswordLen+2);
//固定报头
//控制报文类型
mqtt_txbuf[mqtt_txlen++] = 0x10; //MQTT Message Type CONNECT
//剩余长度(不包括固定头部)
do
{
uint8_t encodedByte = DataLen % 128;
DataLen = DataLen / 128;
// if there are more data to encode, set the top bit of this byte
if ( DataLen > 0 )
encodedByte = encodedByte | 128;
mqtt_txbuf[mqtt_txlen++] = encodedByte;
}while ( DataLen > 0 );
//可变报头
//协议名
mqtt_txbuf[mqtt_txlen++] = 0; // Protocol Name Length MSB
mqtt_txbuf[mqtt_txlen++] = 4; // Protocol Name Length LSB
mqtt_txbuf[mqtt_txlen++] = 'M'; // ASCII Code for M
mqtt_txbuf[mqtt_txlen++] = 'Q'; // ASCII Code for Q
mqtt_txbuf[mqtt_txlen++] = 'T'; // ASCII Code for T
mqtt_txbuf[mqtt_txlen++] = 'T'; // ASCII Code for T
//协议级别
mqtt_txbuf[mqtt_txlen++] = 4; // MQTT Protocol version = 4
//连接标志
mqtt_txbuf[mqtt_txlen++] = 0xc2; // conn flags
mqtt_txbuf[mqtt_txlen++] = 0; // Keep-alive Time Length MSB
mqtt_txbuf[mqtt_txlen++] = 100; // Keep-alive Time Length LSB 100S心跳包
mqtt_txbuf[mqtt_txlen++] = BYTE1(ClientIDLen);// Client ID length MSB
mqtt_txbuf[mqtt_txlen++] = BYTE0(ClientIDLen);// Client ID length LSB
memcpy(&mqtt_txbuf[mqtt_txlen],ClientID,ClientIDLen);
mqtt_txlen += ClientIDLen;
if(UsernameLen > 0)
{
mqtt_txbuf[mqtt_txlen++] = BYTE1(UsernameLen); //username length MSB
mqtt_txbuf[mqtt_txlen++] = BYTE0(UsernameLen); //username length LSB
memcpy(&mqtt_txbuf[mqtt_txlen],Username,UsernameLen);
mqtt_txlen += UsernameLen;
}
if(PasswordLen > 0)
{
mqtt_txbuf[mqtt_txlen++] = BYTE1(PasswordLen); //password length MSB
mqtt_txbuf[mqtt_txlen++] = BYTE0(PasswordLen); //password length LSB
memcpy(&mqtt_txbuf[mqtt_txlen],Password,PasswordLen);
mqtt_txlen += PasswordLen;
}
for(i=0;i<10;i++)
{
memset(mqtt_rxbuf,0,mqtt_rxlen);
mqtt_send_data(mqtt_txbuf,mqtt_txlen);
for(j=0;j<10;j++)
{
delay_ms(50);
if (esp8266_wait_receive() == ESP8266_EOK)
esp8266_copy_rxdata((char *)mqtt_rxbuf);
//CONNECT
if(mqtt_rxbuf[0]==parket_connetAck[0] && mqtt_rxbuf[1]==parket_connetAck[1] && mqtt_rxbuf[2]==parket_connetAck[2]) //连接成功
{
return 0;//连接成功
}
}
}
return 1;
}
5.5.2 PUBLISH 报文
PUBLISH 报文的编写及发送代码,报文编写就按照我们7.2节的理论编写即可,报文内容:30 +剩余长度+L+主题+数据(JSON)。
uint8_t mqtt_publish_data(char *topic, char *message, uint8_t qos)
{
int topicLength = strlen(topic);
int messageLength = strlen(message);
static uint16_t id=0;
int DataLen;
mqtt_txlen=0;
//有效载荷的长度这样计算:用固定报头中的剩余长度字段的值减去可变报头的长度
//QOS为0时没有标识符
//数据长度 主题名 报文标识符 有效载荷
if(qos) DataLen = (2+topicLength) + 2 + messageLength;
else DataLen = (2+topicLength) + messageLength;
//固定报头
//控制报文类型
mqtt_txbuf[mqtt_txlen++] = 0x30; // MQTT Message Type PUBLISH
//剩余长度
do
{
uint8_t encodedByte = DataLen % 128;
DataLen = DataLen / 128;
// if there are more data to encode, set the top bit of this byte
if ( DataLen > 0 )
encodedByte = encodedByte | 128;
mqtt_txbuf[mqtt_txlen++] = encodedByte;
}while ( DataLen > 0 );
mqtt_txbuf[mqtt_txlen++] = BYTE1(topicLength);//主题长度MSB
mqtt_txbuf[mqtt_txlen++] = BYTE0(topicLength);//主题长度LSB
memcpy(&mqtt_txbuf[mqtt_txlen],topic,topicLength);//拷贝主题
mqtt_txlen += topicLength;
//报文标识符
if(qos)
{
mqtt_txbuf[mqtt_txlen++] = BYTE1(id);
mqtt_txbuf[mqtt_txlen++] = BYTE0(id);
id++;
}
memcpy(&mqtt_txbuf[mqtt_txlen],message,messageLength);
mqtt_txlen += messageLength;
// int i = 0;
// for(i=0;i<mqtt_txlen;i++)
// printf("%02X ", mqtt_txbuf[i]);
// printf("\r\n");
mqtt_send_data(mqtt_txbuf,mqtt_txlen);
return mqtt_txlen;
}
5.6 主函数逻辑代码
主函数代码如下:
#include "sys.h"
#include "usart.h"
#include "delay.h"
#include "esp8266.h"
#include "onenet.h"
#include "dht11.h"
extern char dht11_data[5];
int main(void)
{
HAL_Init(); /* 初始化HAL库 */
sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟,72M */
delay_init(72); /* 初始化延时函数 */
usart_init(115200); /* 波特率设为115200 */
printf("初始化ESP8266...\r\n");
esp8266_init(115200);
printf("初始化MQTT...\r\n");
mqtt_init();
printf("MQTT连接...\r\n");
mqtt_connect(MQTT_ClientID,MQTT_UserName,MQTT_PassWord);
while(1)
{
uint8_t data_send_buff[512];
memset(data_send_buff, 0, sizeof(data_send_buff));
dht11_read();
sprintf((char *)data_send_buff,"{\"id\":\"1386772172\",\"version\":\"1.0\",\"params\":{\"temperature\":{\"value\":%d.%d},\"humidity\":{\"value\":%d.%d}}}"
,dht11_data[2], dht11_data[3], dht11_data[0], dht11_data[1]);
mqtt_publish_data(POST_TOPIC, (char *)data_send_buff, 0);
HAL_Delay(3000); //3s发送一次
printf("\r\n~~~~~~~~发送心跳包~~~~~~~~\r\n");
mqtt_send_heart();
printf("~~~~~~~~心跳包发送结束~~~~~~~~\r\n");
}
}
5.7 运行过程
烧录好后,串口效果如下:
OneNET 平台效果如下:
总结
把前面学的知识整合成一个小项目后是不是成就感爆棚了,接下来会不断继续优化这个小项目,让它更完整,更人性。感谢各位看官,love and peace!