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

Posted QUESTAE

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了串口助手Python从零开始制作温湿度串口上位机相关的知识,希望对你有一定的参考价值。

文章目录

1. 项目介绍

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


2. 功能简介

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()

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()
           
 # ... 略

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

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 控件上。

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

    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")

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

3.3.2 整活

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

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

    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

vb6.0开发的上位机串口助手(自动识别电脑端口号支持文本十六进制发送)

VB是可视化的编程,就是把一些图标控件拖到编程板上,然后用代码编写每个按钮的实现功能,(如:按下按钮会发生什么事)

vb在零几年的时候非常流行,近年来逐渐淡出人们的视野,但是用其来编写一些简单的上位机还是很方便的,可搭配单片机使用。如编写个串口助手,单片机检测到温湿度在上位机上显示等

言归正传,我就是编写了一个串口助手的简单上位机,来给大家分享一下。

程序中含有超多注释,用最直观的大白话解释清楚代码的意思,极利于学习。

首先看一下界面:

内容比较丰富,基本上和我们日常所用串口助手差不多。

介绍一下本程序重要的几部分,也是较难的几部分

一、串口连接部分

可自动识别电脑上的可用端口

我查了很多资料,发现其他人写的程序大多是将串口一一罗列,很不好,如下所示:

                                      

而本次介绍的程序和设备管理器中的端口号一一对应而不是单纯的罗列出来。

'函数功能:查询电脑可用的串口号,将其显示在组合框中
Private Sub Uart_Init()
Dim a
Dim temp%
temp = 0
For a = 1 To 16                                              '循环检查可能存在的16个串口
      MSComm1.CommPort = a
      On Error Resume Next                                   '出现错误时,不会被打断(假如电脑上可用端口为COM1、COM2、COM4,若没有这行代码 当a=3时会报错)
      MSComm1.PortOpen = True                                '打开串口
      If MSComm1.PortOpen = True Then
            Combo1_select.AddItem ("COM" & a), temp          '将检测可用的端口添加到组合框中
            temp = temp + 1
            MSComm1.PortOpen = False
      End If
Next

二、发送数据

采用定时器控件,可连续发送也可手动调用发送


'函数功能:发送数据(判断数据格式)
'如何实现:1、通过开启定时器连续发送; 2、直接调用该函数单词发送
Private Sub Timer1_Timer()
    Dim longth As Integer
    
    If Option3.Value = True Then
    intOutMode = 1
    Else
    intOutMode = 0
    End If

    strSendText = Text2.Text
    If intOutMode = 0 Then
        MSComm1.Output = strSendText
    Else
        longth = strHexToByteArray(strSendText, bytSendByte())      '只要勾选了十六进制发送,Text2中的数据都是十六进制的ASCII码(文本形式)(如1的十六进制ASCII码为 31)
        
        If longth > 0 Then
            MSComm1.Output = bytSendByte
        End If
        
    End If

End Sub

三、接受数据

用MSComm控件的OnComm事件。当接收到数据时,就会触发OnComm事件,从而接收到数据。


'只要有通讯错误或事件发生时都会产生 OnComm 事件
Private Sub MSComm1_OnComm()
    
    Dim bytInput() As Byte
    Dim intInputLen As Integer
    Dim n As Integer
    Dim teststring As String
    
    Select Case MSComm1.CommEvent
        
        
        Case comEvReceive                            '接受事件中断
            If Option1.Value = True Then
            MSComm1.InputMode = 1                    '1:十六进制显示
            Else
            MSComm1.InputMode = 0                    '0:文本方式显示
            End If
            
            intInputLen = MSComm1.InBufferCount
            bytInput = MSComm1.Input                 '提取接收缓冲区中的数据(数据格式:十进制的ascii码)
            
            If Option1.Value = True Then
            For n = 0 To intInputLen - 1
               Text1.Text = Trim(Text1.Text) & " " & IIf(Len(Hex$(bytInput(n))) > 1, Hex$(bytInput(n)), "0" & Hex$(bytInput(n)))    'hex函数:用十进制的ASCII码 返回十六进制的String
            Next n
            Else
            teststring = bytInput
            Text1.Text = Text1.Text + teststring
            End If
            
    End Select
    Text1.SelStart = Len(Text1.Text) '光标移到最后,每次显示最后一行
End Sub

在这里重点强调一下MSComm控件。

该控件就时串口通讯最主要的控件,串口连接、串口通讯都是该控件。

当新加载工程时,在左侧控件窗口中是没有该控件的,如下图:

需要手动去添加:工程——>部件——>Microsoft Comm Control 6.0前面勾选上

完整开源工程:https://download.csdn.net/download/m0_59113542/76680304

下面这个也是用vb6.0做的温度采集控制系统,单片机测数据经过串口传输给上位机显示。

开源工程:https://download.csdn.net/download/m0_59113542/77294120

欢迎大家提出宝贵的意见。

有机会一起讨论代码,嘿嘿嘿!

以上是关于串口助手Python从零开始制作温湿度串口上位机的主要内容,如果未能解决你的问题,请参考以下文章

vb6.0开发的上位机串口助手(自动识别电脑端口号支持文本十六进制发送)

Zigbee无线传感网 CC2530+DHT11&DS18B20 温湿度采集 串口上位机显示

基于单片机双路温度检测报警系统设计(串口上位机控制)

C#上位机专题06 - 串口助手开发(完善发送,支持中文,保存信息)

stm32发送数据给上位机用串口调试助手接收为什么只接收到第一个字节数据?

electron制作上位机软件篇使用serialport进行串口通信