diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c52b793ecd..f76596ce79 100755 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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' @@ -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' diff --git a/components/odrive_native/README.md b/components/odrive_native/README.md index f4bd59c27a..78b5aa0edb 100644 --- a/components/odrive_native/README.md +++ b/components/odrive_native/README.md @@ -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 diff --git a/components/usb_device/example/.gitignore b/components/odrive_native/usb_example/.gitignore similarity index 100% rename from components/usb_device/example/.gitignore rename to components/odrive_native/usb_example/.gitignore diff --git a/components/odrive_native/usb_example/CMakeLists.txt b/components/odrive_native/usb_example/CMakeLists.txt new file mode 100644 index 0000000000..24272fa10d --- /dev/null +++ b/components/odrive_native/usb_example/CMakeLists.txt @@ -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) diff --git a/components/usb_device/example/HARDWARE_TEST.md b/components/odrive_native/usb_example/HARDWARE_TEST.md similarity index 99% rename from components/usb_device/example/HARDWARE_TEST.md rename to components/odrive_native/usb_example/HARDWARE_TEST.md index 2edf62aee9..933b7d8c68 100644 --- a/components/usb_device/example/HARDWARE_TEST.md +++ b/components/odrive_native/usb_example/HARDWARE_TEST.md @@ -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 flash monitor ``` diff --git a/components/odrive_native/usb_example/README.md b/components/odrive_native/usb_example/README.md new file mode 100644 index 0000000000..aaa89a0b77 --- /dev/null +++ b/components/odrive_native/usb_example/README.md @@ -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. + + +**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) + + + +## 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. diff --git a/components/odrive_native/usb_example/main/CMakeLists.txt b/components/odrive_native/usb_example/main/CMakeLists.txt new file mode 100644 index 0000000000..181b27a42b --- /dev/null +++ b/components/odrive_native/usb_example/main/CMakeLists.txt @@ -0,0 +1,5 @@ +idf_component_register( + SRC_DIRS "." + INCLUDE_DIRS "." + REQUIRES usb_device odrive_ascii odrive_native hid-rp esp_tinyusb +) diff --git a/components/usb_device/example/main/usb_cdc_example.cpp b/components/odrive_native/usb_example/main/odrive_usb_example.cpp similarity index 99% rename from components/usb_device/example/main/usb_cdc_example.cpp rename to components/odrive_native/usb_example/main/odrive_usb_example.cpp index 14decad5e3..05f306124b 100644 --- a/components/usb_device/example/main/usb_cdc_example.cpp +++ b/components/odrive_native/usb_example/main/odrive_usb_example.cpp @@ -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 { @@ -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 / diff --git a/components/usb_device/example/odrive_usb_probe.py b/components/odrive_native/usb_example/odrive_usb_probe.py similarity index 100% rename from components/usb_device/example/odrive_usb_probe.py rename to components/odrive_native/usb_example/odrive_usb_probe.py diff --git a/components/odrive_native/usb_example/sdkconfig.defaults b/components/odrive_native/usb_example/sdkconfig.defaults new file mode 100644 index 0000000000..022cca5953 --- /dev/null +++ b/components/odrive_native/usb_example/sdkconfig.defaults @@ -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 diff --git a/components/usb_device/example/sdkconfig.defaults.esp32s3 b/components/odrive_native/usb_example/sdkconfig.defaults.esp32s3 similarity index 100% rename from components/usb_device/example/sdkconfig.defaults.esp32s3 rename to components/odrive_native/usb_example/sdkconfig.defaults.esp32s3 diff --git a/components/usb_device/example/CMakeLists.txt b/components/usb_device/example/CMakeLists.txt index 8a7c3ace3c..638b88f1c5 100644 --- a/components/usb_device/example/CMakeLists.txt +++ b/components/usb_device/example/CMakeLists.txt @@ -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) -# 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) diff --git a/components/usb_device/example/README.md b/components/usb_device/example/README.md index 0b1b92d863..8d67567035 100644 --- a/components/usb_device/example/README.md +++ b/components/usb_device/example/README.md @@ -1,119 +1,101 @@ -# 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 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. +# USB Device Example (CDC + Vendor/WebUSB + MSC, with the standard espp USB services) + +The reference `espp::UsbDevice` example: **one composite USB device with the +three interface classes the component provides**, and the standard espp USB +services on every framed link, so the hosted web consoles, the Device Hub and +the `espp_ota` / `espp_coredump` command-line tools all work against it. + +| Interface | What it carries | Talk to it with | +|-----------|-----------------|-----------------| +| **CDC-ACM** (a serial port) | the espp framed protocol (`stream_frame`, routed by an `espp::DispatcherWorker`) | the hosted consoles over **Web Serial**; the `espp_ota` / `espp_coredump` CLIs over the serial port | +| **vendor-specific (class 0xFF, WebUSB)** | the same framed protocol over bulk IN/OUT | the hosted consoles over **WebUSB** (the BOS landing page points at the system console); the CLIs over libusb | +| **MSC** (a USB drive) | a wear-levelled FAT partition in flash (`storage`, 896K), with a `README.txt` the firmware writes at boot | mount it like any removable drive; eject it to hand it back to the firmware | + +The device enumerates as VID `0x1209` / PID **`0x0d38`** (manufacturer "espp", +product "espp USB Device"); the PID is distinct from the other espp examples so a +host-side filter can be specific. The log console is on **UART0** (USB-Serial-JTAG +as the early-boot secondary): on the ESP32-S3 the USB-Serial-JTAG controller and +USB-OTG share the same native USB PHY, so the console cannot stay there once +TinyUSB owns the port. **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) +- [USB Device Example (CDC + Vendor/WebUSB + MSC, with the standard espp USB services)](#usb-device-example-cdc--vendorwebusb--msc-with-the-standard-espp-usb-services) + - [Standard USB services](#standard-usb-services) + - [The USB drive](#the-usb-drive) + - [Partition layout](#partition-layout) + - [Build, flash, run](#build-flash-run) - [How it works](#how-it-works) -## Requirements +## Standard USB services -- 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. +Both framed links (CDC and vendor) serve the same set, each service registered +on each link's dispatcher and advertised through capability discovery, so the +[Device Hub](https://esp-cpp.github.io/espp/apps/dispatcher_hub.html) lists them +and every console finds its module by protocol id (the module ids below are the +published defaults; hosts do not depend on them): -The example's `sdkconfig.defaults` enables the CDC, vendor, and HID classes: +| Service | Protocol id | Module | Console / tool | +|---------|-------------|--------|----------------| +| `espp::SystemService` — chip / firmware / partition info, reboot, reboot into the ROM bootloader (download mode) | `espp.system` v1 | 7 | [system console](https://esp-cpp.github.io/espp/apps/system_console.html) | +| `espp::MonitorService` — heap regions and the task table, on request or streamed | `espp.monitor` v1 | 8 | system console | +| `espp::OtaService` — firmware update into the other OTA slot, with rollback confirmation | `espp.ota` v1 | 0 | [OTA console](https://esp-cpp.github.io/espp/apps/ota_console.html), `espp_ota` / `idf.py ota-usb` | +| `espp::CoreDumpService` — last-crash report, core dump download / erase | `espp.coredump` v1 | 4 | [coredump console](https://esp-cpp.github.io/espp/apps/coredump_console.html), `espp_coredump` / `idf.py coredump-usb` | -``` -CONFIG_TINYUSB_CDC_ENABLED=y -CONFIG_TINYUSB_CDC_COUNT=1 -CONFIG_TINYUSB_VENDOR_COUNT=1 -CONFIG_TINYUSB_HID_COUNT=1 -``` - -## Build - -```sh -cd components/usb_device/example -idf.py set-target esp32s3 -idf.py build -``` +Reboot requests go through the example's `on_reboot_request` callback, which +logs and permits them; an application would refuse or defer one while, say, the +host is writing to the drive. -## Flash and Monitor +## The USB drive -Flash / monitor over the UART0 console (a USB-UART adapter), which is independent -of the native USB interfaces this example presents: +The MSC function exposes the `storage` partition (a `data, fat` partition in +`partitions.csv`, accessed through wear levelling) as a removable drive named +`ESPP USB`. Ownership follows the `usb_device` MSC model: the firmware owns the +volume first (it formats it if needed and writes `README.txt`), the host takes +it when it mounts the device, and ejecting the drive on the host gives it back +to the firmware (logged by the heartbeat). The firmware and the host never +write the volume at the same time. -```sh -idf.py flash monitor -``` +## Partition layout -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". +`partitions.csv` (4 MB flash): `nvs`, `otadata`, `phy_init`, two 1536K app slots +`ota_0` / `ota_1` (`idf.py flash` writes `ota_0`, each OTA update alternates to +the other slot), a 64K `coredump` partition and the 896K `storage` FAT volume. -## Usage +## Build, flash, run -**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()) +```sh +cd components/usb_device/example +idf.py set-target esp32s3 +idf.py build flash monitor # console is on UART0 (USB-UART adapter) ``` -(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. +Then connect the native USB port: the host sees a serial port, a WebUSB +interface and a drive. Open the +[system console](https://esp-cpp.github.io/espp/apps/system_console.html) (WebUSB +or Web Serial) for the device info, the reboot buttons and the live heap / task +view, the OTA and coredump consoles for updates and crash dumps, and mount the +drive to read the README the firmware wrote. ## 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. +- `espp::UsbDevice` installs the TinyUSB driver and builds the descriptors for + the enabled CDC + vendor + MSC functions, allocating interfaces / endpoints + sequentially; the vendor function advertises WebUSB + MS OS 2.0 descriptors + so a browser (and Windows, via WinUSB) can bind it driverlessly. +- Each framed link has its own `espp::DispatcherWorker` (a bounded receive queue + + worker task feeding one `espp::Dispatcher`), fed from the TinyUSB receive + callbacks; the four services are registered on each worker and + `serve_discovery()` answers the hub's query. Every device->host write on a + transport goes through one application-level mutex, so a streamed monitor + event and a reply from another service never interleave. +- The OTA and core-dump engines (`espp::Ota`, `espp::CoreDump`) are shared by + the per-link services; an RX overflow on a link aborts an OTA transfer that + link owned and tells the host. +- The MSC medium is configured with the application as the initial owner and + `connect_on_initialize = false`, so the README is written before the device + presents itself to the host; `auto_handover` then moves the drive to the host + on mount and back on eject. diff --git a/components/usb_device/example/main/CMakeLists.txt b/components/usb_device/example/main/CMakeLists.txt index 181b27a42b..17aa71019e 100644 --- a/components/usb_device/example/main/CMakeLists.txt +++ b/components/usb_device/example/main/CMakeLists.txt @@ -1,5 +1,5 @@ idf_component_register( SRC_DIRS "." INCLUDE_DIRS "." - REQUIRES usb_device odrive_ascii odrive_native hid-rp esp_tinyusb + REQUIRES usb_device esp_tinyusb dispatcher task system monitor ota coredump espcoredump fatfs ) diff --git a/components/usb_device/example/main/usb_device_example.cpp b/components/usb_device/example/main/usb_device_example.cpp new file mode 100644 index 0000000000..f6f9165440 --- /dev/null +++ b/components/usb_device/example/main/usb_device_example.cpp @@ -0,0 +1,269 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "sdkconfig.h" + +#include "esp_heap_caps.h" + +#include "coredump.hpp" +#include "coredump_service.hpp" +#include "dispatcher_worker.hpp" +#include "logger.hpp" +#include "monitor_service.hpp" +#include "ota.hpp" +#include "ota_service.hpp" +#include "system_control.hpp" +#include "system_info.hpp" +#include "system_service.hpp" +#include "usb_device.hpp" + +using namespace std::chrono_literals; + +// USB device reference example: one espp::UsbDevice, three interface classes. +// +// - CDC-ACM (a serial port): the espp framed protocol, so the hosted web +// consoles can talk to the device over Web Serial (and the espp_* CLIs +// over the serial port) +// - vendor-specific / WebUSB: the same framed protocol over bulk endpoints, +// for WebUSB and the espp_* CLIs over libusb +// - MSC (a USB drive): a wear-levelled FAT partition in flash, exposed to the +// host as a removable drive +// +// Each framed link has its own espp::DispatcherWorker (a bounded receive queue +// + worker task feeding a Dispatcher), on which the standard espp USB services +// are registered and advertised for discovery: +// - espp::SystemService (module 7, `espp.system`): device identity / +// status, reboot, reboot into the ROM bootloader (download mode) +// - espp::MonitorService (module 8, `espp.monitor`): heap regions and the +// task table, on request or streamed +// - espp::OtaService (module 0, `espp.ota`): firmware update +// - espp::CoreDumpService (module 4, `espp.coredump`): last-crash report + +// core dump download / erase +// The hosted system / OTA / coredump consoles and the Device Hub work against +// this device over either framed interface. + +static constexpr const char *kMscBasePath = "/usb"; // where the app sees the FAT volume + +// Write a small README onto the drive while the application owns it (before +// the host takes it), so the drive explains itself when mounted. +static void write_drive_readme(espp::Logger &logger) { + const std::string path = std::string(kMscBasePath) + "/README.txt"; + // stdio, not iostreams: keeps the example's binary and heap footprint small + if (FILE *probe = std::fopen(path.c_str(), "r"); probe != nullptr) { + std::fclose(probe); + logger.info("drive README already present"); + return; + } + FILE *readme = std::fopen(path.c_str(), "w"); + if (readme == nullptr) { + logger.error("could not write {}", path); + return; + } + static constexpr const char *kText = + "espp USB Device example\n" + "=======================\n" + "This drive is a wear-levelled FAT partition in the ESP32-S3's flash,\n" + "exposed over USB mass storage by espp::UsbDevice.\n" + "The same device also presents a CDC serial port and a vendor/WebUSB\n" + "interface carrying the espp framed protocol: system info / reboot,\n" + "heap + task monitor, OTA update and core-dump download.\n" + "Open https://esp-cpp.github.io/espp/apps/ to use them from a browser.\n"; + const bool ok = std::fputs(kText, readme) >= 0; + std::fclose(readme); + if (ok) + logger.info("wrote {}", path); + else + logger.error("could not write {}", path); +} + +extern "C" void app_main(void) { + espp::Logger logger({.tag = "USB Device", .level = espp::Logger::Verbosity::INFO}); + logger.info("Starting USB device example (CDC + vendor/WebUSB + MSC)"); + + //! [usb_device_example] + logger.info("System:\n{}", espp::SystemInfo::to_string()); + + // The OTA and core-dump engines, shared by the per-transport services below. + // A panic core-dumps to the `coredump` partition (partitions.csv) and is + // reported here on the next boot; the coredump console downloads / erases + // it. OTA alternates between ota_0 / ota_1 with host-driven rollback + // confirmation, exactly as the ota example does. + espp::CoreDump core_dump({.log_level = espp::Logger::Verbosity::INFO}); + const std::string crash_report = core_dump.format_report(); + if (crash_report.empty()) + logger.info("Clean boot history (reset reason: {})", + espp::CoreDump::reset_reason_name(espp::CoreDump::reset_reason())); + else + logger.error("Previous abnormal reset:\n{}", crash_report); + espp::Ota ota({.reject_same_version = false, .log_level = espp::Logger::Verbosity::INFO}); + if (ota.is_pending_verify()) + logger.warn("This image is PENDING VERIFY (first boot after an OTA update): it rolls back on " + "the next reset unless the host confirms it (MARK_VALID from the OTA console)"); + + // --- the composite USB device: CDC + vendor (WebUSB) + MSC ------------------ + espp::UsbDevice::Config usb_cfg; + usb_cfg.pid = 0x0d38; // distinct from the other espp examples so a host filter is specific + usb_cfg.manufacturer = "espp"; + usb_cfg.product = "espp USB Device"; + usb_cfg.connect_on_initialize = false; // write the drive's README before the host can mount it + usb_cfg.log_level = espp::Logger::Verbosity::WARN; + espp::UsbDevice::CdcFunction cdc; + cdc.interface_name = "espp USB Device (CDC)"; + usb_cfg.cdc = cdc; + espp::UsbDevice::VendorFunction vendor; + vendor.interface_name = "espp USB Device (WebUSB)"; + vendor.webusb = true; // advertise BOS / WebUSB / MS OS 2.0 descriptors + vendor.landing_page_url = "esp-cpp.github.io/espp/apps/system_console.html"; + usb_cfg.vendor = vendor; + // MSC: the `storage` FAT partition, owned by the application first (so it + // can write the README), then handed to the host when it mounts the device + // (auto_handover); ejecting the drive gives it back to the application. + using MscOwner = espp::UsbDevice::MscOwner; + using MscEvent = espp::UsbDevice::MscEvent; + std::atomic drive_regained{false}; + espp::UsbDevice::MscMedium medium; + medium.type = espp::UsbDevice::MscMedium::Type::FlashPartition; + medium.partition_label = "storage"; // `data, fat` partition in partitions.csv + medium.base_path = kMscBasePath; + medium.volume_label = "ESPP USB"; // the name the host shows for the drive + medium.format_if_unformatted = true; // the only FAT volume on this device + medium.initial_owner = MscOwner::App; + espp::UsbDevice::MscFunction msc; + msc.interface_name = "espp USB Device (MSC)"; + msc.media = {medium}; + msc.auto_handover = true; + msc.on_event = [&](size_t lun, MscEvent event, MscOwner owner) { + if (event == MscEvent::OwnerChanged) { + logger.info("drive {} now owned by the {}", lun, owner == MscOwner::App ? "app" : "host"); + if (owner == MscOwner::App) + drive_regained = true; + } else if (event == MscEvent::FormatRequired) { + logger.warn("drive {} has no filesystem", lun); + } else if (event == MscEvent::FormatFailed || event == MscEvent::OwnerChangeFailed) { + logger.error("drive {}: {}", lun, + event == MscEvent::FormatFailed ? "format failed" : "hand-over failed"); + } + }; + usb_cfg.msc = msc; + espp::UsbDevice usb(usb_cfg); + + // Replies go back on the stream the request came in on: one send function + // per transport. All services share each transport and only serialize their + // OWN frames (a streamed monitor event and a system reply come from + // different tasks), so every device->host write on a transport goes through + // one application-level mutex; write_vendor / write_cdc are all-or-nothing + // per call, so a frame is never truncated or interleaved. + std::mutex vendor_tx_mutex, cdc_tx_mutex; + auto vendor_send = [&](std::span frame) { + std::lock_guard lock(vendor_tx_mutex); + usb.write_vendor(frame); + }; + auto cdc_send = [&](std::span frame) { + std::lock_guard lock(cdc_tx_mutex); + usb.write_cdc(frame); + }; + + // The application decides whether a reboot may happen right now: this demo + // permits every request and logs it. A real application would refuse (or + // defer) while, say, the host is writing to the drive. + auto reboot_request = [&](espp::SystemService::RebootKind kind) { + logger.warn("Host requested a {}; allowing it", + kind == espp::SystemService::RebootKind::Bootloader ? "reboot into the bootloader" + : "reboot"); + return true; + }; + + // One service instance per transport (they are cheap; each replies on its + // own stream). Declared BEFORE the workers that call into them. + espp::SystemService vendor_system({.send = vendor_send, + .on_reboot_request = reboot_request, + .log_level = espp::Logger::Verbosity::INFO}); + espp::SystemService cdc_system({.send = cdc_send, + .on_reboot_request = reboot_request, + .log_level = espp::Logger::Verbosity::INFO}); + espp::MonitorService vendor_monitor( + {.send = vendor_send, + .task_config = {.name = "monitor_v", .stack_size_bytes = 6 * 1024}, + .log_level = espp::Logger::Verbosity::INFO}); + espp::MonitorService cdc_monitor( + {.send = cdc_send, + .task_config = {.name = "monitor_c", .stack_size_bytes = 6 * 1024}, + .log_level = espp::Logger::Verbosity::INFO}); + // OTA and core dump: one service per transport over the shared engines (each + // OtaService only touches the session IT began, so the two cannot interfere). + espp::OtaService vendor_ota(ota, + {.send = vendor_send, .log_level = espp::Logger::Verbosity::INFO}); + espp::OtaService cdc_ota(ota, {.send = cdc_send, .log_level = espp::Logger::Verbosity::INFO}); + espp::CoreDumpService vendor_coredump( + core_dump, {.send = vendor_send, .log_level = espp::Logger::Verbosity::INFO}); + espp::CoreDumpService cdc_coredump( + core_dump, {.send = cdc_send, .log_level = espp::Logger::Verbosity::INFO}); + + // One DispatcherWorker per byte stream, so the services never run on the + // TinyUSB task. Registering a service routes its module to it and advertises + // it for discovery; serve_discovery() answers the hub's ListModules query. + // On an RX overflow the OTA service aborts a transfer it owned and tells the + // host (an OTA image with dropped bytes is unusable). + espp::DispatcherWorker vendor_link( + {.send = vendor_send, + .on_overflow = [&]() { vendor_ota.on_rx_overflow(); }, + .task_config = {.name = "usb_rx_vendor", .stack_size_bytes = 8192}}); + espp::DispatcherWorker cdc_link( + {.send = cdc_send, + .on_overflow = [&]() { cdc_ota.on_rx_overflow(); }, + .task_config = {.name = "usb_rx_cdc", .stack_size_bytes = 8192}}); + vendor_link.register_module(vendor_system); + vendor_link.register_module(vendor_monitor); + vendor_link.register_module(vendor_ota); + vendor_link.register_module(vendor_coredump); + cdc_link.register_module(cdc_system); + cdc_link.register_module(cdc_monitor); + cdc_link.register_module(cdc_ota); + cdc_link.register_module(cdc_coredump); + vendor_link.serve_discovery(usb_cfg.product); + cdc_link.serve_discovery(usb_cfg.product); + + // RX plumbing: the TinyUSB callbacks just queue the bytes for the workers. + usb.set_vendor_receive_callback([&](std::span data) { vendor_link.push(data); }); + usb.set_cdc_receive_callback([&](std::span data) { cdc_link.push(data); }); + + std::error_code usb_ec; + if (!usb.initialize(usb_ec)) { + logger.error("Failed to initialize USB device: {}", usb_ec.message()); + return; + } + // The drive is ours until the host mounts the device: describe it, then + // let the host in. + if (const auto capacity = usb.msc_capacity(0)) + logger.info("drive: {} sectors x {} bytes = {} KiB", capacity->sector_count, + capacity->sector_size, capacity->bytes() / 1024); + if (usb.msc_owner(0) == MscOwner::App) + write_drive_readme(logger); + usb.connect(); + //! [usb_device_example] + + logger.info("Enumerated as VID 0x{:04x} PID 0x{:04x} '{}': CDC (framed protocol / Web Serial), " + "vendor (framed protocol / WebUSB, landing page https://{}), MSC (drive '{}')", + usb_cfg.vid, usb_cfg.pid, usb_cfg.product, vendor.landing_page_url, + medium.volume_label); + logger.info("Ready: open the system / OTA / coredump consoles or the Device Hub, or mount the " + "drive"); + + // Idle; all work happens in the dispatcher workers, the monitor stream tasks + // and TinyUSB. A small heartbeat shows the device is alive and what the + // drive is doing. + while (true) { + std::this_thread::sleep_for(10s); + if (drive_regained.exchange(false)) + logger.info("the host ejected the drive; the application owns it again"); + logger.info("alive: free heap {} B (min {} B), drive owned by the {}", + heap_caps_get_free_size(MALLOC_CAP_DEFAULT), + heap_caps_get_minimum_free_size(MALLOC_CAP_DEFAULT), + usb.msc_owner(0) == MscOwner::App ? "app" : "host"); + } +} diff --git a/components/usb_device/example/partitions.csv b/components/usb_device/example/partitions.csv new file mode 100644 index 0000000000..efa8a516a6 --- /dev/null +++ b/components/usb_device/example/partitions.csv @@ -0,0 +1,15 @@ +# ESP-IDF Partition Table for the usb_device example (4 MB flash): +# - factory-less OTA layout: otadata picks which of the two equal app slots +# boots; `idf.py flash` writes ota_0 and each OTA update (over USB, through +# the OTA service) alternates to the other slot +# - `coredump`: a panic core-dumps here, served over USB by the core-dump service +# - `storage`: the wear-levelled FAT volume the MSC function exposes as a USB +# drive (896K minus wear-levelling overhead) +# Name, Type, SubType, Offset, Size, Flags +nvs, data, nvs, 0x9000, 0x4000, +otadata, data, ota, 0xd000, 0x2000, +phy_init, data, phy, 0xf000, 0x1000, +ota_0, app, ota_0, 0x10000, 1536K, +ota_1, app, ota_1, , 1536K, +coredump, data, coredump, , 64K, +storage, data, fat, , 896K, diff --git a/components/usb_device/example/sdkconfig.defaults b/components/usb_device/example/sdkconfig.defaults index 022cca5953..9f82688f28 100644 --- a/components/usb_device/example/sdkconfig.defaults +++ b/components/usb_device/example/sdkconfig.defaults @@ -1,26 +1,55 @@ -# 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. +# The USB vendor / WebUSB, CDC and MSC functions use the native USB-OTG +# peripheral, available on the ESP32-S3 (also S2 / P4) -- NOT the classic ESP32. +# Pin the target so a bare `idf.py build` does not fall back to esp32. 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. +# Console on UART0 (secondary on USB-Serial-JTAG): on the S3, USB-Serial-JTAG +# shares the native USB PHY with USB-OTG, which TinyUSB takes over for the +# interfaces this example presents. +CONFIG_ESP_CONSOLE_UART_DEFAULT=y +CONFIG_ESP_CONSOLE_SECONDARY_USB_SERIAL_JTAG=y + +# TinyUSB class drivers: vendor (WebUSB) + CDC (Web Serial) for the framed +# protocol, MSC for the USB drive; HID count only so usb_device's HID code +# paths compile (no HID interface is instantiated here). +CONFIG_TINYUSB_VENDOR_COUNT=1 CONFIG_TINYUSB_CDC_ENABLED=y CONFIG_TINYUSB_CDC_COUNT=1 +CONFIG_TINYUSB_HID_COUNT=1 +CONFIG_TINYUSB_MSC_ENABLED=y +CONFIG_TINYUSB_VENDOR_RX_BUFSIZE=4096 +CONFIG_TINYUSB_VENDOR_TX_BUFSIZE=4096 +CONFIG_TINYUSB_CDC_TX_BUFSIZE=4096 -# 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 +# MSC over a flash partition: 4096-byte wear-levelling sectors with a 4096-byte +# MSC buffer, so each host write is one flash erase + write (see +# UsbDevice::MscFunction); the FAT volume label the host shows needs +# CONFIG_FATFS_USE_LABEL. +CONFIG_WL_SECTOR_SIZE_4096=y +CONFIG_TINYUSB_MSC_BUFSIZE=4096 +CONFIG_FATFS_LFN_HEAP=y +CONFIG_FATFS_USE_LABEL=y -# 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 +# Task statistics for MonitorService (GET_TASKS / TASKS stream): the FreeRTOS +# trace facility + run-time stats, and the core id per task. +CONFIG_FREERTOS_USE_TRACE_FACILITY=y +CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS=y +CONFIG_FREERTOS_VTASKLIST_INCLUDE_COREID=y + +CONFIG_COMPILER_CXX_EXCEPTIONS=y + +# Standard USB services (system / monitor / OTA / core dump): the OTA+coredump +# (+ storage) partition layout (partitions.csv), core dumps to flash, OTA +# rollback, and the FreeRTOS run-time statistics the task monitor reads. +CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y +CONFIG_PARTITION_TABLE_CUSTOM=y +CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv" +CONFIG_PARTITION_TABLE_FILENAME="partitions.csv" +CONFIG_PARTITION_TABLE_OFFSET=0x8000 +CONFIG_PARTITION_TABLE_MD5=y +CONFIG_ESP_COREDUMP_ENABLE_TO_FLASH=y +CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE=y diff --git a/components/usb_device/include/usb_cdc.hpp b/components/usb_device/include/usb_cdc.hpp index e7e046c88c..4798fa90c2 100644 --- a/components/usb_device/include/usb_cdc.hpp +++ b/components/usb_device/include/usb_cdc.hpp @@ -34,7 +34,7 @@ namespace espp { * short and non-blocking; it is safe to call write() from within it. * * \section usb_cdc_ex1 UsbCdc Example - * \snippet usb_cdc_example.cpp usb_cdc_example + * \snippet usb_device_example.cpp usb_device_example */ class UsbCdc : public BaseComponent { public: diff --git a/components/usb_device/include/usb_device.hpp b/components/usb_device/include/usb_device.hpp index 185db98732..4d5df7e542 100644 --- a/components/usb_device/include/usb_device.hpp +++ b/components/usb_device/include/usb_device.hpp @@ -60,8 +60,8 @@ namespace espp { * short and non-blocking; it is safe to call the matching write() from * within them. * - * \section usb_device_ex1 UsbDevice (composite CDC + Vendor/WebUSB) Example - * \snippet usb_cdc_example.cpp usb_cdc_example + * \section usb_device_ex1 UsbDevice (composite CDC + Vendor/WebUSB + MSC) Example + * \snippet usb_device_example.cpp usb_device_example */ // Forward-declare the extern "C" trampoline (defined in usb_device.cpp, inside diff --git a/doc/Doxyfile b/doc/Doxyfile index f60edb835a..7185bdc5f9 100755 --- a/doc/Doxyfile +++ b/doc/Doxyfile @@ -161,6 +161,7 @@ EXAMPLE_PATH = \ $(PROJECT_PATH)/components/nvs/example/main/nvs_example.cpp \ $(PROJECT_PATH)/components/odrive_ascii/example/main/odrive_ascii_example.cpp \ $(PROJECT_PATH)/components/odrive_native/example/main/odrive_native_example.cpp \ + $(PROJECT_PATH)/components/odrive_native/usb_example/main/odrive_usb_example.cpp \ $(PROJECT_PATH)/components/ota/example/main/ota_example.cpp \ $(PROJECT_PATH)/components/pca9535/example/main/pca9535_example.cpp \ $(PROJECT_PATH)/components/pcf85063/example/main/pcf85063_example.cpp \ @@ -203,7 +204,7 @@ EXAMPLE_PATH = \ $(PROJECT_PATH)/components/trajectory_planner/example/main/trajectory_planner_example.cpp \ $(PROJECT_PATH)/components/twai/example/main/twai_example.cpp \ $(PROJECT_PATH)/components/tt21100/example/main/tt21100_example.cpp \ - $(PROJECT_PATH)/components/usb_device/example/main/usb_cdc_example.cpp \ + $(PROJECT_PATH)/components/usb_device/example/main/usb_device_example.cpp \ $(PROJECT_PATH)/components/usb_device/msc_example/main/msc_example.cpp \ $(PROJECT_PATH)/components/usb_device/xinput_example/main/xinput_example.cpp \ $(PROJECT_PATH)/components/usb_host/example/main/usb_host_example.cpp \ diff --git a/doc/en/motor_control/odrive_native.rst b/doc/en/motor_control/odrive_native.rst index 0be34881e7..d00826b8f6 100644 --- a/doc/en/motor_control/odrive_native.rst +++ b/doc/en/motor_control/odrive_native.rst @@ -65,6 +65,7 @@ USB device stack is handled in a later phase. .. toctree:: odrive_native_example.md + odrive_usb_example.md .. ---------------------------- API Reference ---------------------------------- diff --git a/doc/en/motor_control/odrive_usb_example.md b/doc/en/motor_control/odrive_usb_example.md new file mode 100644 index 0000000000..1bd2b54557 --- /dev/null +++ b/doc/en/motor_control/odrive_usb_example.md @@ -0,0 +1,2 @@ +```{include} ../../../components/odrive_native/usb_example/README.md +```