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().
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()WebUSB: 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, while
tud_hid_get_report_cb returns 0 and tud_hid_set_report_cb is a no-op since
the gamepad is input-only. 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). If the HID function is requested but
CFG_TUD_HID == 0, initialize() fails with
std::errc::function_not_supported.
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) |
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 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
Composable native-USB device built on ESP-IDF’s `esp_tinyusb` managed component and the ESP32-S3 / -S2 / -P4 USB-OTG peripheral.
`esppUsbDevice` assembles a USB device from a *set of selectable functions* rather than hard-coding a single class. Today it can enable a **CDC-ACM** (virtual serial port) function and/or a **vendor-specific** function (bInterfaceClass 0xFF, one bulk IN + one bulk OUT) that optionally advertises **WebUSB** + **MS OS 2.0** descriptors so a browser can talk to it driverlessly, and/or a **HID** function (one interrupt IN, optionally one interrupt OUT) carrying an application-supplied report descriptor (e.g. a gamepad built with the espp `hid-rp` component). Interface numbers, endpoint addresses and string indices are allocated *sequentially* as functions are enabled, and the device checks the result against the USB-OTG endpoint budget (reporting an error via `std::error_code` if it is exceeded).
The design also leaves room for an **MSC** function to be added later without changing the descriptor-building model (see `MscFunction` below and the endpoint-budget table in the README).
The VID/PID and manufacturer / product / serial strings are configurable so a device can advertise its own identifiers (e.g. ODrive-like) on a link that is completely separate from the ESP console (which normally rides the built-in USB-Serial-JTAG peripheral or a UART).
The class is idiomatic espp: it does not throw, reports initialization failures via `std::error_code`, and marshals the TinyUSB RX callbacks (which run in the TinyUSB device task context) into per-function user callbacks.
UsbDevice (composite CDC + Vendor/WebUSB) 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 `esppUsbDevice` (or `espp::UsbCdc`) may exist at a time; the TinyUSB device stack, the vendor RX routing and the BOS/WebUSB control requests are all global. USB-OTG is only available on the ESP32-S2, ESP32-S3 and ESP32-P4 targets.
Note
Receive callbacks are invoked from the TinyUSB device task. Keep them short and non-blocking; it is safe to call the matching write() from within them.
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 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.
- Parameters:
data – Bytes to send.
ec – [out] Set on failure (e.g. CDC not enabled / not initialized).
- 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.
- Parameters:
data – Bytes to send.
ec – [out] Set on failure (e.g. vendor not enabled / not initialized).
- 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.
-
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).
-
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).
-
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.
-
void handle_cdc_rx()
Internal: drain the CDC RX FIFO and dispatch to the CDC callback.
-
void handle_vendor_rx(const uint8_t *buffer = nullptr, size_t bufsize = 0)
Internal: dispatch received vendor bytes to the vendor callback.
- Parameters:
buffer – When non-null (TinyUSB zero-copy RX variant, RX_BUFSIZE==0), the just-received bytes to dispatch directly. When null (the FIFO variant), the FIFO is drained via `tud_vendor_read()` instead.
bufsize – Number of bytes at
buffer(0 whenbufferis null).
-
const uint8_t *bos_descriptor() const
Internal: pointer to the BOS descriptor bytes (nullptr if none).
-
const uint8_t *ms_os_20_descriptor(uint16_t &total_len) const
Internal: pointer to the MS OS 2.0 descriptor bytes (nullptr if none).
-
const uint8_t *webusb_url_descriptor(uint8_t &length) const
Internal: pointer to the WebUSB URL descriptor bytes (nullptr if none).
-
const uint8_t *hid_report_descriptor() const
Internal: pointer to the stored HID report descriptor bytes (nullptr if the HID function is not enabled). Returned to the TinyUSB HID class driver from `tud_hid_descriptor_report_cb`.
-
inline const std::optional<VendorFunction> &vendor_config() const
Internal: config for the vendor control-request handler.
-
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
Public Static Functions
-
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.
-
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<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`.
-
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"}
-
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)>