【问题标题】:Getting the "path" of connected/disconnected USB HID devices in C#在 C# 中获取连接/断开的 USB HID 设备的“路径”
【发布时间】:2020-11-28 17:19:02
【问题描述】:

作为一名飞行模拟爱好者,我正在制作自己的驾驶舱硬件。我使用 USB HID 接口(基于 PIC 微控制器的硬件)将此硬件与我的计算机连接。所有硬件都具有相同的 VID/PID,产品描述符字符串为“FMGS HW Device”(仅供参考:FMGS 是我正在使用的 Airbus A320 模拟器软件的名称)。

在启动时,我的应用程序会扫描所有 USB 设备,并且只将具有我的“VID/PID/产品描述符”组合的设备放入字典中。作为关键,我正在使用通过 PSP_DEVICE_INTERFACE_DETAIL_DATA 结构检索到的“DevicePath”,并调用 SetupDiGetDeviceInterfaceDetail (setupapi.dll)。

下面是一小段代码摘录,展示了此操作的核心 - 只是为了让您明白:

// Retrieving the detailDataBuffer
SetupApi.SetupDiGetDeviceInterfaceDetail(deviceInfoSet, ref deviceInterfaceData, detailDataBuffer, bufferSize, ref bufferSize, IntPtr.Zero);
 
// Skip over cbsize (4 bytes) to get the address of the devicePathName
var pDevicePath = new IntPtr(detailDataBuffer.ToInt32() + 4);

// Get the String containing the devicePath
AddDevice(Marshal.PtrToStringAuto(pDevicePath));

此 DevicePath 具有以下格式:

\?\hid#vid_04d8&pid_003f#9&599cfdc&0&0000#{4d1e55b2-f16f-11cf-88cb-001111000030}

问题 1: 我可以清楚地看到 VID/PID 部分 (vid_04d8&pid_003f) 和 HidGuid (4d1e55b2-f16f-11cf-88cb-001111000030),但是“9&599cfdc&0&0000”的部分是什么?每个设备都是独一无二的吗?换句话说,我的 2 台设备具有完全相同的 DevicePath 是否存在风险?

现在我还想检测应用程序运行时设备是否连接/断开。如果设备断开连接,我需要将它们从我的字典中删除。如果设备已连接,我需要将它们放入我的字典并开始与它们通信。

我正在使用“WMI 方法”(我知道 WndProc 中还有一个 WM_DEVICECHANGE 方法,不知道哪个更好?)。以下是代码(尚未完成,但目前有效)。

    private void DeviceInsertedEvent(object sender, EventArrivedEventArgs e)
    {
        Debug.WriteLine($"DeviceInsertEvent");
        ManagementBaseObject instance = (ManagementBaseObject)e.NewEvent["TargetInstance"];

        foreach(PropertyData p in instance.Properties )
        {
            Debug.WriteLine($"{p.Name} = {p.Value }");
        }

        Debug.WriteLine($"{instance.Properties["Dependent"].Value }");
    }

    private void DeviceRemovedEvent(object sender, EventArrivedEventArgs e)
    {
        Debug.WriteLine($"DeviceRemovedEvent");
        ManagementBaseObject instance = (ManagementBaseObject)e.NewEvent["TargetInstance"];

        foreach (PropertyData p in instance.Properties)
        {
            Debug.WriteLine($"{p.Name} = {p.Value }");
        }

        Debug.WriteLine($"{instance.Properties["Dependent"].Value}");
    }

    private void backgroundWorker_DoWork(object sender, DoWorkEventArgs e)
    {
        var scope = new ManagementScope("root\\CIMV2");
        scope.Options.EnablePrivileges = true;

        try
        {
            WqlEventQuery insertQuery = new WqlEventQuery();
            insertQuery.EventClassName = "__InstanceCreationEvent";
            insertQuery.WithinInterval = new TimeSpan(0, 0, 1);
            insertQuery.Condition = @"TargetInstance ISA 'Win32_USBControllerdevice'";
            ManagementEventWatcher insertWatcher = new ManagementEventWatcher(insertQuery);
            insertWatcher.EventArrived += new EventArrivedEventHandler(DeviceInsertedEvent);
            insertWatcher.Start();

            WqlEventQuery removeQuery = new WqlEventQuery();
            removeQuery.EventClassName = "__InstanceDeletionEvent";
            removeQuery.WithinInterval = new TimeSpan(0, 0, 1);
            removeQuery.Condition = @"TargetInstance ISA 'Win32_USBControllerdevice'";
            ManagementEventWatcher removeWatcher = new ManagementEventWatcher(removeQuery);
            removeWatcher.EventArrived += new EventArrivedEventHandler(DeviceRemovedEvent);
            removeWatcher.Start();
        }
        catch (Exception ex)
        {
            Debug.WriteLine($"backgroundWorker_DoWork Exception: {ex}");
        }

        while (true) ;
    }

我不是 WMI 专家,所以老实说,我不知道自己在做什么(仍在学习)——只是从几个谷歌搜索中获得了代码。我发现 "instance.Properties["Dependent"].Value" 也给了我某种 DevicePath,格式如下。

\DESKTOP-HANS\root\cimv2:Win32_PnPEntity.DeviceID="HID\VID_04D8&PID_003F\9&2E7AE93E&0&0000"

我看到了相同的 VID/PID 组合,以及我在问题 1 中要求的未知“9&2E7AE93E&0&0000”部分。所以基本上,通过一些字符串操作,我可以重建我在字典中用作键的相同 DevicePath。

问题 2: 是否有另一种方法来发现设备连接/断开连接事件,这些事件给我的 DevicePath 与 SetupDiGetDeviceInterfaceDetail 返回的相同?

【问题讨论】:

  • 刚刚发现一篇有趣的文章,通过 WM_DEVICECHANGE 详细解释了到达/移除事件。请在此处找到它:codeproject.com/Articles/14500/…。此事件提供了一个结构 DEV_BROADCAST_DEVICEINTERFACE,其成员 dbcc_name 提供与 SetupDiGetDeviceInterfaceDetail 相同格式的 DevicePath。我还没有尝试过这种方法,但肯定会这样做。 WMI 方法有效(见下文),但当我以每毫秒 1 条消息的速度对设备进行压力测试时,它感觉不是很敏感。只是想看看 WM_DEVICECHANGE 是如何工作的。

标签: c# visual-studio usb hid


【解决方案1】:

我一直在继续上面的概念,它就像一个魅力。我更改了下面的代码,现在我的 USB 设备在我的应用程序运行时可以很好地动态添加或删除。 “令人费解的部分”是在 DeviceCreatedEvent 和 DeviceRemovedEvent 中构造正确的 DevicePath 格式。

_stringHidGuid 在我的 UsbServer 的构造函数中获得。我需要它几次,所以我为它创建了一个私人成员。它是一个格式为的字符串(已经添加了“#”和大括号):

#{4d1e55b2-f16f-11cf-88cb-001111000030}

我使用它根据从 WMI __InstanceCreationEvent 和 __InstanceDeletionEvent 获得的“依赖”值“构造”DevicePath。

_DeviceArrivedWatcher 和 _DeviceRemovedWatcher 是 ManagementEventWatcher 类型的另外 2 个私有成员,用于在初始化代码时保留通知程序。 _vid 和 _pid 包含我的 VID 和 PID。

以下是完整的代码摘录。

    private void DeviceCreatedEvent(object sender, EventArrivedEventArgs e)
    {
        ManagementBaseObject instance = (ManagementBaseObject)e.NewEvent["TargetInstance"];

        // Describes the logical device connected to the Universal Serial Bus (USB) controller.
        string path = (string)instance.Properties["Dependent"].Value;

        // Check if it is our device
        int start = path.IndexOf($"HID\\\\VID_{_vid:X4}&PID_{_pid:X4}");
        if (start != -1)
        {
            // Create the DevicePath to be used in UsbServer
            path = path.Substring(start, path.Length - start - 1).Replace("\\\\", "#");
            path = string.Concat("\\\\?\\", path, _stringHidGuid);
            Debug.WriteLine($"DeviceCreatedEvent for {path}");
            AddDevice(path);
        }
    }

    private void DeviceRemovedEvent(object sender, EventArrivedEventArgs e)
    {
        ManagementBaseObject instance = (ManagementBaseObject)e.NewEvent["TargetInstance"];

        // Describes the logical device connected to the Universal Serial Bus (USB) controller.
        string path = (string)instance.Properties["Dependent"].Value;

        // Check if it is our device
        int start = path.IndexOf($"HID\\\\VID_{_vid:X4}&PID_{_pid:X4}");
        if (start != -1)
        {
            // Create the DevicePath to be used in UsbServer
            path = path.Substring(start, path.Length - start - 1).Replace("\\\\", "#");
            path = string.Concat("\\\\?\\", path, _stringHidGuid);
            Debug.WriteLine($"DeviceRemovedEvent for {path}");
            RemoveDevice(path);
        }
    }

    private void AddDeviceArrivedHandler()
    {
        try
        {
            var scope = new ManagementScope("root\\CIMV2");
            scope.Options.EnablePrivileges = true;

            var q = new WqlEventQuery();
            q.EventClassName = "__InstanceCreationEvent";
            q.WithinInterval = new TimeSpan(0, 0, 1);
            q.Condition = @"TargetInstance ISA 'Win32_USBControllerdevice'";

            _deviceArrivedWatcher = new ManagementEventWatcher(scope, q);
            _deviceArrivedWatcher.EventArrived += DeviceCreatedEvent;
            _deviceArrivedWatcher.Start();
        }
        catch (Exception ex)
        {
            Debug.WriteLine($"AddDeviceArrivedHandler() Exception: {ex}");
            if (_deviceArrivedWatcher != null)
                _deviceArrivedWatcher.Stop();
        }
    }

    private void AddDeviceRemovedHandler()
    {
        try
        {
            var scope = new ManagementScope("root\\CIMV2");
            scope.Options.EnablePrivileges = true;

            var q = new WqlEventQuery();
            q.EventClassName = "__InstanceDeletionEvent";
            q.WithinInterval = new TimeSpan(0, 0, 1);
            q.Condition = @"TargetInstance ISA 'Win32_USBControllerdevice'";

            _deviceRemovedWatcher = new ManagementEventWatcher(scope, q);
            _deviceRemovedWatcher.EventArrived += DeviceRemovedEvent;
            _deviceRemovedWatcher.Start();
        }
        catch (Exception ex)
        {
            Debug.WriteLine($"AddDeviceRemovedHandler() Exception: {ex}");
            if (_deviceRemovedWatcher != null)
                _deviceRemovedWatcher.Stop();
        }
    }

    private void DeviceNotificationsStop()
    {
        try
        {
            if (_deviceArrivedWatcher != null)
                _deviceArrivedWatcher.Stop();
            if (_deviceRemovedWatcher != null)
                _deviceRemovedWatcher.Stop();
        }
        catch (Exception ex)
        {
            Debug.WriteLine($"DeviceNotificationStop() Exception: {ex}");
            throw;
        }
    }

【讨论】:

    【解决方案2】:

    为了获得有关此主题的最佳知识来源,我可以在 GitHub 上推荐 USB Complete The Developer's Guide 书籍、USBViewHClient 官方 MS 示例。

    \?\hid#vid_04d8&pid_003f#9&599cfdc&0&0000#{4d1e55b2-f16f-11cf-88cb-001111000030} 是所谓的设备接口路径。 MS 没有记录其格式(因此可能随时更改)并且解析它不安全。每个设备都可以提供多个接口与之交互。例如每个 USB HID 设备也有与GUID_DEVINTERFACE_USB_DEVICE - {A5DCBF10-6530-11D2-901F-00C04FB951ED} 的接口。

    有两个主要的 Win32 API 用于查询连接的设备:SetupDi*(在SetupAPI.h 中定义)和CM_*(在cfgmgr32.h 中定义)。您也可以使用 WMI,但我不熟悉它。

    关于如何将设备接口路径转换为VID/PID:

    您可以使用CM_Get_Device_Interface_PropertyW(或SetupDiGetDevicePropertyW)和DEVPKEY_Device_InstanceId 参数来请求“设备实例标识符”。之后,您可以通过CM_Locate_DevNodeW(或SetupDiGetClassDevsW)使用您获得的设备实例ID 打开该设备,并在其上查询DEVPKEY_Device_HardwareIds(字符串列表)属性。并返回东西you can parse,因为它记录在案:

    Each interface has a device ID of the following form:
    USB\VID_v(4)&PID_d(4)&MI_z(2)
    
    Where:
      v(4) is the 4-digit vendor code that the USB committee assigns to the vendor.
      d(4) is the 4-digit product code that the vendor assigns to the device.
      z(2) is the interface number that is extracted from the bInterfaceNumber field of the interface descriptor.
    

    PS:在我提到的一本书中找到了带有 VID/PID 过滤的 WMI 示例代码:

    void FindDevice()
    {
        const String deviceIdString = @”USB\VID_0925&PID_150C”;
        _deviceReady = false;
        var searcher = new ManagementObjectSearcher(“root\\CIMV2”, “SELECT PNPDeviceID FROM Win32_PnPEntity”);
        foreach (ManagementObject queryObj in searcher.Get()) {
            if (queryObj[“PNPDeviceID”].ToString().Contains(deviceIdString)) {
                _deviceReady = true;
            }
        }
    }
    

    【讨论】:

    • 4d1e55b2-f16f-11cf-88cb-001111000030 是一个GUID_DEVINTERFACE_HID,可以在运行时通过 Hid.dll 中的HidD_GetHidGuid 调用来检索。
    猜你喜欢
    • 2018-04-03
    • 1970-01-01
    • 1970-01-01
    • 2011-11-02
    • 1970-01-01
    • 1970-01-01
    • 2013-10-07
    • 1970-01-01
    • 2023-04-01
    相关资源
    最近更新 更多