串口调试助纯代码开发【PySide6】

基于PySide6的串口调试助手代码开发

一、编译环境

操作系统:Windows 11

编译器:Pycharm 2025.1.3.1

辅助工具:虚拟序列端口模拟器(VSPE)

类别依赖库 / 工具作用说明
界面框架PySide6构建图形化界面(UI),支持跨平台
串口通信pyserial实现串口的打开、关闭、数据收发
多线程Python 内置 QThread后台接收串口数据,避免阻塞 UI 线程
  • pyserial:处理串口硬件通信(枚举端口、配置参数、数据读写);

  • PySide6:Qt 框架的 Python 绑定,用于创建可视化界面。

1.1 安装pyside6

  直接执行pip命令安装,执行命令:

pip install pyside6

或者使用清华大学源安装

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

1.2 安装pyserial

需通过 pip 安装以下依赖库,执行命令:

pip install pyserial

二、核心模块解析

        工程核心由两个类构成,分别负责 “串口数据接收” 和 “UI 交互与串口控制”,类间通过 Qt 信号与槽(Signal/Slot)机制实现数据通信。

2.1 串口接收线程类 SerialThread

2.1.1 类定位

后台线程类,异步接收串口数据,避免因串口数据阻塞主线程(UI 线程)导致界面卡顿。

2.1.2 核心属性与方法
名称类型 / 返回值作用说明
data_receivedSignal(str)数据接收信号,将处理后的数据发送给主线程
__init__(serial_port)-初始化线程,传入已配置的串口对象(serial.Serial
run()-线程主逻辑:循环读取串口数据,处理后发送信号
stop()-停止线程:设置 running=False 并等待线程结束
2.1.3 数据处理规则
  • 优先以 UTF-8 编码 解码接收的二进制数据(适用于文本类数据,如 Hello World);

  • 若解码失败(如接收二进制数据、十六进制指令),自动转为 十六进制字符串 显示(格式:XX XX XX,每个字节占 2 位)。

2.2 主窗口类 SerialMonitor

2.2.1 类定位

程序主窗口,负责 UI 初始化、串口参数控制、数据收发交互,是用户操作的核心入口。

2.2.2 核心属性
名称类型作用说明
serial_portserial.Serial/None串口对象,None 表示未打开串口
receive_threadSerialThread/None后台接收线程,None 表示线程未启动
port_timerQTimer定时器(1 秒 / 次),自动刷新串口列表
2.2.3 关键方法解析
方法名核心功能关键逻辑
init_ui()初始化 UI 界面创建 “串口设置区”“数据显示区”“数据发送区”,绑定控件点击事件与快捷键
refresh_ports()自动刷新串口列表枚举系统可用串口,保持用户当前选中的串口(若仍存在)
toggle_connection()切换串口连接状态(打开 / 关闭)调用 open_serial() 或 close_serial(),同步更新按钮文本与控件权限
open_serial()打开串口读取 UI 参数(端口、波特率等),初始化串口对象,启动接收线程,禁用参数修改
close_serial()关闭串口停止接收线程,关闭串口,启用参数修改,更新 UI 状态
send_data()发送数据将输入框的字符串转为 UTF-8 字节流,通过串口发送,发送后清空输入框
append_data(data)显示接收数据将 data_received 信号传入的数据追加到显示区,并自动滚动到最新数据
closeEvent(event)窗口关闭事件确保关闭窗口前停止线程、关闭串口,避免资源泄漏

三、UI 界面说明

UI 界面分为 3 个核心区域,布局清晰且交互友好,支持快捷键操作。

3.1 串口设置区(顶部)

包含串口参数配置与连接控制,采用 “标签 + 下拉框” 表单布局,参数默认值符合行业常用配置:

控件名称可选值默认值作用说明
端口下拉框系统可用串口(如 COM3、/dev/ttyxxx)-选择需连接的串口
波特率下拉框9600/19200/38400/57600/115200 等115200选择串口通信速率(需与硬件匹配)
数据位下拉框5/6/7/88选择每帧数据的位数
校验位下拉框None/Odd/Even/Mark/SpaceNone选择数据校验方式(通常用 “无校验”)
停止位下拉框1/1.5/21选择每帧数据的停止位数
连接按钮打开串口 / 关闭串口打开串口切换串口连接状态

3.2 数据显示区(中部)

  • 控件类型:只读 QTextEdit(不可编辑,避免误操作);

  • 字体设置:等宽字体 Consolas(10 号),确保文本 / 十六进制数据对齐;

  • 显示规则:接收数据实时追加,自动滚动到最新行;解码失败时显示十六进制(如 A5 01 FF)。

3.3 数据发送区(底部)

控件名称功能说明快捷键
发送输入框输入需发送的文本数据(如指令)-
发送按钮发送输入框中的数据(UTF-8 编码)回车键(Return)
清除接收区按钮清空数据显示区的内容-

四、完整代码(带注释)

import sys
import serial
import serial.tools.list_ports
from PySide6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
                               QHBoxLayout, QLabel, QComboBox, QPushButton,
                               QTextEdit, QLineEdit, QGroupBox, QFormLayout,
                               QMessageBox, QSplitter)
from PySide6.QtCore import QThread, Signal, Qt, QTimer
from PySide6.QtGui import QFont, QTextCursor


class SerialThread(QThread):
    """串口数据接收线程:后台线程中异步接收串口数据,避免因串口通信阻塞主线程(尤其是 UI 线程)"""
    data_received = Signal(str)

    def __init__(self, serial_port):
        super().__init__()
        self.serial_port = serial_port
        self.running = False

    def run(self): #线程主逻辑
        self.running = True # 启动线程时,设置运行状态为 True
        while self.running and self.serial_port.is_open: # 循环条件:线程运行中 且 串口已打开
            try:
                if self.serial_port.in_waiting: # 检查串口是否有等待接收的数据
                    data = self.serial_port.read(self.serial_port.in_waiting) # 读取所有可用数据(一次读完缓冲区中的数据)
                    # 尝试以UTF-8解码,失败则用十六进制显示
                    try:
                        text = data.decode('utf-8')
                    except UnicodeDecodeError:
                        text = ' '.join([f'{b:02X}' for b in data]) + ' '  # 当接收为二进时,二进制数据转为十六进制字符串(每个字节用 2 位十六进制表示,空格分隔)
                    self.data_received.emit(text) # 通过信号将处理后的数据发送给主线程
                self.msleep(10)  # 短暂休眠,减少CPU占用
            except Exception as e:
                self.data_received.emit(f"串口错误: {str(e)}\n")
                self.running = False
                break

    def stop(self):
        self.running = False
        self.wait()


class SerialMonitor(QMainWindow):
    """串口调试助手主窗口:主要作用是完成程序启动时的基础设置,为后续的串口操作和界面交互做准备"""

    def __init__(self):
        super().__init__()
        self.serial_port = None #用于存储串口对象(serial.Serial 实例)
        self.receive_thread = None #用于存储串口接收线程(SerialThread 实例)
        self.init_ui()
        self.refresh_ports()

        # 定时刷新串口列表
        self.port_timer = QTimer(self)
        self.port_timer.timeout.connect(self.refresh_ports) #将定时器的 “超时信号” 与 refresh_ports 方法绑定,即定时器触发时会自动调用 refresh_ports
        self.port_timer.start(1000)  # 1秒刷新一次

    def init_ui(self):
        """初始化UI界面:界面初始化核心代码,负责创建并布局所有 UI 控件,定义界面的整体结构和交互元素"""
        self.setWindowTitle("串口调试助手")
        self.setGeometry(100, 100, 800, 600)

        # 创建主部件和布局
        central_widget = QWidget()
        self.setCentralWidget(central_widget)  # 将主部件设为窗口的中央部件
        main_layout = QVBoxLayout(central_widget) # 创建垂直布局(QVBoxLayout),作为整个界面的根布局

        # 串口设置区域
        serial_group = QGroupBox("串口设置") # 创建分组框(QGroupBox),给串口设置区域添加边框和标题,让用户直观区分功能区域
        serial_layout = QFormLayout() # 表单布局,自动对齐标签和控件:适合“标签+控件”成对出现的场景(如“端口: [下拉框]”)

        # 端口选择
        self.port_combo = QComboBox()
        serial_layout.addRow("端口:", self.port_combo)

        # 波特率选择
        self.baudrate_combo = QComboBox()
        self.baudrate_combo.addItems(["9600", "19200", "38400", "57600", "115200", "230400", "460800", "921600"])
        self.baudrate_combo.setCurrentText("115200")  # 设置默认波特率(115200是最常用的速率)
        serial_layout.addRow("波特率:", self.baudrate_combo)

        # 数据位选择
        self.databits_combo = QComboBox()
        self.databits_combo.addItems(["5", "6", "7", "8"])
        self.databits_combo.setCurrentText("8")
        serial_layout.addRow("数据位:", self.databits_combo)

        # 校验位选择
        self.parity_combo = QComboBox()
        self.parity_combo.addItems(["None", "Odd", "Even", "Mark", "Space"])
        self.parity_combo.setCurrentText("None")
        serial_layout.addRow("校验位:", self.parity_combo)

        # 停止位选择
        self.stopbits_combo = QComboBox()
        self.stopbits_combo.addItems(["1", "1.5", "2"])
        self.stopbits_combo.setCurrentText("1")
        serial_layout.addRow("停止位:", self.stopbits_combo)

        # 连接按钮
        self.connect_btn = QPushButton("打开串口")
        self.connect_btn.clicked.connect(self.toggle_connection)
        serial_layout.addRow(self.connect_btn)

        serial_group.setLayout(serial_layout) # 将表单布局绑定到分组框
        main_layout.addWidget(serial_group) # 将分组框加入根布局(垂直布局的第一个元素)

        # 数据显示区域
        self.receive_text = QTextEdit()
        self.receive_text.setReadOnly(True)  # 设置为只读(防止用户误编辑接收的数据)
        self.receive_text.setFont(QFont("Consolas", 10))# 使用等宽字体(方便对齐,尤其显示十六进制数据时)
        main_layout.addWidget(QLabel("接收数据:"))# 添加到根布局:先加“接收数据:”标签,再加文本编辑控件
        main_layout.addWidget(self.receive_text)

        # 数据发送区域
        send_layout = QHBoxLayout()
        self.send_edit = QLineEdit()
        self.send_btn = QPushButton("发送")
        self.send_btn.clicked.connect(self.send_data)
        self.clear_btn = QPushButton("清除接收区")
        self.clear_btn.clicked.connect(lambda: self.receive_text.clear())

        send_layout.addWidget(self.send_edit)
        send_layout.addWidget(self.send_btn)
        send_layout.addWidget(self.clear_btn)

        main_layout.addWidget(QLabel("发送数据:"))
        main_layout.addLayout(send_layout)

        # 设置快捷键
        self.send_btn.setShortcut("Return")  # 回车键发送数据

    def refresh_ports(self):
        """刷新串口列表"""
        current_port = self.port_combo.currentText() #方法获取当前下拉框中用户选中的串口名称,保存这个值的目的是:刷新列表后,如果该串口仍然可用,要保持用户的选中状态
        self.port_combo.clear() #清空下拉框中已有的所有选项,避免新旧串口列表混合显示(比如之前的串口已被拔掉,仍显示在列表中)。

        # 获取所有可用串口
        ports = serial.tools.list_ports.comports()
        for port in ports: # 遍历每个可用串口,添加到下拉框
            self.port_combo.addItem(port.device)

        # 如果之前选择的端口仍然存在,则保持选中
        if current_port and self.port_combo.findText(current_port) >= 0:
            self.port_combo.setCurrentText(current_port)

    def toggle_connection(self):
        """切换串口连接状态"""
        if self.serial_port and self.serial_port.is_open:
            self.close_serial()
        else:
            self.open_serial()

    def open_serial(self):
        """打开串口"""
        try:
            # 获取串口参数
            port_name = self.port_combo.currentText()
            if not port_name:
                QMessageBox.warning(self, "警告", "请选择串口")
                return

            baudrate = int(self.baudrate_combo.currentText())
            databits = int(self.databits_combo.currentText())
            parity = self.get_parity_value(self.parity_combo.currentText())
            stopbits = float(self.stopbits_combo.currentText())

            # 配置并打开串口:创建 serial.Serial 类的实例,通过传入的参数配置串口属性,并自动打开串口(Serial 类的默认行为是创建实例时自动打开串口
            self.serial_port = serial.Serial(
                port=port_name,
                baudrate=baudrate,
                bytesize=databits,
                parity=parity,
                stopbits=stopbits,
                timeout=0.1 #设置串口读取操作的超时时间
            )

            if self.serial_port.is_open:
                self.connect_btn.setText("关闭串口")
                self.receive_text.append(f"串口 {port_name} 已打开,波特率 {baudrate}\n")

                # 启动接收线程
                self.receive_thread = SerialThread(self.serial_port)
                self.receive_thread.data_received.connect(self.append_data) #接收到的数据追加接收的数据到显示区域
                self.receive_thread.start()

                # 禁用串口设置
                self.port_combo.setEnabled(False)
                self.baudrate_combo.setEnabled(False)
                self.databits_combo.setEnabled(False)
                self.parity_combo.setEnabled(False)
                self.stopbits_combo.setEnabled(False)

        except Exception as e:
            QMessageBox.critical(self, "错误", f"无法打开串口: {str(e)}")
            self.serial_port = None

    def close_serial(self):
        """关闭串口:用于安全关闭串口连接的核心逻辑,负责终止数据接收线程、释放串口资源,并同步更新界面状态"""
        if self.receive_thread and self.receive_thread.isRunning():
            self.receive_thread.stop()
            self.receive_thread = None

        if self.serial_port and self.serial_port.is_open:
            self.serial_port.close()
            self.receive_text.append(f"串口 {self.serial_port.port} 已关闭\n")
            self.serial_port = None

        self.connect_btn.setText("打开串口")

        # 启用串口设置
        self.port_combo.setEnabled(True)
        self.baudrate_combo.setEnabled(True)
        self.databits_combo.setEnabled(False)
        self.parity_combo.setEnabled(True)
        self.stopbits_combo.setEnabled(True)

    def send_data(self):
        """发送数据"""
        if not self.serial_port or not self.serial_port.is_open:
            QMessageBox.warning(self, "警告", "请先打开串口")
            return

        data = self.send_edit.text()
        if not data:
            return

        try:
            # 发送数据:串口传输的是二进制字节流,而用户在界面输入的是字符串(data 来自 self.send_edit.text()),因此需要通过 encode('utf-8') 将字符串转换为 UTF-8 编码的字节流(如字符串 "hello" 会转为字节 b'hello')。
            self.serial_port.write(data.encode('utf-8'))
            self.send_edit.clear()
        except Exception as e:
            QMessageBox.critical(self, "错误", f"发送数据失败: {str(e)}")

    def append_data(self, data):
        """追加接收的数据到显示区域"""
        self.receive_text.insertPlainText(data)
        # 自动滚动到底部:因为当接收的数据量超过 QTextEdit 控件的显示范围时,新数据会被隐藏在视野外,用户需要手动滚动才能看到最新内容,体验较差。
        # 获取当前文本光标
        cursor = self.receive_text.textCursor()
        # 将光标移动到文本末尾
        cursor.movePosition(QTextCursor.End)
        # 应用光标位置,触发滚动
        self.receive_text.setTextCursor(cursor)

    def get_parity_value(self, parity_str):
        """字典映射:将校验位字符串转换为serial库对应的常量"""
        parity_map = {
            "None": serial.PARITY_NONE,
            "Odd": serial.PARITY_ODD,
            "Even": serial.PARITY_EVEN,
            "Mark": serial.PARITY_MARK,
            "Space": serial.PARITY_SPACE
        }
        return parity_map.get(parity_str, serial.PARITY_NONE) #如果 parity_str 不在字典中(如异常值或空字符串),则返回默认值 serial.PARITY_NONE(无校验)

    def closeEvent(self, event):
        """窗口关闭时确保关闭串口和线程"""
        if self.receive_thread and self.receive_thread.isRunning():
            self.receive_thread.stop()

        if self.serial_port and self.serial_port.is_open:
            self.serial_port.close()

        event.accept()

"""触发closeEvent的条件
1. 用户点击窗口标题栏的关闭按钮(最常见)
2. 用户使用系统快捷键关闭窗口
3. 程序内部调用 close() 方法
4. 系统级别的关闭请求
"""

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = SerialMonitor()
    window.show()
    sys.exit(app.exec())

五、运行结果

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值