AxlUsb — USB device enumeration + descriptor reads

USB device enumeration via EFI_USB_IO_PROTOCOL.

Header: <axl/axl-usb.h>. Lazy on first call: AxlUsb walks the firmware-installed USB I/O protocol handles, derives stable (bus, addr, intf) ordinals from each handle’s device path, and caches the result. On platforms without a USB stack (rare; some constrained BMC firmware) every axl_usb_* call returns -1 / NULL cleanly.

Cursor-style enumeration matches axl_pci_next and axl_smbios_find_next:

AxlUsbAddr *u = NULL;
while ((u = axl_usb_next(u)) != NULL) {
    uint16_t vid, pid;
    if (axl_usb_get_vid_pid(*u, &vid, &pid) == 0) {
        axl_printf("Bus %03u Device %03u If %u  %04x:%04x\n",
                   u->bus, u->addr, u->intf, vid, pid);
    }
}

axl_usb_next emits one entry per EFI_USB_IO_PROTOCOL handle — i.e. one per USB interface. A composite device with N interfaces returns N entries that share (bus, addr) and differ only in intf. A consumer that wants one row per physical device should dedupe on (bus, addr) (mirrors what Linux lsusb does in its default short form; see tools/lsusb.c for the reference renderer).

Address tuple

typedef struct {
    uint8_t  bus;    ///< host-controller ordinal (1-based)
    uint8_t  addr;   ///< device ordinal within bus (1-based)
    uint8_t  intf;   ///< interface number from interface descriptor
} AxlUsbAddr;

bus and addr are synthesized — UEFI doesn’t expose USB topology the way Linux does, so AxlUsb derives ordinals from each handle’s device path: bus numbers each unique host controller in device-path order; addr numbers each unique physical device within a bus. They’re stable within a single boot but may shift across boots if the firmware enumerates handles in a different order. Consumers needing a persistent identity should hash the device descriptor (vid:pid:serial) instead.

intf IS the real bInterfaceNumber from the interface descriptor.

Per-device introspection

uint16_t vid, pid;
axl_usb_get_vid_pid(addr, &vid, &pid);

uint8_t cls, sub, prot;
axl_usb_get_class(addr, &cls, &sub, &prot);  // any out param may be NULL

char buf[AXL_USB_STRING_MAX];
axl_usb_get_manufacturer(addr, buf, sizeof(buf));
axl_usb_get_product     (addr, buf, sizeof(buf));
axl_usb_get_serial      (addr, buf, sizeof(buf));

axl_usb_get_vid_pid reads the device descriptor’s idVendor / idProduct (so all interfaces of one physical device return the same pair). axl_usb_get_class reads the interface’s class triplet — composite devices set bDeviceClass = 0 and drive their identity per interface, so this is the right granularity for the “one row per interface” walk lsusb does in -vv mode.

axl_usb_get_manufacturer / _product / _serial are convenience wrappers around axl_usb_get_string(addr, idx, buf, buflen) that read the device descriptor’s iManufacturer / iProduct / iSerialNumber index byte first. They return -1 when the device declares no string at that slot (index == 0). Each one lazily probes the device’s supported-language table on first use and caches the first language ID per interface.

Class triplet decode

char buf[AXL_USB_CLASS_NAME_MAX];
axl_usb_class_string_fmt(0x03, 0x01, 0x02, AXL_USB_CLASS_FMT_FULL,
                         buf, sizeof(buf));
// → "Human Interface Device / Boot Interface / Mouse"

AxlUsbClassFmt selects the output shape — same posture as AxlPciClassFmt:

  • FMT_FULL"<base> / <sub> / <prot>" (default; verbose tools)

  • FMT_SUBCLASS"<sub>" (collapses to <base> when sub unknown, then numeric — Linux lsusb shape)

  • FMT_BASE"<base>" (collapses to numeric when unknown)

Tiers with no spec-defined name are omitted rather than rendered as <unknown> placeholders; wholly-unknown class falls back to "Class XXXXXX" numeric. Compiled-in tables in src/usb/axl-usb-class.c cover the USB-IF Defined Class Codes (https://www.usb.org/defined-class-codes) — base classes, common subclasses (HID Boot, Mass Storage SCSI, CDC variants, …), and the most-used protocol bytes (HID Mouse / Keyboard, BBB, UAS). No sidecar overlay yet (AxlPci has one — the classes[] section of pci-ids.json5 — but the USB compiled-in set covers what we ship today).

Topology walk

static int print_node(AxlUsbAddr a, unsigned depth, void *ctx) {
    (void)ctx;
    for (unsigned i = 0; i < depth; i++) axl_print("  ");
    axl_printf("Bus %03u Dev %03u If %u\n", a.bus, a.addr, a.intf);
    return 0;
}

axl_usb_tree_for_each(print_node, NULL);

axl_usb_tree_for_each walks every interface in (bus, port_chain, intf) ascending order — guaranteeing parents arrive before children, so a renderer can emit indentation directly from depth without lookahead. depth is the USB hub depth: 0 = directly attached to the host controller’s root hub, 1 = behind one hub, etc. AXL_USB_TREE_MAX_DEPTH = 8 caps the recorded chain (USB spec real-world maximum is 5 hubs / 6 USB nodes).

The walker is built from each handle’s EFI device path — the chain of consecutive MSG_USB_DP nodes encodes the full hub-port path — and the existing dev-key sort guarantees parents-before-children (parent dev_keys are byte prefixes of children’s). tools/lsusb.c’s -t mode is the reference consumer.

Vendor / device name database

axl_usb_ids_load(override_path) loads a curated JSON5 sidecar (usb-ids.json5). When override_path is non-NULL it is used authoritatively (no fallback — explicit means explicit). When it is NULL the loader autodiscovers in this order: companion to the running .efi, then current working directory. axl-sdk ships a starter set in share/usb-ids.json5 covering common HID, NIC, hub, and storage vendors; bulk-extract from canonical usb.ids via scripts/usb-ids-to-json5.py.

if (axl_usb_ids_load(NULL) == AXL_SIDECAR_OK) {
    const char *vendor = axl_usb_vendor_name(0x046D);
    const char *device = axl_usb_device_name(0x046D, 0xC52B);
    /* both NULL-safe; consumers fall back to numeric IDs */
}

axl_usb_ids_load returns:

  • AXL_SIDECAR_OK on success (idempotent on subsequent calls)

  • AXL_SIDECAR_FILE_MISSING if no candidate file exists

  • AXL_SIDECAR_PARSE_ERROR if a candidate was found but failed to parse

The split lets tools log differently — “no database shipped” is a deployment problem (numeric fallback is fine), while “parse error” is an authoring problem worth being loud about. See <axl/axl-sidecar.h> for the shared status enum.

Schema 1 is the only supported layout — hierarchical from the start (vendors with nested devices). USB has no subsystem dimension that motivated AxlPciIds’s v1 (flat) → v2 (hierarchical) split.

{
    schema: 1,
    vendors: [
        { id: 0x046D, name: 'Logitech, Inc.',
          devices: [
            { pid: 0xC52B, name: 'Unifying Receiver' },
          ],
        },
    ],
}

Composed-name helper

axl_usb_format_name(vid, pid, buf, buflen) centralizes the “vendor + device + numeric tail” rendering convention so every consumer prints the same string for the same (vid, pid) pair:

char buf[AXL_USB_NAME_COMPOSED_MAX];
axl_usb_format_name(0x046D, 0xC52B, buf, sizeof(buf));
// → "Logitech, Inc. Unifying Receiver"

Vendor-known + device-unknown produces "<vendor> Device <pid hex>"; vendor-unknown short-circuits to "<vid>:<pid>" regardless of whether a device entry happens to exist (without a verified vendor the device hit is ambiguous provenance).

Layered databases (handle API)

Same shape as AxlPciIds. Consumers that ship a private OEM sheet on top of the public set load two handles and query in priority order:

AxlUsbIds *pub  = NULL;
AxlUsbIds *priv = NULL;
axl_usb_ids_open("usb-ids.json5",         &pub);
axl_usb_ids_open("private-usb-ids.json5", &priv);

const char *d = axl_usb_ids_device_name(priv, vid, pid);
if (d == NULL) d = axl_usb_ids_device_name(pub, vid, pid);

axl_usb_ids_close(priv);
axl_usb_ids_close(pub);

axl_usb_ids_format_name(handle, vid, pid, buf, buflen) is the handle-aware equivalent of axl_usb_format_name. For “show me everything in this overlay” use axl_usb_ids_foreach_vendor / _foreach_device.

Per-name length contracts

AXL_USB_VENDOR_NAME_MAX     = 128 bytes
AXL_USB_DEVICE_NAME_MAX     = 192 bytes
AXL_USB_NAME_COMPOSED_MAX   = 384 bytes
AXL_USB_CLASS_NAME_MAX      = 128 bytes
AXL_USB_STRING_MAX          = 384 bytes  (USB string descriptors;
                                          127 BMP chars * 3 UTF-8
                                          bytes + NUL; BMP only)

Sized to comfortably hold real usb.ids entries; loader silently truncates over-cap names. Pin char buf[AXL_USB_NAME_COMPOSED_MAX] on the stack and never have to worry about formatter overflow.

Bulk population from canonical usb.ids

The shipped share/usb-ids.json5 is a curated starter set (~22 vendors). For fleet-scale OEM-rebadge coverage, run the conversion against the canonical usb.ids:

# Full set:
scripts/usb-ids-to-json5.py /usr/share/hwdata/usb.ids \
    > usb-ids.json5

# Curated subset (vendor entries always emitted; only their
# devices are dropped for vendors not in the list):
scripts/usb-ids-to-json5.py --vendors-only 046d,0bda,1d6b \
    /usr/share/hwdata/usb.ids > usb-ids.json5

# Verify the script itself:
scripts/usb-ids-to-json5.py --self-test

The .deb / .rpm install the converter under /usr/share/axl/scripts/; the line-level parser is shared with pci-ids-to-json5.py via _ids_parser.py.

API Reference

USB device enumeration via EFI_USB_IO_PROTOCOL handles.

The UEFI USB stack exposes one EFI_USB_IO_PROTOCOL handle per interface* of every enumerated device — multiple handles therefore refer to the same physical device when it has more than one function (e.g. a composite keyboard+mouse, or a USB-net adapter with separate control / data interfaces). AxlUsb derives stable (bus, addr, intf) ordinals from each handle’s device path so consumers can present a Linux-lsusb-shaped view: one row per interface, deduplicated and ordered by bus/device/interface.

Cursor-style iteration mirrors axl_pci_next:

AxlUsbAddr *u = NULL;
while ((u = axl_usb_next(u)) != NULL) {
    uint16_t vid, pid;
    if (axl_usb_get_vid_pid(*u, &vid, &pid) == 0) {
        // ...
    }
}

Phase A: enumeration + vendor/product ID readout. Phase B: interface class triplet (class / subclass / protocol) decode via the same compiled-in tables AxlPci uses for PCI classes. Phase C: string descriptor reads — manufacturer / product / serial via UsbGetStringDescriptor + UCS-2 → UTF-8. Phase D: vendor/device-name database via usb-ids.json5 sidecar (handle + singleton API mirroring AxlPciIds). See docs/AXL-Usb-Handoff.md.

USB-name length contracts

Maximum bytes (including NUL) any USB-IDs database lookup can return.

Sized comfortably for real usb.ids entries — vendor strings like “Logitech, Inc.” run ~20 bytes, device strings frequently 60-100, the rare full descriptive name fits in 192. Composed name covers vendor + device + small fixed overhead, so axl_usb_format_name never truncates non-truncated inputs.

AXL_USB_VENDOR_NAME_MAX

vendor entry max bytes

AXL_USB_DEVICE_NAME_MAX

device entry max bytes

AXL_USB_NAME_COMPOSED_MAX

axl_usb_format_name output max

Database iteration callbacks

Non-zero callback return stops iteration and propagates as the iter rc.

typedef int (*AxlUsbIdsVendorFn)(uint16_t vid, const char *name, void *ctx)
typedef int (*AxlUsbIdsDeviceFn)(uint16_t vid, uint16_t pid, const char *name, void *ctx)
int axl_usb_ids_foreach_vendor(const AxlUsbIds *ids, AxlUsbIdsVendorFn fn, void *ctx)
int axl_usb_ids_foreach_device(const AxlUsbIds *ids, AxlUsbIdsDeviceFn fn, void *ctx)

Defines

AXL_USB_TREE_MAX_DEPTH

Maximum hub depth the topology walker descends, and the basis for the port-path buffer cap below. USB 2.0/3.x caps the bus depth at 5 hubs between root and device (= 6 USB device-path nodes including the leaf), so 8 levels is generous headroom against malformed firmware paths. (Also used by axl_usb_tree_for_each, further down.)

AXL_USB_PORT_PATH_MAX

Buffer cap for a port-path string: up to AXL_USB_TREE_MAX_DEPTH port numbers (3 digits each) joined by ‘.’ separators, plus NUL. Sized 4 bytes/port (digit-triple + separator) + 1, one byte generous since the last port has no trailing separator.

AXL_USB_CLASS_NAME_MAX

Buffer cap for a class-triplet decoded string. Sized for the longest plausible “Base / Sub / Prot” composition.

AXL_USB_STRING_MAX

Buffer cap for UTF-8 string-descriptor output. USB string descriptors max out at 254 bytes of UCS-2 payload (127 BMP code points); a BMP code point in U+0800..U+FFFF expands to 3 UTF-8 bytes, so the worst-case payload is 127 * 3 = 381 bytes plus NUL. BMP only — axl_ucs2_to_utf8_buf does not decode surrogate pairs, so supplementary-plane code points (U+10000+) are not supported.

Typedefs

typedef int (*AxlUsbTreeFn)(AxlUsbAddr addr, unsigned depth, void *ctx)

Per-node callback for axl_usb_tree_for_each.

Param addr:

the interface being visited (same shape axl_usb_next emits)

Param depth:

USB hub depth — 0 means the device is directly attached to the host controller’s root hub; 1 means the device is one hub deep; etc. Different from addr.bus (which is the controller ordinal).

Param ctx:

caller’s opaque context

Return:

non-zero to stop the walk early; the value becomes the return of axl_usb_tree_for_each. Return 0 to continue.

typedef struct AxlUsbIds AxlUsbIds

Opaque handle to a loaded USB vendor/device-name database.

Created by axl_usb_ids_open or axl_usb_ids_open_from_buffer, destroyed by axl_usb_ids_close. Multiple handles can coexist — a consumer that wants a “public + private” overlay loads two handles and queries them in priority order. Mirrors AxlPciIds and AxlSpdIds.

Schema 1 is the only supported layout. The structure is hierarchical from the start (vendors with nested devices) — USB has no subsystem dimension that motivated PCI’s v1→v2 split, so there’s no flat-vs-hierarchical schema dispatch.

{ schema: 1,
  vendors: [
    { id: 0x046D, name: 'Logitech, Inc.',
      devices: [
        { pid: 0xC52B, name: 'Unifying Receiver' },
      ],
    },
  ],
}

Enums

enum AxlUsbClassFmt

Output shape selector for axl_usb_class_string_fmt.

USB class triplets decode into base / subclass / protocol tiers. Verbose tools want the full slash-joined triplet; row-oriented tools want the subclass alone (matches Linux lsusb shape); coarse categorization wants just the base. Mirrors AxlPciClassFmt.

Values:

enumerator AXL_USB_CLASS_FMT_FULL

“Human Interface Device / Boot Interface / Mouse”

enumerator AXL_USB_CLASS_FMT_SUBCLASS

“Boot Interface” (collapses to base if unknown)

enumerator AXL_USB_CLASS_FMT_BASE

“Human Interface Device” (collapses to numeric if unknown)

enum AxlUsbDataDir

Direction of USB data transfer. Values match the USB-IO protocol’s EFI_USB_DATA_DIRECTION ordering exactly so the implementation can pass through without translation.

Values:

enumerator AXL_USB_DATA_IN

host reads data from device

enumerator AXL_USB_DATA_OUT

host writes data to device

enumerator AXL_USB_NO_DATA

setup-only; data must be NULL, data_len 0

Functions

AxlUsbAddr *axl_usb_next(AxlUsbAddr *prev)

Iterate every USB interface exposed by EFI_USB_IO_PROTOCOL.

Returns a pointer to a static internal cursor; the storage is reused across calls and is invalidated by the next call. Pass NULL to start the walk fresh, or the previous non-NULL return value to advance — passing any other pointer (including a caller-allocated AxlUsbAddr) restarts iteration silently. The caller never owns the cursor’s storage.

Iteration order is (bus, addr, intf) ascending. Returns NULL when the walk is complete or no EFI_USB_IO_PROTOCOL handles are installed (the platform has no USB stack, or the firmware did not enumerate any USB devices).

Parameters:
  • prev – previous result, or NULL to start

Returns:

pointer to the next interface, or NULL when enumeration is complete (or the USB stack is unavailable).

int axl_usb_get_vid_pid(AxlUsbAddr addr, uint16_t *vid, uint16_t *pid)

Read vendor ID and product ID from a USB device descriptor.

Reads the idVendor / idProduct fields of the standard 18-byte device descriptor via EFI_USB_IO_PROTOCOL.UsbGetDeviceDescriptor. All interfaces of one physical device share the same (vid, pid) — the per-interface granularity of AxlUsbAddr does not change the descriptor that backs this call.

Parameters:
  • addr – target interface

  • vid – [out] vendor ID

  • pid – [out] product ID

Returns:

AXL_OK on success (both fields populated), AXL_ERR if addr is not a known interface or the firmware fails the descriptor read.

int axl_usb_get_class(AxlUsbAddr addr, uint8_t *class_, uint8_t *sub, uint8_t *prot)

Read the interface class triplet for a USB interface.

Reads bInterfaceClass / bInterfaceSubClass / bInterfaceProtocol from the interface descriptor via EFI_USB_IO_PROTOCOL.UsbGetInterfaceDescriptor. Composite devices (DeviceDescriptor.bDeviceClass = 0) drive their class identity per interface — the right granularity to query for tools that present one row per interface (Linux lsusb -v shape).

Out parameters may be individually NULL if the caller doesn’t need that field.

Parameters:
  • addr – target interface

  • class_ – [out, optional] base class (e.g. 0x03 for HID)

  • sub – [out, optional] subclass

  • prot – [out, optional] protocol

Returns:

AXL_OK on success, AXL_ERR if addr is not a known interface or the firmware fails the descriptor read.

int axl_usb_get_device_info(AxlUsbAddr addr, AxlUsbDeviceInfo *out)

Read curated device-level descriptor fields for a USB device.

Reads the device descriptor via EFI_USB_IO_PROTOCOL.UsbGetDeviceDescriptor and copies out the fields in AxlUsbDeviceInfo. All interfaces of one physical device share these values (the descriptor is per-device, not per-interface).

Fields are returned raw. In particular device_class is 0 on a composite device, where the class identity lives per interface — a consumer that wants a single “device class” should fall back to axl_usb_get_class when device_class == 0. Likewise bcd_usb is the raw BCD value; format it yourself (e.g. high byte . low byte as hex: 0x0200 -> “2.0”, 0x0110 -> “1.1”). The library returns mechanism, not policy.

Parameters:
  • addr – target interface (any interface of the device)

  • out – [out] populated on success

Returns:

AXL_OK on success (out fully populated), AXL_ERR if out is NULL, addr is not a known interface, or the firmware fails the descriptor read.

int axl_usb_get_num_endpoints(AxlUsbAddr addr, uint8_t *out)

Read the endpoint count of a USB interface.

Reads bNumEndpoints from the interface descriptor via EFI_USB_IO_PROTOCOL.UsbGetInterfaceDescriptor. This is the count of non-control endpoints the interface declares (the control endpoint 0 is implicit and not included, per USB 2.0 §9.6.5).

Parameters:
  • addr – target interface

  • out – [out] bNumEndpoints

Returns:

AXL_OK on success, AXL_ERR if out is NULL, addr is not a known interface, or the firmware fails the descriptor read.

int axl_usb_get_port_info(AxlUsbAddr addr, uint8_t *parent_port, char *port_path, size_t port_path_len)

Read a USB device’s hub-port location.

Surfaces the port chain AxlUsb already parses from the device path (the same chain axl_usb_tree_for_each walks): the sequence of ParentPortNumber values from the root-hub port down to the port the device is plugged into on its immediate parent.

  • parent_port — the port number on the device’s immediate parent (root hub or an intermediate hub); the last element of the chain. This is what the app cannot derive on its own. For a device attached directly to the root hub this equals the whole path.

  • port_path — the full chain rendered as .’-joined port numbers, root first (e.g.”4”for a root-port device,”4.1” for a device on port 1 of a hub on root port 4). Linuxlsusb -t` / sysfs port-path shape. Always NUL-terminated; truncated (snprintf-style) if port_path_len is too small — and truncation is not reflected in the return value, so size with AXL_USB_PORT_PATH_MAX to never truncate.

Both out-parameters are optional (pass NULL to skip either); passing NULL for both is a successful no-op.

Parameters:
  • addr – target interface

  • parent_port – [out, optional] immediate-parent port number

  • port_path – [out, optional] ‘.’-joined port chain, root first

  • port_path_len – capacity of port_path in bytes (incl. NUL)

Returns:

AXL_OK on success, AXL_ERR if addr is not a known interface (the port chain is recovered at enumeration time, so a known interface always has one — there is no firmware call here).

int axl_usb_class_string_fmt(uint8_t class_, uint8_t sub, uint8_t prot, AxlUsbClassFmt fmt, char *buf, size_t buflen)

Format a USB class triplet as a human-readable string.

Decodes per the USB-IF Defined Class Codes spec (https://www.usb.org/defined-class-codes) — up to three tiers: base class, subclass, and protocol. Tiers with no spec-defined name are omitted rather than rendered as <unknown> placeholders.

Output shapes (FMT_FULL):

  • All known: "<base> / <sub> / <prot>" (e.g. "Human Interface Device / Boot Interface / Mouse")

  • Known base+sub, unknown prot: "<base> / <sub>"

  • Known base, unknown sub: "<base>"

  • Wholly unknown: "Class XXXXXX" (numeric hex, in the spirit of Linux lsusb’s numeric fallback).

Always NUL-terminates buf (snprintf-shape).

Parameters:
  • class_ – base class

  • sub – subclass

  • prot – protocol

  • fmt – output shape selector

  • buf – destination buffer

  • buflen – capacity of buf

Returns:

number of bytes written excluding NUL, or -1 if buf is NULL or buflen is 0 or fmt is unrecognized.

int axl_usb_class_string(uint8_t class_, uint8_t sub, uint8_t prot, char *buf, size_t buflen)

Convenience wrapper for axl_usb_class_string_fmt with FMT_FULL.

int axl_usb_get_string(AxlUsbAddr addr, uint8_t string_index, char *buf, size_t buflen)

Read a USB string descriptor by index, decoded to UTF-8.

Indices are unsigned bytes in the device’s string descriptor table: the device descriptor’s iManufacturer / iProduct / iSerialNumber fields name three of them by convention; class- specific descriptors (HID, Audio, Configuration) reference others.

Index 0 is reserved for the language-ID table (USB 2.0 §9.6.7); axl_usb_get_string handles that internally — pass 1..255 for actual strings. The library picks the first language ID the device advertises and caches it per interface; multi-language devices that need a specific LANGID are out of scope for this helper.

Output is NUL-terminated UTF-8 in buf. Truncation is silent at the byte boundary (never writes a partial multi-byte sequence) — axl_ucs2_to_utf8_buf’s contract.

Parameters:
  • addr – target interface

  • string_index – 1..255; 0 is reserved

  • buf – destination UTF-8 buffer

  • buflen – capacity in bytes (incl. NUL)

Returns:

number of UTF-8 bytes written excluding NUL on success, -1 if addr is unknown, the device has no string descriptors, index 0 was passed, the firmware fails the descriptor read, or buf / buflen are bad.

int axl_usb_get_manufacturer(AxlUsbAddr addr, char *buf, size_t buflen)

Read the device’s manufacturer string (UTF-8).

Convenience over axl_usb_get_string — reads the iManufacturer index from the device descriptor, then fetches that string. Returns -1 if the device descriptor declares no manufacturer string (index = 0), mirroring axl_usb_get_string.

Returns:

UTF-8 byte count or -1 (see axl_usb_get_string).

int axl_usb_get_product(AxlUsbAddr addr, char *buf, size_t buflen)

Read the device’s product string (UTF-8). See axl_usb_get_manufacturer.

int axl_usb_get_serial(AxlUsbAddr addr, char *buf, size_t buflen)

Read the device’s serial-number string (UTF-8). See axl_usb_get_manufacturer.

int axl_usb_control_transfer(AxlUsbAddr addr, uint8_t request_type, uint8_t request, uint16_t value, uint16_t index, AxlUsbDataDir direction, uint32_t timeout_ms, void *data, size_t data_len)

Issue a USB control transfer to a specific interface.

Wraps EFI_USB_IO_PROTOCOL.ControlTransfer for a target identified by AxlUsbAddr. Useful for class-specific control requests (CDC SEND_ENCAPSULATED_COMMAND, HID SET_REPORT, etc.) that need to talk to a device after enumeration but without going through a higher- level driver.

For class-targeted requests, build the setup packet’s request_type with 0x21 (Class | Interface | Out) or 0xA1 (Class | Interface | In), and pass the interface number — typically addr.intf — in index.

Parameters:
  • addr – target interface

  • request_type – bmRequestType byte of the USB setup packet

  • request – bRequest byte of the USB setup packet

  • value – wValue field of the setup packet

  • index – wIndex field of the setup packet

  • direction – data-stage direction (in / out / none)

  • timeout_ms – transfer timeout in milliseconds

  • data – buffer for the data stage; NULL if direction is AXL_USB_NO_DATA

  • data_len – bytes to transfer in the data stage

Returns:

AXL_OK on success, AXL_ERR if addr is unknown, the underlying ControlTransfer returns an EFI error, OR the per-transfer USB status word is non-zero (any of EFI_USB_ERR_STALL / _NAK / _CRC / _TIMEOUT / _BABBLE / etc.). Callers that need to distinguish transient NAKs from hard stalls would need a richer API — this helper collapses everything non-zero into AXL_ERR.

int axl_usb_tree_for_each(AxlUsbTreeFn fn, void *ctx)

Walk the USB topology in tree order, depth-first per bus.

Each EFI_USB_IO_PROTOCOL handle’s device path encodes its full port chain via consecutive USB messaging-type nodes (PciRoot/.../Pci(usb_ctrl)/USB(parent_port, intf)/USB(...)). The walker recovers that chain at ingest time and emits entries in (bus, port_chain, intf) ascending order — guaranteeing parents arrive before children, so a renderer can print indentation derived from depth without lookahead.

Hubs that have no leaf descendants still appear (depth = N for an N-deep hub); their interface is enumerable via the same EFI_USB_IO_PROTOCOL handle every other USB device exposes. The caller can disambiguate hubs from leaves by reading the interface class via axl_usb_get_class — class 0x09 is the USB Hub class.

The callback may freely call AxlUsb read-only APIs against the visited addr (vid/pid, class, string descriptors) but must not invoke axl_usb_next during the walk — the cursor it uses is module-static and a re-entrant call would corrupt the outer iteration.

Returns:

0 on a clean walk, the callback’s first non-zero return if it stopped early, or -1 if fn is NULL or no USB stack is available.

AxlSidecarStatus axl_usb_ids_open(const char *path, AxlUsbIds **out)

Open a USB-IDs database from a JSON5 file.

Returns:

AXL_SIDECAR_OK on success (handle returned via out), AXL_SIDECAR_FILE_MISSING if path does not exist, AXL_SIDECAR_PARSE_ERROR on JSON5 / schema rejection.

AxlSidecarStatus axl_usb_ids_open_from_buffer(const char *json5, size_t len, AxlUsbIds **out)

Open a USB-IDs database from an in-memory JSON5 buffer.

Returns:

AXL_SIDECAR_OK on success, AXL_SIDECAR_PARSE_ERROR on parse / schema error.

void axl_usb_ids_close(AxlUsbIds *ids)

Free a database handle. NULL-safe.

const char *axl_usb_ids_vendor_name(const AxlUsbIds *ids, uint16_t vid)

Vendor lookup against an explicit handle.

Returns:

database-owned string or NULL if unknown / handle empty.

const char *axl_usb_ids_device_name(const AxlUsbIds *ids, uint16_t vid, uint16_t pid)

Device lookup against an explicit handle.

Returns:

database-owned string or NULL if (vid, pid) is unknown.

int axl_usb_ids_format_name(const AxlUsbIds *ids, uint16_t vid, uint16_t pid, char *buf, size_t buflen)

Compose a “vendor + device” display string against a handle.

Centralizes the rendering convention every consumer would otherwise reinvent — every tool prints the same string for the same (vid, pid) pair:

  • vendor known + device known → "<vendor> <device>"

  • vendor known + device unknown → "<vendor> Device <PID hex>"

  • vendor unknown → "<VID>:<PID>"

Hex literals are lowercase, 4-wide, zero-padded (matches Linux lsusb’s -d filter form). Output never exceeds AXL_USB_NAME_COMPOSED_MAX bytes.

Returns:

number of bytes written excluding NUL (snprintf shape), or -1 on bad arguments.

AxlSidecarStatus axl_usb_ids_load(const char *override_path)

Load the curated USB vendor/device name database into the process-global slot.

Two modes selected by override_path:

  • Explicit (override_path non-NULL): use exactly that path.

  • Autodiscover (override_path NULL): try usb-ids.json5 next to the running .efi (companion path), then in the current working directory.

Idempotent — a successful load is a no-op on subsequent calls. Registers an axl_atexit cleanup on first success so the parsed table is freed automatically at runtime cleanup.

void axl_usb_ids_free(void)

Free the loaded database. Safe to call when none is loaded.

const char *axl_usb_vendor_name(uint16_t vid)

Singleton-backed vendor lookup.

Returns:

database-owned string, or NULL if no database loaded or vid is not present.

const char *axl_usb_device_name(uint16_t vid, uint16_t pid)

Singleton-backed device lookup. Does not fall back to the vendor name when the device is unknown — callers compose their own “vendor name + numeric PID” via axl_usb_format_name.

int axl_usb_format_name(uint16_t vid, uint16_t pid, char *buf, size_t buflen)

Singleton-backed convenience wrapper for axl_usb_ids_format_name.

struct AxlUsbAddr
#include <axl-usb.h>

A USB controller address (bus / device / interface).

bus is a 1-based ordinal of the host controller (xHCI/EHCI/UHCI/ OHCI) the device is behind, in EFI device-path order. addr is a 1-based ordinal of the physical device within that bus, deduplicated across interfaces. intf is the real interface number returned by UsbGetInterfaceDescriptor — multiple (bus, addr, *) entries with different intf values refer to interfaces of the same physical device.

Ordinals are stable within a single boot but not across boots — the underlying EFI handle order can change. Consumers that need a persistent identity should hash the device descriptor (vid:pid:serial) instead.

Public Members

uint8_t bus

host controller ordinal (1-based)

uint8_t addr

device ordinal within bus (1-based)

uint8_t intf

interface number from interface descriptor

struct AxlUsbDeviceInfo
#include <axl-usb.h>

Curated device-level fields not covered by the per-interface accessors.

A projection of the standard device descriptor (one per physical device, shared by all its interfaces) — the fields an lsusb-style consumer wants beyond vendor/product ID (which axl_usb_get_vid_pid already covers). Not the full 18-byte wire structure.

Public Members

uint16_t bcd_usb

USB spec version, BCD (0x0200 = “2.0”, 0x0110 = “1.1”)

uint8_t device_class

bDeviceClass; 0 on a composite device (class is per-interface)

uint8_t device_sub_class

bDeviceSubClass

uint8_t device_protocol

bDeviceProtocol

uint8_t num_configurations

bNumConfigurations