因为手上的其中一个项目需要监视 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 会默认注册这项通知。
类源码
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
)过来。