基于Python的Qt开发之Pyside6 QtSerialPort库的使用

一、简介

QtSerialPort 是 PySide6 用于实现串口异步通信的核心模块,专门用于与串口设备(如单片机、传感器、蓝牙模块、串口打印机等)进行数据交互。

二、QtSerialPort库的核心内容

QtSerialPort 库的核心功能由两个关键类提供,核心操作类QSerialPort辅助信息类QSerialPortInfo, 所有串口操作均围绕这两个类展开:

1、核心操作类:QSerialPort

QSerialPort 是实现串口通信的核心类,负责串口的配置、打开 / 关闭、数据读写、错误处理等核心操作,它的核心特性包括:

  • 支持串口参数配置(波特率、数据位、校验位、停止位、流控)
  • 提供异步读写接口(基于信号与槽,无阻塞)
  • 支持串口状态检测和错误反馈
  • 支持数据缓冲区操作

QSerialPort 常用属性(串口参数)

串口通信的参数必须与外设保持一致,否则无法正常通信,常用参数如下:

|------|-----------------------------------------------------------|------------------------|
| 参数类型 | 示例 | 说明 |
| 波特率 | QSerialPort.Baud9600QSerialPort.Baud115200 | 串口通信速率,最常用 9600、115200 |
| 数据位 | QSerialPort.Data8QSerialPort.Data7 | 每帧数据的位数,最常用 8 位 |
| 校验位 | QSerialPort.NoParityQSerialPort.EvenParity | 奇偶校验位,最常用无校验(NoParity) |
| 停止位 | QSerialPort.OneStopQSerialPort.TwoStop | 数据帧结束标志,最常用 1 位 |
| 流控 | QSerialPort.NoFlowControlQSerialPort.HardwareControl | 流量控制,最常用无流控 |

QSerialPort 常用信号

由于 PySide6 采用信号与槽机制,QSerialPort 的异步操作通过以下核心信号通知上层逻辑:

  • readyRead():当串口接收缓冲区有新数据到达时触发(读取数据的核心信号
  • errorOccurred(error):当串口发生错误时触发(如端口不存在、权限不足)
  • bytesWritten(bytes):当数据成功写入串口发送缓冲区时触发
QSerialPort 常用方法
  • setPortName(portName):设置串口名(如 "COM3"(Windows)、"/dev/ttyUSB0"(Linux))
  • open(openMode):打开串口,打开模式常用 QSerialPort.ReadWrite(读写模式)
  • close():关闭串口,释放串口资源
  • write(data):向串口写入数据(需传入字节类型 bytes,而非字符串)
  • readAll():读取接收缓冲区中的所有可用数据(返回 QByteArray,可转换为 Python 字节 / 字符串)
  • read(maxSize):读取指定长度的字节数据
  • isOpen():判断串口是否已成功打开
  • clear():清空接收 / 发送缓冲区

2. 辅助信息类:QSerialPortInfo

QSerialPortInfo 是辅助类,用于枚举系统中可用的串口设备,获取串口的详细信息,无需打开串口即可使用,核心作用是帮助用户选择有效的串口。

QSerialPortInfo 常用方法
  • availablePorts():静态方法,返回系统中所有可用串口的 QSerialPortInfo 列表
  • portName():获取串口名(如 "COM3"、"/dev/ttyUSB0")
  • description():获取串口设备描述(如 "USB-SERIAL CH340")
  • manufacturer():获取串口设备制造商信息
  • isValid():判断当前 QSerialPortInfo 对应的串口是否有效

三、QtSerialPort 基本使用流程

使用 QtSerialPort 实现串口通信的核心流程分为 5 步,全程基于信号与槽实现异步无阻塞操作:

  1. 导入核心类
  2. 枚举可用串口(可选,方便用户选择)
  3. 配置串口参数并打开串口
  4. 绑定信号与槽(处理数据接收、错误反馈)
  5. 发送数据 / 关闭串口

四、完整可运行示例

下面实现一个简单的串口通信工具,支持枚举串口、打开 / 关闭串口、发送数据、接收数据并显示,可直接运行测试(需安装 PySide6 和对应串口驱动)。

python 复制代码
from PySide6.QtWidgets import (
    QApplication, QMainWindow, QWidget, QVBoxLayout,
    QHBoxLayout, QPushButton, QComboBox, QLineEdit, QTextEdit
)
from PySide6.QtCore import Qt
from PySide6.QtSerialPort import QSerialPort, QSerialPortInfo
import sys

class SerialPortTool(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("QtSerialPort 串口通信示例")
        self.setFixedSize(600, 400)

        # 1. 初始化串口对象(核心)
        self.serial_port = QSerialPort()

        # 2. 构建界面
        self._init_ui()

        # 3. 绑定信号与槽
        self._bind_signals()

        # 4. 初始化时枚举可用串口
        self.refresh_serial_ports()

    def _init_ui(self):
        """构建界面"""
        central_widget = QWidget()
        self.setCentralWidget(central_widget)
        main_layout = QVBoxLayout(central_widget)

        # 串口选择区域
        port_layout = QHBoxLayout()
        self.port_combo = QComboBox()
        self.refresh_btn = QPushButton("刷新串口")
        self.open_btn = QPushButton("打开串口")
        self.close_btn = QPushButton("关闭串口")
        self.close_btn.setEnabled(False)

        port_layout.addWidget(self.port_combo)
        port_layout.addWidget(self.refresh_btn)
        port_layout.addWidget(self.open_btn)
        port_layout.addWidget(self.close_btn)

        # 数据发送区域
        send_layout = QHBoxLayout()
        self.send_input = QLineEdit()
        self.send_input.setPlaceholderText("输入要发送的数据,按回车或点击发送按钮")
        self.send_btn = QPushButton("发送数据")
        send_layout.addWidget(self.send_input)
        send_layout.addWidget(self.send_btn)

        # 数据接收区域
        self.recv_text = QTextEdit()
        self.recv_text.setPlaceholderText("接收的数据将显示在这里...")
        self.recv_text.setReadOnly(True)

        # 添加到主布局
        main_layout.addLayout(port_layout)
        main_layout.addLayout(send_layout)
        main_layout.addWidget(self.recv_text)

    def _bind_signals(self):
        """绑定信号与槽"""
        # 界面按钮信号
        self.refresh_btn.clicked.connect(self.refresh_serial_ports)
        self.open_btn.clicked.connect(self.open_serial_port)
        self.close_btn.clicked.connect(self.close_serial_port)
        self.send_btn.clicked.connect(self.send_data)
        self.send_input.returnPressed.connect(self.send_data)

        # 串口核心信号
        self.serial_port.readyRead.connect(self.read_serial_data)  # 接收数据
        self.serial_port.errorOccurred.connect(self.handle_serial_error)  # 错误处理

    def refresh_serial_ports(self):
        """枚举并刷新可用串口"""
        self.port_combo.clear()
        # 获取所有可用串口
        available_ports = QSerialPortInfo.availablePorts()
        for port_info in available_ports:
            # 显示格式:串口名 - 设备描述
            port_text = f"{port_info.portName()} - {port_info.description()}"
            self.port_combo.addItem(port_text, port_info.portName())

    def open_serial_port(self):
        """配置并打开串口"""
        if self.port_combo.count() == 0:
            self._append_recv_text("错误:无可用串口!")
            return

        # 获取选中的串口名
        selected_port = self.port_combo.currentData()
        if not selected_port:
            self._append_recv_text("错误:无效的串口!")
            return

        # 1. 配置串口参数
        self.serial_port.setPortName(selected_port)
        self.serial_port.setBaudRate(QSerialPort.Baud9600)  # 波特率 9600
        self.serial_port.setDataBits(QSerialPort.Data8)     # 数据位 8
        self.serial_port.setParity(QSerialPort.NoParity)    # 无校验
        self.serial_port.setStopBits(QSerialPort.OneStop)   # 停止位 1
        self.serial_port.setFlowControl(QSerialPort.NoFlowControl)  # 无流控

        # 2. 打开串口(读写模式)
        if self.serial_port.open(QSerialPort.ReadWrite):
            self._append_recv_text(f"成功:打开串口 {selected_port}")
            self.open_btn.setEnabled(False)
            self.close_btn.setEnabled(True)
            self.port_combo.setEnabled(False)
            self.refresh_btn.setEnabled(False)
        else:
            self._append_recv_text(f"错误:打开串口 {selected_port} 失败(权限不足/端口被占用)")

    def close_serial_port(self):
        """关闭串口"""
        if self.serial_port.isOpen():
            self.serial_port.close()
            self._append_recv_text(f"信息:串口 {self.serial_port.portName()} 已关闭")
            self.open_btn.setEnabled(True)
            self.close_btn.setEnabled(False)
            self.port_combo.setEnabled(True)
            self.refresh_btn.setEnabled(True)

        def send_data(self):
        """向串口发送数据"""
        if not self.serial_manager.serial_port.isOpen():
            self._append_recv_text("错误:串口未打开!")
            return

        # 获取输入框数据并转换为字节类型(必须传入 bytes,字符串需 encode)
        input_text = self.send_input.text().strip()
        if not input_text:
            return

        # 判断是否为十六进制格式(去除空格后检查是否全为十六进制字符)
        clean_text = input_text.replace(' ', '').replace('\t', '').replace('\n', '')
        # 如果是十六进制字符串,则转换为字节数组
        if all(c in '0123456789ABCDEFabcdef' for c in clean_text) and len(clean_text) % 2 == 0:
            try:
                # 将十六进制字符串转换为字节数组
                send_bytes = bytes.fromhex(clean_text)
            except ValueError:
                self._append_recv_text("错误:十六进制数据格式不正确!")
                return
        else:
            # 按普通字符串处理
            send_bytes = input_text.encode("utf-8")

        # 写入串口(encode 转换为 UTF-8 字节流,外设需对应编码)
        written_bytes = self.serial_manager.serial_port.write(send_bytes)
        if written_bytes > 0:
            # 显示发送的十六进制数据
            send_hex = send_bytes.hex().upper()
            self._append_recv_text(f"发送:{send_hex}(字节数:{written_bytes})")
            # self.send_input.clear()

    def read_serial_data(self):
        """读取串口接收的数据(响应 readyRead 信号)"""
        if not self.serial_port.isOpen():
            return

        # 读取所有可用数据(QByteArray -> bytes -> str)
        recv_data = self.serial_port.readAll()
        # 转换为 Python 字节类型,再解码为字符串(与发送端编码一致)
        recv_str = bytes(recv_data).decode("utf-8", errors="ignore")
        if recv_str:
            self._append_recv_text(f"接收:{recv_str}")

    def handle_serial_error(self, error):
        """处理串口错误(响应 errorOccurred 信号)"""
        error_msg = ""
        # 在 PySide6 中,使用 QSerialPort.SerialPortError 枚举值
        if error == QSerialPort.SerialPortError.NoError:
            error_msg = "信息:无错误"
        elif error == QSerialPort.SerialPortError.DeviceNotFoundError:
            error_msg = "错误:串口端口不存在"
        elif error == QSerialPort.SerialPortError.PermissionError:
            error_msg = "错误:串口权限不足"
        elif error == QSerialPort.SerialPortError.OpenError:
            error_msg = "错误:无法打开串口(可能已被其他程序占用)"
        elif error == QSerialPort.SerialPortError.NotOpenError:
            error_msg = "错误:串口未打开"
        elif error == QSerialPort.SerialPortError.ResourceError:
            error_msg = "错误:串口资源被占用或断开连接"
        elif error == QSerialPort.SerialPortError.UnsupportedOperationError:
            error_msg = "错误:不支持的操作"
        elif error == QSerialPort.SerialPortError.TimeoutError:
            error_msg = "错误:操作超时"
        elif error == QSerialPort.SerialPortError.UnknownError:
            error_msg = "错误:未知串口错误"
        else:
            error_msg = f"错误:未知串口错误(代码:{error})"

        self._append_recv_text(error_msg)
        # 错误发生时自动关闭串口
        self.close_serial_port()

    def _append_recv_text(self, text):
        """向接收区域追加文本并自动滚动"""
        self.recv_text.append(text)
        # 自动滚动到最下方
        scroll_bar = self.recv_text.verticalScrollBar()
        scroll_bar.setValue(scroll_bar.maximum())

    def closeEvent(self, event):
        """窗口关闭时自动关闭串口"""
        if self.serial_port.isOpen():
            self.serial_port.close()
        event.accept()

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

五、运行与使用说明

  1. 运行效果:启动程序后,会自动枚举系统中的可用串口,显示在下拉框中。
  2. 操作步骤
    • 选择需要连接的串口(如 "COM3 - USB-SERIAL CH340")
    • 点击「打开串口」(成功后按钮状态切换)
    • 在输入框中输入数据,点击「发送数据」或按回车发送
    • 外设返回数据后,会自动显示在下方接收区域
    • 结束通信后,点击「关闭串口」释放资源

六、关键注意事项

  1. 数据格式QSerialPort.write() 仅接受字节类型(bytes) ,字符串必须通过 encode() 转换;读取数据时,readAll() 返回 QByteArray,需先转换为 bytesdecode() 为字符串。
  2. 串口参数一致性:波特率、数据位等参数必须与外设完全一致,否则会出现乱码或无法通信。
  3. 异步无阻塞 :QtSerialPort 采用异步通信,readyRead() 信号仅在有新数据到达时触发,无需轮询(轮询会导致阻塞,影响界面响应)。
  4. 资源释放 :串口使用完毕后必须调用 close() 关闭,否则会导致端口被占用,下次无法打开。
  5. 编码一致性:收发数据的编码格式(如 UTF-8、GBK)必须与外设保持一致,否则会出现乱码。

七、总结

  1. QtSerialPort 库的核心是 QSerialPort(串口操作)和 QSerialPortInfo(串口枚举)。
  2. 串口通信的核心流程是「配置参数→打开串口→绑定信号槽→读写数据→关闭串口」。
  3. 异步通信依赖 readyRead() 信号接收数据,无需轮询,保证界面响应流畅。
  4. 数据格式转换(字符串 ↔ 字节)和串口参数一致性是串口通信成功的关键。
相关推荐
木头左2 小时前
HMM隐马尔可夫模型在指数期权双币种套利策略
python
@zulnger2 小时前
Django 框架(django-admin 命令详解)
python·django
一只小bit2 小时前
Qt 对话框全方面详解,包含示例与解析
前端·c++·qt·cpp·页面
_Johnny_2 小时前
【API Echo】API测试回调工具 - 回显请求内容
python·flask·api
知乎的哥廷根数学学派2 小时前
基于物理约束指数退化与Hertz接触理论的滚动轴承智能退化趋势分析(Pytorch)
开发语言·人工智能·pytorch·python·深度学习·算法·机器学习
SunkingYang2 小时前
QT中QStringList如何查找指定字符串,有哪些方式?
qt·字符串·查找·子串·qstringlist
Ethan Hunt丶2 小时前
基于Pytorch预训练模型实现声纹识别系统
人工智能·pytorch·python·语音识别
Katecat996632 小时前
【铁路检测】YOLO11-C3k2-StripCGLU模型在铁路轨道缺陷检测中的应用与改进
python
理想是做全栈工程师2 小时前
基于UNet的带噪黑白数字图像分割模型
人工智能·pytorch·python·anaconda