Skip to main content

因为手上的其中一个项目需要监视 USB 或串口,网上可以搜到很多基于 Windows 通知机制以及 PyQt4 的资料。但在此项目基于的 PyQt5 中,事件函数 winEvent 已经被 nativeEvent 所取代。为此我基于网络上的文档,在 PyQt 5.12 上重新编写了串口监视模块。

遇到的坑

通知的 lParam 参数会包含变化的端口数据,需要转成 DBT 结构体才可以正常读出。DEV_BROADCAST_HDR 其实是一个通用结构体,需要读取设备类型才能判断要用什么具体的结构体来转换。我之前看一些文章里串口也是用的 DEV_BROADCAST_DEVICEINTERFACE,结果无法正确读取路径,必须要用对应的 DEV_BROADCAST_PORT 才能读出变更端口的名称,注意一定要对应!

DBT 结构体需要参考微软 API 文档进行编写: https://docs.microsoft.com/en-us/windows/desktop/api/dbt/ns-dbt-dev_broadcast_port_w

在 PyQt 中,只有 QWidget 或继承 QWidget 的类才拥有句柄,并且拥有 nativeEventcloseEvent 等函数的事件机制,而 QObject 是没有的,这就是为什么我的模块用的是 QWidget

另外,如果你想直接在主窗口使用,而不是像我这样包成一个类,可以不用注册与反注册通知,因为在最顶层的窗口 PyQt 会默认注册这项通知。

类源码


import ctypes
import logging
from ctypes import wintypes
from PyQt5 import QtCore, QtWidgets

RegisterDeviceNotification = ctypes.windll.user32.RegisterDeviceNotificationW
UnregisterDeviceNotification = ctypes.windll.user32.UnregisterDeviceNotification


class GUID(ctypes.Structure):
    _fields_ = [
        ('Data1', wintypes.DWORD),
        ('Data2', wintypes.WORD),
        ('Data3', wintypes.WORD),
        ('Data4', wintypes.BYTE * 8),
    ]

    def __str__(self):
        return "{{{:08x}-{:04x}-{:04x}-{}-{}}}".format(
            self.Data1,
            self.Data2,
            self.Data3,
            ''.join(["{:02x}".format(d) for d in self.Data4[:2]]),
            ''.join(["{:02x}".format(d) for d in self.Data4[2:]]),
        )


class DEV_BROADCAST_PORT(ctypes.Structure):
    _pack_ = 1
    _fields_ = [("dbcp_size", wintypes.DWORD),
                ("dbcp_devicetype", wintypes.DWORD),
                ("dbcp_reserved", wintypes.DWORD),
                ("dbcp_name", wintypes.WCHAR * 256)]


class DEV_BROADCAST_HDR(ctypes.Structure):
    _fields_ = [("dbch_size", wintypes.DWORD),
                ("dbch_devicetype", wintypes.DWORD),
                ("dbch_reserved", wintypes.DWORD)]


DBT_DEVTYP_PORT = 0x00000003
DBT_DEVICEREMOVECOMPLETE = 0x8004
DBT_DEVICEARRIVAL = 0x8000
WM_DEVICECHANGE = 0x0219
DBT_DEVTYP_DEVICEINTERFACE = 5
DEVICE_NOTIFY_WINDOW_HANDLE = 0x00000000
DEVICE_NOTIFY_ALL_INTERFACE_CLASSES = 0x00000004

logger = logging.getLogger("SerialEnumerator")


class SerialEnumerator(QtWidgets.QWidget):
    device_discovered = QtCore.pyqtSignal(str)
    device_removed = QtCore.pyqtSignal(str)

    def __init__(self):
        super().__init__()
        self.setupNotification()

    def setupNotification(self):
        dbp = DEV_BROADCAST_PORT()
        dbp.dbcp_size = ctypes.sizeof(DEV_BROADCAST_PORT)
        dbp.dbcp_devicetype = DBT_DEVTYP_DEVICEINTERFACE
        self.hNofity = RegisterDeviceNotification(int(self.winId()),
                                                  ctypes.byref(dbp),
                                                  DEVICE_NOTIFY_WINDOW_HANDLE)
        if self.hNofity == 0:
            logger.debug(ctypes.FormatError(), int(self.winId()))
            logger.debug("RegisterDeviceNotification failed")

    def closeEvent(self, event):
        super().closeEvent(event)
        if self.hNofity:
            UnregisterDeviceNotification(self.hNofity)

    def nativeEvent(self, eventType, message):
        retval, result = super().nativeEvent(eventType, message)
        if eventType == "windows_generic_MSG":
            msg = wintypes.MSG.from_address(message.__int__())
            if msg.message == WM_DEVICECHANGE:
                if DBT_DEVICEARRIVAL == msg.wParam or DBT_DEVICEREMOVECOMPLETE == msg.wParam:
                    dbh = DEV_BROADCAST_HDR.from_address(msg.lParam)
                    if dbh.dbch_devicetype == DBT_DEVTYP_PORT:
                        dbd = DEV_BROADCAST_PORT.from_address(msg.lParam)
                        if DBT_DEVICEARRIVAL == msg.wParam:
                            self.device_discovered.emit(dbd.dbcp_name)
                        elif DBT_DEVICEREMOVECOMPLETE == msg.wParam:
                            self.device_removed.emit(dbd.dbcp_name)
        return retval, result

如何使用

在程序中基于类定义一个对象,并连接 device_removeddevice_discovered 信号至对应的槽函数即可,当设备变化时将会传送端口的友好名字(例如 COM1)过来。

Hintay

帝王鸽

发表评论

表情 图片 粗体 删除线 居中 斜体 下划线 段落引用 代码