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 viastd::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::UsbHostmay exist at a time (the USB Host library and HID class driver are global singletons). It cannot coexist withespp::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.
UsbHostdoes 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
usbandusb_host_hidcomponents come from the ESP Component Registry via the IDF component manager. On ESP-IDF ≥ 6.0usb_host_hiddeclares itsusbdependency 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 otherHidDevicemethods from insideon_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.
HidDevicemethods 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
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
See also
See also
- Returns:
The verbosity level of the logger
-
inline void set_log_level(espp::Logger::Verbosity level)
Set the log level for the logger
See also
See also
- 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
See also
See also
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
See also
See also
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
See also
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 — 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.
-
device_callback_fn on_device_connected = {nullptr}
-
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 — the underlying driver handle is owned by `UsbHost` — 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 ¶ms() 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)
-
uint16_t vid = {0}
-
struct Params
HID interface parameters.
-
using input_callback_fn = std::function<void(std::span<const uint8_t> data)>
-
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 — the underlying driver handle is owned by `UsbHost` — 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 ¶ms() 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)
-
uint16_t vid = {0}
-
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)
-
uint8_t address = {0}
-
using input_callback_fn = std::function<void(std::span<const uint8_t> data)>