C# uses LibUsbDotNet to implement USB device detection

The work content after returning from the National Day basically revolves around various hardware, which undoubtedly makes the already long “Seven-day shift” even more boring. I even I unknowingly learned how to load paper of different sizes into the printer. After Huawei’s Mate 60 was released, voices of “Far ahead” came out one after another from the crowd. Perhaps humans are always keen to evaluate things that they don’t fully understand. This phenomenon will become when it comes to work. There are always some people who think something is particularly easy. actually. Everything you think is “simple” must have countless people searching and working hard behind it, just like the development of computers from the early ENIAC (ENIAC) to today Just because you can use a smartphone doesn’t mean it’s “simple“. People should still remain in awe and humility in the field so far. Back to this article, today I want to talk to you about my attempts to solve those “simple” problems. The protagonist of this story is the USB device that we are most familiar with. It is said that “How many things have happened through the ages“, let me tell you.

The story goes like this. Due to some force majeure factors, the blogger needs to integrate hardware from a certain manufacturer into the program. My guess is that people find this “easy” because they see that the device has a USB cable, because they have the impression that they just plug it into their computer and it works. This is indeed the case, because you only need to consider serial port (SerialPort), USB and the conversion between the two. Of course, there are few things in this world that are perfect and many that are regretful. During the use, the blogger discovered that there was a bug in the SDK provided by the manufacturer. When the device was unplugged from the computer, the initialization function of the SDK still returned normally. This This means that we cannot detect the hardware status “correctly” before using the device. It is still unknown whether the manufacturer is willing to fix this bug, so the blogger has to try to find another way.

I believe everyone has seen this picture countless times. Here you can see various devices connected to the operating system. Taking the mouse as an example, through the following dialog box, we can obtain various attribute information of this device:

Among various attribute information, the hardware ID is the most critical set of information. We can see that the VID of the mouse device is 0000 and the PID is 3825. Among them, VID refers to Vender ID, that is, supplier identification code; PID refers to Product ID, that is, product identification code. In fact, all USB devices have VID and PID. The VID is obtained by the supplier from the USB-IF, and the PID is specified by the supplier. It is the VID, PID and the version number of the device that the computer decides to load or install. Drivers. Therefore, if we want to determine whether a USB device is connected to the computer, we can use the following solution:

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

It should be noted that this is querying USB device information through the ancient WMI. Do you remember the VID and PID we collected earlier? At this point, we need to simply call:

if (HasUsbDevice2("0000", "3825") {
    Console.WriteLine("[WMI] device is connected");
} else {
    Console.WriteLine("[WMI] The device is not connected");
}

Of course, after the release of .NET 8.0, we still stubbornly cling to these Windows platform APIs, which is a bit clinging to the past. Therefore, in actual work, I would recommend the LibUsbDotNet library in the title of this article. In addition to cross-platform considerations, this library is more powerful and can send data to or receive data from USB devices. Let me explain the use of this library. Currently, we can download the corresponding projects from Github and SourceForge. The difference between the two is that the projects on Github are newer:

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

After downloading, it is an executable file. We can click to install it. It will install the relevant libraries and driver files. The default installation directory is: C:\Program Files\LibUsbDotNet. After the installation is complete, it will prompt us to enter the following dialog box. The purpose of this step is to install the libusb driver for a specific device, because only when the driver is installed, everything will happen next, unless LibUsbDotNet will be empty Get something.

Here, we still choose the mouse as hardware. You need to focus on the two parameters PID and VID, because these are the only identifiers that can distinguish different USB devices:

Finally, click the “Install” button to install the libusb driver for the current device. The next thing becomes very simple, we only need to install LibUsbDotNet through NuGet:

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

It can be noted that the VID and PID required by LibUsbDotNet are both of the short type, so compared to the WMI solution, there will be a little difference in the call:

var verdorId = Convert.ToInt16("0x0000", 16);
var productId = Convert.ToInt16("0x3825", 16);
if (HasUsbDevice(verdorId, productId)) {
    Console.WriteLine("[LibUsbDotNet] device is connected");
} else {
    Console.WriteLine("[LibUsbDotNet] device is not connected");
}

Obviously, you will notice that I added characters like “0x” in front of the original “0000” and “3825”. This is because VID and PID are both 16-bit binary numbers, and they can be abbreviated to 4 digits. Hexadecimal number, so whether it is on Windows or in the software provided by LibUsbDotNet, it exists in the abbreviation form of a 4-digit hexadecimal number. Therefore, it is necessary to add “0x” first and then perform conversion.

In addition to determining whether the USB device exists, sometimes we also need to pay attention to the status changes of the USB device. For example, plug in a USB device or unplug a USB device. The ancients said: There is nothing wrong in the world, only mediocre people bother themselves. But there are really such boring people in this world who like to engage in so-called in-depth tests such as unplugging devices and network cables. So, let’s consider how to deal with this extreme scenario. From the beginning, the blogger chose the LibUsbDotNet library because it provided the DeviceNotifier type. However, in subsequent attempts by the blogger, it was discovered that as of version 2.2.29, this type is no longer traceable, and version 3.X is still in pre-release status, and the API is incompatible with the current version, so, This idea had to be dropped.

Of course, after awakening the ancient memory of WMI, we will realize that there is a large database under Windows. In theory, we only need to query this database to monitor the status changes of USB devices. As shown in the figure, we will notice that there is an “Events” tab in the dialog box of each hardware, and these events will eventually be merged in the event viewer. With the help of ChatGPT and wbemtest, we found two important important class names: __InstanceCreationEvent, __InstanceDeletionEvent. At this point, we can write the following code:

void MonitorUsbDevice()
{
    // Listen for USB device insertion
    var queryInsert = new WqlEventQuery("SELECT * FROM __InstanceCreationEvent WITHIN 1 WHERE TargetInstance ISA 'Win32_USBControllerDevice'");
    var watcherInsert = new ManagementEventWatcher(queryInsert);
    watcherInsert.EventArrived + = (sender, e) =>
    {
        //Inserted logic processing
        var targetInstance = (ManagementBaseObject)e.NewEvent["TargetInstance"];
        // \SNOWFLY-PC\root\cimv2:Win32_PnPEntity.DeviceID="HID\VID_0000 & amp;PID_3825\6 & amp;2BE8ADFA & amp;0 & amp;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[] { ' & amp;', '\' }).FirstOrDefault(x => x.StartsWith("PID_ ")).Replace("PID_", "");
        args.Vid = "0x" + deviceId.Split(new char[] { ' & amp;', '\' }).FirstOrDefault(x => x.StartsWith("VID_ ")).Replace("VID_", "");
        if (!args.DeviceId.StartsWith("USB")) return;
        Console.WriteLine($"The device has been inserted => {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) =>
    {
        // Logic processing of being pulled out
        var targetInstance = (ManagementBaseObject)e.NewEvent["TargetInstance"];
        // \SNOWFLY-PC\root\cimv2:Win32_PnPEntity.DeviceID="HID\VID_0000 & amp;PID_3825\6 & amp;2BE8ADFA & amp;0 & amp;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[] { ' & amp;', '\' }).FirstOrDefault(x => x.StartsWith("PID_ ")).Replace("PID_", "");
        args.Vid = "0x" + deviceId.Split(new char[] { ' & amp;', '\' }).FirstOrDefault(x => x.StartsWith("VID_ ")).Replace("VID_", "");
        if (!args.DeviceId.StartsWith("USB")) return;
        Console.WriteLine($"The device has been unplugged => {JsonConvert.SerializeObject(args)}");
    };
    watcherDelete.Start();
}

There is basically no difficulty in understanding this code. The only thing that needs to be explained is that plugging or unplugging a USB device actually generates two messages, which represent the creation of a device instance and an interface instance respectively. This may sound a bit obscure, and even Microsoft may not know what it is talking about. Specifically in the blogger’s example, the rule is that the DeviceID formats of the two are different, one is HID and the other is USB. Therefore, we only need to filter out the HID message. In the end, the effect achieved by the blogger is as shown in the figure below:

With this idea, we can monitor USB devices when the program starts. Once an important device is found to be removed, the program can respond or process it in time without waiting until the device is actually used. Abnormal, I feel more and more that the essence of programming is a group of smart people trying their best to take care of a “giant baby”. Every time the test colleagues say that a prompt should be added here or there, but even if the prompts are added, people are still endless. To ask you why, the error message is just a programmer’s self-placement, and no one except the programmer cares what it is. If you doubt this, you might as well go back and look through the code you wrote. How many lines of code are real and useful, and how much of the code is just to prevent fools? Okay, that’s all the content of this blog, this article is over.