Hid
Talk to a human-interface device the operating system's drivers do not fully expose - a macro pad, a flight yoke, a vendor-defined control surface.
@inject Bit.Butil.Hid hidMDN reference
RequestDevice must run inside a user gesture.
True when the runtime exposes navigator.hid. During prerender/SSR the check returns false rather than throwing, so defer it to OnAfterRenderAsync.
@inject Bit.Butil.Hid hid
var supported = await hid.IsSupported();RequestDevice returns an array, because one physical device can present as several collections and the chooser can hand back more than one. GetDevices returns what this origin was already granted, without a prompt.
private HidDevice? _device;
var devices = await hid.RequestDevice(new HidDeviceFilter { VendorId = 0x054c });
_device = devices.FirstOrDefault();
var granted = await hid.GetDevices();A device has to be opened before any report crosses. GetInfo re-reads its collections - the report ids it will accept, which is what a protocol implementation needs first.
{
// The handle from RequestDevice above - a grant is per device, and this is the way back to one
// the user has already chosen.
private HidDevice? _device;
private async Task Inspect()
{
await _device!.Open();
var info = await _device.GetInfo();
foreach (var collection in info!.Collections)
{
// collection.UsagePage, collection.Usage
// collection.InputReports / OutputReports / FeatureReports
}
}
}The device's unprompted events: buttons, axes, whatever its report descriptor declares. Open the device first - a closed device sends nothing.
IAsyncDisposable
{
private HidDevice? _device; // from RequestDevice, and opened
private ButilSubscription? _reports;
private async Task Listen() =>
_reports = await _device!.SubscribeInputReports(report =>
{
// report.ReportId, report.Data
});
public async ValueTask DisposeAsync()
{
if (_reports is not null) await _reports.DisposeAsync();
}
}Output reports drive the device - an LED, a rumble motor, a display. Feature reports are read and written on demand and carry configuration rather than events.
{
private HidDevice? _device; // from RequestDevice, and opened
private async Task Send()
{
await _device!.SendReport(reportId: 0x01, [0x00, 0xff]);
await _device.SendFeatureReport(reportId: 0x02, [0x01]);
var feature = await _device.ReceiveFeatureReport(reportId: 0x02);
}
}Watches devices appearing and disappearing. Only devices this origin already has permission for raise these.
await using var watch = await hid.SubscribeConnection(
onConnected: device => Console.WriteLine($"+ {device.ProductName}"),
onDisconnected: device => Console.WriteLine($"- {device.ProductName}"));Drops this origin's permission for the device, so it stops appearing in GetDevices until the user picks it again.
{
private HidDevice? _device; // from RequestDevice
// Drops the grant, so GetDevices stops returning it and the picker has to be shown again.
private async Task Revoke()
{
var revoked = await _device!.Forget();
_device = null;
}
}API reference
ValueTask<bool> IsSupported()ValueTask<HidDevice[]> RequestDevice(params HidDeviceFilter[] filters)ValueTask<HidDevice[]> GetDevices()ValueTask<ButilSubscription> SubscribeConnection(Action<HidDevice>? onConnected = null, Action<HidDevice>? onDisconnected = null)ValueTask DisposeAsync()void InvokeHidInputReport(Guid id, HidInputReport report)void InvokeHidConnected(Guid id, HidDeviceInfo info)void InvokeHidDisconnected(Guid id, HidDeviceInfo info)HidDeviceInfo Info { get; } / string? ProductName { get; }ValueTask<bool> Open() / ValueTask Close() / ValueTask<bool> IsOpened()ValueTask<HidDeviceInfo?> GetInfo()ValueTask<bool> SendReport(byte reportId, byte[] data)ValueTask<bool> SendFeatureReport(byte reportId, byte[] data) / ValueTask<byte[]?> ReceiveFeatureReport(byte reportId)ValueTask<ButilSubscription> SubscribeInputReports(Action<HidInputReport> handler)ValueTask<bool> Forget()ValueTask DisposeAsync()class HidDeviceFilter { ushort? VendorId; ushort? ProductId; ushort? UsagePage; ushort? Usage; }class HidDeviceInfo { string Id; ushort VendorId; ushort ProductId; string? ProductName; bool Opened; HidCollectionInfo[] Collections; }class HidCollectionInfo { ushort UsagePage; ushort Usage; HidReportInfo[] InputReports; HidReportInfo[] OutputReports; HidReportInfo[] FeatureReports; }class HidInputReport { byte ReportId; byte[] Data; }