# USB Host Component
[](https://components.espressif.com/components/espp/usb_host)
`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 espp wrapper over the ESP-IDF USB Host library (`usb`)
and the `usb_host_hid` class driver: it owns the whole host lifecycle (install
the host library + HID driver, run the event tasks, open interfaces, teardown)
and marshals the driver's C callbacks into per-device `std::function`s. Like the
rest of espp it does not throw and reports failures via `std::error_code`.
## 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
you only open the devices you care about (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 — symmetric with how
`espp::UsbDevice`'s HID receive callback delivers OUT reports).
- 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**.
Report directions are named from the connected **device's** point of view, as in
the USB HID spec: an *Input* report is device→host, an *Output* report is
host→device. This mirrors `espp::UsbDevice` exactly, so the two ends of a link
(e.g. the device and host roles of the `wdi` component) line up.
## Requirements / 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`).
- Targets with two USB-OTG peripherals (ESP32-P4) host on the library's default
(peripheral 0) unless `Config::port` picks one; `num_usb_devices()` counts
every enumerated device (HID or not), which tells a "nothing is wired to this
port" apart from a "device has no HID interface".
- The root port is powered only after the HID class driver has registered,
so a device already attached at boot is enumerated while someone is
listening (the library only reports devices that enumerate after a client
registers, and the HID driver never scans existing ones).
- Boards whose jack 5 V is switched by an IO expander rather than the USB
controller give the host a `Config::vbus_control` hook, so the jack can stay
off until the host is listening and is dropped at deinitialize(); a
`Config::root_port_power_on_delay` adds a margin before the first
enumeration after boot.
- Devices behind a **hub** need `Config::full_speed_only` on the ESP32-P4:
IDF's hub support has no transaction translator, so a full-speed device (every
HID keyboard / mouse / gamepad) behind a high-speed hub cannot be reached
("TT is not supported"). With the option set the root port runs in
full/low-speed-only mode, the hub enumerates at full speed, and its devices are
reachable directly. HID never needs more bandwidth than that. The setting is
re-applied on a timer (`Config::full_speed_reassert_interval`, 100ms by
default) because a root port recovery clears it without reporting an event; it
has no effect on the full-speed-only ESP32-S2 / -S3.
- The host runs three internal tasks (the USB Host library loop, the HID class
driver's event pump and the dispatch task). Where internal RAM is scarce,
`Config::task_stack_alloc_caps = MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT` puts
their stacks (~14 KB together) in PSRAM; it needs
`CONFIG_SPIRAM_ALLOW_STACK_EXTERNAL_MEMORY` and leaves the stacks in internal
RAM at its default of 0.
- Requires **ESP-IDF ≥ 5.4** (root-port power control in IDF's built-in USB
Host library). On **ESP-IDF ≥ 6.0** the USB Host library is the registry
`usb` component instead (≥ 1.3.0, whose `peripheral_map` is what
`Config::port` needs: `ESPP_USB_HOST_HAS_PORT_SELECT`), and `usb_host_hid`
declares that dependency only through the component manager, so build the
examples with the manager **on** (the default) rather than the manager-off
flow used by the device-side USB examples. On 5.x `Config::port` must stay
at its default.
## 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 (it times out). `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. Consequences:
- 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) — they complete normally.
- 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, so an app-task control transfer can't
race the device being retired on disconnect.
- Keep callbacks reasonably short: one that blocks delays every later event.
- The queue is bounded (`Config::max_queued_events`); if the consumer falls
behind, Input reports are dropped (logged) rather than blocking the USB stack.
`HidDevice::info()` / `params()` / `report_descriptor()` are snapshots taken at
connect time, so they remain valid after the device disconnects.
## Example
```cpp
#include "usb_host.hpp"
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);
```
See `example/` for a full runnable example (esp32s3) that logs every connected
HID device and hex-dumps its Input reports, and `tab5_example/` for one on the
M5Stack Tab5 that decodes keyboards, mice, gamepads and a SpaceMouse with the
`hid-rp` runtime report decoders and shows them on the screen.
## 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.
dd207f27d573f6a3bedae360227d5dc5862d614f
idf.py add-dependency "espp/usb_host^1.3.5"