USB Device Component
Overview
espp::UsbDevice is an idiomatic wrapper around ESP-IDF’s esp_tinyusb
managed component that assembles a native USB device from a set of
selectable functions on the ESP32-S3 / -S2 / -P4 USB-OTG peripheral, with a
configurable VID/PID and manufacturer / product / serial strings.
Today it can enable, in any combination (subject to the endpoint budget):
A CDC-ACM function (virtual serial port).
A vendor-specific function (
bInterfaceClass0xFF, one bulk IN + one bulk OUT) that carries a raw byte stream and optionally advertises WebUSB + MS OS 2.0 descriptors so a browser can talk to it driverlessly (and Windows binds WinUSB with no driver).A HID function (one interrupt IN, optionally one interrupt OUT) carrying an application-supplied report descriptor (for example a gamepad built with the espp
hid-rpcomponent), with input reports sent viawrite_hid_report().An X-Input function that presents the device as a wired Xbox 360 controller (a custom TinyUSB application class driver built into this component — no
CFG_TUD_*count needed). Gamepad state is sent withupdate_xinput_state()(seexinput.hpp) and rumble/LED reports arrive via anon_rumblecallback. Because the host’s XUSB driver only binds a recognized Xbox 360 VID/PID and the built-in vendor class also claims interface class 0xFF, use X-Input as the only enabled function (Microsoft’s IDs, for emulation / testing of your own device only).
Interface numbers, endpoint addresses and string indices are allocated
sequentially as functions are enabled, and the result is checked against the
USB-OTG endpoint budget (an error is reported via std::error_code if it is
exceeded). The model is designed so an MSC function can be added later
without changing the descriptor-building approach.
Because it uses the native USB-OTG peripheral rather than the built-in USB-Serial-JTAG that carries the ESP console, a device can advertise its own USB identifiers (for example ODrive-like ones) on a link that is fully separate from the logging console.
espp::UsbCdc is retained as a thin CDC-only preset over
espp::UsbDevice for back-compatibility.
Features
Composable: enable a CDC function and/or a vendor/WebUSB function and/or a HID function (composite)
Vendor-specific interface (class 0xFF) with a bulk IN + bulk OUT raw byte stream
HID interface with an application-supplied report descriptor (built with
hid-rpin the example) andwrite_hid_report()X-Input interface (wired Xbox 360 controller) via a custom application class driver, with
update_xinput_state()and anon_rumblecallbackConsole over CDC: optionally route the ESP console (stdout) to the CDC interface (
CdcFunction::route_consoleorroute_console_to_cdc()) so one native USB cable carries the logs alongside a vendor / HID / XInput interface; non-blocking, and teed to the primary UART console by defaultWebUSB: BOS descriptor + WebUSB URL descriptor + MS OS 2.0 descriptor for driverless browser access, with a configurable landing-page URL
Sequential interface / endpoint / string allocation with an endpoint-budget check
Configurable VID, PID, and manufacturer / product / serial / interface strings
No exceptions;
initialize()reports failures viastd::error_codeSafely marshals the TinyUSB RX callbacks (TinyUSB task context) into per-function user callbacks
Basic Usage
Composite CDC + vendor/WebUSB device, both interfaces carrying the same raw byte stream:
espp::UsbDevice::Config cfg;
cfg.vid = 0x1209; // pid.codes VID (ODrive uses this)
cfg.pid = 0x0d32; // ODrive-like PID
espp::UsbDevice::CdcFunction cdc;
cdc.on_receive = [&](std::span<const uint8_t> data) { /* handle serial rx */ };
cfg.cdc = cdc;
espp::UsbDevice::VendorFunction vendor;
vendor.webusb = true; // advertise WebUSB / MS OS 2.0 descriptors
// landing_page_url defaults to the espp docs-hosted ODrive WebUSB console
vendor.on_receive = [&](std::span<const uint8_t> data) { /* handle vendor rx */ };
cfg.vendor = vendor;
espp::UsbDevice usb(cfg);
std::error_code ec;
if (!usb.initialize(ec)) { /* handle ec (e.g. endpoint budget exceeded) */ }
uint8_t hello[] = {'h', 'i', '\n'};
usb.write_cdc(hello);
usb.write_vendor(hello);
CDC-only preset (unchanged API):
espp::UsbCdc::Config cfg;
cfg.vid = 0x1209;
cfg.pid = 0x0d32;
cfg.on_receive = [](std::span<const uint8_t> data) { /* handle rx */ };
espp::UsbCdc usb(cfg);
std::error_code ec;
if (!usb.initialize(ec)) { /* handle ec */ }
Enabling the vendor / WebUSB class
The vendor class is gated in esp_tinyusb behind a Kconfig option. To use the
vendor function you must set, in your project’s sdkconfig.defaults (in
addition to the CDC options if you also enable CDC):
CONFIG_TINYUSB_CDC_ENABLED=y
CONFIG_TINYUSB_CDC_COUNT=1
CONFIG_TINYUSB_VENDOR_COUNT=1 # THE key enablement: compiles in the vendor class
Setting CONFIG_TINYUSB_VENDOR_COUNT greater than 0 makes esp_tinyusb
define CFG_TUD_VENDOR and compile the TinyUSB vendor class driver. If the
vendor function is requested but CFG_TUD_VENDOR == 0, initialize() fails
with std::errc::function_not_supported. No custom tusb_config is required;
the BOS descriptor and the WebUSB / MS-OS-2.0 vendor control requests are provided
by espp::UsbDevice via the standard TinyUSB weak-callback overrides.
Enabling the HID class
Like the vendor class, the HID class is gated in esp_tinyusb behind a Kconfig
option. To use the HID function you must set, in your project’s
sdkconfig.defaults:
CONFIG_TINYUSB_HID_COUNT=1 # compiles in the TinyUSB HID class driver (CFG_TUD_HID)
espp::UsbDevice provides the required TinyUSB HID weak-callback overrides:
tud_hid_descriptor_report_cb returns the stored report descriptor and
tud_hid_get_report_cb returns 0. Supply the report-descriptor bytes yourself
(the example builds them with the espp hid-rp component), assign them to
HidFunction::report_descriptor, and send input reports with
write_hid_report(report_id, report).
To receive host→device OUTPUT / SET_REPORT reports (for request/response HID
protocols such as the Nintendo Switch Pro controller handshake), set
HidFunction::on_receive (or set_hid_receive_callback()) and set
HidFunction::has_out_endpoint for interrupt-OUT reports. The callback is
invoked from the TinyUSB task with the report id as byte 0 of its span; reply by
sending an INPUT report with write_hid_report(). If the HID function is
requested but CFG_TUD_HID == 0, initialize() fails with
std::errc::function_not_supported.
Enabling X-Input (Xbox 360)
X-Input needs no CFG_TUD_* count — it is served by a custom TinyUSB
application class driver built into this component (registered via the weak
usbd_app_driver_get_cb, forced into the link with -u). An X-Input-only
project therefore enables no built-in USB class; the xinput_example disables
them all (CONFIG_TINYUSB_CDC_ENABLED=n). Keep CFG_TUD_VENDOR at 0 so the
built-in bulk vendor driver does not claim the X-Input 0xFF interface, and use
X-Input as the only enabled function (it then advertises the Xbox 360 identity
+ 0xFF/0xFF/0xFF device class so the host’s XUSB driver binds it). Send gamepad
state with update_xinput_state() and receive rumble/LED via on_rumble. The
interface uses one interrupt-IN (0x81) + one interrupt-OUT endpoint with separate
endpoint numbers, and the report DMA buffers are word-aligned as the ESP32-S3 DWC2
requires.
Routing the console over CDC
When the native USB port is given to TinyUSB for a vendor / HID / XInput interface, the ESP console can no longer live on USB-Serial-JTAG (on the ESP32-S3 it shares the USB-OTG PHY, so it contends and reboot-loops the device). Add a CDC function and route the console to it, and one native USB cable carries both the logs and the other interface:
espp::UsbDevice::CdcFunction cdc;
cdc.route_console = true; // redirect stdout -> CDC at the end of initialize()
// cdc.tee_console = true; // (default) also keep the primary UART console
usb_cfg.cdc = cdc;
usb_cfg.vendor = my_vendor; // CDC is just the log channel
espp::UsbDevice usb(usb_cfg);
usb.initialize(ec); // console now on CDC (teed to UART)
Or call usb.route_console_to_cdc() yourself after a successful initialize().
printf / ESP_LOG / espp::Logger all write to stdout, which is
freopen``ed onto a tiny write-only VFS device; its writes forward to
``write_cdc() only when the whole chunk fits the TX FIFO (never blocking on an
absent reader, and not gated on DTR) and, with tee_console (default), are also
written to the primary UART console so idf.py monitor keeps working. Recommended
console config: UART0 primary (CONFIG_ESP_CONSOLE_UART_DEFAULT) with
USB-Serial-JTAG as the secondary console for early-boot logs. The ota example
uses this.
Endpoint budget (ESP32-S3 USB-OTG)
The ESP32-S3 (and -S2) USB-OTG core is full-speed and, besides the control endpoint EP0, provides roughly 5 usable data IN endpoints and 5 usable data OUT endpoints. Each function consumes:
Function |
IN endpoints |
OUT endpoints |
|---|---|---|
CDC-ACM |
2 (1 interrupt-IN notification + 1 bulk-IN) |
1 (bulk-OUT) |
Vendor / WebUSB |
1 (bulk-IN) |
1 (bulk-OUT) |
HID |
1 (interrupt-IN) |
0 or 1 (optional interrupt-OUT) |
X-Input (Xbox 360) |
1 (interrupt-IN) |
1 (interrupt-OUT) |
MSC (future) |
1 (bulk-IN) |
1 (bulk-OUT) |
This is why the device is selectable (“not all at once”). Combinations that fit comfortably:
CDC + Vendor: 3 IN / 2 OUT (used by the example)
CDC + Vendor + HID: 4 IN / 2-3 OUT
CDC + Vendor + MSC: 4 IN / 3 OUT
Enabling CDC + Vendor + HID + MSC together reaches 5 IN endpoints, which is at the
hard limit and is not recommended. espp::UsbDevice computes the totals as
functions are enabled and returns std::errc::value_too_large if the IN or OUT
budget is exceeded.
Extending with MSC
The HID function is implemented (see “Enabling the HID class” above): it
appends one HID interface (application-supplied report descriptor) claiming an
interrupt-IN endpoint, plus an optional interrupt-OUT endpoint.
espp::UsbDevice::Config still reserves a std::optional slot for an
MscFunction as a documented extension point; it is not implemented yet, and
enabling it today makes initialize() fail with
std::errc::function_not_supported. When implemented it slots into the same
sequential interface / endpoint / string allocator: an MSC function appends one
MSC interface (SCSI + storage read/write/capacity callbacks) claiming a bulk IN +
bulk OUT endpoint.
Notes
USB-OTG is only available on the ESP32-S2, ESP32-S3 and ESP32-P4 targets.
Only one
espp::UsbDevice/espp::UsbCdcinstance may exist at a time (the TinyUSB stack and the BOS / vendor control callbacks are global).The receive callbacks run in the TinyUSB device task; keep them short and non-blocking. It is safe to call the matching
write_*()from within them.The TinyUSB device lifecycle callbacks (
tud_mount_cb/tud_umount_cb/tud_suspend_cb/tud_resume_cb) are owned byesp_tinyusb. Register mount / unmount handlers viaset_mount_callback()/set_unmount_callback()rather than defining those callbacks yourself (which would be a duplicate symbol). On unmount the component clears the vendor + CDC TX FIFOs — so a departed host’s queued backlog is not delivered to the next host that mounts — before invoking your callback; both handlers run in the TinyUSB device task.The WebUSB landing-page URL is configured without a scheme; the scheme is encoded separately via
VendorFunction::url_scheme(0 = http, 1 = https).
API Reference
Header File
Classes
-
class UsbDevice : public espp::BaseComponent
Public Types
-
using receive_callback_fn = std::function<void(std::span<const uint8_t> data)>
Callback invoked with received bytes.
- Param data:
Span of received bytes (valid only for the duration of the call).
-
using event_callback_fn = std::function<void()>
Callback for a device lifecycle event (mount / unmount). Invoked in the TinyUSB device-task context.
Public Functions
-
explicit UsbDevice(const Config &config)
Construct a UsbDevice. Does not touch hardware until initialize().
- Parameters:
config – Configuration parameters.
-
~UsbDevice()
Uninstalls the enabled functions and the TinyUSB driver if initialized.
-
bool initialize(std::error_code &ec)
Install the TinyUSB driver and initialize the enabled functions using the configured descriptors / VID-PID / strings.
- Parameters:
ec – [out] Set on failure (invalid config, endpoint budget exceeded, driver install failure, or unsupported function requested).
- Returns:
true on success, false otherwise (ec is set).
-
bool write_cdc(std::span<const uint8_t> data, std::error_code &ec)
Queue bytes for transmission over the CDC function and flush.
Note
Same backpressure contract as write_vendor(). A frame that fits in the TX FIFO (CONFIG_TINYUSB_CDC_TX_BUFSIZE) is written ALL-OR-NOTHING: the call sleep-waits (bounded, 250 ms) for room for the WHOLE frame and then enqueues it in a single write, so a drain-timeout or a mid-write disconnect returns false WITHOUT leaving a truncated prefix on the wire (a partial frame would poison the host-side framing parser). When called from TinyUSB-callback context (e.g. inside a receive callback, which runs on the TinyUSB task) the drain can never happen while this call blocks, so it fails fast with `no_buffer_space` if the whole frame does not ALREADY fit - again without enqueueing anything. A frame LARGER than the FIFO cannot be atomic and is streamed across drains (a mid-stream timeout may leave a prefix on the wire); keep framed payloads within the FIFO, or send large replies from your own task rather than a receive callback, for atomic writes.
- Parameters:
data – Bytes to send.
ec – [out] Set on failure (e.g. CDC not enabled / not initialized, or the TX FIFO could not accept all bytes - see note below).
- Returns:
true if all bytes were queued, false otherwise.
-
bool write_cdc(std::span<const uint8_t> data)
Convenience overload of write_cdc() that ignores errors.
-
bool write_vendor(std::span<const uint8_t> data, std::error_code &ec)
Queue bytes for transmission over the vendor function and flush.
Note
A frame that fits in the TX FIFO (CONFIG_TINYUSB_VENDOR_TX_BUFSIZE) is written ALL-OR-NOTHING: the call sleep-waits (bounded, 250 ms) for room for the WHOLE frame and then enqueues it in a single write, so a drain-timeout or a mid-write unmount returns false WITHOUT leaving a truncated prefix on the wire (a partial frame would poison the host-side framing parser). When called from TinyUSB-callback context (e.g. inside a receive callback, which runs on the TinyUSB task) the drain can never happen while this call blocks, so it fails fast with `no_buffer_space` if the whole frame does not ALREADY fit - again without enqueueing anything. A frame LARGER than the FIFO cannot be atomic and is streamed across drains (a mid-stream timeout may leave a prefix on the wire); keep framed payloads within the FIFO, or send large replies from your own task rather than a receive callback, for atomic writes.
- Parameters:
data – Bytes to send.
ec – [out] Set on failure (e.g. vendor not enabled / not initialized, or the TX FIFO could not accept all bytes - see note below).
- Returns:
true if all bytes were queued, false otherwise.
-
bool write_vendor(std::span<const uint8_t> data)
Convenience overload of write_vendor() that ignores errors.
-
size_t vendor_write_available() const
Bytes of free space currently in the vendor TX FIFO.
- Returns:
How many bytes write_vendor() can accept right now without blocking, or 0 if not initialized / no vendor interface / not mounted. A point-in-time hint: with a single serialized writer it is stable, otherwise treat it as advisory. Use it to skip or defer a streaming frame when the host has stopped draining the endpoint, instead of building the frame and having write_vendor() drop it.
-
size_t cdc_write_available() const
Bytes of free space currently in the CDC TX FIFO.
- Returns:
How many bytes write_cdc() can accept right now, or 0 if not initialized / no CDC interface / not mounted. See vendor_write_available() for usage notes.
-
void vendor_write_clear()
Discard any bytes queued in the vendor TX FIFO that have not been sent yet. Call this when the host goes away (e.g. on a detected disconnect / stream stall) so a stale backlog (queued telemetry) is not delivered to the next host that connects and mis-parsed as a reply to its first command.
-
void cdc_write_clear()
Discard any bytes queued in the CDC TX FIFO that have not been sent yet. See vendor_write_clear() for usage notes.
-
bool route_console_to_cdc(std::error_code &ec)
Redirect the ESP console (stdout) to the CDC interface, so the device’s logs travel over the same native USB cable as the other USB interface(s) (vendor / HID / XInput). Call this AFTER a successful `initialize()`; or just set `CdcFunction::route_console` and it is done for you at the end of `initialize()`.
`printf`, `ESP_LOG` (via its default vprintf), and `espp::Logger` (which uses `fmt::print`) all write to `stdout`, so redirecting stdout captures them all. A small write-only VFS device is registered and `stdout` is `freopen`ed onto it; its writes forward to `write_cdc()` only when the CDC TX FIFO can take the whole chunk right now, so logging NEVER blocks on an absent or slow reader (dropped console bytes are harmless). When `CdcFunction::tee_console` is set (the default) and the primary console is a UART, writes are also teed to that UART so `idf.py monitor` keeps working.
Idempotent (a second call is a no-op). Requires the CDC function to be enabled and the device initialized.
Note
Lifetime: routing points `stdout` at this device. On destruction the device detaches itself (later stdout writes degrade to the UART tee), but a write already in flight can still race destruction — so a console-routed UsbDevice must outlive concurrent logging. This is normally trivial: it is a program-lifetime singleton.
- Parameters:
ec – [out] Set on failure (CDC not enabled / not initialized, or the VFS device could not be registered / stdout could not be reopened).
- Returns:
true if the console is now routed to CDC (or already was).
-
bool route_console_to_cdc()
Convenience overload of route_console_to_cdc() that ignores errors.
-
bool is_console_routed_to_cdc() const
Whether the console is currently routed to the CDC interface.
-
bool write_hid_report(uint8_t report_id, std::span<const uint8_t> report, std::error_code &ec)
Send a HID input report on the HID function’s interrupt IN endpoint.
- Parameters:
report_id – HID report id (0 if the report descriptor has no report id; otherwise the id baked into the descriptor, e.g. 1 for the gamepad).
report – Report payload bytes (without the report-id prefix).
ec – [out] Set on failure (HID not enabled / not initialized, host not ready, or the HID class driver is not compiled in).
- Returns:
true if the report was queued for transmission, false otherwise.
-
bool write_hid_report(uint8_t report_id, std::span<const uint8_t> report)
Convenience overload of write_hid_report() that ignores errors.
-
bool is_hid_ready() const
Whether the HID function is enabled, mounted and ready to accept a new input report (no report in flight).
-
bool update_xinput_state(const espp::xinput::GamepadState &state, std::error_code &ec)
Send a fresh X-Input (Xbox 360) input report from a gamepad state.
Note
Single-writer: call from one task. The report bytes are held in an internal buffer for the duration of the (asynchronous) transfer.
- Parameters:
state – Buttons / triggers / sticks to serialize into the 20-byte report.
ec – [out] Set on failure (XInput not enabled / not initialized, host not ready / a previous report still in flight, or a transfer error).
- Returns:
true if the report was queued for transmission, false otherwise.
-
bool update_xinput_state(const espp::xinput::GamepadState &state)
Convenience overload of update_xinput_state() that ignores errors.
-
bool is_xinput_ready() const
Whether the XInput function is enabled, mounted and ready to accept a new input report (no report in flight).
-
void set_cdc_receive_callback(const receive_callback_fn &cb)
Set or replace the CDC receive callback (nullptr to detach).
-
void set_vendor_receive_callback(const receive_callback_fn &cb)
Set or replace the vendor receive callback (nullptr to detach).
-
void set_hid_receive_callback(const receive_callback_fn &cb)
Set or replace the HID receive callback (received OUTPUT / SET_REPORT bytes, host -> device; nullptr to detach).
-
void set_mount_callback(const event_callback_fn &cb)
Register a callback invoked when the device is mounted (the host has configured it). Runs in the TinyUSB device-task context; nullptr detaches. esp_tinyusb owns the raw tud_mount_cb, so applications should register here rather than defining that callback themselves.
-
void set_unmount_callback(const event_callback_fn &cb)
Register a callback invoked when the device is unmounted (detached / re-enumerated). The component clears the vendor + CDC TX FIFOs before invoking it. Runs in the TinyUSB device-task context; nullptr detaches. Register here instead of defining tud_umount_cb (esp_tinyusb already defines it).
-
bool is_initialized() const
Whether initialize() has completed successfully.
-
bool is_cdc_connected() const
Whether the CDC function is enabled and a host has asserted DTR.
-
bool is_vendor_connected() const
Whether the vendor function is enabled and the device is mounted.
-
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 CdcFunction
CDC-ACM (virtual serial port) function.
Consumes 1 interrupt IN (notification) + 1 bulk IN + 1 bulk OUT endpoint (across two USB interfaces joined by an IAD).
Public Members
-
std::string interface_name = {"espp CDC"}
CDC interface string descriptor.
-
receive_callback_fn on_receive = {nullptr}
Callback invoked with received bytes.
-
size_t rx_chunk_size = {64}
Buffer size used to drain the CDC RX FIFO per read.
-
bool route_console = {false}
Route the ESP console to this CDC interface once `initialize()` succeeds (equivalent to calling `route_console_to_cdc()` yourself).
The native USB port is often handed to TinyUSB for a vendor / HID / XInput interface, which on the ESP32-S3 means the console can no longer live on USB-Serial-JTAG (it shares that USB PHY). Enabling this redirects the console (stdout — `printf`, `ESP_LOG`, and `espp::Logger`’s `fmt::print` all default there) to this CDC interface, so a single native USB cable carries both the logs and the other interface(s). Writes are non-blocking and are dropped when no host is draining the CDC endpoint.
-
bool tee_console = {true}
When `route_console` (or `route_console_to_cdc()`) redirects the console, also keep writing it to the ORIGINAL console (a tee), so `idf.py monitor` on the primary UART keeps working and nothing is lost when no CDC host is attached. Best-effort: teeing is only done when the primary console is a UART (it has an independent port); with a USB-Serial-JTAG or no console there is nothing to tee to.
-
std::string interface_name = {"espp CDC"}
-
struct Config
Configuration for the composable UsbDevice.
Public Members
-
uint16_t vid = {0x1209}
USB Vendor ID (defaults to the pid.codes VID used by ODrive).
-
uint16_t pid = {0x0d32}
USB Product ID (defaults to an ODrive-like PID).
-
std::string manufacturer = {"espp"}
Manufacturer string descriptor.
-
std::string product = {"espp USB Device"}
Product string descriptor.
-
std::string serial_number = {"000000000001"}
Serial number string descriptor.
-
std::optional<CdcFunction> cdc = {}
Enable a CDC-ACM function.
-
std::optional<VendorFunction> vendor = {}
Enable a vendor-specific / WebUSB function.
-
std::optional<HidFunction> hid = {}
Enable a HID function.
-
std::optional<XInputFunction> xinput = {}
Enable an X-Input (Xbox 360) function.
-
std::optional<MscFunction> msc = {}
(Future) enable an MSC function.
-
uint16_t vid = {0x1209}
-
struct HidFunction
HID (Human Interface Device) function.
A HID function consumes 1 interrupt IN endpoint (and optionally 1 interrupt OUT if `has_out_endpoint` is set). It advertises the application-supplied `report_descriptor` bytes (the TinyUSB HID class driver returns them from `tud_hid_descriptor_report_cb`), and input reports are sent with `UsbDevicewrite_hid_report()`. The descriptor bytes are typically built with the espp `hid-rp` component (e.g. `espp::GamepadInputReport`); the component itself stays descriptor-bytes based and does not depend on hid-rp.
Requires the TinyUSB HID class driver to be compiled in (`CONFIG_TINYUSB_HID_COUNT` > 0, which defines `CFG_TUD_HID`); otherwise enabling this function makes `initialize()` fail with `std::errc::function_not_supported`.
Public Members
-
std::string interface_name = {"espp HID"}
HID interface string descriptor.
-
std::vector<uint8_t> report_descriptor = {}
HID report descriptor bytes.
-
bool has_out_endpoint = {false}
Whether to allocate an interrupt OUT endpoint.
-
receive_callback_fn on_receive = {nullptr}
Callback invoked with received HID OUTPUT / SET_REPORT bytes (host -> device). Enables request/response HID protocols (e.g. the Nintendo Switch Pro controller handshake): reply by sending an INPUT report with `write_hid_report()`. When the report descriptor uses report IDs, byte 0 of the delivered span is the report id. Delivered from the TinyUSB device task; `write_hid_report()` is safe to call from within it. Requires `has_out_endpoint` for interrupt-OUT reports (control SET_REPORT is delivered regardless).
-
std::string interface_name = {"espp HID"}
-
struct MscFunction
(Future) MSC (mass storage) function extension point. Not implemented yet.
An MSC function consumes 1 bulk IN + 1 bulk OUT endpoint and requires SCSI + storage callbacks (read10 / write10 / inquiry / capacity). Enabling it today makes initialize() fail with `std::errc::function_not_supported`.
-
struct VendorFunction
Vendor-specific function (bInterfaceClass 0xFF) carrying a raw byte stream over one bulk IN + one bulk OUT endpoint.
When `webusb` is true a BOS descriptor advertising the WebUSB platform capability (with `webusb_vendor_code` + landing-page index 1) and an MS OS 2.0 platform capability (with `ms_os_vendor_code`, so Windows binds WinUSB automatically with no driver) is exposed, and the WebUSB URL / MS-OS-2.0 descriptor vendor control requests are answered.
Public Members
-
std::string interface_name = {"espp Vendor"}
Vendor interface string descriptor.
-
receive_callback_fn on_receive = {nullptr}
Callback invoked with received bytes.
-
size_t rx_chunk_size = {64}
Buffer size used to drain the vendor RX FIFO per read.
-
bool webusb = {true}
Advertise WebUSB + MS OS 2.0 descriptors for driverless access.
-
std::string landing_page_url = {"esp-cpp.github.io/espp/apps/board_console.html"}
WebUSB landing-page URL. When `url_scheme` is 0 (http) or 1 (https) the URL must be given *without* a scheme (the scheme is prepended by the host from `url_scheme`). When `url_scheme` is 255 the URL must instead *include* its own scheme (e.g. “http://…”). Defaults to the espp docs-hosted board console + ESP flasher (scheme-less, https), a general-purpose Web Serial monitor and esptool-js flasher.
Note
The descriptor length (3 + URL bytes) must fit a uint8_t, so the URL is limited to 252 bytes; `initialize()` rejects a longer URL.
-
uint8_t url_scheme = {1}
0 = http, 1 = https, 255 = URL includes its own scheme.
-
uint8_t webusb_vendor_code = {1}
bRequest used for the WebUSB URL control request.
-
uint8_t ms_os_vendor_code{2}
bRequest used for the MS OS 2.0 descriptor control request.
-
std::string interface_name = {"espp Vendor"}
-
struct XInputFunction
X-Input (Xbox 360 wired controller) function.
Presents a vendor-specific interface (bInterfaceClass 0xFF / SubClass 0x5D / Protocol 0x01) with one interrupt IN endpoint (20-byte input reports, sent with `UsbDeviceupdate_xinput_state()`) and one interrupt OUT endpoint (8-byte rumble / LED reports, delivered to `on_rumble`). Unlike HID it is served by a small custom TinyUSB application class driver built into this component (no `CFG_TUD_*` count is required).
A PC’s XUSB driver only binds a device whose VID/PID is a recognized Xbox 360 controller, so `vid` / `pid` default to Microsoft’s identifiers (`0x045E:0x028E`) — for emulation / testing of your own device only. When the XInput function is the ONLY enabled function these identifiers (and a 0xFF/0xFF/0xFF device class) override the top-level Config vid/pid so the host recognizes it; combine XInput with other functions only if you do not need XUSB to bind (the built-in vendor/WebUSB class also claims class 0xFF).
Consumes 1 interrupt IN + 1 interrupt OUT endpoint.
Public Members
-
std::string interface_name = {"espp XInput"}
XInput interface string descriptor.
-
uint16_t vid = {espp::xinput::kDefaultVid}
Xbox 360 controller VID (Microsoft).
-
uint16_t pid = {espp::xinput::kDefaultPid}
Xbox 360 controller PID.
-
receive_callback_fn on_rumble = {nullptr}
Callback invoked with received rumble / LED report bytes (8-byte reports on the interrupt OUT endpoint). Runs in the TinyUSB device task.
-
std::string interface_name = {"espp XInput"}
-
using receive_callback_fn = std::function<void(std::span<const uint8_t> data)>
Header File
Classes
-
class UsbCdc : public espp::BaseComponent
Native USB CDC-ACM transport: a thin CDC-only preset over `espp::UsbDevice`.
`esppUsbCdc` presents a single dedicated CDC-ACM (virtual serial port) interface on the native USB peripheral with a *configurable* VID/PID and manufacturer / product / serial strings. It is kept for back-compatibility and is implemented on top of the composable `espp::UsbDevice` (which can also add a vendor-specific / WebUSB interface, HID, MSC, …). For anything beyond a plain serial port, prefer `espp::UsbDevice` directly.
Incoming bytes are delivered to a user callback and outgoing bytes are sent via write(). The class does not throw and reports initialization failures via `std::error_code`.
UsbCdc Example
// One simulated motor state, shared by both protocol servers. struct { std::atomic<float> vbus{24.0f}; std::atomic<float> position{0.0f}; std::atomic<float> velocity{0.0f}; std::atomic<float> torque{0.0f}; std::atomic<float> vel_limit{20.0f}; std::atomic<uint32_t> error{0}; std::atomic<uint64_t> serial{0xA1B2C3D4E5ULL}; } state; // --- ASCII protocol server (CDC / terminal / WebSerial) ------------------- OdriveAscii::Config ascii_cfg; ascii_cfg.log_level = Logger::Verbosity::WARN; OdriveAscii ascii(ascii_cfg); ascii.register_float_property( "axis0.encoder.pos_estimate", [&]() { return state.position.load(); }, [&](float v, std::error_code &ec) { ec.clear(); state.position = v; return true; }); ascii.register_float_property("axis0.encoder.vel_estimate", [&]() { return state.velocity.load(); }); ascii.register_float_property( "axis0.controller.input_pos", [&]() { return state.position.load(); }, [&](float v, std::error_code &ec) { ec.clear(); state.position = v; return true; }); ascii.on_position_command([&](int axis, float pos, std::optional<float> vel_ff, std::optional<float> torque_ff, std::error_code &ec) { (void)axis; ec.clear(); state.position = pos; if (vel_ff.has_value()) state.velocity = *vel_ff; if (torque_ff.has_value()) state.torque = *torque_ff; return true; }); ascii.on_feedback_request([&](int axis, float &pos_out, float &vel_out, std::error_code &ec) { (void)axis; ec.clear(); pos_out = state.position.load(); vel_out = state.velocity.load(); return true; }); // --- Native (Fibre) protocol server (vendor interface / odrivetool) ------- // Register an ODrive-style endpoint tree; odrivetool / the fibre library // download this tree from endpoint 0 and read/write it by numeric id. OdriveNative::Config native_cfg; native_cfg.log_level = Logger::Verbosity::WARN; OdriveNative native(native_cfg); native.register_float_property("vbus_voltage", [&]() { return state.vbus.load(); }); native.register_uint32_property("axis0.error", [&]() { return state.error.load(); }); native.register_float_property("axis0.encoder.pos_estimate", [&]() { return state.position.load(); }); native.register_float_property("axis0.encoder.vel_estimate", [&]() { return state.velocity.load(); }); native.register_float_property( "axis0.controller.input_pos", [&]() { return state.position.load(); }, [&](float v, std::error_code &ec) { ec.clear(); state.position = v; return true; }); native.register_float_property( "axis0.controller.config.vel_limit", [&]() { return state.vel_limit.load(); }, [&](float v, std::error_code &ec) { ec.clear(); state.vel_limit = v; return true; }); native.register_uint64_property("serial_number", [&]() { return state.serial.load(); }); // --- Composite native USB device ------------------------------------------ UsbDevice::Config usb_cfg; usb_cfg.vid = 0x1209; // pid.codes VID used by ODrive usb_cfg.pid = 0x0d32; // ODrive v3-like PID usb_cfg.manufacturer = "espp"; usb_cfg.product = "espp ODrive"; usb_cfg.serial_number = "0001"; usb_cfg.log_level = Logger::Verbosity::INFO; UsbDevice::CdcFunction cdc; cdc.interface_name = "espp ODrive ASCII (CDC)"; usb_cfg.cdc = cdc; UsbDevice::VendorFunction vendor; vendor.interface_name = "espp ODrive native (Fibre)"; vendor.webusb = true; // advertise BOS / WebUSB / MS OS 2.0 descriptors usb_cfg.vendor = vendor; // HID gamepad function. The report descriptor is built with the espp hid-rp // component: wrap espp::GamepadInputReport's descriptor fragment in a // generic-desktop GAMEPAD application collection and serialize it to bytes. static constexpr uint8_t kHidReportId = 1; static constexpr size_t kNumButtons = 15; using Gamepad = espp::GamepadInputReport<kNumButtons, std::uint16_t, std::uint16_t, 0, 65535, 0, 1023, kHidReportId>; Gamepad gamepad; gamepad.reset(); std::vector<uint8_t> hid_report_descriptor; { using namespace hid::page; using namespace hid::rdf; auto raw_descriptor = descriptor(usage_page<generic_desktop>(), usage(generic_desktop::GAMEPAD), collection::application(gamepad.get_descriptor())); hid_report_descriptor.assign(raw_descriptor.begin(), raw_descriptor.end()); } logger.info("HID gamepad report descriptor: {} bytes", hid_report_descriptor.size()); UsbDevice::HidFunction hid; hid.interface_name = "espp Gamepad HID"; hid.report_descriptor = hid_report_descriptor; usb_cfg.hid = hid; UsbDevice usb(usb_cfg); // CDC RX -> ASCII protocol -> CDC write. usb.set_cdc_receive_callback([&](std::span<const uint8_t> data) { auto response = ascii.process_bytes(data); if (!response.empty()) usb.write_cdc(response); }); // Vendor RX -> native (Fibre) protocol -> vendor write. // // The Fibre packet protocol over USB relies on USB transfer boundaries: each // host bulk-OUT transfer is exactly one packet. odrivetool's requests are // small (< 64 B), so each vendor RX callback delivers one whole packet, which // is what process_bytes() expects. (If a future client sent packets larger // than a single bulk transfer, this callback would need a length-based // reassembly step.) usb.set_vendor_receive_callback([&](std::span<const uint8_t> data) { auto response = native.process_bytes(data); if (!response.empty()) usb.write_vendor(response); }); std::error_code ec; if (!usb.initialize(ec)) { logger.error("Failed to initialize USB device: {}", ec.message()); return; } logger.info("ODrive-compatible native USB device ready:"); logger.info(" CDC serial interface -> ODrive ASCII (terminal / WebSerial)"); logger.info(" vendor interface (WebUSB) -> ODrive native/Fibre (odrivetool over USB)"); logger.info(" HID interface -> animated gamepad (visualize with hid_visualizer.html)"); logger.info("Native endpoint tree ({} bytes, json_crc=0x{:04x})", native.json().size(), native.json_crc());
Note
Only one `esppUsbCdc` / `espp::UsbDevice` instance may exist at a time. USB-OTG is only available on the ESP32-S2, ESP32-S3 and ESP32-P4 targets.
Note
The receive callback is invoked from the TinyUSB device task. Keep it short and non-blocking; it is safe to call write() from within it.
Public Types
-
using receive_callback_fn = std::function<void(std::span<const uint8_t> data)>
Callback invoked with received bytes.
- Param data:
Span of received bytes (valid only for the duration of the call).
Public Functions
-
explicit UsbCdc(const Config &config)
Construct a UsbCdc transport. Does not touch hardware until initialize() is called.
- Parameters:
config – Configuration parameters.
-
~UsbCdc()
Uninstalls the CDC-ACM interface and TinyUSB driver if initialized.
-
bool initialize(std::error_code &ec)
Install the TinyUSB driver and initialize the CDC-ACM interface using the configured descriptors / VID-PID / strings.
- Parameters:
ec – [out] Set on failure.
- Returns:
true on success, false otherwise (ec is set).
-
bool write(std::span<const uint8_t> data, std::error_code &ec)
Queue bytes for transmission over the CDC interface and flush.
- Parameters:
data – Bytes to send.
ec – [out] Set on failure (e.g. not initialized).
- Returns:
true if all bytes were queued, false otherwise.
-
bool write(std::span<const uint8_t> data)
Convenience overload of write() that ignores errors.
- Parameters:
data – Bytes to send.
- Returns:
true if all bytes were queued, false otherwise.
-
void set_receive_callback(const receive_callback_fn &cb)
Set or replace the receive callback.
- Parameters:
cb – Callback to invoke with received bytes (may be nullptr to detach).
-
bool is_initialized() const
Whether initialize() has completed successfully.
-
bool is_connected() const
Whether a USB host has opened (asserted DTR on) the CDC port.
-
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 UsbCdc transport.
Public Members
-
uint16_t vid = {0x1209}
USB Vendor ID advertised in the device descriptor. Defaults to the pid.codes VID used by ODrive.
-
uint16_t pid = {0x0d32}
USB Product ID advertised in the device descriptor. Defaults to an ODrive-like PID.
-
std::string manufacturer = {"espp"}
Manufacturer string descriptor.
-
std::string product = {"espp USB CDC"}
Product string descriptor.
-
std::string serial_number = {"000000000001"}
Serial number string descriptor.
-
std::string interface_name = {"espp CDC"}
CDC interface string descriptor.
-
receive_callback_fn on_receive = {nullptr}
Callback invoked with received bytes. May be set/replaced later via set_receive_callback().
-
size_t rx_chunk_size = {64}
Size of the buffer used to drain the CDC RX FIFO per read.
-
uint16_t vid = {0x1209}
-
using receive_callback_fn = std::function<void(std::span<const uint8_t> data)>