使用 HID 协议与 USB 设备通信
本文将帮助您了解如何使用 C# 中的 WinAPI 与 USB 设备通信
警告
由于这种 USB 嗅探器以用户模式访问硬件的方式,无法读取 RID 为 0 的 HID 数据包,这是因为 Windows 的保护级别为了防止键盘记录器/间谍软件。
(不要添加 0x,否则应用程序将会崩溃,因为我还没有添加 0x 前缀支持)
背景
这篇文章的实现得益于这个 WDK 示例
基本上,这只是对该示例的重写,但以更简单的方式呈现。
Using the Code
主要代码
void CheckHIDRead()
{
HIDReadInfo.Device = new HID_DEVICE[DeviceDiscovery.FindDeviceNumber()];
DeviceDiscovery.FindKnownHIDDevices(ref HIDReadInfo.Device);
for (var Index = 0; Index < HIDReadInfo.Device.Length; Index++)
{
if (HIDReadInfo.VendorID != 0)
{
var Count = 0;
if (HIDReadInfo.Device[Index].Attributes.VendorID == HIDReadInfo.VendorID)
{
Count++;
}
if (HIDReadInfo.Device[Index].Attributes.ProductID == HIDReadInfo.ProductID)
{
Count++;
}
if (Count == 2)
{
HIDReadInfo.iDevice = Index;
HIDReadInfo.Active = true;
return;
}
}
}
}
void CheckHIDWrite()
{
HIDWriteInfo.Device = new HID_DEVICE[DeviceDiscovery.FindDeviceNumber()];
DeviceDiscovery.FindKnownHIDDevices(ref HIDWriteInfo.Device);
for (var Index = 0; Index < HIDWriteInfo.Device.Length; Index++)
{
if (HIDWriteInfo.VendorID != 0)
{
var Count = 0;
if (HIDWriteInfo.Device[Index].Attributes.VendorID == HIDWriteInfo.VendorID)
{
Count++;
}
if (HIDWriteInfo.Device[Index].Attributes.ProductID == HIDWriteInfo.ProductID)
{
Count++;
}
if (HIDWriteInfo.Device[Index].Caps.UsagePage == HIDWriteInfo.UsagePage)
{
Count++;
}
if (HIDWriteInfo.Device[Index].Caps.Usage == HIDWriteInfo.Usage)
{
Count++;
}
if (Count == 4)
{
HIDWriteInfo.iDevice = Index;
HIDWriteInfo.Active = true;
return;
}
}
}
}
CheckHIDRead()
和 CheckHIDWrite()
用于检查我们是否按下了 读取 或 发送 按钮,以及输入的 数据 (VID-PID-Usa...
) 是否与连接的 USB 设备相对应。
此函数返回要扫描的 USB HID 的数量。
Int32 FindDeviceNumber()
{
var hidGuid = new Guid();
var deviceInfoData = new SP_DEVICE_INTERFACE_DATA();
HidD_GetHidGuid(ref hidGuid);
//
// Open a handle to the plug and play dev node.
//
SetupDiDestroyDeviceInfoList(hardwareDeviceInfo);
hardwareDeviceInfo = SetupDiGetClassDevs(ref hidGuid, IntPtr.Zero,
IntPtr.Zero, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE);
deviceInfoData.cbSize = Marshal.SizeOf(typeof(SP_DEVICE_INTERFACE_DATA));
var Index = 0;
while (SetupDiEnumDeviceInterfaces(hardwareDeviceInfo, IntPtr.Zero,
ref hidGuid, Index, ref deviceInfoData))
{
Index++;
}
return (Index);
}
此函数返回每个 USB 设备所需的数据结构,用于 HIDRead()
和 HIDWrite()
。
static public void FindKnownHIDDevices(ref HID_DEVICE[] HID_Devices)
{
var hidGuid = new Guid();
var deviceInfoData = new SP_DEVICE_INTERFACE_DATA();
var functionClassDeviceData = new SP_DEVICE_INTERFACE_DETAIL_DATA();
Hid.HidD_GetHidGuid(ref hidGuid);
//
// Open a handle to the plug and play dev node.
//
SetupDiDestroyDeviceInfoList(hardwareDeviceInfo);
hardwareDeviceInfo = SetupDiGetClassDevs(ref hidGuid, IntPtr.Zero, IntPtr.Zero,
DIGCF_PRESENT | DIGCF_DEVICEINTERFACE);
deviceInfoData.cbSize = Marshal.SizeOf(typeof(SP_DEVICE_INTERFACE_DATA));
for (var iHIDD = 0; iHIDD < HID_Devices.Length; iHIDD++)
{
SetupDiEnumDeviceInterfaces(hardwareDeviceInfo, IntPtr.Zero, ref hidGuid, iHIDD, ref deviceInfoData);
//
// Allocate a function class device data structure to receive the
// goods about this particular device.
//
var RequiredLength = 0;
SetupDiGetDeviceInterfaceDetail(hardwareDeviceInfo, ref deviceInfoData,
IntPtr.Zero, 0, ref RequiredLength, IntPtr.Zero);
if (IntPtr.Size == 8)
{
functionClassDeviceData.cbSize = 8;
}
else if (IntPtr.Size == 4)
{
functionClassDeviceData.cbSize = 5;
}
//
// Retrieve the information from Plug and Play.
//
SetupDiGetDeviceInterfaceDetail(hardwareDeviceInfo, ref deviceInfoData,
ref functionClassDeviceData, RequiredLength,
ref RequiredLength, IntPtr.Zero);
//
// Open device with just generic query abilities to begin with
//
OpenHIDDevice(functionClassDeviceData.DevicePath, ref HID_Devices, iHIDD);
}
}
此函数扩展了 FindKnownHIDDevices()
。
static void OpenHIDDevice(String DevicePath, ref HID_DEVICE[] HID_Device, Int32 iHIDD)
{
//
// RoutineDescription:
// Given the HardwareDeviceInfo, representing a handle to the plug and
// play information, and deviceInfoData, representing a specific hid device,
// open that device and fill in all the relivant information in the given
// HID_DEVICE structure.
//
HID_Device[iHIDD].DevicePath = DevicePath;
//
// The hid.dll api's do not pass the overlapped structure into deviceiocontrol
// so to use them we must have a non overlapped device. If the request is for
// an overlapped device we will close the device below and get a handle to an
// overlapped device
//
CloseHandle(HID_Device[iHIDD].Handle);
HID_Device[iHIDD].Handle = CreateFile(HID_Device[iHIDD].DevicePath,
FileAccess.ReadWrite, FileShare.ReadWrite,
0, FileMode.Open, FileOptions.None,
IntPtr.Zero);
HID_Device[iHIDD].Caps = new HIDP_CAPS();
HID_Device[iHIDD].Attributes = new HIDD_ATTRIBUTES();
//
// If the device was not opened as overlapped, then fill in the rest of the
// HID_Device structure. However, if opened as overlapped, this handle cannot
// be used in the calls to the HidD_ exported functions since each of these
// functions does synchronous I/O.
//
Hid.HidD_FreePreparsedData(ref HID_Device[iHIDD].Ppd);
HID_Device[iHIDD].Ppd = IntPtr.Zero;
Hid.HidD_GetPreparsedData(HID_Device[iHIDD].Handle, ref HID_Device[iHIDD].Ppd);
Hid.HidD_GetAttributes (HID_Device[iHIDD].Handle, ref HID_Device[iHIDD].Attributes);
Hid.HidP_GetCaps (HID_Device[iHIDD].Ppd , ref HID_Device[iHIDD].Caps);
var Buffer = Marshal.AllocHGlobal(126);
{
Hid.HidD_GetManufacturerString(HID_Device[iHIDD].Handle, Buffer, 126);
HID_Device[iHIDD].Manufacturer = Marshal.PtrToStringAuto(Buffer);
Hid.HidD_GetProductString(HID_Device[iHIDD].Handle, Buffer, 126);
HID_Device[iHIDD].Product = Marshal.PtrToStringAuto(Buffer);
Hid.HidD_GetSerialNumberString(HID_Device[iHIDD].Handle, Buffer, 126);
HID_Device[iHIDD].SerialNumber = Marshal.PtrToStructure<Int32>(Buffer);
}
Marshal.FreeHGlobal(Buffer);
}
接下来是两个重要的函数,它们将使您能够读取或发送 USB HID 和 PC 之间的 HID 数据包。
static async public void BeginAsyncRead(object? state)
{
//
// Read what the USB device has sent to the PC and store the result inside HID_Report[]
//
if (HIDReadInfo.Active == true)
{
var Device = HIDReadInfo.Device[HIDReadInfo.iDevice];
var ReportBuffer = new Byte[Device.Caps.InputReportByteLength];
if (ReportBuffer.Length > 0)
{
await Task.Run(() =>
{
var NumberOfBytesRead = 0U;
Kernel32.ReadFile(Device.Handle, ReportBuffer, Device.Caps.InputReportByteLength, ref NumberOfBytesRead, IntPtr.Zero);
});
HIDReadInfo.ReportData = new List<Byte>(ReportBuffer);
}
}
}
static public void BeginSyncSend(object? state)
{
//
// Sent to the USB device what is stored inside WriteData[]
//
if (HIDWriteInfo.Done is false && HIDWriteInfo.Active is true)
{
var Device = HIDWriteInfo.Device[HIDWriteInfo.iDevice];
var ReportBuffer = new Byte[Device.Caps.OutputReportByteLength];
if (ReportBuffer.Length > 0)
{
//
// Add ReportID to the first byte of HID_ReportContent
//
ReportBuffer[0] = HIDWriteInfo.ReportID;
//
// Copy ReportData into HID_ReportContent starting from index 1
//
Array.Copy(HIDWriteInfo.ReportData, 0, ReportBuffer, 1, ReportBuffer.Length - 1);
var varA = 0U;
Kernel32.WriteFile(Device.Handle, ReportBuffer, Device.Caps.OutputReportByteLength, ref varA, IntPtr.Zero);
}
HIDWriteInfo.Done = true;
}
}
为了能够做到这一点,您需要先设置以下数据
VendorID (供应商ID)
ProductID
UsagePage (用途页)
用法
ReportID (报告ID)
但请小心,您需要为所有这些参数设置正确的值,如果有一个是 false
,您将无法发送 HID 数据包。
要读取 HID 数据包,您只需要
VendorID (供应商ID)
ProductID
此外,如果 USB 设备无法发送数据,您就无法读取,如果您无法读取数据,您也无法写入数据 (在 HID 报告描述符中定义)。
设备由其 VendorID
:ProductID
定义,但被缩小到由其 UsagePage
、Usage
和 ReportID
定义的几个函数中。
例如,鼠标的第一个功能是发送坐标数据,因此您可以从 PC 读取数据,第二个功能是接收鼠标按钮自定义数据,因此您可以从 PC 发送数据。
为了设置这些变量,您需要读取目标 USB 设备的 HID 描述符,可以使用 USB 嗅探器(如 https://github.com/djpnewton/busdog 或 http://www.usblyzer.com/usb-analysis-features.htm)来检索。
HID 描述符通常以 0x05, 0x01 开头。
要学习读取 HID 描述符,请使用此工具:http://www.usb.org/developers/hidpage#HID Descriptor Tool 和 https://docs.linuxkernel.org.cn/hid/hidintro.html
由于此代码只是对 90 年代早期 C 代码的重写,因此它适用于所有 Windows 版本。
历史
- 2019 年 10 月 2 日:初始版本
- 2023 年 9 月 10 日:项目从 Windows Forms .NET framework 升级到 WPF .NET Core