Python+PyQt6实现跨平台串口设备热插拔检测

 更新时间:2026年08月07日 09:07:32   作者:小灰灰搞电子  
在嵌入式开发、工业控制或物联网应用中,经常需要通过串口与外部设备通信,本文将详细介绍如何在基于 PyQt6 的图形界面应用程序中,实现跨平台的串口设备热插拔检测,文中的示例代码讲解详细,感兴趣的小伙伴可以了解下

引言

在嵌入式开发、工业控制或物联网应用中,经常需要通过串口与外部设备通信。一个常见的需求是:当用户插入或拔出一个 USB 转串口适配器(如 CH340、CP2102 等)时,应用程序需要能够自动检测到设备的变化,并更新可用的串口列表,而无需重启程序。这种功能被称为“设备热插拔检测”。

本文将详细介绍如何在基于 PyQt6 的图形界面应用程序中,实现跨平台(Windows、Linux、macOS)的串口设备热插拔检测。我们将结合 PyQt6 的信号槽机制和 Python 的 pyserial 库,构建一个稳定、响应及时的解决方案。

核心思路与原理

实现热插拔检测的核心在于监听系统底层发出的设备变更事件,并在事件发生时,重新扫描可用的串口列表。不同操作系统的底层机制不同:

  • Windows: 可以通过监听 Windows 消息(如 WM_DEVICECHANGE)或使用 Windows API(如 RegisterDeviceNotification)。在 PyQt6 中,我们可以利用其事件过滤器(eventFilter)来捕获这些系统级消息。
  • Linux/macOS: 通常通过监听 /dev 目录下文件(如 ttyUSB*, ttyACM*)的变化来实现。我们可以使用 QFileSystemWatcher 来监控目录。

为了简化跨平台开发,我们将采用一种更通用的策略:使用一个定时器(QTimer)定期轮询当前可用的串口列表,并与之前保存的列表进行比较。如果列表发生变化,则发出信号通知界面更新。虽然轮询不如事件驱动高效,但对于串口设备变化频率不高的场景来说,实现简单且完全跨平台,是更稳妥的选择。

环境准备与依赖安装

首先,确保你的 Python 环境已安装必要的库:

pip install PyQt6 pyserial
  • PyQt6: 用于构建图形用户界面。
  • pyserial: 用于列举和操作串口设备。

代码实现:热插拔检测器类

我们将创建一个名为 SerialPortMonitor 的类,它继承自 QObject,以便使用 PyQt6 的信号槽机制。

import sys
from PyQt6.QtCore import QObject, QTimer, pyqtSignal
from PyQt6.QtWidgets import QApplication, QMainWindow, QComboBox, QVBoxLayout, QWidget
import serial.tools.list_ports

class SerialPortMonitor(QObject):
    """
    串口设备热插拔检测器。
    使用定时器轮询方式,跨平台兼容。
    """
    # 定义一个信号,当可用串口列表发生变化时发出,参数为新的端口名列表
    ports_changed = pyqtSignal(list)

    def __init__(self, poll_interval=2000):
        """
        初始化检测器。
        :param poll_interval: 轮询间隔,单位毫秒,默认2秒
        """
        super().__init__()
        self.poll_interval = poll_interval
        self._current_ports = []  # 保存当前检测到的端口列表
        self._timer = QTimer()
        self._timer.timeout.connect(self._check_ports)
        self._init_ports_list()

    def _init_ports_list(self):
        """初始化当前端口列表"""
        self._current_ports = [port.device for port in serial.tools.list_ports.comports()]

    def _check_ports(self):
        """定时器触发的检查函数"""
        # 获取当前所有可用串口
        available_ports = [port.device for port in serial.tools.list_ports.comports()]

        # 比较列表是否发生变化(考虑顺序无关)
        if set(available_ports) != set(self._current_ports):
            print(f"串口列表已变更。旧列表: {self._current_ports}, 新列表: {available_ports}")
            self._current_ports = available_ports
            # 发出信号,传递新的端口列表
            self.ports_changed.emit(available_ports)

    def start(self):
        """启动监控"""
        self._init_ports_list()  # 启动时先获取一次
        self._timer.start(self.poll_interval)
        print("串口热插拔监控已启动。")

    def stop(self):
        """停止监控"""
        self._timer.stop()
        print("串口热插拔监控已停止。")

    def get_current_ports(self):
        """获取当前检测到的端口列表"""
        return self._current_ports.copy()

关键点说明:

  1. ports_changed 信号:这是连接检测逻辑和用户界面的桥梁。当端口列表变化时,此信号会被触发。
  2. _check_ports 方法:核心轮询逻辑。使用 serial.tools.list_ports.comports() 获取当前所有串口设备,并与之前保存的列表进行对比。
  3. start() / stop() 方法:控制监控的启停。

集成到 PyQt6 主界面

下面创建一个简单的主窗口,包含一个下拉框(QComboBox)来显示可用串口,并将监控器集成进去。

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("PyQt6 串口热插拔检测示例")
        self.setGeometry(100, 100, 400, 200)

        # 创建中央部件和布局
        central_widget = QWidget()
        self.setCentralWidget(central_widget)
        layout = QVBoxLayout(central_widget)

        # 创建下拉框用于显示串口
        self.combo_ports = QComboBox()
        layout.addWidget(self.combo_ports)

        # 初始化串口监控器
        self.monitor = SerialPortMonitor(poll_interval=1500)  # 每1.5秒检查一次
        # 连接监控器的信号到更新界面的槽函数
        self.monitor.ports_changed.connect(self.update_com_port_list)
        # 启动监控
        self.monitor.start()

        # 初始更新一次列表
        self.update_com_port_list(self.monitor.get_current_ports())

    def update_com_port_list(self, port_list):
        """
        更新下拉框中的串口列表。
        :param port_list: 新的可用串口设备名列表
        """
        current_selection = self.combo_ports.currentText()
        self.combo_ports.clear()
        self.combo_ports.addItems(port_list)

        # 尝试恢复之前的选择(如果该端口仍然存在)
        if current_selection in port_list:
            index = self.combo_ports.findText(current_selection)
            if index >= 0:
                self.combo_ports.setCurrentIndex(index)
        elif port_list:  # 如果列表不为空,默认选择第一个
            self.combo_ports.setCurrentIndex(0)

        print(f"界面已更新,可用串口: {port_list}")

    def closeEvent(self, event):
        """重写关闭事件,确保停止监控器"""
        self.monitor.stop()
        event.accept()

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

运行效果:运行上述程序后,窗口会显示当前可用的串口(如 COM3, /dev/ttyUSB0)。当你插入或拔出一个 USB 串口设备时,控制台会打印变更日志,并且下拉框中的列表会自动刷新。

进阶优化与平台特定方法

减少轮询开销

如果担心定时轮询对性能有影响,可以:

  • 动态调整轮询间隔:设备稳定时延长间隔(如 5 秒),检测到一次变化后,在短时间内提高频率(如 500 毫秒)连续检查几次,以应对系统枚举设备较慢的情况。
  • 使用线程:将端口扫描放在单独的线程中,避免阻塞主界面。

Windows 事件驱动方法(可选)

对于 Windows 平台,可以实现更高效的事件驱动监听。这需要重写主窗口的 nativeEvent 方法或使用事件过滤器来捕获 WM_DEVICECHANGE 消息。此方法更复杂,但响应更及时。

# 简要示例:在MainWindow中增加事件过滤(仅Windows有效)
from PyQt6.QtCore import QEvent
import ctypes
import win32con  # 需要 pip install pywin32

class MainWindow(QMainWindow):
    # ... 之前的初始化代码 ...

    def nativeEvent(self, eventType, message):
        """Windows 原生事件处理"""
        try:
            msg = ctypes.wintypes.MSG.from_address(message.__int__())
            if msg.message == win32con.WM_DEVICECHANGE:
                # 收到设备变更消息,通知监控器立即检查(或直接在这里处理)
                print("收到设备变更系统消息")
                # 可以在这里直接调用 self.monitor._check_ports() 或发一个信号
                self.monitor._check_ports()
        except:
            pass
        return super().nativeEvent(eventType, message)

Linux/macOS 目录监控方法(可选)

在 Linux/macOS 上,可以使用 QFileSystemWatcher 监控 /dev 目录。

from PyQt6.QtCore import QFileSystemWatcher

class SerialPortMonitor(QObject):
    def __init__(self):
        super().__init__()
        self.watcher = QFileSystemWatcher()
        # 注意:可能需要监控 /dev 目录,但权限和事件粒度需要处理
        # self.watcher.addPath('/dev')
        # self.watcher.directoryChanged.connect(self._check_ports)
        # 更简单的做法是监控一个特定的符号链接目录或结合轮询
        # 此处作为思路展示,不展开实现

总结

本文提供了一种在 PyQt6 应用中实现串口设备热插拔检测的可靠、跨平台的解决方案。核心是使用 QTimer 定期轮询并结合 pyserial 库来检测串口列表变化。虽然轮询方式在实时性上略有妥协,但其实现简单、稳定性高,非常适合大多数桌面应用场景。

完整代码

定时器扫描

import sys
from PyQt6.QtWidgets import QApplication, QMainWindow, QComboBox, QVBoxLayout, QWidget, QLabel
from PyQt6.QtCore import QTimer, Qt
from PyQt6.QtSerialPort import QSerialPortInfo

class SerialHotPlugWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("串口热插拔检测 - 轮询方案")
        self.resize(400, 200)

        # UI
        central = QWidget()
        self.setCentralWidget(central)
        layout = QVBoxLayout(central)
        self.cbx_port = QComboBox()
        self.label_tip = QLabel("插拔USB串口会自动刷新下拉框")
        layout.addWidget(self.label_tip)
        layout.addWidget(self.cbx_port)

        # 保存上一次串口列表
        self.last_port_names = set()

        # 定时刷新 500ms一次
        self.timer = QTimer(self)
        self.timer.setInterval(500)
        self.timer.timeout.connect(self.scan_serial_ports)
        self.timer.start()

    def scan_serial_ports(self):
        # 获取当前所有串口名称
        current_ports = {info.portName() for info in QSerialPortInfo.availablePorts()}
        # 新增
        new_ports = current_ports - self.last_port_names
        # 移除
        lost_ports = self.last_port_names - current_ports

        if new_ports:
            print(f"【插入串口】{new_ports}")
        if lost_ports:
            print(f"【拔出串口】{lost_ports}")


        # 更新下拉框
        self.cbx_port.clear()
        self.cbx_port.addItems(sorted(current_ports))

        # 更新历史列表
        self.last_port_names = current_ports

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

运行结果

以上就是Python+PyQt6实现跨平台串口设备热插拔检测的详细内容,更多关于Python设备热插拔检测的资料请关注脚本之家其它相关文章!

相关文章

  • OpenCV 光流Optical Flow示例

    OpenCV 光流Optical Flow示例

    这篇文章主要为大家介绍了OpenCV 光流Optical Flow示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-04-04
  • Python 实现遥感影像波段组合的示例代码

    Python 实现遥感影像波段组合的示例代码

    这篇文章主要介绍了Python 实现遥感影像波段组合的示例代码,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-08-08
  • Python使用Selenium自动进行百度搜索的实现

    Python使用Selenium自动进行百度搜索的实现

    我们今天介绍一个非常适合新手的python自动化小项目,这个例子非常适合新手学习Python网络自动化,不仅能够了解如何使用Selenium,而且还能知道一些超级好用的小工具。感兴趣的可以了解一下
    2021-07-07
  • python中使用matplotlib绘制热力图

    python中使用matplotlib绘制热力图

    热力图,是一种通过对色块着色来显示数据的统计图表,它通过使用颜色编码来表示数据的值,并在二维平面上呈现出来,本文就给大家介绍一下python使用matplotlib绘制热力图的方法,需要的朋友可以参考下
    2023-08-08
  • Python批量写入ES索引数据的示例代码

    Python批量写入ES索引数据的示例代码

    这篇文章主要为大家详细介绍了如何使用python脚本批量写ES数据(需要使用pip提前下载安装es依赖库),感兴趣的小伙伴可以学习一下
    2024-02-02
  • Python中遍历字典过程中更改元素导致异常的解决方法

    Python中遍历字典过程中更改元素导致异常的解决方法

    这篇文章主要介绍了Python中遍历字典过程中更改元素导致错误的解决方法,针对增删元素后出现dictionary changed size during iteration的异常解决做出讨论和解决,需要的朋友可以参考下
    2016-05-05
  • Python使用openpyxl实现Excel超链接批量化设置

    Python使用openpyxl实现Excel超链接批量化设置

    在Excel中,超链接是一种非常有用的功能,本文我们将介绍如何使用Python来处理Excel中的超链接,以及如何将超链接与对应的工作表链接起来,需要的可以参考一下
    2023-07-07
  • Python打印各种图形实现方式

    Python打印各种图形实现方式

    文章介绍了如何使用循环打印各种图形,包括直角三角形、矩形、反向直角三角形、等边三角形和变形三角形,通过外层循环控制行数,内层循环控制每行的显示,将复杂的图形拆分为简单的图形进行打印
    2026-03-03
  • python中列表对象pop()方法的使用说明

    python中列表对象pop()方法的使用说明

    这篇文章主要介绍了python中列表对象pop()方法的使用说明,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-05-05
  • Python中String模块示例详解

    Python中String模块示例详解

    string模块主要包含关于字符串的处理函数,这篇文章主要介绍了Python中String模块示例代码,代码简单易懂,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-12-12

最新评论