前言
本文介绍如何搭建一个flask服务器,通过Arduino UNO板、DHT11温湿度传感器和ESP-01S WiFi模块实现物联。
所需器件:Arduino UNO板、DHT11温湿度传感器、ESP-01S Wi-Fi模块
使用的库:DHT11
开发环境:Python、PyCharm、MySQL 8.0、Arduino IDE(文章默认已经安装好这些软件)
前端技术:Echarts、BootStrap v3、Ajax、WebSocket通信
后端技术:Flask、WebSocket通信、跨域请求处理
调试工具:(可安装)Navicat、VS Code、Postman、任意串口调试助手
项目运行演示与功能讲解
仪表盘每3s从后台获取最新的温湿度数据并显示
折线图每30s获取最新的10份数据并显示
筛选出一定时间范围内的数据并显示
项目结构:
WIFI模块:定期发送HOST请求到名为upload的的api接口上,Flask服务器接受这个POST类型的请求,更新数据库数据。
前端:各个组件通过Ajax发送请求到api接口上,根据返回的JSON数据更新组件。
MySQL配置
首先需要安装MySQL,安装成功后,新建数据库,名字随意
// 在新建的数据库下执行
CREATE TABLE `data` (
`id` int NOT NULL AUTO_INCREMENT,
`temperature` float(10,2) DEFAULT NULL,
`humidity` float(10,2) DEFAULT NULL,
`time` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
如果安装了navicat的话,也可以使用手工的方式创建
为了后续的调试,可以先填充几行数据
Flask服务器配置与API编写
- 安装Flask
// 在命令行里输入
pip install flask
- 新建Flask项目
- 导入需要的包
// 在pycharm的terminal里输入
pip install flask-socketio
pip install flask-cors
pip install pymysql
- 设置访问权限
打开项目的设置功能
点击Modify options,勾选Additional options
之后再新出先的输入框里输入--host=0.0.0.0
之后点击apply即可 - 代码部分
总体结构如下所示,新创建config.py与mysql_operate.py
config.py
# MySQL配置
MYSQL_HOST = "127.0.0.1" # 表示本地的地址
MYSQL_PORT = 3306 # 端口号
MYSQL_USER = "root"
MYSQL_PASSWD = "123456"
MYSQL_DB = "dht11" # 数据库名称
mysql_operate.py
# 需导入pymysql,自行下载包
import pymysql
# 导入config包中导入config,py文件中对数据库进行的配置
from config.config import MYSQL_HOST, MYSQL_PORT, MYSQL_USER, MYSQL_PASSWD, MYSQL_DB
class MysqlDb():
def __init__(self, host, port, user, passwd, database):
# 建立数据库连接
self.conn = pymysql.connect(host=host, port=port, user=user, passwd=passwd, database=database)
self.cur = self.conn.cursor(cursor=pymysql.cursors.DictCursor)
def select_db(self, sql):
"""查询"""
# 检查连接是否断开,如果断开就进行重连
self.conn.ping(reconnect=True)
# 使用 execute() 执行sql
self.cur.execute(sql)
# 使用 fetchall() 获取查询结果
data = self.cur.fetchall()
return data
def __del__(self): # 对象资源被释放时触发,在对象即将被删除时的最后操作
# 关闭游标
self.cur.close()
# 关闭数据库连接
self.conn.close()
def execute_db(self, sql):
"""更新/新增/删除"""
try:
# 检查连接是否断开,如果断开就进行重连
self.conn.ping(reconnect=True)
# 使用 execute() 执行sql
self.cur.execute(sql)
# 提交事务
self.conn.commit()
return "插入成功"
except Exception as e:
# 回滚所有更改
self.conn.rollback()
return "操作出现错误"
# 定义一个实例对象,方便别的文件引用其方法
db = MysqlDb(MYSQL_HOST, MYSQL_PORT, MYSQL_USER, MYSQL_PASSWD, MYSQL_DB)
app.py
import datetime
from flask import Flask, render_template, request, json
from flask_cors import CORS # 处理跨域请求
from flask_socketio import SocketIO, emit # 用来实现服务端和客户端通信
from common import mysql_operate # 导入自己写的数据库操作
# 初始化一个数据库,用于进行数据库操作
db = mysql_operate.db
app = Flask(__name__)
# 开启CORS(跨源资源)以共享处理跨域请求
CORS(app, origins='*', supports_credentials=True)
socketio = SocketIO(app)
@app.route('/')
def index():
return render_template("index.html")
# 返回最新的6行数据,用于生成折线图
@app.route('/query', methods=['GET'])
def query():
sql = "SELECT * FROM data ORDER BY id DESC LIMIT 10; "
data = db.select_db(sql)
# 返回json类型的数据
return json.jsonify(data)
# 根据接受到的数据返回一定时间范围内的数据
@app.route('/record', methods=['POST'])
def record():
data = request.get_json()
start_time = data['start_time'].replace('T', ' ')
end_time = data['end_time'].replace('T', ' ')
sql = "select * from data where time >= '{}' and time <= '{}'".format(start_time, end_time)
data = db.select_db(sql)
return json.jsonify(data)
# 上传数据
@app.route('/upload', methods=['POST'])
def upload():
data = request.get_json()
temperature = data['temperature']
humidity = data['humidity']
time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
insert = 'insert into data(temperature, humidity, time) values("{}","{}","{}")'.format(
temperature, humidity, time)
db.execute_db(insert)
return "upload success!"
# 下载最新的一行数据
@app.route('/download', methods=['GET'])
def download():
sql = "SELECT * FROM data ORDER BY id DESC LIMIT 1; "
data = db.select_db(sql)
# 返回json类型的数据
return json.jsonify(data)
# 服务器被连接后发送一个名为response的事件,内容为'Hello Client!'
@socketio.on('connect')
def handle_connect():
emit('response', 'Hello Client!')
# 服务器接受到事件message后的响应
@socketio.on('message')
def handle_message(message):
print('received message: ' + message)
emit('response', 'Hello Client!')
if __name__ == '__main__':
socketio.run(app) # 以支持 WebSocket的通信的方式启动服务器
前端html界面编写
在templates下新建index.html
// An highlighted block
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>温湿度显示</title>
<!-- 引入BootStrap v3-->
<link rel="stylesheet" href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.4.1/css/bootstrap.min.css" integrity="sha384-HSMxcRTRxnN+Bdg0JdbxYKrThecOKuH5zCYotlSAcp1+c8xmyTe9GYg1l9a69psu" crossorigin="anonymous">
<script src="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.4.1/js/bootstrap.min.js" integrity="sha384-aJ21OjlMXNL5UyIl/XNwTMqvzeRMZH2w8c5cRVpzpU8Y5bApTppSuUkhZXN0VxHd" crossorigin="anonymous"></script>
<!-- 引入jquery-->
<script src="https://code.jquery.com/jquery-1.12.4.min.js"></script>
<!-- 引入ECharts JS -->
<script type="text/javascript" src="https://registry.npmmirror.com/echarts/5.5.0/files/dist/echarts.min.js"></script>
<!-- 用于将JSON中的时间数据切换为'YYYY-MM-DD HH:mm:ss'的格式 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.1/moment.min.js"></script>
<!-- 处理客户端与服务器通讯,现在还没有对应功能,可以删掉 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.0/socket.io.js"></script>
</head>
<body>
<!-- 温度表和湿度表的容器-->
<div class="container panel panel-primary" style=" height:50vh">
<div id="container1" style="height: 100%" class="row col-md-6 col-xs-6"></div>
<div id="container2" style="height: 100%" class="row col-md-6 col-xs-6"></div>
</div>
<!-- 折线图容器-->
<div class="container panel panel-primary" style=" height:50vh">
<div id="container3" style="height: 100%"></div>
</div>
<!-- 历史记录容器 -->
<div class="container panel panel-primary" style=" height:50vh">
<div id="container4" style="height: 20%" class="col-md-12">
<!-- 显示区域 -->
<div style="width: 100%;height:50%;min-height: 300px;border:1px solid #dddddd;margin-top: 10px;overflow: auto;">
<table class="table table-hover" id="history_table">
<tr>
<td class="col-md-3">序号</td>
<td class="col-md-3">温度</td>
<td class="col-md-3">湿度</td>
<td class="col-md-3">时间</td>
</tr>
</table>
</div>
<!-- 日历区域,这里将其放到form里,便于一起打包提交数据 -->
<form method="post" id="form_post">
<div class="col-md-offset-3 col-md-12">
<label class="col-md-1">起始时间</label>
<input class="col-md-2" type="datetime-local" name="start_time" />
<label class="col-md-1">结束时间</label>
<input class="col-md-2" type="datetime-local" name="end_time" />
</div>
</form>
<!-- 按钮区域-->
<div class="col-md-offset-5 col-xs-offset-4 col-md-12 ">
<button type="button" id="record_query" class="btn btn-default">查找记录</button>
<button type="button" id="data_send" class="btn btn-default">发送数据</button>
</div>
</div>
</div>
<!-- 温度与湿度表 -->
<script type="text/javascript">
var dom1 = document.getElementById('container1');
var dom2 = document.getElementById('container2');
var temperature_Chart = echarts.init(dom1, null, {
renderer: 'canvas',
useDirtyRect: false
});
var humidity_Chart = echarts.init(dom2, null, {
renderer: 'canvas',
useDirtyRect: false
});
// option1 和 option2 分别存储温度表和湿度表的表格设置
var option1;
var option2;
option1 = {
title: {
text: '温度表'
},
series: [
{
type: 'gauge',
center: ['50%', '60%'],
startAngle: 200,
endAngle: -20,
min: 0,
max: 60,
splitNumber: 12,
itemStyle: {
color: '#FFAB91'
},
progress: {
show: true,
width: 30
},
pointer: {
show: false
},
axisLine: {
lineStyle: {
width: 30
}
},
axisTick: {
distance: -45,
splitNumber: 5,
lineStyle: {
width: 2,
color: '#999'
}
},
splitLine: {
distance: -52,
length: 14,
lineStyle: {
width: 3,
color: '#999'
}
},
axisLabel: {
distance: -20,
color: '#999',
fontSize: 20
},
anchor: {
show: false
},
title: {
show: false
},
detail: {
valueAnimation: true,
width: '60%',
lineHeight: 40,
borderRadius: 8,
offsetCenter: [0, '-15%'],
fontSize: 60,
fontWeight: 'bolder',
formatter: '{value} °C',
color: 'inherit'
},
data: [
{
value: 0
}
]
},
{
type: 'gauge',
center: ['50%', '60%'],
startAngle: 200,
endAngle: -20,
min: 0,
max: 60,
itemStyle: {
color: '#FD7347'
},
progress: {
show: true,
width: 8
},
pointer: {
show: false
},
axisLine: {
show: false
},
axisTick: {
show: false
},
splitLine: {
show: false
},
axisLabel: {
show: false
},
detail: {
show: false
},
data: [
{
value: 0
}
]
}
]
};
option2 = {
title: {
text: '湿度表'
},
series: [
{
type: 'gauge',
center: ['50%', '60%'],
startAngle: 200,
endAngle: -20,
min: 0,
max: 60,
splitNumber: 12,
itemStyle: {
color: '#FFAB91'
},
progress: {
show: true,
width: 30
},
pointer: {
show: false
},
axisLine: {
lineStyle: {
width: 30
}
},
axisTick: {
distance: -45,
splitNumber: 5,
lineStyle: {
width: 2,
color: '#999'
}
},
splitLine: {
distance: -52,
length: 14,
lineStyle: {
width: 3,
color: '#999'
}
},
axisLabel: {
distance: -20,
color: '#999',
fontSize: 20
},
anchor: {
show: false
},
title: {
show: false
},
detail: {
valueAnimation: true,
width: '60%',
lineHeight: 40,
borderRadius: 8,
offsetCenter: [0, '-15%'],
fontSize: 60,
fontWeight: 'bolder',
formatter: '{value} RH',
color: 'inherit'
},
data: [
{
value: 0
}
]
},
{
type: 'gauge',
center: ['50%', '60%'],
startAngle: 200,
endAngle: -20,
min: 0,
max: 60,
itemStyle: {
color: '#FD7347'
},
progress: {
show: true,
width: 8
},
pointer: {
show: false
},
axisLine: {
show: false
},
axisTick: {
show: false
},
splitLine: {
show: false
},
axisLabel: {
show: false
},
detail: {
show: false
},
data: [
{
value: 0
}
]
}
]
};
function update() {
$.ajax({
// url里填http://127.0.0.1:5000/download http://192.168.230.240:5000/download 都是可以的,这样写是为了内网穿透时能够识别到自己的api
url: "/download",
type: "GET",
dataType: "json",
success: function (data) {
setHumidity(data[0].humidity); //不知道为什么,返回的是一个数组,所以就用这样的方式读取方式
setTemperature(data[0].temperature);
}
})
}
function setHumidity(humidity) {
humidity_Chart.setOption({
series: [
{
data: [
{
value: humidity
}
]
},
{
data: [
{
value: humidity
}
]
}
]
});
}
function setTemperature(temperature) {
temperature_Chart.setOption({
series: [
{
data: [
{
value: temperature
}
]
},
{
data: [
{
value: temperature
}
]
}
]
});
}
update();
setInterval(update, 3000); // wifi模块每3s发送一次数据,所以这里也3s刷新一次
if (option1 && typeof option1 === 'object') {
temperature_Chart.setOption(option1);
}
if (option2 && typeof option2 === 'object') { //不知道什么用,那就别动它
humidity_Chart.setOption(option2);
}
window.addEventListener('resize', temperature_Chart.resize);
window.addEventListener('resize', humidity_Chart.resize);
</script>
<!-- 折线图 -->
<script type="text/javascript">
var dom = document.getElementById('container3');
var line_Chart = echarts.init(dom, null, {
renderer: 'canvas',
useDirtyRect: false
});
var option;
option = {
title: {
text: '温湿度折线图'
},
tooltip: {
trigger: 'axis'
},
legend: {
data: ['温度', '湿度']
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
toolbox: {
feature: {
saveAsImage: {}
}
},
xAxis: {
type: 'category',
boundaryGap: false,
data: []
},
yAxis: {
type: 'value'
},
series: [
{
name: '温度',
type: 'line',
stack: 'Total',
data: []
},
{
name: '湿度',
type: 'line',
stack: 'Total',
data: []
},
]
};
function update() {
$.ajax({
url: "/query",
type: "GET",
dataType: "json",
success: function (data) {
line_Chart.setOption({
xAxis: {
type: 'category',
boundaryGap: false,
data: [data[9].time.substring(17, 25),data[8].time.substring(17, 25),data[7].time.substring(17, 25),data[6].time.substring(17, 25),data[5].time.substring(17, 25), data[4].time.substring(17, 25), data[3].time.substring(17, 25), data[2].time.substring(17, 25), data[1].time.substring(17, 25), data[0].time.substring(17, 25)]
},
series: [
{
name: '温度',
type: 'line',
stack: 'Total',
data: [data[9].temperature,data[8].temperature,data[7].temperature,data[6].temperature,data[5].temperature, data[4].temperature, data[3].temperature, data[2].temperature, data[1].temperature, data[0].temperature]
},
{
name: '湿度',
type: 'line',
stack: 'Total',
data: [data[9].humidity,data[8].humidity,data[7].humidity,data[6].humidity,data[5].humidity, data[4].humidity, data[3].humidity, data[2].humidity, data[1].humidity, data[0].humidity]
},
]
});
}
})
}
update();
setInterval(update, 30000); // 调用最新6个数据,3*6=18,正好实现完全刷新图表
if (option && typeof option === 'object') {
line_Chart.setOption(option);
}
window.addEventListener('resize', line_Chart.resize);
</script>
<!--按钮(查找记录)的响应事件-->
<script type="text/javascript">
$("#record_query").click(function () {
// 获取表中信息
const form = document.getElementById('form_post');
const formData = new FormData(form);
const dataObject = {};
for (let [name, value] of formData.entries()) {
dataObject[name] = value;
}
// 将获取的信息打包成JSON格式
const jsonData = JSON.stringify(dataObject);
$.ajax({
url: "/record",
type: "POST",
contentType: "application/json",
data: jsonData,
success: function (data) {
initTable(data)
}
});
});
// 根据获取到的JSON数据包更新表格
function initTable(data) {
const table = document.getElementById('history_table');
data.forEach(item => {
time = moment.utc(item.time).format('YYYY-MM-DD HH:mm:ss');
$('<tr>')
.append($('<td>').text(item.id))
.append($('<td>').text(item.temperature))
.append($('<td>').text(item.humidity))
.append($('<td>').text(time))
.appendTo('#history_table tbody');
});
}
</script>
<!-- 客户端与服务器的通讯事件,待扩展,把注释去掉即可运行
<script type="text/javascript" charset="utf-8">
var socket = io.connect('http://' + document.domain + ':' + location.port);
socket.on('connect', function() {
console.log('Connected to the server!');
});
socket.on('response', function(message) {
console.log('received response: ' + message);
// 处理服务器返回的信息
});
</script>
-->
</body>
</html>
验证
点击右上角的运行,出现如下界面
点击网址后界面如下所示(我的数据库已经存好数据了)
Arduino板配置
具体连接可以参考博客使用Arduino UNO板、DHT11温湿度传感器和ESP-01S WiFi模块,并搭建spring boot框架后台实现物联。这里只给出代码
#include <SoftwareSerial.h>
#include <DHT11.h>
#define POST_TIME 3000
SoftwareSerial espSerial(2, 3); // RX, TX
DHT11 dht11(12); // 12号引脚用于连接温湿度传感器
const String ssid = "kabi"; // Wi-Fi名
const String password = "xingzhikabi"; // Wi-Fi密码
const String host = "192.168.230.240"; // 局域网内地址
const int port = 5000; //Flask服务器端口号
const String url = "http://192.168.230.240:5000/upload"; //api
void setup()
{
Serial.begin(115200);
espSerial.begin(115200);
connectToServer();
}
void loop()
{
/* 读取温湿度,打包至postData里 */
//改为float试一下
int temperature = 0;
int humidity = 0;
int result = dht11.readTemperatureHumidity(temperature, humidity);
String postData = "{ \"humidity\":\"" + String(humidity) + "\",\"temperature\":\"" + String(temperature) + "\"}";
/* 调用的api以及发送数据 */
sendHttpPostRequest(postData);
delay(POST_TIME);
}
void connectToServer()
{
espSerial.println("AT+CWQAP"); //断开当前连接,用于在不断电的情况下刷新WiFi模块
bool wifiConnected = false;
bool flag = false;
espSerial.println("AT+CWMODE=1"); // 设置模式为 Station mode(如果在串口调试器已经调试好了的,可以不用在这里设置)
while (!wifiConnected)
{
espSerial.println("AT+CWJAP=\"" + String(ssid) + "\",\"" + String(password) + "\"" + "\r\n"); // 连接WiFi
delay(2000);
if (espSerial.find("OK")) {
Serial.println("Connected to WiFi.");
wifiConnected = true; // 设置标志位为true,跳出循环
break;
} else {
Serial.println("Connection failed. Retrying...");
delay(2000);
}
}
espSerial.println("AT+CIPMODE=1"); // 设置透传模式,目的是为了持续地发送数据。不设置的话,就需要声明发送数据的长度,无法实现持续传输
espSerial.println("AT+CIPMUX=0"); // 设置单连接模式
while(!flag)
{
espSerial.println("AT+CIPSTART=\"TCP\",\"" + host + "\"," + String(port)); //连接到服务器
delay(2000);
if(espSerial.find("OK")){
Serial.println("Connected to Server.");
flag=true;
break;
}
else{
Serial.println("Connected to Server failed.");
delay(2000);
}
}
espSerial.println("AT+CIPSEND"); // 开始发送数据
}
void sendHttpPostRequest(const String& postData) {
espSerial.println("POST /upload HTTP/1.1"); // 注意这里,要和自己定义的api相对应
espSerial.println("Host: "+ host + ":" + port);
espSerial.println("Content-Type: application/json");
espSerial.println("Content-Length: " + String(postData.length()));
espSerial.println();
espSerial.print(postData);
Serial.println("发送数据:"+ postData); // 检验发送数据
}