Usb
Pick a USB device, claim one of its interfaces, and run control, bulk or interrupt transfers against it from C#.
@inject Bit.Butil.Usb usbMDN reference
RequestDevice must run inside a user gesture.
True when the runtime exposes navigator.usb. During prerender/SSR the check returns false rather than throwing, so defer it to OnAfterRenderAsync.
@inject Bit.Butil.Usb usb
var supported = await usb.IsSupported();RequestDevice opens the browser's chooser, optionally narrowed by vendor and product id. GetDevices returns what this origin was already granted, without a prompt.
private UsbDevice? _device;
_device = await usb.RequestDevice(); // every device the browser will show
_device = await usb.RequestDevice(new UsbDeviceFilter { VendorId = 0x2341 });
var granted = await usb.GetDevices();Open, configure, claim
Open / SelectConfiguration / ClaimInterface / SelectAlternateInterface / ReleaseInterface / Close / IsOpened / GetInfoThe order is fixed: open, select a configuration, claim an interface, and only then transfer. Skipping a step fails with an InvalidStateError rather than a hint about which step was missed.
{
// The handle from RequestDevice above - a grant is per device, and this is the way back to one
// the user has already chosen.
private UsbDevice? _device;
private async Task Session()
{
await _device!.Open();
await _device.SelectConfiguration(1);
// Nothing can be transferred until the interface is claimed, and the OS may already hold it
// for a driver of its own.
await _device.ClaimInterface(0);
// ... transfers ...
await _device.ReleaseInterface(0);
await _device.Close();
}
}Control transfers carry a setup packet and address the device itself; bulk and interrupt transfers address an endpoint number taken from the claimed interface's descriptor. A 'stall' status means the endpoint halted - ClearHalt is the way out.
{
private UsbDevice? _device; // opened, with its interface claimed
private async Task Transfer()
{
var read = await _device!.TransferIn(endpointNumber: 1, length: 64);
var text = read?.Data is null ? "" : System.Text.Encoding.UTF8.GetString(read.Data);
await _device.TransferOut(endpointNumber: 2, "ping"u8.ToArray());
await _device.ControlTransferOut(new UsbControlTransferParameters
{
RequestType = "vendor",
Recipient = "device",
Request = 0x01,
Value = 0x0001,
Index = 0
});
}
}Watches devices appearing and disappearing. Only devices this origin already has permission for raise these - plugging in a stranger's device is not something the page is told about.
await using var watch = await usb.SubscribeConnection(
onConnected: device => Console.WriteLine($"+ {device.Info.ProductName}"),
onDisconnected: device => Console.WriteLine($"- {device.Info.ProductName}"));Drops this origin's permission for the device, so it stops appearing in GetDevices until the user picks it again.
{
private UsbDevice? _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<UsbDevice?> RequestDevice(params UsbDeviceFilter[] filters)ValueTask<UsbDevice[]> GetDevices()ValueTask<ButilSubscription> SubscribeConnection(Action<UsbDevice>? onConnected = null, Action<UsbDevice>? onDisconnected = null)ValueTask DisposeAsync()void InvokeUsbConnected(Guid id, UsbDeviceInfo info)void InvokeUsbDisconnected(Guid id, UsbDeviceInfo info)UsbDeviceInfo Info { get; }ValueTask<bool> Open() / ValueTask Close() / ValueTask<bool> IsOpened()ValueTask<UsbDeviceInfo?> GetInfo()ValueTask<bool> SelectConfiguration(byte configurationValue)ValueTask<bool> ClaimInterface(byte interfaceNumber) / ValueTask<bool> ReleaseInterface(byte interfaceNumber)ValueTask<bool> SelectAlternateInterface(byte interfaceNumber, byte alternateSetting)ValueTask<UsbTransferResult?> ControlTransferIn(UsbControlTransferParameters parameters, ushort length) / ControlTransferOut(UsbControlTransferParameters parameters, byte[]? data = null)ValueTask<UsbTransferResult?> TransferIn(byte endpointNumber, uint length) / TransferOut(byte endpointNumber, byte[] data)ValueTask<bool> ClearHalt(string direction, byte endpointNumber)ValueTask<bool> Reset()ValueTask<bool> Forget()ValueTask DisposeAsync()class UsbDeviceFilter { ushort? VendorId; ushort? ProductId; byte? ClassCode; byte? SubclassCode; byte? ProtocolCode; string? SerialNumber; }class UsbDeviceInfo { string Id; ushort VendorId; ushort ProductId; byte DeviceClass; byte DeviceSubclass; byte DeviceProtocol; string? ManufacturerName; string? ProductName; string? SerialNumber; bool Opened; byte? ConfigurationValue; UsbConfigurationInfo[] Configurations; }class UsbConfigurationInfo { ... UsbInterfaceInfo[] Interfaces; } etc.class UsbControlTransferParameters { string RequestType; string Recipient; byte Request; ushort Value; ushort Index; }class UsbTransferResult { string Status; uint BytesWritten; byte[]? Data; }