基于Python与PyQt5的串口调试助手开发实战
本文详细介绍如何使用Python的serial模块结合PyQt5图形界面库,开发一款功能完整的串口调试助手。文章涵盖串口初始化配置、数据收发逻辑、485半双工通信协议处理及完整源代码解析,旨在帮助开发者摆脱第三方收费软件限制,实现数据的自主获取与实时处理。
Python串口通信基础与开发背景
Python环境下的serial模块支持通过RS-485或RS-232接口与外部串口设备进行数据交互。

尽管网络上存在大量现成的串口调试工具,但这些软件往往存在收费限制(如仅允许30天试用)或功能体验不佳的问题。
更重要的是,使用他人开发的程序通常无法直接提取和处理其中的数据。因此,自主开发一款串口调试工具,既能满足日常使用需求,又能灵活获取并处理通信数据,具有更高的实用价值。
开发环境与硬件准备
开发环境配置如下:软件方面需安装Python 3.10版本及PyCharm 2021集成开发环境;硬件方面需准备Windows 10操作系统电脑、支持485接口的国产流量计设备,以及用于连接电脑与设备的串口转USB线缆(适用于无原生串口接口的电脑)。


完成环境准备后,即可着手编写程序代码。
串口初始化与连接管理
串口通信程序的首要步骤是对串口参数进行初始化设置,包括波特率、数据位、停止位和校验位等关键配置。
初始化过程的具体实现如下所示:
def serial_init(self): #初始化
self.port = self.port_set.currentText()
self.bps = int(self.baud_set.currentText())
self.timeout = float(self.timeout_set.text())
try:
self.ser = serial.Serial(port=self.port,baudrate=self.bps,bytesize=8,parity='N',stopbits=1)
print(self.ser)
if self.ser.is_open:
print('串口正常')
except Exception as e:
QMessageBox.warning(self, 'tips!', str(e), QMessageBox.Ok | QMessageBox.Cancel, QMessageBox.Ok)
print('异常:', e)完成初始化后,必须显式打开串口连接,只有确保串口处于开启状态,后续的数据传输才能正常进行。Python的serial模块提供了is_open属性用于检查串口状态。需要注意的是,在创建serial实例时,串口实际上已经被同时打开。因此,如果在初始化代码中已经完成了实例化操作:
self.ser = serial.Serial(port=self.port,baudrate=self.bps,bytesize=8,parity='N',stopbits=1)则无需再次执行打开串口的操作,可直接进行后续步骤。
串口打开功能的实现代码如下:
def open_serial(self): #打开串口
try:
self.ser.open()
except Exception as e:
QMessageBox.warning(self, 'tips!', str(e), QMessageBox.Ok | QMessageBox.Cancel, QMessageBox.Ok)
print('异常:', e)串口关闭功能的实现代码如下:
def close_serial(self): #关闭串口
try:
self.ser.close()
except Exception as e:
QMessageBox.warning(self,'tips!',str(e),QMessageBox.Ok|QMessageBox.Cancel,QMessageBox.Ok)
print('异常:', e)调试工作结束后,建议及时关闭串口连接,以确保资源释放和系统稳定性。
数据收发逻辑与485通信实现
完成初始化和打开操作后,即可执行数据的读取与写入。本示例中,程序用于与流量计通信以获取实时流量数据。该流量计在读写数据时遵循特定的数据格式规范:

因此,从计算机端发送指令时,必须严格遵循流量计规定的格式,才能获取正确的反馈数据。由于RS-485采用半双工传输模式,同一时刻只能进行读取或写入操作中的一种,读写过程不能并发进行。
在本示例中,读取流量的指令格式为:01 03 00 10 00 02 C5 CE。在实际操作中,将流量计设备地址修改为2后,指令变为:02 03 00 10 00 02 C5 CE。
发送数据的具体实现代码如下:
def send_data(self):
ct = datetime.datetime.now() #获取当前系统时间
ct_str = ct.strftime("%Y-%m-%d %H:%M:%S") #格式化当前系统时间(字符形式)
try:
self.senddata_s=self.le_senddata.text()
self.senddata=bytes.fromhex(self.senddata_s) #字符转成字节
self.ser.write(self.senddata)
#self.senddata_str=self.senddata.hex()
self.senddata_str_fg=self.str_fenge(self.senddata_s)
#self.le_senddata.setText(self.senddata.hex())
self.le_recdata.append('\n' + '[' + ct_str+ ']' + ' ' + self.senddata_str_fg + '\n')
#print(self.senddata)
time.sleep(0.1)
self.read_data_size()
except Exception as e:
QMessageBox.warning(self, 'tips!', str(e), QMessageBox.Ok | QMessageBox.Cancel, QMessageBox.Ok)读取数据的具体实现代码如下:
def read_data_size(self):
ct=datetime.datetime.now()
ct_str=ct.strftime("%Y-%m-%d %H:%M:%S")
try:
#self.size=10
self.read_data=self.ser.read_all()
#print(self.read_data)
self.read_data_str=self.read_data.hex() #字节转成16进制字符显示
#re.findall(r'.{3}',self.read_data_str)
self.read_data_str_fg=self.str_fenge(self.read_data_str)
#print(self.read_data_str)
self.le_recdata.append('\n'+'['+ct_str+']'+' '+self.read_data_str_fg+'\n')
except Exception as e:
QMessageBox.warning(self, 'tips!', str(e), QMessageBox.Ok | QMessageBox.Cancel, QMessageBox.Ok)
#return self.read_data在serial模块中,数据读取主要提供三种方式:read()、read_all()和readline()。其中,read(size)用于按指定字节数size从缓冲区读取数据;read_all()用于读取缓冲区中的所有剩余数据;readline()则用于读取缓冲区中的最新一行数据。
完整源代码解析
完整的程序源代码如下所示:
from numpy.lib.function_base import place
import serial
import serial.tools.list_ports
import sys
import re
import os
import time
import datetime
import threading
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
class Serialwindow(QWidget):
def __init__(self) -> None:
super().__init__()
self.initUI()
def initUI(self):
#self.stylefile = 'E:\\100proworld2021\opencvtestpro01\爬虫程序\爬虫-style.qss'
#self.qssstyle = CommonHelper.readQSS(self.stylefile)
self.port = ''
self.bps = 0
self.timeout = 0.0
self.senddata=bytes()
self.read_data=''
self.btn_plist=QPushButton('获取可用串口',self)
self.btn_plist.setGeometry(20,20,60,20)
self.btn_plist.clicked.connect(self.get_serial_info)
self.btn_plist.adjustSize()
self.btn_ser_init=QPushButton('初始化',self)
self.btn_ser_init.setGeometry(20,50,60,20)
self.btn_ser_init.clicked.connect(self.serial_init)
self.btn_ser_init.adjustSize()
self.btn_open=QPushButton('打开串口',self)
self.btn_open.setGeometry(20,80,60,20)
self.btn_open.clicked.connect(self.open_serial)
self.btn_open.adjustSize()
self.btn_close=QPushButton('关闭串口',self)
self.btn_close.setGeometry(20,110,60,20)
self.btn_close.clicked.connect(self.close_serial)
self.btn_close.adjustSize()
self.btn_read_data=QPushButton('读取数据',self)
self.btn_read_data.setGeometry(20,140,60,20)
self.btn_read_data.clicked.connect(self.read_data_size)
self.btn_read_data.adjustSize()
self.btn_write_data=QPushButton('发送数据',self)
self.btn_write_data.setGeometry(40,460,60,20)
self.btn_write_data.clicked.connect(self.send_data)
self.btn_write_data.adjustSize()
#Qcombobox
self.port_set=QComboBox(self)
self.port_set.setGeometry(500,20,100,20)
self.port_set.addItems(['COM1'])
#self.port_set.activated.connect(self.Qcombo)
self.lbl_port_set=QLabel(self)
self.lbl_port_set.setGeometry(420,20,60,20)
self.lbl_port_set.setText('串口号:')
self.baud_set=QComboBox(self)
self.baud_set.setGeometry(500,50,100,20)
self.baud_set.addItems(['9600','19200','38400','115200'])
self.lbl_baud_set=QLabel(self)
self.lbl_baud_set.setGeometry(420,50,60,20)
self.lbl_baud_set.setText('波特率:')
self.stopbit_set=QComboBox(self)
self.stopbit_set.setGeometry(500,80,100,20)
self.stopbit_set.addItems(['0','1'])
self.lbl_stopbit_set = QLabel(self)
self.lbl_stopbit_set.setGeometry(420, 80, 60, 20)
self.lbl_stopbit_set.setText('停止位:')
self.parity_set=QComboBox(self)
self.parity_set.setGeometry(500,110,100,20)
self.parity_set.addItems(['无','奇校验','偶校验'])
self.lbl_parity_set = QLabel(self)
self.lbl_parity_set.setGeometry(420, 110, 60, 20)
self.lbl_parity_set.setText('校验位:')
self.databit_set=QComboBox(self)
self.databit_set.setGeometry(500,140,100,20)
self.databit_set.addItems(['8','7'])
self.lbl_databit_set=QLabel(self)
self.lbl_databit_set.setGeometry(420,140,60,20)
self.lbl_databit_set.setText('数据位:')
self.timeout_set=QLineEdit(self)
self.timeout_set.setGeometry(500,170,100,20)
self.timeout_set.setText('1000')
self.lbl_timeout_set=QLabel(self)
self.lbl_timeout_set.setGeometry(420,170,60,20)
self.lbl_timeout_set.setText('超时设置:')
self.lbl_timeout_set_2=QLabel(self)
self.lbl_timeout_set_2.setGeometry(610,170,60,20)
self.lbl_timeout_set_2.setText('ms')
#
self.le_senddata=QLineEdit(self)
self.le_senddata.setGeometry(120,460,300,20)
self.le_senddata.setText('010300100002C5CE')
self.le_recdata=QTextEdit(self)
self.le_recdata.setGeometry(120,220,600,200)
self.setGeometry(100,100,800,600)
self.setWindowTitle('简易串口调试助手-菌尘')
# self.setStyleSheet(self.qssstyle)
self.show()
def Qcombo(self):
print(self.port_set.currentText())
print(self.baud_set.currentText())
print(self.stopbit_set.currentText())
print(self.parity_set.currentText())
print(self.databit_set.currentText())
print(self.timeout_set.text())
def get_serial_info(self): #获取可用串口列表
#打印可用串口列表
#self.need_serial = ''
self.plist = list(serial.tools.list_ports.comports())
if len(self.plist) <= 0:
print('未找到串口')
qm = QMessageBox.warning(self, '提示窗口', '未找到串口!请检查接线和电脑接口。', QMessageBox.Ok|QMessageBox.Cancel,QMessageBox.Ok)
if qm == QMessageBox.Yes:
print('Yes')
else:
print('No')
else:
for i in list(self.plist):
self.port_set.addItem(i.name)
#return self.need_serial
#print(self.plist)
def serial_init(self): #初始化
self.port = self.port_set.currentText()
self.bps = int(self.baud_set.currentText())
self.timeout = float(self.timeout_set.text())
try:
self.ser = serial.Serial(port=self.port,baudrate=self.bps,bytesize=8,parity='N',stopbits=1)
print(self.ser)
if self.ser.is_open:
print('串口正常')
except Exception as e:
QMessageBox.warning(self, 'tips!', str(e), QMessageBox.Ok | QMessageBox.Cancel, QMessageBox.Ok)
print('异常:', e)
def open_serial(self): #打开串口
try:
self.ser.open()
except Exception as e:
QMessageBox.warning(self, 'tips!', str(e), QMessageBox.Ok | QMessageBox.Cancel, QMessageBox.Ok)
print('异常:', e)
def close_serial(self): #关闭串口
try:
self.ser.close()
except Exception as e:
QMessageBox.warning(self,'tips!',str(e),QMessageBox.Ok|QMessageBox.Cancel,QMessageBox.Ok)
print('异常:', e)
def read_data_size(self):
ct=datetime.datetime.now()
ct_str=ct.strftime("%Y-%m-%d %H:%M:%S")
try:
#self.size=10
self.read_data=self.ser.read_all()
#print(self.read_data)
self.read_data_str=self.read_data.hex() #字节转成16进制字符显示
#re.findall(r'.{3}',self.read_data_str)
self.read_data_str_fg=self.str_fenge(self.read_data_str)
#print(self.read_data_str)
self.le_recdata.append('\n'+'['+ct_str+']'+' '+self.read_data_str_fg+'\n')
except Exception as e:
QMessageBox.warning(self, 'tips!', str(e), QMessageBox.Ok | QMessageBox.Cancel, QMessageBox.Ok)
#return self.read_data
def read_data_line(self):
self.read_data=self.ser.readline()
return self.read_data
def send_data(self):
ct = datetime.datetime.now() #获取当前系统时间
ct_str = ct.strftime("%Y-%m-%d %H:%M:%S") #格式化当前系统时间(字符形式)
try:
self.senddata_s=self.le_senddata.text()
self.senddata=bytes.fromhex(self.senddata_s) #字符转成字节
self.ser.write(self.senddata)
#self.senddata_str=self.senddata.hex()
self.senddata_str_fg=self.str_fenge(self.senddata_s)
#self.le_senddata.setText(self.senddata.hex())
self.le_recdata.append('\n' + '[' + ct_str+ ']' + ' ' + self.senddata_str_fg + '\n')
#print(self.senddata)
time.sleep(0.1)
self.read_data_size()
except Exception as e:
QMessageBox.warning(self, 'tips!', str(e), QMessageBox.Ok | QMessageBox.Cancel, QMessageBox.Ok)
def read_data_alway(self, way):
print('开始接受数据:')
while True:
try:
if self.ser.inWaiting:
if(way == 0 ):
for i in range(self.ser.inWaiting):
print('接收ascII数据:'+str(self.read_data_size(1)))
data1 = self.read_data_size(1).hex()
data2 = int(data1, 16)
print('接收到16进制数据:'+data1+'接收到10进制数据:'+str(data2))
if(way == 1 ):
data = self.ser.read_all()
except Exception as e:
print('异常:', e)
def str_fenge(self,A):
'''
对字符串进行按长度分割,并在中间加入其他字符,如空格、短横等
'''
b = re.findall(r'.{2}',A)
c = ' '.join(b)
#print(c)
return c
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = Serialwindow()
sys.exit(app.exec_())经测试,上述程序能够与流量计进行正常通信,数据交互稳定。
至此,关于使用Python结合PyQt5实现串口调试助手的介绍已完成。如需了解更多相关内容,可搜索本站往期文章或继续浏览后续教程,感谢支持。
免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。









