使用flask框架与ESP8266WiFi模块实现dht11上传数据至网页,附带内网穿透

前言

本文介绍如何搭建一个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获取最新的温湿度数据
仪表盘每3s从后台获取最新的温湿度数据并显示
每30s获取最新的10份数据
折线图每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编写

  1. 安装Flask
// 在命令行里输入
 pip install flask
  1. 新建Flask项目
    在这里插入图片描述
  2. 导入需要的包
// 在pycharm的terminal里输入
pip install flask-socketio
pip install flask-cors
pip install pymysql
  1. 设置访问权限
    打开项目的设置功能在这里插入图片描述
    点击Modify options,勾选Additional options
    在这里插入图片描述
    之后再新出先的输入框里输入--host=0.0.0.0
    在这里插入图片描述
    之后点击apply即可
  2. 代码部分
    总体结构如下所示,新创建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);  // 检验发送数据
}

总结

  • 29
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值