falsk+flask_socketio+socket.io Client设置温度仪表盘实时数据更新

NO.1
前言

上期文章描述使用vue+echarts设置温度仪表盘并自定义区段颜色

本期文章接着用python flask设置socket服务和前端通信实时获取温度数据

主要内容为

  1. 后端设置flask服务并启用socket

  2. 前端使用socket.io(websocket)实时获取温度数据

  3. arduino和python usb端口通信的关闭和独占访问模式(仅POSIX)

NO.2
设置flask

安装依赖flask

pip install flask

安装依赖flask_cors

pip install flask_cors

安装依赖flask_socketio

pip install flask_socketio

以下代码使用python flask启动服务并支持socket

# 引入flask
from flask import Flask
# 引入跨域设置
from flask_cors import CORS
# 引入socketio
from flask_socketio import SocketIO
from flask_socketio import send, emit


# 实例化app
app = Flask(__name__)

# 设置跨域
CORS(app)
# 设置socket
socketio = SocketIO(app, cors_allowed_origins='*')

# 运行主程序
if __name__ == '__main__':
    # app.run()
    socketio.run(app)

服务启动后,默认地址为

http://127.0.0.1:5000/

NO.3
前端socket

安装依赖socket.io

npm install socket.io

vue-cli项目中引用socket.io client

import io from "socket.io-client";

建立socket连接

格式const socket = io(server url);

const socket = io("http://127.0.0.1:5000");

NO.4
设置事件

服务端定义事件接收

  • 使用装饰器@socketio.on

  • 事件名自定义,此处为hello

  • 收到socket信息后打印数据到控制台台

  • emit为发送给前端定义的事件响应

@socketio.on('hello')
def hello(str):
    print("接收到的数据为->", str)
    emit('reply', "reply server")

前端定义事件接收

  • 使用socket.on定义事件名

  • 自定义事件名为replay

  • 收到socket信息后打印数据到控制台台

socket.on("reply", data => {
    console.log("reply", data);
});

事件通信测试

前端发起hello事情,并传递参数字符串world

socket.emit("hello", "world");

后端控制台日志如图

socket

前端收到回应如图

socket

NO.5
实时数据

原先设计为

  1. 储存到sqlite温湿度数据和时间戳

  2. 前端通过http轮询或者socket获取最后一次数据库数据

  3. 比对当前时间戳,误差3秒左右为可用数据

  4. 时间跨度大,则图表置灰,数据不可用(因为不是当前的温度)

  5. 设置为动态更新折线图

现在改为

  1. 通过socket每秒一次通信事情开启usb串口,获取数据

  2. 服务器获取到温湿度数据后通过socket事情传给前端,然后关闭串口

  3. 前端拿到数据后绘制/更新仪表盘数据(不用折线图),然后一秒后再次请求数据(频率前端自行控制)

  4. 循环往复

NO.6
实际代码

前端socket.vue,具体细节参考注释

<template>
    <section class="socket">
        <!-- 用于渲染仪表盘的DOM -->
        <div id="main" class="main"></div>
    </section>
</template>
<script>
import io from "socket.io-client";
export default {
    name: `socket`,
    data() {
        return {
            myChart: null,
            option: {
                tooltip: {
                    formatter: "{a} <br/>{b} : {c}℃"
                },
                toolbox: {
                    feature: {
                        restore: {},
                        saveAsImage: {}
                    }
                },
                series: [
                    {
                        name: "当前温度",
                        type: "gauge",
                        min: 0,
                        max: 40,
                        detail: { formatter: "{value}℃" },
                        data: [{ value: 26, name: "温度" }],
                        axisLine: {
                            lineStyle: {
                                color: [
                                    [0.5, "#4dabf7"],
                                    [0.65, "#69db7c"],
                                    [0.8, "#ffa94d"],
                                    [1, "#ff6b6b"]
                                ]
                            }
                        }
                    }
                ]
            }
        };
    },
    created() {
        // 页面初始化时建立socket连接
        this.conSocket();
    },
    mounted() {
        // DOM更新后渲染仪表盘图表
        this.$nextTick(() => {
            this.initEcharts();
        });
    },
    methods: {
        conSocket() {
            let that = this;
            console.log(`开始建立socket连接`);
            const socket = io("http://127.0.0.1:5000");
            // 第一次请求获取温度数据
            socket.emit("getData", "world");
            // 接收温度数据socket响应
            socket.on("reply", data => {
                console.log("reply", data);
                // 获取温度并设置到echarts参数
                that.option.series[0].data[0].value = data.temperature || 0;
                // 更新echarts
                let echarts = require("echarts");
                let myChart = echarts.init(document.getElementById("main"));
                myChart.setOption(that.option, true);
                // 更新后再次获取服务端最新温度数据
                socket.emit("getData", "world");
            });
        },
        // 初始化仪表盘
        initEcharts() {
            let that = this;
            // 基于准备好的dom,初始化echarts实例
            let echarts = require("echarts");
            let myChart = echarts.init(document.getElementById("main"));
            // 绘制图表
            myChart.setOption(that.option);
        }
    }
};
</script>
<style lang="less" scoped>
.socket {
    .main {
        width: 500px;
        height: 500px;
    }
}
</style>

服务端代码app.py,具体细节参考注释

# 引入flask
from flask import Flask
# 引入跨域设置
from flask_cors import CORS
# 引入socketio
from flask_socketio import SocketIO
from flask_socketio import send, emit

# 引入串口库(注意是serial,不是pyserial)
import serial
# 引入json库
import json
# 引入时间
import time


# 实例化app
app = Flask(__name__)

# 设置跨域
CORS(app)
# 设置socket
socketio = SocketIO(app, cors_allowed_origins='*')


@socketio.on('getData')
# 获取温湿度数据
def getUsbData(strData):
    print("接收到的数据为>>>", strData)
    # 设置端口变量和值
    serialPosrt = "COM3"
    # 设置波特率变量和值
    baudRate = 9600
    # 设置超时时间,单位为s
    timeout = 0.5
    # 获取端口数据
    ser = serial.Serial(serialPosrt, baudRate, timeout=timeout, exclusive=True)
    # 循环获取数据
    while ser.is_open:
        # 读取接收到的数据的第一行
        strData = ser.readline()
        # 把拿到的数据转为字符串(串口接收到的数据为bytes字符串类型,需要转码字符串类型)
        strJson = str(strData, encoding='utf-8')
        # 如果有数据,则进行json转换
        if strJson:
            # 只有当检测到字符串中含有温湿度字符名时才进行json转码,其他的字符串内容不作操作
            if "temperature" in strJson:
                # print("当前接受到的数据位->", strJson)
                # 字符串转为json(每个字符串变量名必须为双引号包括,而不是单引号)
                jsonData = json.loads(strJson)
                # print("转码成功,当前类型为->", type(jsonData))
                # 温度
                temperature = jsonData["temperature"]
                # 湿度
                humidity = jsonData["humidity"]
                # 把数据传给前端
                emit('reply', {
                    "temperature": temperature,
                    "humidity": humidity,
                })
                # 关闭串口
                ser.close()
        else:
            print("当前接收到的数据为空")


# 运行主程序
if __name__ == '__main__':
    # app.run()
    socketio.run(app)

NO.7
tips

serial.Serial新加参数exclusive为True

Exclusive(bool)–设置互斥访问模式(仅POSIX)。如果端口已经以独占访问模式打开,则不能以独占访问模式打开端口。

这样在重复socket连接flask打开串口时,就不会因为独占模式报错无权限开启端口

如果需要优化,也可以加上try except 进行异常处理

本文功能优先,暂不细化

ser = serial.Serial(serialPosrt, baudRate, timeout=timeout, exclusive=True)

is_open:获取串行端口的状态,无论它是否打开

while ser.is_open: 循环获取数据(条件)

当每次通信成功获取和传递数据后,及时关闭串口,条件就不成立,就不会一直读取arduino数据,只有当需要的时候才获取

ser.close():关闭串口

serial.Serial方法会默认init和open端口,所以要使用close开关闭端口

NO.8
文档地址

pyserial文档地址

https://pyserial.readthedocs.io/en/latest/pyserial_api.html

flask-socketio文档地址

https://flask-socketio.readthedocs.io/en/latest/

socket.io文档地址

https://socket.io/

NO.9
总结

本文描述了

  • flask服务端socket代码

  • 前端socket和echarts图表代码

  • 技术构思流程和技术细节tips

其他前置条件

  • usb连接arduino获取温湿度(按之前的文章做好准备工作)

  • 代码中的端口,设备名,其他细节根据自己环境修改

至此,一个实用arduino获取温湿度计,并通过usb串口实现在web显示温度实时仪表盘图表的功能完成

其他湿度,光感,声音等传感器数据都可以用本demo提供的思路和方法来完成实时图表和数据通信以及储存功能

下期计划

  • web面板使用控制arduino的led灯的开关(反向控制下位机)

  • 蓝牙通信和wifi通信等通信协议尝试和深入(脱离有线,开始无线互联)

END.