PyQt6系列(3)——设计实战:简易串口助手与打包

系列启语

作者属计算机在读本科生,编写的大部分内容属于个人原创,发文章的目的一是为了总结自己的学习路程,帮助正在学习这方面的人士,二是为了能吸引更优秀的人对文章提出建议,共同进步。

系列更新速度根据个人时间安排,预计会在十一前后完成。

设计实战介绍

利用Python3.11.x、PyCharm与Qt-Designer设计并打包一款简易的Windows桌面串口助手工具。

可以实现基础的串口收发、HEX发送接收、保存文件的功能。

经Pyinstaller打包三方文件,生成桌面exe可执行程序。

设计目录

预览

页面设计预览

 功能实现预览

与树莓派5进行串口通信实例:

功能:串口接收数据

HEX接收功能也是可以的

功能:串口HEX发送数据 

 

 功能:串口发送数据

 

程序目录

打包前

打包后

图标文件iconfont

以上三个icon图标文件,可以选择从网上的开源平台下载。

下载时注意选择合适的大小与颜色。

网站链接:iconfont-阿里巴巴矢量图标库

逻辑流程图

原创,可能会有不清晰的地方,见谅。

具体的代码会在后续步骤:

页面设计

使用Qt-Designer设计页面,养成好习惯:功能控件起一个好理解的英文名字。

页面

控件名称

从上到下依次是:接收区清除按钮、保存文件按钮、发送区清除按钮、发送区发送按钮、Hex接收选择框、Hex发送选择框、几个QLabel标签、接收区文本框(readonly只读:也可以在代码中设置此功能)、发送区文本框。

 从上到下依次是:检测串口(COM)按钮、QLabel标签、串口端口Layout(内含一个文本标签和一个下拉列表ComboBox)、波特率Layout、数据位Layout、校验位Layout、停止位Layout。

 从上到下依次是:分割线line1~4、pushButton(串口状态“打开串口”按钮)

功能函数

按照之前系列的方法可得到Uart.py,运行此文件,可以显示刚才设计的页面。

下列代码如果有变量看不太明白的,回到“控件名称”查找对应的控件,理解代码意思。

创建串口助手Uart_Assistant()类

所有的功能函数都放到此类下。

注意:一定要继承Uart_Form类。(经过pyuic生成的代码不叫Uart_Form类,好像是Ui_Form,根据项目大小可以选择不更改。不过建议更改,养成好习惯,根据此类的功能改用好理解的名字)

class Uart_Assistant(Uart_Form):
    def __init__(self):
        super().__init__()
        # 定义串口
        self.ser = serial.Serial()
        # 串口功能初始化
        self.Serial_Init()

 主函数入口也要记得更改!

if __name__ == "__main__":
    app = QApplication(sys.argv)
    ui = Uart_Assistant()

    ui.show()
    sys.exit(app.exec())

功能函数

# 检测串口
def port_detect(self):
# 串口设置功能是否停用
def set_setting_enable(self, enable):
# 串口状态按钮
def port_open_close(self):
# 发送功能
def Send_Text(self, send_string):
# 接收数据
def Data_Receive(self):
# 发送指令
def SendData_Once(self):
# 保存文件
def Save_File(self):
# 发送区清除
def Clear_TextEdit_Send(self):
# 接收区清除
def Clear_TextEdit_Receive(self):
# 串口初始化
def Serial_Init(self):

 功能包

import sys

# 没有serial三方库,去当前虚拟环境pip安装pyserial即可
import serial

# 用于读取当前电脑串口信息
from serial.tools import list_ports

# 安装PyQt6就有的三方库
from PyQt6 import QtCore, QtGui, QtWidgets
from PyQt6.QtCore import QTimer
from PyQt6.QtGui import QIcon
from PyQt6.QtWidgets import QWidget, QApplication, QMessageBox, QFileDialog

串口初始化Serial_Init

常用的串口配置,一般都是用波特率9600和115200,数据位8,校验位N(None)、停止位1。

因此,只设置了波特率可调整功能。提供了9600和115200的选项。

此外,功能函数中还应包含所有按钮的连接槽函数代码。

    def Serial_Init(self):
        # 串口波特率下拉列表选择
        self.comboBox_2.addItem("9600")
        self.comboBox_2.addItem("115200")
        # 串口检测按钮
        self.Button_ComDetect.clicked.connect(self.port_detect)
        # “打开/关闭”串口按钮的选中事件 槽函数:port_open_close()
        self.pushButton.clicked.connect(self.port_open_close)
        # 定时器接受数据
        self.serial_receive_timer = QTimer(self)
        self.serial_receive_timer.timeout.connect(self.Data_Receive)
        # "发送区发送"按钮点击事件
        self.Button_SendSend.clicked.connect(self.SendData_Once)
        # 两个清除按钮的点击事件
        self.Button_SendClear.clicked.connect(self.Clear_TextEdit_Send)
        self.Button_ReceiveClear.clicked.connect(self.Clear_TextEdit_Receive)
        # 保存文件按钮的点击事件
        self.Button_SaveData.clicked.connect(self.Save_File)

串口检测port_detect

代码注释蛮详细的,不懂得可以看注释。

    def port_detect(self):
        self.port_dict = {}  # 检测所有存在的串口 将信息存在字典中 list_ports.comports()返回计算机上所有的串口信息
        port_list = list(list_ports.comports())  # 将其存在列表中
        self.comboBox.clear()  # 清除下拉列表中已有的选项
        for port in port_list:
            self.port_dict["%s" % port[0]] = "%s" % port[1]  # 添加到字典里
            self.comboBox.addItem(port[0])  # 添加到下拉列表选项
        if len(self.port_dict) == 0:
            self.comboBox.addItem('无串口')
        self.pushButton.setEnabled(True)  # 只有点击串口检测按钮之后才有权限打开串口,否则无法开启,也没有弹窗提示

我的电脑会存在两个蓝牙链接上的标准串行(COM3 + 4),所以不会出现无串口的情况。

不过为了兼容性,还是加了个if判断。

串口设置功能是否停用

 将所有的comboBox设置为停用 / 可用:

    def set_setting_enable(self, enable):
        self.comboBox.setEnabled(enable)
        self.comboBox_2.setEnabled(enable)
        self.comboBox_shujuwei.setEnabled(enable)
        self.comboBox_jiaoyanwei.setEnabled(enable)
        self.comboBox_tingzhiwei.setEnabled(enable)

 此函数不连接槽,用于其他槽函数调用。

串口状态按钮功能函数

    def port_open_close(self):
        # 按打开串口按钮 且 串口字典里有值
        if (self.pushButton.text() == '打开串口') and self.port_dict:
            self.ser.port = self.comboBox.currentText()  # 设置端口
            self.ser.baudrate = int(self.comboBox_2.currentText())  # 波特率
            '''
            不知道为什么将数据位校验位和停止位赋给ser串口就会出问题
            按照树莓派上,代开串口只需要设置串口号和波特率的特性,将此三位注释便可
            # self.ser.bytesize = int(self.comboBox_shujuwei.currentText())  # 数据位
            # self.ser.parity = self.comboBox_jiaoyanwei.currentText()  # 校验位
            # self.ser.stopbits = int(self.comboBox_tingzhiwei.currentText())  # 停止位
            '''
            # 捕获 串口无法打开的异常
            try:
                self.ser.open()
            except serial.SerialException:
                QMessageBox.critical(self, "Error", "串口不能正常打开!")
                return None

            # 打开串口接收定时器 周期为2ms
            self.serial_receive_timer.start(2)

            # 判断 串口的打开状态
            if self.ser.isOpen():
                self.pushButton.setText("关闭串口")
                self.pushButton.setIcon(QIcon("Lib/Button_Open.png"))
                self.set_setting_enable(False)

        # 点击关闭串口按钮
        elif self.pushButton.text() == "关闭串口":
            # 停止定时器
            self.serial_receive_timer.stop()
            try:
                self.ser.close()
            except:
                QMessageBox.critical(self, "Error", "串口不能正常关闭!")
                return None
            self.pushButton.setText("打开串口")
            self.pushButton.setIcon(QIcon("Lib/Button_Close.png"))
            self.set_setting_enable(True)

根据pushButton的文本内容,判断当前点击按钮的功能。

打开串口:将用户设置的串口配置给电脑串口,尝试打开串口、打开定时器(这里直接对serial_receive_timer进行开始计数的操作,因此在初始化串口函数中,要定义此定时器)、页面按钮状态改变。

关闭串口:接收定时器停止、尝试关闭、按钮状态改变。(关闭串口时也可以加入将两个checkBox清除、TextEdit清空的功能,此程序暂未添加。直接在结尾补上代码即可。)

发送数据功能Send_Text

此部分参考了CSDN一位大佬的文章,在此感谢,由于我找不到那篇PyQt5设计串口助手的文章了,就无法放链接在这里了。

功能讲解参考代码注释:

    def Send_Text(self, send_string):
        if self.ser.isOpen():
            if send_string != '':
                # 勾选HEX?
                if self.CheckBox_HexSend.isChecked():
                    # 移除头尾的空格或换行符
                    send_string = send_string.strip()
                    sent_list = []
                    while send_string != '':
                        # 检查是否是16进制 如果不是则抛出异常
                        try:
                            # 将send_string前两个字符以16进制解析成整数
                            num = int(send_string[0:2], 16)
                        except ValueError:
                            QMessageBox.critical(self, 'Wrong Data', '请输入十六进制数据,以空格分开!')
                            self.CheckBox_HexSend.setChecked(False)
                            return None
                        else:
                            send_string = send_string[2:].strip()
                            # 将需要发送的字符串保存在sent_list里
                            sent_list.append(num)
                    # 转化为byte
                    single_sent_string = bytes(sent_list)
                # 否则ASCII发送
                else:
                    single_sent_string = (send_string + '\r\n').encode('utf-8')
                # 发送
                self.ser.write(single_sent_string)
        else:
            QMessageBox.warning(self, 'Port Warning', '没有可用的串口,请先打开串口!')
            return None

发送指令SendData_Once

读取TextEdit中的文本,调用上面的发送函数。

    def SendData_Once(self):
        # 获取已输入的字符串
        single_sent_string = self.TextEdit2_Send.toPlainText()
        self.Send_Text(single_sent_string)

 接收数据Data_Receive

    def Data_Receive(self):
        if self.ser.in_waiting > 0:
            data = self.ser.read(self.ser.in_waiting)
            # 勾选HEX ?
            if self.CheckBox_HexReceive.isChecked():
                data = data.hex().upper()  # upper() 可以保证结果属于0~9~A~F
            else:
                data = data.decode('utf-8')  # 解码为UTF-8
            self.TextEdit1_Receive.append(data)

此功能函数链接接收定时器,定时器一旦timeout,就会进入“中断”(此处可以和嵌入式系统单片机中的中断函数相似地理解)。此函数可以叫做“中断函数”。

判断接收缓存区是否有数据? >>  读取缓存区数据  >>  判断勾选HEX?  

>>  Hex转换  >>  TextEdit显示接收数据data

保存文件Save_File

    def Save_File(self):
        text = self.TextEdit1_Receive.toPlainText()
        fileName, _ = QFileDialog.getSaveFileName(self, "Save File", "", "Text Files (*.txt);;All Files (*)")
        if fileName:
            with open(fileName, 'w', encoding='utf-8') as file:
                file.write(text)

 获取接收TextEdit当前的文本并赋值text,使用QFileDialog模块,会打开本地文件管理器,在对应位置保存接收信息为txt文本内容。

    def Save_File(self):
        text = self.TextEdit1_Receive.toPlainText()
        fileName, _ = QFileDialog.getSaveFileName(self, "Save File", "", "Text Files (*.txt);;All Files (*)")
        print(f"Selected file: {fileName}")
        if fileName:
            print(f"Saving text: {text}")
            with open(fileName, 'w', encoding='utf-8') as file:
                file.write(text)

print调试,当时是为了检查代码执行过程。好像可以不加。

TextEdit清除功能

    def Clear_TextEdit_Send(self):
        self.TextEdit2_Send.clear()

    def Clear_TextEdit_Receive(self):
        self.TextEdit1_Receive.clear()

程序打包

Pyinstaller安装

pip install Pyinstaller -i https://pypi.tuna.tsinghua.edu.cn/simple

常用命令行选项

  • -F:将所有文件打包为一个单独的可执行文件。

  • -D:将所有文件打包为一个目录,包含可执行文件和所有依赖的文件。

  • -c:将程序与命令提示符结合在一起,以便在命令提示符下运行。

  • -d:将调试信息打包进可执行文件中。

  • –onefile:将所有文件打包为一个单独的可执行文件。

  • -o:指定输出文件的位置。

  • -w:打包为窗口文件。

  • -p DIR, –path=DIR:设置导入路径,从而导入需要的模块

当你要打包的程序包含pip安装的三方库时,还有以下参数可使用:

  • --paths:指定第三方模块的安装路径。

  • -w:表示窗口程序。

  • --icon:可选项,如果设置了窗口图标,则指定相应文件路径;如果没有,则省略。

  • 文件名.py:窗口程序的入口文件。

设计打包

PyCharm中,右键PyQt6_Uart_Assistant项目文件夹。

 

在打开的终端中,输入:

pyinstaller -F -w Uart.py

等待大约1min,会生成一些新的文件与文件夹。

见:【程序目录】(打包前  对比  打包后)

不需要管build文件夹,dist目录下有对应名称的exe执行文件,图标是默认图标,如果想要更改Uart.exe的图标可以在打包时,使用--icon参数(具体方法见网络,作者并未使用)。

注意!!

此时,在dist目录下的exe文件,可以执行,但是发现里面的图标图片文件都丢失了。

只需要将Lib文件夹复制到dist目录下,包整Lib文件夹和exe文件同级。

再次打开exe文件,便可以看到程序调用的icon图标图片。

结语

本文主要介绍了如何通过 PyQt6 设计制作 并 打包生成一个基本串口收发功能的应用程序。

它包含一些日常使用串口助手的基本功能。

里面对于计算机当前串口状态的功能仍有小bug,但不影响正常使用!

例如,多次点击某些按钮,按钮按下的先后顺序并没有添加状态标志位。

不过一些重要的先后顺序已添加。

如果是正常的、规范的使用是不会出现问题的。

如果本文有文字问题还请谅解,大体文章不影响理解。

如果你觉得这篇实战设计文章对你有用,记得点赞收藏评论!

有设计上的问题,欢迎在评论区评论,看到会及时回答!

预告

系列(4)预告:PyQt6 & 树莓派5 <石头剪刀布:人机博弈对战平台>

在系列(4)发布之前,会有<树莓派5 — 官方Raspberry Pi OS —Mediapipe框架手部识别>

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值