【串口助手】Python从零开始制作温湿度串口上位机

文章目录

    • 1. 项目介绍
    • 2. 功能简介
    • 3. 开发过程
      • 3.1 准备工作
      • 3.2 编写串口上位机界面
      • 3.3 功能实现
        • 3.3.1 基本功能
        • 3.3.2 整活
      • 3.4 打包 exe 可执行文件

1. 项目介绍

【串口助手】Python从零开始制作温湿度串口上位机_第1张图片

该项目为本人的一次课设,在很多项目开发中,都需要通过上位机来控制或者读取 MCU、MPU 中的数据。上位机和设备间的通信协议有串口、CAN、RS485 等等。本项目基于 python 编写,将串口获取到的数据显示在上位机中,并将数据以可视化图形显示出来。废话少说,上图!!!

【串口助手】Python从零开始制作温湿度串口上位机_第2张图片
【串口助手】Python从零开始制作温湿度串口上位机_第3张图片

2. 功能简介

【串口助手】Python从零开始制作温湿度串口上位机_第4张图片

3. 开发过程

3.1 准备工作

本项目用到的库有 tkinter、pyserial、matplotlib、pyautogui、configparser、webbrowser 等,其中 pyserial 与 pyautogui 需要自行安装其余库皆是 python 自带库。如没有安装过这两个库可以使用以下命令安装。

pip install pyserial
pip install pyautogui

3.2 编写串口上位机界面

首先,先将上位机基本界面框架搭建好,此部分给出代码自行研究。

from tkinter import *
from tkinter.messagebox import *

import ctypes


class zsh_serial:
    def __init__(self):
        self.window = Tk()  # 实例化出一个父窗口
        # self.com = serial.Serial()
        self.serial_combobox = None
        self.bound_combobox = None
        self.txt = None

    def ui(self):
        ############################################
        # 窗口配置
        ############################################
        # self.window = Tk()  # 实例化出一个父窗口
        self.window.title("温湿度串口调试助手")
        # self.window.iconbitmap(default='data\\COM.ico')  # 修改 logo
        width = self.window.winfo_screenwidth()
        height = self.window.winfo_screenheight()
        print(width, height)
        win = '{}x{}+{}+{}'.format(880, 500, width // 3, height // 5)  # {}x{} 窗口大小,+10 +10 定义窗口弹出时的默认展示位置
        self.window.geometry(win)
        self.window.resizable(False, False)

        # 调用api设置成由应用程序缩放
        ctypes.windll.shcore.SetProcessDpiAwareness(1)
        # 调用api获得当前的缩放因子
        ScaleFactor = ctypes.windll.shcore.GetScaleFactorForDevice(0)
        # 设置缩放因子
        self.window.tk.call('tk', 'scaling', ScaleFactor / 75)

        ############################################
        # 串口设置子菜单 1
        ############################################

        # 串口设置
        group_serial_set = LabelFrame(self.window, text="串口设置")
        group_serial_set.grid(row=0, padx=10, pady=10)

        serial_label = Label(group_serial_set, text="串口号")
        serial_label.grid(row=0, column=0, padx=10, pady=10, sticky=W)
        self.serial_combobox = ttk.Combobox(group_serial_set, width=8)
        # self.serial_combobox['value'] = zsh_serial.getSerialPort()
        self.serial_combobox.grid(row=0, column=1, padx=10, pady=10)

        bound_label_set = Label(group_serial_set, text="波特率")
        bound_label_set.grid(row=1, column=0)
        self.bound_combobox = ttk.Combobox(group_serial_set, width=8)
        self.bound_combobox['value'] = ("9600", "19200", "38400", "57600", "115200", "128000")
        self.bound_combobox.grid(row=1, column=1)

        databits_label = Label(group_serial_set, text="数据位")
        databits_label.grid(row=2, column=0, pady=10)
        databits_combobox = ttk.Combobox(group_serial_set, width=8)
        databits_combobox['value'] = ("1", "1.5", "2")
        databits_combobox.grid(row=2, column=1)

        checkbits_label = Label(group_serial_set, text="校验位")
        checkbits_label.grid(row=3, column=0)
        checkbits_combobox = ttk.Combobox(group_serial_set, width=8)
        checkbits_combobox['value'] = ("None", "Odd", "Even")
        checkbits_combobox.grid(row=3, column=1)

        xxx_label = Label(group_serial_set, text="   ")
        xxx_label.grid(row=4, column=0, pady=1)

        # 接收设置
        recv_set = LabelFrame(self.window, text="接收设置")
        recv_set.grid(row=1, padx=10)

        recv_set_v = IntVar()
        recv_set_radiobutton1 = Radiobutton(recv_set, text="ASCII", variable=recv_set_v, value=1)
        recv_set_radiobutton1.grid(row=0, column=0, sticky=W, padx=10)
        recv_set_radiobutton2 = Radiobutton(recv_set, text="HEX", variable=recv_set_v, value=2)
        recv_set_radiobutton2.grid(row=0, column=1, sticky=W, padx=10)

        recv_set_v1 = IntVar()
        recv_set_v2 = IntVar()
        recv_set_v3 = IntVar()
        recv_set_checkbutton1 = Checkbutton(recv_set, text="自动换行", variable=recv_set_v1, onvalue=1, offvalue=2)
        recv_set_checkbutton1.grid(row=1, column=0, padx=10)
        recv_set_checkbutton2 = Checkbutton(recv_set, text="显示发送", variable=recv_set_v2, onvalue=1, offvalue=2)
        recv_set_checkbutton2.grid(row=2, column=0, padx=10)
        recv_set_checkbutton3 = Checkbutton(recv_set, text="显示时间", variable=recv_set_v3, onvalue=1, offvalue=2)
        recv_set_checkbutton3.grid(row=3, column=0, padx=10)

        # 串口操作
        group_serial_event = LabelFrame(self.window, text="串口操作")
        group_serial_event.grid(row=2, padx=10, pady=10)

        self.serial_btn_flag_str = StringVar()
        self.serial_btn_flag_str.set("串口未打开")
        label_name = Label(group_serial_event, textvariable=self.serial_btn_flag_str, bg='#ff001a', fg='#ffffff')
        label_name.grid(row=0, column=0, padx=55, pady=2)

        self.serial_btn_str = StringVar()
        self.serial_btn_str.set("打开串口")
        serial_btn = Button(group_serial_event, textvariable=self.serial_btn_str)
        serial_btn.grid(row=1, column=0, padx=55, pady=10)

        # 数据显示
        self.txt = Text(self.window, width=70, height=26.5, font=("SimHei", 10))
        self.txt.grid(row=0, rowspan=3, column=1, padx=8, pady=10, sticky='s')

        # 串口子菜单设置初值
        self.bound_combobox.set(self.bound_combobox['value'][4])
        databits_combobox.set(databits_combobox['value'][0])
        checkbits_combobox.set(checkbits_combobox['value'][0])
        recv_set_v.set(2)
        recv_set_v1.set(1)
        recv_set_v2.set(2)
        recv_set_v3.set(2)

        ############################################
        # 配置tkinter样式
        ############################################
        # self.window.config(menu=menubar)

        ############################################
        # 退出检测
        ############################################
        def bye():
            self.window.destroy()

        self.window.protocol("WM_DELETE_WINDOW", bye)

        # 窗口循环显示
        self.window.mainloop()


if __name__ == "__main__":
    mySerial = zsh_serial()
    mySerial.ui()

【串口助手】Python从零开始制作温湿度串口上位机_第5张图片

PS:当前版本仅支持 125%缩放 1920x1080分辨率 or 2560x1440分辨率
现在界面还是太简陋了,接下来增加 menu 菜单栏。这里用到了 ttk 子模块,因为 tkinter 没有下拉菜单控件,代码如下:

from tkinter import ttk  # 导入ttk模块,因为Combobox下拉菜单控件在ttk中

# ... 略

    	############################################
        # menu菜单
        ############################################
        menubar = Menu(self.window)  # 创建一个顶级菜单
        menu = MENU(self.window)
        filemenu1 = Menu(menubar, tearoff=False)  # 在顶级菜单menubar下, 创建一个子菜单filemenu1
        filemenu2 = Menu(menubar, tearoff=False)  # 在顶级菜单menubar下, 创建一个子菜单filemenu2
        filemenu3 = Menu(menubar, tearoff=False)  # 在顶级菜单menubar下, 创建一个子菜单filemenu3
        menubar.add_cascade(label="文件", menu=filemenu1)  # 为子菜单filemenu1取个名字
        menubar.add_cascade(label="工具", menu=filemenu2)  # 为子菜单filemenu2取个名字
        menubar.add_cascade(label="折线图", menu=filemenu3)  # 为子菜单filemenu3取个名字
        menubar.add_command(label="帮助", command=menu.callback7)
        menubar.add_command(label="关于", command=menu.callback8)
        filemenu1.add_command(label="更新检测", command=menu.callback9)  # 为子菜单filemenu1添加选项,取名"更新检测"
        filemenu1.add_command(label="获取源码", command=menu.callback1)  # 为子菜单filemenu1添加选项,取名"获取源码"
        filemenu1.add_command(label="博客教程", command=menu.callback10)  # 为子菜单filemenu1添加选项,取名"博客教程"
        filemenu1.add_separator()  # 添加一条分割线
        filemenu1.add_command(label="退出", command=menu.callback2)  # 为子菜单filemenu1添加选项,取名"关闭"
        filemenu2.add_command(label="刷新串口", command=self.cleanSerial)  # 为子菜单filemenu2添加选项,取名"刷新串口"
        filemenu2.add_command(label="截图", command=menu.callback4)  # 为子菜单filemenu2添加选项,取名"截图"
        filemenu3.add_command(label="温度图", command=menu.callback5)  # 为子菜单filemenu2添加选项,取名"温度图"
        filemenu3.add_command(label="湿度图", command=menu.callback6)  # 为子菜单filemenu2添加选项,取名"湿度图"
        
 # ... 略

这一步完成后,是运行不了的,我们要为菜单栏增加回调函数。

import webbrowser

class MENU:
    def __init__(self, init_window_name):
        self.init_window_name = init_window_name

    @staticmethod
    def callback1():
        print("--- 获取源码 ---")
        showwarning("warning", "Please follow the GPL3.0")
        webbrowser.open("https://github.com/Theo-s-Open-Source-Project")

    @staticmethod
    def callback2():
        print("--- 退出 ---")
        sys.exit()

    def callback3(self):
        print("--- 刷新串口 ---")

    @staticmethod
    def callback4():
        print("--- 截图 ---")
        # window_capture()
           
 # ... 略

【串口助手】Python从零开始制作温湿度串口上位机_第6张图片

到此,我们的界面已经搭建完成了,接下来就是注入灵魂的时候,为其增加功能函数。

3.3 功能实现

3.3.1 基本功能

在进行通信前,要先获取电脑可用串口进行连接,借助 pyserial 库的 serial.tools.list_ports.comports() 获取电脑目前所有串口号。

    @staticmethod
    def getSerialPort():
        port = []
        portList = list(serial.tools.list_ports.comports())
        # print(portList)

        if len(portList) == 0:
            print("--- 无串口 ---")
            port.append('None')
        else:
            for comport in portList:
                # print(list(comport)[0])
                # print(list(comport)[1])
                port.append(list(comport)[0])
                pass
        return port

获取到串口号后,将其显示在 tkinter 的 combobox 控件中。

self.serial_combobox['value'] = zsh_serial.getSerialPort()

接下来就是打开串口,这里不做详细讲解(如需要的话评论区留言)给出具体实现代码。

    def openSerial(self, port, bps, timeout):
        """
        打开串口
        :param port: 端口号
        :param bps: 波特率
        :param timeout: 超时时间
        :return: True or False
        """
        ser_flag = False
        try:
            # 打开串口
            self.com = serial.Serial(port, bps, timeout=timeout)
            if self.com.isOpen():
                ser_flag = True
                # threading.Thread(target=self.readSerial, args=(self.com,)).start()
                # print("Debug: 串口已打开\n")
            # else:
            #     print("Debug: 串口未打开")
        except Exception as e:
            print("error: ", e)
            error = "error: {}".format(e)
            showerror('error', error)
        return self.com, ser_flag

将其与打开串口 button 事件进行绑定,代码如下:

 ... 
        self.serial_btn_str = StringVar()
        self.serial_btn_str.set("打开串口")
        serial_btn = Button(group_serial_event, textvariable=self.serial_btn_str, command=self.hit1)  # 添加点击事件
        serial_btn.grid(row=1, column=0, padx=55, pady=10)
 ...
    
    def hit1(self):
        """
        打开串口按钮回调
        """
        # print(self.com.isOpen())
        if self.com.isOpen():
            self.com.close()
            print("--- 串口未打开 ---")
            self.serial_btn_flag_str.set("串口未打开")
            self.serial_btn_str.set("打开串口")
        else:
            self.com, ser_flag = self.openSerial(self.serial_combobox.get(), self.bound_combobox.get(), None)
            if ser_flag:
                print("--- 串口已打开 ---")
                self.serial_btn_flag_str.set("串口已打开")
                self.serial_btn_str.set("关闭串口")

到此,一个串口调试助手的最基本功能就实现了,接下来就是让串口获取到的信息显示到上位机中的 txt 控件上。

【串口助手】Python从零开始制作温湿度串口上位机_第7张图片

我们该如何实时获取并打印串口中的数据呢,这里使用一个线程不断的去读取。

    def readSerial(self, com):
        """
        读取串口数据
        :return:
        """
        global serialData
        while True:
            if self.com.in_waiting:
                textSetial = self.com.read(self.com.in_waiting)
                serialData = textSetial
                # print(textSetial)
                self.txt.config(state=NORMAL)
                self.txt.insert(END, textSetial)
                self.txt.config(state=DISABLED)
            # print("Debug: thread_readSerial is running")

【串口助手】Python从零开始制作温湿度串口上位机_第8张图片

基本功能实现,但现在的上位机还是太单调了,接下来就是整活时间

3.3.2 整活

在最开始时,我们创建了一行菜单栏,接下来为其注入灵魂!

【串口助手】Python从零开始制作温湿度串口上位机_第9张图片

首先是这款上位机的重中之重 ”折线图“(注:当前版本的折线图数据非串口获取到到真实数据,仅做功能演示!!)

【串口助手】Python从零开始制作温湿度串口上位机_第10张图片

    def createTempWindow(self):
        """
        创建新的窗口
        """
        new_window = self.window
        new_window.title("温度折线图")
        new_window.geometry("720x480")
        # Button(new_window,
        #        text="This is new window").pack()

        # 创建一个容器, 没有画布时的背景
        frame = Frame(new_window, bg="#ffffff")
        frame.place(x=0, y=0, width=720, height=480)
        plt.rcParams['font.sans-serif'] = ['SimHei']  # 用来正常显示中文标签
        fig = plt.figure(figsize=(6, 3.9), edgecolor='blue')
        # 定义刻度
        ax = fig.add_subplot(111)
        ax.set(xlim=[0, 121], ylim=[0, 40], title="温度折线图", ylabel='温度/°C')
        canvas = FigureCanvasTkAgg(fig, master=frame)
        canvas.draw()
        # 显示画布
        canvas.get_tk_widget().place(x=0, y=0)

        # 定义存储坐标的空数组
        self.i = 0
        self.x = []
        self.y = []

        def drawTemp():
            global tempData
            self.i += 1
            # time.sleep(1)
            ax.clear()
            ax.set(xlim=[0, 121], ylim=[0, 40], title="温度折线图", ylabel='温度/°C')
            t = self.i
            if t >= 120:
                bye()
            dtax = t
            self.x.append(dtax)
            # 温度数据处理
            """
            xxxxxxxxxxxxxxxxxxxxx
            """
            dtay = random.randint(22, 36)
            # print(dtay)
            self.y.append(dtay)
            ax.plot(self.x, self.y)
            canvas.draw()
            self.afterHandler = self.window.after(100, drawTemp)

        drawTemp()

        def bye():
            plt.close('all')
            new_window.destroy()
            self.window.mainloop()

        new_window.protocol("WM_DELETE_WINDOW", bye)

        # 窗口循环显示
        new_window.mainloop()

将其与菜单栏的回调进行绑定,这里加了一个专业版和社区版的识别函数(是不是有 B 格起来了)

    @staticmethod
    def callback5():
        config = version.config()
        if config['power'] == 'Professional':
            print("--- 温度折线图 ---")
            new_win = zsh_serial()
            new_win.createTempWindow()

【串口助手】Python从零开始制作温湿度串口上位机_第11张图片

通过读取存放在 config.ini 中的 JSON 数据进行分析判断是专业版还是社区版来赋予访问折线图的权限。

from configparser import ConfigParser

class version:
    @staticmethod
    def config():
        """
        获取配置文件
        :return: 读取到的配置文件信息
        """
        config = ConfigParser()
        config.read("src\\config.ini")
        cfg = dict(config.items("config"))  # 字符串转换为字典
        # print(cfg)
        # print(cfg['version'])
        return cfg

社区版会弹出提示框,这里放的二维码是俺的博客地址。实现方法也非常简单,简单来说就是新建一个窗口并显示。这里需要注意的是 tkinter 库的 PhotoImage 函数只能显示 gif 格式的图片,所以需要进行一个图片格式转换。

【串口助手】Python从零开始制作温湿度串口上位机_第12张图片
【串口助手】Python从零开始制作温湿度串口上位机_第13张图片

保存串口信息功能(如下图),实现方法其实很简单,因为在前面将 txt 窗口设为只读模式,所以 copy 串口打印信息时,需要将 txt 控件解除只读,为了保证串口数据不被人为的误改, get 数据后再将其恢复为只读模式。将获取到的数据保存到 txt 文件中,默认保存路径位桌面,这里用到了 os.getlogin() 获取系统用户名。

    def window_save(self):
        self.txt.config(state=NORMAL)
        result = self.txt.get("1.0", "end")
        self.txt.config(state=DISABLED)

        with open('C:\\Users\\{}\\Desktop\\zshSerial.txt'.format(os.getlogin()), 'w') as f:
            for text in result:
                f.write(text)

【串口助手】Python从零开始制作温湿度串口上位机_第14张图片

还记得上面有提到过的更新检测吗(好像没有提到过 bushi ),通过对比服务器上的版本信息进行判断,如果有时间下个版本会更新在线升级(咕咕~)。

【串口助手】Python从零开始制作温湿度串口上位机_第15张图片

    @staticmethod
    def callback9():
        print("--- 更新检测 ---")
        import requests
        ver = requests.get('http://xxx.xxx.xxx.xx/download/open-source-project/zshSerial/version.txt')
        print(ver.text)
        config = version.config()
        if "lastest: v{}".format(config['version']) == ver.text:
            versionCheck = "当前版本:v{} 为最新版".format(config['version'])
            showinfo('更新检测', versionCheck)
        else:
            versionCheck = "当前版本:v{} 版本过低,请及时更新".format(config['version'])
            showwarning('更新检测', versionCheck)
            version.update()

【串口助手】Python从零开始制作温湿度串口上位机_第16张图片

3.4 打包 exe 可执行文件

首先,我们从 GitHub 仓库将源码克隆到本地。

git clone https://github.com/Theo-s-Open-Source-Project/zshSerial.git

克隆下来的文件夹结构如下:

.
├── data  			//存放一些数据
│   ├──COM.png
│   ├──blog.gif
│   ├──xxx.ico
│   ├──readme.txt
├── image           //README文件的图片
├── src             //zshSerial库
│   ├──__pycache__
│   ├──config.ini
│   ├──parameter.py
│   ├──ui.py
├── .gitignore       
├── LICENSE       
├── README.md       //README文档
├── zshSerial.py    //main函数   

准备好后,(这里使用 pyinstaller 打包)打开终端,输入以下命令进行打包。

pyinstaller -D -w -i 1.ico zshSerial.py

【串口助手】Python从零开始制作温湿度串口上位机_第17张图片

打包好的程序存放在当前目录的 dist 文件夹下,用 pyinstaller 打包的文件夹有些许大,这里有几种方法可以压缩(自行百度),but 俺懒得尝试了,使用最简单粗暴的方法 delete!!! 哈哈哈。确保 exe 文件有在运行,选中所有的 .dll 文件 delete 即可。

【串口助手】Python从零开始制作温湿度串口上位机_第18张图片

如果觉得这篇文章对您有帮助可以 Github 给个小星星谢谢大伙!

https://github.com/Theo-s-Open-Source-Project/zshSerial <= 【源码下载链接】

PS:当前版本仅支持 125%缩放 1920x1080分辨率 or 2560x1440分辨率

你可能感兴趣的:(python,python,嵌入式)