HID client driver sample?

Are there any online samples for how to develop at HID client driver that speaks HID below and exposes itself as something else above?

I've already examined the vhidmini2 & hidusbfx2 samples on Windows-driver-samples/hid at main · microsoft/Windows-driver-samples · GitHub . Unfortunately, both samples expose themselves as HID devices above, which is something else than what I'm trying to do. I've also tried reading Microsoft's "Minidrivers and the HID class driver" documentation, but remain unsure if a WDF HID minidriver is really the right approach for my problem in the first place. Any comments on that?

The reason for asking is that I'm working on addressing limitations in Microsoft's "HidBatt" driver for battery packs that communicate over the "HID Power Device" protocol. I've already developed a filter driver on HidBattery/HidBattExt at master · forderud/HidBattery · GitHub for parsing missing parameters. However, it doesn't address other shortcomings, like a few missing WMI interfaces and the driver being missing from ARM64 builds of Windows. I've already opened support cases to Microsoft for all discovered limitations, and am hoping for a root-cause fix, but would like a short-term solution meanwhile.

Status update: I've posted the same request on HID client driver sample? · Issue #1287 · microsoft/Windows-driver-samples · GitHub after failing to get a response here.

There probably aren't any helpful samples, because there's nothing particularly unique in this situation. You need an INF that matches the "HID\VID_xxxx&PID_xxxx\COL_xxx" collection that you want to claim, and that driver needs to create a PDO with a hardware ID that looks like a battery device. You'll need to translate the APIs, but I presume you know how to do that.

Thanks for clarifying the situation Tim. I then assume I should develop the HID client driver as a normal function driver, and not as a "WDF HID minidriver" that runs beneath MsHidKmdf?

Well, in general, everything is easier with KMDF than without it. I would start with MsHidKmdf/ It's a very thin layer.

Both approaches are KMDF-based, so this is not what differs between them. I've already tried to pursue both approaches, but have then bumped into the following obstacles:

  • If developing a "regular" KMDF function driver on top of HidUsb, then PDO opening starts failing with STATUS_SHARING_VIOLATION after removing the WdfFdoInitSetFilter(DeviceInit) call. I've tried tweaking the ShareAccess field in multiple ways, but it doesn't seem to make any difference. Attempting to instead use the default I/O target leads to STATUS_PRIVILEGE_NOT_HELD.
  • If instead developing a WDF HID minidriver beneath MsHidKmdf, then the driver is auto-unloaded after EvtDriverDeviceAdd have completed without any errors from within my driver. Windows Device Manager does report a STATUS_DEVICE_DATA_ERROR that I guess originates from MsHidKmdf above in the device stack. By examining the hidusbfx2 & vhidmini samples it looks like WDF HID minidriver are expected to provide device- and report-descriptor tables for the HID device. This only makes sense for "virtual device" drivers that simulate a HID device, since these tables are otherwise delivered by the HID device and not by the driver. This makes me question if a WDF HID minidriver is really the right approach for my problem.

There are probably good reasons for the problems listed above, but the terse error messages and lack of sample code makes it challenging to figure out what is missing or wrong. It feels like I'm struggling in the dark here.

Small correction: I'm observing 0xc0000010 (STATUS_INVALID_DEVICE_REQUEST) for HID IOCTL requests after removing WdfFdoInitSetFilter(DeviceInit) to convert the KMDF driver into a function driver.

I am somewhat catching up on the thread. i don't think you want to be a hid miniport driver as your hardware is already HID, yes? you want to consume HID data and transform it into something else. Basically the same function as kbhdid for keyboards, but for batteries. So to be a HID mapper driver....

HID requires you open a file handle to send io. The default wdfiotarget does not open a file handle, so you can't use it. To make matters complicated, you are not allowed to open a handle until the ENTIRE stack has processed the pnp start irp, so you can't open your custom wdfiotarget in any of the init/power up callbacks. the most deterministic way to open the handle is to register for device interace arrival notifications (IoRegisterPlugPlayNotification) and open the handle from that callback. The question is which device interface to register notifications for

  1. if the HID device interface, you need to attempt to open each one and then query for the PDO or pdo name and compare it to your own stack's PDO
  2. register a custom device interface and register for notifications on it. If there is only 1 instance of your driver, it is deterministic and easy. if there can be > 1, you have the same problem as choice 1 as you need to determine the PDO.

Note that opening your custom device interface notification is the same as opening the HID device interface since it is the same stack // PDO you are opening, so you need to have the sharing and access writes set appropriately.

You will also need to configure your FDO to forward open/clean/close down the stack by calling WdfDeviceInitSetFileObjectConfig where WDF_FILEOBJECT_CONFIG.AutoForwardCleanupClose = WdfTrue and read the remarks section about what you need to do if you register your own EvtDeviceFileCreate callback, _WDF_FILEOBJECT_CONFIG (wdfdevice.h) - Windows drivers | Microsoft Learn

1 Like

OR, perhaps simpler

  1. Write a service of UMDF driver to consume the HID data.
  2. Create a battery miniport driver that accepts the HID data sent by #1.

Thanks a lot for giving clear advise on this matter Doron.

Correct. I'm trying to develop a driver that consumes HID data and transforms it into something else. Thank you for confirming that a HID miniport driver is then probably not the best approach.

This is the same limitation as for HID filter drivers, right? I think I'm already tackling that sub-problem by deferring PDO opening to a timer running at IRQ passive after EvtDeviceSelfManagedIoInit have completed. It might be that replacing the timer with IoRegisterPlugPlayNotification would be preferable by reducing the time window of the driver being partially initialized though(?) Or am I misunderstanding something?

I'm currently not performing any PDO searches, just retrieving the PDO name based on DevicePropertyPhysicalDeviceObjectName and passing the string to WdfIoTargetOpen with access=FILE_READ_ACCESS|FILE_WRITE_ACCESS and ShareAccess=FILE_SHARE_WRITE|FILE_SHARE_READ|FILE_SHARE_DELETE. This call then succeeds, but later IOCTL_HID_GET_FEATURE requests fail with STATUS_INVALID_DEVICE_REQUEST. Do you think adopting IoRegisterPlugPlayNotification would help on this?

Thanks a lot for making me aware of this requirement Doron. Is this the type of code I should add to EvtDriverDeviceAdd?

WDF_FILEOBJECT_CONFIG fileConfig{};
WDF_FILEOBJECT_CONFIG_INIT(&fileConfig, WDF_NO_EVENT_CALLBACK, WDF_NO_EVENT_CALLBACK, WDF_NO_EVENT_CALLBACK);
fileConfig.AutoForwardCleanupClose = WdfTrue; // forward open/clean/close down the stack

WdfDeviceInitSetFileObjectConfig(DeviceInit, &fileConfig, WDF_NO_OBJECT_ATTRIBUTES);

It unfortunately doesn't seem to make any difference in addressing the failing IOCTL_HID_GET_FEATURE. I've tried adding a custom EvtDeviceFileCreate callback with WdfRequestFormatRequestUsingCurrentType(Request); BOOLEAN res = WdfRequestSend(Request, WdfDeviceGetIoTarget(Device), NULL); if (res == FALSE) {..} logic for request forwarding, but it only makes matters worse by causing WdfIoTargetOpen of the PDO to fail.

The timer is problematic. You don't know when the start irp completes back to the pnp manager and thus allows creates. You would have to requeue the timer if you can't open it the first time (and deal with cleanup if the start fails or the device is immediately removed). In hindsight you may not want to forward the creates down the stack. that is what kbdhid does as it doesn't open a handle on its own, it relies on the handle opened to the stack by the input manager to start reading. Your situation is probably different.

Do any HID operations succeed other than IOCTL_HID_GET_FEATURE? have you tried stepping into the hidclass IOCTL handler and see what it is checking for?

Understood. I'll then update my driver to replace the timer with IoRegisterPlugPlayNotification-based PDO opening if it eliminates the hypothetical risk of sporadic failures. Having to open each added HID interface, querying for PDO and comparing PDO strings feels a bit clunky though, but I guess I'll have to live with it.

Nice to hear that I can probably skip that step.

I'm still able to query IOCTL_HID_GET_COLLECTION_INFORMATION and IOCTL_HID_GET_COLLECTION_DESCRIPTOR from the local I/O target. However, all IOCTL_HID_GET_FEATURE requests against the PDO started failing with STATUS_INVALID_DEVICE_REQUEST after converting my HID driver from a filter to a function driver by removing the WdfFdoInitSetFilter call.

I've seen you mention elsewhere on this forum a need for copying the IRP FileObject after formatting requests. I've already tried doing that, but it seems like IoGetCurrentIrpStackLocation(irp)->FileObject is NULL for the requests I create. Not sure what to do about that(?)

Interesting. I was not aware that this was a possibility. Are the hidclass sources and symbols available somewhere?

You need a file object for get feature. The iotarget you open in the interface notification callback will have a file open when you open the device interface. You have to delay the init logic which uses get feature until that point

Debugging: no source. You set a BP on the hidclass ioctl dispatch handler and start stepping through asm. But first start sending the request with a iotarget with a file object

Correction: I'm getting a FileObject, but the FsContext field becomes null after removing the WdfFdoInitSetFilter call to convert the filter driver into a function driver.

This is what I'm observing when opening the PDO directly from the IoRegisterPlugPlayNotification callback:

1: kd> !wdfiotarget 0x000047f4ef527b18
Treating handle as a KMDF handle!

WDFIOTARGET 000047f4ef527b18
=========================
!wdfdevice 0x000047f4f1483fd8
Target Device: !devobj  0xffffb80b0fd61de0
Target PDO: !devobj  0xffffb80b0f99d060

Type: Remote target
State:  WdfIoTargetStarted

Requests pending: 0

Requests sent: 0

Requests sent with ignore-target-state: 0


Target name:  \??\HID#VID_2341&PID_8037#6&257120a9&0&0000#{4d1e55b2-f16f-11cf-88cb-001111000030}
Target FileObject: dt nt!_FILE_OBJECT  0xffffb80b0f28ddd0
WDF file !handle  0xffffffff80000a38. Search for 'Object: xxxx Type: File', run '!fileobj xxxx'
Open type:  WdfIoTargetOpenByName

1: kd> dt nt!_FILE_OBJECT  0xffffb80b0f28ddd0
   +0x000 Type             : 0n5
   +0x002 Size             : 0n216
   +0x008 DeviceObject     : 0xffffb80b`0f99d060 _DEVICE_OBJECT
   +0x010 Vpb              : (null) 
   +0x018 FsContext        : (null) 
   +0x020 FsContext2       : (null) 
   +0x028 SectionObjectPointer : (null) 
   +0x030 PrivateCacheMap  : (null) 
   +0x038 FinalStatus      : 0n0
   +0x040 RelatedFileObject : (null) 
   +0x048 LockOperation    : 0 ''
   +0x049 DeletePending    : 0 ''
   +0x04a ReadAccess       : 0 ''
   +0x04b WriteAccess      : 0 ''
   +0x04c DeleteAccess     : 0 ''
   +0x04d SharedRead       : 0 ''
   +0x04e SharedWrite      : 0 ''
   +0x04f SharedDelete     : 0 ''
   +0x050 Flags            : 0x40000
   +0x058 FileName         : _UNICODE_STRING ""
   +0x068 CurrentByteOffset : _LARGE_INTEGER 0x0
   +0x070 Waiters          : 0
   +0x074 Busy             : 0
   +0x078 LastLock         : (null) 
   +0x080 Lock             : _KEVENT
   +0x098 Event            : _KEVENT
   +0x0b0 CompletionContext : (null) 
   +0x0b8 IrpListLock      : 0
   +0x0c0 IrpList          : _LIST_ENTRY [ 0xffffb80b`0f28de90 - 0xffffb80b`0f28de90 ]
   +0x0d0 FileObjectExtension : (null) 

sorry for the indecision about how to handle creates. this is where you need to end up:

  1. when you open the HID device the create arrives in your driver first since you are attached to the HID.
  2. you need to forward that create down the stack so that HIDclass clan process it and initialize its file object state
  3. the cleanup and close need to be forwarded to hidclass as well for the teardown of the file objec state
  4. I don't remember if hidclass uses FsContext or FsContext2 for its state stored in the FO, you need to configure WDF to store its FO state with the other field with WDF_FILEOBJECT_CLASS so it doesn't conflict with hidclass

so you need to configure WDF like i described previously plus take into account point 4

1 Like

Thanks for very helpful advise, Doron. The HID client drivers docs state that the FsContext field is reserved for the HID class driver, and other drivers in the stack should therefore instead use FsContext2. I've therefore updated my driver with the following code:

// configure handling of device create, close & cleanup requests
WDF_FILEOBJECT_CONFIG fileConfig{};
WDF_FILEOBJECT_CONFIG_INIT(&fileConfig, WDF_NO_EVENT_CALLBACK, WDF_NO_EVENT_CALLBACK, WDF_NO_EVENT_CALLBACK); // keep default callbacks
fileConfig.AutoForwardCleanupClose = WdfTrue; // forward requests down the stack
fileConfig.FileObjectClass = WdfFileObjectWdfCanUseFsContext2; // cannot use FsContext, since it's reserved by HIDclass

WdfDeviceInitSetFileObjectConfig(DeviceInit, &fileConfig, WDF_NO_OBJECT_ATTRIBUTES);

I'm now observing that both the FsContext and FsContext2 fields are set when opening the PDO - which is great! The bad news is that IOCTL_HID_GET_FEATURE requests are still failing with STATUS_INVALID_DEVICE_REQUEST...