目录
前言
这是一个简单,但是横跨嵌入式开发、后端Web开发、前端开发的项目。在开始之前,你可能需要了解Golang语言基础
、Gin框架
、MySQL基础
、HTTP请求
、Arduino
、Vue3
需要注意的是,项目并没有完全实现所有接口,读者可以根据自己的需求修改调整
设计与架构
本文使用ESP8266+DHT11作为温湿度的收集设备,使用Golang的Gin框架作为Web后端,使用Vue3作为前端数据展示。
环境要求
系统环境的配置较多,详情查看下面的表格,本文不会讲解关于环境配置的相关问题。此处以笔者的配置为例,笔者使用的是windows11系统
环境 | 版本 |
---|---|
node.js(nvm) | 16.15.1 |
golang | 1.18.3 |
mysql(docker) | 8.0.29-1debian10 |
redis(docker) | 7.0.2 |
arduino | * |
接口定义
title: 个人气象平台 v1.0.0
language_tabs:
- shell: Shell
- http: HTTP
- javascript: JavaScript
- ruby: Ruby
- python: Python
- php: PHP
- java: Java
- go: Go
toc_footers: []
includes: []
search: true
code_clipboard: true
highlight_theme: darkula
headingLevel: 2
generator: “@tarslib/widdershins v4.0.15”
后端服务状态
GET ping测试
GET /server/ping
不限频率的接口,测试服务端是否存活,可用于服务看板
返回示例
成功
{
"code": 0,
"data": "pong",
"msg": "ok"
}
返回结果
状态码 | 状态码含义 | 说明 | 数据模型 |
---|---|---|---|
200 | OK | 成功 | Inline |
返回数据结构
状态码 200
名称 | 类型 | 必选 | 约束 | 中文名 | 说明 |
---|---|---|---|---|---|
» code | integer | true | none | none | |
» data | string | true | none | none | |
» msg | string | true | none | none |
POST 服务端状态
POST /server/statue
监测服务端的基本状态,包括数据库延迟等,需要鉴权以及限制请求频率,防止过多请求,服务端被攻击。
请求参数
名称 | 位置 | 类型 | 必选 | 说明 |
---|---|---|---|---|
MAC | header | string | 是 | MAC地址 |
SN | header | string | 是 | 设备编码 |
返回示例
成功
{
"code": 0,
"msg": "ok",
"data": {
"to_mysql": 13.9,
"to_redis": 21.8,
"to_hyper": 30.8
}
}
返回结果
状态码 | 状态码含义 | 说明 | 数据模型 |
---|---|---|---|
200 | OK | 成功 | Inline |
返回数据结构
状态码 200
名称 | 类型 | 必选 | 约束 | 中文名 | 说明 |
---|---|---|---|---|---|
» code | integer | true | none | none | |
» msg | string | true | none | none | |
» data | object | true | none | none | |
»» to_mysql | number | true | none | MySQL读写延迟 | |
»» to_redis | number | true | none | Redis读写延迟 | |
»» to_hyper | number | false | none | 心知天气API延迟 |
数据上报
POST 数据收集平台数据上报
POST /record/upload
Body 请求参数
{
"temperature": 0,
"humidity": 0
}
请求参数
名称 | 位置 | 类型 | 必选 | 说明 |
---|---|---|---|---|
SN | header | string | 是 | 设备注册ID |
body | body | object | 否 | none |
» temperature | body | integer | 是 | none |
» humidity | body | number | 是 | none |
返回示例
成功
{
"code": 0,
"data": null,
"msg": "ok"
}
返回结果
状态码 | 状态码含义 | 说明 | 数据模型 |
---|---|---|---|
200 | OK | 成功 | Inline |
返回数据结构
状态码 200
名称 | 类型 | 必选 | 约束 | 中文名 | 说明 |
---|---|---|---|---|---|
» code | integer | true | none | none | |
» data | null | true | none | none | |
» msg | string | true | none | none |
数据提供
GET 现在的温湿度
GET /data/new
返回示例
成功
{
"code": 0,
"data": {
"temperature": 0,
"humidity": 0,
"time": ""
},
"msg": "ok"
}
返回结果
状态码 | 状态码含义 | 说明 | 数据模型 |
---|---|---|---|
200 | OK | 成功 | Inline |
返回数据结构
状态码 200
名称 | 类型 | 必选 | 约束 | 中文名 | 说明 |
---|---|---|---|---|---|
» code | integer | true | none | none | |
» data | object | true | none | none | |
»» temperature | integer | true | none | none | |
»» humidity | integer | true | none | none | |
»» time | string | true | none | none | |
» msg | string | true | none | none |
POST 历史数据
POST /data/history
Body 请求参数
{
"start_time": 0,
"end_time": 0
}
请求参数
名称 | 位置 | 类型 | 必选 | 说明 |
---|---|---|---|---|
body | body | object | 否 | none |
» start_time | body | integer | 是 | none |
» end_time | body | integer | 是 | none |
返回示例
成功
{
"code": 0,
"data": [
{
"temperature": 1,
"humidity": 90,
"time": "2022-07-05T21:38:46+08:00"
},
{
"temperature": 1,
"humidity": 90,
"time": "2022-07-05T21:38:46+08:00"
},
{
"temperature": 1,
"humidity": 90,
"time": "2022-07-05T21:38:46+08:00"
},
{
"temperature": 1,
"humidity": 90,
"time": "2022-07-05T21:38:46+08:00"
},
{
"temperature": 1,
"humidity": 90,
"time": "2022-07-05T21:38:46+08:00"
},
{
"temperature": 1,
"humidity": 90,
"time": "2022-07-05T21:38:46+08:00"
},
{
"temperature": 1,
"humidity": 90,
"time": "2022-07-05T21:38:46+08:00"
},
{
"temperature": 1,
"humidity": 90,
"time": "2022-07-05T21:38:46+08:00"
},
{
"temperature": 1,
"humidity": 90,
"time": "2022-07-05T21:38:46+08:00"
},
{
"temperature": 1,
"humidity": 90,
"time": "2022-07-05T21:38:46+08:00"
},
{
"temperature": 1,
"humidity": 90,
"time": "2022-07-05T21:38:46+08:00"
}
],
"msg": "ok"
}
返回结果
状态码 | 状态码含义 | 说明 | 数据模型 |
---|---|---|---|
200 | OK | 成功 | Inline |
返回数据结构
状态码 200
名称 | 类型 | 必选 | 约束 | 中文名 | 说明 |
---|---|---|---|---|---|
» code | integer | true | none | none | |
» data | [object] | true | none | none | |
»» temperature | integer | true | none | none | |
»» humidity | integer | true | none | none | |
»» time | integer | true | none | none | |
» msg | string | true | none | none |
数据模型
基本返回结构
{
"code": 0,
"msg": "string",
"data": "string"
}
属性
名称 | 类型 | 必选 | 约束 | 中文名 | 说明 |
---|---|---|---|---|---|
code | integer | true | none | none | |
msg | string | true | none | none | |
data | string | true | none | none |
数据收集平台制作
设备平台
- ESP8266(带底板)
- DHT11温湿度传感器(带底板3脚)
- 杜邦线若干
物理连接
使用ESP8266的GPIO14
接口作为传感器的信号线
如果您的ESP8266带底板,可以参考下图中的引脚对应关系,或查询自己所购买主板的说明文档
本文使用的DHT11传感器也带底板,引脚数为3,如图
连接对应表
ESP8266 | DHT11 |
---|---|
D5(GPIO14) | DATA |
3V | VCC |
GND | GND |
程序编写
首先新建一个空项目
// The setup() function runs once each time the micro-controller starts
void setup() {
}
// Add the main program code into the continuous loop() function
void loop() {
}
添加网络连接,需要使用ESP8266WiFi.h
具体可以查看代码注释,注意需要修改WIFI配置
#include <ESP8266WiFi.h>
// 设置WIFI连接参数,记得更改
#ifndef STASSID
#define STASSID "XXXX" // WIFI连接SSID
#define STAPSK "XXXXXXXX" // WIFI连接密码
#endif
void setup() {
// 设置WIFI连接参数,并开始连接
WiFi.begin(STASSID, STAPSK);
// 循环检测WIFI连接状态,是否连接成功
while (WiFi.status() != WL_CONNECTED) {
delay(500);
}
}
void loop() {
if ((WiFi.status() == WL_CONNECTED)) {
// 接下来会在此处添加处理程序
}
}
你们会注意到在loop
函数中,我们依旧会判断WIFI连接是否正常,以防连接断开。
添加获取温度,需要使用SimpleDHT.h
需要注意读取频率,DHT11限制为1Hz,但是我们读取的颗粒精度不需要这么精细,我们会在每一个循环休眠2s
#include <ESP8266WiFi.h>
#include <SimpleDHT.h>
// 设置WIFI连接参数,记得更改
#ifndef STASSID
#define STASSID "XXXX" // WIFI连接SSID
#define STAPSK "XXXXXXXX" // WIFI连接密码
#endif
// DHT11对象
int pinDHT11 = 14;
SimpleDHT11 dht11(pinDHT11);
void setup() {
// 设置WIFI连接参数,并开始连接
WiFi.begin(STASSID, STAPSK);
// 循环检测WIFI连接状态,是否连接成功
while (WiFi.status() != WL_CONNECTED) {
delay(500);
}
}
void loop() {
if ((WiFi.status() == WL_CONNECTED)) {
// 从DHT11获取温度
byte temperature = 0; //温度
byte humidity = 0; //湿度
// 读取数据,并写入
dht11.read(&temperature, &humidity, NULL);
}
// 休眠2秒
delay(2000);
}
我们成功的读取了温湿度数据,但是总有可能导致数据读取失败,需要我们判断温度是否读取失败,然后在短时间内重试
#include <ESP8266WiFi.h>
#include <SimpleDHT.h>
// 设置WIFI连接参数,记得更改
#ifndef STASSID
#define STASSID "XXXX" // WIFI连接SSID
#define STAPSK "XXXXXXXX" // WIFI连接密码
#endif
// DHT11对象
int pinDHT11 = 14;
SimpleDHT11 dht11(pinDHT11);
void setup() {
// 设置WIFI连接参数,并开始连接
WiFi.begin(STASSID, STAPSK);
// 循环检测WIFI连接状态,是否连接成功
while (WiFi.status() != WL_CONNECTED) {
delay(500);
}
}
void loop() {
if ((WiFi.status() == WL_CONNECTED)) {
// 从DHT11获取温度
byte temperature = 0; //温度
byte humidity = 0; //湿度
int err = SimpleDHTErrSuccess; //错误码
// 读取数据,并写入
if ((err = dht11.read(&temperature, &humidity, NULL)) != SimpleDHTErrSuccess) {
// 如果读取错误,就在短时间内重试
delay(800);
return;
}
}
// 休眠2秒
delay(2000);
}
接下来,我们会向服务器发送请求,并完成所有的嵌入式代码
完整的代码如下:
#include <ESP8266HTTPClient.h>
#include <ESP8266WiFi.h>
#include <SimpleDHT.h>
#define SERVER_IP "weather.station.ptianya.top" // 后端服务的位置
// 设置WIFI连接参数,记得更改
#ifndef STASSID
#define STASSID "XXXX" // WIFI连接SSID
#define STAPSK "XXXXXXXX" // WIFI连接密码
#endif
// DHT11对象
int pinDHT11 = 14;
SimpleDHT11 dht11(pinDHT11);
void setup() {
// 设置WIFI连接参数,并开始连接
WiFi.begin(STASSID, STAPSK);
// 循环检测WIFI连接状态,是否连接成功
while (WiFi.status() != WL_CONNECTED) {
delay(500);
}
}
void loop() {
if ((WiFi.status() == WL_CONNECTED)) {
// 从DHT11获取温度
byte temperature = 0; //温度
byte humidity = 0; //湿度
int err = SimpleDHTErrSuccess; //错误码
// 读取数据,并写入
if ((err = dht11.read(&temperature, &humidity, NULL)) != SimpleDHTErrSuccess) {
// 如果读取错误,就在短时间内重试
delay(800);
return;
}
WiFiClient client;
HTTPClient http;
// 设置请求路由
http.begin(client, "http://" SERVER_IP "/record/upload"); //HTTP
// 设置请求头
http.addHeader("Content-Type", "application/json");
http.addHeader("sn", "9d0dd9f2e3bc");
// 设置Body
char body[1024];
snprintf(body, sizeof(body), "{\"temperature\":%d,\"humidity\":%d}", (int)temperature, (int)humidity);
// 开始请求
int httpCode = http.POST(body);
// httpCode will be negative on error
http.end();
}
// 休眠2秒
delay(2000);
}
后端部分实现
目前,后端项目已经开源在了Github上,具体可以访问源码,具体说明,可以查看项目文档
前端部分实现
前端部分大致只修改了App.vue
部分,故直接在此处贴出代码
需要使用arco
的组件库
并修改文中的服务地址
<template>
<div class = main>
<a-space size="large">
<a-statistic :value=temperature :precision="1" show-group-separator :value-style="{ color: temperature_color }">
<template #suffix>℃</template>
<template #title>温度</template>
</a-statistic>
<a-statistic title="湿度" :value=humidity :precision="2" :value-style="{ color: humidity_color }">
<template #suffix>%</template>
</a-statistic>
</a-space>
</div>
<div id="update_time_div">
<a-space>
<a-statistic title="更新时间" :value=time format="YYYY-MM-DD HH:mm:ss" id="update_time" animation>
</a-statistic>
</a-space>
</div>
</template>
<script>
import axios from 'axios'
import dayjs from 'dayjs'
export default {
mounted() {
this.getData()
setInterval(() => {
this.getData()
}, 2000)
},
data() {
return {
temperature: -999,
humidity: -99.99,
time: '',
temperature_color: '#0fbf60',
humidity_color: '#0fbf60',
}
},
methods: {
getData() {
axios.get("http://XXXX").then(res => {
this.temperature = res.data.data.temperature
this.humidity = res.data.data.humidity
this.time = dayjs(res.data.data.time)
if (this.temperature < 10) {
this.temperature_color = '#3491FA'
} else if (this.temperature >= 30) {
this.temperature_color = '#F53F3F'
} else {
this.temperature_color = '#0fbf60'
}
if (this.humidity < 58) {
this.humidity_color = '#F53F3F'
} else if (this.humidity > 80) {
this.humidity_color = '#3491FA'
} else {
this.humidity_color = '#0fbf60'
}
})
}
}
}
</script>
<style>
.main {
margin: 0 auto;
text-align: center;
}
#update_time .arco-statistic-value {
font-size: 16px;
}
#update_time_div {
margin-top: 20px;
}
</style>