基于Python与PyQt5的串口调试助手开发实战

橘子海
橘子海
发布时间:2026-10-15 | 5人浏览过
原创

本文详细介绍如何使用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实现串口调试助手的介绍已完成。如需了解更多相关内容,可搜索本站往期文章或继续浏览后续教程,感谢支持。

相关标签:

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表