Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,8 @@ jobs:
- path: 'components/usb_device/xinput_example'
target: esp32s3
command: 'IDF_COMPONENT_MANAGER=0 idf.py build'
- path: 'components/usb_host/example'
target: esp32s3
- path: 'components/vl53l/example'
target: esp32s3
- path: 'components/wifi/example'
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/upload_components.yml
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ jobs:
components/tt21100
components/twai
components/usb_device
components/usb_host
components/utils
components/vl53l
components/wifi
Expand Down
5 changes: 5 additions & 0 deletions components/usb_host/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
idf_component_register(
INCLUDE_DIRS "include"
SRC_DIRS "src"
REQUIRES base_component task usb usb_host_hid
)
119 changes: 119 additions & 0 deletions components/usb_host/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# USB Host Component

[![Badge](https://components.espressif.com/components/espp/usb_host/badge.svg)](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`).
- The USB Host library (`usb`) and `usb_host_hid` 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 the example
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 (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.

## 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.
33 changes: 33 additions & 0 deletions components/usb_host/example/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# The following lines of boilerplate have to be in your project's CMakeLists
# in this exact order for cmake to work correctly
cmake_minimum_required(VERSION 3.20)

include($ENV{IDF_PATH}/tools/cmake/project.cmake)

# add only the component directories that we want to use
set(EXTRA_COMPONENT_DIRS
"../../../components/base_component"
"../../../components/format"
"../../../components/logger"
"../../../components/task"
"../../../components/timer"
"../../../components/usb_host"
)

# The USB Host library (`usb`) and the HID class driver (`usb_host_hid`) are
# fetched from the ESP Component Registry by the IDF component manager (which is
# enabled by default). On ESP-IDF >= 6.0 the `usb_host_hid` component declares
# its `usb` dependency only through the manager, so unlike the device-side USB
# examples this one is built with the component manager **on** (the espp/*
# components are still resolved locally via EXTRA_COMPONENT_DIRS above).

set(
COMPONENTS
"main esptool_py base_component format logger task timer usb_host usb usb_host_hid"
CACHE STRING
"List of components to include"
)

project(usb_host_example)

set(CMAKE_CXX_STANDARD 20)
45 changes: 45 additions & 0 deletions components/usb_host/example/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# USB Host Example

This example uses `espp::UsbHost` to drive the ESP32-S3 USB-OTG peripheral as a
**USB host**. It enumerates attached USB **HID** devices (mice, keyboards,
gamepads, or vendor HID devices such as an espp `WdiUsbPeripheral`), logs each
connected device's identity and report-descriptor length, and hex-dumps every
Input report the device sends.

## How it works

- Constructs an `espp::UsbHost` with `on_device_connected` /
`on_device_disconnected` callbacks.
- On connect, reads the device `info()` (VID/PID + strings) and `params()`
(interface / protocol), fetches the HID `report_descriptor()`, and installs a
per-device input callback that logs each report.
- `initialize()` installs the USB Host library + HID class driver and starts the
event tasks; the device callbacks then fire as devices are plugged / unplugged.

## Hardware / build notes

- USB-OTG **host** mode requires an **ESP32-S2 / -S3 / -P4**, and 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).
- The native USB-OTG port is used for the host role, so the console runs on
**UART0** (USB-Serial-JTAG shares the PHY on the ESP32-S3). Monitor over a UART
adapter.
- The USB Host library (`usb`) and `usb_host_hid` come from the ESP Component
Registry, so build this example with the **component manager enabled** (the
default `idf.py build`), not the manager-off flow used by the device-side USB
examples.

## Use it with the WDI USB device

Flash the `wdi` component's `usb_example` onto a second ESP32-S3 (it enumerates
as a WDI HID device) and connect it to the host running this example — you will
see the WDI Control / keepalive Input reports arrive in the log. This is the
basis for the forthcoming WDI **host** (wheelchair) role.

## Build and flash

```
idf.py set-target esp32s3
idf.py build
idf.py -p PORT flash monitor
```
5 changes: 5 additions & 0 deletions components/usb_host/example/main/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
idf_component_register(
SRC_DIRS "."
INCLUDE_DIRS "."
REQUIRES usb_host
)
60 changes: 60 additions & 0 deletions components/usb_host/example/main/usb_host_example.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#include <chrono>
#include <thread>

#include "logger.hpp"
#include "usb_host.hpp"

using namespace std::chrono_literals;

// USB Host (HID) example: act as a USB host, enumerate attached HID devices
// (mice, keyboards, gamepads, or vendor HID devices such as an espp WDI
// peripheral) and log each device plus a hex dump of every Input report it
// sends. Plug the ESP32-S3 (in host mode) into a USB HID device and watch the
// monitor.
//
// NOTE: 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). The console
// runs on UART0 because the native USB-OTG port is used for the host role.

//! [usb_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);
}
}
//! [usb_host_example]
7 changes: 7 additions & 0 deletions components/usb_host/example/sdkconfig.defaults
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
CONFIG_IDF_TARGET="esp32s3"
CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192
# The native USB-OTG port is used for the USB **host** role. On the ESP32-S3 the
# USB-Serial-JTAG shares that PHY, so the console runs on UART0 (with
# USB-Serial-JTAG as an early-boot secondary). Use a UART adapter to monitor.
CONFIG_ESP_CONSOLE_UART_DEFAULT=y
CONFIG_ESP_CONSOLE_SECONDARY_USB_SERIAL_JTAG=y
26 changes: 26 additions & 0 deletions components/usb_host/idf_component.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
## IDF Component Manager Manifest File
license: "MIT"
description: "Native USB host (ESP-IDF USB Host library + usb_host_hid): enumerate and talk to USB HID devices (mice, keyboards, gamepads, vendor HID) from an ESP32-S2/-S3/-P4"
url: "https://github.com/esp-cpp/espp/tree/main/components/usb_host"
repository: "https://github.com/esp-cpp/espp.git"
maintainers:
- William Emfinger <waemfinger@gmail.com>
documentation: "https://esp-cpp.github.io/espp/usb_host/usb_host.html"
examples:
- path: example
tags:
- cpp
- Component
- USB
- Host
- HID
- Transport
- Gamepad
dependencies:
idf:
version: '>=5.0'
espp/base_component: '>=1.0'
# The USB Host library (`usb`) and the HID class driver come from esp-usb. For
# IDF >= 6.0 the `usb` component is provided by esp-usb (usb_host_hid pulls it
# in). Pin the tested floors.
espressif/usb_host_hid: '>=1.0.0'
Loading
Loading