C# 使用 LibUsbDotNet 实现 USB 设备检测

news2024/12/27 10:43:58

国庆节回来后的工作内容,基本都在围绕着各种各样的硬件展开,这无疑让本就漫长的 “七天班” ,更加平添了三分枯燥,我甚至在不知不觉中学会了,如何给打印机装上不同尺寸的纸张。华为的 Mate 60 发布以后,人群中此起彼伏地传出 “遥遥领先” 的声音,大概人类总是热衷于评价那些不甚了解的事物。这个现象到了工作中就会变成,总有某些人觉得某件事情特别简单。其实。一切你认为“简单”的东西,背后一定有无数的人们上下求索、苦心孤诣,就像计算机从早期的埃尼阿克(ENIAC)发展到今天的智能手机,你能使用它并不代表它就“简单”,人还是应该对为止的领域保持敬畏和谦逊。回到这篇文章,今天我想和大家聊一聊,我为了解决那些“简单”的问题而做出的尝试。本期的故事主角是我们最熟悉不过的 USB 设备,有道是 “千古兴亡多少事”,且听我娓娓道来。

故事是这样的,基于某些不可抗因素上的考虑,博主需要在程序中集成某厂商的硬件。我猜测,人们觉得这件事情“简单”,或许是看到这个设备有一条 USB 连接线,因为在人们的固有印象中,只要把它接到电脑上就可以正常工作了。事实的确如此,因为你只要考虑串口(SerialPort)、USB 以及这两者间的相互转换即可。当然,这世上的事情圆满者少,遗憾者多,博主在使用过程中发现,厂商的提供的 SDK 存在 Bug,当设备从电脑上拔出后,其 SDK 的初始化函数依然正常返回了,这意味着我们无法在使用设备前“正确”地检测出硬件状态。考虑厂商愿不愿意修复这个 Bug 还是个未知数,博主不得不尝试另辟蹊径。

在这里插入图片描述

相信这张图片大家都见过无数次啦,在这里你可以看到操作系统接入的各种设备。以鼠标为例,通过下面这个对话框,我们可以获得这个设备的各种属性信息:

在这里插入图片描述

在各种属性信息中,硬件 Id 是最为关键的一组信息,我们可以看到鼠标这个设备的 VID 为 0000,PID 为 3825。其中,VID 是指 Vender ID,即:供应商识别码;PID 是指 Product ID,即:产品识别码。事实上,所有的 USB 设备都有 VID 和 PID,VID 由供应商向 USB-IF 申请获得,而 PID 则由供应商自行指定,计算机正是 VID、PID 以及设备的版本号来决定加载或者安装相应的驱动程序。因此,如果想要判断计算机是否连接了某个 USB 设备,我们可以使用下面的方案:

bool HasUsbDevice(string vid, string pid)
{
    var query = $"SELECT * FROM Win32_PnPEntity WHERE DeviceID LIKE 'USB%VID_{vid}&PID_{pid}%'";
    var searcher = new ManagementObjectSearcher(query);
    var devices = searcher.Get();
    return devices.Count > 0;
}

需要说明的是,这是通过古老的 WMI 来查询 USB 设备信息,还记得我们前面收集到的 VID 以及 PID 吗?此时,我们需要简单调用一下即可:

if (HasUsbDevice2("0000", "3825") {
    Console.WriteLine("[WMI]设备已连接");
} else {
    Console.WriteLine("[WMI]设备未连接");
}

当然,在 .NET 8.0 发布以后,依然固执地抱着这些 Windows 平台的 API 不放,多少有点食古不化的意味。所以,实际工作中我会推荐本文题目中的 LibUsbDotNet 库,除了跨平台方面的考量,这个库的功能要更强大一点,可以做到向 USB 设备发送数据或者从 USB 设备接收数据。下面由我来对这个库的使用进行说明,目前,我们可以从 Github 以及 SourceForge 上下载对应的项目,两者的区别是 Github 上的项目更新一点:

  • https://github.com/LibUsbDotNet/LibUsbDotNet
  • https://sourceforge.net/projects/libusbdotnet/

下载后是一个可执行文件,我们点击安装即可,它会安装好相关的库以及驱动文件,默认的安装目录为:C:\Program Files\LibUsbDotNet。在安装完成后,它会提示我们进入下面的对话框,这一步的目的是给特定的设备安装 libusb 驱动,因为只有安装了驱动的情况下,接下来的一切才会发生,除非 LibUsbDotNet 会隔空取物。

在这里插入图片描述

这里,我们还是选择鼠标这个硬件,你需要重点关注 PID 以及 VID 两个参数,因为这是唯一能区分不同 USB 设备的标识:

在这里插入图片描述

最后,点击 “Install” 按钮即可为当前设备安装 libusb 驱动。接下来的事情就变得非常简单啦,我们只需要通过 NuGet 安装 LibUsbDotNet 即可:

bool HasUsbDevice(short vid, short pid)
{
    var useDeviceFinder = new UsbDeviceFinder(vid, pid);
    var usbDevice = UsbDevice.OpenUsbDevice(useDeviceFinder);
    return usbDevice != null;
}

可以注意到,LibUsbDotNet 需要的 VID 以及 PID 都是 short 类型的,所以,相比于 WMI 的方案,它在调用上会存在一点差异:

var verdorId = Convert.ToInt16("0x0000", 16);
var productId = Convert.ToInt16("0x3825", 16);
if (HasUsbDevice(verdorId, productId)) {
    Console.WriteLine("[LibUsbDotNet]设备已连接");
} else {
    Console.WriteLine("[LibUsbDotNet]设备未连接");
}

显然,你会注意到,我在原本的 “0000” 和 “3825” 前面都补了 “0x” 这样的字符,这是因为 VID 和 PID 都是 16 位的二进制数,它们都可以简写为 4 位十六进制数,所以,不管是在 Windows 上还是 LibUsbDotNet 提供的软件中,它都是以 4 位十六进制数的简写形式存在的。因此,这里就需要进行先补 “0x” 再做转换的处理。

在这里插入图片描述

除了判断 USB 设备是否存在,有时候我们还需要关注 USB 设备的状态变化。例如,插入 USB 设备或者拔出 USB 设备。古人云:世上本无事,庸人自扰之。可这个世界上还就真的有这般无聊的人,动辄喜欢搞拔设备、拔网线这种所谓的深度测试。所以,下面我们来考虑如何处理这种极端的场景。从一开始,博主选择 LibUsbDotNet 这个库,就是看到它提供了 DeviceNotifier 这个类型。不过,在博主后续的尝试中发现,截止到 2.2.29 版本,这个类型已然无迹可寻,而 3.X 版本目前依然出于预发行状态,并且 API 与现在的版本不兼容,所以,这个念头不得不就此作罢。

在这里插入图片描述

当然,在觉醒了 WMI 的远古记忆以后,我们会意识到 Windows 下存在着一个大型的数据库,理论上我们只需要查询这个数据库,就可以监听到 USB 设备的状态变化。如图所示,我们会注意到每个硬件的对话框里有一个 “事件” 选项卡,而这些事件最终会在事件查看器里面汇合。在 ChatGPT 以及 wbemtest 的帮助下,我们找到了两个重要的重要的类名:__InstanceCreationEvent__InstanceDeletionEvent。此时,我们可以编写出下面的代码:

void MonitorUsbDevice()
{
    // 监听 USB 设备插入
    var queryInsert = new WqlEventQuery("SELECT * FROM __InstanceCreationEvent WITHIN 1 WHERE TargetInstance ISA 'Win32_USBControllerDevice'");
    var watcherInsert = new ManagementEventWatcher(queryInsert);
    watcherInsert.EventArrived += (sender, e) =>
    {
        // 被插入的逻辑处理
        var targetInstance = (ManagementBaseObject)e.NewEvent["TargetInstance"];
        // \\SNOWFLY-PC\root\cimv2:Win32_PnPEntity.DeviceID="HID\\VID_0000&PID_3825\\6&2BE8ADFA&0&0000"
        var deviceId = targetInstance.Properties["Dependent"].Value.ToString();
        var device = new ManagementObject(deviceId);

        var args = new DeviceNotifierEventArgs();
        // Win32_PnPEntity.DeviceID="HID\\VID_0000&PID_3825\\6&2BE8ADFA&0&0000"
        args.DeviceId = device.Path.RelativePath.Split("=")[1].Replace("\"", "");
        args.DevicePath = device.Path.ToString();
        args.Pid = "0x" + deviceId.Split(new char[] { '&', '\\' }).FirstOrDefault(x => x.StartsWith("PID_")).Replace("PID_", "");
        args.Vid = "0x" + deviceId.Split(new char[] { '&', '\\' }).FirstOrDefault(x => x.StartsWith("VID_")).Replace("VID_", "");
        if (!args.DeviceId.StartsWith("USB")) return;
        Console.WriteLine($"设备已插入 => {JsonConvert.SerializeObject(args)}");
    };
    watcherInsert.Start();

    var queryDelete = new WqlEventQuery("SELECT * FROM __InstanceDeletionEvent WITHIN 1 WHERE TargetInstance ISA 'Win32_USBControllerDevice'");
    var watcherDelete = new ManagementEventWatcher(queryDelete);
    watcherDelete.EventArrived += (sender, e) =>
    {
        // 被拔出的逻辑处理
        var targetInstance = (ManagementBaseObject)e.NewEvent["TargetInstance"];
        // \\SNOWFLY-PC\root\cimv2:Win32_PnPEntity.DeviceID="HID\\VID_0000&PID_3825\\6&2BE8ADFA&0&0000"
        var deviceId = targetInstance.Properties["Dependent"].Value.ToString();
        var device = new ManagementObject(deviceId);

        var args = new DeviceNotifierEventArgs();
        // Win32_PnPEntity.DeviceID="HID\\VID_0000&PID_3825\\6&2BE8ADFA&0&0000"
        args.DeviceId = device.Path.RelativePath.Split("=")[1].Replace("\"", "");
        args.DevicePath = device.Path.ToString();
        args.Pid = "0x" + deviceId.Split(new char[] { '&', '\\' }).FirstOrDefault(x => x.StartsWith("PID_")).Replace("PID_", "");
        args.Vid = "0x" + deviceId.Split(new char[] { '&', '\\' }).FirstOrDefault(x => x.StartsWith("VID_")).Replace("VID_", "");
        if (!args.DeviceId.StartsWith("USB")) return;
        Console.WriteLine($"设备已拔出 => {JsonConvert.SerializeObject(args)}");
    };
    watcherDelete.Start();
}

理解这段代码基本上没有任何难度,唯一需要说明的地方是,插入或者拔出一个 USB 设备实际上会产生两条消息,它们分别表示的是设备实例与接口实例的创建。这个话听起来或许有些晦涩,可能连微软都不知道它自己在说什么。具体到博主的这个示例中,其规律是两者的 DeviceID 格式不同,一次是 HID ,一个是 USB,因此,我们只需要过滤掉 HID 的那条消息即可。最终,博主实现的效果如下图所示:

在这里插入图片描述

有了这个思路,我们就可以在程序启动时对 USB 设备进行监控,一旦发现某个重要的设备被移除,程序就可以及时地做出响应或处理,而不用等到真正要用设备的时候引发异常,我越来越觉得,编程本质就是一群聪明人在千方百计地照顾一个“巨婴”,每次测试同事都说这里或者那里要加一个提示,可即使增加了提示,人们依然无止无休地问你为什么,错误信息不过是程序员自我安慰剂,除了程序员以外没有人会在乎它具体是什么。如果你对此怀疑表示怀疑的话,不妨回去翻翻你写的代码,有多少行是真正的、有用的代码,又有多少代码是为了防呆呢?好了,以上就是这篇博客的全部内容啦,本文完。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1113420.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

RabbitMQ 消息模型

参考 ​​​​​​【RabbitMQ】RabbitMQ架构模型_rabbitmq结构模型-CSDN博客 之前的学习都只是知道名字,但并没有真正的理解,每次看还是不懂,所以今日理解透 ! RabbitMQ 收发消息过程如下: 首先从消费者开始&#xff1…

云南毕业旅游攻略

第一站:长沙-大理 大理景点推荐 苍山:大理的最佳观景台,拥有变幻万千的云景和素负盛名的大理“风花雪月”四景之一的苍山雪景。可以乘坐索道上山,观赏珍珑棋局、清碧溪、七龙女池、苍山大峡谷、玉带云游路等。洱海:大理…

文件的逻辑结构(顺序文件,索引文件)

所谓的“逻辑结构”,就是指在用户看来,文件内部的数据应该是如何组织起来的。 而“物理结构”指的是在操作系统看来,文件的数据是如何存放在外存中的。 1.无结构文件 无结构文件:文件内部的数据就是一系列二进制流或字符流组成。无明显的逻…

ChatGPT的狂飙之路

ChatGPT的狂飙之路 第一章:AI顶流-闪耀互联网世界的新宠 根据UBS发布的研究报告显示,ChatGPT在1月份的月活跃用户数已达1亿,成为史上用户数增长最快的消费者应用。TikTok在全球上线后花了大约9个月的时间才增加了1亿用户,而Inst…

冰箱监控温度需要安装温度采集器需要什么条件

冰箱监控温度需要安装一个温度采集器在冰箱内部,以实时监测冰箱的温度。采集器可以是数字温度传感器、热敏电阻或其他类型的温度传感器。 当然也需要安装信号中继器也就是我们的智能网关,用于接收和记录温度采集器的数据。这一套系统就是温度监控系统&am…

【毕设必备】手把手带你用Python搭建一个简单的后端服务- API的创建,前后端交互的数据传递,GET,POST,JSON,FLASK

目录 Python 介绍Python的特性Python的使用场景python基本语法 FlaskViewModelControlhtmlsimple api连接数据库 跨域 Mojo比python快68000倍相关链接 Python 介绍 Python是一种流行的高级编程语言,具有易于学习和使用的特性,被广泛应用于各种领域。 P…

移动硬盘被格式化了如何恢复数据?四步教你如何恢复

在日常生活中,我们常常会使用各种存储设备来保存和备份我们的重要数据。移动硬盘作为一种便携式的存储设备,被广泛应用于数据的存储和传输。然而,有时候我们会不小心将移动硬盘格式化,从而丢失了里面的数据。本文将介绍移动硬盘格…

如何通过沉浸式投影技术提升文旅夜游的互动体验?

伴随着国民经济的提升,文旅夜游市场也开始通过各类创新设计形式,来吸引更多的游客前来打卡游玩,使其逐渐成为了当下热度较高的一种游玩模式,其中在收集各类用户的体验反馈时,沉浸式投影依靠新颖的视觉体验以及沉浸式观…

安装Apache2.4

二、安装配置Apache: 中文官网:Apache 中文网 官网 (p2hp.com) 我下的是图中那个版本,最新的64位 下载下后解压缩。如解压到D:\Program Files\Apache\Apache24 PS:特别要注意使用的场景和64位还是32位版本 2、修改Apcahe配置文…

2023.10.18

头文件 #ifndef WIDGET_H #define WIDGET_H#include <QWidget> #include <QDebug>QT_BEGIN_NAMESPACE namespace Ui { class Widget; } QT_END_NAMESPACEclass Widget : public QWidget {Q_OBJECTpublic:Widget(QWidget *parent nullptr);~Widget();private slot…

电商数据采集的10个经典方法

电商数据采集的网页抓取数据、淘宝、天猫、京东等平台的电商数据抓取&#xff0c;网页爬虫、采集网站数据、网页数据采集软件、python爬虫、HTM网页提取、APP数据抓包、APP数据采集、一站式网站采集技术、BI数据的数据分析、数据标注等成为大数据发展中的热门技术关键词。那么电…

什么是零拷贝

普通拷贝流程 在实际应用中&#xff0c;如果我们需要把磁盘中的某个文件内容发送到远程服务器上&#xff0c;那么它必须要经过几个拷贝的过程&#xff0c;。从磁盘中读取目标文件内容拷贝到内核缓冲区&#xff0c;CPU 控制器再把内核缓冲区的数据赋值到用户空间的缓冲区中&…

全志R128外设模块配置——ADC按键配置方法

ADC 按键配置方法 FreeRTOS平台上使用的按键为ADC-KEY&#xff0c;采用的ADC模块为GPADC。 按键功能驱动的实现是通过ADC分压&#xff0c;使每个按键检测的电压值不同&#xff0c;从而实现区分不同的按键。按下或者弹起中断之后&#xff0c;通过中断触发&#xff0c;主动检测…

电子技术基础之一(电容和电感)

Electronic Techonolgy 1、电容和电感1.1、电容(Capacitor)1.1.1、滤波功能1.1.2、储能功能 1.2、电感(Inductor)1.2.1、楞次定律1.2.2、储能作用 1、电容和电感 先讲一个概念&#xff0c;电流分为直流电和交流电&#xff0c;其中直流电再分为稳定直流电和脉动直流电。 直流电…

如何使用VSCode将iPad Pro转化为功能强大的开发工具?

文章目录 前言1. 本地环境配置2. 内网穿透2.1 安装cpolar内网穿透(支持一键自动安装脚本)2.2 创建HTTP隧道 3. 测试远程访问4. 配置固定二级子域名4.1 保留二级子域名4.2 配置二级子域名 5. 测试使用固定二级子域名远程访问6. iPad通过软件远程vscode6.1 创建TCP隧道 7. ipad远…

SpringBoot (3) Profiles,外部化配置,自定义starter

目录 1 Profiles 1.1 "组件"环境隔离 1.1.1 标识环境 1.1.2 激活环境 1.2 "配置"环境隔离 1.2.1 添加"副配置文件" 1.2.2 激活环境 2 外部化配置 2.1 配置优先级 2.2 快速部署 3 自定义starter 3.1 基本抽取 3.1.1 导yaml提示包 3…

mac录屏怎么录?看这里,告别繁琐操作!

屏幕录制是mac用户在日常生活中经常需要使用到的功能&#xff0c;无论是记录游戏精彩瞬间、分享在线会议、制作教学视频还是保存重要资料&#xff0c;屏幕录制都可以帮助我们更便捷地实现这些需求。但是很多用户并不知道mac录屏怎么录。在本文中&#xff0c;我们将介绍两种mac录…

TT119电源芯片用什么代替?

根据你提供的信息&#xff0c;Tt119电源芯片的替代选项可以是AH8652芯片。 AH8652芯片采用了SOT23-3封装&#xff0c;与Tt119芯片相同。同时&#xff0c;你提到外围元件也是一模一样的&#xff0c;因此不需要进行任何更改。 AH8652芯片的输入电压范围为45V至265V&#xff0c;…

用好快捷键事半功倍!如何在Excel中快速取消隐藏的行

工作时,我们需要经常取消隐藏Excel中的行。要知道行是否被隐藏,请仔细检查行号。如果缺少某些数字,则意味着行被隐藏。这样做很容易,而且您可以用不同的快捷方式取消隐藏Excel中的行。 在本文中,我将讨论如何在各种情况下取消隐藏Excel中的行的两种快捷方式。这些快捷方式…

【JavaEE】Java的多线程编程基础知识 -- 多线程篇(2)

Java多线程编程基础知识 一、多线程的创建二、Thread类常用的方法和API2.1 Thread 的几个常见的属性2.2 start 启动一个线程2.3 终止一个线程2.4 等待一个线程-join()2.5 线程休眠函数 -sleep() 三、线程状态3.1 观察所有线程的状态3.2 线程状态和线程转移的意义 四、线程安全&…