因为手上的其中一个项目需要监视 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
的类才拥有句柄,并且拥有 nativeEvent
和 closeEvent
等函数的事件机制,而 QObject
是没有的,这就是为什么我的模块用的是 QWidget
。
另外,如果你想直接在主窗口使用,而不是像我这样包成一个类,可以不用注册与反注册通知,因为在最顶层的窗口 PyQt 会默认注册这项通知。
类源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 |
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_removed
与 device_discovered
信号至对应的槽函数即可,当设备变化时将会传送端口的友好名字(例如 COM1
)过来。