English | 简体中文
A cross-platform USB communication library for FirmwareKit. It provides a unified USB abstraction over native platform backends (Windows / Linux / macOS / HarmonyOS) and LibUsbDotNet, with device discovery, filtering, session management, and structured transfer diagnostics. Transport primitives only — protocol layers (Sahara, Firehose, Fastboot, and so on) are out of scope and are implemented by callers on top of the unified session interfaces.
- Unified session API: synchronous (
IUsbDeviceSession) and asynchronous (IAsyncUsbDeviceSession) read/write/control-transfer over one abstraction, with per-direction serialization for full-duplex protocol threads. - Four native backends + libusb: Windows WinUSB (and legacy driver), Linux usbfs, macOS IOUSBHost.framework, HarmonyOS USBManager DDK, and LibUsbDotNet.
- Device discovery & filtering: by
VendorId,ProductId,SerialNumber,DevicePath, interface class/subclass/protocol, interface number(s), and explicit endpoint addresses. - Packet semantics:
ReadPacket/ReadPacketAsyncreturn aUsbReadResultthat distinguishes a short packet (USB message boundary) from a timeout — what fastboot/EDL/bootrom framing needs.ReadExact/ReadExactAsyncread a fixed length within a total deadline. - Zero-length packet (ZLP) control:
WriteZlp/WriteZlpAsyncterminate transfers whose payload is an exact multiple of the endpoint max packet size. - Progress reporting:
ReadPacketAsync/WriteAsyncoverloads acceptIProgress<long>and report cumulative bytes after each chunk (flashing large images). - Safety guardrails: a read-length cap (
UsbTransferPolicies.MaxReadLength) prevents OOM from untrusted protocol lengths; session disposal is idempotent;ReadInto(Span<byte>)is available on net8.0+. - Observable enumeration diagnostics:
UsbCommunicationLayer.GetEnumerationDiagnostics()and per-finder counters distinguish "mechanism did not run" from "ran but found no devices" on device-less CI. - Mode-switch workflows:
WaitForUsbDeviceAppearAsync/WaitForUsbDeviceDisappearAsync/WaitForUsbDeviceModeSwitchAsync, plus re-opening byDeviceKeyviaOpenDeviceSessionByKey. - Hot-plug monitoring: event-driven where available (
WM_DEVICECHANGEon Windows native, libusb hotplug), polling fallback otherwise;Added/Removed/Changedchange events. - Backend capability model:
UsbApiCapabilities(and per-backendUsbBackendCapability) exposes async support, hot-plug support, external-runtime requirements, andResetReenumeratesDevice. - Diagnostics:
UsbTracestructured transfer events (TransferObserved) and opt-in frame capture;LogFormatteddefers string interpolation until logging is enabled. - Extensibility: register custom providers via
RegisterUsbApi/UsbApiRegistry. - Built-in CLI:
FirmwareKit.Comm.CLIwithapis,devices,all-devices,monitor,selftest,io-test, andinterrupt-testcommands.
FirmwareKit.Comm focuses on cross-platform USB transport primitives:
- Device discovery and filtering
- Session management (open / read / write / control / interrupt / reset)
- Unified read/write with timeout control
- Transport-level reset
Discovery prefers metadata-first paths so simple enumeration does not require long-lived read/write sessions. Actual payload I/O starts after calling OpenUsbDeviceSessions.
Backend matrix:
| Backend | Platform | Transport | Notes |
|---|---|---|---|
native (WinUSB) |
Windows | WinUSB API | Overlapped (true async) I/O; requires a WinUSB-bound interface (Zadig etc.) |
native (legacy) |
Windows | DeviceIoControl | Fallback for legacy USB drivers; no ZLP, no native async |
native (usbfs) |
Linux | usbfs ioctl / URB | True async via URB + poll; multi-interface claim supported |
native (IOUSBLib) |
macOS | IOUSBHost.framework | Requires macOS 10.15+; pipe-level reset |
native (HarmonyOS) |
HarmonyOS | USBManager DDK | Opt-in via FIRMWAREKIT_USB_ENABLE_HARMONY=1; requires OH_Usb_Init() to succeed |
libusb |
all | LibUsbDotNet | Native async transfers; needs the native libusb runtime (bundled per-RID in the package); degrades gracefully when absent |
HarmonyOS is hidden from GetAvailableApis() by default because it cannot be detected reliably with file probes. On macOS below 10.15, use the libusb backend.
- The timeout passed to
Read/Write(and theirReadIntovariants) applies per chunk, not to the whole operation: a transfer larger than the backend chunk size is split into multiple chunks, so the total time can reachchunkCount × timeoutMs. - When an exact number of bytes must be read within a total budget (e.g. fixed-size fastboot/EDL responses), use
ReadExact(length, timeoutMs)/ReadExactAsync— they loop over short reads with a total deadline and return the bytes actually received on timeout. - Short reads/writes stop the transfer and return the partial byte count; a disconnected device throws
UsbDeviceDisconnectedException(distinct from ordinaryIOException/UsbTransferException).
Default timeouts differ per backend. Omit timeoutMs only when the default is acceptable for your use case:
| Backend | Default timeout |
|---|---|
| Windows WinUSB | 60 000 ms |
| Windows legacy / Linux usbfs / libusb / macOS / HarmonyOS | 5 000 ms |
Pass an explicit timeoutMs (or UsbTransferPolicies.InfiniteTimeoutMs = -1 for an unbounded wait) when the operation must not depend on the backend default.
The async extension overloads that omit the timeout (ReadAsync(length), ReadIntoAsync(...), ReadPacketAsync(...), WriteAsync(...), WriteZlpAsync(), ControlTransferAsync(...), ReadInterruptAsync(...), WriteInterruptAsync(...)) use the session's DefaultTimeoutMs.
On the Linux usbfs backend, interrupt-endpoint reads/writes (ReadInterrupt/WriteInterrupt) wait on poll() from a thread-pool thread; with InfiniteTimeoutMs and an unresponsive device the waiting thread is held until the device responds or disconnects.
Retries for recoverable errors are configurable process-wide via UsbTransferPolicies.DefaultRetryPolicy (a UsbTransferRetryPolicy with MaxRetries and RetryDelayMs).
Bulk transfers whose payload length is an exact multiple of the endpoint's max packet size (typically 512 or 1024 bytes) must be terminated with a zero-length packet so the device knows the transfer ended. This matters for protocol downloads (adb push, fastboot download:, EDL firehose) and for bootrom loaders.
- Check whether a ZLP is needed:
payloadLength % maxPacketSize == 0(readMaxPacketSizefrom the device'sInterfaces[i].Endpointsmetadata). - After such a write, call
session.WriteZlp(timeoutMs)(orWriteZlpAsync) to send the terminating zero-length packet. - Backends that cannot perform an explicit ZLP write (legacy Windows drivers) throw
NotSupportedException; checkUsbApiCapabilities/ backend capabilities first.
// Example: writing a 512 KiB block to a device whose OUT max packet size is 512.
long written = session.Write(block, 0, block.Length, timeoutMs);
if (block.Length % 512 == 0)
session.WriteZlp(timeoutMs);IUsbDeviceSession.Reset() semantics differ per backend. Query UsbApiCapabilities.ResetReenumeratesDevice (or UsbBackendCapability.ResetReenumeratesDevice for the concrete backend) before relying on the session after a reset:
| Backend | Reset effect | Session after Reset | ResetReenumeratesDevice |
|---|---|---|---|
| Windows WinUSB | WinUsb_ResetPipe (pipe-level) |
still usable | false |
| Windows legacy | no-op | still usable | false |
| macOS IOUSBHost | pipe abort + clear stall | still usable | false |
| Linux usbfs | USBDEVFS_RESET (device reset) |
invalid — re-enumerate and re-open | true |
| libusb | libusb_reset_device (device reset) |
invalid — re-enumerate and re-open | true |
| HarmonyOS DDK | re-init DDK session + re-claim | invalid — re-open | true |
For device-level resets, use WaitForUsbDeviceDisappearAsync / WaitForUsbDeviceAppearAsync (or WaitForUsbDeviceModeSwitchAsync) and then OpenUsbDeviceSession / OpenUsbDeviceSessionByKey to obtain a fresh session.
True asynchronous (non-blocking) I/O is implemented natively by WinUSB (overlapped I/O), Linux usbfs (URB + poll) and libusb (LibUsbDotNet async transfers). All other backends — macOS IOUSBHost, HarmonyOS DDK, and the AsAsync() adapter — execute the underlying synchronous transfer on a thread-pool thread (UsbAsyncExecution.Run).
Check UsbApiCapabilities.SupportsNativeAsyncIo (or UsbBackendCapability.SupportsNativeAsyncIo per backend) to know whether ReadAsync/WriteAsync are truly non-blocking or just offloaded. Protocol layers that must not block the caller's thread should treat SupportsNativeAsyncIo == false backends as synchronous-with-offload.
When no device is attached (hosted CI runners), an empty enumeration can mean either "no devices" or "the enumeration mechanism failed". Each finder exposes observability state, and UsbCommunicationLayer.GetEnumerationDiagnostics() summarizes it in a key=value; ... string:
| Backend | Mechanism diagnostic | Scan proof | Counters |
|---|---|---|---|
| Linux usbfs | LastUsbfsRootExists (usbfs mounted?) |
LastScannedNodes |
LastMatchedDeviceCount, PermissionDeniedCount, BusyCount |
| Windows | LastSetupDiSucceeded (SetupDi handle opened?) |
LastScannedNodeCount |
LastMatchedDeviceCount |
| macOS | LastCopyDevicesSucceeded (IOUSBLib copy returned?) |
LastScannedDeviceCount |
LastMatchedDeviceCount |
| libusb | IsRuntimeAvailable(out reason) |
device list | device list |
The CLI prints [enum-diagnostics] <summary> on every all-devices run; CI asserts on it instead of only checking "exit 0". The library also ships unit tests that feed constructed USB descriptor bytes into the pure parser (LinuxUsbFinder.TryParseDescriptor) to verify enumeration correctness without hardware.
MonitorUsbDevices prefers event-driven notifications where available: WM_DEVICECHANGE (Windows native backend) and libusb hotplug (Linux/macOS, UsbApiKind.LibUsbDotNet); unsupported platforms fall back to polling (pollInterval, default 1 s). Change events include Added, Removed and Changed (metadata changed while keeping the same physical identity). Pass a CancellationToken to auto-dispose the monitor handle, or use the WaitForUsbDeviceAppearAsync / WaitForUsbDeviceDisappearAsync / WaitForUsbDeviceModeSwitchAsync helpers for mode-switch workflows.
Install via NuGet:
dotnet add package FirmwareKit.CommThe package targets net10.0, net8.0 and netstandard2.0, and bundles the native libusb runtime for each supported RID (win-x64 / win-arm64 / osx-x64 / osx-arm64 / linux-x64 / linux-arm64 / linux-riscv64 / linux-loong64). Requires the .NET 10 SDK to build the solution (.slnx).
Use the FirmwareKitComm facade to enumerate APIs and devices:
using FirmwareKit.Comm;
using FirmwareKit.Comm.Abstractions;
var comm = new FirmwareKitComm();
// List registered USB APIs
foreach (var api in comm.GetAvailableUsbApis())
Console.WriteLine(api);
// Print backend capability summary
foreach (var capability in comm.GetAvailableUsbApiCapabilities())
{
Console.WriteLine($"api={capability.ApiName} nativeDiscovery={capability.SupportsNativeDiscovery} nativeAsync={capability.SupportsNativeAsyncIo} hotplug={capability.SupportsNativeHotPlugMonitoring} externalRuntime={capability.RequiresExternalRuntime}");
}
// Sync device enumeration with VendorId filter (example: 0x18D1)
var devices = comm.EnumerateUsbDevices(UsbApiKind.Auto, new UsbDeviceFilter { VendorId = 0x18D1 });
foreach (var d in devices)
{
var ifClass = d.InterfaceClass.HasValue ? $"0x{d.InterfaceClass.Value:X2}" : "--";
var ifSubClass = d.InterfaceSubClass.HasValue ? $"0x{d.InterfaceSubClass.Value:X2}" : "--";
var ifProto = d.InterfaceProtocol.HasValue ? $"0x{d.InterfaceProtocol.Value:X2}" : "--";
Console.WriteLine($"api={d.ApiName} vid=0x{d.VendorId:X4} pid=0x{d.ProductId:X4} if={ifClass}/{ifSubClass}/{ifProto} serial={d.SerialNumber ?? "<null>"} path={d.DevicePath}");
}
// Optional: filter by USB interface class (for example Qualcomm EDL often uses 0xFF/0xFF/0xFF)
var edlLikeDevices = comm.EnumerateUsbDevices(UsbApiKind.Auto, new UsbDeviceFilter
{
VendorId = 0x05C6,
InterfaceClass = 0xFF,
InterfaceSubClass = 0xFF,
InterfaceProtocol = 0xFF
});
// Multi-interface claim (e.g. CDC-ACM control + data): every listed interface must exist
var multiIf = comm.EnumerateUsbDevices(UsbApiKind.Auto, new UsbDeviceFilter
{
InterfaceNumber = 0, // primary data interface
InterfaceNumbers = new byte[] { 1 } // additional interface to claim (Linux/libusb)
});
// Async enumeration
var asyncDevices = await comm.EnumerateUsbDevicesAsync(UsbApiKind.LibUsbDotNet);
// Open sessions and do unified read/write (protocol parsing is caller-defined)
using var sessions = comm.OpenUsbDeviceSessions(UsbApiKind.Auto, new UsbDeviceFilter
{
VendorId = 0x05C6,
ProductId = 0x9008,
InterfaceClass = 0xFF,
InterfaceSubClass = 0xFF,
InterfaceProtocol = 0xFF
});
var session = sessions.Sessions.FirstOrDefault();
if (session != null)
{
// Example only: command/protocol payload is app-specific
_ = session.Write(new byte[] { 0x7E, 0x00 }, 2, 3000);
var response = session.Read(512, 3000);
Console.WriteLine($"response bytes: {response.Length}");
// Control transfer example: read current alternate setting
var setup = new UsbSetupPacket
{
RequestType = 0x81,
Request = 0x0A,
Value = 0,
Index = 0,
Length = 1
};
var ctrlBuffer = new byte[1];
var ctrlCount = session.ControlTransfer(setup, ctrlBuffer, 0, ctrlBuffer.Length, 3000);
Console.WriteLine($"control bytes: {ctrlCount}, alt={ctrlBuffer[0]}");
// Async session (if backend does not implement async natively, use AsAsync())
var asyncSession = session.AsAsync();
var asyncResponse = await asyncSession.ReadAsync(512, 3000);
Console.WriteLine($"async response bytes: {asyncResponse.Length}");
// Packet-aware read: short packet vs timeout (fastboot/EDL message boundary)
var buf = new byte[512];
var result = session.ReadPacket(buf, 0, buf.Length, 3000);
Console.WriteLine($"packet bytes={result.Count} timeout={result.IsTimeout} short={result.IsShortPacket}");
// Progress reporting while flashing a large image
var progress = new Progress<long>(total => Console.WriteLine($"written: {total}"));
await asyncSession.WriteAsync(image, 0, image.Length, 3000, progress);
}
// Device change monitoring (dispose when appropriate)
using var monitor = comm.MonitorUsbDevices(
changes =>
{
foreach (var change in changes)
{
Console.WriteLine($"device {change.Kind}: {change.Device.ApiName} {change.Device.DevicePath}");
}
},
UsbApiKind.Auto,
pollInterval: TimeSpan.FromSeconds(1),
fireInitialSnapshot: false,
onError: ex => Console.WriteLine($"monitor error: {ex.Message}"));
// Structured diagnostics event (for metrics/log aggregation)
UsbTrace.TransferObserved += evt =>
{
Console.WriteLine($"usb {evt.Operation} backend={evt.Backend} outcome={evt.Outcome} bytes={evt.TransferredBytes}/{evt.RequestedBytes} retry={evt.RetryCount} err={evt.NativeErrorCode}");
};Register a custom USB API provider:
comm.RegisterUsbApi("my-custom", () => new MyCustomUsbApiProvider());FirmwareKit.Comm.CLI provides these commands:
apis: list available USB APIs.devices: enumerate devices with optional filters.all-devices: list all USB devices recognized by the current platform (native backend by default); prints[enum-diagnostics]even when empty.monitor: run a device-change monitor (options:--api,--vid,--pid,--interval <seconds>).selftest: read-only smoke test — open session, GET_DESCRIPTOR control transfer, shortReadExact(no writes/resets);--duration <seconds>loops it (0 = single pass).io-test: write/reset smoke test (bulk OUT write, reset, endpoint-open, concurrent-io, offset-write); options include--ep-in,--ep-out,--pattern-size,--concurrent,--offset-write.interrupt-test: interrupt-IN endpoint read test (options:--ep <addr>).
Examples:
# List APIs
dotnet run --project FirmwareKit.Comm.CLI -- apis
# List devices (libusb backend, filtered by VID/PID)
dotnet run --project FirmwareKit.Comm.CLI -- devices --api libusb --vid 0x18D1 --pid 0x4E11
# List all USB devices recognized by current platform (JSON output)
dotnet run --project FirmwareKit.Comm.CLI -- all-devices --json
# Read-only selftest against a specific device
dotnet run --project FirmwareKit.Comm.CLI -- selftest --api native --vid 0x18D1 --duration 10Supported devices / all-devices options:
--api auto|native|libusb: select backend API.--vid <hex>: vendor ID (hex or decimal).--pid <hex>: product ID (hex or decimal).--serial <text>: device serial number.--path-contains <text>: substring filter on device path.--if-class <hex|dec>: interface class filter.--if-subclass <hex|dec>: interface subclass filter.--if-protocol <hex|dec>: interface protocol filter.--json: JSON output (devices / all-devices).
Diagnostics are opt-in via environment variables: FIRMWAREKIT_USB_DEBUG=1 (plain-text logs), FIRMWAREKIT_USB_CAPTURE_FRAMES=1 (raw frame capture into transfer events, capped at 256 bytes per event).
dotnet restore
dotnet build -c Release # CI validates Release
dotnet test -c Release # xunit v3; empty enumeration is the expected CI outcome- The solution is
.slnxand requires the .NET 10 SDK. GeneratePackageOnBuild=trueemits nupkg/snupkg on every build — never treatbin//obj/artifacts as sources of truth.tools/fetch-libusb.shdownloads native libusb runtimes intonative/<target>/(mirror flags:--mirror tuna|ustc,--github-mirror,--ghcr-mirror).tools/test-windows.ps1/tools/test-linux.sh/tools/test-macos.share real-device automated test scripts (build + unit tests + CLI smoke + read-only selftest + 3 s monitor hot-plug smoke); they exit 0 withSKIPwhen no device matches.- CI workflows:
dotnet-ci.yml(build/test/behavior observation on win/ubuntu/macos, including enumeration-mechanism assertions),cross-publish.yml(RID publish graph check),fetch-libusb.yml(native runtime fetch),qemu-linux-guest.yml(real emulated USB inside a QEMU Linux guest).
MIT