WebUSB

Pair with and drive a USB device — custom hardware, a dev board, an instrument — straight from C# via IUsb (the WebUSB API): show its descriptor, open it, claim an interface, and run bulk / interrupt / control transfers. WASM-only: requestDevice() needs a live user gesture and the live device handle, and it's Chromium-family only at the time of writing.

RequestDeviceAsync shows the browser device chooser and returns an IUsbDevice (null if the user dismisses it). Transfer payloads cross as byte[]; dispose the device to release it. Actual transfers are device-specific, so the demo shows discovery + lifecycle. Gate on IsSupportedAsync.

UsbDemo.cs

using Rask.Site;
using Rask.Wasm.Browser;

namespace Rask.Site.Features;

/// <summary>
///     <see cref="IUsb" /> — pair with a USB device from a gesture and read its descriptor (vendor / product /
///     manufacturer / serial), then open and release it. WASM-only: requestDevice() needs a live user gesture
///     and the live device handle, and it's Chromium-family only at the time of writing. Actual data transfer
///     (claim an interface, transferIn/Out) is device-specific, so this demo shows discovery + lifecycle.
/// </summary>
public sealed partial class UsbDemo(IUsb usb) : Component, IAsyncDisposable
{
    private IUsbDevice? _device;
    private UsbDeviceInfo? _info;
    private bool _open;
    private string _status = "(idle)";

    protected override Component? Render() =>
        Div.Class($"{Tw.Card} shadow-sm border-0")[
            Div.Class(Tw.CardBody)[
                Div.Class("flex gap-2 flex-wrap mb-2")[
                    Button.Class(Tw.BtnPrimary).Id("usb-request").OnClickAsync(RequestDevice)[
                        UiIcon.Name(UiIconName.Cube).Class("me-1"), "Pair device"],
                    Button
                        .Class(Tw.BtnOutlinePrimary)
                        .Id("usb-open")
                        .Disabled(_device is null || _open)
                        .OnClickAsync(Open)["Open"],
                    Button
                        .Class(Tw.BtnOutlineDanger)
                        .Id("usb-close")
                        .Disabled(_device is null)
                        .OnClickAsync(Release)["Release"]
                ],
                _info is null
                    ? Div.Class("text-sm text-ui-muted")["No device paired."]
                    : Dl.Class("grid grid-cols-12 gap-4 text-sm mb-2").Id("usb-info")[
                        Dt.Class("col-span-5 sm:col-span-4 text-ui-muted")["Vendor ID"],
                        Dd.Class("col-span-7 sm:col-span-8")[Code[Hex(_info.VendorId)]],
                        Dt.Class("col-span-5 sm:col-span-4 text-ui-muted")["Product ID"],
                        Dd.Class("col-span-7 sm:col-span-8")[Code[Hex(_info.ProductId)]],
                        Dt.Class("col-span-5 sm:col-span-4 text-ui-muted")["Manufacturer"],
                        Dd.Class("col-span-7 sm:col-span-8")[_info.ManufacturerName ?? "—"],
                        Dt.Class("col-span-5 sm:col-span-4 text-ui-muted")["Product"],
                        Dd.Class("col-span-7 sm:col-span-8")[_info.ProductName ?? "—"],
                        Dt.Class("col-span-5 sm:col-span-4 text-ui-muted")["Serial"],
                        Dd.Class("col-span-7 sm:col-span-8")[_info.SerialNumber ?? "—"]
                    ],
                Div.Class("text-sm text-ui-muted")["Status: ", Code.Id("usb-status")[_status]]
            ]
        ];

    private static string Hex(int value) => "0x" + value.ToString("x4");

    private async Task RequestDevice()
    {
        try
        {
            if (!await usb.IsSupportedAsync())
            {
                _status = "WebUSB not supported in this browser (Chromium-family only)";
                return;
            }

            await CloseInternal();
            _device = await usb.RequestDeviceAsync(onDisconnect: OnDisconnect); // no filters → offer all devices
            _info = _device?.Info;
            _status = _device is null ? "No device selected" : "Paired";
        }
        catch (Exception ex)
        {
            _status = "Failed: " + ex.Message;
        }
    }

    private async Task Open()
    {
        if (_device is null)
        {
            return;
        }

        try
        {
            await _device.OpenAsync();
            _open = true;
            _status = "Opened — ready for device-specific transfers";
        }
        catch (Exception ex)
        {
            _status = "Open failed: " + ex.Message;
        }
    }

    private Task OnDisconnect()
    {
        // The device was unplugged — the framework already evicted it; just reset the UI.
        _device = null;
        _info = null;
        _open = false;
        _status = "Device disconnected";
        StateHasChanged();
        return Task.CompletedTask;
    }

    private async Task Release()
    {
        await CloseInternal();
        _status = "Released — device closed";
    }

    private async Task CloseInternal()
    {
        if (_device is not null)
        {
            await _device.DisposeAsync();
            _device = null;
            _info = null;
            _open = false;
        }
    }

    public async ValueTask DisposeAsync() => await CloseInternal();
}
Live result
No device paired.
Status: (idle)