ODrive-compatible USB Device Example (CDC + Vendor/WebUSB + HID)
This example demonstrates a composite espp::UsbDevice that presents an
ODrive-compatible device with three interfaces, all backed by one simulated motor
state (matching how a real ODrive splits its protocols across interfaces):
CDC-ACM serial → the ODrive ASCII protocol (
espp::OdriveAscii; text, for a terminal or the Web Serial console).vendor-specific (class 0xFF, WebUSB) → the ODrive native / Fibre binary protocol (
espp::OdriveNative) — the oneodrivetool/ thefibrelibrary auto-discover and speak over USB.HID → an animated gamepad input device (built with the
hid-rpcomponent; visualize it with the WebHIDhid_visualizer.html).
The device enumerates with an ODrive-like VID/PID (0x1209 / 0x0d32), separate from the log console which stays on the USB-Serial-JTAG peripheral.
Each protocol server is transport-agnostic: the CDC RX callback feeds bytes to
OdriveAscii::process_bytes(), the vendor RX callback feeds bytes to
OdriveNative::process_bytes(), and each writes its response back out the same
interface. The HID interface periodically pushes gamepad input reports.
Table of Contents
Requirements
An ESP32-S3 (or -S2 / -P4) with access to the native USB-OTG pins.
ESP-IDF installed and available in your shell.
The IDF component manager is enabled for this example so it can fetch the managed
espressif/esp_tinyusbcomponent.
The example’s sdkconfig.defaults enables the CDC, vendor, and HID classes:
CONFIG_TINYUSB_CDC_ENABLED=y
CONFIG_TINYUSB_CDC_COUNT=1
CONFIG_TINYUSB_VENDOR_COUNT=1
CONFIG_TINYUSB_HID_COUNT=1
Build
cd components/usb_device/example
idf.py set-target esp32s3
idf.py build
Flash and Monitor
Flash / monitor over the USB-Serial-JTAG (or UART) console, which is kept separate from the native USB interfaces:
idf.py flash monitor
The native USB-OTG connector will appear on the host as a new composite device: a serial port (CDC), a vendor interface (WebUSB), and a HID gamepad, with manufacturer “espp” and product “espp ODrive”.
Usage
CDC serial (ODrive ASCII): open the CDC serial port and send ODrive ASCII commands, e.g. from Python:
import serial
ser = serial.Serial('/dev/tty.usbmodemXXXX', 115200, timeout=0.5)
ser.write(b'r axis0.encoder.pos_estimate\n'); print(ser.readline())
ser.write(b'w axis0.controller.input_pos 12.34\n'); print(ser.readline())
ser.write(b'p 0 1.0 0.5 0.1\n'); print(ser.readline())
ser.write(b'f 0\n'); print(ser.readline())
(Writes/setpoints are silent by default — ODrive semantics; only r/f respond.)
Vendor / WebUSB (ODrive native / Fibre): the vendor interface (class 0xFF,
bulk IN + bulk OUT) speaks the ODrive native binary protocol. odrivetool / the
reference fibre library discover it over USB and read/write the endpoint tree;
or, from a Chromium-based browser, open the native-protocol WebUSB control panel
(odrive_control_panel.html) and connect. The BOS/WebUSB descriptors point to a
configurable landing-page URL. See HARDWARE_TEST.md and odrive_usb_probe.py.
HID (gamepad): the device also enumerates as a HID gamepad whose sticks and
buttons the firmware animates. Your OS will see a gamepad; to inspect the raw
input reports in the browser, open the WebHID hid_visualizer.html in Chromium
and connect.
How it works
espp::UsbDeviceinstalls the TinyUSB driver and builds descriptors for the enabled CDC + vendor + HID functions, allocating interfaces / endpoints sequentially (the S3 USB-OTG endpoint budget fits CDC + vendor + HID).The vendor function advertises WebUSB + MS OS 2.0 descriptors so a browser (and Windows, via WinUSB) can bind it driverlessly.
The CDC receive callback feeds bytes to
espp::OdriveAscii::process_bytes()and writes the response back out viawrite_cdc(); the vendor receive callback feeds bytes toespp::OdriveNative::process_bytes()and writes back viawrite_vendor(). Both servers share one simulated motor state.The HID report descriptor is built with the
hid-rpcomponent (espp::GamepadInputReport); the main loop animates the state and pushes reports withwrite_hid_report()when the HID interface is ready.The log console remains on the USB-Serial-JTAG peripheral (see
sdkconfig.defaults.esp32s3).