USB Host Component

Overview

espp::UsbHost is the host-side counterpart to espp::UsbDevice. It drives the ESP32-S2 / -S3 / -P4 USB-OTG peripheral as a USB host, enumerates attached devices, and exposes the HID class devices it finds — mice, keyboards, gamepads, and vendor-specific HID devices (for example another ESP running espp::UsbDevice as a HID device, such as an espp::WdiUsbPeripheral).

It is a thin, idiomatic wrapper over the ESP-IDF USB Host library (usb) and the usb_host_hid class driver: it owns the whole host lifecycle — installing the host library and HID driver, running their event tasks, opening interfaces, and teardown — and marshals the driver’s C callbacks into per-device std::function objects. Like the rest of espp it does not throw and reports failures via std::error_code.

Report directions are named from the connected device’s point of view, as in the USB HID spec: an Input report is device→host (delivered to a HidDevice input callback), an Output report is host→device (sent with HidDevice::send_output_report()). This mirrors espp::UsbDevice exactly, so the two ends of a link (for example the device and host roles of the wdi component) line up.

Features

  • Installs / uninstalls the USB Host library and the HID class driver and runs their event-handling tasks.

  • Device connect / disconnect callbacks, with an optional filter predicate so only the devices you care about are opened (by VID/PID, interface, etc.).

  • Per-device Input report callback (device→host) delivering the raw report bytes (report id in byte 0 for report-ID’d descriptors).

  • Send Output reports (host→device) and issue the HID class control requests (Get/Set Report, Get/Set Idle, Set Protocol).

  • Read a connected device’s HID report descriptor.

  • No exceptions; initialize() reports failures via std::error_code.

Basic Usage

espp::UsbHost host({
    .on_device_connected =
        [](const std::shared_ptr<espp::UsbHost::HidDevice> &dev) {
          auto info = dev->info();
          printf("connected: %s %s %04x:%04x\n", info.manufacturer.c_str(),
                 info.product.c_str(), info.vid, info.pid);
          dev->set_input_callback([](std::span<const uint8_t> report) {
            // handle a device->host Input report (report[0] is the report id)
          });
        },
    .on_device_disconnected = [](const auto &) { /* ... */ },
    // optional: only open the devices you want
    // .should_open = [](const auto &info, const auto &) { return info.vid == 0x1209; },
});

std::error_code ec;
if (!host.initialize(ec)) { /* handle ec */ }

// later, send an Output report (host->device) -- devices() may be empty, so
// guard it (or keep the shared_ptr handed to on_device_connected and use that):
std::array<uint8_t, 4> payload{/* ... */};
if (auto devs = host.devices(); !devs.empty())
  devs.front()->send_output_report(/*report_id*/ 0x02, payload, ec);

Requirements and caveats

  • USB-OTG host mode is only available on the ESP32-S2, -S3 and -P4.

  • Only one espp::UsbHost may exist at a time (the USB Host library and HID class driver are global singletons). It cannot coexist with espp::UsbDevice (they both claim the USB-OTG peripheral).

  • The board must be able to source VBUS to the attached device — a board with a USB-A host port / VBUS switch, or a self-powered hub. UsbHost does not manage board power.

  • On the ESP32-S3 the USB-Serial-JTAG shares the USB-OTG PHY, so when the host role is active the console must run on UART0 (see the example’s sdkconfig.defaults).

  • The usb and usb_host_hid components come from the ESP Component Registry via the IDF component manager. On ESP-IDF ≥ 6.0 usb_host_hid declares its usb dependency only through the manager, so build with the component manager on (the default) rather than the manager-off flow used by the device-side USB examples.

Threading model

The HID class driver delivers its events on its own background task, and that same task is what completes the driver’s synchronous control transfers (Set/Get Report, Set Protocol, …). A control transfer issued from that task can therefore never complete. espp::UsbHost handles this the way the ESP-IDF HID host example does, but internally: the driver task only enqueues events (copying each Input report out of the driver’s buffer, which must happen inside the callback), and a dedicated dispatch task owned by UsbHost opens/starts/closes devices and invokes every user callback.

  • It is safe to call send_output_report() and the other HidDevice methods from inside on_device_connected / the input callback (e.g. to answer a request/response HID protocol).

  • Events for a device are delivered in order (connected → inputs → disconnected), and the connect callback runs before the device is started, so an input callback installed there sees the very first report.

  • HidDevice methods may also be called from any application task; each device serializes its driver calls internally.

  • Keep callbacks reasonably short: one that blocks delays every later event. The event queue is bounded (Config::max_queued_events); when the consumer falls behind, Input reports are dropped (logged) rather than blocking the USB stack.

  • info() / params() / report_descriptor() are snapshots taken at connect time and remain valid after the device disconnects.

Roadmap

Only the HID class driver is wired up today (it covers mice, keyboards, gamepads and vendor HID devices, and is what the wdi host role needs). The component is structured so other class drivers (CDC-ACM, MSC) can be layered in later without changing the host-lifecycle model — the same way espp::UsbDevice composes CDC / Vendor / HID functions on the device side.

API Reference

Header File

Classes

class UsbHost : public espp::BaseComponent

Native-USB **host** built on the ESP-IDF USB Host library (`usb`) and the `usb_host_hid` class driver, for the ESP32-S2 / -S3 / -P4 USB-OTG peripheral acting as a host.

`esppUsbHost` is the counterpart to `espp::UsbDevice`: instead of enumerating *as* a USB device, it drives the bus as a **host**, enumerates attached devices, and exposes the **HID** class devices it finds (mice, keyboards, gamepads, and vendor-specific HID devices such as an `espp::WdiUsbPeripheral`). It owns the whole host-side lifecycle:

  • installs the USB Host library and runs its event-handling task,

  • installs the HID class driver (with its own background task),

  • on device attach, opens each HID interface and (optionally) starts receiving its **Input** reports, delivering them to a per-device callback,

  • lets the application send **Output** reports (and issue the HID class Get/Set Report / Idle / Protocol control requests) back to a device,

  • and cleans everything up on teardown.

Report directions are named from the connected **device’s** point of view (as in the USB HID spec): an *Input* report is device→host (delivered to `HidDevice`’s input callback), an *Output* report is host→device (sent with `HidDevice::send_output_report()`). This is deliberately symmetric with `espp::UsbDevice`’s HID function, so the two sides of a link (e.g. the two roles of the `wdi` component) mirror each other.

**Threading model.** The HID class driver delivers its events on its own background task, and that same task is the one that completes the driver’s synchronous control transfers (Set/Get Report, Set Protocol, …). A control transfer issued *from* that task can therefore never complete. `UsbHost` handles this the way the ESP-IDF HID host example does: the driver task only *enqueues* events (copying each Input report out of the driver’s buffer, which must happen inside the callback), and a dedicated **dispatch task** owned by `UsbHost` opens/starts/closes devices and invokes every user callback. So it is safe to call `HidDevice::send_output_report()` (and the other device methods) from within the callbacks, and callbacks never stall the USB stack. Events for a device are delivered in order (connected → inputs → disconnected). Device methods may also be called from any application task; each device serializes its driver calls internally.

The class is idiomatic espp: it does not throw, reports failures via `std::error_code`, and marshals the USB-host driver’s C callbacks into per-device `std::function`s.

UsbHost (generic HID host) Example

extern "C" void app_main(void) {
  espp::Logger logger({.tag = "USB Host", .level = espp::Logger::Verbosity::INFO});
  logger.info("Starting USB HID host example");

  espp::UsbHost host({
      .on_device_connected =
          [&](const std::shared_ptr<espp::UsbHost::HidDevice> &device) {
            auto info = device->info();
            auto params = device->params();
            logger.info("connected: '{}' '{}' VID={:#06x} PID={:#06x} iface={} proto={}",
                        info.manufacturer, info.product, info.vid, info.pid,
                        params.interface_number, params.protocol);
            auto desc = device->report_descriptor();
            logger.info("  report descriptor: {} bytes", desc.size());

            // Log every Input report this device sends (device -> host).
            device->set_input_callback([&logger](std::span<const uint8_t> data) {
              logger.info("input report ({} bytes): {::#04x}", data.size(), data);
            });
          },
      .on_device_disconnected =
          [&](const std::shared_ptr<espp::UsbHost::HidDevice> &device) {
            logger.info("disconnected: PID={:#06x}", device->info().pid);
          },
      // .should_open = [](const auto &info, const auto &) { return info.vid == 0x1209; },
      .log_level = espp::Logger::Verbosity::INFO,
  });

  std::error_code ec;
  if (!host.initialize(ec)) {
    logger.error("Failed to initialize USB host: {}", ec.message());
    return;
  }
  logger.info("USB host ready; plug in a USB HID device.");

  while (true) {
    logger.debug("connected HID devices: {}", host.devices().size());
    std::this_thread::sleep_for(2s);
  }
}

Note

Only one `esppUsbHost` may exist at a time: the USB Host library and the HID class driver are global singletons. USB-OTG **host** mode is only available on the ESP32-S2, ESP32-S3 and ESP32-P4, and the board must be able to source VBUS to the attached device (a self-powered hub or a board with a VBUS switch); the host does not manage board power.

Note

Callbacks run on the dispatch task. Keep them reasonably short: a callback that blocks delays every later event (and `deinitialize()`).

Public Types

using device_callback_fn = std::function<void(const std::shared_ptr<HidDevice> &device)>

Callback invoked when a HID device is connected / disconnected.

using open_filter_fn = std::function<bool(const HidDevice::Info &info, const HidDevice::Params &params)>

Predicate deciding whether to open a newly attached HID interface. Return false to ignore it (no callbacks, not listed in devices()).

Public Functions

explicit UsbHost(const Config &config)

Construct a USB host. Call initialize() to actually install the stack.

~UsbHost()

Uninstall the stack (if still installed).

bool initialize(std::error_code &ec)

Install the USB Host library + HID class driver and start the tasks.

Parameters:

ec – Set on failure.

Returns:

true on success.

bool deinitialize(std::error_code &ec)

Uninstall the HID class driver + USB Host library and stop the tasks. Attached devices are closed (their disconnect callbacks fire, on the calling task) and the root port is powered down so the driver can release them. Must not be called from within a `UsbHost` callback.

Parameters:

ec – Set on failure. If any step of the teardown fails (a device the driver cannot release, or the library refusing to uninstall) the host stays initialized (is_initialized() remains true) and false is returned, rather than tearing down under a live driver. In that state event delivery has already stopped, every device has been retired, and the root port is left powered off (powering it back up would only make the driver re-track the device that a retry needs gone), so the host is not usable: the only valid next steps are to call deinitialize() again (which waits for the driver again) or to destroy the object, which aborts if teardown still fails (see the destructor).

Returns:

true on success.

inline bool is_initialized() const

Whether the host stack is installed.

std::vector<std::shared_ptr<HidDevice>> devices() const

Snapshot of the currently connected (opened) HID devices.

inline const std::string &get_name() const

Get the name of the component

Note

This is the tag of the logger

Returns:

A const reference to the name of the component

inline void set_log_tag(const std::string_view &tag)

Set the tag for the logger

Parameters:

tag – The tag to use for the logger

inline espp::Logger::Verbosity get_log_level() const

Get the log level for the logger

Returns:

The verbosity level of the logger

inline void set_log_level(espp::Logger::Verbosity level)

Set the log level for the logger

Parameters:

level – The verbosity level to use for the logger

inline void set_log_verbosity(espp::Logger::Verbosity level)

Set the log verbosity for the logger

See also

set_log_level

Note

This is a convenience method that calls set_log_level

Parameters:

level – The verbosity level to use for the logger

inline espp::Logger::Verbosity get_log_verbosity() const

Get the log verbosity for the logger

See also

get_log_level

Note

This is a convenience method that calls get_log_level

Returns:

The verbosity level of the logger

inline void set_log_rate_limit(std::chrono::duration<float> rate_limit)

Set the rate limit for the logger

Note

Only calls to the logger that have _rate_limit suffix will be rate limited

Parameters:

rate_limit – The rate limit to use for the logger

struct Config

Configuration for the USB host.

Public Members

device_callback_fn on_device_connected = {nullptr}

a HID device attached and opened

device_callback_fn on_device_disconnected = {nullptr}

a HID device detached

open_filter_fn should_open = {nullptr}

optional filter (default: open every HID interface)

bool auto_start = {true}

start receiving Input reports as soon as a device opens

size_t task_priority = {5}

priority of the internal tasks

int task_core_id = {-1}

core for the internal tasks (-1 = no affinity)

size_t lib_task_stack_size = {4096}

stack for the USB-host-library event task

size_t hid_task_stack_size = {4096}

stack for the HID class driver’s task (it only enqueues)

size_t dispatch_task_stack_size = {6 * 1024}

Stack for the dispatch task that runs the user callbacks (size it for what your callbacks do &#8212; logging with fmt, protocol work, …).

size_t max_input_report_size = {64}

Per-device Input-report copy size. A report larger than this is truncated (the driver copies at most this many bytes); raise it if your device sends larger reports. 64 covers full-speed HID.

size_t max_queued_events = {32}

Bound on queued-but-undispatched events. When the queue is full: a new Input report is dropped; a lifecycle event first evicts the oldest queued Input report, and if there is none a *new-device* event is dropped (that device simply stays unopened while the consumer is overloaded) while a *disconnect* is always kept (it is needed to release the device, and can only follow a device that was opened). The queue length is therefore never more than max_queued_events + the number of currently open devices, and the USB stack is never blocked. Drops are counted and logged at a rate-limited cadence.

class HidDevice

A HID interface on a device connected to the host.

Created by `UsbHost` when a HID device is attached; handed to the application (as a `std::shared_ptr`) through the connect / disconnect callbacks and `UsbHostdevices()`. Owns nothing itself &#8212; the underlying driver handle is owned by `UsbHost` &#8212; and becomes inert once the device is disconnected (methods then fail with `std::errc::no_such_device`; the identity accessors keep returning the values captured at connect time).

Public Types

using input_callback_fn = std::function<void(std::span<const uint8_t> data)>

Callback invoked with a raw HID Input report from the device.

Param data:

The report bytes. For a device whose report descriptor uses report IDs, byte 0 is the report ID (matching how `espp::UsbDevice`’s HID receive callback delivers OUT reports).

Public Functions

inline const Info &info() const

The identity of the connected device (captured at connect time, so it stays valid after a disconnect).

inline const Params &params() const

The parameters of this HID interface (captured at connect time).

inline const std::vector<uint8_t> &report_descriptor() const

The device’s HID report descriptor (captured at connect time; a copy owned by this object, not a view into driver memory).

void set_input_callback(input_callback_fn cb)

Install the callback invoked with each Input report. Install it from the connect callback: `UsbHost` invokes that *before* it starts the device, so no report is missed.

bool start(std::error_code &ec)

Start receiving Input reports (called automatically on open when `Config::auto_start` is set).

bool stop(std::error_code &ec)

Stop receiving Input reports.

bool send_output_report(uint8_t report_id, std::span<const uint8_t> data, std::error_code &ec)

Send a HID **Output** report to the device (host→device).

Parameters:
  • report_id – The report ID (0 if the descriptor is not report-ID’d).

  • data – The report payload (without the report-ID byte).

  • ec – Set on failure.

Returns:

true on success.

bool get_report(hid_report_type_t report_type, uint8_t report_id, std::span<uint8_t> buffer, size_t &out_length, std::error_code &ec)

Request a report from the device (HID class Get_Report).

Parameters:
  • report_type – The HID report type (HID_REPORT_TYPE_INPUT / _OUTPUT / _FEATURE).

  • report_id – The report ID.

  • buffer – Buffer that receives the report.

  • out_length – Number of bytes written into buffer.

  • ec – Set on failure.

bool set_idle(uint8_t duration, uint8_t report_id, std::error_code &ec)

Set the device’s idle rate (HID class Set_Idle).

bool set_protocol(hid_report_protocol_t protocol, std::error_code &ec)

Set the device’s HID protocol (boot vs report; HID class Set_Protocol).

inline bool is_connected() const

Whether the device is still connected/usable.

inline hid_host_device_handle_t handle() const

The underlying driver handle (for advanced use; only valid while is_connected()).

struct Info

Device descriptor identity (VID/PID + string descriptors).

Public Members

uint16_t vid = {0}

idVendor

uint16_t pid = {0}

idProduct

std::string manufacturer = {}

iManufacturer string (UTF-8, converted from the device’s UTF-16)

std::string product = {}

iProduct string (UTF-8, converted from the device’s UTF-16)

std::string serial_number = {}

iSerialNumber string (UTF-8, converted from the device’s UTF-16)

struct Params

HID interface parameters.

Public Members

uint8_t address = {0}

USB device address.

uint8_t interface_number = {0}

bInterfaceNumber of this HID interface

uint8_t sub_class = {0}

bInterfaceSubClass (1 = boot interface)

uint8_t protocol = {0}

bInterfaceProtocol (1 = keyboard, 2 = mouse, 0 = none)

class HidDevice

A HID interface on a device connected to the host.

Created by `UsbHost` when a HID device is attached; handed to the application (as a `std::shared_ptr`) through the connect / disconnect callbacks and `UsbHostdevices()`. Owns nothing itself &#8212; the underlying driver handle is owned by `UsbHost` &#8212; and becomes inert once the device is disconnected (methods then fail with `std::errc::no_such_device`; the identity accessors keep returning the values captured at connect time).

Public Types

using input_callback_fn = std::function<void(std::span<const uint8_t> data)>

Callback invoked with a raw HID Input report from the device.

Param data:

The report bytes. For a device whose report descriptor uses report IDs, byte 0 is the report ID (matching how `espp::UsbDevice`’s HID receive callback delivers OUT reports).

Public Functions

inline const Info &info() const

The identity of the connected device (captured at connect time, so it stays valid after a disconnect).

inline const Params &params() const

The parameters of this HID interface (captured at connect time).

inline const std::vector<uint8_t> &report_descriptor() const

The device’s HID report descriptor (captured at connect time; a copy owned by this object, not a view into driver memory).

void set_input_callback(input_callback_fn cb)

Install the callback invoked with each Input report. Install it from the connect callback: `UsbHost` invokes that *before* it starts the device, so no report is missed.

bool start(std::error_code &ec)

Start receiving Input reports (called automatically on open when `Config::auto_start` is set).

bool stop(std::error_code &ec)

Stop receiving Input reports.

bool send_output_report(uint8_t report_id, std::span<const uint8_t> data, std::error_code &ec)

Send a HID **Output** report to the device (host→device).

Parameters:
  • report_id – The report ID (0 if the descriptor is not report-ID’d).

  • data – The report payload (without the report-ID byte).

  • ec – Set on failure.

Returns:

true on success.

bool get_report(hid_report_type_t report_type, uint8_t report_id, std::span<uint8_t> buffer, size_t &out_length, std::error_code &ec)

Request a report from the device (HID class Get_Report).

Parameters:
  • report_type – The HID report type (HID_REPORT_TYPE_INPUT / _OUTPUT / _FEATURE).

  • report_id – The report ID.

  • buffer – Buffer that receives the report.

  • out_length – Number of bytes written into buffer.

  • ec – Set on failure.

bool set_idle(uint8_t duration, uint8_t report_id, std::error_code &ec)

Set the device’s idle rate (HID class Set_Idle).

bool set_protocol(hid_report_protocol_t protocol, std::error_code &ec)

Set the device’s HID protocol (boot vs report; HID class Set_Protocol).

inline bool is_connected() const

Whether the device is still connected/usable.

inline hid_host_device_handle_t handle() const

The underlying driver handle (for advanced use; only valid while is_connected()).

struct Info

Device descriptor identity (VID/PID + string descriptors).

Public Members

uint16_t vid = {0}

idVendor

uint16_t pid = {0}

idProduct

std::string manufacturer = {}

iManufacturer string (UTF-8, converted from the device’s UTF-16)

std::string product = {}

iProduct string (UTF-8, converted from the device’s UTF-16)

std::string serial_number = {}

iSerialNumber string (UTF-8, converted from the device’s UTF-16)

struct Params

HID interface parameters.

Public Members

uint8_t address = {0}

USB device address.

uint8_t interface_number = {0}

bInterfaceNumber of this HID interface

uint8_t sub_class = {0}

bInterfaceSubClass (1 = boot interface)

uint8_t protocol = {0}

bInterfaceProtocol (1 = keyboard, 2 = mouse, 0 = none)