SkillAgentSearch skills...

HIDDevices

Cross-platform .NET Standard library for asynchronous controller input reading (e.g. gamepads, joysticks, etc.)

Install / Use

/learn @DevDecoder/HIDDevices
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

GitHub tag License

Publish Nuget<br/> Publish Nuget

Description

This library provides a cross-platform service for asynchronously accessing HID devices, such as Gamepads, Joysticks, Multi-axis controllers, and programmable button pads. It supports Plug & Play, correctly identifying when controllers are added and removed, and Reactive frameworks. It also allows the creation of custom Controller implementations, which are matched automatically against devices for easy use.

Since Version 4, you can now install the HIDDevices.Usages NuGet entirely independently, providing up-to-date Usage Pages, generated directly from the the USB HID Usage Tables. In the same way, you do not need to include the HIDDevices.Usages NuGet to make use of the HIDDevices NuGet.

Important Notes

  • The project is based on HIDSharp but deliberately does not expose any of its API explicitly, as I may replace it in a future version.
  • Although the project isn't active, I refresh the usage tables on occasion and respond to issues if you raise them in the issue tracker.
  • As the Usages and UsagePages are auto-generated they can change between versions whenever the USB HID Usage Tables are updated. This can cause breaking changes in your code, so be careful when updating versions of the NuGet. In particular, any time the HID tables are updated, I will update the minor version number (at least). Notable changes include from 2.0-2.1 when many of the 1-indexed usages were changed to 0-indexed, e.g. ButtonPage.Button1 became ButtonPage.Button0; and when moving from version 2 to version 3, when I changed the code generation algorithm and the source of truth was changed to directly reference the published PDF specification (see #6).
  • The XInput-compatible HID device driver only transmits events from the HID device whilst the current process has a focussed window, so console applications/background services don't appear to work! This is not a bug in this library, although I have been unable to find where this 'feature' is documented. It affects the "Microsoft XBox One for Windows Controller".
  • When changing from version 3.x to version 4.0, the Usages and UsagePages were split into their own NuGet, see "Migrating to Version 4" for the justification and an explanation of breaking changes.

Installation

The library is available via NuGet and is delivered via NuGet Package Manager:

Install-Package HIDDevices

If you are targeting .NET Core, use the following command:

dotnet add package HIDDevices

Installing the HIDDevices.Usages NuGet is also highly recommended.

Usage

The sample program demonstrates using the library in various scenarios.

The HID Tables

The library accepts usage identifiers as a raw uint, however that is prone to error. As such, it was designed to work alongside the HIDDevices.Usages Nuget which encodes the USB HID Usage Tables into enumerations and types that can be implicitly converted to the raw uint identifier. These types encode a lot of information and can add ~400kb to your deployed code size, so may not be suitable for all projects. Using the raw uint identifier is always an option, especially when using with a non-standard device.

Devices

Initialisation

To start monitoring controllers, add the following code:

using var devices = new Devices();

Note This instantiates a new instance of the Devices class, which immediately starts listening for new HID devices. In practice, you should only ever create one of these. The Devices class implements IDisposable for asynchronous disposal, which cleans up all listeners.

Alternatively, the library is fully compatible with Dependency injection frameworks. Register the service as a Singleton (so only one instance is created) using code similar to:

services.AddSingleton<Devices>();
...
var devices = serviceProvider.GetService<Devices>();

Modern DI frameworks should correctly handle instantiation and disposal automatically and supply a logger if registered.

Logging

The Devices constructor accepts an ILogger<Devices> for logging, most often supplied via dependency injection, but you can find an example of a simple logger in the samples - SimpleConsoleLogger.

Detecting changes in devices

The Devices service implements an IObservableCache<Device, string> property, which publishes add/update/remove events for devices. For more information on IObservableCache<,> and how to consume them, see DynamicData. e.g.

using var subscription = devices.Connect().Subscribe(changeSet => { ... });

The standard Connect() method retrieves an observable collection of all devices but does not attempt to connect to them, which is useful when you only want to see what is known to the Operating System. However, you can also use the Connected() extension method, which does attempt to establish a working connection to the devices, and only includes devices that are currently connected (whilst they remain connected). It is a subset of the observable collection returned by Connect().

A disconnected device is connected to the system, but the library can not establish a connection to it. For example, Windows prevents access to Keyboard and Mouse devices, but they are still listed. Devices that are physically disconnected (and hence not seen by the Operating System) are absent from both collections.

Both methods accept a predicate that you can use to efficiently filter devices only to include those you are interested in, for example:

using var subscription = 
    devices.Connected(
      device => device.UsagesAll(GenericDesktopPage.GamePad &&
                device.ControlUsagesAll(GenericDesktopPage.X, GenericDesktopPage.Y))
    .Subscribe()

This uses the UsagesAll extension method to filter devices that don't implement the GamePad usage and the ControlUsagesAll extension method to only select devices that have controls that implement all the specified usages (i.e. must have an X and Y axis - which, according to the HID specs, all GamePads are supposed to expose, but there's no guarantee). There are also UsagesAny and ControlUsagesAny extension methods and DeviceUsages* and ControlUsages* extension methods that you can apply to Devices directly (and are equivalent to calling Connect(...) with the appropriate delegate).

Supplying a delegate to filter the Connected() extension method is recommended to prevent unnecessary connection attempts to devices you are not interested in.

Connecting to a device

Each Device class implements IObservable<IList<ControlChange>>, which you can use to observe changes in control values. A connection to the device is only established when there is at least one subscriber to this interface. There is also an IObservable<bool> ConnectionState property that changes value when the device connects/disconnects; subscribing to the ConnectionState will also ensure a subscription to the main observable - attempting a connection. To see the current connection state, you can use the IsConnected property, which returns the instantaneous value but doesn't attempt a connection. Using the Connected() extension method on the Devices collection will also ensure a subscription and connection attempt.

Detecting changes in controls

As mentioned above, the Device object implements IObservable<IList<ControlChange>>, which returns batches of changes reported by a device when subscribed to. Device also implements IReadOnlyDictionary<Control, ControlChange>, which you can use to find the last observed state of the Device's controls - however this doesn't establish a connection itself, so you should first connect by subscribing to the Device. A control's value is always mapped to a value between 0 and 1, or double.NaN to indicate null. You can look for control changes across all devices using the ControlChanges extension method for convenience. e.g.

using var subscription2 = devices
    // Watch for control changes only
    .ControlChanges()
    .Subscribe(changes =>
    {
        ...
    });

A ControlChange indicates the changed Control, the PreviousValue and the new Value. It also shows how stale the change is by having Timestamp and the associated Elapsed properties.

Note: HID devices are not required to

View on GitHub
GitHub Stars48
CategoryDevelopment
Updated2mo ago
Forks11

Languages

C#

Security Score

80/100

Audited on Jan 7, 2026

No findings