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
3 changes: 3 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,8 @@ jobs:
target: esp32
- path: 'components/odrive_native/example'
target: esp32
- path: 'components/odrive_native/usb_example'
target: esp32s3
- path: 'components/ota/example'
target: esp32s3
command: 'IDF_COMPONENT_MANAGER=0 idf.py build'
Expand Down Expand Up @@ -362,6 +364,7 @@ jobs:
target: esp32
- path: 'components/usb_device/example'
target: esp32s3
command: 'IDF_COMPONENT_MANAGER=0 idf.py build'
- path: 'components/usb_device/msc_example'
target: esp32s3
command: 'IDF_COMPONENT_MANAGER=0 idf.py build'
Expand Down
5 changes: 5 additions & 0 deletions components/odrive_native/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ A scripted example is provided in [`example`](./example) and is built by CI. It
registers a few simulated-motor properties and feeds crafted packets through
`process_bytes`, logging the responses.

A second example, [`usb_example`](./usb_example), presents an ODrive-compatible
**USB device** on an ESP32-S3: the ODrive ASCII protocol over CDC, the native /
Fibre protocol over the vendor (WebUSB) interface, and a HID gamepad, all from
one simulated motor state (see its README and `HARDWARE_TEST.md`).

## Notes

- This component implements the **properties** (primitive get/set) surface of the
Expand Down
35 changes: 35 additions & 0 deletions components/odrive_native/usb_example/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# 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)

# NOTE: the IDF component manager is intentionally left ENABLED here (unlike most
# espp examples) so that it can fetch the managed `espressif/esp_tinyusb`
# dependency declared by the usb_device component's idf_component.yml. To avoid
# the component manager scanning every espp component manifest (some board
# components declare target-specific constraints that would fail on esp32s3),
# EXTRA_COMPONENT_DIRS is narrowed to just the components this example uses; the
# in-repo espp components there satisfy the `espp/*` dependencies locally (no
# example manifest / override_path needed).
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/hid-rp"
"../../../components/logger"
"../../../components/odrive_ascii"
"../../../components/odrive_native"
"../../../components/usb_device"
)

set(
COMPONENTS
"main esptool_py base_component format hid-rp logger odrive_ascii odrive_native usb_device esp_tinyusb"
CACHE STRING
"List of components to include"
)

project(odrive_usb_example)

set(CMAKE_CXX_STANDARD 20)
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ built-in USB-Serial-JTAG, separate from this device.
## 1. Flash

```sh
cd components/usb_device/example
cd components/odrive_native/usb_example
idf.py set-target esp32s3
idf.py -p <PORT> flash monitor
```
Expand Down
123 changes: 123 additions & 0 deletions components/odrive_native/usb_example/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# ODrive-compatible USB Device Example (CDC + Vendor/WebUSB + HID)

This is the `odrive_native` component's USB example: a **composite**
`espp::UsbDevice` that presents an ODrive-compatible device (the ODrive ASCII
protocol over CDC, the ODrive native / Fibre protocol over the vendor interface,
and a HID gamepad), all backed by one simulated motor state, matching how a real
ODrive splits its protocols across interfaces. (It used to live in
`components/usb_device/example`; that is now the generic USB device example with
the standard espp USB services.)

- **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 one `odrivetool` / the `fibre` library
auto-discover and speak over USB.
- **HID** → an animated **gamepad** input device (built with the `hid-rp`
component; visualize it with the WebHID `hid_visualizer.html`).

The device enumerates with an ODrive-like VID/PID (0x1209 / 0x0d32) on the native
USB port. The log console is on **UART0** (with USB-Serial-JTAG as an early-boot
secondary): on the ESP32-S3 the USB-Serial-JTAG controller and USB-OTG share the
same native USB PHY, so keeping the console on it would contend with the TinyUSB
interfaces here and reboot-loop the device.

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.

<!-- markdown-toc start - Don't edit this section. Run M-x markdown-toc-refresh-toc -->
**Table of Contents**

- [ODrive-compatible USB Device Example (CDC + Vendor/WebUSB + HID)](#odrive-compatible-usb-device-example-cdc--vendorwebusb--hid)
- [Requirements](#requirements)
- [Build](#build)
- [Flash and Monitor](#flash-and-monitor)
- [Usage](#usage)
- [How it works](#how-it-works)

<!-- markdown-toc end -->

## 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_tinyusb` component.

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

```sh
cd components/odrive_native/usb_example
idf.py set-target esp32s3
idf.py build
```

## Flash and Monitor

Flash / monitor over the UART0 console (a USB-UART adapter), which is independent
of the native USB interfaces this example presents:

```sh
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:

```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::UsbDevice` installs 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 via `write_cdc()`; the vendor receive callback feeds
bytes to `espp::OdriveNative::process_bytes()` and writes back via
`write_vendor()`. Both servers share one simulated motor state.
- The HID report descriptor is built with the `hid-rp` component
(`espp::GamepadInputReport`); the main loop animates the state and pushes reports
with `write_hid_report()` when the HID interface is ready.
- The log console is on UART0, with USB-Serial-JTAG as an early-boot secondary
(see `sdkconfig.defaults.esp32s3`) — it cannot stay on USB-Serial-JTAG because
that shares the native USB PHY with the USB-OTG interfaces here.
5 changes: 5 additions & 0 deletions components/odrive_native/usb_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_device odrive_ascii odrive_native hid-rp esp_tinyusb
)
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ extern "C" void app_main(void) {
// - HID interface -> an animated gamepad (visualize with hid_visualizer.html)
Logger logger({.tag = "OdriveUsbExample", .level = Logger::Verbosity::INFO});

//! [usb_cdc_example]
//! [odrive_usb_example]

// One simulated motor state, shared by both protocol servers.
struct {
Expand Down Expand Up @@ -191,7 +191,7 @@ extern "C" void app_main(void) {
logger.info("Native endpoint tree ({} bytes, json_crc=0x{:04x})", native.json().size(),
native.json_crc());

//! [usb_cdc_example]
//! [odrive_usb_example]

// The CDC / vendor transports run off the TinyUSB task + their RX callbacks.
// Here on the main task we animate the shared motor state (seen by the ASCII /
Expand Down
26 changes: 26 additions & 0 deletions components/odrive_native/usb_example/sdkconfig.defaults
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# This example uses the native USB-OTG peripheral, which is only available on the
# ESP32-S3 (also S2 / P4) -- NOT the classic ESP32. Pin the target here so a bare
# `idf.py build` (without an explicit `set-target`) does not fall back to esp32
# and fail to compile the USB device.
CONFIG_IDF_TARGET="esp32s3"

# Common ESP-related
CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=4096
CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192

# Enable the TinyUSB CDC-ACM feature (provided by the managed espressif/esp_tinyusb
# component). The espp::UsbDevice CDC function drives a dedicated CDC-ACM
# interface over the native USB-OTG peripheral.
CONFIG_TINYUSB_CDC_ENABLED=y
CONFIG_TINYUSB_CDC_COUNT=1

# Enable the TinyUSB vendor-specific class (this is THE key enablement for the
# vendor / WebUSB interface). esp_tinyusb gates CFG_TUD_VENDOR behind
# CONFIG_TINYUSB_VENDOR_COUNT; setting it > 0 compiles in the vendor class driver
# so espp::UsbDevice's vendor function (bInterfaceClass 0xFF + WebUSB) works.
CONFIG_TINYUSB_VENDOR_COUNT=1

# Enable the TinyUSB HID class driver. esp_tinyusb gates CFG_TUD_HID behind
# CONFIG_TINYUSB_HID_COUNT; setting it > 0 compiles in the HID class driver so
# espp::UsbDevice's HID function (gamepad report descriptor from hid-rp) works.
CONFIG_TINYUSB_HID_COUNT=1
47 changes: 31 additions & 16 deletions components/usb_device/example/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,49 @@
# in this exact order for cmake to work correctly
cmake_minimum_required(VERSION 3.20)

# NOTE: the IDF component manager is intentionally left ENABLED here (unlike most
# espp examples) so that it can fetch the managed `espressif/esp_tinyusb`
# dependency declared by the usb_device component's idf_component.yml. To avoid
# the component manager scanning every espp component manifest (some board
# components declare target-specific constraints that would fail on esp32s3),
# EXTRA_COMPONENT_DIRS is narrowed to just the components this example uses; the
# in-repo espp components there satisfy the `espp/*` dependencies locally (no
# example manifest / override_path needed).
set(CMAKE_CXX_STANDARD 20)

# This example needs the managed `espressif/esp_tinyusb` component (required by
# usb_device). It supports two build modes:
#
# * DEFAULT (component manager ON) - how an end user builds it from the
# component registry: the manager fetches esp_tinyusb (and its tinyusb
# dependency) and the espp/* dependencies from the registry. EXTRA_COMPONENT_DIRS
# is narrowed to just the components this example uses so the manager does not
# scan every espp manifest.
#
# * MANAGER OFF (IDF_COMPONENT_MANAGER=0) - used by CI so the build does not need
# the espp/* components in the registry: every espp dependency resolves locally
# from EXTRA_COMPONENT_DIRS, and esp_tinyusb / tinyusb come from the vendored git
# submodules under external/.
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
Comment thread
finger563 marked this conversation as resolved.

# add only the component directories that we want to use
set(EXTRA_COMPONENT_DIRS
"../../../components/base_component"
"../../../components/coredump"
"../../../components/dispatcher"
"../../../components/format"
"../../../components/hid-rp"
"../../../components/logger"
"../../../components/odrive_ascii"
"../../../components/odrive_native"
"../../../components/monitor"
"../../../components/ota"
"../../../components/stream_frame"
"../../../components/system"
"../../../components/task"
"../../../components/usb_device"
)

if(DEFINED ENV{IDF_COMPONENT_MANAGER} AND "$ENV{IDF_COMPONENT_MANAGER}" STREQUAL "0")
list(APPEND EXTRA_COMPONENT_DIRS
"../../../external/esp-usb/device/esp_tinyusb"
"../../../external/tinyusb"
)
endif()

set(
COMPONENTS
"main esptool_py base_component format hid-rp logger odrive_ascii odrive_native usb_device esp_tinyusb"
"main esptool_py base_component dispatcher format logger monitor stream_frame system task usb_device esp_tinyusb esp_psram coredump ota espcoredump fatfs"
CACHE STRING
"List of components to include"
)

project(usb_cdc_example)

set(CMAKE_CXX_STANDARD 20)
project(usb_device_example)
Loading
Loading