系列启语
作者属计算机在读本科生,编写的大部分内容属于个人原创,发文章的目的一是为了总结自己的学习路程,帮助正在学习这方面的人士,二是为了能吸引更优秀的人对文章提出建议,共同进步。
系列更新速度根据个人时间安排,预计会在十一前后完成。
设计实战介绍
利用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框架手部识别>