news 2026/9/7 19:40:28

Qt5 USB设备检测实战:Linux udev与Windows通知机制解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Qt5 USB设备检测实战:Linux udev与Windows通知机制解析

简介:需要为Qt应用加入USB设备热插拔检测的开发者,可直接使用这份基于wang-bin-qdevicewatcher、并已适配QT5环境的项目包。资源面向有一定Qt基础、正在做设备管理或硬件交互的开发者,解决跨平台实时监控USB设备插拔事件的问题。包内共72个文件,以cpp/h源码、pro工程配置为主,同时附带so/dll预编译库、exe示例、pri模块文件及VS工程文件,整体仅1.18MB,目录按src/lib/bin等划分,便于按需查阅。已有1382人学习下载。通过学习可掌握QDeviceWatcher的信号槽用法、设备路径监控、以及设备插入/移除事件的处理逻辑,并参考作者在Windows/Linux下的跨平台编译最佳实践,免去自行适配底层API的重复工作,快速集成到自己的Qt项目中。 做 Qt 客户端开发的朋友迟早会接到一个需求:程序要能实时感知 USB 设备的插拔,并且把设备的 VID/PID、名称、串口节点显示出来。我第一次做这个功能时以为调一个系统 API 就行,结果在 Windows 和 Linux 上分别踩了一轮坑,才搞明白关键不在 Qt,而在操作系统如何把热插拔事件传递到应用层。这篇分享围绕“QT5 检测 USB 设备”这件事,把两种平台下的底层机制、可直接抄的代码、以及最容易引起线上事故的时序问题讲透。适合正在用 QT5 写上位机、工控检测程序,或者需要在 Qt 应用里管理 usb 转串口设备的开发者参考,全文没有理论堆砌,都是从实际项目里复盘的结论。

1. 需求其实分三层:接入事件、设备信息、文件节点

不要急着写代码。先厘清“检测 USB 设备”到底要检测什么。我见过不少同事把需求和轮询方案绑在一起,最后换来一堆 bug。实际上这个需求可以拆成三层,每一层对应不同的实现难点。

1.1 第一层:插拔事件本身

最基础的是“什么时候插入、什么时候拔出”。这里有个容易被忽略的粒度问题:你要的是 USB 总线上的物理设备,比如一个 U 盘、一个扫码枪,还是一个逻辑接口,比如 usb 转串口产生的 /dev/ttyUSB0?两者时机不同,而且“拔出”不一定先于“端口失效”,极端情况下还会漏事件。代码上要拿到的是系统的 add/remove 动作,而不是自己在设备列表里比对差值。

1.2 第二层:设备属性

光知道插拔还不够,界面上通常要显示厂商名、设备名、VID/PID、序列号、设备类。这部分在 Windows 上要查设备管理器,在 Linux 上要读 sysfs 或 udev 数据库。有时设备已经接入但属性读不到,比如“设备描述符请求失败”时,VID/PID 可能是空值或异常值,这时候程序不能崩,界面要能显示异常状态。

1.3 第三层:文件节点与业务动作

很多场景最终要打开设备,比如 usb 转串口要打开 COM9 或 /dev/ttyUSB0,U 盘要挂载或读取 /dev/sda1。这一层最容易出问题:事件到了,节点还没就绪;节点就绪了,又被占用。后面专门用一节讲这个坑。

把“检测”理解成三层之后,再选技术路线就清晰多了。说直白一点,这套需求是事件驱动 + 属性查询 + 节点确认三件事的组合,不是一个简单的“查询当前是否有 USB 设备”的接口。

1.4 事件驱动 vs 轮询扫描:先做对比再动手

我建议直接用事件通知机制,但为了让你说服自己或者同事,先看对比表格:

对比项事件通知(RegisterDeviceNotification / udev)轮询扫描(QTimer + 设备列表比对)
实时性毫秒级,系统事件一发生就到取决于定时器间隔,1 秒算快的
资源占用几乎为零,事件没来就一直闲着CPU 周期性忙一次,嵌入式平台不友好
实现复杂度中,要处理 GUID、系统消息、权限低,代码直观
可靠性一般不会漏,但事件送达可能早于驱动就绪可能漏瞬时插拔,也可能读到半初始化状态
跨平台思路Linux 用 udev,Windows 用设备通知两边都用 QTimer,逻辑统一

我的选择很直接:GUI 程序一定要走事件通知。轮询只适合没有窗口的服务进程,或者只是“界面加个刷新按钮”这种弱需求。事件驱动能拿到操作系统层面的插拔动作,程序在后台也能保持状态同步,后期维护起来逻辑干净得多。

2. 底层机制:热插拔事件在 Linux 和 Windows 上分别怎么流转

要写出靠谱的代码,你得知道事件从设备到 Qt 中间到底经过哪些环节。很多网上抄来的代码,直接改个类名就贴进项目里,一旦收不到事件或者事件重复,立刻抓瞎,根源就是不懂机制。

2.1 Linux 侧:内核 -> udev -> netlink -> Qt

Linux 上 USB 设备插入后,内核 USB 核心会注册一个新的 usb_device 对象,同时把信息暴露在 /sys/bus/usb/devices/ 下。用户态的 udev 服务通过内核 netlink 获取 uevent,再更新设备节点和 udev 数据库。

应用层监听热插拔有两种常见姿势:

  • 直接用udev_monitor_new_from_netlink()订阅 udev 广播消息;
  • 自己解析 netlink socket 的字符串,不推荐,容易解析错。

两种方式本质都是读一个文件描述符(fd)。好消息是 Qt 的 QSocketNotifier 可以直接把这个 fd 挂进事件循环,有数据到了自动调用槽函数,不用开线程,也不用自己写 poll。这也是选择 libudev 的最大理由:它把复杂的 uevent 解析封装好了,同时能和 Qt 事件循环无缝协作。

2.2 Windows 侧:PnP -> RegisterDeviceNotification -> WM_DEVICECHANGE

Windows 把设备接入信息封装成窗口消息 WM_DEVICECHANGE,由即插即用(PnP)管理器广播给注册过的窗口。Qt 程序虽然是跨平台框架,但它本质上还是一个 Win32 消息循环,所以我们可以用 RegisterDeviceNotification 注册感兴趣的设备类别,再通过 QAbstractNativeEventFilter 拦截 WM_DEVICECHANGE,把结果转成 Qt signal。

这里有个很重要的点:注册通知时不能只写“广义 USB”,要通过 GUID 指定设备接口类。USB 设备、USB HID、COM 口对应的 GUID 各不相同,选错类别就收不到消息。后面代码里我会给出常用的 GUID 写法。

2.3 机制层面的三个约束

  • 事件发生在系统消息层,回调时机要当成“中断上下文”看待,不能在里面做阻塞操作。
  • Linux 的事件源是 QSocketNotifier,Windows 的事件源是 nativeEventFilter,两者都要尽量快,重活丢给队列或定时器。
  • 事件到达不代表驱动就绪,这是两个平台都存在的时序问题,后面专门展开。

理解了这条链路,写的代码才会具备“排错直觉”。比如 Windows 收不到消息,你会先检查 GUID 和 HWND,而不是怀疑 Qt 版本。

3. Linux 实战:用 udev + QSocketNotifier 写 USB 检测模块

这部分直接给能编译的代码。我写这个类的时候只依赖 libudev 和 Qt 的事件循环,没有引入额外的框架,在真实项目里跑得很稳。

3.1 环境准备与 .pro 配置

Ubuntu/Debian 系需要 libudev-dev,CentOS/RHEL 上包名叫 libudev-devel。装好后在 .pro 里加一行:

QT += core gui widgets CONFIG += c++11 unix: LIBS += -ludev

3.2 核心类头文件

#ifndef USBMONITOR_H #define USBMONITOR_H #include <QObject> #include <QSocketNotifier> struct udev; struct udev_monitor; class UsbMonitor : public QObject { Q_OBJECT public: explicit UsbMonitor(QObject *parent = nullptr); ~UsbMonitor() override; signals: void deviceAdded(const QVariantMap &info); void deviceRemoved(const QVariantMap &info); private slots: void handleUdevEvent(int socket); private: udev *m_udev = nullptr; udev_monitor *m_monitor = nullptr; QSocketNotifier *m_notifier = nullptr; }; #endif // USBMONITOR_H

3.3 核心类实现

#include "usb_monitor.h" #include <libudev.h> #include <QVariantMap> #include <QDebug> UsbMonitor::UsbMonitor(QObject *parent) : QObject(parent) { m_udev = udev_new(); if (!m_udev) { qWarning("udev_new failed"); return; } m_monitor = udev_monitor_new_from_netlink(m_udev, "udev"); udev_monitor_filter_add_match_subsystem_devtype(m_monitor, "usb", "usb_device"); udev_monitor_enable_receiving(m_monitor); int fd = udev_monitor_get_fd(m_monitor); m_notifier = new QSocketNotifier(fd, QSocketNotifier::Read, this); connect(m_notifier, &QSocketNotifier::activated, this, &UsbMonitor::handleUdevEvent); } UsbMonitor::~UsbMonitor() { if (m_notifier) m_notifier->setEnabled(false); if (m_monitor) udev_monitor_unref(m_monitor); if (m_udev) udev_unref(m_udev); } void UsbMonitor::handleUdevEvent(int socket) { if (!m_monitor) return; udev_device *dev = udev_monitor_receive_device(m_monitor); if (!dev) return; const char *action = udev_device_get_action(dev); const char *sysname = udev_device_get_sysname(dev); const char *devnode = udev_device_get_devnode(dev); const char *vid = udev_device_get_property_value(dev, "ID_VENDOR_ID"); const char *pid = udev_device_get_property_value(dev, "ID_MODEL_ID"); const char *vendor = udev_device_get_property_value(dev, "ID_VENDOR"); const char *model = udev_device_get_property_value(dev, "ID_MODEL"); const char *serial = udev_device_get_property_value(dev, "ID_SERIAL_SHORT"); QVariantMap info; info["action"] = QString::fromUtf8(action); info["sysname"] = QString::fromUtf8(sysname); info["devnode"] = devnode ? QString::fromUtf8(devnode) : QString(); info["vid"] = vid ? QString::fromUtf8(vid) : QString(); info["pid"] = pid ? QString::fromUtf8(pid) : QString(); info["vendor"] = vendor ? QString::fromUtf8(vendor) : QString(); info["model"] = model ? QString::fromUtf8(model) : QString(); info["serial"] = serial ? QString::fromUtf8(serial) : QString(); if (qstrcmp(action, "add") == 0) emit deviceAdded(info); else if (qstrcmp(action, "remove") == 0) emit deviceRemoved(info); udev_device_unref(dev); }

过滤器用"usb", "usb_device",这个细节很关键。如果不加 devtype 过滤,一个 USB 转串口线插入时会产生 usb_device、usb_interface、tty 等多条事件,你会看到一个物理设备重复插入。如果你业务上还需要监听串口节点,可以再注册一个 tty 子系统过滤器,或者在里面判断devnode后缀。

类里基本没做重活:收到事件,解析属性,打包成 QVariantMap,发射信号。这里不用 QThread,因为 QSocketNotifier 本身已经挂在 Qt 事件循环上,数据来了自然会回调。

3.4 使用方式与验证

UsbMonitor monitor; QObject::connect(&monitor, &UsbMonitor::deviceAdded, [](const QVariantMap &info) { qInfo() << "USB插入:" << info.value("vendor").toString() << info.value("model").toString() << "VID/PID:" << info.value("vid").toString() << info.value("pid").toString() << "节点:" << info.value("devnode").toString(); }); QObject::connect(&monitor, &UsbMonitor::deviceRemoved, [](const QVariantMap &info) { qInfo() << "USB拔出:" << info.value("sysname").toString(); });

验证时建议开两个终端,一个跑程序,另一个执行:

udevadm monitor --property --udev

插拔一个 U 盘,两边的输出对比一下,你就能看到 Qt 收到的属性和系统原始 uevent 的映射关系。这个习惯能帮你快速确认是不是过滤条件写窄了。

4. Windows 实战:用 RegisterDeviceNotification 补齐跨平台拼图

Windows 的实现思路和 Linux 完全不同。它的事件源头是窗口消息,所以你的 Qt 程序必须先有一个窗口句柄,然后注册设备通知。

4.1 初始化设备通知

头文件里除了 QObject,还要继承 QAbstractNativeEventFilter:

#ifndef USBMONITOR_WIN_H #define USBMONITOR_WIN_H #include <QObject> #include <QAbstractNativeEventFilter> #include <QVariantMap> class QWidget; class UsbMonitor : public QObject, public QAbstractNativeEventFilter { Q_OBJECT public: explicit UsbMonitor(QWidget *owner, QObject *parent = nullptr); ~UsbMonitor(); bool nativeEventFilter(const QByteArray &eventType, void *message, qintptr *result) override; signals: void deviceAdded(const QVariantMap &info); void deviceRemoved(const QVariantMap &info); private: void handleDeviceChange(const QString &devicePath, bool arrived); void *m_devNotifyHandle = nullptr; QWidget *m_owner = nullptr; }; #endif // USBMONITOR_WIN_H

实现文件里先注册通知:

#include "usb_monitor_win.h" #include <windows.h> #include <dbt.h> #include <initguid.h> #include <setupapi.h> #include <devguid.h> #include <QWidget> #include <QDebug> // usb device interface 的 GUID,SDK 里在 usbiodef.h 有定义 DEFINE_GUID(GUID_USB_DEVICE_INTERFACE, 0xa5dcbf10, 0x6530, 0x11d2, 0x90, 0x1f, 0x00, 0xc0, 0x4f, 0xb9, 0x51, 0xed); UsbMonitor::UsbMonitor(QWidget *owner, QObject *parent) : QObject(parent), m_owner(owner) { qApp->installNativeEventFilter(this); DEV_BROADCAST_DEVICEINTERFACE filter; ZeroMemory(&filter, sizeof(filter)); filter.dbcc_size = sizeof(DEV_BROADCAST_DEVICEINTERFACE); filter.dbcc_devicetype = DBT_DEVTYP_DEVICEINTERFACE; filter.dbcc_classguid = GUID_USB_DEVICE_INTERFACE; m_devNotifyHandle = RegisterDeviceNotification( (HWND)owner->winId(), &filter, DEVICE_NOTIFY_WINDOW_HANDLE); if (!m_devNotifyHandle) qWarning() << "RegisterDeviceNotification failed:" << GetLastError(); } UsbMonitor::~UsbMonitor() { if (m_devNotifyHandle) { UnregisterDeviceNotification(m_devNotifyHandle); m_devNotifyHandle = nullptr; } qApp->removeNativeEventFilter(this); }

RegisterDeviceNotification必须要一个 HWND 参数。如果传主窗口,那么主窗口销毁前必须先 unregister,否则窗口销毁后系统还会往旧句柄发消息。我实际踩过这个坑:窗口关闭后再次插拔设备,程序直接崩溃,原因就是通知没注销。

4.2 拦截 WM_DEVICECHANGE 并转成 Qt signal

bool UsbMonitor::nativeEventFilter(const QByteArray &eventType, void *message, qintptr *result) { MSG *msg = static_cast<MSG *>(message); if (msg->message == WM_DEVICECHANGE) { if (msg->wParam == DBT_DEVICEARRIVAL || msg->wParam == DBT_DEVICEREMOVECOMPLETE) { DEV_BROADCAST_HDR *hdr = reinterpret_cast<DEV_BROADCAST_HDR *>(msg->lParam); if (hdr && hdr->dbch_devicetype == DBT_DEVTYP_DEVICEINTERFACE) { DEV_BROADCAST_DEVICEINTERFACE *di = reinterpret_cast<DEV_BROADCAST_DEVICEINTERFACE *>(hdr); QString devicePath = QString::fromWCharArray(di->dbcc_name); handleDeviceChange(devicePath, msg->wParam == DBT_DEVICEARRIVAL); } } } return false; }

注意 lParam 可能为空,尤其是设备移除事件在某些系统版本上会有异常消息,所以要先判空。还有一个细节:DBT_DEVICEARRIVALDBT_DEVICEREMOVECOMPLETE只是最常见的两个,严格来说还有DBT_DEVNODES_CHANGED,如果想捕获“设备信息变化但不涉及插拔”的场景也可以监听。

4.3 根据设备路径解析 VID/PID

从 dbcc_name 拿到的是一长串 device interface path,例如:

\\?\usb#vid_046d&pid_c539#6&2e6c0f32&0&2#{a5dcbf10-6530-11d2-901f-00c04fb951ed}

这串东西不适合直接显示,我们要用 SetupAPI 枚举当前的 USB 设备接口,找到匹配项,再取设备实例 ID(instance ID),从中解析 VID/PID:

void UsbMonitor::handleDeviceChange(const QString &devicePath, bool arrived) { QVariantMap info; info["devicePath"] = devicePath; HDEVINFO devInfo = SetupDiGetClassDevs( &GUID_USB_DEVICE_INTERFACE, nullptr, nullptr, DIGCF_DEVICEINTERFACE | DIGCF_PRESENT); if (devInfo == INVALID_HANDLE_VALUE) return; SP_DEVICE_INTERFACE_DATA ifData; SP_DEVINFO_DATA devData; BYTE buffer[4096]; DWORD index = 0; while (true) { ZeroMemory(&ifData, sizeof(ifData)); ifData.cbSize = sizeof(ifData); if (!SetupDiEnumDeviceInterfaces(devInfo, nullptr, &GUID_USB_DEVICE_INTERFACE, index++, &ifData)) break; DWORD size = 0; if (!SetupDiGetDeviceInterfaceDetail(devInfo, &ifData, nullptr, 0, &size, nullptr) && GetLastError() != ERROR_INSUFFICIENT_BUFFER) continue; auto *detail = reinterpret_cast<SP_DEVICE_INTERFACE_DETAIL_DATA *>(buffer); detail->cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA); ZeroMemory(&devData, sizeof(devData)); devData.cbSize = sizeof(devData); if (SetupDiGetDeviceInterfaceDetail(devInfo, &ifData, detail, sizeof(buffer), nullptr, &devData)) { if (QString::fromWCharArray(detail->DevicePath) == devicePath) { wchar_t instanceId[256] = {0}; if (SetupDiGetDeviceInstanceId(devInfo, &devData, instanceId, 256, nullptr)) { info["instanceId"] = QString::fromWCharArray(instanceId); } break; } } } SetupDiDestroyDeviceInfoList(devInfo); if (arrived) emit deviceAdded(info); else emit deviceRemoved(info); }

拿到 instanceId 之后,解析vid_046dpid_c539这类字段就很容易了。Windows SDK 里也提供了SetupDiGetDeviceRegistryProperty去查设备描述、厂商名等,业务需要时再补。如果你的进程是 Windows 服务,没有窗口句柄,RegisterDeviceNotification 这套方案不成立,要改用 CM_Register_Notification,项目里有服务端检测需求的朋友需要额外注意。

5. 存量设备枚举:程序启动时把已经插上的设备拉出来

只做事件监听的程序有个尴尬局面:设备在程序启动前就插好了,事件永远不会再发生,界面初始状态是空的,用户还以为是 bug。所以事件监听和存量枚举必须配合使用。

5.1 Linux 下扫描 /sys/bus/usb/devices

Linux 的 sysfs 非常适合做这事,不用 root 权限,直接读文件:

QList<QVariantMap> listCurrentUsbDevices() { QList<QVariantMap> result; QDir usbDir("/sys/bus/usb/devices"); const QStringList entries = usbDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); for (const QString &entry : entries) { QDir devDir = usbDir.filePath(entry); QFile vidFile(devDir.filePath("idVendor")); QFile pidFile(devDir.filePath("idProduct")); if (!vidFile.open(QIODevice::ReadOnly) || !pidFile.open(QIODevice::ReadOnly)) continue; QVariantMap m; m["vid"] = QString::fromLatin1(vidFile.readAll()).trimmed(); m["pid"] = QString::fromLatin1(pidFile.readAll()).trimmed(); m["sysname"] = entry; result.append(m); } return result; }

这个目录下同时包含 usb_device 和 usb_interface 两种条目。usb_interface 没有 idVendor/idProduct 文件,所以读取失败会自动跳过,剩下的都是物理 USB 设备。

5.2 Windows 下用 SetupAPI 枚举设备接口

Windows 同样可以用 SetupAPI 枚举当前已连接的 USB 设备:

HDEVINFO devInfo = SetupDiGetClassDevs( &GUID_USB_DEVICE_INTERFACE, nullptr, nullptr, DIGCF_DEVICEINTERFACE | DIGCF_PRESENT); SP_DEVICE_INTERFACE_DATA ifData; for (DWORD i = 0; ; ++i) { ZeroMemory(&ifData, sizeof(ifData)); ifData.cbSize = sizeof(ifData); if (!SetupDiEnumDeviceInterfaces(devInfo, nullptr, &GUID_USB_DEVICE_INTERFACE, i, &ifData)) break; // 获取 DevicePath、instanceId,参考上一节 handleDeviceChange } SetupDiDestroyDeviceInfoList(devInfo);

这段代码和事件处理里的解析逻辑高度相似,建议抽成公共函数,事件和枚举共用一套解析,避免两边字段不一致。

5.3 为什么启动枚举不能省

我接手过一个项目,上线后客户报“程序重启后 U 盘状态丢失”,排查了很久才发现实现里只有一个 udev 监听,完全没有启动枚举。事件通知只能覆盖“当前时间点之后”的变化,启动前的现状必须主动查一遍。正确顺序是:启动时调一次枚举,得到初始设备列表;再启动事件监听,两个数据源合并成统一状态。这样无论设备是什么时候插的,界面都能正确显示。

6. 最容易翻车的地方:回调时机、设备路径和驱动就绪

代码能跑通之后,真正的上线考验才开始。以下是几个我在现场和测试环境反复遇到的坑,每一个都值得写在注释里。

6.1 事件到了,但设备还没就绪

Windows 上收到 DBT_DEVICEARRIVAL 后,立刻打开 COM 口大概率失败,错误是“设备未就绪”或“无法找到指定文件”。Linux 上 udev 发出 usb_device 的 add 事件时,/dev/ttyUSB0 可能还没生成完。

我现在的做法是:收到 add 事件后不立刻处理业务,先用 QTimer::singleShot 延迟几百毫秒,再检查节点是否存在;如果存在就继续,不存在就做有限次重试。延迟时间根据设备驱动类型调整,普通的 USB 转串口 300 到 800 毫秒基本够用,某些 4G 上网卡可能要更久。

QTimer::singleShot(400, this, [this, info]() { const QString devnode = info.value("devnode").toString(); if (devnode.isEmpty() || !QFile::exists(devnode)) { qWarning() << "设备节点尚未就绪:" << info.value("sysname").toString(); return; } // 设备就绪,执行打开串口等操作 });

这个延迟不是玄学,也不是“为了保险随便加的”,本质是给驱动枚举和节点创建留时间。把业务动作放进定时器,还能避免事件回调里执行耗时代码阻塞 Qt 事件循环。

6.2 拔出事件来临时的资源清理

设备拔出后,程序里的文件句柄不会自动失效。Linux 下打开 /dev/ttyUSB0 的 fd 仍然存在,但 read/write 会返回 I/O 错误;Windows 下 QSerialPort 会进入错误状态,错误码往往是资源不存在。

所以 deviceRemoved 信号里要做的第一件事是关闭所有相关句柄,并通知上层清理缓存状态。有些同事习惯在拔出事件里弹一个 QMessageBox,这非常危险:模态对话框会嵌套进事件循环,后续的移除事件全部排队,用户在极端拔插场景下可能看到几十个弹窗。正确姿势是只发信号,由 UI 层决定如何展示。

6.3 设备描述符请求失败与异常设备

USB 协议层面,设备插入后主机会发送 GET_DESCRIPTOR 请求读取设备描述符。如果线材太长、供电不足、设备芯片异常,就可能出现“设备描述符请求失败”,Windows 设备管理器显示 Unknown USB Device。

从检测代码的角度看,这类设备可能还是会触发插入事件,但 VID/PID 读取异常或为空。程序要把它识别为“未知设备”而不是当作正常设备处理。排查这类问题的手段我提两个:Windows 下可以用 Wireshark 加 USBPcap 做 usb 抓包,Linux 下直接看dmesg -w,里面会输出device descriptor read/64, error -71之类的内核日志。看到这个,就能确认是设备硬件或线缆问题,而不是应用层代码问题。

6.4 快速连续插拔和事件风暴

USB 集线器上同时插入多个设备、或者用户快速插拔,会产生大量事件。QSocketNotifier 一次激活可能只读掉一条 uevent,但事件被排在 netlink socket 的缓冲区里。如果消息积压过多,较早的事件可能丢失,尤其是移除事件。

我的缓解方案是在 handleUdevEvent 里做循环,把当前 socket 里可读的 uevent 全部取完再返回。另一个建议是按“串号 + VID/PID + 父子设备地址”做去重,不要把事件里的设备路径当作唯一标识,因为它每次插入都可能变化。

7. 长时间运行后沉淀下来的三个习惯

这套方案在公司两个产品线上跑了快一年,期间迭代过几轮,最后我养成了三个习惯,对排查问题帮助很大。

第一,所有 USB 事件统一打日志。QVariantMap 打包完成后,不管谁消费,都先写一条异步日志。现场用户报“设备没反应”的时候,日志能立刻判断是系统没发事件,还是 Qt 收到事件但业务处理失败,省掉大量远程扯皮。

第二,设备匹配用序列号,不用路径。devnode、devicePath 每次插入都可能变化,只有 VID/PID + serial 才能唯一确定一个物理设备。串口读不到时至少要用 VID/PID 兜底。

第三,把检测模块和 UI 拆开。UsbMonitor 内部不弹窗、不写界面,只发 signal。这样既方便用 QTest 做自动化,也能在无界面的服务进程里复用同一套事件逻辑。以后从 QT5 迁到 Qt6,要动的部分也主要集中在事件过滤器接口的细微差异上,业务层几乎不受影响。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 19:38:39

基于SpringBoot的城市美食排行榜网站的设计与实现源码+文档

温馨提示&#xff1a;本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;本人主页置顶文章(点我)开头有 CSDN 平台…

作者头像 李华
网站建设 2026/9/7 19:38:02

在很多人生的十字路口,处于35岁左右的女性朋友往往会遇到一个职业的分水岭:有的人面临发展瓶颈需要复合能力升级,有的人因家庭原因短暂停职后希望再就业,还有的人想彻底转换赛道寻找更有长期发展空间的工作。

当您在搜索“35岁女想考个实用的证”时&#xff0c;说明您已经意识到了提升自我的重要性。但在当下的求职现状中&#xff0c;证书的种类繁多&#xff0c;培训机构的宣传口径又五花八门。很多女性朋友的核心痛点在于&#xff1a;花了大量时间和金钱考下来的证&#xff0c;到了面…

作者头像 李华
网站建设 2026/9/7 19:35:26

伯努利试验与二项分布:从经典真题到工程实践

伯努利试验与二项分布&#xff0c;这两个概念在概率论里属于“看着简单、用着容易翻车”的那一类。最近刚好在一套考研复习题里看到一道关于重复独立试验的经典真题&#xff0c;评论区不少人因为“恰好命中”和“至少命中一次”之间的转换出了问题。我干脆把这类题背后的核心逻…

作者头像 李华
网站建设 2026/9/7 19:34:19

VSCode 配置 MATLAB 开发环境:从安装到调试全指南

开头 前阵子帮一个师弟配 MATLAB 开发环境&#xff0c;他习惯用 VSCode 写代码&#xff0c;不想每次跑个脚本都切回 MATLAB 桌面。折腾了一会儿把环境搭好后&#xff0c;我觉得这套流程值得整理出来。网上关于“VSCode 中使用 MATLAB”的教程不少&#xff0c;但要么是零散记一笔…

作者头像 李华
网站建设 2026/9/7 19:33:13

Tox自动化测试与虚拟环境管理配置实战

1. 从“包管理地狱”到自动化测试矩阵做过Python项目的人&#xff0c;大概率都遇到过这样一个场景&#xff1a;本地代码跑得好好的&#xff0c;一换环境就崩&#xff1b;明明在Windows上测试通过&#xff0c;同事在Linux上一跑就报错&#xff1b;更别提不同Python版本之间那点微…

作者头像 李华
网站建设 2026/9/7 19:30:14

Spark on YARN 实战调优:从内存模型到 CPU 核数疑难杂症

开头 写这个系列的前几篇时&#xff0c;我都是以“把环境跑起来、把任务提交上去”为主线&#xff0c;到了第七篇&#xff0c;思路得换一换了。Scala 和 Spark 这套技术栈&#xff0c;真正难的不是 API 怎么调&#xff0c;而是你搭好集群、提交作业之后&#xff0c;那些藏在日…

作者头像 李华