超级好用的Python QT GUI串口调试助手
开发工具: pycharm-community-2022.1.2 + Python38 + PyQt5
目录
1.PyQt5_Serial_Debug_Assistant_V1.0源码
前言
Python串口调试助手支持常用的50bps - 10Mbps波特率,能设置校验、数据位和停止位,能以ASCII码或十六进制接收或发送任何数据或字符,可以任意设定自动发送周期,并能将接收数据实时保存成文本文件,能发送任意大小的数据或字符。
备注: V1.0为简单Demo,适合初级用户使用,V1.1可用于工程应用
高级版: 【博客9】缤果PyQt5串口调试助手V2.0(高级篇)
英文名:PyQt5_Serial_Debug_Assistant_V1.1
支持:常用的50bps ~ 10Mbps波特率
类型:串口调试助手
软件大小:18M
软件版本:V1.1
软件下载&更新:百度网盘链接_提取码 6666
一、软件概要:
一款强大而稳定的PyQt串口调试助手,支持常用的110-921600bps波特率及自定义波特率,波特率最高可支持8000000(串口硬件有关),可适应于非标准波特率。支持动态修改串口和波特率,保存当前日志,打开当前日志以及实时保存日志(默认按每小时分包保存日志,100M自动清空接收窗口,防止UI阻塞)。
二、软件界面:
1.App动态演示
2.其他扩展展示
PyQt5_Serial_Debug_Assistant_V1.1
PyQt5_Serial_Debug_Assistant_V1.1_串口UI布局
PyQt5_Serial_Debug_Assistant_V1.0
三、main.py源码:
1.PyQt5_Serial_Debug_Assistant_V1.0源码
代码如下(示例):
import sys
import serial # 导入模块 #安装: pip3 install pyserial
import serial.tools.list_ports
import webbrowser
import time
import datetime
# 导入Ui设计
from PyQt5.QtWidgets import QApplication, QMainWindow # 串口Ui文件
from PyQt5.QtWidgets import QMessageBox, QLabel, QFileDialog
from PyQt5.QtCore import QTimer
from PyQt5.QtGui import QIcon
import Serial_Ui_Designer # 串口UI文件
# PyQt5程序打包
# 1.使用PyInstaller来打包:
# 安装:pip3 install PyInstaller
# 打包:pyinstaller -F -w -i=Com.ico main.py #单文件打包-优缺点:只生成exe文件,但文件大,打开软件时加载时间长
# 打包:pyinstaller -D -w -i=Com.ico main.py #多文件打包-优缺点:生成exe关联包,可删除无效库,文件小,运行顺畅
# 主窗口
class PyQt5_Serial(QMainWindow, Serial_Ui_Designer.Ui_MainWindow):
# 初始化程序
def __init__(self):
super(PyQt5_Serial, self).__init__()
self.setupUi(self)
self.Init()
self.Qt5_Ui_Init()
# 初始化
def Init(self):
self.ser = serial.Serial()
self.port_check()
self.baudrateBox.setCurrentIndex(5) # 921600-20 9600-5
self.dataBitsBox.setCurrentIndex(3) # 8
# 设置Logo和标题
self.setWindowIcon(QIcon('Com.ico'))
self.setWindowTitle("PyQt5_串口调试助手_V1.0")
#self.setWindowTitle("PyQt5_Serial_Debug_Assistant_V1.0")
# 发送数据和接收数据数目置零
self.data_num_sended = 0
self.label_Tx.setText(str(self.data_num_sended))
self.data_num_received = 0
self.label_Rx.setText(str(self.data_num_received))
# 串口关闭按钮使能关闭
self.sendButton.setEnabled(0)
self.checkBox_autoSend.setEnabled(0)
# 发送框、文本框清除
self.sendTextEdit.setText("")
self.recvTextEdit.setText("")
# 建立控件信号与槽关系
def Qt5_Ui_Init(self):
# 串口检测按钮
self.pushButton_Refresh.clicked.connect(self.port_check)
# 串口打开按钮
self.pushButton_Open_Close.clicked.connect(self.port_open_close)
# 定时发送数据
self.timer_send = QTimer()
self.timer_send.timeout.connect(self.data_send)
self.checkBox_autoSend.stateChanged.connect(self.data_send_timer)
# 发送数据按钮
self.sendButton.clicked.connect(self.data_send)
# 保存日志
self.pushButton_saveLog.clicked.connect(self.savefiles)
# 加载文件
self.pushButton_openLog.clicked.connect(self.openfiles)
# 跳转链接
self.commandLinkButton.clicked.connect(self.link)
# 清除发送按钮
self.pushButton_ClearSend.clicked.connect(self.send_data_clear)
# 清除接收按钮
self.pushButton_ClearRecive.clicked.connect(self.receive_data_clear)
# RTS
self.checkBox_RTS.clicked.connect(self.rts_handle)
# DTR
self.checkBox_DTR.clicked.connect(self.dtr_handle)
# 串口检测
def port_check(self):
# 检测所有存在的串口,将信息存储在字典中
self.Com_Dict = {}
port_list = list(serial.tools.list_ports.comports())
self.portNameBox.clear()
for port in port_list:
self.Com_Dict["%s" % port[0]] = "%s" % port[1]
self.portNameBox.addItem(port[0])
# 无串口判断
if len(self.Com_Dict) == 0:
self.portNameBox.addItem("无串口")
# 打开/关闭串口
def port_open_close(self):
if self.pushButton_Open_Close.text() == "打开串口":
self.port_open()
else:
self.port_close()
# 打开串口
def port_open(self):
port = self.portNameBox.currentText()
# print("port:", port)
baudrate = int(self.baudrateBox.currentText())
# print("baudrate:", baudrate)
bytesize = int(self.dataBitsBox.currentText()) # 数据位
# print("bytesize:", bytesize)
parity = self.ParityBox.currentText() # 校验位
# print("parity:", parity)
stopbits = self.stopBitsBox.currentText() # 停止位
# print("stopbits:", stopbits)
flowctrl = self.flowControlBox.currentText() # 流控
self.ser.port = port
self.ser.baudrate = baudrate
# print("bytesize:", bytesize)
if bytesize == 5:
self.ser.bytesize = serial.FIVEBITS
elif bytesize == 6:
self.ser.bytesize = serial.SIXBITS
elif bytesize == 7:
self.ser.bytesize = serial.SEVENBITS
elif bytesize == 8:
self.ser.bytesize = serial.EIGHTBITS
else:
self.ser.bytesize = serial.EIGHTBITS
# print("parity:", parity)
if parity == "None":
self.ser.parity = serial.PARITY_NONE
elif parity == "Even":
self.ser.parity = serial.PARITY_EVEN
elif parity == "Odd":
self.ser.parity = serial.PARITY_ODD
elif parity == "Space":
self.ser.parity = serial.PARITY_SPACE
elif parity == "Mark":
self.ser.parity = serial.PARITY_MARK
else:
self.ser.parity = serial.PARITY_NONE
# print("stopbits:", stopbits)
if stopbits == "1":
self.ser.stopbits = serial.STOPBITS_ONE
elif stopbits == "1.5":
self.ser.parity = serial.STOPBITS_ONE_POINT_FIVE
elif stopbits == "2":
self.ser.parity = serial.STOPBITS_TWO
else:
self.ser.stopbits = serial.STOPBITS_ONE
self.ser.xonxoff = False # 软件流控
self.ser.rtscts = False # 硬件流控 RTS
self.ser.dsrdtr = False # 硬件流控 DTR
# print("flowctrl:", flowctrl)
if flowctrl == "OFF":
self.ser.xonxoff = False # 软件流控
self.ser.rtscts = False # 硬件流控 RTS
self.ser.dsrdtr = False # 硬件流控 DTR
elif flowctrl == "Hardware":
if self.checkBox_DTR.isChecked():
self.ser.dsrdtr = True #硬件流控 DTR
if self.checkBox_RTS.isChecked():
self.ser.rtscts = True #硬件流控 RTS
elif flowctrl == "Software":
self.ser.xonxoff = True # 软件流控
self.ser.rtscts = False # 硬件流控 RTS
self.ser.dsrdtr = False # 硬件流控 DTR
# print(self.ser)
# Serial < id = 0x4883040, open = False > (port='COM1', baudrate=9600, bytesize=8, parity='N', stopbits=1, timeout=None, xonxoff=False, rtscts=False, dsrdtr=False)
try:
self.ser.open()
except:
QMessageBox.critical(self, "串口异常", "串口打开失败! 错误: 拒绝访问(被占用).")
return None
# 串口打开后,切换开关串口按钮使能状态,防止失误操作
if self.ser.isOpen():
self.pushButton_Open_Close.setText("关闭串口")
self.portNameBox.setEnabled(0)
self.baudrateBox.setEnabled(0)
self.dataBitsBox.setEnabled(0)
self.ParityBox.setEnabled(0)
self.stopBitsBox.setEnabled(0)
self.flowControlBox.setEnabled(0)
self.pushButton_Refresh.setEnabled(0)
self.sendButton.setEnabled(1)
self.checkBox_autoSend.setEnabled(1)
# 定时器接收数据
self.timer = QTimer()
self.timer.timeout.connect(self.data_receive)
# 打开串口接收定时器,周期为1ms
self.timer.start(1)
# 关闭串口
def port_close(self):
try:
self.timer.stop()
self.timer_send.stop()
self.ser.close()
self.pushButton_Open_Close.setText("打开串口")
self.portNameBox.setEnabled(1)
self.baudrateBox.setEnabled(1)
self.dataBitsBox.setEnabled(1)
self.ParityBox.setEnabled(1)
self.stopBitsBox.setEnabled(1)
self.flowControlBox.setEnabled(1)
self.pushButton_Refresh.setEnabled(1)
self.sendButton.setEnabled(0)
self.checkBox_autoSend.setEnabled(0)
except:
QMessageBox.critical(self, '串口异常', '关闭串口失败,请重启程序!')
return None
# 定时发送数据
def data_send_timer(self):
try:
if 1<= int(self.spinBox_timeDly.text()) <= 300000: # 定时时间1ms~30s内
if self.checkBox_autoSend.isChecked():
self.timer_send.start(int(self.spinBox_timeDly.text()))
self.spinBox_timeDly.setEnabled(False)
else:
self.timer_send.stop()
self.spinBox_timeDly.setEnabled(True)
else:
QMessageBox.critical(self, '定时发送数据异常', '定时发送数据周期仅可设置在300秒内!')
except:
QMessageBox.critical(self, '定时发送数据异常', '请设置正确的数值类型!')
# 发送数据
def data_send(self):
if self.ser.isOpen():
input_s = self.sendTextEdit.toPlainText()
# 判断是否为非空字符串
if input_s != "":
# 时间显示
if self.checkBox_displayTime.isChecked():
if self.checkBox_displaySend.isChecked():
self.recvTextEdit.insertPlainText(self.get_datetime())
# HEX发送
if self.checkBox_hexSend.isChecked():
#input_s = input_s.strip() #strip() 方法用于移除字符串头尾指定的字符(默认为空格或换行符)或字符序列。注意:该方法只能删除开头或是结尾的字符,不能删除中间部分的字符。
input_s = input_s.replace(" ", "")
send_list = []
while input_s != '':
try:
num = int(input_s[0:2], 16) # 没有步长的简单切片
#print( hex(num) )
except ValueError:
QMessageBox.critical(self, '数据异常', '请输入规范的十六进制数据!')
return None
input_s = input_s[2:]
send_list.append(num)
if self.checkBox_CR_LF.isChecked():
send_list.append(0x0D)
send_list.append(0x0A)
input_s = bytes(send_list)
# ASCII发送
else:
if self.checkBox_CR_LF.isChecked():
input_s += '\r\n'
input_s = (input_s).encode('utf-8')
# HEX接收显示
if self.checkBox_hexReceive.isChecked():
out_s = ''
for i in range(0, len(input_s)):
out_s = out_s + '{:02X}'.format(input_s[i]) + ' '
if self.checkBox_displaySend.isChecked():
self.recvTextEdit.insertPlainText(out_s)
# ASCII接收显示
else:
if self.checkBox_displaySend.isChecked():
self.recvTextEdit.insertPlainText(input_s.decode('utf-8'))
# 接收换行
if self.checkBox_AutoLineBreak.isChecked():
if self.checkBox_displaySend.isChecked():
self.recvTextEdit.insertPlainText('\r\n')
# 获取到Text光标
textCursor = self.recvTextEdit.textCursor()
# 滚动到底部
textCursor.movePosition(textCursor.End)
# 设置光标到Text中去
self.recvTextEdit.setTextCursor(textCursor)
# 统计发送字符数量
num = self.ser.write(input_s)
self.data_num_sended += num
self.label_Tx.setText(str(self.data_num_sended))
else:
pass
# 接收数据
def data_receive(self):
try:
num = self.ser.inWaiting()
# if num > 0:
# time.sleep(0.1) #100ms
# num = self.ser.inWaiting() # 延时,再读一次数据,确保数据完整性
except:
# QMessageBox.critical(self, '串口异常', '串口接收数据异常,请重新连接设备!')
# self.port_close()
return None
if num > 0:
data = self.ser.read(num)
num = len(data)
# HEX显示数据
if self.checkBox_hexReceive.checkState():
# 时间显示
if self.checkBox_displayTime.isChecked():
self.recvTextEdit.insertPlainText(self.get_datetime())
out_s = ''
for i in range(0, len(data)):
out_s = out_s + '{:02X}'.format(data[i]) + ' '
self.recvTextEdit.insertPlainText(out_s)
# 接收换行
if self.checkBox_AutoLineBreak.isChecked():
self.recvTextEdit.insertPlainText('\r\n')
# ASCII显示数据
else:
try:
if self.checkBox_displayTime.isChecked():
displayStr = self.get_datetime()
displayStr += data.decode('utf-8',"ignore")
displayStr = displayStr.replace("\n", "\n" + self.get_datetime())
# 接收换行
if self.checkBox_AutoLineBreak.isChecked():
displayStr += "\r\n" # 接收换行
self.recvTextEdit.insertPlainText(displayStr)
else:
self.recvTextEdit.insertPlainText(data.decode('utf-8',"ignore"))
# 接收换行
if self.checkBox_AutoLineBreak.isChecked():
self.recvTextEdit.insertPlainText('\r\n')
except Exception as e:
print("接收数据异常,波特率错误,请重新配置!\n", e)
# 获取到text光标
textCursor = self.recvTextEdit.textCursor()
# 滚动到底部
textCursor.movePosition(textCursor.End)
# 设置光标到text中去
self.recvTextEdit.setTextCursor(textCursor)
# 统计接收字符的数量
self.data_num_received += num
self.label_Rx.setText(str(self.data_num_received))
else:
pass
# 保存日志
def savefiles(self):
dlg = QFileDialog()
filename = self.portNameBox.currentText() + time.strftime("_%Y-%m-%d_%H_%M_%S", time.localtime())
filenames = dlg.getSaveFileName(None, "保存日志文件", filename, "Txt files(*.txt)")
try:
with open(file = filenames[0], mode='w', encoding='utf-8') as file:
file.write(self.recvTextEdit.toPlainText())
except:
#QMessageBox.critical(self, '日志异常', '保存日志文件失败!')
pass
# 加载日志
def openfiles(self):
dlg = QFileDialog()
filenames = dlg.getOpenFileName(None, "加载日志文件", None, "Txt files(*.txt)")
try:
with open(file = filenames[0], mode='r', encoding='utf-8') as file:
self.sendTextEdit.setPlainText(file.read())
except:
# QMessageBox.critical(self, '日志异常', '加载日志文件失败!')
pass
# 打开博客链接
def link(self):
webbrowser.open('https://blog.csdn.net/santu5234?type=blog')
# 清除发送数据显示
def send_data_clear(self):
self.sendTextEdit.setText("")
self.data_num_sended = 0
self.label_Tx.setText(str(self.data_num_sended))
# 清除接收数据显示
def receive_data_clear(self):
self.recvTextEdit.setText("")
self.data_num_received = 0
self.label_Rx.setText(str(self.data_num_received))
self.data_num_sended = 0
self.label_Tx.setText(str(self.data_num_sended))
# 时间格式
def get_datetime(self):
time_now = datetime.datetime.now()
# print(str(time_now)[:-3])
time_now = "[" + str(time_now)[:-3] + "] " # 转为字符串后切片
return time_now
# RTS
def rts_handle(self):
if self.ser.isOpen():
if self.checkBox_RTS.isChecked():
self.ser.setRTS(1)
else:
self.ser.setRTS(0)
# DTR
def dtr_handle(self):
if self.ser.isOpen():
if self.checkBox_DTR.isChecked():
self.ser.setDTR(1)
else:
self.ser.setDTR(0)
# 主函数
def main():
print("Hello, I'm PyQt5_Serial_Debug_Assistant_V1.0")
# 1、创建QApplication类的实例对象
app = QApplication(sys.argv)
# 2、创建一个 PyQt5_Serial 实例对象
myMainWindow = PyQt5_Serial()
# 3、显示主窗口
myMainWindow.show()
# 4、进入程序的主循环、并通过exit函数确保主循环安全结束
sys.exit(app.exec_())
if __name__ == '__main__':
main()
四、获取 >> 源码以及Git记录:
PyQt5_Serial_Debug_Assistant_V1.0&V1.1
总结
欢迎下载&更新使用。