From 12b33dc90ea0f59815dd5cde0fd3be4601c19fa3 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Sun, 16 Aug 2026 21:44:27 -0500 Subject: [PATCH 01/12] feat(usb_device): native USB CDC transport (esp_tinyusb) + OdriveAscii example Add espp::UsbCdc, a thin idiomatic wrapper around ESP-IDF's esp_tinyusb managed component that presents a dedicated native USB CDC-ACM interface on the ESP32-S3/-S2/-P4 USB-OTG peripheral with a configurable VID/PID and manufacturer/product/serial strings, separate from the log console. The example wires espp::UsbCdc RX -> espp::OdriveAscii::process_bytes -> espp::UsbCdc::write so the device enumerates as an ODrive-like serial port while the log console stays on USB-Serial-JTAG. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/build.yml | 2 + .github/workflows/upload_components.yml | 1 + components/usb_device/CMakeLists.txt | 5 + components/usb_device/README.md | 77 ++++++ components/usb_device/example/CMakeLists.txt | 32 +++ components/usb_device/example/README.md | 69 ++++++ .../usb_device/example/main/CMakeLists.txt | 5 + .../usb_device/example/main/idf_component.yml | 25 ++ .../example/main/usb_cdc_example.cpp | 115 +++++++++ .../usb_device/example/sdkconfig.defaults | 9 + .../example/sdkconfig.defaults.esp32s3 | 7 + components/usb_device/idf_component.yml | 24 ++ components/usb_device/include/usb_cdc.hpp | 142 +++++++++++ components/usb_device/src/usb_cdc.cpp | 225 ++++++++++++++++++ doc/Doxyfile | 2 + doc/en/buses/index.rst | 1 + doc/en/buses/usb_cdc.rst | 64 +++++ doc/en/buses/usb_cdc_example.md | 2 + 18 files changed, 807 insertions(+) create mode 100644 components/usb_device/CMakeLists.txt create mode 100644 components/usb_device/README.md create mode 100644 components/usb_device/example/CMakeLists.txt create mode 100644 components/usb_device/example/README.md create mode 100644 components/usb_device/example/main/CMakeLists.txt create mode 100644 components/usb_device/example/main/idf_component.yml create mode 100644 components/usb_device/example/main/usb_cdc_example.cpp create mode 100644 components/usb_device/example/sdkconfig.defaults create mode 100644 components/usb_device/example/sdkconfig.defaults.esp32s3 create mode 100644 components/usb_device/idf_component.yml create mode 100644 components/usb_device/include/usb_cdc.hpp create mode 100644 components/usb_device/src/usb_cdc.cpp create mode 100644 doc/en/buses/usb_cdc.rst create mode 100644 doc/en/buses/usb_cdc_example.md diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1fe6bab24c..6a64f88f74 100755 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -210,6 +210,8 @@ jobs: target: esp32s3 - path: 'components/odrive_ascii/example' target: esp32 + - path: 'components/usb_device/example' + target: esp32s3 - path: 'components/pca9535/example' target: esp32s3 - path: 'components/pcf85063/example' diff --git a/.github/workflows/upload_components.yml b/.github/workflows/upload_components.yml index 721b3c85b1..4aa82849d2 100755 --- a/.github/workflows/upload_components.yml +++ b/.github/workflows/upload_components.yml @@ -153,6 +153,7 @@ jobs: components/touch components/tla2528 components/tt21100 + components/usb_device components/utils components/vl53l components/wifi diff --git a/components/usb_device/CMakeLists.txt b/components/usb_device/CMakeLists.txt new file mode 100644 index 0000000000..4fed54b8eb --- /dev/null +++ b/components/usb_device/CMakeLists.txt @@ -0,0 +1,5 @@ +idf_component_register( + INCLUDE_DIRS "include" + SRC_DIRS "src" + REQUIRES base_component esp_tinyusb +) diff --git a/components/usb_device/README.md b/components/usb_device/README.md new file mode 100644 index 0000000000..92db230405 --- /dev/null +++ b/components/usb_device/README.md @@ -0,0 +1,77 @@ +# USB CDC Transport Component + +[![Badge](https://components.espressif.com/components/espp/usb_device/badge.svg)](https://components.espressif.com/components/espp/usb_device) + +`espp::UsbCdc` is a thin, idiomatic wrapper around ESP-IDF's `esp_tinyusb` +managed component that presents a single dedicated **native USB CDC-ACM** +(virtual serial port) interface on the ESP32-S3 / -S2 / -P4 USB-OTG peripheral, +with a **configurable VID/PID** and manufacturer / product / serial strings. + +Because the CDC interface uses the native USB-OTG peripheral (not the built-in +USB-Serial-JTAG that carries the ESP console), a device can advertise its own USB +identifiers (e.g. ODrive-like) on a link that is completely separate from the +logging console. This also lays the groundwork for adding a WebUSB *vendor* +interface later. + + +**Table of Contents** + +- [USB CDC Transport Component](#usb-cdc-transport-component) + - [Features](#features) + - [API](#api) + - [Example](#example) + - [Notes](#notes) + + + +## Features + +- **Native USB**: uses the USB-OTG peripheral via TinyUSB, separate from the log console. +- **Configurable identity**: VID, PID, manufacturer / product / serial / interface strings. +- **Byte-stream transport**: feed bytes in via a receive callback, send bytes out via `write()`. +- **Idiomatic espp**: no exceptions; `initialize()` reports failures via `std::error_code`. +- **Safe marshaling**: the TinyUSB RX callback (which runs in the TinyUSB task) is drained and + delivered to the user callback; `write()` is safe to call from within it. + +## API + +```cpp +espp::UsbCdc::Config cfg; +cfg.vid = 0x1209; // pid.codes VID (ODrive uses this) +cfg.pid = 0x0d32; // ODrive-like PID +cfg.manufacturer = "espp"; +cfg.product = "espp USB CDC"; +cfg.serial_number = "0001"; +cfg.on_receive = [](std::span data) { /* handle rx */ }; + +espp::UsbCdc usb(cfg); +std::error_code ec; +if (!usb.initialize(ec)) { /* handle ec */ } + +// send bytes +uint8_t hello[] = {'h','i','\n'}; +usb.write(hello); + +// replace the receive callback at any time +usb.set_receive_callback([](std::span data) { /* ... */ }); +``` + +Key methods: + +- `bool initialize(std::error_code &ec)` — install the TinyUSB driver + CDC-ACM and set descriptors. +- `bool write(std::span data[, std::error_code &ec])` — queue + non-blocking flush. +- `void set_receive_callback(const receive_callback_fn &cb)` — set/replace the RX callback. +- `bool is_initialized() const`, `bool is_connected() const`. + +## Example + +See `example/` for a full project that wires `espp::UsbCdc` to `espp::OdriveAscii` +so the device shows up as an ODrive-like serial port speaking the ODrive ASCII +protocol, while the log console stays on the USB-Serial-JTAG peripheral. + +## Notes + +- USB-OTG is only available on the ESP32-S2, ESP32-S3 and ESP32-P4 targets. +- Enable `CONFIG_TINYUSB_CDC_ENABLED=y` in your project (see the example `sdkconfig.defaults`). +- Only one `espp::UsbCdc` instance may drive a given CDC port. +- The receive callback runs in the TinyUSB device task; keep it short and non-blocking. diff --git a/components/usb_device/example/CMakeLists.txt b/components/usb_device/example/CMakeLists.txt new file mode 100644 index 0000000000..66c258c558 --- /dev/null +++ b/components/usb_device/example/CMakeLists.txt @@ -0,0 +1,32 @@ +# 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` +# component from the ESP registry. 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, and the espp dependencies are pinned to +# their in-repo copies via override_path in main/idf_component.yml. +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/logger" + "../../../components/odrive_ascii" + "../../../components/usb_device" +) + +set( + COMPONENTS + "main esptool_py base_component format logger odrive_ascii usb_device esp_tinyusb" + CACHE STRING + "List of components to include" + ) + +project(usb_cdc_example) + +set(CMAKE_CXX_STANDARD 20) diff --git a/components/usb_device/example/README.md b/components/usb_device/example/README.md new file mode 100644 index 0000000000..2407bcca71 --- /dev/null +++ b/components/usb_device/example/README.md @@ -0,0 +1,69 @@ +# USB CDC + ODrive ASCII Example + +This example demonstrates the `espp::UsbCdc` native-USB CDC-ACM transport wired to +the transport-agnostic `espp::OdriveAscii` protocol server. The device enumerates +as a dedicated USB serial port with an ODrive-like VID/PID (0x1209 / 0x0d32), +separate from the log console which stays on the USB-Serial-JTAG peripheral. + + +**Table of Contents** + +- [USB CDC + ODrive ASCII Example](#usb-cdc--odrive-ascii-example) + - [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. + +## Build + +```sh +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 CDC interface: + +```sh +idf.py flash monitor +``` + +The native USB-OTG connector will appear on the host as a new serial port with +manufacturer "espp" and product "espp ODrive ASCII". + +## Usage + +Open the native USB serial port and send ODrive ASCII commands, e.g. from Python: + +```python +import serial +# The port that enumerated with VID 0x1209 / PID 0x0d32 +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()) +``` + +## How it works + +- `espp::UsbCdc` installs the TinyUSB driver and a single CDC-ACM interface with + the configured VID/PID/strings. +- Its receive callback feeds incoming bytes to `espp::OdriveAscii::process_bytes()`. +- The returned response bytes are written back out over `espp::UsbCdc::write()`. +- The log console remains on the USB-Serial-JTAG peripheral (see + `sdkconfig.defaults.esp32s3`). diff --git a/components/usb_device/example/main/CMakeLists.txt b/components/usb_device/example/main/CMakeLists.txt new file mode 100644 index 0000000000..4200fac6aa --- /dev/null +++ b/components/usb_device/example/main/CMakeLists.txt @@ -0,0 +1,5 @@ +idf_component_register( + SRC_DIRS "." + INCLUDE_DIRS "." + REQUIRES usb_device odrive_ascii esp_tinyusb +) diff --git a/components/usb_device/example/main/idf_component.yml b/components/usb_device/example/main/idf_component.yml new file mode 100644 index 0000000000..aa94168041 --- /dev/null +++ b/components/usb_device/example/main/idf_component.yml @@ -0,0 +1,25 @@ +## IDF Component Manager Manifest File - example project overrides. +## +## The component manager is ENABLED for this example so it can fetch the managed +## `espressif/esp_tinyusb` component from the ESP registry. The espp components +## are resolved to their in-repo copies via override_path so the version solver +## does not try to fetch them from the registry. +dependencies: + idf: + version: '>=5.0' + espressif/esp_tinyusb: '>=1.4' + espp/base_component: + version: '*' + override_path: '../../../base_component' + espp/logger: + version: '*' + override_path: '../../../logger' + espp/format: + version: '*' + override_path: '../../../format' + espp/odrive_ascii: + version: '*' + override_path: '../../../odrive_ascii' + espp/usb_device: + version: '*' + override_path: '../..' diff --git a/components/usb_device/example/main/usb_cdc_example.cpp b/components/usb_device/example/main/usb_cdc_example.cpp new file mode 100644 index 0000000000..a1203ff344 --- /dev/null +++ b/components/usb_device/example/main/usb_cdc_example.cpp @@ -0,0 +1,115 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "logger.hpp" +#include "odrive_ascii.hpp" +#include "usb_cdc.hpp" + +using namespace std::chrono_literals; + +extern "C" void app_main(void) { + using namespace espp; + + // The log console stays on the built-in USB-Serial-JTAG / UART (configured via + // sdkconfig). The native USB CDC port created below is a *separate* USB + // interface dedicated to the ODrive ASCII protocol. + Logger logger({.tag = "UsbCdcExample", .level = Logger::Verbosity::INFO}); + + //! [usb_cdc_example] + + // Simulated motor state driven by the ODrive ASCII commands. + struct { + float position = 0.0f; + float velocity = 0.0f; + float torque = 0.0f; + } state; + + // Transport-agnostic ODrive ASCII protocol server. + OdriveAscii::Config proto_cfg; + proto_cfg.log_level = Logger::Verbosity::WARN; + OdriveAscii proto(proto_cfg); + + // Register a couple of demo properties and command callbacks (mirrors the + // odrive_ascii example). + proto.register_float_property( + "axis0.encoder.pos_estimate", [&]() { return state.position; }, + [&](float v, std::error_code &ec) { + ec.clear(); + state.position = v; + return true; + }); + proto.register_float_property("axis0.encoder.vel_estimate", [&]() { return state.velocity; }); + proto.register_float_property( + "axis0.controller.input_pos", [&]() { return state.position; }, + [&](float v, std::error_code &ec) { + ec.clear(); + state.position = v; + return true; + }); + proto.on_position_command([&](int axis, float pos, std::optional vel_ff, + std::optional 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; + }); + proto.on_feedback_request([&](int axis, float &pos_out, float &vel_out, std::error_code &ec) { + (void)axis; + ec.clear(); + pos_out = state.position; + vel_out = state.velocity; + return true; + }); + + // Native USB CDC transport. We create it *before* wiring the RX callback so we + // can capture the instance in the lambda. + UsbCdc::Config usb_cfg; + usb_cfg.vid = 0x1209; // pid.codes VID used by ODrive + usb_cfg.pid = 0x0d32; // ODrive-like PID + usb_cfg.manufacturer = "espp"; + usb_cfg.product = "espp ODrive ASCII"; + usb_cfg.serial_number = "0001"; + usb_cfg.log_level = Logger::Verbosity::INFO; + UsbCdc usb(usb_cfg); + + // Wire: UsbCdc RX -> proto.process_bytes -> UsbCdc.write. + // This callback runs in the TinyUSB device task; process_bytes() is fast and + // write() uses a non-blocking flush, so it is safe to respond inline here. + usb.set_receive_callback([&](std::span data) { + auto response = proto.process_bytes(data); + if (!response.empty()) { + usb.write(response); + } + }); + + std::error_code ec; + if (!usb.initialize(ec)) { + logger.error("Failed to initialize USB CDC: {}", ec.message()); + return; + } + logger.info("Native USB CDC ready. Connect to the ODrive-like serial port and send commands"); + logger.info("e.g. 'r axis0.encoder.pos_estimate' or 'p 0 1.0 0.5 0.1'"); + + //! [usb_cdc_example] + + // Nothing else to do on the main task; the transport runs off the TinyUSB + // task and its RX callback. + while (true) { + std::this_thread::sleep_for(1s); + if (usb.is_connected()) { + logger.debug_rate_limited("USB host connected; pos={} vel={}", state.position, + state.velocity); + } + } +} diff --git a/components/usb_device/example/sdkconfig.defaults b/components/usb_device/example/sdkconfig.defaults new file mode 100644 index 0000000000..cacd1cb436 --- /dev/null +++ b/components/usb_device/example/sdkconfig.defaults @@ -0,0 +1,9 @@ +# 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::UsbCdc component drives a single dedicated CDC-ACM +# interface over the native USB-OTG peripheral. +CONFIG_TINYUSB_CDC_ENABLED=y +CONFIG_TINYUSB_CDC_COUNT=1 diff --git a/components/usb_device/example/sdkconfig.defaults.esp32s3 b/components/usb_device/example/sdkconfig.defaults.esp32s3 new file mode 100644 index 0000000000..bcdec26af9 --- /dev/null +++ b/components/usb_device/example/sdkconfig.defaults.esp32s3 @@ -0,0 +1,7 @@ +# USB-OTG is available on the ESP32-S3 (also S2 / P4). +CONFIG_IDF_TARGET="esp32s3" + +# Keep the log console on the built-in USB-Serial-JTAG peripheral so it stays +# completely separate from the native USB-OTG CDC interface created by +# espp::UsbCdc. (On an ESP32-S3 devkit these are two distinct USB connectors.) +CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG=y diff --git a/components/usb_device/idf_component.yml b/components/usb_device/idf_component.yml new file mode 100644 index 0000000000..379bb4d748 --- /dev/null +++ b/components/usb_device/idf_component.yml @@ -0,0 +1,24 @@ +## IDF Component Manager Manifest File +license: "MIT" +description: "Native USB CDC-ACM transport (esp_tinyusb) with configurable VID/PID for ESP-IDF" +url: "https://github.com/esp-cpp/espp/tree/main/components/usb_device" +repository: "git://github.com/esp-cpp/espp.git" +maintainers: + - William Emfinger +documentation: "https://esp-cpp.github.io/espp/buses/usb_cdc.html" +examples: + - path: example +tags: + - cpp + - Component + - USB + - CDC + - CDC-ACM + - TinyUSB + - Serial + - Transport +dependencies: + idf: + version: '>=5.0' + espp/base_component: '>=1.0' + espressif/esp_tinyusb: '>=1.4' diff --git a/components/usb_device/include/usb_cdc.hpp b/components/usb_device/include/usb_cdc.hpp new file mode 100644 index 0000000000..0ec1e5844c --- /dev/null +++ b/components/usb_device/include/usb_cdc.hpp @@ -0,0 +1,142 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "base_component.hpp" + +namespace espp { + +/** + * @brief Native USB CDC-ACM transport built on ESP-IDF's `esp_tinyusb` managed + * component and the ESP32-S3 / -S2 / -P4 USB-OTG peripheral. + * + * @details `espp::UsbCdc` 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. This lets a device 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). Incoming bytes are delivered to a user callback and outgoing bytes are + * sent via write(). + * + * The class is a thin, idiomatic espp wrapper: it does not throw, reports + * initialization failures via `std::error_code`, and marshals the TinyUSB RX + * callback (which runs in the TinyUSB device task context) into the user's + * receive callback. + * + * @note Only one instance per CDC port should be created. 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. + * + * \section usb_cdc_ex1 UsbCdc Example + * \snippet usb_cdc_example.cpp usb_cdc_example + */ +class UsbCdc : public BaseComponent { +public: + /** + * @brief Callback invoked with received bytes. + * @param data Span of received bytes (valid only for the duration of the call). + */ + using receive_callback_fn = std::function data)>; + + /** + * @brief Configuration for the UsbCdc transport. + */ + struct Config { + 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. */ + espp::Logger::Verbosity log_level{espp::Logger::Verbosity::WARN}; /**< Logger verbosity. */ + }; + + /** + * @brief Construct a UsbCdc transport. Does not touch hardware until + * initialize() is called. + * @param config Configuration parameters. + */ + explicit UsbCdc(const Config &config); + + /** + * @brief Uninstalls the CDC-ACM interface and TinyUSB driver if initialized. + */ + ~UsbCdc(); + + // Non-copyable, non-movable (holds a stable `this` used by the C callback). + UsbCdc(const UsbCdc &) = delete; + UsbCdc &operator=(const UsbCdc &) = delete; + + /** + * @brief Install the TinyUSB driver and initialize the CDC-ACM interface using + * the configured descriptors / VID-PID / strings. + * @param[out] ec Set on failure. + * @return true on success, false otherwise (ec is set). + * @note Safe to call once. Subsequent calls while already initialized are no-ops. + */ + bool initialize(std::error_code &ec); + + /** + * @brief Queue bytes for transmission over the CDC interface and flush. + * @param data Bytes to send. + * @param[out] ec Set on failure (e.g. not initialized). + * @return true if all bytes were queued, false otherwise. + * @note Uses a non-blocking flush; safe to call from the receive callback. + */ + bool write(std::span data, std::error_code &ec); + + /** + * @brief Convenience overload of write() that ignores errors. + * @param data Bytes to send. + * @return true if all bytes were queued, false otherwise. + */ + bool write(std::span data); + + /** + * @brief Set or replace the receive callback. + * @param cb Callback to invoke with received bytes (may be nullptr to detach). + */ + void set_receive_callback(const receive_callback_fn &cb); + + /** + * @brief Whether initialize() has completed successfully. + */ + bool is_initialized() const; + + /** + * @brief Whether a USB host has opened (asserted DTR on) the CDC port. + */ + bool is_connected() const; + + /** + * @brief Internal: drain the CDC RX FIFO and dispatch to the receive callback. + * @note Invoked from the TinyUSB device task via the C callback trampoline. + * Not intended to be called by application code. + */ + void handle_rx(); + +private: + struct Impl; // holds TinyUSB descriptors, kept alive for driver lifetime + std::unique_ptr impl_; + + Config config_; + bool initialized_{false}; + + std::mutex cb_mutex_; + receive_callback_fn on_receive_; +}; + +} // namespace espp diff --git a/components/usb_device/src/usb_cdc.cpp b/components/usb_device/src/usb_cdc.cpp new file mode 100644 index 0000000000..eceabc4b47 --- /dev/null +++ b/components/usb_device/src/usb_cdc.cpp @@ -0,0 +1,225 @@ +#include "usb_cdc.hpp" + +#include +#include +#include + +#include "tinyusb.h" +#include "tinyusb_cdc_acm.h" +#include "tinyusb_default_config.h" + +namespace espp { + +// The CDC port this component uses. A single dedicated CDC-ACM interface. +static constexpr tinyusb_cdcacm_itf_t kCdcPort = TINYUSB_CDC_ACM_0; + +// The TinyUSB CDC RX callback is a plain C function pointer with no user +// argument, so we keep a file-scope pointer to the active instance (per port) +// and marshal into the instance method. Only one UsbCdc per port is supported. +static UsbCdc *s_instances[TINYUSB_CDC_ACM_MAX] = {nullptr}; + +// Storage for the descriptors that TinyUSB references by pointer for the +// lifetime of the driver. These must outlive tinyusb_driver_install(). +struct UsbCdc::Impl { + tusb_desc_device_t device_desc{}; + std::vector config_desc; + // String descriptors: index 0 is the LANGID (0x0409), then manufacturer, + // product, serial, and CDC interface name. We keep the owning strings and an + // array of pointers TinyUSB can read. + std::array langid{{0x09, 0x04}}; + std::string manufacturer; + std::string product; + std::string serial_number; + std::string interface_name; + std::array strings{}; +}; + +UsbCdc::UsbCdc(const Config &config) + : BaseComponent("UsbCdc", config.log_level) + , impl_(std::make_unique()) + , config_(config) + , on_receive_(config.on_receive) {} + +UsbCdc::~UsbCdc() { + if (initialized_) { + tinyusb_cdcacm_deinit(kCdcPort); + tinyusb_driver_uninstall(); + s_instances[kCdcPort] = nullptr; + initialized_ = false; + } +} + +// Static trampoline registered with TinyUSB; runs in the TinyUSB task context. +static void rx_trampoline(int itf, cdcacm_event_t *event) { + (void)event; + if (itf < 0 || itf >= TINYUSB_CDC_ACM_MAX) + return; + UsbCdc *self = s_instances[itf]; + if (self) + self->handle_rx(); +} + +void UsbCdc::handle_rx() { + // Copy the current callback under lock, then invoke it outside the lock. + receive_callback_fn cb; + { + std::scoped_lock lk(cb_mutex_); + cb = on_receive_; + } + if (!cb) + return; + std::vector buf(config_.rx_chunk_size); + size_t rx_size = 0; + // Drain the RX FIFO; a single RX event may hold more than one chunk. + do { + rx_size = 0; + esp_err_t err = tinyusb_cdcacm_read(kCdcPort, buf.data(), buf.size(), &rx_size); + if (err != ESP_OK) { + logger_.error("CDC read error: {}", esp_err_to_name(err)); + break; + } + if (rx_size > 0) + cb(std::span(buf.data(), rx_size)); + } while (rx_size == buf.size()); +} + +bool UsbCdc::initialize(std::error_code &ec) { + ec.clear(); + if (initialized_) { + logger_.warn("Already initialized"); + return true; + } + if (s_instances[kCdcPort] != nullptr) { + logger_.error("CDC port {} already in use by another UsbCdc instance", (int)kCdcPort); + ec = std::make_error_code(std::errc::device_or_resource_busy); + return false; + } + + // Build the string descriptor table (kept alive by impl_). + impl_->manufacturer = config_.manufacturer; + impl_->product = config_.product; + impl_->serial_number = config_.serial_number; + impl_->interface_name = config_.interface_name; + impl_->strings[0] = reinterpret_cast(impl_->langid.data()); + impl_->strings[1] = impl_->manufacturer.c_str(); + impl_->strings[2] = impl_->product.c_str(); + impl_->strings[3] = impl_->serial_number.c_str(); + impl_->strings[4] = impl_->interface_name.c_str(); + + // Build the device descriptor with the configured VID/PID. TUSB_CLASS_MISC + + // IAD is used so that the CDC interface association is exposed correctly. + impl_->device_desc = tusb_desc_device_t{}; + impl_->device_desc.bLength = sizeof(tusb_desc_device_t); + impl_->device_desc.bDescriptorType = TUSB_DESC_DEVICE; + impl_->device_desc.bcdUSB = 0x0200; + impl_->device_desc.bDeviceClass = TUSB_CLASS_MISC; + impl_->device_desc.bDeviceSubClass = MISC_SUBCLASS_COMMON; + impl_->device_desc.bDeviceProtocol = MISC_PROTOCOL_IAD; + impl_->device_desc.bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE; + impl_->device_desc.idVendor = config_.vid; + impl_->device_desc.idProduct = config_.pid; + impl_->device_desc.bcdDevice = 0x0100; + impl_->device_desc.iManufacturer = 0x01; + impl_->device_desc.iProduct = 0x02; + impl_->device_desc.iSerialNumber = 0x03; + impl_->device_desc.bNumConfigurations = 0x01; + + // Build the configuration descriptor: one CDC-ACM interface (2 USB interfaces: + // notification + data). Interface string index 4 matches the strings table. + const uint16_t cdc_desc_len = TUD_CONFIG_DESC_LEN + TUD_CDC_DESC_LEN; + const uint16_t ep_size = (TUD_OPT_HIGH_SPEED ? 512 : 64); + const uint8_t cfg_desc[] = { + // config number, interface count, string index, total length, attribute, power (mA) + TUD_CONFIG_DESCRIPTOR(1, 2, 0, cdc_desc_len, TUSB_DESC_CONFIG_ATT_REMOTE_WAKEUP, 100), + // interface number, string index, EP notification, notif size, EP out, EP in, EP data size + TUD_CDC_DESCRIPTOR(0, 4, 0x81, 8, 0x02, 0x82, ep_size), + }; + impl_->config_desc.assign(cfg_desc, cfg_desc + sizeof(cfg_desc)); + + // Install the TinyUSB driver with our descriptors. + tinyusb_config_t tusb_cfg = TINYUSB_DEFAULT_CONFIG(); + tusb_cfg.descriptor.device = &impl_->device_desc; + tusb_cfg.descriptor.string = impl_->strings.data(); + tusb_cfg.descriptor.string_count = static_cast(impl_->strings.size()); + tusb_cfg.descriptor.full_speed_config = impl_->config_desc.data(); +#if (TUD_OPT_HIGH_SPEED) + tusb_cfg.descriptor.high_speed_config = impl_->config_desc.data(); +#endif + + esp_err_t err = tinyusb_driver_install(&tusb_cfg); + if (err != ESP_OK) { + logger_.error("tinyusb_driver_install failed: {}", esp_err_to_name(err)); + ec = std::make_error_code(std::errc::io_error); + return false; + } + + // Register this instance before initializing CDC so the RX callback can find us. + s_instances[kCdcPort] = this; + + tinyusb_config_cdcacm_t acm_cfg = {}; + acm_cfg.cdc_port = kCdcPort; + acm_cfg.callback_rx = &rx_trampoline; + acm_cfg.callback_rx_wanted_char = nullptr; + acm_cfg.callback_line_state_changed = nullptr; + acm_cfg.callback_line_coding_changed = nullptr; + + err = tinyusb_cdcacm_init(&acm_cfg); + if (err != ESP_OK) { + logger_.error("tinyusb_cdcacm_init failed: {}", esp_err_to_name(err)); + s_instances[kCdcPort] = nullptr; + tinyusb_driver_uninstall(); + ec = std::make_error_code(std::errc::io_error); + return false; + } + + initialized_ = true; + logger_.info("Initialized native USB CDC (VID=0x{:04x} PID=0x{:04x})", config_.vid, config_.pid); + return true; +} + +bool UsbCdc::write(std::span data, std::error_code &ec) { + ec.clear(); + if (!initialized_) { + ec = std::make_error_code(std::errc::not_connected); + return false; + } + size_t offset = 0; + while (offset < data.size()) { + size_t queued = + tinyusb_cdcacm_write_queue(kCdcPort, data.data() + offset, data.size() - offset); + if (queued == 0) { + // The TX buffer is full; flush what we have and retry once. + tinyusb_cdcacm_write_flush(kCdcPort, 0); + queued = tinyusb_cdcacm_write_queue(kCdcPort, data.data() + offset, data.size() - offset); + if (queued == 0) { + logger_.warn_rate_limited("CDC TX buffer full, dropping {} bytes", data.size() - offset); + ec = std::make_error_code(std::errc::no_buffer_space); + break; + } + } + offset += queued; + // Non-blocking flush (timeout 0) - safe to call from within callbacks. + tinyusb_cdcacm_write_flush(kCdcPort, 0); + } + return offset == data.size(); +} + +bool UsbCdc::write(std::span data) { + std::error_code ec; + return write(data, ec); +} + +void UsbCdc::set_receive_callback(const receive_callback_fn &cb) { + std::scoped_lock lk(cb_mutex_); + on_receive_ = cb; +} + +bool UsbCdc::is_initialized() const { return initialized_; } + +bool UsbCdc::is_connected() const { + if (!initialized_) + return false; + return tud_cdc_n_connected(kCdcPort); +} + +} // namespace espp diff --git a/doc/Doxyfile b/doc/Doxyfile index 8573631231..5f76da941d 100755 --- a/doc/Doxyfile +++ b/doc/Doxyfile @@ -151,6 +151,7 @@ EXAMPLE_PATH = \ $(PROJECT_PATH)/components/neopixel/example/main/neopixel_example.cpp \ $(PROJECT_PATH)/components/nvs/example/main/nvs_example.cpp \ $(PROJECT_PATH)/components/odrive_ascii/example/main/odrive_ascii_example.cpp \ + $(PROJECT_PATH)/components/usb_device/example/main/usb_cdc_example.cpp \ $(PROJECT_PATH)/components/pca9535/example/main/pca9535_example.cpp \ $(PROJECT_PATH)/components/pcf85063/example/main/pcf85063_example.cpp \ $(PROJECT_PATH)/components/pid/example/main/pid_example.cpp \ @@ -359,6 +360,7 @@ INPUT = \ $(PROJECT_PATH)/components/meshtastic/include/meshtastic_types.hpp \ $(PROJECT_PATH)/components/mt6701/include/mt6701.hpp \ $(PROJECT_PATH)/components/odrive_ascii/include/odrive_ascii.hpp \ + $(PROJECT_PATH)/components/usb_device/include/usb_cdc.hpp \ $(PROJECT_PATH)/components/pca9535/include/pca9535.hpp \ $(PROJECT_PATH)/components/pcf85063/include/pcf85063.hpp \ $(PROJECT_PATH)/components/pid/include/pid.hpp \ diff --git a/doc/en/buses/index.rst b/doc/en/buses/index.rst index 26a27f8904..7fc5bcda91 100644 --- a/doc/en/buses/index.rst +++ b/doc/en/buses/index.rst @@ -10,3 +10,4 @@ external chips. i2c spi rmt + usb_cdc diff --git a/doc/en/buses/usb_cdc.rst b/doc/en/buses/usb_cdc.rst new file mode 100644 index 0000000000..f0b6d30f7b --- /dev/null +++ b/doc/en/buses/usb_cdc.rst @@ -0,0 +1,64 @@ +USB CDC Transport Component +=========================== + +Overview +-------- + +``espp::UsbCdc`` is a thin, idiomatic wrapper around ESP-IDF's ``esp_tinyusb`` +managed component. It presents a single dedicated **native USB CDC-ACM** (virtual +serial port) interface on the ESP32-S3 / -S2 / -P4 USB-OTG peripheral, with a +**configurable VID/PID** and manufacturer / product / serial strings. + +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. This also lays the groundwork for adding a WebUSB *vendor* +interface later. + +Features +-------- + +- Native USB CDC-ACM interface over USB-OTG, separate from the log console +- Configurable VID, PID, and manufacturer / product / serial / interface strings +- Byte-stream transport: receive callback for RX, ``write()`` for TX +- No exceptions; ``initialize()`` reports failures via ``std::error_code`` +- Safely marshals the TinyUSB RX callback (TinyUSB task context) into the user callback + +Basic Usage +----------- + +.. code-block:: cpp + + espp::UsbCdc::Config cfg; + cfg.vid = 0x1209; // pid.codes VID (ODrive uses this) + cfg.pid = 0x0d32; // ODrive-like PID + cfg.manufacturer = "espp"; + cfg.product = "espp USB CDC"; + cfg.on_receive = [](std::span data) { /* handle rx */ }; + + espp::UsbCdc usb(cfg); + std::error_code ec; + if (!usb.initialize(ec)) { /* handle ec */ } + uint8_t hello[] = {'h', 'i', '\n'}; + usb.write(hello); + +Notes +----- + +- USB-OTG is only available on the ESP32-S2, ESP32-S3 and ESP32-P4 targets. +- Enable ``CONFIG_TINYUSB_CDC_ENABLED=y`` in your project. +- Only one ``espp::UsbCdc`` instance may drive a given CDC port. +- The receive callback runs in the TinyUSB device task; keep it short and non-blocking. + +.. ------------------------------- Example ------------------------------------- + +.. toctree:: + + usb_cdc_example.md + +.. ---------------------------- API Reference ---------------------------------- + +API Reference +------------- + +.. include-build-file:: inc/usb_cdc.inc diff --git a/doc/en/buses/usb_cdc_example.md b/doc/en/buses/usb_cdc_example.md new file mode 100644 index 0000000000..06b6e2b278 --- /dev/null +++ b/doc/en/buses/usb_cdc_example.md @@ -0,0 +1,2 @@ +```{include} ../../../components/usb_device/example/README.md +``` From e50ef491b3915cec778f38e78d85a9759fb8d011 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Sun, 16 Aug 2026 22:42:23 -0500 Subject: [PATCH 02/12] feat(usb_device): vendor + WebUSB interface, composable multi-class device Generalize the usb_device component from a single hard-coded CDC-ACM transport into espp::UsbDevice, which assembles a native USB device from a set of selectable functions (CDC and/or vendor-specific) with interface numbers, endpoint addresses and string indices allocated sequentially and checked against the ESP32-S3 USB-OTG endpoint budget. - Add a vendor-specific interface (bInterfaceClass 0xFF, bulk IN + bulk OUT) carrying a raw byte stream, plus WebUSB + MS OS 2.0 descriptors (BOS, URL descriptor, MS-OS-2.0 set) so browsers/Windows bind driverlessly. - Vendor class enabled via CONFIG_TINYUSB_VENDOR_COUNT>0 (CFG_TUD_VENDOR); all tud_vendor_* paths are #if-guarded so CDC-only builds still link. - Keep espp::UsbCdc as a thin CDC-only preset over UsbDevice (back-compat). - Reserve HID/MSC extension points and document the endpoint budget table. - Update example to a composite CDC + Vendor/WebUSB device feeding one OdriveAscii; docs (README, rst, Doxyfile) updated. esp32s3 build passes. Co-Authored-By: Claude Opus 4.8 --- components/usb_device/README.md | 167 +++-- components/usb_device/example/README.md | 53 +- .../example/main/usb_cdc_example.cpp | 64 +- .../usb_device/example/sdkconfig.defaults | 8 +- components/usb_device/idf_component.yml | 4 +- components/usb_device/include/usb_cdc.hpp | 43 +- components/usb_device/include/usb_device.hpp | 247 +++++++ components/usb_device/src/usb_cdc.cpp | 228 +----- components/usb_device/src/usb_device.cpp | 697 ++++++++++++++++++ doc/Doxyfile | 1 + doc/en/buses/usb_cdc.rst | 154 +++- 11 files changed, 1332 insertions(+), 334 deletions(-) create mode 100644 components/usb_device/include/usb_device.hpp create mode 100644 components/usb_device/src/usb_device.cpp diff --git a/components/usb_device/README.md b/components/usb_device/README.md index 92db230405..d3f7fc725a 100644 --- a/components/usb_device/README.md +++ b/components/usb_device/README.md @@ -1,24 +1,39 @@ -# USB CDC Transport Component +# USB Device Component [![Badge](https://components.espressif.com/components/espp/usb_device/badge.svg)](https://components.espressif.com/components/espp/usb_device) -`espp::UsbCdc` is a thin, idiomatic wrapper around ESP-IDF's `esp_tinyusb` -managed component that presents a single dedicated **native USB CDC-ACM** -(virtual serial port) interface on the ESP32-S3 / -S2 / -P4 USB-OTG peripheral, -with a **configurable VID/PID** and manufacturer / product / serial strings. +`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. -Because the CDC interface uses the native USB-OTG peripheral (not the built-in -USB-Serial-JTAG that carries the ESP console), a device can advertise its own USB -identifiers (e.g. ODrive-like) on a link that is completely separate from the -logging console. This also lays the groundwork for adding a WebUSB *vendor* -interface later. +Today it can enable, in any combination (subject to the endpoint budget): + +- A **CDC-ACM** function (virtual serial port). +- A **vendor-specific** function (`bInterfaceClass` 0xFF, one bulk IN + one bulk + OUT) carrying a raw byte stream, optionally advertising **WebUSB** + **MS OS + 2.0** descriptors so a browser can talk to it driverlessly (and Windows binds + WinUSB with no driver). + +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. Because it uses the native USB-OTG peripheral (not the +built-in USB-Serial-JTAG that carries the ESP console), a device can advertise its +own USB identifiers (e.g. ODrive-like) on a link that is completely separate from +the logging console. + +`espp::UsbCdc` is retained as a thin **CDC-only preset** over `espp::UsbDevice` +for back-compatibility. **Table of Contents** -- [USB CDC Transport Component](#usb-cdc-transport-component) +- [USB Device Component](#usb-device-component) - [Features](#features) - [API](#api) + - [Enabling the vendor / WebUSB class](#enabling-the-vendor--webusb-class) + - [Endpoint budget (ESP32-S3 USB-OTG)](#endpoint-budget-esp32-s3-usb-otg) + - [Extending with HID / MSC](#extending-with-hid--msc) - [Example](#example) - [Notes](#notes) @@ -26,52 +41,120 @@ interface later. ## Features -- **Native USB**: uses the USB-OTG peripheral via TinyUSB, separate from the log console. -- **Configurable identity**: VID, PID, manufacturer / product / serial / interface strings. -- **Byte-stream transport**: feed bytes in via a receive callback, send bytes out via `write()`. -- **Idiomatic espp**: no exceptions; `initialize()` reports failures via `std::error_code`. -- **Safe marshaling**: the TinyUSB RX callback (which runs in the TinyUSB task) is drained and - delivered to the user callback; `write()` is safe to call from within it. +- **Composable**: enable a CDC function and/or a vendor/WebUSB function (composite). +- **Vendor-specific interface** (class 0xFF): raw bulk IN + bulk OUT byte stream. +- **WebUSB**: BOS + WebUSB URL + MS OS 2.0 descriptors for driverless browser + access, with a configurable landing-page URL. +- **Sequential allocation** of interfaces / endpoints / strings with an + endpoint-budget check (error via `std::error_code` if exceeded). +- **Configurable identity**: VID, PID, manufacturer / product / serial / interface + strings. +- **Idiomatic espp**: no exceptions; `initialize()` reports failures via + `std::error_code`. +- **Safe marshaling**: the TinyUSB RX callbacks (TinyUSB task context) are drained + and delivered to per-function user callbacks; the matching `write_*()` is safe to + call from within them. ## API +Composite CDC + vendor/WebUSB device (both interfaces carry the same raw stream): + ```cpp -espp::UsbCdc::Config cfg; -cfg.vid = 0x1209; // pid.codes VID (ODrive uses this) -cfg.pid = 0x0d32; // ODrive-like PID -cfg.manufacturer = "espp"; -cfg.product = "espp USB CDC"; -cfg.serial_number = "0001"; -cfg.on_receive = [](std::span data) { /* handle rx */ }; - -espp::UsbCdc usb(cfg); +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 data) { /* serial rx */ }; +cfg.cdc = cdc; + +espp::UsbDevice::VendorFunction vendor; +vendor.webusb = true; // advertise WebUSB / MS OS 2.0 descriptors +// vendor.landing_page_url defaults to the espp docs-hosted ODrive WebUSB console, +// without a scheme; vendor.url_scheme selects http (0) or https (1). +vendor.on_receive = [&](std::span data) { /* vendor rx */ }; +cfg.vendor = vendor; + +espp::UsbDevice usb(cfg); std::error_code ec; -if (!usb.initialize(ec)) { /* handle ec */ } +if (!usb.initialize(ec)) { /* handle ec (e.g. endpoint budget exceeded) */ } -// send bytes uint8_t hello[] = {'h','i','\n'}; -usb.write(hello); - -// replace the receive callback at any time -usb.set_receive_callback([](std::span data) { /* ... */ }); +usb.write_cdc(hello); +usb.write_vendor(hello); ``` Key methods: -- `bool initialize(std::error_code &ec)` — install the TinyUSB driver + CDC-ACM and set descriptors. -- `bool write(std::span data[, std::error_code &ec])` — queue + non-blocking flush. -- `void set_receive_callback(const receive_callback_fn &cb)` — set/replace the RX callback. -- `bool is_initialized() const`, `bool is_connected() const`. +- `bool initialize(std::error_code &ec)` — build descriptors from the enabled + functions, check the endpoint budget, install the TinyUSB driver. +- `bool write_cdc(...)` / `bool write_vendor(...)` — queue + non-blocking flush on + the respective interface. +- `void set_cdc_receive_callback(...)` / `void set_vendor_receive_callback(...)`. +- `bool is_cdc_connected() const` / `bool is_vendor_connected() const`. + +CDC-only preset (`espp::UsbCdc`, unchanged API): `initialize()`, `write()`, +`set_receive_callback()`, `is_connected()`. + +## Enabling the vendor / WebUSB class + +The vendor class is gated in `esp_tinyusb` behind a Kconfig option. To use the +vendor function, set in your project's `sdkconfig.defaults`: + +``` +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` > 0 makes `esp_tinyusb` define +`CFG_TUD_VENDOR` and compile the TinyUSB vendor class driver. No custom +`tusb_config` is needed — the BOS descriptor and the WebUSB / MS-OS-2.0 vendor +control requests are provided by `espp::UsbDevice` through the standard TinyUSB +weak-callback overrides (`tud_descriptor_bos_cb`, `tud_vendor_control_xfer_cb`, +`tud_vendor_rx_cb`). If the vendor function is requested but `CFG_TUD_VENDOR == 0`, +`initialize()` fails with `std::errc::function_not_supported`. + +## Endpoint budget (ESP32-S3 USB-OTG) + +The ESP32-S3 / -S2 USB-OTG core is full-speed and, besides 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 notif + 1 bulk-IN) | 1 (bulk-OUT) | +| Vendor / WebUSB | 1 (bulk-IN) | 1 (bulk-OUT) | +| HID (future) | 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, +CDC+Vendor+MSC. Enabling CDC+Vendor+HID+MSC reaches 5 IN endpoints — at the hard +limit, not recommended. `initialize()` returns `std::errc::value_too_large` if the +IN or OUT budget is exceeded. + +## Extending with HID / MSC + +`espp::UsbDevice::Config` reserves `std::optional` slots for `HidFunction` and +`MscFunction` as documented extension points. They are not implemented yet; +enabling one today makes `initialize()` fail with +`std::errc::function_not_supported`. When implemented they slot into the same +sequential allocator: HID appends one interface (report descriptor + report +get/set callbacks) claiming an interrupt-IN endpoint; MSC appends one interface +(SCSI + storage read/write/capacity callbacks) claiming a bulk IN + bulk OUT. ## Example -See `example/` for a full project that wires `espp::UsbCdc` to `espp::OdriveAscii` -so the device shows up as an ODrive-like serial port speaking the ODrive ASCII -protocol, while the log console stays on the USB-Serial-JTAG peripheral. +See `example/` for a full project that wires a **composite CDC + Vendor/WebUSB** +`espp::UsbDevice` to the transport-agnostic `espp::OdriveAscii` protocol server. +Both interfaces feed the same server (RX from either interface → `process_bytes` +→ response written back out the same interface), while the log console stays on +the USB-Serial-JTAG peripheral. ## Notes - USB-OTG is only available on the ESP32-S2, ESP32-S3 and ESP32-P4 targets. -- Enable `CONFIG_TINYUSB_CDC_ENABLED=y` in your project (see the example `sdkconfig.defaults`). -- Only one `espp::UsbCdc` instance may drive a given CDC port. -- The receive callback runs in the TinyUSB device task; keep it short and non-blocking. +- Only one `espp::UsbDevice` / `espp::UsbCdc` instance may exist at a time. +- The receive callbacks run in the TinyUSB device task; keep them short and + non-blocking. diff --git a/components/usb_device/example/README.md b/components/usb_device/example/README.md index 2407bcca71..8048f102ab 100644 --- a/components/usb_device/example/README.md +++ b/components/usb_device/example/README.md @@ -1,14 +1,19 @@ -# USB CDC + ODrive ASCII Example +# USB Device (CDC + Vendor/WebUSB) + ODrive ASCII Example -This example demonstrates the `espp::UsbCdc` native-USB CDC-ACM transport wired to -the transport-agnostic `espp::OdriveAscii` protocol server. The device enumerates -as a dedicated USB serial port with an ODrive-like VID/PID (0x1209 / 0x0d32), -separate from the log console which stays on the USB-Serial-JTAG peripheral. +This example demonstrates a **composite** `espp::UsbDevice` that exposes both a +**CDC-ACM serial** interface and a **vendor-specific / WebUSB** interface, both +wired to the transport-agnostic `espp::OdriveAscii` protocol server. The device +enumerates with an ODrive-like VID/PID (0x1209 / 0x0d32), separate from the log +console which stays on the USB-Serial-JTAG peripheral. + +Both interfaces carry the identical raw ODrive ASCII byte stream: RX from either +interface is fed to `process_bytes()`, and the response is written back out the +same interface. **Table of Contents** -- [USB CDC + ODrive ASCII Example](#usb-cdc--odrive-ascii-example) +- [USB Device (CDC + Vendor/WebUSB) + ODrive ASCII Example](#usb-device-cdc--vendorwebusb--odrive-ascii-example) - [Requirements](#requirements) - [Build](#build) - [Flash and Monitor](#flash-and-monitor) @@ -24,6 +29,14 @@ separate from the log console which stays on the USB-Serial-JTAG peripheral. - 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 both the CDC and vendor classes: + +``` +CONFIG_TINYUSB_CDC_ENABLED=y +CONFIG_TINYUSB_CDC_COUNT=1 +CONFIG_TINYUSB_VENDOR_COUNT=1 +``` + ## Build ```sh @@ -35,35 +48,43 @@ idf.py build ## Flash and Monitor Flash / monitor over the USB-Serial-JTAG (or UART) console, which is kept separate -from the native USB CDC interface: +from the native USB interfaces: ```sh idf.py flash monitor ``` -The native USB-OTG connector will appear on the host as a new serial port with -manufacturer "espp" and product "espp ODrive ASCII". +The native USB-OTG connector will appear on the host as a new composite device: +a serial port (CDC) plus a vendor interface (WebUSB), manufacturer "espp", +product "espp ODrive ASCII". ## Usage -Open the native USB serial port and send ODrive ASCII commands, e.g. from Python: +Serial: open the CDC serial port and send ODrive ASCII commands, e.g. from Python: ```python import serial -# The port that enumerated with VID 0x1209 / PID 0x0d32 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()) ``` +WebUSB: from a Chromium-based browser, open the WebUSB console and connect to the +vendor interface (class 0xFF, bulk IN + bulk OUT). The same ODrive ASCII commands +work over the vendor byte stream. The BOS/WebUSB descriptors point to a +configurable landing-page URL (default: the espp docs-hosted ODrive WebUSB +console). + ## How it works -- `espp::UsbCdc` installs the TinyUSB driver and a single CDC-ACM interface with - the configured VID/PID/strings. -- Its receive callback feeds incoming bytes to `espp::OdriveAscii::process_bytes()`. -- The returned response bytes are written back out over `espp::UsbCdc::write()`. +- `espp::UsbDevice` installs the TinyUSB driver and builds descriptors for the + enabled CDC + vendor 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 interface's receive callback feeds incoming bytes to + `espp::OdriveAscii::process_bytes()` and writes the response back out that same + interface (`write_cdc()` / `write_vendor()`). - The log console remains on the USB-Serial-JTAG peripheral (see `sdkconfig.defaults.esp32s3`). diff --git a/components/usb_device/example/main/usb_cdc_example.cpp b/components/usb_device/example/main/usb_cdc_example.cpp index a1203ff344..4279eba403 100644 --- a/components/usb_device/example/main/usb_cdc_example.cpp +++ b/components/usb_device/example/main/usb_cdc_example.cpp @@ -10,7 +10,7 @@ #include "logger.hpp" #include "odrive_ascii.hpp" -#include "usb_cdc.hpp" +#include "usb_device.hpp" using namespace std::chrono_literals; @@ -18,9 +18,10 @@ extern "C" void app_main(void) { using namespace espp; // The log console stays on the built-in USB-Serial-JTAG / UART (configured via - // sdkconfig). The native USB CDC port created below is a *separate* USB - // interface dedicated to the ODrive ASCII protocol. - Logger logger({.tag = "UsbCdcExample", .level = Logger::Verbosity::INFO}); + // sdkconfig). The native USB device created below is a *separate* USB + // peripheral that exposes two interfaces (CDC serial + vendor/WebUSB), both + // dedicated to the ODrive ASCII protocol. + Logger logger({.tag = "UsbDeviceExample", .level = Logger::Verbosity::INFO}); //! [usb_cdc_example] @@ -31,7 +32,8 @@ extern "C" void app_main(void) { float torque = 0.0f; } state; - // Transport-agnostic ODrive ASCII protocol server. + // Transport-agnostic ODrive ASCII protocol server. Both USB interfaces feed + // the same server. OdriveAscii::Config proto_cfg; proto_cfg.log_level = Logger::Verbosity::WARN; OdriveAscii proto(proto_cfg); @@ -72,42 +74,62 @@ extern "C" void app_main(void) { return true; }); - // Native USB CDC transport. We create it *before* wiring the RX callback so we - // can capture the instance in the lambda. - UsbCdc::Config usb_cfg; + // Composite native USB device: CDC serial + vendor-specific (WebUSB) function. + // We create it *before* wiring the RX callbacks so the callbacks can capture + // the instance and write the response back out the *same* interface. + UsbDevice::Config usb_cfg; usb_cfg.vid = 0x1209; // pid.codes VID used by ODrive usb_cfg.pid = 0x0d32; // ODrive-like PID usb_cfg.manufacturer = "espp"; usb_cfg.product = "espp ODrive ASCII"; usb_cfg.serial_number = "0001"; usb_cfg.log_level = Logger::Verbosity::INFO; - UsbCdc usb(usb_cfg); - // Wire: UsbCdc RX -> proto.process_bytes -> UsbCdc.write. - // This callback runs in the TinyUSB device task; process_bytes() is fast and - // write() uses a non-blocking flush, so it is safe to respond inline here. - usb.set_receive_callback([&](std::span data) { + // CDC serial function. + UsbDevice::CdcFunction cdc; + cdc.interface_name = "espp ODrive CDC"; + usb_cfg.cdc = cdc; + + // Vendor-specific function with WebUSB so a browser can talk to it driverlessly. + // The landing page defaults to the espp docs-hosted ODrive WebUSB console. + UsbDevice::VendorFunction vendor; + vendor.interface_name = "espp ODrive WebUSB"; + vendor.webusb = true; // advertise BOS / WebUSB / MS OS 2.0 descriptors + usb_cfg.vendor = vendor; + + UsbDevice usb(usb_cfg); + + // Wire: CDC RX -> proto.process_bytes -> CDC write. + usb.set_cdc_receive_callback([&](std::span data) { auto response = proto.process_bytes(data); - if (!response.empty()) { - usb.write(response); - } + if (!response.empty()) + usb.write_cdc(response); + }); + + // Wire: Vendor RX -> proto.process_bytes -> Vendor write (identical payload). + usb.set_vendor_receive_callback([&](std::span data) { + auto response = proto.process_bytes(data); + if (!response.empty()) + usb.write_vendor(response); }); std::error_code ec; if (!usb.initialize(ec)) { - logger.error("Failed to initialize USB CDC: {}", ec.message()); + logger.error("Failed to initialize USB device: {}", ec.message()); return; } - logger.info("Native USB CDC ready. Connect to the ODrive-like serial port and send commands"); - logger.info("e.g. 'r axis0.encoder.pos_estimate' or 'p 0 1.0 0.5 0.1'"); + logger.info("Native USB device ready (CDC serial + vendor/WebUSB)."); + logger.info("Serial: connect to the ODrive-like port and send commands, e.g."); + logger.info(" 'r axis0.encoder.pos_estimate' or 'p 0 1.0 0.5 0.1'"); + logger.info("WebUSB: open the browser console and connect to the vendor interface."); //! [usb_cdc_example] // Nothing else to do on the main task; the transport runs off the TinyUSB - // task and its RX callback. + // task and its RX callbacks. while (true) { std::this_thread::sleep_for(1s); - if (usb.is_connected()) { + if (usb.is_cdc_connected() || usb.is_vendor_connected()) { logger.debug_rate_limited("USB host connected; pos={} vel={}", state.position, state.velocity); } diff --git a/components/usb_device/example/sdkconfig.defaults b/components/usb_device/example/sdkconfig.defaults index cacd1cb436..0f8a186167 100644 --- a/components/usb_device/example/sdkconfig.defaults +++ b/components/usb_device/example/sdkconfig.defaults @@ -3,7 +3,13 @@ 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::UsbCdc component drives a single dedicated CDC-ACM +# 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 diff --git a/components/usb_device/idf_component.yml b/components/usb_device/idf_component.yml index 379bb4d748..282c6e5255 100644 --- a/components/usb_device/idf_component.yml +++ b/components/usb_device/idf_component.yml @@ -1,6 +1,6 @@ ## IDF Component Manager Manifest File license: "MIT" -description: "Native USB CDC-ACM transport (esp_tinyusb) with configurable VID/PID for ESP-IDF" +description: "Composable native USB device (esp_tinyusb): CDC-ACM + vendor-specific/WebUSB with configurable VID/PID for ESP-IDF" url: "https://github.com/esp-cpp/espp/tree/main/components/usb_device" repository: "git://github.com/esp-cpp/espp.git" maintainers: @@ -14,6 +14,8 @@ tags: - USB - CDC - CDC-ACM + - Vendor + - WebUSB - TinyUSB - Serial - Transport diff --git a/components/usb_device/include/usb_cdc.hpp b/components/usb_device/include/usb_cdc.hpp index 0ec1e5844c..e7e046c88c 100644 --- a/components/usb_device/include/usb_cdc.hpp +++ b/components/usb_device/include/usb_cdc.hpp @@ -3,34 +3,32 @@ #include #include #include -#include #include #include #include #include "base_component.hpp" +#include "usb_device.hpp" namespace espp { /** - * @brief Native USB CDC-ACM transport built on ESP-IDF's `esp_tinyusb` managed - * component and the ESP32-S3 / -S2 / -P4 USB-OTG peripheral. + * @brief Native USB CDC-ACM transport: a thin CDC-only preset over + * `espp::UsbDevice`. * * @details `espp::UsbCdc` 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. This lets a device 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). Incoming bytes are delivered to a user callback and outgoing bytes are - * sent via write(). + * 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. * - * The class is a thin, idiomatic espp wrapper: it does not throw, reports - * initialization failures via `std::error_code`, and marshals the TinyUSB RX - * callback (which runs in the TinyUSB device task context) into the user's - * receive callback. + * 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`. * - * @note Only one instance per CDC port should be created. USB-OTG is only - * available on the ESP32-S2, ESP32-S3 and ESP32-P4 targets. + * @note Only one `espp::UsbCdc` / `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. @@ -85,7 +83,6 @@ class UsbCdc : public BaseComponent { * the configured descriptors / VID-PID / strings. * @param[out] ec Set on failure. * @return true on success, false otherwise (ec is set). - * @note Safe to call once. Subsequent calls while already initialized are no-ops. */ bool initialize(std::error_code &ec); @@ -94,7 +91,6 @@ class UsbCdc : public BaseComponent { * @param data Bytes to send. * @param[out] ec Set on failure (e.g. not initialized). * @return true if all bytes were queued, false otherwise. - * @note Uses a non-blocking flush; safe to call from the receive callback. */ bool write(std::span data, std::error_code &ec); @@ -121,22 +117,9 @@ class UsbCdc : public BaseComponent { */ bool is_connected() const; - /** - * @brief Internal: drain the CDC RX FIFO and dispatch to the receive callback. - * @note Invoked from the TinyUSB device task via the C callback trampoline. - * Not intended to be called by application code. - */ - void handle_rx(); - private: - struct Impl; // holds TinyUSB descriptors, kept alive for driver lifetime - std::unique_ptr impl_; - Config config_; - bool initialized_{false}; - - std::mutex cb_mutex_; - receive_callback_fn on_receive_; + std::unique_ptr device_; }; } // namespace espp diff --git a/components/usb_device/include/usb_device.hpp b/components/usb_device/include/usb_device.hpp new file mode 100644 index 0000000000..33192efc45 --- /dev/null +++ b/components/usb_device/include/usb_device.hpp @@ -0,0 +1,247 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "base_component.hpp" + +namespace espp { + +/** + * @brief Composable native-USB device built on ESP-IDF's `esp_tinyusb` managed + * component and the ESP32-S3 / -S2 / -P4 USB-OTG peripheral. + * + * @details `espp::UsbDevice` 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. 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 leaves room for **HID** and **MSC** functions to be added later + * without changing the descriptor-building model (see `HidFunction` / + * `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. + * + * @note Only one `espp::UsbDevice` (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. + * + * \section usb_device_ex1 UsbDevice (composite CDC + Vendor/WebUSB) Example + * \snippet usb_cdc_example.cpp usb_cdc_example + */ +class UsbDevice : public BaseComponent { +public: + /** + * @brief Callback invoked with received bytes. + * @param data Span of received bytes (valid only for the duration of the call). + */ + using receive_callback_fn = std::function data)>; + + /** + * @brief 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). + */ + struct CdcFunction { + 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. */ + }; + + /** + * @brief 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. + */ + struct VendorFunction { + 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. */ + /** + * @brief WebUSB landing-page URL, *without* a scheme (the scheme is encoded + * separately via `url_scheme`). Defaults to the espp docs-hosted + * ODrive WebUSB console. + */ + std::string landing_page_url{"esp-cpp.github.io/espp/apps/odrive_webusb_console.html"}; + 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. */ + }; + + /** + * @brief (Future) HID function extension point. Not implemented yet. + * + * A HID function consumes 1 interrupt IN endpoint (and optionally 1 interrupt + * OUT). When implemented it will carry a HID report descriptor plus report + * get/set callbacks. Enabling it today makes initialize() fail with + * `std::errc::function_not_supported` so the API slot is reserved without + * silently doing nothing. + */ + struct HidFunction { + std::string interface_name{"espp HID"}; + std::vector report_descriptor{}; /**< HID report descriptor bytes. */ + bool has_out_endpoint{false}; /**< Whether to allocate an interrupt OUT endpoint. */ + }; + + /** + * @brief (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 MscFunction { + std::string interface_name{"espp MSC"}; + // Future: SCSI inquiry strings + read/write/capacity callbacks. + }; + + /** + * @brief Configuration for the composable UsbDevice. + */ + struct Config { + 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 cdc{}; /**< Enable a CDC-ACM function. */ + std::optional vendor{}; /**< Enable a vendor-specific / WebUSB function. */ + std::optional hid{}; /**< (Future) enable a HID function. */ + std::optional msc{}; /**< (Future) enable an MSC function. */ + + espp::Logger::Verbosity log_level{espp::Logger::Verbosity::WARN}; /**< Logger verbosity. */ + }; + + /** + * @brief Construct a UsbDevice. Does not touch hardware until initialize(). + * @param config Configuration parameters. + */ + explicit UsbDevice(const Config &config); + + /** + * @brief Uninstalls the enabled functions and the TinyUSB driver if initialized. + */ + ~UsbDevice(); + + // Non-copyable, non-movable (holds a stable `this` used by the C callbacks). + UsbDevice(const UsbDevice &) = delete; + UsbDevice &operator=(const UsbDevice &) = delete; + + /** + * @brief Install the TinyUSB driver and initialize the enabled functions using + * the configured descriptors / VID-PID / strings. + * @param[out] ec Set on failure (invalid config, endpoint budget exceeded, + * driver install failure, or unsupported function requested). + * @return true on success, false otherwise (ec is set). + */ + bool initialize(std::error_code &ec); + + /** + * @brief Queue bytes for transmission over the CDC function and flush. + * @param data Bytes to send. + * @param[out] ec Set on failure (e.g. CDC not enabled / not initialized). + * @return true if all bytes were queued, false otherwise. + */ + bool write_cdc(std::span data, std::error_code &ec); + + /// @brief Convenience overload of write_cdc() that ignores errors. + bool write_cdc(std::span data); + + /** + * @brief Queue bytes for transmission over the vendor function and flush. + * @param data Bytes to send. + * @param[out] ec Set on failure (e.g. vendor not enabled / not initialized). + * @return true if all bytes were queued, false otherwise. + */ + bool write_vendor(std::span data, std::error_code &ec); + + /// @brief Convenience overload of write_vendor() that ignores errors. + bool write_vendor(std::span data); + + /// @brief Set or replace the CDC receive callback (nullptr to detach). + void set_cdc_receive_callback(const receive_callback_fn &cb); + + /// @brief Set or replace the vendor receive callback (nullptr to detach). + void set_vendor_receive_callback(const receive_callback_fn &cb); + + /// @brief Whether initialize() has completed successfully. + bool is_initialized() const; + + /// @brief Whether the CDC function is enabled and a host has asserted DTR. + bool is_cdc_connected() const; + + /// @brief Whether the vendor function is enabled and the device is mounted. + bool is_vendor_connected() const; + + // + // Internal: invoked from the TinyUSB device task via C trampolines / weak + // overrides. Not intended to be called by application code. + // + + /// @brief Internal: drain the CDC RX FIFO and dispatch to the CDC callback. + void handle_cdc_rx(); + + /// @brief Internal: drain the vendor RX FIFO and dispatch to the vendor callback. + void handle_vendor_rx(); + + /// @brief Internal: pointer to the BOS descriptor bytes (nullptr if none). + const uint8_t *bos_descriptor() const; + + /// @brief Internal: pointer to the MS OS 2.0 descriptor bytes (nullptr if none). + const uint8_t *ms_os_20_descriptor(uint16_t &total_len) const; + + /// @brief Internal: pointer to the WebUSB URL descriptor bytes (nullptr if none). + const uint8_t *webusb_url_descriptor(uint8_t &length) const; + + /// @brief Internal: config for the vendor control-request handler. + const std::optional &vendor_config() const { return config_.vendor; } + + /// @brief Internal: the singleton instance handling the global USB callbacks. + static UsbDevice *instance(); + +private: + struct Impl; // holds TinyUSB descriptors, kept alive for driver lifetime + std::unique_ptr impl_; + + Config config_; + bool initialized_{false}; + + std::mutex cb_mutex_; + receive_callback_fn on_cdc_receive_; + receive_callback_fn on_vendor_receive_; +}; + +} // namespace espp diff --git a/components/usb_device/src/usb_cdc.cpp b/components/usb_device/src/usb_cdc.cpp index eceabc4b47..b30185d4b1 100644 --- a/components/usb_device/src/usb_cdc.cpp +++ b/components/usb_device/src/usb_cdc.cpp @@ -1,225 +1,45 @@ #include "usb_cdc.hpp" -#include -#include -#include - -#include "tinyusb.h" -#include "tinyusb_cdc_acm.h" -#include "tinyusb_default_config.h" - namespace espp { -// The CDC port this component uses. A single dedicated CDC-ACM interface. -static constexpr tinyusb_cdcacm_itf_t kCdcPort = TINYUSB_CDC_ACM_0; - -// The TinyUSB CDC RX callback is a plain C function pointer with no user -// argument, so we keep a file-scope pointer to the active instance (per port) -// and marshal into the instance method. Only one UsbCdc per port is supported. -static UsbCdc *s_instances[TINYUSB_CDC_ACM_MAX] = {nullptr}; - -// Storage for the descriptors that TinyUSB references by pointer for the -// lifetime of the driver. These must outlive tinyusb_driver_install(). -struct UsbCdc::Impl { - tusb_desc_device_t device_desc{}; - std::vector config_desc; - // String descriptors: index 0 is the LANGID (0x0409), then manufacturer, - // product, serial, and CDC interface name. We keep the owning strings and an - // array of pointers TinyUSB can read. - std::array langid{{0x09, 0x04}}; - std::string manufacturer; - std::string product; - std::string serial_number; - std::string interface_name; - std::array strings{}; -}; +// Translate the CDC-only Config into a UsbDevice::Config with a single CDC function. +static UsbDevice::Config make_device_config(const UsbCdc::Config &c) { + UsbDevice::Config dc; + dc.vid = c.vid; + dc.pid = c.pid; + dc.manufacturer = c.manufacturer; + dc.product = c.product; + dc.serial_number = c.serial_number; + dc.log_level = c.log_level; + UsbDevice::CdcFunction cdc; + cdc.interface_name = c.interface_name; + cdc.on_receive = c.on_receive; + cdc.rx_chunk_size = c.rx_chunk_size; + dc.cdc = cdc; + return dc; +} UsbCdc::UsbCdc(const Config &config) : BaseComponent("UsbCdc", config.log_level) - , impl_(std::make_unique()) , config_(config) - , on_receive_(config.on_receive) {} - -UsbCdc::~UsbCdc() { - if (initialized_) { - tinyusb_cdcacm_deinit(kCdcPort); - tinyusb_driver_uninstall(); - s_instances[kCdcPort] = nullptr; - initialized_ = false; - } -} - -// Static trampoline registered with TinyUSB; runs in the TinyUSB task context. -static void rx_trampoline(int itf, cdcacm_event_t *event) { - (void)event; - if (itf < 0 || itf >= TINYUSB_CDC_ACM_MAX) - return; - UsbCdc *self = s_instances[itf]; - if (self) - self->handle_rx(); -} - -void UsbCdc::handle_rx() { - // Copy the current callback under lock, then invoke it outside the lock. - receive_callback_fn cb; - { - std::scoped_lock lk(cb_mutex_); - cb = on_receive_; - } - if (!cb) - return; - std::vector buf(config_.rx_chunk_size); - size_t rx_size = 0; - // Drain the RX FIFO; a single RX event may hold more than one chunk. - do { - rx_size = 0; - esp_err_t err = tinyusb_cdcacm_read(kCdcPort, buf.data(), buf.size(), &rx_size); - if (err != ESP_OK) { - logger_.error("CDC read error: {}", esp_err_to_name(err)); - break; - } - if (rx_size > 0) - cb(std::span(buf.data(), rx_size)); - } while (rx_size == buf.size()); -} + , device_(std::make_unique(make_device_config(config))) {} -bool UsbCdc::initialize(std::error_code &ec) { - ec.clear(); - if (initialized_) { - logger_.warn("Already initialized"); - return true; - } - if (s_instances[kCdcPort] != nullptr) { - logger_.error("CDC port {} already in use by another UsbCdc instance", (int)kCdcPort); - ec = std::make_error_code(std::errc::device_or_resource_busy); - return false; - } +UsbCdc::~UsbCdc() = default; - // Build the string descriptor table (kept alive by impl_). - impl_->manufacturer = config_.manufacturer; - impl_->product = config_.product; - impl_->serial_number = config_.serial_number; - impl_->interface_name = config_.interface_name; - impl_->strings[0] = reinterpret_cast(impl_->langid.data()); - impl_->strings[1] = impl_->manufacturer.c_str(); - impl_->strings[2] = impl_->product.c_str(); - impl_->strings[3] = impl_->serial_number.c_str(); - impl_->strings[4] = impl_->interface_name.c_str(); - - // Build the device descriptor with the configured VID/PID. TUSB_CLASS_MISC + - // IAD is used so that the CDC interface association is exposed correctly. - impl_->device_desc = tusb_desc_device_t{}; - impl_->device_desc.bLength = sizeof(tusb_desc_device_t); - impl_->device_desc.bDescriptorType = TUSB_DESC_DEVICE; - impl_->device_desc.bcdUSB = 0x0200; - impl_->device_desc.bDeviceClass = TUSB_CLASS_MISC; - impl_->device_desc.bDeviceSubClass = MISC_SUBCLASS_COMMON; - impl_->device_desc.bDeviceProtocol = MISC_PROTOCOL_IAD; - impl_->device_desc.bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE; - impl_->device_desc.idVendor = config_.vid; - impl_->device_desc.idProduct = config_.pid; - impl_->device_desc.bcdDevice = 0x0100; - impl_->device_desc.iManufacturer = 0x01; - impl_->device_desc.iProduct = 0x02; - impl_->device_desc.iSerialNumber = 0x03; - impl_->device_desc.bNumConfigurations = 0x01; - - // Build the configuration descriptor: one CDC-ACM interface (2 USB interfaces: - // notification + data). Interface string index 4 matches the strings table. - const uint16_t cdc_desc_len = TUD_CONFIG_DESC_LEN + TUD_CDC_DESC_LEN; - const uint16_t ep_size = (TUD_OPT_HIGH_SPEED ? 512 : 64); - const uint8_t cfg_desc[] = { - // config number, interface count, string index, total length, attribute, power (mA) - TUD_CONFIG_DESCRIPTOR(1, 2, 0, cdc_desc_len, TUSB_DESC_CONFIG_ATT_REMOTE_WAKEUP, 100), - // interface number, string index, EP notification, notif size, EP out, EP in, EP data size - TUD_CDC_DESCRIPTOR(0, 4, 0x81, 8, 0x02, 0x82, ep_size), - }; - impl_->config_desc.assign(cfg_desc, cfg_desc + sizeof(cfg_desc)); - - // Install the TinyUSB driver with our descriptors. - tinyusb_config_t tusb_cfg = TINYUSB_DEFAULT_CONFIG(); - tusb_cfg.descriptor.device = &impl_->device_desc; - tusb_cfg.descriptor.string = impl_->strings.data(); - tusb_cfg.descriptor.string_count = static_cast(impl_->strings.size()); - tusb_cfg.descriptor.full_speed_config = impl_->config_desc.data(); -#if (TUD_OPT_HIGH_SPEED) - tusb_cfg.descriptor.high_speed_config = impl_->config_desc.data(); -#endif - - esp_err_t err = tinyusb_driver_install(&tusb_cfg); - if (err != ESP_OK) { - logger_.error("tinyusb_driver_install failed: {}", esp_err_to_name(err)); - ec = std::make_error_code(std::errc::io_error); - return false; - } - - // Register this instance before initializing CDC so the RX callback can find us. - s_instances[kCdcPort] = this; - - tinyusb_config_cdcacm_t acm_cfg = {}; - acm_cfg.cdc_port = kCdcPort; - acm_cfg.callback_rx = &rx_trampoline; - acm_cfg.callback_rx_wanted_char = nullptr; - acm_cfg.callback_line_state_changed = nullptr; - acm_cfg.callback_line_coding_changed = nullptr; - - err = tinyusb_cdcacm_init(&acm_cfg); - if (err != ESP_OK) { - logger_.error("tinyusb_cdcacm_init failed: {}", esp_err_to_name(err)); - s_instances[kCdcPort] = nullptr; - tinyusb_driver_uninstall(); - ec = std::make_error_code(std::errc::io_error); - return false; - } - - initialized_ = true; - logger_.info("Initialized native USB CDC (VID=0x{:04x} PID=0x{:04x})", config_.vid, config_.pid); - return true; -} +bool UsbCdc::initialize(std::error_code &ec) { return device_->initialize(ec); } bool UsbCdc::write(std::span data, std::error_code &ec) { - ec.clear(); - if (!initialized_) { - ec = std::make_error_code(std::errc::not_connected); - return false; - } - size_t offset = 0; - while (offset < data.size()) { - size_t queued = - tinyusb_cdcacm_write_queue(kCdcPort, data.data() + offset, data.size() - offset); - if (queued == 0) { - // The TX buffer is full; flush what we have and retry once. - tinyusb_cdcacm_write_flush(kCdcPort, 0); - queued = tinyusb_cdcacm_write_queue(kCdcPort, data.data() + offset, data.size() - offset); - if (queued == 0) { - logger_.warn_rate_limited("CDC TX buffer full, dropping {} bytes", data.size() - offset); - ec = std::make_error_code(std::errc::no_buffer_space); - break; - } - } - offset += queued; - // Non-blocking flush (timeout 0) - safe to call from within callbacks. - tinyusb_cdcacm_write_flush(kCdcPort, 0); - } - return offset == data.size(); + return device_->write_cdc(data, ec); } -bool UsbCdc::write(std::span data) { - std::error_code ec; - return write(data, ec); -} +bool UsbCdc::write(std::span data) { return device_->write_cdc(data); } void UsbCdc::set_receive_callback(const receive_callback_fn &cb) { - std::scoped_lock lk(cb_mutex_); - on_receive_ = cb; + device_->set_cdc_receive_callback(cb); } -bool UsbCdc::is_initialized() const { return initialized_; } +bool UsbCdc::is_initialized() const { return device_->is_initialized(); } -bool UsbCdc::is_connected() const { - if (!initialized_) - return false; - return tud_cdc_n_connected(kCdcPort); -} +bool UsbCdc::is_connected() const { return device_->is_cdc_connected(); } } // namespace espp diff --git a/components/usb_device/src/usb_device.cpp b/components/usb_device/src/usb_device.cpp new file mode 100644 index 0000000000..cd7bdfa617 --- /dev/null +++ b/components/usb_device/src/usb_device.cpp @@ -0,0 +1,697 @@ +#include "usb_device.hpp" + +#include +#include + +#include "tinyusb.h" +#include "tinyusb_cdc_acm.h" +#include "tinyusb_default_config.h" +#include "tusb.h" + +namespace { + +// Only a single USB device exists on the chip; the BOS descriptor and the vendor +// RX / control-request callbacks are global (no user pointer), so we route them +// through a file-scope pointer to the active instance. +espp::UsbDevice *s_device = nullptr; + +// The CDC port this component uses. A single dedicated CDC-ACM interface. +constexpr tinyusb_cdcacm_itf_t kCdcPort = TINYUSB_CDC_ACM_0; + +// ESP32-S3 / -S2 USB-OTG (DWC2, full-speed) endpoint budget: besides the control +// endpoint EP0, there are ~5 usable data IN endpoints and ~5 usable data OUT +// endpoints. See the README endpoint-budget table for which class combinations +// fit. +constexpr uint8_t kMaxInEndpoints = 5; +constexpr uint8_t kMaxOutEndpoints = 5; + +// The MS OS 2.0 descriptor set length used below (fixed by the registry-property +// payload; identical to TinyUSB's webusb_serial example). +constexpr uint16_t kMsOs20DescLen = 0xB2; + +} // namespace + +namespace espp { + +// Storage for the descriptors that TinyUSB references by pointer for the lifetime +// of the driver. These must outlive tinyusb_driver_install(). +struct UsbDevice::Impl { + tusb_desc_device_t device_desc{}; + std::vector config_desc; + std::vector bos_desc; // BOS (WebUSB + MS OS 2.0), empty if unused + std::vector ms_os_20_desc; // MS OS 2.0 descriptor set, empty if unused + std::vector webusb_url_desc; // WebUSB URL descriptor, empty if unused + + // Owning strings + the pointer table TinyUSB reads (index 0 is the LANGID). + std::array langid{{0x09, 0x04}}; + std::vector owned_strings; + std::vector strings; + + // Allocated interface / endpoint identifiers, filled in during initialize(). + uint8_t vendor_itf{0xFF}; +}; + +UsbDevice *UsbDevice::instance() { return s_device; } + +UsbDevice::UsbDevice(const Config &config) + : BaseComponent("UsbDevice", config.log_level) + , impl_(std::make_unique()) + , config_(config) + , on_cdc_receive_(config.cdc ? config.cdc->on_receive : nullptr) + , on_vendor_receive_(config.vendor ? config.vendor->on_receive : nullptr) {} + +UsbDevice::~UsbDevice() { + if (initialized_) { + if (config_.cdc) + tinyusb_cdcacm_deinit(kCdcPort); + tinyusb_driver_uninstall(); + if (s_device == this) + s_device = nullptr; + initialized_ = false; + } +} + +// --------------------------------------------------------------------------- +// TinyUSB C callbacks (global; routed to the active instance). +// --------------------------------------------------------------------------- + +// CDC RX trampoline registered with esp_tinyusb; runs in the TinyUSB task. +static void cdc_rx_trampoline(int itf, cdcacm_event_t *event) { + (void)event; + if (itf != (int)kCdcPort) + return; + if (s_device) + s_device->handle_cdc_rx(); +} + +extern "C" { + +// BOS descriptor (weak in TinyUSB core). Returns our WebUSB/MS-OS BOS when the +// vendor+WebUSB function is enabled, otherwise NULL (no BOS). +uint8_t const *tud_descriptor_bos_cb(void) { + return s_device ? s_device->bos_descriptor() : nullptr; +} + +#if (CFG_TUD_VENDOR > 0) + +// Vendor RX callback: drain the FIFO and dispatch to the user callback. +#if CFG_TUD_API_V0_19_COMPAT +void tud_vendor_rx_cb(uint8_t itf, uint8_t const *buffer, uint16_t bufsize) { +#else +void tud_vendor_rx_cb(uint8_t itf, uint8_t const *buffer, uint32_t bufsize) { +#endif + (void)itf; + (void)buffer; + (void)bufsize; + if (s_device) + s_device->handle_vendor_rx(); +} + +// Vendor control-transfer callback: answer the WebUSB URL and MS OS 2.0 +// descriptor requests, and the WebUSB "connect" class request (0x22). +bool tud_vendor_control_xfer_cb(uint8_t rhport, uint8_t stage, + tusb_control_request_t const *request) { + if (stage != CONTROL_STAGE_SETUP) + return true; // nothing to do on DATA / ACK stages + if (!s_device || !s_device->vendor_config().has_value()) + return false; + const auto &vendor = *s_device->vendor_config(); + + switch (request->bmRequestType_bit.type) { + case TUSB_REQ_TYPE_VENDOR: + if (request->bRequest == vendor.webusb_vendor_code) { + // Return the WebUSB landing-page URL descriptor. + uint8_t len = 0; + const uint8_t *url = s_device->webusb_url_descriptor(len); + if (!url) + return false; + return tud_control_xfer(rhport, request, (void *)(uintptr_t)url, len); + } + if (request->bRequest == vendor.ms_os_vendor_code && request->wIndex == 7) { + // Return the MS OS 2.0 descriptor set. + uint16_t total_len = 0; + const uint8_t *ms = s_device->ms_os_20_descriptor(total_len); + if (!ms) + return false; + return tud_control_xfer(rhport, request, (void *)(uintptr_t)ms, total_len); + } + return false; + + case TUSB_REQ_TYPE_CLASS: + if (request->bRequest == 0x22) { + // WebUSB simulates CDC SET_CONTROL_LINE_STATE (0x22) to signal connect. + if (request->wValue == 0) + tud_vendor_write_clear(); + return tud_control_status(rhport, request); + } + return false; + + default: + return false; + } +} + +#endif // CFG_TUD_VENDOR > 0 + +} // extern "C" + +// --------------------------------------------------------------------------- +// Descriptor accessors used by the global callbacks. +// --------------------------------------------------------------------------- + +const uint8_t *UsbDevice::bos_descriptor() const { + return impl_->bos_desc.empty() ? nullptr : impl_->bos_desc.data(); +} + +const uint8_t *UsbDevice::ms_os_20_descriptor(uint16_t &total_len) const { + if (impl_->ms_os_20_desc.empty()) + return nullptr; + total_len = static_cast(impl_->ms_os_20_desc.size()); + return impl_->ms_os_20_desc.data(); +} + +const uint8_t *UsbDevice::webusb_url_descriptor(uint8_t &length) const { + if (impl_->webusb_url_desc.empty()) + return nullptr; + length = static_cast(impl_->webusb_url_desc.size()); + return impl_->webusb_url_desc.data(); +} + +// --------------------------------------------------------------------------- +// RX handling. +// --------------------------------------------------------------------------- + +void UsbDevice::handle_cdc_rx() { + receive_callback_fn cb; + { + std::scoped_lock lk(cb_mutex_); + cb = on_cdc_receive_; + } + if (!cb || !config_.cdc) + return; + std::vector buf(config_.cdc->rx_chunk_size); + size_t rx_size = 0; + do { + rx_size = 0; + esp_err_t err = tinyusb_cdcacm_read(kCdcPort, buf.data(), buf.size(), &rx_size); + if (err != ESP_OK) { + logger_.error("CDC read error: {}", esp_err_to_name(err)); + break; + } + if (rx_size > 0) + cb(std::span(buf.data(), rx_size)); + } while (rx_size == buf.size()); +} + +void UsbDevice::handle_vendor_rx() { +#if (CFG_TUD_VENDOR > 0) + receive_callback_fn cb; + { + std::scoped_lock lk(cb_mutex_); + cb = on_vendor_receive_; + } + if (!cb || !config_.vendor) + return; + std::vector buf(config_.vendor->rx_chunk_size); + while (tud_vendor_available()) { + uint32_t count = tud_vendor_read(buf.data(), buf.size()); + if (count == 0) + break; + cb(std::span(buf.data(), count)); + } +#endif +} + +// --------------------------------------------------------------------------- +// Initialization: build descriptors from the selected functions. +// --------------------------------------------------------------------------- + +bool UsbDevice::initialize(std::error_code &ec) { + ec.clear(); + if (initialized_) { + logger_.warn("Already initialized"); + return true; + } + if (s_device != nullptr) { + logger_.error("Another UsbDevice/UsbCdc instance is already active"); + ec = std::make_error_code(std::errc::device_or_resource_busy); + return false; + } + if (!config_.cdc && !config_.vendor) { + logger_.error("No USB function enabled (enable cdc and/or vendor)"); + ec = std::make_error_code(std::errc::invalid_argument); + return false; + } + if (config_.hid || config_.msc) { + // Reserved extension points; not implemented yet (see README endpoint table). + logger_.error("HID/MSC functions are not implemented yet"); + ec = std::make_error_code(std::errc::function_not_supported); + return false; + } + if (config_.vendor) { +#if (CFG_TUD_VENDOR == 0) + logger_.error("Vendor function requested but CFG_TUD_VENDOR==0. Set " + "CONFIG_TINYUSB_VENDOR_COUNT>0 in sdkconfig."); + ec = std::make_error_code(std::errc::function_not_supported); + return false; +#endif + } + + // --- Sequentially allocate interface numbers, endpoint addresses, strings --- + uint8_t next_itf = 0; + uint8_t next_ep = 1; // endpoint number (1..); IN uses 0x80|n, OUT uses n + uint8_t in_used = 0, out_used = 0; + const int ep_size = (TUD_OPT_HIGH_SPEED ? 512 : 64); + + // String table: 0=LANGID, 1=manufacturer, 2=product, 3=serial, then per-itf. + impl_->owned_strings = {config_.manufacturer, config_.product, config_.serial_number}; + uint8_t next_str = 4; + + uint8_t cdc_itf = 0, cdc_str = 0, cdc_notif = 0, cdc_out = 0, cdc_in = 0; + if (config_.cdc) { + cdc_itf = next_itf; + next_itf = static_cast(next_itf + 2); // comm + data interfaces + cdc_str = next_str++; + impl_->owned_strings.push_back(config_.cdc->interface_name); + cdc_notif = static_cast(0x80 | next_ep++); // interrupt IN (notification) + in_used++; + const uint8_t data_ep = next_ep++; + cdc_out = data_ep; // bulk OUT + cdc_in = static_cast(0x80 | data_ep); // bulk IN + in_used++; + out_used++; + } + + uint8_t vendor_itf = 0, vendor_str = 0, vendor_out = 0, vendor_in = 0; + if (config_.vendor) { + vendor_itf = next_itf++; + vendor_str = next_str++; + impl_->owned_strings.push_back(config_.vendor->interface_name); + const uint8_t v_ep = next_ep++; + vendor_out = v_ep; // bulk OUT + vendor_in = static_cast(0x80 | v_ep); // bulk IN + in_used++; + out_used++; + impl_->vendor_itf = vendor_itf; + } + + // --- Endpoint budget check --- + if (in_used > kMaxInEndpoints || out_used > kMaxOutEndpoints) { + logger_.error("Endpoint budget exceeded: IN={} (max {}), OUT={} (max {})", in_used, + kMaxInEndpoints, out_used, kMaxOutEndpoints); + ec = std::make_error_code(std::errc::value_too_large); + return false; + } + + // --- Build the string pointer table TinyUSB reads --- + impl_->strings.clear(); + impl_->strings.push_back(reinterpret_cast(impl_->langid.data())); + for (const auto &s : impl_->owned_strings) + impl_->strings.push_back(s.c_str()); + + // --- Device descriptor --- + const bool webusb = config_.vendor && config_.vendor->webusb; + impl_->device_desc = tusb_desc_device_t{}; + impl_->device_desc.bLength = sizeof(tusb_desc_device_t); + impl_->device_desc.bDescriptorType = TUSB_DESC_DEVICE; + // BOS/WebUSB requires bcdUSB >= 2.1. + impl_->device_desc.bcdUSB = webusb ? 0x0210 : 0x0200; + impl_->device_desc.bDeviceClass = TUSB_CLASS_MISC; + impl_->device_desc.bDeviceSubClass = MISC_SUBCLASS_COMMON; + impl_->device_desc.bDeviceProtocol = MISC_PROTOCOL_IAD; + impl_->device_desc.bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE; + impl_->device_desc.idVendor = config_.vid; + impl_->device_desc.idProduct = config_.pid; + impl_->device_desc.bcdDevice = 0x0100; + impl_->device_desc.iManufacturer = 0x01; + impl_->device_desc.iProduct = 0x02; + impl_->device_desc.iSerialNumber = 0x03; + impl_->device_desc.bNumConfigurations = 0x01; + + // --- Configuration descriptor --- + uint8_t itf_count = 0; + uint16_t total_len = TUD_CONFIG_DESC_LEN; + if (config_.cdc) { + itf_count = static_cast(itf_count + 2); + total_len = static_cast(total_len + TUD_CDC_DESC_LEN); + } + if (config_.vendor) { + itf_count = static_cast(itf_count + 1); + total_len = static_cast(total_len + TUD_VENDOR_DESC_LEN); + } + + impl_->config_desc.clear(); + auto append = [&](const uint8_t *p, size_t n) { + impl_->config_desc.insert(impl_->config_desc.end(), p, p + n); + }; + { + const uint8_t hdr[] = { + // config number, interface count, string index, total length, attribute, power (mA) + TUD_CONFIG_DESCRIPTOR(1, itf_count, 0, total_len, TUSB_DESC_CONFIG_ATT_REMOTE_WAKEUP, 100), + }; + append(hdr, sizeof(hdr)); + } + if (config_.cdc) { + const uint8_t d[] = { + TUD_CDC_DESCRIPTOR(cdc_itf, cdc_str, cdc_notif, 8, cdc_out, cdc_in, ep_size), + }; + append(d, sizeof(d)); + } + if (config_.vendor) { + const uint8_t d[] = { + TUD_VENDOR_DESCRIPTOR(vendor_itf, vendor_str, vendor_out, vendor_in, ep_size), + }; + append(d, sizeof(d)); + } + + // --- WebUSB / MS OS 2.0 descriptors (only when the vendor+WebUSB is enabled) --- + if (webusb) { + const auto &v = *config_.vendor; + + // WebUSB URL descriptor: bLength, bDescriptorType(3), bScheme, url... + impl_->webusb_url_desc.clear(); + impl_->webusb_url_desc.push_back(static_cast(3 + v.landing_page_url.size())); + impl_->webusb_url_desc.push_back(3); // WEBUSB URL descriptor type + impl_->webusb_url_desc.push_back(v.url_scheme); + impl_->webusb_url_desc.insert(impl_->webusb_url_desc.end(), v.landing_page_url.begin(), + v.landing_page_url.end()); + + // MS OS 2.0 descriptor set (identical layout to TinyUSB's webusb example, with + // the function-subset "first interface" byte set to our vendor interface). + const uint8_t ms_os_20[] = { + // Set header: length, type, windows version, total length + U16_TO_U8S_LE(0x000A), + U16_TO_U8S_LE(MS_OS_20_SET_HEADER_DESCRIPTOR), + U32_TO_U8S_LE(0x06030000), + U16_TO_U8S_LE(kMsOs20DescLen), + // Configuration subset header: length, type, config index, reserved, total length + U16_TO_U8S_LE(0x0008), + U16_TO_U8S_LE(MS_OS_20_SUBSET_HEADER_CONFIGURATION), + 0, + 0, + U16_TO_U8S_LE(kMsOs20DescLen - 0x0A), + // Function subset header: length, type, first interface, reserved, subset length + U16_TO_U8S_LE(0x0008), + U16_TO_U8S_LE(MS_OS_20_SUBSET_HEADER_FUNCTION), + vendor_itf, + 0, + U16_TO_U8S_LE(kMsOs20DescLen - 0x0A - 0x08), + // MS OS 2.0 compatible ID: length, type, compatible ID, sub compatible ID + U16_TO_U8S_LE(0x0014), + U16_TO_U8S_LE(MS_OS_20_FEATURE_COMPATBLE_ID), + 'W', + 'I', + 'N', + 'U', + 'S', + 'B', + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + // MS OS 2.0 registry property: length, type + U16_TO_U8S_LE(kMsOs20DescLen - 0x0A - 0x08 - 0x08 - 0x14), + U16_TO_U8S_LE(MS_OS_20_FEATURE_REG_PROPERTY), + U16_TO_U8S_LE(0x0007), + U16_TO_U8S_LE(0x002A), + 'D', + 0x00, + 'e', + 0x00, + 'v', + 0x00, + 'i', + 0x00, + 'c', + 0x00, + 'e', + 0x00, + 'I', + 0x00, + 'n', + 0x00, + 't', + 0x00, + 'e', + 0x00, + 'r', + 0x00, + 'f', + 0x00, + 'a', + 0x00, + 'c', + 0x00, + 'e', + 0x00, + 'G', + 0x00, + 'U', + 0x00, + 'I', + 0x00, + 'D', + 0x00, + 's', + 0x00, + 0x00, + 0x00, + U16_TO_U8S_LE(0x0050), + // bPropertyData: "{975F44D9-0D08-43FD-8B3E-127CA8AFFF9D}" + '{', + 0x00, + '9', + 0x00, + '7', + 0x00, + '5', + 0x00, + 'F', + 0x00, + '4', + 0x00, + '4', + 0x00, + 'D', + 0x00, + '9', + 0x00, + '-', + 0x00, + '0', + 0x00, + 'D', + 0x00, + '0', + 0x00, + '8', + 0x00, + '-', + 0x00, + '4', + 0x00, + '3', + 0x00, + 'F', + 0x00, + 'D', + 0x00, + '-', + 0x00, + '8', + 0x00, + 'B', + 0x00, + '3', + 0x00, + 'E', + 0x00, + '-', + 0x00, + '1', + 0x00, + '2', + 0x00, + '7', + 0x00, + 'C', + 0x00, + 'A', + 0x00, + '8', + 0x00, + 'A', + 0x00, + 'F', + 0x00, + 'F', + 0x00, + 'F', + 0x00, + '9', + 0x00, + 'D', + 0x00, + '}', + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + }; + static_assert(sizeof(ms_os_20) == kMsOs20DescLen, "MS OS 2.0 descriptor size mismatch"); + impl_->ms_os_20_desc.assign(ms_os_20, ms_os_20 + sizeof(ms_os_20)); + + // BOS descriptor: WebUSB + MS OS 2.0 platform capabilities. + const uint16_t bos_total = + TUD_BOS_DESC_LEN + TUD_BOS_WEBUSB_DESC_LEN + TUD_BOS_MICROSOFT_OS_DESC_LEN; + const uint8_t bos[] = { + TUD_BOS_DESCRIPTOR(bos_total, 2), + TUD_BOS_WEBUSB_DESCRIPTOR(v.webusb_vendor_code, 1), + TUD_BOS_MS_OS_20_DESCRIPTOR(kMsOs20DescLen, v.ms_os_vendor_code), + }; + impl_->bos_desc.assign(bos, bos + sizeof(bos)); + } + + // --- Install the TinyUSB driver with our descriptors --- + tinyusb_config_t tusb_cfg = TINYUSB_DEFAULT_CONFIG(); + tusb_cfg.descriptor.device = &impl_->device_desc; + tusb_cfg.descriptor.string = impl_->strings.data(); + tusb_cfg.descriptor.string_count = static_cast(impl_->strings.size()); + tusb_cfg.descriptor.full_speed_config = impl_->config_desc.data(); +#if (TUD_OPT_HIGH_SPEED) + tusb_cfg.descriptor.high_speed_config = impl_->config_desc.data(); +#endif + + // Register before installing so the BOS / vendor callbacks can find us. + s_device = this; + + esp_err_t err = tinyusb_driver_install(&tusb_cfg); + if (err != ESP_OK) { + logger_.error("tinyusb_driver_install failed: {}", esp_err_to_name(err)); + s_device = nullptr; + ec = std::make_error_code(std::errc::io_error); + return false; + } + + // --- Initialize the CDC-ACM function (vendor needs no explicit init) --- + if (config_.cdc) { + tinyusb_config_cdcacm_t acm_cfg = {}; + acm_cfg.cdc_port = kCdcPort; + acm_cfg.callback_rx = &cdc_rx_trampoline; + acm_cfg.callback_rx_wanted_char = nullptr; + acm_cfg.callback_line_state_changed = nullptr; + acm_cfg.callback_line_coding_changed = nullptr; + err = tinyusb_cdcacm_init(&acm_cfg); + if (err != ESP_OK) { + logger_.error("tinyusb_cdcacm_init failed: {}", esp_err_to_name(err)); + s_device = nullptr; + tinyusb_driver_uninstall(); + ec = std::make_error_code(std::errc::io_error); + return false; + } + } + + initialized_ = true; + logger_.info("Initialized native USB device (VID=0x{:04x} PID=0x{:04x}) cdc={} vendor={}{}", + config_.vid, config_.pid, config_.cdc.has_value(), config_.vendor.has_value(), + webusb ? " webusb" : ""); + return true; +} + +// --------------------------------------------------------------------------- +// Write paths. +// --------------------------------------------------------------------------- + +bool UsbDevice::write_cdc(std::span data, std::error_code &ec) { + ec.clear(); + if (!initialized_ || !config_.cdc) { + ec = std::make_error_code(std::errc::not_connected); + return false; + } + size_t offset = 0; + while (offset < data.size()) { + size_t queued = + tinyusb_cdcacm_write_queue(kCdcPort, data.data() + offset, data.size() - offset); + if (queued == 0) { + tinyusb_cdcacm_write_flush(kCdcPort, 0); + queued = tinyusb_cdcacm_write_queue(kCdcPort, data.data() + offset, data.size() - offset); + if (queued == 0) { + logger_.warn_rate_limited("CDC TX buffer full, dropping {} bytes", data.size() - offset); + ec = std::make_error_code(std::errc::no_buffer_space); + break; + } + } + offset += queued; + tinyusb_cdcacm_write_flush(kCdcPort, 0); + } + return offset == data.size(); +} + +bool UsbDevice::write_cdc(std::span data) { + std::error_code ec; + return write_cdc(data, ec); +} + +bool UsbDevice::write_vendor(std::span data, std::error_code &ec) { + ec.clear(); +#if (CFG_TUD_VENDOR > 0) + if (!initialized_ || !config_.vendor) { + ec = std::make_error_code(std::errc::not_connected); + return false; + } + size_t offset = 0; + while (offset < data.size()) { + uint32_t queued = tud_vendor_write(data.data() + offset, data.size() - offset); + tud_vendor_write_flush(); + if (queued == 0) { + logger_.warn_rate_limited("Vendor TX buffer full, dropping {} bytes", data.size() - offset); + ec = std::make_error_code(std::errc::no_buffer_space); + break; + } + offset += queued; + } + return offset == data.size(); +#else + (void)data; + ec = std::make_error_code(std::errc::function_not_supported); + return false; +#endif +} + +bool UsbDevice::write_vendor(std::span data) { + std::error_code ec; + return write_vendor(data, ec); +} + +void UsbDevice::set_cdc_receive_callback(const receive_callback_fn &cb) { + std::scoped_lock lk(cb_mutex_); + on_cdc_receive_ = cb; +} + +void UsbDevice::set_vendor_receive_callback(const receive_callback_fn &cb) { + std::scoped_lock lk(cb_mutex_); + on_vendor_receive_ = cb; +} + +bool UsbDevice::is_initialized() const { return initialized_; } + +bool UsbDevice::is_cdc_connected() const { + if (!initialized_ || !config_.cdc) + return false; + return tud_cdc_n_connected(kCdcPort); +} + +bool UsbDevice::is_vendor_connected() const { + if (!initialized_ || !config_.vendor) + return false; + return tud_mounted(); +} + +} // namespace espp diff --git a/doc/Doxyfile b/doc/Doxyfile index 5f76da941d..24913672b2 100755 --- a/doc/Doxyfile +++ b/doc/Doxyfile @@ -360,6 +360,7 @@ INPUT = \ $(PROJECT_PATH)/components/meshtastic/include/meshtastic_types.hpp \ $(PROJECT_PATH)/components/mt6701/include/mt6701.hpp \ $(PROJECT_PATH)/components/odrive_ascii/include/odrive_ascii.hpp \ + $(PROJECT_PATH)/components/usb_device/include/usb_device.hpp \ $(PROJECT_PATH)/components/usb_device/include/usb_cdc.hpp \ $(PROJECT_PATH)/components/pca9535/include/pca9535.hpp \ $(PROJECT_PATH)/components/pcf85063/include/pcf85063.hpp \ diff --git a/doc/en/buses/usb_cdc.rst b/doc/en/buses/usb_cdc.rst index f0b6d30f7b..ea0047efc5 100644 --- a/doc/en/buses/usb_cdc.rst +++ b/doc/en/buses/usb_cdc.rst @@ -1,54 +1,169 @@ -USB CDC Transport Component -=========================== +USB Device Component +==================== Overview -------- -``espp::UsbCdc`` is a thin, idiomatic wrapper around ESP-IDF's ``esp_tinyusb`` -managed component. It presents a single dedicated **native USB CDC-ACM** (virtual -serial port) interface on the ESP32-S3 / -S2 / -P4 USB-OTG peripheral, with a +``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 (``bInterfaceClass`` 0xFF, 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). + +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 **HID** and **MSC** functions 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. This also lays the groundwork for adding a WebUSB *vendor* -interface later. +the logging console. + +``espp::UsbCdc`` is retained as a thin **CDC-only preset** over +``espp::UsbDevice`` for back-compatibility. Features -------- -- Native USB CDC-ACM interface over USB-OTG, separate from the log console +- Composable: enable a CDC function and/or a vendor/WebUSB function (composite) +- Vendor-specific interface (class 0xFF) with a bulk IN + bulk OUT raw byte stream +- 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 -- Byte-stream transport: receive callback for RX, ``write()`` for TX - No exceptions; ``initialize()`` reports failures via ``std::error_code`` -- Safely marshals the TinyUSB RX callback (TinyUSB task context) into the user callback +- Safely 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: + .. code-block:: cpp - espp::UsbCdc::Config cfg; + espp::UsbDevice::Config cfg; cfg.vid = 0x1209; // pid.codes VID (ODrive uses this) cfg.pid = 0x0d32; // ODrive-like PID - cfg.manufacturer = "espp"; - cfg.product = "espp USB CDC"; - cfg.on_receive = [](std::span data) { /* handle rx */ }; + espp::UsbDevice::CdcFunction cdc; + cdc.on_receive = [&](std::span 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 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): + +.. code-block:: cpp + + espp::UsbCdc::Config cfg; + cfg.vid = 0x1209; + cfg.pid = 0x0d32; + cfg.on_receive = [](std::span data) { /* handle rx */ }; espp::UsbCdc usb(cfg); std::error_code ec; if (!usb.initialize(ec)) { /* handle ec */ } - uint8_t hello[] = {'h', 'i', '\n'}; - usb.write(hello); + +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. + +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: + +.. list-table:: + :header-rows: 1 + + * - 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 (future) + - 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 HID / MSC +------------------------ + +``espp::UsbDevice::Config`` reserves ``std::optional`` slots for ``HidFunction`` +and ``MscFunction`` as documented extension points. They are not implemented yet; +enabling one today makes ``initialize()`` fail with +``std::errc::function_not_supported``. When implemented they slot into the same +sequential interface / endpoint / string allocator: a HID function appends one +HID interface (report descriptor + report get/set callbacks) claiming an +interrupt-IN endpoint, and 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. -- Enable ``CONFIG_TINYUSB_CDC_ENABLED=y`` in your project. -- Only one ``espp::UsbCdc`` instance may drive a given CDC port. -- The receive callback runs in the TinyUSB device task; keep it short and non-blocking. +- Only one ``espp::UsbDevice`` / ``espp::UsbCdc`` instance 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). .. ------------------------------- Example ------------------------------------- @@ -61,4 +176,5 @@ Notes API Reference ------------- +.. include-build-file:: inc/usb_device.inc .. include-build-file:: inc/usb_cdc.inc From 247534c8f84c5e5deda622b67136dc469832dd33 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Mon, 17 Aug 2026 22:34:45 -0500 Subject: [PATCH 03/12] feat(usb_device): HID function + example-manifest cleanup + PR #720 review fixes Implement the HID function using TinyUSB's HID class driver: store the application-supplied report descriptor, provide the tud_hid_* weak-callback overrides, allocate the interrupt IN (+ optional OUT) endpoint and interface, append TUD_HID_DESCRIPTOR to the config descriptor, include HID in the endpoint-budget accounting, and add write_hid_report()/is_hid_ready(). All tud_hid_* usage is gated behind CFG_TUD_HID so HID-less builds still link. Exercise it in the example as a composite CDC + vendor + HID gamepad: build the report descriptor from espp::GamepadInputReport (hid-rp) and animate axes + buttons, sending input reports at ~10 Hz. Example-manifest cleanup: move esp_tinyusb into the usb_device component's idf_component.yml, delete the example main manifest, and resolve espp deps via EXTRA_COMPONENT_DIRS (no override_path). PR #720 review fixes: device descriptor uses MISC/IAD only when CDC is enabled (else 0x00/0x00/0x00); validate WebUSB URL length fits a uint8_t; clear s_device before driver teardown; allocation-free CDC/vendor RX via preallocated buffers; clarify landing_page_url / url_scheme=255 docs. Co-Authored-By: Claude Opus 4.8 --- components/usb_device/README.md | 53 +++-- components/usb_device/example/CMakeLists.txt | 3 +- .../usb_device/example/main/CMakeLists.txt | 2 +- .../usb_device/example/main/idf_component.yml | 25 --- .../example/main/usb_cdc_example.cpp | 80 +++++++- .../usb_device/example/sdkconfig.defaults | 5 + components/usb_device/include/usb_device.hpp | 76 +++++-- components/usb_device/src/usb_device.cpp | 191 ++++++++++++++++-- doc/en/buses/usb_cdc.rst | 57 ++++-- 9 files changed, 397 insertions(+), 95 deletions(-) delete mode 100644 components/usb_device/example/main/idf_component.yml diff --git a/components/usb_device/README.md b/components/usb_device/README.md index d3f7fc725a..691053d12a 100644 --- a/components/usb_device/README.md +++ b/components/usb_device/README.md @@ -14,6 +14,9 @@ Today it can enable, in any combination (subject to the endpoint budget): OUT) carrying a raw byte stream, optionally advertising **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 (e.g. a gamepad built with the espp + `hid-rp` component), with input reports sent via `write_hid_report()`. Interface numbers, endpoint addresses and string indices are allocated *sequentially* as functions are enabled, and the result is checked against the @@ -41,8 +44,11 @@ for back-compatibility. ## Features -- **Composable**: enable a CDC function and/or a vendor/WebUSB function (composite). +- **Composable**: enable a CDC function and/or a vendor/WebUSB function and/or a + HID function (composite). - **Vendor-specific interface** (class 0xFF): raw bulk IN + bulk OUT byte stream. +- **HID interface**: application-supplied report descriptor (built with `hid-rp` + in the example) on an interrupt IN endpoint; `write_hid_report()` sends reports. - **WebUSB**: BOS + WebUSB URL + MS OS 2.0 descriptors for driverless browser access, with a configurable landing-page URL. - **Sequential allocation** of interfaces / endpoints / strings with an @@ -90,8 +96,11 @@ Key methods: functions, check the endpoint budget, install the TinyUSB driver. - `bool write_cdc(...)` / `bool write_vendor(...)` — queue + non-blocking flush on the respective interface. +- `bool write_hid_report(uint8_t report_id, std::span report, ...)` — + send a HID input report on the HID interrupt IN endpoint. - `void set_cdc_receive_callback(...)` / `void set_vendor_receive_callback(...)`. -- `bool is_cdc_connected() const` / `bool is_vendor_connected() const`. +- `bool is_cdc_connected() const` / `bool is_vendor_connected() const` / + `bool is_hid_ready() const`. CDC-only preset (`espp::UsbCdc`, unchanged API): `initialize()`, `write()`, `set_receive_callback()`, `is_connected()`. @@ -115,6 +124,24 @@ weak-callback overrides (`tud_descriptor_bos_cb`, `tud_vendor_control_xfer_cb`, `tud_vendor_rx_cb`). If the vendor function is requested but `CFG_TUD_VENDOR == 0`, `initialize()` fails with `std::errc::function_not_supported`. +## 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, 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; +`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 / -S2 USB-OTG core is full-speed and, besides EP0, provides roughly @@ -125,7 +152,7 @@ consumes: |-------------------|---------------------------------------------|--------------------------------| | CDC-ACM | 2 (1 interrupt-IN notif + 1 bulk-IN) | 1 (bulk-OUT) | | Vendor / WebUSB | 1 (bulk-IN) | 1 (bulk-OUT) | -| HID (future) | 1 (interrupt-IN) | 0 or 1 (optional interrupt-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 @@ -134,15 +161,17 @@ CDC+Vendor+MSC. Enabling CDC+Vendor+HID+MSC reaches 5 IN endpoints — at the ha limit, not recommended. `initialize()` returns `std::errc::value_too_large` if the IN or OUT budget is exceeded. -## Extending with HID / MSC - -`espp::UsbDevice::Config` reserves `std::optional` slots for `HidFunction` and -`MscFunction` as documented extension points. They are not implemented yet; -enabling one today makes `initialize()` fail with -`std::errc::function_not_supported`. When implemented they slot into the same -sequential allocator: HID appends one interface (report descriptor + report -get/set callbacks) claiming an interrupt-IN endpoint; MSC appends one interface -(SCSI + storage read/write/capacity callbacks) claiming a bulk IN + bulk OUT. +## Extending with MSC + +The **HID** function is implemented (see "Enabling the HID class" above). +`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 allocator: MSC appends one interface (SCSI + storage +read/write/capacity callbacks) claiming a bulk IN + bulk OUT endpoint, exactly +as HID appends one interface claiming an interrupt-IN endpoint (plus an optional +interrupt-OUT). ## Example diff --git a/components/usb_device/example/CMakeLists.txt b/components/usb_device/example/CMakeLists.txt index 66c258c558..1e3bc046c8 100644 --- a/components/usb_device/example/CMakeLists.txt +++ b/components/usb_device/example/CMakeLists.txt @@ -15,6 +15,7 @@ include($ENV{IDF_PATH}/tools/cmake/project.cmake) set(EXTRA_COMPONENT_DIRS "../../../components/base_component" "../../../components/format" + "../../../components/hid-rp" "../../../components/logger" "../../../components/odrive_ascii" "../../../components/usb_device" @@ -22,7 +23,7 @@ set(EXTRA_COMPONENT_DIRS set( COMPONENTS - "main esptool_py base_component format logger odrive_ascii usb_device esp_tinyusb" + "main esptool_py base_component format hid-rp logger odrive_ascii usb_device esp_tinyusb" CACHE STRING "List of components to include" ) diff --git a/components/usb_device/example/main/CMakeLists.txt b/components/usb_device/example/main/CMakeLists.txt index 4200fac6aa..7ab15b919a 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 esp_tinyusb + REQUIRES usb_device odrive_ascii hid-rp esp_tinyusb ) diff --git a/components/usb_device/example/main/idf_component.yml b/components/usb_device/example/main/idf_component.yml deleted file mode 100644 index aa94168041..0000000000 --- a/components/usb_device/example/main/idf_component.yml +++ /dev/null @@ -1,25 +0,0 @@ -## IDF Component Manager Manifest File - example project overrides. -## -## The component manager is ENABLED for this example so it can fetch the managed -## `espressif/esp_tinyusb` component from the ESP registry. The espp components -## are resolved to their in-repo copies via override_path so the version solver -## does not try to fetch them from the registry. -dependencies: - idf: - version: '>=5.0' - espressif/esp_tinyusb: '>=1.4' - espp/base_component: - version: '*' - override_path: '../../../base_component' - espp/logger: - version: '*' - override_path: '../../../logger' - espp/format: - version: '*' - override_path: '../../../format' - espp/odrive_ascii: - version: '*' - override_path: '../../../odrive_ascii' - espp/usb_device: - version: '*' - override_path: '../..' diff --git a/components/usb_device/example/main/usb_cdc_example.cpp b/components/usb_device/example/main/usb_cdc_example.cpp index 4279eba403..910c7bb79a 100644 --- a/components/usb_device/example/main/usb_cdc_example.cpp +++ b/components/usb_device/example/main/usb_cdc_example.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include #include @@ -12,6 +13,10 @@ #include "odrive_ascii.hpp" #include "usb_device.hpp" +// hid-rp: build the HID gamepad report descriptor + serialize input reports. +#include "hid-rp-gamepad.hpp" +#include "hid-rp.hpp" + using namespace std::chrono_literals; extern "C" void app_main(void) { @@ -19,8 +24,9 @@ extern "C" void app_main(void) { // The log console stays on the built-in USB-Serial-JTAG / UART (configured via // sdkconfig). The native USB device created below is a *separate* USB - // peripheral that exposes two interfaces (CDC serial + vendor/WebUSB), both - // dedicated to the ODrive ASCII protocol. + // peripheral that exposes a composite CDC serial + vendor/WebUSB + HID gamepad + // device. CDC and vendor both feed the ODrive ASCII protocol; the HID function + // presents an animated gamepad. Logger logger({.tag = "UsbDeviceExample", .level = Logger::Verbosity::INFO}); //! [usb_cdc_example] @@ -97,6 +103,31 @@ extern "C" void app_main(void) { 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; + Gamepad gamepad; + gamepad.reset(); + + std::vector hid_report_descriptor; + { + using namespace hid::page; + using namespace hid::rdf; + auto raw_descriptor = descriptor(usage_page(), 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); // Wire: CDC RX -> proto.process_bytes -> CDC write. @@ -118,20 +149,51 @@ extern "C" void app_main(void) { logger.error("Failed to initialize USB device: {}", ec.message()); return; } - logger.info("Native USB device ready (CDC serial + vendor/WebUSB)."); + logger.info("Native USB device ready (CDC serial + vendor/WebUSB + HID gamepad)."); logger.info("Serial: connect to the ODrive-like port and send commands, e.g."); logger.info(" 'r axis0.encoder.pos_estimate' or 'p 0 1.0 0.5 0.1'"); logger.info("WebUSB: open the browser console and connect to the vendor interface."); + logger.info("HID: the host sees a live gamepad with animated sticks + toggling buttons."); //! [usb_cdc_example] - // Nothing else to do on the main task; the transport runs off the TinyUSB - // task and its RX callbacks. + // The CDC / vendor transports run off the TinyUSB task and their RX callbacks. + // Here on the main task we animate the HID gamepad and push an input report a + // few times a second so the host sees a live device. + float phase = 0.0f; + size_t tick = 0; while (true) { - std::this_thread::sleep_for(1s); - if (usb.is_cdc_connected() || usb.is_vendor_connected()) { - logger.debug_rate_limited("USB host connected; pos={} vel={}", state.position, - state.velocity); + // Animate the two joysticks (values in [-1, 1]) and toggle a couple of + // buttons at different rates. + const float lx = std::sin(phase); + const float ly = std::cos(phase); + const float rx = std::cos(phase); + const float ry = std::sin(phase); + const bool button_a = ((tick / 5) % 2) == 0; // toggles ~1 Hz at 10 Hz loop + const bool button_b = ((tick / 10) % 2) == 0; // toggles ~0.5 Hz + + gamepad.reset(); + gamepad.set_left_joystick(lx, ly); + gamepad.set_right_joystick(rx, ry); + gamepad.set_button(1, button_a); + gamepad.set_button(2, button_b); + + if (usb.is_hid_ready()) { + auto report = gamepad.get_report(); + std::error_code hid_ec; + usb.write_hid_report(kHidReportId, report, hid_ec); + if (hid_ec) + logger.warn_rate_limited("HID report send failed: {}", hid_ec.message()); } + + // Log the values we send once per second (the loop runs at ~10 Hz). + if ((tick % 10) == 0) { + logger.info("HID gamepad: lx={:+.2f} ly={:+.2f} rx={:+.2f} ry={:+.2f} A={} B={} ready={}", lx, + ly, rx, ry, button_a, button_b, usb.is_hid_ready()); + } + + phase += 0.1f; + ++tick; + std::this_thread::sleep_for(100ms); } } diff --git a/components/usb_device/example/sdkconfig.defaults b/components/usb_device/example/sdkconfig.defaults index 0f8a186167..5512a8e8dd 100644 --- a/components/usb_device/example/sdkconfig.defaults +++ b/components/usb_device/example/sdkconfig.defaults @@ -13,3 +13,8 @@ CONFIG_TINYUSB_CDC_COUNT=1 # 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/include/usb_device.hpp b/components/usb_device/include/usb_device.hpp index 33192efc45..369a9af95c 100644 --- a/components/usb_device/include/usb_device.hpp +++ b/components/usb_device/include/usb_device.hpp @@ -23,14 +23,16 @@ namespace espp { * **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. 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). + * 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 leaves room for **HID** and **MSC** functions to be added later - * without changing the descriptor-building model (see `HidFunction` / - * `MscFunction` below and the endpoint-budget table in the README). + * 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 @@ -89,9 +91,13 @@ class UsbDevice : public BaseComponent { 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. */ /** - * @brief WebUSB landing-page URL, *without* a scheme (the scheme is encoded - * separately via `url_scheme`). Defaults to the espp docs-hosted - * ODrive WebUSB console. + * @brief 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 ODrive WebUSB console (scheme-less, https). + * @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. */ std::string landing_page_url{"esp-cpp.github.io/espp/apps/odrive_webusb_console.html"}; uint8_t url_scheme{1}; /**< 0 = http, 1 = https, 255 = URL includes its own scheme. */ @@ -101,18 +107,26 @@ class UsbDevice : public BaseComponent { }; /** - * @brief (Future) HID function extension point. Not implemented yet. + * @brief HID (Human Interface Device) function. * * A HID function consumes 1 interrupt IN endpoint (and optionally 1 interrupt - * OUT). When implemented it will carry a HID report descriptor plus report - * get/set callbacks. Enabling it today makes initialize() fail with - * `std::errc::function_not_supported` so the API slot is reserved without - * silently doing nothing. + * 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 + * `UsbDevice::write_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 HidFunction { - std::string interface_name{"espp HID"}; + std::string interface_name{"espp HID"}; /**< HID interface string descriptor. */ std::vector report_descriptor{}; /**< HID report descriptor bytes. */ bool has_out_endpoint{false}; /**< Whether to allocate an interrupt OUT endpoint. */ + uint8_t poll_interval_ms{10}; /**< Interrupt IN polling interval (bInterval), ms. */ }; /** @@ -139,7 +153,7 @@ class UsbDevice : public BaseComponent { std::optional cdc{}; /**< Enable a CDC-ACM function. */ std::optional vendor{}; /**< Enable a vendor-specific / WebUSB function. */ - std::optional hid{}; /**< (Future) enable a HID function. */ + std::optional hid{}; /**< Enable a HID function. */ std::optional msc{}; /**< (Future) enable an MSC function. */ espp::Logger::Verbosity log_level{espp::Logger::Verbosity::WARN}; /**< Logger verbosity. */ @@ -191,6 +205,24 @@ class UsbDevice : public BaseComponent { /// @brief Convenience overload of write_vendor() that ignores errors. bool write_vendor(std::span data); + /** + * @brief Send a HID input report on the HID function's interrupt IN endpoint. + * @param 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). + * @param report Report payload bytes (without the report-id prefix). + * @param[out] ec Set on failure (HID not enabled / not initialized, host not + * ready, or the HID class driver is not compiled in). + * @return true if the report was queued for transmission, false otherwise. + */ + bool write_hid_report(uint8_t report_id, std::span report, std::error_code &ec); + + /// @brief Convenience overload of write_hid_report() that ignores errors. + bool write_hid_report(uint8_t report_id, std::span report); + + /// @brief Whether the HID function is enabled, mounted and ready to accept a + /// new input report (no report in flight). + bool is_hid_ready() const; + /// @brief Set or replace the CDC receive callback (nullptr to detach). void set_cdc_receive_callback(const receive_callback_fn &cb); @@ -226,6 +258,11 @@ class UsbDevice : public BaseComponent { /// @brief Internal: pointer to the WebUSB URL descriptor bytes (nullptr if none). const uint8_t *webusb_url_descriptor(uint8_t &length) const; + /// @brief 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`. + const uint8_t *hid_report_descriptor() const; + /// @brief Internal: config for the vendor control-request handler. const std::optional &vendor_config() const { return config_.vendor; } @@ -242,6 +279,11 @@ class UsbDevice : public BaseComponent { std::mutex cb_mutex_; receive_callback_fn on_cdc_receive_; receive_callback_fn on_vendor_receive_; + + // Preallocated RX scratch buffers (sized in initialize()) so the TinyUSB-task + // RX handlers stay allocation-free (no heap churn on the hot path). + std::vector cdc_rx_buf_; + std::vector vendor_rx_buf_; }; } // namespace espp diff --git a/components/usb_device/src/usb_device.cpp b/components/usb_device/src/usb_device.cpp index cd7bdfa617..c19bc73ae4 100644 --- a/components/usb_device/src/usb_device.cpp +++ b/components/usb_device/src/usb_device.cpp @@ -41,6 +41,7 @@ struct UsbDevice::Impl { std::vector bos_desc; // BOS (WebUSB + MS OS 2.0), empty if unused std::vector ms_os_20_desc; // MS OS 2.0 descriptor set, empty if unused std::vector webusb_url_desc; // WebUSB URL descriptor, empty if unused + std::vector hid_report_desc; // HID report descriptor bytes, empty if unused // Owning strings + the pointer table TinyUSB reads (index 0 is the LANGID). std::array langid{{0x09, 0x04}}; @@ -49,6 +50,7 @@ struct UsbDevice::Impl { // Allocated interface / endpoint identifiers, filled in during initialize(). uint8_t vendor_itf{0xFF}; + uint8_t hid_itf{0xFF}; }; UsbDevice *UsbDevice::instance() { return s_device; } @@ -62,11 +64,14 @@ UsbDevice::UsbDevice(const Config &config) UsbDevice::~UsbDevice() { if (initialized_) { + // Detach the global callback routing BEFORE tearing down the driver so a + // TinyUSB callback that fires during deinit cannot dereference this + // destructing instance (use-after-free). + if (s_device == this) + s_device = nullptr; if (config_.cdc) tinyusb_cdcacm_deinit(kCdcPort); tinyusb_driver_uninstall(); - if (s_device == this) - s_device = nullptr; initialized_ = false; } } @@ -153,6 +158,37 @@ bool tud_vendor_control_xfer_cb(uint8_t rhport, uint8_t stage, #endif // CFG_TUD_VENDOR > 0 +#if (CFG_TUD_HID > 0) + +// HID: return the application-supplied report descriptor for the given instance. +uint8_t const *tud_hid_descriptor_report_cb(uint8_t instance) { + (void)instance; + return s_device ? s_device->hid_report_descriptor() : nullptr; +} + +// HID GET_REPORT control request: this device is input-only, so nothing to do. +uint16_t tud_hid_get_report_cb(uint8_t instance, uint8_t report_id, hid_report_type_t report_type, + uint8_t *buffer, uint16_t reqlen) { + (void)instance; + (void)report_id; + (void)report_type; + (void)buffer; + (void)reqlen; + return 0; +} + +// HID SET_REPORT control request (and OUT endpoint data): unused / ignored. +void tud_hid_set_report_cb(uint8_t instance, uint8_t report_id, hid_report_type_t report_type, + uint8_t const *buffer, uint16_t bufsize) { + (void)instance; + (void)report_id; + (void)report_type; + (void)buffer; + (void)bufsize; +} + +#endif // CFG_TUD_HID > 0 + } // extern "C" // --------------------------------------------------------------------------- @@ -177,6 +213,10 @@ const uint8_t *UsbDevice::webusb_url_descriptor(uint8_t &length) const { return impl_->webusb_url_desc.data(); } +const uint8_t *UsbDevice::hid_report_descriptor() const { + return impl_->hid_report_desc.empty() ? nullptr : impl_->hid_report_desc.data(); +} + // --------------------------------------------------------------------------- // RX handling. // --------------------------------------------------------------------------- @@ -189,7 +229,7 @@ void UsbDevice::handle_cdc_rx() { } if (!cb || !config_.cdc) return; - std::vector buf(config_.cdc->rx_chunk_size); + std::vector &buf = cdc_rx_buf_; size_t rx_size = 0; do { rx_size = 0; @@ -212,7 +252,7 @@ void UsbDevice::handle_vendor_rx() { } if (!cb || !config_.vendor) return; - std::vector buf(config_.vendor->rx_chunk_size); + std::vector &buf = vendor_rx_buf_; while (tud_vendor_available()) { uint32_t count = tud_vendor_read(buf.data(), buf.size()); if (count == 0) @@ -237,14 +277,14 @@ bool UsbDevice::initialize(std::error_code &ec) { ec = std::make_error_code(std::errc::device_or_resource_busy); return false; } - if (!config_.cdc && !config_.vendor) { - logger_.error("No USB function enabled (enable cdc and/or vendor)"); + if (!config_.cdc && !config_.vendor && !config_.hid) { + logger_.error("No USB function enabled (enable cdc, vendor and/or hid)"); ec = std::make_error_code(std::errc::invalid_argument); return false; } - if (config_.hid || config_.msc) { - // Reserved extension points; not implemented yet (see README endpoint table). - logger_.error("HID/MSC functions are not implemented yet"); + if (config_.msc) { + // Reserved extension point; not implemented yet (see README endpoint table). + logger_.error("MSC function is not implemented yet"); ec = std::make_error_code(std::errc::function_not_supported); return false; } @@ -256,6 +296,14 @@ bool UsbDevice::initialize(std::error_code &ec) { return false; #endif } + if (config_.hid) { +#if (CFG_TUD_HID == 0) + logger_.error("HID function requested but CFG_TUD_HID==0. Set " + "CONFIG_TINYUSB_HID_COUNT>0 in sdkconfig."); + ec = std::make_error_code(std::errc::function_not_supported); + return false; +#endif + } // --- Sequentially allocate interface numbers, endpoint addresses, strings --- uint8_t next_itf = 0; @@ -263,6 +311,13 @@ bool UsbDevice::initialize(std::error_code &ec) { uint8_t in_used = 0, out_used = 0; const int ep_size = (TUD_OPT_HIGH_SPEED ? 512 : 64); + // Preallocate the RX scratch buffers now so the TinyUSB-task RX handlers never + // allocate on the hot path. + if (config_.cdc) + cdc_rx_buf_.assign(config_.cdc->rx_chunk_size, 0); + if (config_.vendor) + vendor_rx_buf_.assign(config_.vendor->rx_chunk_size, 0); + // String table: 0=LANGID, 1=manufacturer, 2=product, 3=serial, then per-itf. impl_->owned_strings = {config_.manufacturer, config_.product, config_.serial_number}; uint8_t next_str = 4; @@ -295,6 +350,23 @@ bool UsbDevice::initialize(std::error_code &ec) { impl_->vendor_itf = vendor_itf; } + uint8_t hid_itf = 0, hid_str = 0, hid_in = 0, hid_out = 0; + if (config_.hid) { + hid_itf = next_itf++; + hid_str = next_str++; + impl_->owned_strings.push_back(config_.hid->interface_name); + const uint8_t h_ep = next_ep++; + hid_in = static_cast(0x80 | h_ep); // interrupt IN + in_used++; + if (config_.hid->has_out_endpoint) { + hid_out = h_ep; // interrupt OUT (shares the endpoint number with IN) + out_used++; + } + impl_->hid_itf = hid_itf; + // Keep our own copy of the report descriptor alive for the driver lifetime. + impl_->hid_report_desc = config_.hid->report_descriptor; + } + // --- Endpoint budget check --- if (in_used > kMaxInEndpoints || out_used > kMaxOutEndpoints) { logger_.error("Endpoint budget exceeded: IN={} (max {}), OUT={} (max {})", in_used, @@ -316,9 +388,19 @@ bool UsbDevice::initialize(std::error_code &ec) { impl_->device_desc.bDescriptorType = TUSB_DESC_DEVICE; // BOS/WebUSB requires bcdUSB >= 2.1. impl_->device_desc.bcdUSB = webusb ? 0x0210 : 0x0200; - impl_->device_desc.bDeviceClass = TUSB_CLASS_MISC; - impl_->device_desc.bDeviceSubClass = MISC_SUBCLASS_COMMON; - impl_->device_desc.bDeviceProtocol = MISC_PROTOCOL_IAD; + // Advertise the IAD-based composite class (0xEF/0x02/0x01) only when CDC is + // enabled, since CDC is the function that emits an Interface Association + // Descriptor. For a vendor-only and/or HID-only device there is no IAD, so use + // 0x00/0x00/0x00 and let the interface descriptors declare the class(es). + if (config_.cdc) { + impl_->device_desc.bDeviceClass = TUSB_CLASS_MISC; + impl_->device_desc.bDeviceSubClass = MISC_SUBCLASS_COMMON; + impl_->device_desc.bDeviceProtocol = MISC_PROTOCOL_IAD; + } else { + impl_->device_desc.bDeviceClass = 0x00; + impl_->device_desc.bDeviceSubClass = 0x00; + impl_->device_desc.bDeviceProtocol = 0x00; + } impl_->device_desc.bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE; impl_->device_desc.idVendor = config_.vid; impl_->device_desc.idProduct = config_.pid; @@ -339,6 +421,11 @@ bool UsbDevice::initialize(std::error_code &ec) { itf_count = static_cast(itf_count + 1); total_len = static_cast(total_len + TUD_VENDOR_DESC_LEN); } + if (config_.hid) { + itf_count = static_cast(itf_count + 1); + total_len = static_cast( + total_len + (config_.hid->has_out_endpoint ? TUD_HID_INOUT_DESC_LEN : TUD_HID_DESC_LEN)); + } impl_->config_desc.clear(); auto append = [&](const uint8_t *p, size_t n) { @@ -363,11 +450,42 @@ bool UsbDevice::initialize(std::error_code &ec) { }; append(d, sizeof(d)); } + if (config_.hid) { + const uint16_t report_len = static_cast(impl_->hid_report_desc.size()); + const uint8_t poll = config_.hid->poll_interval_ms; + // Interrupt endpoints are full-speed (<=64 byte packets) even on high-speed + // parts; a 64-byte endpoint buffer comfortably fits the gamepad report. + constexpr uint8_t kHidEpSize = 64; + if (config_.hid->has_out_endpoint) { + const uint8_t d[] = { + TUD_HID_INOUT_DESCRIPTOR(hid_itf, hid_str, HID_ITF_PROTOCOL_NONE, report_len, hid_out, + hid_in, kHidEpSize, poll), + }; + append(d, sizeof(d)); + } else { + const uint8_t d[] = { + TUD_HID_DESCRIPTOR(hid_itf, hid_str, HID_ITF_PROTOCOL_NONE, report_len, hid_in, + kHidEpSize, poll), + }; + append(d, sizeof(d)); + } + } // --- WebUSB / MS OS 2.0 descriptors (only when the vendor+WebUSB is enabled) --- if (webusb) { const auto &v = *config_.vendor; + // The WebUSB URL descriptor encodes its total length in a single byte + // (bLength = 3 header bytes + URL bytes). Reject a URL that would overflow + // that byte and produce an invalid descriptor (which can break enumeration). + if (v.landing_page_url.size() > (0xFF - 3)) { + logger_.error("WebUSB landing_page_url too long ({} bytes); max {} so bLength (3+url) fits a " + "uint8_t", + v.landing_page_url.size(), 0xFF - 3); + ec = std::make_error_code(std::errc::invalid_argument); + return false; + } + // WebUSB URL descriptor: bLength, bDescriptorType(3), bScheme, url... impl_->webusb_url_desc.clear(); impl_->webusb_url_desc.push_back(static_cast(3 + v.landing_page_url.size())); @@ -599,9 +717,10 @@ bool UsbDevice::initialize(std::error_code &ec) { } initialized_ = true; - logger_.info("Initialized native USB device (VID=0x{:04x} PID=0x{:04x}) cdc={} vendor={}{}", - config_.vid, config_.pid, config_.cdc.has_value(), config_.vendor.has_value(), - webusb ? " webusb" : ""); + logger_.info( + "Initialized native USB device (VID=0x{:04x} PID=0x{:04x}) cdc={} vendor={} hid={}{}", + config_.vid, config_.pid, config_.cdc.has_value(), config_.vendor.has_value(), + config_.hid.has_value(), webusb ? " webusb" : ""); return true; } @@ -670,6 +789,38 @@ bool UsbDevice::write_vendor(std::span data) { return write_vendor(data, ec); } +bool UsbDevice::write_hid_report(uint8_t report_id, std::span report, + std::error_code &ec) { + ec.clear(); +#if (CFG_TUD_HID > 0) + if (!initialized_ || !config_.hid) { + ec = std::make_error_code(std::errc::not_connected); + return false; + } + if (!tud_hid_ready()) { + // Not mounted yet, or a previous report is still in flight. + ec = std::make_error_code(std::errc::not_connected); + return false; + } + if (!tud_hid_report(report_id, report.data(), static_cast(report.size()))) { + logger_.warn_rate_limited("HID report send failed (report_id={})", report_id); + ec = std::make_error_code(std::errc::io_error); + return false; + } + return true; +#else + (void)report_id; + (void)report; + ec = std::make_error_code(std::errc::function_not_supported); + return false; +#endif +} + +bool UsbDevice::write_hid_report(uint8_t report_id, std::span report) { + std::error_code ec; + return write_hid_report(report_id, report, ec); +} + void UsbDevice::set_cdc_receive_callback(const receive_callback_fn &cb) { std::scoped_lock lk(cb_mutex_); on_cdc_receive_ = cb; @@ -694,4 +845,14 @@ bool UsbDevice::is_vendor_connected() const { return tud_mounted(); } +bool UsbDevice::is_hid_ready() const { +#if (CFG_TUD_HID > 0) + if (!initialized_ || !config_.hid) + return false; + return tud_hid_ready(); +#else + return false; +#endif +} + } // namespace espp diff --git a/doc/en/buses/usb_cdc.rst b/doc/en/buses/usb_cdc.rst index ea0047efc5..dfbfe55cda 100644 --- a/doc/en/buses/usb_cdc.rst +++ b/doc/en/buses/usb_cdc.rst @@ -16,12 +16,15 @@ Today it can enable, in any combination (subject to the endpoint budget): 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-rp`` component), with input reports sent via ``write_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 **HID** and **MSC** functions can be added -later without changing the descriptor-building approach. +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 @@ -34,8 +37,11 @@ the logging console. Features -------- -- Composable: enable a CDC function and/or a vendor/WebUSB function (composite) +- 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-rp`` in the example) and ``write_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 @@ -104,6 +110,25 @@ with ``std::errc::function_not_supported``. No custom ``tusb_config`` is require 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) ---------------------------------- @@ -123,7 +148,7 @@ OUT endpoints**. Each function consumes: * - Vendor / WebUSB - 1 (bulk-IN) - 1 (bulk-OUT) - * - HID (future) + * - HID - 1 (interrupt-IN) - 0 or 1 (optional interrupt-OUT) * - MSC (future) @@ -142,17 +167,19 @@ 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 HID / MSC ------------------------- - -``espp::UsbDevice::Config`` reserves ``std::optional`` slots for ``HidFunction`` -and ``MscFunction`` as documented extension points. They are not implemented yet; -enabling one today makes ``initialize()`` fail with -``std::errc::function_not_supported``. When implemented they slot into the same -sequential interface / endpoint / string allocator: a HID function appends one -HID interface (report descriptor + report get/set callbacks) claiming an -interrupt-IN endpoint, and an MSC function appends one MSC interface (SCSI + -storage read/write/capacity callbacks) claiming a bulk IN + bulk OUT endpoint. +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 ----- From e2d13c49910beaba971778bf6065c6b415df0baa Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Mon, 17 Aug 2026 22:36:36 -0500 Subject: [PATCH 04/12] docs(usb_device): fix stale example CMakeLists comment (no override_path) Co-Authored-By: Claude Opus 4.8 --- components/usb_device/example/CMakeLists.txt | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/components/usb_device/example/CMakeLists.txt b/components/usb_device/example/CMakeLists.txt index 1e3bc046c8..75be2e1bd2 100644 --- a/components/usb_device/example/CMakeLists.txt +++ b/components/usb_device/example/CMakeLists.txt @@ -4,11 +4,12 @@ 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` -# component from the ESP registry. 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, and the espp dependencies are pinned to -# their in-repo copies via override_path in main/idf_component.yml. +# 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 From dbfef87ebc1f510784c86872dcc2bfc865b5df7f Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Mon, 17 Aug 2026 23:00:37 -0500 Subject: [PATCH 05/12] feat(usb_device): Web Serial board console + ESP flasher (esptool-js) as the default WebUSB landing page Add components/usb_device/web/board_console.html: a general-purpose, single-file browser tool for any espp/ESP board. - Serial monitor over the Web Serial API: connect/disconnect with selectable baud, live RX view (robust to binary), TX with CR/LF/none line ending + command history, autoscroll/pause/clear/save-log, and Reset / Enter-bootloader buttons (DTR/RTS control signals). - ESP flasher using Espressif's official esptool-js: per-binary rows (file + hex offset), erase-all + flashing-baud options, chip/MAC/flash detection, per-file progress, and a hard reset when done. - Theme-aware (light/dark + manual toggle), responsive, no third-party runtime dependency: esptool-js is vendored same-origin as esptool-bundle.js (esptool-js 0.5.7, Apache-2.0, (c) Espressif). - README documents usage, port choice, flash offsets, and attribution. Host it + make it the default landing page: - build_and_publish_docs.yml copies components/usb_device/web/*.{html,js} into docs/apps/ (nullglob), alongside the other hosted browser apps. - usb_device.hpp: default VendorFunction::landing_page_url now points at esp-cpp.github.io/espp/apps/board_console.html (doc comment updated). Also bump esp_tinyusb dependency to >=2.0 (code uses the 2.x API; resolves to 2.2.1) and drop the registry-invalid `CDC-ACM` tag. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/build_and_publish_docs.yml | 7 + components/usb_device/idf_component.yml | 3 +- components/usb_device/include/usb_device.hpp | 5 +- components/usb_device/web/README.md | 78 ++ components/usb_device/web/board_console.html | 811 +++++++++++++++++++ components/usb_device/web/esptool-bundle.js | 2 + 6 files changed, 902 insertions(+), 4 deletions(-) create mode 100644 components/usb_device/web/README.md create mode 100644 components/usb_device/web/board_console.html create mode 100644 components/usb_device/web/esptool-bundle.js diff --git a/.github/workflows/build_and_publish_docs.yml b/.github/workflows/build_and_publish_docs.yml index 7187439217..dd4928c399 100644 --- a/.github/workflows/build_and_publish_docs.yml +++ b/.github/workflows/build_and_publish_docs.yml @@ -50,6 +50,13 @@ jobs: # copy the generated HTML files to the docs directory for GitHub Pages mkdir -p ../docs cp -r "${build_dir_from_doc}/html/"* ../docs/. + # copy hosted browser apps (WebUSB / Web Serial tools) into docs/apps/ + mkdir -p ../docs/apps + shopt -s nullglob + for f in ../components/usb_device/web/*.html ../components/usb_device/web/*.js; do + cp "$f" ../docs/apps/. + done + shopt -u nullglob - name: Build Documentation (PDF) run: | diff --git a/components/usb_device/idf_component.yml b/components/usb_device/idf_component.yml index 282c6e5255..dce7685309 100644 --- a/components/usb_device/idf_component.yml +++ b/components/usb_device/idf_component.yml @@ -13,7 +13,6 @@ tags: - Component - USB - CDC - - CDC-ACM - Vendor - WebUSB - TinyUSB @@ -23,4 +22,4 @@ dependencies: idf: version: '>=5.0' espp/base_component: '>=1.0' - espressif/esp_tinyusb: '>=1.4' + espressif/esp_tinyusb: '>=2.0' diff --git a/components/usb_device/include/usb_device.hpp b/components/usb_device/include/usb_device.hpp index 369a9af95c..501d1c4043 100644 --- a/components/usb_device/include/usb_device.hpp +++ b/components/usb_device/include/usb_device.hpp @@ -95,11 +95,12 @@ class UsbDevice : public BaseComponent { * 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 ODrive WebUSB console (scheme-less, https). + * 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. */ - std::string landing_page_url{"esp-cpp.github.io/espp/apps/odrive_webusb_console.html"}; + std::string landing_page_url{"esp-cpp.github.io/espp/apps/board_console.html"}; 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{ diff --git a/components/usb_device/web/README.md b/components/usb_device/web/README.md new file mode 100644 index 0000000000..1133942e68 --- /dev/null +++ b/components/usb_device/web/README.md @@ -0,0 +1,78 @@ +# espp Board Console & ESP Flasher (Web Serial) + +`board_console.html` is a single-file, general-purpose browser tool for **any** +espp / ESP board: + +- **Serial monitor** — connect to a board's serial port with the + [Web Serial API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Serial_API), + watch its output live, send commands (configurable CR/LF/none line ending, + command history), autoscroll / pause / clear / save-log-to-file, plus + **Reset board** and **Enter bootloader** buttons that toggle the DTR/RTS + control signals (RTS = EN/reset, DTR = GPIO0/boot). +- **ESP flasher** — flash your own `.bin` files to the connected chip using + Espressif's official [esptool-js](https://github.com/espressif/esptool-js). + Add one row per binary (file picker + hex flash **offset**), or drop a single + merged image at `0x0`. Options: erase-entire-flash, flashing baud rate. The + flasher resets the board into its ROM bootloader, detects the chip (name / + MAC / flash size), writes each file with a per-file progress bar, then + hard-resets the board. + +It is theme-aware (light / dark, with a manual toggle), responsive, and makes +**no third-party network requests** — the only external resource is the +same-origin vendored `esptool-bundle.js`. + +## Running it + +Web Serial only works in a **Chromium-based browser** (Chrome, Edge, Opera, +Brave — Firefox and Safari do not implement it) served from a secure context: +`https://`, `http://localhost`, or a `file://` URL. + +- **Hosted copy:** . + This is also the default WebUSB landing page advertised by the espp + `usb_device` component (open it, then click **Connect**). +- **Locally:** serve this directory over http and open the page, e.g. + ```sh + cd components/usb_device/web + python3 -m http.server 8000 + # then browse to http://localhost:8000/board_console.html + ``` + (Both `board_console.html` and `esptool-bundle.js` must be served from the + same origin, because the page imports the bundle as an ES module.) + +### Which port to connect to + +Connect to the board's **USB-Serial-JTAG** or **UART** port — *not* a USB CDC +port created by the firmware itself (e.g. the espp `usb_device` CDC function). +The flasher drives the ROM serial bootloader over that port by pulsing the +reset / GPIO0 lines, which a firmware-created CDC port cannot do. + +### Flash offset guidance + +Offsets are entered in hex. Typical layout: + +| Region | Offset | +| -------------------------- | ------------------------------------------------------------- | +| Second-stage bootloader | `0x0` on ESP32-S3 / -C3 / -C6 / -H2; `0x1000` on ESP32 / -S2 | +| Partition table | `0x8000` | +| Application (factory app) | `0x10000` | + +A single **merged** image (e.g. produced by `esptool.py merge_bin`) goes in one +row at `0x0`. The offsets ESP-IDF actually uses for a given project are printed +by `idf.py build` (see `flash_args` / the "flash" output). + +## esptool-js attribution + +The flashing feature is powered by **esptool-js** +(), © Espressif Systems, licensed under +the **Apache License 2.0**. To keep this app free of any runtime third-party CDN +dependency, esptool-js is **vendored same-origin**: the published bundled build +is committed here verbatim as `esptool-bundle.js` and imported by +`board_console.html` via `import { ESPLoader, Transport } from './esptool-bundle.js'`. + +- Pinned version: **esptool-js 0.5.7** +- Source of the bundle: + `https://cdn.jsdelivr.net/npm/esptool-js@0.5.7/bundle.js` + +To update it, re-download that pinned URL (bump the version) and replace +`esptool-bundle.js`; no change to `board_console.html` is required as long as +the bundle keeps exporting `ESPLoader` and `Transport`. diff --git a/components/usb_device/web/board_console.html b/components/usb_device/web/board_console.html new file mode 100644 index 0000000000..57c1aa06ff --- /dev/null +++ b/components/usb_device/web/board_console.html @@ -0,0 +1,811 @@ + + + + + + espp Board Console & ESP Flasher + + + + +
+

espp Board Console & ESP Flasher

+
+ + + + Disconnected +
+ +
+ Web Serial is not available in this browser. + This tool needs a Chromium-based browser (Chrome, Edge, Opera, Brave — + Firefox and Safari do not implement the Web Serial API) served over + https://, http://localhost, + or a file:// URL. +
+ +
+ +
+
+

Serial Monitor + +

+
+ + + + + + + +
+
-- Connect to a board's USB-Serial-JTAG or UART port to begin. +
+
+
+ +
+
+ + +
+ +
+
+ Connect to the board's USB-Serial-JTAG or UART port — + not a firmware-created USB CDC port. The flasher resets the chip into its ROM bootloader. +
+
+
+ + +
+
+

ESP Flasher + esptool-js +

+ +
+ Chip + MAC + Flash +
+ +
+
+ + Offsets are hex. Merged image → one row at 0x0. +
+ +
+ + +
+ +
+ + + +
+ +
+ The flasher takes over the serial port: if the monitor is connected it is + briefly closed, esptool-js resets the chip into the ROM bootloader, writes + your binaries, then hard-resets and the monitor reconnects. +
+
+ Typical offsets — second-stage bootloader 0x0 + (ESP32/-S2 use 0x1000, ESP32-S3/-C3 use 0x0), + partition table 0x8000, application 0x10000. +
+ +
+
+
+
+ +
+ espp Board Console & ESP Flasher — Web Serial monitor + esptool-js flasher. + Flashing powered by esptool-js (Apache-2.0, © Espressif), vendored same-origin. +
+ + + + diff --git a/components/usb_device/web/esptool-bundle.js b/components/usb_device/web/esptool-bundle.js new file mode 100644 index 0000000000..efa2a3f05e --- /dev/null +++ b/components/usb_device/web/esptool-bundle.js @@ -0,0 +1,2 @@ +class A extends Error{} +/*! pako 2.1.0 https://github.com/nodeca/pako @license (MIT AND Zlib) */function t(A){let t=A.length;for(;--t>=0;)A[t]=0}const e=256,i=286,s=30,a=15,E=new Uint8Array([0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0]),n=new Uint8Array([0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13]),r=new Uint8Array([0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,3,7]),h=new Uint8Array([16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15]),g=new Array(576);t(g);const o=new Array(60);t(o);const B=new Array(512);t(B);const w=new Array(256);t(w);const c=new Array(29);t(c);const C=new Array(s);function _(A,t,e,i,s){this.static_tree=A,this.extra_bits=t,this.extra_base=e,this.elems=i,this.max_length=s,this.has_stree=A&&A.length}let I,l,d;function D(A,t){this.dyn_tree=A,this.max_code=0,this.stat_desc=t}t(C);const S=A=>A<256?B[A]:B[256+(A>>>7)],R=(A,t)=>{A.pending_buf[A.pending++]=255&t,A.pending_buf[A.pending++]=t>>>8&255},M=(A,t,e)=>{A.bi_valid>16-e?(A.bi_buf|=t<>16-A.bi_valid,A.bi_valid+=e-16):(A.bi_buf|=t<{M(A,e[2*t],e[2*t+1])},F=(A,t)=>{let e=0;do{e|=1&A,A>>>=1,e<<=1}while(--t>0);return e>>>1},f=(A,t,e)=>{const i=new Array(16);let s,E,n=0;for(s=1;s<=a;s++)n=n+e[s-1]<<1,i[s]=n;for(E=0;E<=t;E++){let t=A[2*E+1];0!==t&&(A[2*E]=F(i[t]++,t))}},T=A=>{let t;for(t=0;t{A.bi_valid>8?R(A,A.bi_buf):A.bi_valid>0&&(A.pending_buf[A.pending++]=A.bi_buf),A.bi_buf=0,A.bi_valid=0},P=(A,t,e,i)=>{const s=2*t,a=2*e;return A[s]{const i=A.heap[e];let s=e<<1;for(;s<=A.heap_len&&(s{let s,a,r,h,g=0;if(0!==A.sym_next)do{s=255&A.pending_buf[A.sym_buf+g++],s+=(255&A.pending_buf[A.sym_buf+g++])<<8,a=A.pending_buf[A.sym_buf+g++],0===s?Q(A,a,t):(r=w[a],Q(A,r+e+1,t),h=E[r],0!==h&&(a-=c[r],M(A,a,h)),s--,r=S(s),Q(A,r,i),h=n[r],0!==h&&(s-=C[r],M(A,s,h)))}while(g{const e=t.dyn_tree,i=t.stat_desc.static_tree,s=t.stat_desc.has_stree,E=t.stat_desc.elems;let n,r,h,g=-1;for(A.heap_len=0,A.heap_max=573,n=0;n>1;n>=1;n--)U(A,e,n);h=E;do{n=A.heap[1],A.heap[1]=A.heap[A.heap_len--],U(A,e,1),r=A.heap[1],A.heap[--A.heap_max]=n,A.heap[--A.heap_max]=r,e[2*h]=e[2*n]+e[2*r],A.depth[h]=(A.depth[n]>=A.depth[r]?A.depth[n]:A.depth[r])+1,e[2*n+1]=e[2*r+1]=h,A.heap[1]=h++,U(A,e,1)}while(A.heap_len>=2);A.heap[--A.heap_max]=A.heap[1],((A,t)=>{const e=t.dyn_tree,i=t.max_code,s=t.stat_desc.static_tree,E=t.stat_desc.has_stree,n=t.stat_desc.extra_bits,r=t.stat_desc.extra_base,h=t.stat_desc.max_length;let g,o,B,w,c,C,_=0;for(w=0;w<=a;w++)A.bl_count[w]=0;for(e[2*A.heap[A.heap_max]+1]=0,g=A.heap_max+1;g<573;g++)o=A.heap[g],w=e[2*e[2*o+1]+1]+1,w>h&&(w=h,_++),e[2*o+1]=w,o>i||(A.bl_count[w]++,c=0,o>=r&&(c=n[o-r]),C=e[2*o],A.opt_len+=C*(w+c),E&&(A.static_len+=C*(s[2*o+1]+c)));if(0!==_){do{for(w=h-1;0===A.bl_count[w];)w--;A.bl_count[w]--,A.bl_count[w+1]+=2,A.bl_count[h]--,_-=2}while(_>0);for(w=h;0!==w;w--)for(o=A.bl_count[w];0!==o;)B=A.heap[--g],B>i||(e[2*B+1]!==w&&(A.opt_len+=(w-e[2*B+1])*e[2*B],e[2*B+1]=w),o--)}})(A,t),f(e,g,A.bl_count)},y=(A,t,e)=>{let i,s,a=-1,E=t[1],n=0,r=7,h=4;for(0===E&&(r=138,h=3),t[2*(e+1)+1]=65535,i=0;i<=e;i++)s=E,E=t[2*(i+1)+1],++n{let i,s,a=-1,E=t[1],n=0,r=7,h=4;for(0===E&&(r=138,h=3),i=0;i<=e;i++)if(s=E,E=t[2*(i+1)+1],!(++n{M(A,0+(i?1:0),3),u(A),R(A,e),R(A,~e),e&&A.pending_buf.set(A.window.subarray(t,t+e),A.pending),A.pending+=e};var G=(A,t,i,s)=>{let a,E,n=0;A.level>0?(2===A.strm.data_type&&(A.strm.data_type=(A=>{let t,i=4093624447;for(t=0;t<=31;t++,i>>>=1)if(1&i&&0!==A.dyn_ltree[2*t])return 0;if(0!==A.dyn_ltree[18]||0!==A.dyn_ltree[20]||0!==A.dyn_ltree[26])return 1;for(t=32;t{let t;for(y(A,A.dyn_ltree,A.l_desc.max_code),y(A,A.dyn_dtree,A.d_desc.max_code),p(A,A.bl_desc),t=18;t>=3&&0===A.bl_tree[2*h[t]+1];t--);return A.opt_len+=3*(t+1)+5+5+4,t})(A),a=A.opt_len+3+7>>>3,E=A.static_len+3+7>>>3,E<=a&&(a=E)):a=E=i+5,i+4<=a&&-1!==t?Y(A,t,i,s):4===A.strategy||E===a?(M(A,2+(s?1:0),3),O(A,g,o)):(M(A,4+(s?1:0),3),((A,t,e,i)=>{let s;for(M(A,t-257,5),M(A,e-1,5),M(A,i-4,4),s=0;s{k||((()=>{let A,t,e,h,D;const S=new Array(16);for(e=0,h=0;h<28;h++)for(c[h]=e,A=0;A<1<>=7;h(A.pending_buf[A.sym_buf+A.sym_next++]=t,A.pending_buf[A.sym_buf+A.sym_next++]=t>>8,A.pending_buf[A.sym_buf+A.sym_next++]=i,0===t?A.dyn_ltree[2*i]++:(A.matches++,t--,A.dyn_ltree[2*(w[i]+e+1)]++,A.dyn_dtree[2*S(t)]++),A.sym_next===A.sym_end),_tr_align:A=>{M(A,2,3),Q(A,256,g),(A=>{16===A.bi_valid?(R(A,A.bi_buf),A.bi_buf=0,A.bi_valid=0):A.bi_valid>=8&&(A.pending_buf[A.pending++]=255&A.bi_buf,A.bi_buf>>=8,A.bi_valid-=8)})(A)}};var m=(A,t,e,i)=>{let s=65535&A,a=A>>>16&65535,E=0;for(;0!==e;){E=e>2e3?2e3:e,e-=E;do{s=s+t[i++]|0,a=a+s|0}while(--E);s%=65521,a%=65521}return s|a<<16};const x=new Uint32Array((()=>{let A,t=[];for(var e=0;e<256;e++){A=e;for(var i=0;i<8;i++)A=1&A?3988292384^A>>>1:A>>>1;t[e]=A}return t})());var K=(A,t,e,i)=>{const s=x,a=i+e;A^=-1;for(let e=i;e>>8^s[255&(A^t[e])];return~A},L={2:"need dictionary",1:"stream end",0:"","-1":"file error","-2":"stream error","-3":"data error","-4":"insufficient memory","-5":"buffer error","-6":"incompatible version"},J={Z_NO_FLUSH:0,Z_PARTIAL_FLUSH:1,Z_SYNC_FLUSH:2,Z_FULL_FLUSH:3,Z_FINISH:4,Z_BLOCK:5,Z_TREES:6,Z_OK:0,Z_STREAM_END:1,Z_NEED_DICT:2,Z_ERRNO:-1,Z_STREAM_ERROR:-2,Z_DATA_ERROR:-3,Z_MEM_ERROR:-4,Z_BUF_ERROR:-5,Z_NO_COMPRESSION:0,Z_BEST_SPEED:1,Z_BEST_COMPRESSION:9,Z_DEFAULT_COMPRESSION:-1,Z_FILTERED:1,Z_HUFFMAN_ONLY:2,Z_RLE:3,Z_FIXED:4,Z_DEFAULT_STRATEGY:0,Z_BINARY:0,Z_TEXT:1,Z_UNKNOWN:2,Z_DEFLATED:8};const{_tr_init:N,_tr_stored_block:v,_tr_flush_block:z,_tr_tally:j,_tr_align:W}=b,{Z_NO_FLUSH:Z,Z_PARTIAL_FLUSH:X,Z_FULL_FLUSH:q,Z_FINISH:V,Z_BLOCK:$,Z_OK:AA,Z_STREAM_END:tA,Z_STREAM_ERROR:eA,Z_DATA_ERROR:iA,Z_BUF_ERROR:sA,Z_DEFAULT_COMPRESSION:aA,Z_FILTERED:EA,Z_HUFFMAN_ONLY:nA,Z_RLE:rA,Z_FIXED:hA,Z_DEFAULT_STRATEGY:gA,Z_UNKNOWN:oA,Z_DEFLATED:BA}=J,wA=258,cA=262,CA=42,_A=113,IA=666,lA=(A,t)=>(A.msg=L[t],t),dA=A=>2*A-(A>4?9:0),DA=A=>{let t=A.length;for(;--t>=0;)A[t]=0},SA=A=>{let t,e,i,s=A.w_size;t=A.hash_size,i=t;do{e=A.head[--i],A.head[i]=e>=s?e-s:0}while(--t);t=s,i=t;do{e=A.prev[--i],A.prev[i]=e>=s?e-s:0}while(--t)};let RA=(A,t,e)=>(t<{const t=A.state;let e=t.pending;e>A.avail_out&&(e=A.avail_out),0!==e&&(A.output.set(t.pending_buf.subarray(t.pending_out,t.pending_out+e),A.next_out),A.next_out+=e,t.pending_out+=e,A.total_out+=e,A.avail_out-=e,t.pending-=e,0===t.pending&&(t.pending_out=0))},QA=(A,t)=>{z(A,A.block_start>=0?A.block_start:-1,A.strstart-A.block_start,t),A.block_start=A.strstart,MA(A.strm)},FA=(A,t)=>{A.pending_buf[A.pending++]=t},fA=(A,t)=>{A.pending_buf[A.pending++]=t>>>8&255,A.pending_buf[A.pending++]=255&t},TA=(A,t,e,i)=>{let s=A.avail_in;return s>i&&(s=i),0===s?0:(A.avail_in-=s,t.set(A.input.subarray(A.next_in,A.next_in+s),e),1===A.state.wrap?A.adler=m(A.adler,t,s,e):2===A.state.wrap&&(A.adler=K(A.adler,t,s,e)),A.next_in+=s,A.total_in+=s,s)},uA=(A,t)=>{let e,i,s=A.max_chain_length,a=A.strstart,E=A.prev_length,n=A.nice_match;const r=A.strstart>A.w_size-cA?A.strstart-(A.w_size-cA):0,h=A.window,g=A.w_mask,o=A.prev,B=A.strstart+wA;let w=h[a+E-1],c=h[a+E];A.prev_length>=A.good_match&&(s>>=2),n>A.lookahead&&(n=A.lookahead);do{if(e=t,h[e+E]===c&&h[e+E-1]===w&&h[e]===h[a]&&h[++e]===h[a+1]){a+=2,e++;do{}while(h[++a]===h[++e]&&h[++a]===h[++e]&&h[++a]===h[++e]&&h[++a]===h[++e]&&h[++a]===h[++e]&&h[++a]===h[++e]&&h[++a]===h[++e]&&h[++a]===h[++e]&&aE){if(A.match_start=t,E=i,i>=n)break;w=h[a+E-1],c=h[a+E]}}}while((t=o[t&g])>r&&0!=--s);return E<=A.lookahead?E:A.lookahead},PA=A=>{const t=A.w_size;let e,i,s;do{if(i=A.window_size-A.lookahead-A.strstart,A.strstart>=t+(t-cA)&&(A.window.set(A.window.subarray(t,t+t-i),0),A.match_start-=t,A.strstart-=t,A.block_start-=t,A.insert>A.strstart&&(A.insert=A.strstart),SA(A),i+=t),0===A.strm.avail_in)break;if(e=TA(A.strm,A.window,A.strstart+A.lookahead,i),A.lookahead+=e,A.lookahead+A.insert>=3)for(s=A.strstart-A.insert,A.ins_h=A.window[s],A.ins_h=RA(A,A.ins_h,A.window[s+1]);A.insert&&(A.ins_h=RA(A,A.ins_h,A.window[s+3-1]),A.prev[s&A.w_mask]=A.head[A.ins_h],A.head[A.ins_h]=s,s++,A.insert--,!(A.lookahead+A.insert<3)););}while(A.lookahead{let e,i,s,a=A.pending_buf_size-5>A.w_size?A.w_size:A.pending_buf_size-5,E=0,n=A.strm.avail_in;do{if(e=65535,s=A.bi_valid+42>>3,A.strm.avail_outi+A.strm.avail_in&&(e=i+A.strm.avail_in),e>s&&(e=s),e>8,A.pending_buf[A.pending-2]=~e,A.pending_buf[A.pending-1]=~e>>8,MA(A.strm),i&&(i>e&&(i=e),A.strm.output.set(A.window.subarray(A.block_start,A.block_start+i),A.strm.next_out),A.strm.next_out+=i,A.strm.avail_out-=i,A.strm.total_out+=i,A.block_start+=i,e-=i),e&&(TA(A.strm,A.strm.output,A.strm.next_out,e),A.strm.next_out+=e,A.strm.avail_out-=e,A.strm.total_out+=e)}while(0===E);return n-=A.strm.avail_in,n&&(n>=A.w_size?(A.matches=2,A.window.set(A.strm.input.subarray(A.strm.next_in-A.w_size,A.strm.next_in),0),A.strstart=A.w_size,A.insert=A.strstart):(A.window_size-A.strstart<=n&&(A.strstart-=A.w_size,A.window.set(A.window.subarray(A.w_size,A.w_size+A.strstart),0),A.matches<2&&A.matches++,A.insert>A.strstart&&(A.insert=A.strstart)),A.window.set(A.strm.input.subarray(A.strm.next_in-n,A.strm.next_in),A.strstart),A.strstart+=n,A.insert+=n>A.w_size-A.insert?A.w_size-A.insert:n),A.block_start=A.strstart),A.high_waters&&A.block_start>=A.w_size&&(A.block_start-=A.w_size,A.strstart-=A.w_size,A.window.set(A.window.subarray(A.w_size,A.w_size+A.strstart),0),A.matches<2&&A.matches++,s+=A.w_size,A.insert>A.strstart&&(A.insert=A.strstart)),s>A.strm.avail_in&&(s=A.strm.avail_in),s&&(TA(A.strm,A.window,A.strstart,s),A.strstart+=s,A.insert+=s>A.w_size-A.insert?A.w_size-A.insert:s),A.high_water>3,s=A.pending_buf_size-s>65535?65535:A.pending_buf_size-s,a=s>A.w_size?A.w_size:s,i=A.strstart-A.block_start,(i>=a||(i||t===V)&&t!==Z&&0===A.strm.avail_in&&i<=s)&&(e=i>s?s:i,E=t===V&&0===A.strm.avail_in&&e===i?1:0,v(A,A.block_start,e,E),A.block_start+=e,MA(A.strm)),E?3:1)},OA=(A,t)=>{let e,i;for(;;){if(A.lookahead=3&&(A.ins_h=RA(A,A.ins_h,A.window[A.strstart+3-1]),e=A.prev[A.strstart&A.w_mask]=A.head[A.ins_h],A.head[A.ins_h]=A.strstart),0!==e&&A.strstart-e<=A.w_size-cA&&(A.match_length=uA(A,e)),A.match_length>=3)if(i=j(A,A.strstart-A.match_start,A.match_length-3),A.lookahead-=A.match_length,A.match_length<=A.max_lazy_match&&A.lookahead>=3){A.match_length--;do{A.strstart++,A.ins_h=RA(A,A.ins_h,A.window[A.strstart+3-1]),e=A.prev[A.strstart&A.w_mask]=A.head[A.ins_h],A.head[A.ins_h]=A.strstart}while(0!=--A.match_length);A.strstart++}else A.strstart+=A.match_length,A.match_length=0,A.ins_h=A.window[A.strstart],A.ins_h=RA(A,A.ins_h,A.window[A.strstart+1]);else i=j(A,0,A.window[A.strstart]),A.lookahead--,A.strstart++;if(i&&(QA(A,!1),0===A.strm.avail_out))return 1}return A.insert=A.strstart<2?A.strstart:2,t===V?(QA(A,!0),0===A.strm.avail_out?3:4):A.sym_next&&(QA(A,!1),0===A.strm.avail_out)?1:2},pA=(A,t)=>{let e,i,s;for(;;){if(A.lookahead=3&&(A.ins_h=RA(A,A.ins_h,A.window[A.strstart+3-1]),e=A.prev[A.strstart&A.w_mask]=A.head[A.ins_h],A.head[A.ins_h]=A.strstart),A.prev_length=A.match_length,A.prev_match=A.match_start,A.match_length=2,0!==e&&A.prev_length4096)&&(A.match_length=2)),A.prev_length>=3&&A.match_length<=A.prev_length){s=A.strstart+A.lookahead-3,i=j(A,A.strstart-1-A.prev_match,A.prev_length-3),A.lookahead-=A.prev_length-1,A.prev_length-=2;do{++A.strstart<=s&&(A.ins_h=RA(A,A.ins_h,A.window[A.strstart+3-1]),e=A.prev[A.strstart&A.w_mask]=A.head[A.ins_h],A.head[A.ins_h]=A.strstart)}while(0!=--A.prev_length);if(A.match_available=0,A.match_length=2,A.strstart++,i&&(QA(A,!1),0===A.strm.avail_out))return 1}else if(A.match_available){if(i=j(A,0,A.window[A.strstart-1]),i&&QA(A,!1),A.strstart++,A.lookahead--,0===A.strm.avail_out)return 1}else A.match_available=1,A.strstart++,A.lookahead--}return A.match_available&&(i=j(A,0,A.window[A.strstart-1]),A.match_available=0),A.insert=A.strstart<2?A.strstart:2,t===V?(QA(A,!0),0===A.strm.avail_out?3:4):A.sym_next&&(QA(A,!1),0===A.strm.avail_out)?1:2};function yA(A,t,e,i,s){this.good_length=A,this.max_lazy=t,this.nice_length=e,this.max_chain=i,this.func=s}const HA=[new yA(0,0,0,0,UA),new yA(4,4,8,4,OA),new yA(4,5,16,8,OA),new yA(4,6,32,32,OA),new yA(4,4,16,16,pA),new yA(8,16,32,32,pA),new yA(8,16,128,128,pA),new yA(8,32,128,256,pA),new yA(32,128,258,1024,pA),new yA(32,258,258,4096,pA)];function kA(){this.strm=null,this.status=0,this.pending_buf=null,this.pending_buf_size=0,this.pending_out=0,this.pending=0,this.wrap=0,this.gzhead=null,this.gzindex=0,this.method=BA,this.last_flush=-1,this.w_size=0,this.w_bits=0,this.w_mask=0,this.window=null,this.window_size=0,this.prev=null,this.head=null,this.ins_h=0,this.hash_size=0,this.hash_bits=0,this.hash_mask=0,this.hash_shift=0,this.block_start=0,this.match_length=0,this.prev_match=0,this.match_available=0,this.strstart=0,this.match_start=0,this.lookahead=0,this.prev_length=0,this.max_chain_length=0,this.max_lazy_match=0,this.level=0,this.strategy=0,this.good_match=0,this.nice_match=0,this.dyn_ltree=new Uint16Array(1146),this.dyn_dtree=new Uint16Array(122),this.bl_tree=new Uint16Array(78),DA(this.dyn_ltree),DA(this.dyn_dtree),DA(this.bl_tree),this.l_desc=null,this.d_desc=null,this.bl_desc=null,this.bl_count=new Uint16Array(16),this.heap=new Uint16Array(573),DA(this.heap),this.heap_len=0,this.heap_max=0,this.depth=new Uint16Array(573),DA(this.depth),this.sym_buf=0,this.lit_bufsize=0,this.sym_next=0,this.sym_end=0,this.opt_len=0,this.static_len=0,this.matches=0,this.insert=0,this.bi_buf=0,this.bi_valid=0}const YA=A=>{if(!A)return 1;const t=A.state;return!t||t.strm!==A||t.status!==CA&&57!==t.status&&69!==t.status&&73!==t.status&&91!==t.status&&103!==t.status&&t.status!==_A&&t.status!==IA?1:0},GA=A=>{if(YA(A))return lA(A,eA);A.total_in=A.total_out=0,A.data_type=oA;const t=A.state;return t.pending=0,t.pending_out=0,t.wrap<0&&(t.wrap=-t.wrap),t.status=2===t.wrap?57:t.wrap?CA:_A,A.adler=2===t.wrap?0:1,t.last_flush=-2,N(t),AA},bA=A=>{const t=GA(A);var e;return t===AA&&((e=A.state).window_size=2*e.w_size,DA(e.head),e.max_lazy_match=HA[e.level].max_lazy,e.good_match=HA[e.level].good_length,e.nice_match=HA[e.level].nice_length,e.max_chain_length=HA[e.level].max_chain,e.strstart=0,e.block_start=0,e.lookahead=0,e.insert=0,e.match_length=e.prev_length=2,e.match_available=0,e.ins_h=0),t},mA=(A,t,e,i,s,a)=>{if(!A)return eA;let E=1;if(t===aA&&(t=6),i<0?(E=0,i=-i):i>15&&(E=2,i-=16),s<1||s>9||e!==BA||i<8||i>15||t<0||t>9||a<0||a>hA||8===i&&1!==E)return lA(A,eA);8===i&&(i=9);const n=new kA;return A.state=n,n.strm=A,n.status=CA,n.wrap=E,n.gzhead=null,n.w_bits=i,n.w_size=1<mA(A,t,BA,15,8,gA),deflateInit2:mA,deflateReset:bA,deflateResetKeep:GA,deflateSetHeader:(A,t)=>YA(A)||2!==A.state.wrap?eA:(A.state.gzhead=t,AA),deflate:(A,t)=>{if(YA(A)||t>$||t<0)return A?lA(A,eA):eA;const e=A.state;if(!A.output||0!==A.avail_in&&!A.input||e.status===IA&&t!==V)return lA(A,0===A.avail_out?sA:eA);const i=e.last_flush;if(e.last_flush=t,0!==e.pending){if(MA(A),0===A.avail_out)return e.last_flush=-1,AA}else if(0===A.avail_in&&dA(t)<=dA(i)&&t!==V)return lA(A,sA);if(e.status===IA&&0!==A.avail_in)return lA(A,sA);if(e.status===CA&&0===e.wrap&&(e.status=_A),e.status===CA){let t=BA+(e.w_bits-8<<4)<<8,i=-1;if(i=e.strategy>=nA||e.level<2?0:e.level<6?1:6===e.level?2:3,t|=i<<6,0!==e.strstart&&(t|=32),t+=31-t%31,fA(e,t),0!==e.strstart&&(fA(e,A.adler>>>16),fA(e,65535&A.adler)),A.adler=1,e.status=_A,MA(A),0!==e.pending)return e.last_flush=-1,AA}if(57===e.status)if(A.adler=0,FA(e,31),FA(e,139),FA(e,8),e.gzhead)FA(e,(e.gzhead.text?1:0)+(e.gzhead.hcrc?2:0)+(e.gzhead.extra?4:0)+(e.gzhead.name?8:0)+(e.gzhead.comment?16:0)),FA(e,255&e.gzhead.time),FA(e,e.gzhead.time>>8&255),FA(e,e.gzhead.time>>16&255),FA(e,e.gzhead.time>>24&255),FA(e,9===e.level?2:e.strategy>=nA||e.level<2?4:0),FA(e,255&e.gzhead.os),e.gzhead.extra&&e.gzhead.extra.length&&(FA(e,255&e.gzhead.extra.length),FA(e,e.gzhead.extra.length>>8&255)),e.gzhead.hcrc&&(A.adler=K(A.adler,e.pending_buf,e.pending,0)),e.gzindex=0,e.status=69;else if(FA(e,0),FA(e,0),FA(e,0),FA(e,0),FA(e,0),FA(e,9===e.level?2:e.strategy>=nA||e.level<2?4:0),FA(e,3),e.status=_A,MA(A),0!==e.pending)return e.last_flush=-1,AA;if(69===e.status){if(e.gzhead.extra){let t=e.pending,i=(65535&e.gzhead.extra.length)-e.gzindex;for(;e.pending+i>e.pending_buf_size;){let s=e.pending_buf_size-e.pending;if(e.pending_buf.set(e.gzhead.extra.subarray(e.gzindex,e.gzindex+s),e.pending),e.pending=e.pending_buf_size,e.gzhead.hcrc&&e.pending>t&&(A.adler=K(A.adler,e.pending_buf,e.pending-t,t)),e.gzindex+=s,MA(A),0!==e.pending)return e.last_flush=-1,AA;t=0,i-=s}let s=new Uint8Array(e.gzhead.extra);e.pending_buf.set(s.subarray(e.gzindex,e.gzindex+i),e.pending),e.pending+=i,e.gzhead.hcrc&&e.pending>t&&(A.adler=K(A.adler,e.pending_buf,e.pending-t,t)),e.gzindex=0}e.status=73}if(73===e.status){if(e.gzhead.name){let t,i=e.pending;do{if(e.pending===e.pending_buf_size){if(e.gzhead.hcrc&&e.pending>i&&(A.adler=K(A.adler,e.pending_buf,e.pending-i,i)),MA(A),0!==e.pending)return e.last_flush=-1,AA;i=0}t=e.gzindexi&&(A.adler=K(A.adler,e.pending_buf,e.pending-i,i)),e.gzindex=0}e.status=91}if(91===e.status){if(e.gzhead.comment){let t,i=e.pending;do{if(e.pending===e.pending_buf_size){if(e.gzhead.hcrc&&e.pending>i&&(A.adler=K(A.adler,e.pending_buf,e.pending-i,i)),MA(A),0!==e.pending)return e.last_flush=-1,AA;i=0}t=e.gzindexi&&(A.adler=K(A.adler,e.pending_buf,e.pending-i,i))}e.status=103}if(103===e.status){if(e.gzhead.hcrc){if(e.pending+2>e.pending_buf_size&&(MA(A),0!==e.pending))return e.last_flush=-1,AA;FA(e,255&A.adler),FA(e,A.adler>>8&255),A.adler=0}if(e.status=_A,MA(A),0!==e.pending)return e.last_flush=-1,AA}if(0!==A.avail_in||0!==e.lookahead||t!==Z&&e.status!==IA){let i=0===e.level?UA(e,t):e.strategy===nA?((A,t)=>{let e;for(;;){if(0===A.lookahead&&(PA(A),0===A.lookahead)){if(t===Z)return 1;break}if(A.match_length=0,e=j(A,0,A.window[A.strstart]),A.lookahead--,A.strstart++,e&&(QA(A,!1),0===A.strm.avail_out))return 1}return A.insert=0,t===V?(QA(A,!0),0===A.strm.avail_out?3:4):A.sym_next&&(QA(A,!1),0===A.strm.avail_out)?1:2})(e,t):e.strategy===rA?((A,t)=>{let e,i,s,a;const E=A.window;for(;;){if(A.lookahead<=wA){if(PA(A),A.lookahead<=wA&&t===Z)return 1;if(0===A.lookahead)break}if(A.match_length=0,A.lookahead>=3&&A.strstart>0&&(s=A.strstart-1,i=E[s],i===E[++s]&&i===E[++s]&&i===E[++s])){a=A.strstart+wA;do{}while(i===E[++s]&&i===E[++s]&&i===E[++s]&&i===E[++s]&&i===E[++s]&&i===E[++s]&&i===E[++s]&&i===E[++s]&&sA.lookahead&&(A.match_length=A.lookahead)}if(A.match_length>=3?(e=j(A,1,A.match_length-3),A.lookahead-=A.match_length,A.strstart+=A.match_length,A.match_length=0):(e=j(A,0,A.window[A.strstart]),A.lookahead--,A.strstart++),e&&(QA(A,!1),0===A.strm.avail_out))return 1}return A.insert=0,t===V?(QA(A,!0),0===A.strm.avail_out?3:4):A.sym_next&&(QA(A,!1),0===A.strm.avail_out)?1:2})(e,t):HA[e.level].func(e,t);if(3!==i&&4!==i||(e.status=IA),1===i||3===i)return 0===A.avail_out&&(e.last_flush=-1),AA;if(2===i&&(t===X?W(e):t!==$&&(v(e,0,0,!1),t===q&&(DA(e.head),0===e.lookahead&&(e.strstart=0,e.block_start=0,e.insert=0))),MA(A),0===A.avail_out))return e.last_flush=-1,AA}return t!==V?AA:e.wrap<=0?tA:(2===e.wrap?(FA(e,255&A.adler),FA(e,A.adler>>8&255),FA(e,A.adler>>16&255),FA(e,A.adler>>24&255),FA(e,255&A.total_in),FA(e,A.total_in>>8&255),FA(e,A.total_in>>16&255),FA(e,A.total_in>>24&255)):(fA(e,A.adler>>>16),fA(e,65535&A.adler)),MA(A),e.wrap>0&&(e.wrap=-e.wrap),0!==e.pending?AA:tA)},deflateEnd:A=>{if(YA(A))return eA;const t=A.state.status;return A.state=null,t===_A?lA(A,iA):AA},deflateSetDictionary:(A,t)=>{let e=t.length;if(YA(A))return eA;const i=A.state,s=i.wrap;if(2===s||1===s&&i.status!==CA||i.lookahead)return eA;if(1===s&&(A.adler=m(A.adler,t,e,0)),i.wrap=0,e>=i.w_size){0===s&&(DA(i.head),i.strstart=0,i.block_start=0,i.insert=0);let A=new Uint8Array(i.w_size);A.set(t.subarray(e-i.w_size,e),0),t=A,e=i.w_size}const a=A.avail_in,E=A.next_in,n=A.input;for(A.avail_in=e,A.next_in=0,A.input=t,PA(i);i.lookahead>=3;){let A=i.strstart,t=i.lookahead-2;do{i.ins_h=RA(i,i.ins_h,i.window[A+3-1]),i.prev[A&i.w_mask]=i.head[i.ins_h],i.head[i.ins_h]=A,A++}while(--t);i.strstart=A,i.lookahead=2,PA(i)}return i.strstart+=i.lookahead,i.block_start=i.strstart,i.insert=i.lookahead,i.lookahead=0,i.match_length=i.prev_length=2,i.match_available=0,A.next_in=E,A.input=n,A.avail_in=a,i.wrap=s,AA},deflateInfo:"pako deflate (from Nodeca project)"};const KA=(A,t)=>Object.prototype.hasOwnProperty.call(A,t);var LA=function(A){const t=Array.prototype.slice.call(arguments,1);for(;t.length;){const e=t.shift();if(e){if("object"!=typeof e)throw new TypeError(e+"must be non-object");for(const t in e)KA(e,t)&&(A[t]=e[t])}}return A},JA=A=>{let t=0;for(let e=0,i=A.length;e=252?6:A>=248?5:A>=240?4:A>=224?3:A>=192?2:1;vA[254]=vA[254]=1;var zA=A=>{if("function"==typeof TextEncoder&&TextEncoder.prototype.encode)return(new TextEncoder).encode(A);let t,e,i,s,a,E=A.length,n=0;for(s=0;s>>6,t[a++]=128|63&e):e<65536?(t[a++]=224|e>>>12,t[a++]=128|e>>>6&63,t[a++]=128|63&e):(t[a++]=240|e>>>18,t[a++]=128|e>>>12&63,t[a++]=128|e>>>6&63,t[a++]=128|63&e);return t},jA=(A,t)=>{const e=t||A.length;if("function"==typeof TextDecoder&&TextDecoder.prototype.decode)return(new TextDecoder).decode(A.subarray(0,t));let i,s;const a=new Array(2*e);for(s=0,i=0;i4)a[s++]=65533,i+=E-1;else{for(t&=2===E?31:3===E?15:7;E>1&&i1?a[s++]=65533:t<65536?a[s++]=t:(t-=65536,a[s++]=55296|t>>10&1023,a[s++]=56320|1023&t)}}return((A,t)=>{if(t<65534&&A.subarray&&NA)return String.fromCharCode.apply(null,A.length===t?A:A.subarray(0,t));let e="";for(let i=0;i{(t=t||A.length)>A.length&&(t=A.length);let e=t-1;for(;e>=0&&128==(192&A[e]);)e--;return e<0||0===e?t:e+vA[A[e]]>t?e:t};var ZA=function(){this.input=null,this.next_in=0,this.avail_in=0,this.total_in=0,this.output=null,this.next_out=0,this.avail_out=0,this.total_out=0,this.msg="",this.state=null,this.data_type=2,this.adler=0};const XA=Object.prototype.toString,{Z_NO_FLUSH:qA,Z_SYNC_FLUSH:VA,Z_FULL_FLUSH:$A,Z_FINISH:At,Z_OK:tt,Z_STREAM_END:et,Z_DEFAULT_COMPRESSION:it,Z_DEFAULT_STRATEGY:st,Z_DEFLATED:at}=J;function Et(A){this.options=LA({level:it,method:at,chunkSize:16384,windowBits:15,memLevel:8,strategy:st},A||{});let t=this.options;t.raw&&t.windowBits>0?t.windowBits=-t.windowBits:t.gzip&&t.windowBits>0&&t.windowBits<16&&(t.windowBits+=16),this.err=0,this.msg="",this.ended=!1,this.chunks=[],this.strm=new ZA,this.strm.avail_out=0;let e=xA.deflateInit2(this.strm,t.level,t.method,t.windowBits,t.memLevel,t.strategy);if(e!==tt)throw new Error(L[e]);if(t.header&&xA.deflateSetHeader(this.strm,t.header),t.dictionary){let A;if(A="string"==typeof t.dictionary?zA(t.dictionary):"[object ArrayBuffer]"===XA.call(t.dictionary)?new Uint8Array(t.dictionary):t.dictionary,e=xA.deflateSetDictionary(this.strm,A),e!==tt)throw new Error(L[e]);this._dict_set=!0}}function nt(A,t){const e=new Et(t);if(e.push(A,!0),e.err)throw e.msg||L[e.err];return e.result}Et.prototype.push=function(A,t){const e=this.strm,i=this.options.chunkSize;let s,a;if(this.ended)return!1;for(a=t===~~t?t:!0===t?At:qA,"string"==typeof A?e.input=zA(A):"[object ArrayBuffer]"===XA.call(A)?e.input=new Uint8Array(A):e.input=A,e.next_in=0,e.avail_in=e.input.length;;)if(0===e.avail_out&&(e.output=new Uint8Array(i),e.next_out=0,e.avail_out=i),(a===VA||a===$A)&&e.avail_out<=6)this.onData(e.output.subarray(0,e.next_out)),e.avail_out=0;else{if(s=xA.deflate(e,a),s===et)return e.next_out>0&&this.onData(e.output.subarray(0,e.next_out)),s=xA.deflateEnd(this.strm),this.onEnd(s),this.ended=!0,s===tt;if(0!==e.avail_out){if(a>0&&e.next_out>0)this.onData(e.output.subarray(0,e.next_out)),e.avail_out=0;else if(0===e.avail_in)break}else this.onData(e.output)}return!0},Et.prototype.onData=function(A){this.chunks.push(A)},Et.prototype.onEnd=function(A){A===tt&&(this.result=JA(this.chunks)),this.chunks=[],this.err=A,this.msg=this.strm.msg};var rt={Deflate:Et,deflate:nt,deflateRaw:function(A,t){return(t=t||{}).raw=!0,nt(A,t)},gzip:function(A,t){return(t=t||{}).gzip=!0,nt(A,t)},constants:J};const ht=16209;var gt=function(A,t){let e,i,s,a,E,n,r,h,g,o,B,w,c,C,_,I,l,d,D,S,R,M,Q,F;const f=A.state;e=A.next_in,Q=A.input,i=e+(A.avail_in-5),s=A.next_out,F=A.output,a=s-(t-A.avail_out),E=s+(A.avail_out-257),n=f.dmax,r=f.wsize,h=f.whave,g=f.wnext,o=f.window,B=f.hold,w=f.bits,c=f.lencode,C=f.distcode,_=(1<>>24,B>>>=d,w-=d,d=l>>>16&255,0===d)F[s++]=65535&l;else{if(!(16&d)){if(64&d){if(32&d){f.mode=16191;break A}A.msg="invalid literal/length code",f.mode=ht;break A}l=c[(65535&l)+(B&(1<>>=d,w-=d),w<15&&(B+=Q[e++]<>>24,B>>>=d,w-=d,d=l>>>16&255,16&d){if(S=65535&l,d&=15,wn){A.msg="invalid distance too far back",f.mode=ht;break A}if(B>>>=d,w-=d,d=s-a,S>d){if(d=S-d,d>h&&f.sane){A.msg="invalid distance too far back",f.mode=ht;break A}if(R=0,M=o,0===g){if(R+=r-d,d2;)F[s++]=M[R++],F[s++]=M[R++],F[s++]=M[R++],D-=3;D&&(F[s++]=M[R++],D>1&&(F[s++]=M[R++]))}else{R=s-S;do{F[s++]=F[R++],F[s++]=F[R++],F[s++]=F[R++],D-=3}while(D>2);D&&(F[s++]=F[R++],D>1&&(F[s++]=F[R++]))}break}if(64&d){A.msg="invalid distance code",f.mode=ht;break A}l=C[(65535&l)+(B&(1<>3,e-=D,w-=D<<3,B&=(1<{const r=n.bits;let h,g,o,B,w,c,C=0,_=0,I=0,l=0,d=0,D=0,S=0,R=0,M=0,Q=0,F=null;const f=new Uint16Array(16),T=new Uint16Array(16);let u,P,U,O=null;for(C=0;C<=ot;C++)f[C]=0;for(_=0;_=1&&0===f[l];l--);if(d>l&&(d=l),0===l)return s[a++]=20971520,s[a++]=20971520,n.bits=1,0;for(I=1;I0&&(0===A||1!==l))return-1;for(T[1]=0,C=1;C852||2===A&&M>592)return 1;for(;;){u=C-S,E[_]+1=c?(P=O[E[_]-c],U=F[E[_]-c]):(P=96,U=0),h=1<>S)+g]=u<<24|P<<16|U}while(0!==g);for(h=1<>=1;if(0!==h?(Q&=h-1,Q+=h):Q=0,_++,0==--f[C]){if(C===l)break;C=t[e+E[_]]}if(C>d&&(Q&B)!==o){for(0===S&&(S=d),w+=I,D=C-S,R=1<852||2===A&&M>592)return 1;o=Q&B,s[o]=d<<24|D<<16|w-a}}return 0!==Q&&(s[w+Q]=C-S<<24|64<<16),n.bits=d,0};const{Z_FINISH:It,Z_BLOCK:lt,Z_TREES:dt,Z_OK:Dt,Z_STREAM_END:St,Z_NEED_DICT:Rt,Z_STREAM_ERROR:Mt,Z_DATA_ERROR:Qt,Z_MEM_ERROR:Ft,Z_BUF_ERROR:ft,Z_DEFLATED:Tt}=J,ut=16180,Pt=16190,Ut=16191,Ot=16192,pt=16194,yt=16199,Ht=16200,kt=16206,Yt=16209,Gt=A=>(A>>>24&255)+(A>>>8&65280)+((65280&A)<<8)+((255&A)<<24);function bt(){this.strm=null,this.mode=0,this.last=!1,this.wrap=0,this.havedict=!1,this.flags=0,this.dmax=0,this.check=0,this.total=0,this.head=null,this.wbits=0,this.wsize=0,this.whave=0,this.wnext=0,this.window=null,this.hold=0,this.bits=0,this.length=0,this.offset=0,this.extra=0,this.lencode=null,this.distcode=null,this.lenbits=0,this.distbits=0,this.ncode=0,this.nlen=0,this.ndist=0,this.have=0,this.next=null,this.lens=new Uint16Array(320),this.work=new Uint16Array(288),this.lendyn=null,this.distdyn=null,this.sane=0,this.back=0,this.was=0}const mt=A=>{if(!A)return 1;const t=A.state;return!t||t.strm!==A||t.mode16211?1:0},xt=A=>{if(mt(A))return Mt;const t=A.state;return A.total_in=A.total_out=t.total=0,A.msg="",t.wrap&&(A.adler=1&t.wrap),t.mode=ut,t.last=0,t.havedict=0,t.flags=-1,t.dmax=32768,t.head=null,t.hold=0,t.bits=0,t.lencode=t.lendyn=new Int32Array(852),t.distcode=t.distdyn=new Int32Array(592),t.sane=1,t.back=-1,Dt},Kt=A=>{if(mt(A))return Mt;const t=A.state;return t.wsize=0,t.whave=0,t.wnext=0,xt(A)},Lt=(A,t)=>{let e;if(mt(A))return Mt;const i=A.state;return t<0?(e=0,t=-t):(e=5+(t>>4),t<48&&(t&=15)),t&&(t<8||t>15)?Mt:(null!==i.window&&i.wbits!==t&&(i.window=null),i.wrap=e,i.wbits=t,Kt(A))},Jt=(A,t)=>{if(!A)return Mt;const e=new bt;A.state=e,e.strm=A,e.window=null,e.mode=ut;const i=Lt(A,t);return i!==Dt&&(A.state=null),i};let Nt,vt,zt=!0;const jt=A=>{if(zt){Nt=new Int32Array(512),vt=new Int32Array(32);let t=0;for(;t<144;)A.lens[t++]=8;for(;t<256;)A.lens[t++]=9;for(;t<280;)A.lens[t++]=7;for(;t<288;)A.lens[t++]=8;for(_t(1,A.lens,0,288,Nt,0,A.work,{bits:9}),t=0;t<32;)A.lens[t++]=5;_t(2,A.lens,0,32,vt,0,A.work,{bits:5}),zt=!1}A.lencode=Nt,A.lenbits=9,A.distcode=vt,A.distbits=5},Wt=(A,t,e,i)=>{let s;const a=A.state;return null===a.window&&(a.wsize=1<=a.wsize?(a.window.set(t.subarray(e-a.wsize,e),0),a.wnext=0,a.whave=a.wsize):(s=a.wsize-a.wnext,s>i&&(s=i),a.window.set(t.subarray(e-i,e-i+s),a.wnext),(i-=s)?(a.window.set(t.subarray(e-i,e),0),a.wnext=i,a.whave=a.wsize):(a.wnext+=s,a.wnext===a.wsize&&(a.wnext=0),a.whaveJt(A,15),inflateInit2:Jt,inflate:(A,t)=>{let e,i,s,a,E,n,r,h,g,o,B,w,c,C,_,I,l,d,D,S,R,M,Q=0;const F=new Uint8Array(4);let f,T;const u=new Uint8Array([16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15]);if(mt(A)||!A.output||!A.input&&0!==A.avail_in)return Mt;e=A.state,e.mode===Ut&&(e.mode=Ot),E=A.next_out,s=A.output,r=A.avail_out,a=A.next_in,i=A.input,n=A.avail_in,h=e.hold,g=e.bits,o=n,B=r,M=Dt;A:for(;;)switch(e.mode){case ut:if(0===e.wrap){e.mode=Ot;break}for(;g<16;){if(0===n)break A;n--,h+=i[a++]<>>8&255,e.check=K(e.check,F,2,0),h=0,g=0,e.mode=16181;break}if(e.head&&(e.head.done=!1),!(1&e.wrap)||(((255&h)<<8)+(h>>8))%31){A.msg="incorrect header check",e.mode=Yt;break}if((15&h)!==Tt){A.msg="unknown compression method",e.mode=Yt;break}if(h>>>=4,g-=4,R=8+(15&h),0===e.wbits&&(e.wbits=R),R>15||R>e.wbits){A.msg="invalid window size",e.mode=Yt;break}e.dmax=1<>8&1),512&e.flags&&4&e.wrap&&(F[0]=255&h,F[1]=h>>>8&255,e.check=K(e.check,F,2,0)),h=0,g=0,e.mode=16182;case 16182:for(;g<32;){if(0===n)break A;n--,h+=i[a++]<>>8&255,F[2]=h>>>16&255,F[3]=h>>>24&255,e.check=K(e.check,F,4,0)),h=0,g=0,e.mode=16183;case 16183:for(;g<16;){if(0===n)break A;n--,h+=i[a++]<>8),512&e.flags&&4&e.wrap&&(F[0]=255&h,F[1]=h>>>8&255,e.check=K(e.check,F,2,0)),h=0,g=0,e.mode=16184;case 16184:if(1024&e.flags){for(;g<16;){if(0===n)break A;n--,h+=i[a++]<>>8&255,e.check=K(e.check,F,2,0)),h=0,g=0}else e.head&&(e.head.extra=null);e.mode=16185;case 16185:if(1024&e.flags&&(w=e.length,w>n&&(w=n),w&&(e.head&&(R=e.head.extra_len-e.length,e.head.extra||(e.head.extra=new Uint8Array(e.head.extra_len)),e.head.extra.set(i.subarray(a,a+w),R)),512&e.flags&&4&e.wrap&&(e.check=K(e.check,i,w,a)),n-=w,a+=w,e.length-=w),e.length))break A;e.length=0,e.mode=16186;case 16186:if(2048&e.flags){if(0===n)break A;w=0;do{R=i[a+w++],e.head&&R&&e.length<65536&&(e.head.name+=String.fromCharCode(R))}while(R&&w>9&1,e.head.done=!0),A.adler=e.check=0,e.mode=Ut;break;case 16189:for(;g<32;){if(0===n)break A;n--,h+=i[a++]<>>=7&g,g-=7&g,e.mode=kt;break}for(;g<3;){if(0===n)break A;n--,h+=i[a++]<>>=1,g-=1,3&h){case 0:e.mode=16193;break;case 1:if(jt(e),e.mode=yt,t===dt){h>>>=2,g-=2;break A}break;case 2:e.mode=16196;break;case 3:A.msg="invalid block type",e.mode=Yt}h>>>=2,g-=2;break;case 16193:for(h>>>=7&g,g-=7&g;g<32;){if(0===n)break A;n--,h+=i[a++]<>>16^65535)){A.msg="invalid stored block lengths",e.mode=Yt;break}if(e.length=65535&h,h=0,g=0,e.mode=pt,t===dt)break A;case pt:e.mode=16195;case 16195:if(w=e.length,w){if(w>n&&(w=n),w>r&&(w=r),0===w)break A;s.set(i.subarray(a,a+w),E),n-=w,a+=w,r-=w,E+=w,e.length-=w;break}e.mode=Ut;break;case 16196:for(;g<14;){if(0===n)break A;n--,h+=i[a++]<>>=5,g-=5,e.ndist=1+(31&h),h>>>=5,g-=5,e.ncode=4+(15&h),h>>>=4,g-=4,e.nlen>286||e.ndist>30){A.msg="too many length or distance symbols",e.mode=Yt;break}e.have=0,e.mode=16197;case 16197:for(;e.have>>=3,g-=3}for(;e.have<19;)e.lens[u[e.have++]]=0;if(e.lencode=e.lendyn,e.lenbits=7,f={bits:e.lenbits},M=_t(0,e.lens,0,19,e.lencode,0,e.work,f),e.lenbits=f.bits,M){A.msg="invalid code lengths set",e.mode=Yt;break}e.have=0,e.mode=16198;case 16198:for(;e.have>>24,I=Q>>>16&255,l=65535&Q,!(_<=g);){if(0===n)break A;n--,h+=i[a++]<>>=_,g-=_,e.lens[e.have++]=l;else{if(16===l){for(T=_+2;g>>=_,g-=_,0===e.have){A.msg="invalid bit length repeat",e.mode=Yt;break}R=e.lens[e.have-1],w=3+(3&h),h>>>=2,g-=2}else if(17===l){for(T=_+3;g>>=_,g-=_,R=0,w=3+(7&h),h>>>=3,g-=3}else{for(T=_+7;g>>=_,g-=_,R=0,w=11+(127&h),h>>>=7,g-=7}if(e.have+w>e.nlen+e.ndist){A.msg="invalid bit length repeat",e.mode=Yt;break}for(;w--;)e.lens[e.have++]=R}}if(e.mode===Yt)break;if(0===e.lens[256]){A.msg="invalid code -- missing end-of-block",e.mode=Yt;break}if(e.lenbits=9,f={bits:e.lenbits},M=_t(1,e.lens,0,e.nlen,e.lencode,0,e.work,f),e.lenbits=f.bits,M){A.msg="invalid literal/lengths set",e.mode=Yt;break}if(e.distbits=6,e.distcode=e.distdyn,f={bits:e.distbits},M=_t(2,e.lens,e.nlen,e.ndist,e.distcode,0,e.work,f),e.distbits=f.bits,M){A.msg="invalid distances set",e.mode=Yt;break}if(e.mode=yt,t===dt)break A;case yt:e.mode=Ht;case Ht:if(n>=6&&r>=258){A.next_out=E,A.avail_out=r,A.next_in=a,A.avail_in=n,e.hold=h,e.bits=g,gt(A,B),E=A.next_out,s=A.output,r=A.avail_out,a=A.next_in,i=A.input,n=A.avail_in,h=e.hold,g=e.bits,e.mode===Ut&&(e.back=-1);break}for(e.back=0;Q=e.lencode[h&(1<>>24,I=Q>>>16&255,l=65535&Q,!(_<=g);){if(0===n)break A;n--,h+=i[a++]<>d)],_=Q>>>24,I=Q>>>16&255,l=65535&Q,!(d+_<=g);){if(0===n)break A;n--,h+=i[a++]<>>=d,g-=d,e.back+=d}if(h>>>=_,g-=_,e.back+=_,e.length=l,0===I){e.mode=16205;break}if(32&I){e.back=-1,e.mode=Ut;break}if(64&I){A.msg="invalid literal/length code",e.mode=Yt;break}e.extra=15&I,e.mode=16201;case 16201:if(e.extra){for(T=e.extra;g>>=e.extra,g-=e.extra,e.back+=e.extra}e.was=e.length,e.mode=16202;case 16202:for(;Q=e.distcode[h&(1<>>24,I=Q>>>16&255,l=65535&Q,!(_<=g);){if(0===n)break A;n--,h+=i[a++]<>d)],_=Q>>>24,I=Q>>>16&255,l=65535&Q,!(d+_<=g);){if(0===n)break A;n--,h+=i[a++]<>>=d,g-=d,e.back+=d}if(h>>>=_,g-=_,e.back+=_,64&I){A.msg="invalid distance code",e.mode=Yt;break}e.offset=l,e.extra=15&I,e.mode=16203;case 16203:if(e.extra){for(T=e.extra;g>>=e.extra,g-=e.extra,e.back+=e.extra}if(e.offset>e.dmax){A.msg="invalid distance too far back",e.mode=Yt;break}e.mode=16204;case 16204:if(0===r)break A;if(w=B-r,e.offset>w){if(w=e.offset-w,w>e.whave&&e.sane){A.msg="invalid distance too far back",e.mode=Yt;break}w>e.wnext?(w-=e.wnext,c=e.wsize-w):c=e.wnext-w,w>e.length&&(w=e.length),C=e.window}else C=s,c=E-e.offset,w=e.length;w>r&&(w=r),r-=w,e.length-=w;do{s[E++]=C[c++]}while(--w);0===e.length&&(e.mode=Ht);break;case 16205:if(0===r)break A;s[E++]=e.length,r--,e.mode=Ht;break;case kt:if(e.wrap){for(;g<32;){if(0===n)break A;n--,h|=i[a++]<{if(mt(A))return Mt;let t=A.state;return t.window&&(t.window=null),A.state=null,Dt},inflateGetHeader:(A,t)=>{if(mt(A))return Mt;const e=A.state;return 2&e.wrap?(e.head=t,t.done=!1,Dt):Mt},inflateSetDictionary:(A,t)=>{const e=t.length;let i,s,a;return mt(A)?Mt:(i=A.state,0!==i.wrap&&i.mode!==Pt?Mt:i.mode===Pt&&(s=1,s=m(s,t,e,0),s!==i.check)?Qt:(a=Wt(A,t,e,e),a?(i.mode=16210,Ft):(i.havedict=1,Dt)))},inflateInfo:"pako inflate (from Nodeca project)"};var Xt=function(){this.text=0,this.time=0,this.xflags=0,this.os=0,this.extra=null,this.extra_len=0,this.name="",this.comment="",this.hcrc=0,this.done=!1};const qt=Object.prototype.toString,{Z_NO_FLUSH:Vt,Z_FINISH:$t,Z_OK:Ae,Z_STREAM_END:te,Z_NEED_DICT:ee,Z_STREAM_ERROR:ie,Z_DATA_ERROR:se,Z_MEM_ERROR:ae}=J;function Ee(A){this.options=LA({chunkSize:65536,windowBits:15,to:""},A||{});const t=this.options;t.raw&&t.windowBits>=0&&t.windowBits<16&&(t.windowBits=-t.windowBits,0===t.windowBits&&(t.windowBits=-15)),!(t.windowBits>=0&&t.windowBits<16)||A&&A.windowBits||(t.windowBits+=32),t.windowBits>15&&t.windowBits<48&&(15&t.windowBits||(t.windowBits|=15)),this.err=0,this.msg="",this.ended=!1,this.chunks=[],this.strm=new ZA,this.strm.avail_out=0;let e=Zt.inflateInit2(this.strm,t.windowBits);if(e!==Ae)throw new Error(L[e]);if(this.header=new Xt,Zt.inflateGetHeader(this.strm,this.header),t.dictionary&&("string"==typeof t.dictionary?t.dictionary=zA(t.dictionary):"[object ArrayBuffer]"===qt.call(t.dictionary)&&(t.dictionary=new Uint8Array(t.dictionary)),t.raw&&(e=Zt.inflateSetDictionary(this.strm,t.dictionary),e!==Ae)))throw new Error(L[e])}function ne(A,t){const e=new Ee(t);if(e.push(A),e.err)throw e.msg||L[e.err];return e.result}Ee.prototype.push=function(A,t){const e=this.strm,i=this.options.chunkSize,s=this.options.dictionary;let a,E,n;if(this.ended)return!1;for(E=t===~~t?t:!0===t?$t:Vt,"[object ArrayBuffer]"===qt.call(A)?e.input=new Uint8Array(A):e.input=A,e.next_in=0,e.avail_in=e.input.length;;){for(0===e.avail_out&&(e.output=new Uint8Array(i),e.next_out=0,e.avail_out=i),a=Zt.inflate(e,E),a===ee&&s&&(a=Zt.inflateSetDictionary(e,s),a===Ae?a=Zt.inflate(e,E):a===se&&(a=ee));e.avail_in>0&&a===te&&e.state.wrap>0&&0!==A[e.next_in];)Zt.inflateReset(e),a=Zt.inflate(e,E);switch(a){case ie:case se:case ee:case ae:return this.onEnd(a),this.ended=!0,!1}if(n=e.avail_out,e.next_out&&(0===e.avail_out||a===te))if("string"===this.options.to){let A=WA(e.output,e.next_out),t=e.next_out-A,s=jA(e.output,A);e.next_out=t,e.avail_out=i-t,t&&e.output.set(e.output.subarray(A,A+t),0),this.onData(s)}else this.onData(e.output.length===e.next_out?e.output:e.output.subarray(0,e.next_out));if(a!==Ae||0!==n){if(a===te)return a=Zt.inflateEnd(this.strm),this.onEnd(a),this.ended=!0,!0;if(0===e.avail_in)break}}return!0},Ee.prototype.onData=function(A){this.chunks.push(A)},Ee.prototype.onEnd=function(A){A===Ae&&("string"===this.options.to?this.result=this.chunks.join(""):this.result=JA(this.chunks)),this.chunks=[],this.err=A,this.msg=this.strm.msg};var re={Inflate:Ee,inflate:ne,inflateRaw:function(A,t){return(t=t||{}).raw=!0,ne(A,t)},ungzip:ne,constants:J};const{Deflate:he,deflate:ge,deflateRaw:oe,gzip:Be}=rt,{Inflate:we,inflate:ce,inflateRaw:Ce,ungzip:_e}=re;var Ie=ge,le=we;class de{constructor(A,t=!1,e=!0){this.device=A,this.tracing=t,this.slipReaderEnabled=!1,this.baudrate=0,this.traceLog="",this.lastTraceTime=Date.now(),this.buffer=new Uint8Array(0),this.SLIP_END=192,this.SLIP_ESC=219,this.SLIP_ESC_END=220,this.SLIP_ESC_ESC=221,this._DTR_state=!1,this.slipReaderEnabled=e}getInfo(){const A=this.device.getInfo();return A.usbVendorId&&A.usbProductId?`WebSerial VendorID 0x${A.usbVendorId.toString(16)} ProductID 0x${A.usbProductId.toString(16)}`:""}getPid(){return this.device.getInfo().usbProductId}trace(A){const t=`${`TRACE ${(Date.now()-this.lastTraceTime).toFixed(3)}`} ${A}`;console.log(t),this.traceLog+=t+"\n"}async returnTrace(){try{await navigator.clipboard.writeText(this.traceLog),console.log("Text copied to clipboard!")}catch(A){console.error("Failed to copy text:",A)}}hexify(A){return Array.from(A).map((A=>A.toString(16).padStart(2,"0"))).join("").padEnd(16," ")}hexConvert(A,t=!0){if(t&&A.length>16){let t="",e=A;for(;e.length>0;){const A=e.slice(0,16),i=String.fromCharCode(...A).split("").map((A=>" "===A||A>=" "&&A<="~"&&" "!==A?A:".")).join("");e=e.slice(16),t+=`\n ${this.hexify(A.slice(0,8))} ${this.hexify(A.slice(8))} | ${i}`}return t}return this.hexify(A)}slipWriter(A){const t=[];t.push(192);for(let e=0;esetTimeout((()=>e(new Error("Read timeout exceeded"))),A))),e=await Promise.race([this.reader.read(),t]);if(null===e)break;const{value:i,done:s}=e;if(s||!i)break;yield i}}catch(A){console.error("Error reading from serial port:",A)}finally{this.buffer=new Uint8Array(0)}}async newRead(A,t){if(this.buffer.length>=A){const t=this.buffer.slice(0,A);return this.buffer=this.buffer.slice(A),t}for(;this.buffer.length0?t:1,A);if(!a||0===a.length){const A=null===e?s?"Serial data stream stopped: Possible serial noise or corruption.":"No serial data received.":"Packet content transfer stopped";throw this.trace(A),new Error(A)}this.trace(`Read ${a.length} bytes: ${this.hexConvert(a)}`);let E=0;for(;EsetTimeout(t,A)))}async waitForUnlock(A){for(;this.device.readable&&this.device.readable.locked||this.device.writable&&this.device.writable.locked;)await this.sleep(A)}async disconnect(){var A,t;(null===(A=this.device.readable)||void 0===A?void 0:A.locked)&&await(null===(t=this.reader)||void 0===t?void 0:t.cancel()),await this.waitForUnlock(400),await this.device.close(),this.reader=void 0}}function De(A){return new Promise((t=>setTimeout(t,A)))}class Se{constructor(A,t){this.resetDelay=t,this.transport=A}async reset(){await this.transport.setDTR(!1),await this.transport.setRTS(!0),await De(100),await this.transport.setDTR(!0),await this.transport.setRTS(!1),await De(this.resetDelay),await this.transport.setDTR(!1)}}class Re{constructor(A){this.transport=A}async reset(){await this.transport.setRTS(!1),await this.transport.setDTR(!1),await De(100),await this.transport.setDTR(!0),await this.transport.setRTS(!1),await De(100),await this.transport.setRTS(!0),await this.transport.setDTR(!1),await this.transport.setRTS(!0),await De(100),await this.transport.setRTS(!1),await this.transport.setDTR(!1)}}class Me{constructor(A,t=!1){this.transport=A,this.usingUsbOtg=t,this.transport=A}async reset(){this.usingUsbOtg?(await De(200),await this.transport.setRTS(!1),await De(200)):(await De(100),await this.transport.setRTS(!1))}}function Qe(A){const t=["D","R","W"],e=A.split("|");for(const A of e){const e=A[0],i=A.slice(1);if(!t.includes(e))return!1;if("D"===e||"R"===e){if("0"!==i&&"1"!==i)return!1}else if("W"===e){const A=parseInt(i);if(isNaN(A)||A<=0)return!1}}return!0}class Fe{constructor(A,t){this.transport=A,this.sequenceString=t,this.transport=A}async reset(){const A={D:async A=>await this.transport.setDTR(A),R:async A=>await this.transport.setRTS(A),W:async A=>await De(A)};try{if(!Qe(this.sequenceString))return;const t=this.sequenceString.split("|");for(const e of t){const t=e[0],i=e.slice(1);"W"===t?await A.W(Number(i)):"D"!==t&&"R"!==t||await A[t]("1"===i)}}catch(A){throw new Error("Invalid custom reset sequence")}}}async function fe(A){let t;switch(A){case"ESP32":t=await Promise.resolve().then((function(){return be}));break;case"ESP32-C2":t=await Promise.resolve().then((function(){return ze}));break;case"ESP32-C3":t=await Promise.resolve().then((function(){return Ai}));break;case"ESP32-C5":t=await Promise.resolve().then((function(){return ri}));break;case"ESP32-C6":t=await Promise.resolve().then((function(){return _i}));break;case"ESP32-C61":t=await Promise.resolve().then((function(){return Qi}));break;case"ESP32-H2":t=await Promise.resolve().then((function(){return pi}));break;case"ESP32-P4":t=await Promise.resolve().then((function(){return xi}));break;case"ESP32-S2":t=await Promise.resolve().then((function(){return Wi}));break;case"ESP32-S3":t=await Promise.resolve().then((function(){return es}));break;case"ESP8266":t=await Promise.resolve().then((function(){return gs}))}if(t)return{bss_start:t.bss_start,data:t.data,data_start:t.data_start,entry:t.entry,text:t.text,text_start:t.text_start,decodedData:Te(t.data),decodedText:Te(t.text)}}function Te(A){const t=atob(A).split("").map((function(A){return A.charCodeAt(0)}));return new Uint8Array(t)}function ue(A,t,e=255){const i=A.length%t;if(0!==i){const s=new Uint8Array(t-i).fill(e),a=new Uint8Array(A.length+s.length);return a.set(A),a.set(s,A.length),a}return A}class Pe{constructor(A){var t,e,i,s,a,E,n,r;this.ESP_RAM_BLOCK=6144,this.ESP_FLASH_BEGIN=2,this.ESP_FLASH_DATA=3,this.ESP_FLASH_END=4,this.ESP_MEM_BEGIN=5,this.ESP_MEM_END=6,this.ESP_MEM_DATA=7,this.ESP_WRITE_REG=9,this.ESP_READ_REG=10,this.ESP_SPI_ATTACH=13,this.ESP_CHANGE_BAUDRATE=15,this.ESP_FLASH_DEFL_BEGIN=16,this.ESP_FLASH_DEFL_DATA=17,this.ESP_FLASH_DEFL_END=18,this.ESP_SPI_FLASH_MD5=19,this.ESP_ERASE_FLASH=208,this.ESP_ERASE_REGION=209,this.ESP_READ_FLASH=210,this.ESP_RUN_USER_CODE=211,this.ESP_IMAGE_MAGIC=233,this.ESP_CHECKSUM_MAGIC=239,this.ROM_INVALID_RECV_MSG=5,this.DEFAULT_TIMEOUT=3e3,this.ERASE_REGION_TIMEOUT_PER_MB=3e4,this.ERASE_WRITE_TIMEOUT_PER_MB=4e4,this.MD5_TIMEOUT_PER_MB=8e3,this.CHIP_ERASE_TIMEOUT=12e4,this.FLASH_READ_TIMEOUT=1e5,this.MAX_TIMEOUT=2*this.CHIP_ERASE_TIMEOUT,this.CHIP_DETECT_MAGIC_REG_ADDR=1073745920,this.DETECTED_FLASH_SIZES={18:"256KB",19:"512KB",20:"1MB",21:"2MB",22:"4MB",23:"8MB",24:"16MB"},this.DETECTED_FLASH_SIZES_NUM={18:256,19:512,20:1024,21:2048,22:4096,23:8192,24:16384},this.USB_JTAG_SERIAL_PID=4097,this.romBaudrate=115200,this.debugLogging=!1,this.syncStubDetected=!1,this.flashSizeBytes=function(A){let t=-1;return-1!==A.indexOf("KB")?t=1024*parseInt(A.slice(0,A.indexOf("KB"))):-1!==A.indexOf("MB")&&(t=1024*parseInt(A.slice(0,A.indexOf("MB")))*1024),t},this.IS_STUB=!1,this.FLASH_WRITE_SIZE=16384,this.transport=A.transport,this.baudrate=A.baudrate,this.resetConstructors={classicReset:(A,t)=>new Se(A,t),customReset:(A,t)=>new Fe(A,t),hardReset:(A,t)=>new Me(A,t),usbJTAGSerialReset:A=>new Re(A)},A.serialOptions&&(this.serialOptions=A.serialOptions),A.romBaudrate&&(this.romBaudrate=A.romBaudrate),A.terminal&&(this.terminal=A.terminal,this.terminal.clean()),void 0!==A.debugLogging&&(this.debugLogging=A.debugLogging),A.port&&(this.transport=new de(A.port)),void 0!==A.enableTracing&&(this.transport.tracing=A.enableTracing),(null===(t=A.resetConstructors)||void 0===t?void 0:t.classicReset)&&(this.resetConstructors.classicReset=null===(e=A.resetConstructors)||void 0===e?void 0:e.classicReset),(null===(i=A.resetConstructors)||void 0===i?void 0:i.customReset)&&(this.resetConstructors.customReset=null===(s=A.resetConstructors)||void 0===s?void 0:s.customReset),(null===(a=A.resetConstructors)||void 0===a?void 0:a.hardReset)&&(this.resetConstructors.hardReset=null===(E=A.resetConstructors)||void 0===E?void 0:E.hardReset),(null===(n=A.resetConstructors)||void 0===n?void 0:n.usbJTAGSerialReset)&&(this.resetConstructors.usbJTAGSerialReset=null===(r=A.resetConstructors)||void 0===r?void 0:r.usbJTAGSerialReset),this.info("esptool.js"),this.info("Serial port "+this.transport.getInfo())}_sleep(A){return new Promise((t=>setTimeout(t,A)))}write(A,t=!0){this.terminal?t?this.terminal.writeLine(A):this.terminal.write(A):console.log(A)}error(A,t=!0){this.write(`Error: ${A}`,t)}info(A,t=!0){this.write(A,t)}debug(A,t=!0){this.debugLogging&&this.write(`Debug: ${A}`,t)}_shortToBytearray(A){return new Uint8Array([255&A,A>>8&255])}_intToByteArray(A){return new Uint8Array([255&A,A>>8&255,A>>16&255,A>>24&255])}_byteArrayToShort(A,t){return A|t>>8}_byteArrayToInt(A,t,e,i){return A|t<<8|e<<16|i<<24}_appendBuffer(A,t){const e=new Uint8Array(A.byteLength+t.byteLength);return e.set(new Uint8Array(A),0),e.set(new Uint8Array(t),A.byteLength),e.buffer}_appendArray(A,t){const e=new Uint8Array(A.length+t.length);return e.set(A,0),e.set(t,A.length),e}ui8ToBstr(A){let t="";for(let e=0;e0&&(a=this._appendArray(a,this._intToByteArray(this.chip.UART_DATE_REG_ADDR)),a=this._appendArray(a,this._intToByteArray(0)),a=this._appendArray(a,this._intToByteArray(0)),a=this._appendArray(a,this._intToByteArray(s))),await this.checkCommand("write target memory",this.ESP_WRITE_REG,a)}async sync(){this.debug("Sync");const A=new Uint8Array(36);let t;for(A[0]=7,A[1]=7,A[2]=18,A[3]=32,t=0;t<32;t++)A[4+t]=85;try{let t=await this.command(8,A,void 0,void 0,100);this.syncStubDetected=0===t[0];for(let A=0;A<7;A++)t=await this.command(),this.syncStubDetected=this.syncStubDetected&&0===t[0];return t}catch(A){throw this.debug("Sync err "+A),A}}async _connectAttempt(A="default_reset",t){this.debug("_connect_attempt "+A),t&&await t.reset();const e=this.transport.inWaiting(),i=await this.transport.newRead(e>0?e:1,this.DEFAULT_TIMEOUT),s=Array.from(i,(A=>String.fromCharCode(A))).join("").match(/boot:(0x[0-9a-fA-F]+)(.*waiting for download)?/);let a=!1,E="",n=!1;s&&(a=!0,E=s[1],n=!!s[2]);let r="";for(let A=0;A<5;A++)try{this.debug(`Sync connect attempt ${A}`);const t=await this.sync();return this.debug(t[0].toString()),"success"}catch(A){this.debug(`Error at sync ${A}`),r=A instanceof Error?A.message:"string"==typeof A?A:JSON.stringify(A)}return a&&(r=`Wrong boot mode detected (${E}).\n This chip needs to be in download mode.`,n&&(r="Download mode successfully detected, but getting no sync reply:\n The serial TX path seems to be down.")),r}constructResetSequence(A){if("no_reset"!==A)if("usb_reset"===A||this.transport.getPid()===this.USB_JTAG_SERIAL_PID){if(this.resetConstructors.usbJTAGSerialReset)return this.debug("using USB JTAG Serial Reset"),[this.resetConstructors.usbJTAGSerialReset(this.transport)]}else{const A=50,t=A+500;if(this.resetConstructors.classicReset)return this.debug("using Classic Serial Reset"),[this.resetConstructors.classicReset(this.transport,A),this.resetConstructors.classicReset(this.transport,t)]}return[]}async connect(t="default_reset",e=7,i=!0){let s;this.info("Connecting...",!1),await this.transport.connect(this.romBaudrate,this.serialOptions);const a=this.constructResetSequence(t);for(let A=0;A0?a[A%a.length]:null;if(s=await this._connectAttempt(t,e),"success"===s)break}if("success"!==s)throw new A("Failed to connect with the device");if(this.debug("Connect attempt successful."),this.info("\n\r",!1),i){const t=await this.readReg(this.CHIP_DETECT_MAGIC_REG_ADDR)>>>0;this.debug("Chip Magic "+t.toString(16));const e=await async function(A){switch(A){case 15736195:{const{ESP32ROM:A}=await Promise.resolve().then((function(){return Bs}));return new A}case 203546735:case 1867591791:case 2084675695:{const{ESP32C2ROM:A}=await Promise.resolve().then((function(){return Cs}));return new A}case 1763790959:case 456216687:case 1216438383:case 1130455151:{const{ESP32C3ROM:A}=await Promise.resolve().then((function(){return cs}));return new A}case 752910447:{const{ESP32C6ROM:A}=await Promise.resolve().then((function(){return Is}));return new A}case 606167151:case 871374959:case 1333878895:{const{ESP32C61ROM:A}=await Promise.resolve().then((function(){return ls}));return new A}case 285294703:case 1675706479:case 1607549039:{const{ESP32C5ROM:A}=await Promise.resolve().then((function(){return ds}));return new A}case 3619110528:case 2548236392:{const{ESP32H2ROM:A}=await Promise.resolve().then((function(){return Ds}));return new A}case 9:{const{ESP32S3ROM:A}=await Promise.resolve().then((function(){return Ss}));return new A}case 1990:{const{ESP32S2ROM:A}=await Promise.resolve().then((function(){return Rs}));return new A}case 4293968129:{const{ESP8266ROM:A}=await Promise.resolve().then((function(){return Ms}));return new A}case 0:case 182303440:case 117676761:{const{ESP32P4ROM:A}=await Promise.resolve().then((function(){return Qs}));return new A}default:return null}}(t);if(null===this.chip)throw new A(`Unexpected CHIP magic value ${t}. Failed to autodetect chip type.`);this.chip=e}}async detectChip(A="default_reset"){await this.connect(A),this.info("Detecting chip type... ",!1),null!=this.chip?this.info(this.chip.CHIP_NAME):this.info("unknown!")}async checkCommand(A="",t=null,e=new Uint8Array(0),i=0,s=this.DEFAULT_TIMEOUT){this.debug("check_command "+A);const a=await this.command(t,e,i,void 0,s);return a[1].length>4?a[1]:a[0]}async memBegin(t,e,i,s){if(this.IS_STUB){const e=s,i=s+t,a=await fe(this.chip.CHIP_NAME);if(a){const t=[[a.bss_start||a.data_start,a.data_start+a.decodedData.length],[a.text_start,a.text_start+a.decodedText.length]];for(const[s,a]of t)if(es)throw new A(`Software loader is resident at 0x${s.toString(16).padStart(8,"0")}-0x${a.toString(16).padStart(8,"0")}.\n Can't load binary at overlapping address range 0x${e.toString(16).padStart(8,"0")}-0x${i.toString(16).padStart(8,"0")}.\n Either change binary loading address, or use the no-stub option to disable the software loader.`)}}this.debug("mem_begin "+t+" "+e+" "+i+" "+s.toString(16));let a=this._appendArray(this._intToByteArray(t),this._intToByteArray(e));a=this._appendArray(a,this._intToByteArray(i)),a=this._appendArray(a,this._intToByteArray(s)),await this.checkCommand("enter RAM download mode",this.ESP_MEM_BEGIN,a)}checksum(A,t=this.ESP_CHECKSUM_MAGIC){for(let e=0;e{const e=s+this.chip.SPI_MOSI_DLEN_OFFS,i=s+this.chip.SPI_MISO_DLEN_OFFS;A>0&&await this.writeReg(e,A-1),t>0&&await this.writeReg(i,t-1)}:async(A,t)=>{const e=n,i=(0===t?0:t-1)<<8|(0===A?0:A-1)<<17;await this.writeReg(e,i)};const o=1<<18;if(i>32)throw new A("Reading more than 32 bits back from a SPI flash operation is unsupported");if(e.length>64)throw new A("Writing more than 64 bytes of data with one SPI command is unsupported");const B=8*e.length,w=await this.readReg(E),c=await this.readReg(r);let C,_=1<<31;i>0&&(_|=268435456),B>0&&(_|=134217728),await g(B,i),await this.writeReg(E,_);let I=7<<28|t;if(await this.writeReg(r,I),0==B)await this.writeReg(h,0);else{if(e.length%4!=0){const A=new Uint8Array(e.length%4);e=this._appendArray(e,A)}let A=h;for(C=0;C("00"+A.toString(16)).slice(-2))).join("")}async flashMd5sum(A,t){const e=this.timeoutPerMb(this.MD5_TIMEOUT_PER_MB,t);let i=this._appendArray(this._intToByteArray(A),this._intToByteArray(t));i=this._appendArray(i,this._intToByteArray(0)),i=this._appendArray(i,this._intToByteArray(0));let s=await this.checkCommand("calculate md5sum",this.ESP_SPI_FLASH_MD5,i,void 0,e);s instanceof Uint8Array&&s.length>16&&(s=s.slice(0,16));return this.toHex(s)}async readFlash(t,e,i=null){let s=this._appendArray(this._intToByteArray(t),this._intToByteArray(e));s=this._appendArray(s,this._intToByteArray(4096)),s=this._appendArray(s,this._intToByteArray(1024));const a=await this.checkCommand("read flash",this.ESP_READ_FLASH,s);if(0!=a)throw new A("Failed to read memory: "+a);let E=new Uint8Array(0);for(;E.length0&&(E=this._appendArray(E,t),await this.transport.write(this._intToByteArray(E.length)),i&&i(t,E.length,e))}return E}async runStub(){if(this.syncStubDetected)return this.info("Stub is already running. No upload is necessary."),this.chip;this.info("Uploading stub...");const t=await fe(this.chip.CHIP_NAME);if(void 0===t)throw this.debug("Error loading Stub json"),new Error("Error loading Stub json");const e=[t.decodedText,t.decodedData];for(let A=0;Ae)throw new A(`File ${i+1} doesn't fit in the available flash`)}let e,i;!0===this.IS_STUB&&!0===t.eraseAll&&await this.eraseFlash();for(let s=0;s0;){this.debug("Write loop "+i+" "+r+" "+n),this.info("Writing at 0x"+(i+C).toString(16)+"... ("+Math.floor(100*(r+1)/n)+"%)");const a=this.bstrToUi8(e.slice(0,this.FLASH_WRITE_SIZE));if(!t.compress)throw new A("Yet to handle Non Compressed writes");{const A=C;c.push(a,!1);const t=C-A;let e=3e3;this.timeoutPerMb(this.ERASE_WRITE_TIMEOUT_PER_MB,t)>3e3&&(e=this.timeoutPerMb(this.ERASE_WRITE_TIMEOUT_PER_MB,t)),!1===this.IS_STUB&&(w=e),await this.flashDeflBlock(a,r,w),this.IS_STUB&&(w=e)}h+=a.length,e=e.slice(this.FLASH_WRITE_SIZE,e.length),r++,t.reportProgress&&t.reportProgress(s,h,g)}this.IS_STUB&&await this.readReg(this.CHIP_DETECT_MAGIC_REG_ADDR,w),o=new Date;const _=o.getTime()-B;if(t.compress&&this.info("Wrote "+E+" bytes ("+h+" compressed) at 0x"+i.toString(16)+" in "+_/1e3+" seconds."),a){const t=await this.flashMd5sum(i,E);if(new String(t).valueOf()!=new String(a).valueOf())throw this.info("File md5: "+a),this.info("Flash md5: "+t),new A("MD5 of file does not match data in flash!");this.info("Hash of data verified.")}}this.info("Leaving..."),this.IS_STUB&&(await this.flashBegin(0,0),t.compress?await this.flashDeflFinish():await this.flashFinish())}async flashId(){this.debug("flash_id");const A=await this.readFlashId();this.info("Manufacturer: "+(255&A).toString(16));const t=A>>16&255;this.info("Device: "+(A>>8&255).toString(16)+t.toString(16)),this.info("Detected flash size: "+this.DETECTED_FLASH_SIZES[t])}async getFlashSize(){this.debug("flash_id");const A=await this.readFlashId()>>16&255;return this.DETECTED_FLASH_SIZES_NUM[A]}async softReset(t){if(this.IS_STUB){if("ESP8266"!=this.chip.CHIP_NAME)throw new A("Soft resetting is currently only supported on ESP8266");t?(await this.flashBegin(0,0),await this.flashFinish(!0)):await this.command(this.ESP_RUN_USER_CODE,void 0,void 0,!1)}else{if(t)return;await this.flashBegin(0,0),await this.flashFinish(!1)}}async after(A="hard_reset",t){switch(A){case"hard_reset":if(this.resetConstructors.hardReset){this.info("Hard resetting via RTS pin...");const A=this.resetConstructors.hardReset(this.transport,t);await A.reset()}break;case"soft_reset":this.info("Soft resetting..."),await this.softReset(!1);break;case"no_reset_stub":this.info("Staying in flasher stub.");break;default:this.info("Staying in bootloader."),this.IS_STUB&&this.softReset(!0)}}}class Ue{getEraseSize(A,t){return t}}const Oe=1074521580,pe="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",ye=1074520064,He="DMD8P+znC0B/6AtAZ+0LQAbpC0Cf6AtABukLQGXpC0CC6gtA9OoLQJ3qC0CV5wtAGuoLQHTqC0CI6QtAGOsLQLDpC0AY6wtAbegLQMroC0AG6QtAZekLQIXoC0DI6wtAKe0LQLjmC0BL7QtAuOYLQLjmC0C45gtAuOYLQLjmC0C45gtAuOYLQLjmC0Bv6wtAuOYLQEnsC0Ap7QtA",ke=1073605544,Ye=1073528832;var Ge={entry:Oe,text:pe,text_start:ye,data:He,data_start:ke,bss_start:Ye},be=Object.freeze({__proto__:null,bss_start:Ye,data:He,data_start:ke,default:Ge,entry:Oe,text:pe,text_start:ye});const me=1077413304,xe="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",Ke=1077411840,Le="DEDKP+AIOEAsCThAhAk4QFIKOEC+CjhAbAo4QKgHOEAOCjhATgo4QJgJOEBYBzhAzAk4QFgHOEC6CDhA/gg4QCwJOECECThAzAg4QBIIOEBCCDhAyAg4QBYNOEAsCThA1gs4QMoMOECkBjhA9Aw4QKQGOECkBjhApAY4QKQGOECkBjhApAY4QKQGOECkBjhAcgs4QKQGOEDyCzhAygw4QA==",Je=1070295976,Ne=1070219264;var ve={entry:me,text:xe,text_start:Ke,data:Le,data_start:Je,bss_start:Ne},ze=Object.freeze({__proto__:null,bss_start:Ne,data:Le,data_start:Je,default:ve,entry:me,text:xe,text_start:Ke});const je=1077413584,We="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",Ze=1077411840,Xe="GEDIP8AKOEAQCzhAaAs4QDYMOECiDDhAUAw4QHIJOEDyCzhAMgw4QHwLOEAiCThAsAs4QCIJOECaCjhA4Ao4QBALOEBoCzhArAo4QNYJOEAgCjhAqAo4QPoOOEAQCzhAug04QLIOOEBiCDhA2g44QGIIOEBiCDhAYgg4QGIIOEBiCDhAYgg4QGIIOEBiCDhAVg04QGIIOEDYDThAsg44QA==",qe=1070164916,Ve=1070088192;var $e={entry:je,text:We,text_start:Ze,data:Xe,data_start:qe,bss_start:Ve},Ai=Object.freeze({__proto__:null,bss_start:Ve,data:Xe,data_start:qe,default:$e,entry:je,text:We,text_start:Ze});const ti=1082132164,ei="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",ii=1082130432,si="FACEQG4KgEC+CoBAFguAQOQLgEBQDIBA/guAQDoJgECgC4BA4AuAQCoLgEDqCIBAXguAQOoIgEBICoBAjgqAQL4KgEAWC4BAWgqAQJ4JgEDOCYBAVgqAQKgOgEC+CoBAaA2AQGAOgEAqCIBAiA6AQCoIgEAqCIBAKgiAQCoIgEAqCIBAKgiAQCoIgEAqCIBABA2AQCoIgECGDYBAYA6AQA==",ai=1082469296,Ei=1082392576;var ni={entry:ti,text:ei,text_start:ii,data:si,data_start:ai,bss_start:Ei},ri=Object.freeze({__proto__:null,bss_start:Ei,data:si,data_start:ai,default:ni,entry:ti,text:ei,text_start:ii});const hi=1082132164,gi="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",oi=1082130432,Bi="FACEQHIKgEDCCoBAGguAQOgLgEBUDIBAAgyAQD4JgECkC4BA5AuAQC4LgEDuCIBAYguAQO4IgEBMCoBAkgqAQMIKgEAaC4BAXgqAQKIJgEDSCYBAWgqAQKwOgEDCCoBAbA2AQGQOgEAuCIBAjA6AQC4IgEAuCIBALgiAQC4IgEAuCIBALgiAQC4IgEAuCIBACA2AQC4IgECKDYBAZA6AQA==",wi=1082469296,ci=1082392576;var Ci={entry:hi,text:gi,text_start:oi,data:Bi,data_start:wi,bss_start:ci},_i=Object.freeze({__proto__:null,bss_start:ci,data:Bi,data_start:wi,default:Ci,entry:hi,text:gi,text_start:oi});const Ii=1082132164,li="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",di=1082130432,Di="FECAQHQKgEDECoBAHAuAQOoLgEBWDIBABAyAQEAJgECmC4BA5guAQDALgEDwCIBAZAuAQPAIgEBOCoBAlAqAQMQKgEAcC4BAYAqAQKQJgEDUCYBAXAqAQK4OgEDECoBAbg2AQGYOgEAwCIBAjg6AQDAIgEAwCIBAMAiAQDAIgEAwCIBAMAiAQDAIgEAwCIBACg2AQDAIgECMDYBAZg6AQA==",Si=1082223536,Ri=1082146816;var Mi={entry:Ii,text:li,text_start:di,data:Di,data_start:Si,bss_start:Ri},Qi=Object.freeze({__proto__:null,bss_start:Ri,data:Di,data_start:Si,default:Mi,entry:Ii,text:li,text_start:di});const Fi=1082132164,fi="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",Ti=1082130432,ui="FACDQHIKgEDCCoBAGguAQOgLgEBUDIBAAgyAQD4JgECkC4BA5AuAQC4LgEDuCIBAYguAQO4IgEBMCoBAkgqAQMIKgEAaC4BAXgqAQKIJgEDSCYBAWgqAQKwOgEDCCoBAbA2AQGQOgEAuCIBAjA6AQC4IgEAuCIBALgiAQC4IgEAuCIBALgiAQC4IgEAuCIBACA2AQC4IgECKDYBAZA6AQA==",Pi=1082403760,Ui=1082327040;var Oi={entry:Fi,text:fi,text_start:Ti,data:ui,data_start:Pi,bss_start:Ui},pi=Object.freeze({__proto__:null,bss_start:Ui,data:ui,data_start:Pi,default:Oi,entry:Fi,text:fi,text_start:Ti});const yi=1341195918,Hi="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",ki=1341194240,Yi="EAD1TwYK8U9WCvFPrgrxT4QL8U/wC/FPngvxT9QI8U9AC/FPgAvxT8IK8U+ECPFP9grxT4QI8U/gCfFPJgrxT1YK8U+uCvFP8gnxTzgJ8U9oCfFP7gnxT0AO8U9WCvFPCA3xTwAO8U/EB/FPJA7xT8QH8U/EB/FPxAfxT8QH8U/EB/FPxAfxT8QH8U/EB/FPpAzxT8QH8U8mDfFPAA7xTw==",Gi=1341533100,bi=1341456384;var mi={entry:yi,text:Hi,text_start:ki,data:Yi,data_start:Gi,bss_start:bi},xi=Object.freeze({__proto__:null,bss_start:bi,data:Yi,data_start:Gi,default:mi,entry:yi,text:Hi,text_start:ki});const Ki=1073907716,Li="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",Ji=1073905664,Ni="WAD9P0uLAkDdiwJA8pACQGaMAkD+iwJAZowCQMWMAkDejQJAUY4CQPmNAkDVigJAd40CQNCNAkDojAJAdI4CQBCNAkB0jgJAy4sCQCqMAkBmjAJAxYwCQOOLAkAXiwJAN48CQKqQAkDqiQJA0ZACQOqJAkDqiQJA6okCQOqJAkDqiQJA6okCQOqJAkDqiQJA1I4CQOqJAkDJjwJAqpACQA==",vi=1073622012,zi=1073545216;var ji={entry:Ki,text:Li,text_start:Ji,data:Ni,data_start:vi,bss_start:zi},Wi=Object.freeze({__proto__:null,bss_start:zi,data:Ni,data_start:vi,default:ji,entry:Ki,text:Li,text_start:Ji});const Zi=1077381760,Xi="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",qi=1077379072,Vi="XADKP16ON0AzjzdAR5Q3QL2PN0BTjzdAvY83QB2QN0A6kTdArJE3QFWRN0DpjTdA0JA3QCyRN0BAkDdA0JE3QGiQN0DQkTdAIY83QH6PN0C9jzdAHZA3QDmPN0AqjjdAkJI3QA2UN0AAjTdALZQ3QACNN0AAjTdAAI03QACNN0AAjTdAAI03QACNN0AAjTdAKpI3QACNN0AlkzdADZQ3QAQInwAAAAAAAAAYAQQIBQAAAAAAAAAIAQQIBgAAAAAAAAAAAQQIIQAAAAAAIAAAEQQI3AAAAAAAIAAAEQQIDAAAAAAAIAAAAQQIEgAAAAAAIAAAESAoDAAQAQAA",$i=1070279676,As=1070202880;var ts={entry:Zi,text:Xi,text_start:qi,data:Vi,data_start:$i,bss_start:As},es=Object.freeze({__proto__:null,bss_start:As,data:Vi,data_start:$i,default:ts,entry:Zi,text:Xi,text_start:qi});const is=1074843652,ss="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",as=1074843648,Es="CIH+PwUFBAACAwcAAwMLANTXEEAL2BBAOdgQQNbYEECF5xBAOtkQQJDZEEDc2RBAhecQQKLaEEAf2xBA4NsQQIXnEECF5xBAeNwQQIXnEEBV3xBAHOAQQFfgEECF5xBAhecQQPPgEECF5xBA2+EQQIHiEEDA4xBAf+QQQFDlEECF5xBAhecQQIXnEECF5xBAfuYQQIXnEEB05xBAsN0QQKnYEEDC5RBAydoQQBvaEECF5xBACOcQQE/nEECF5xBAhecQQIXnEECF5xBAhecQQIXnEECF5xBAhecQQELaEEB/2hBA2uUQQAEAAAACAAAAAwAAAAQAAAAFAAAABwAAAAkAAAANAAAAEQAAABkAAAAhAAAAMQAAAEEAAABhAAAAgQAAAMEAAAABAQAAgQEAAAECAAABAwAAAQQAAAEGAAABCAAAAQwAAAEQAAABGAAAASAAAAEwAAABQAAAAWAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAABAAAAAgAAAAIAAAADAAAAAwAAAAQAAAAEAAAABQAAAAUAAAAGAAAABgAAAAcAAAAHAAAACAAAAAgAAAAJAAAACQAAAAoAAAAKAAAACwAAAAsAAAAMAAAADAAAAA0AAAANAAAAAAAAAAAAAAADAAAABAAAAAUAAAAGAAAABwAAAAgAAAAJAAAACgAAAAsAAAANAAAADwAAABEAAAATAAAAFwAAABsAAAAfAAAAIwAAACsAAAAzAAAAOwAAAEMAAABTAAAAYwAAAHMAAACDAAAAowAAAMMAAADjAAAAAgEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAQAAAAEAAAABAAAAAgAAAAIAAAACAAAAAgAAAAMAAAADAAAAAwAAAAMAAAAEAAAABAAAAAQAAAAEAAAABQAAAAUAAAAFAAAABQAAAAAAAAAAAAAAAAAAABAREgAIBwkGCgULBAwDDQIOAQ8AAQEAAAEAAAAEAAAA",ns=1073720488,rs=1073643776;var hs={entry:is,text:ss,text_start:as,data:Es,data_start:ns,bss_start:rs},gs=Object.freeze({__proto__:null,bss_start:rs,data:Es,data_start:ns,default:hs,entry:is,text:ss,text_start:as});class os extends Ue{constructor(){super(...arguments),this.CHIP_NAME="ESP32",this.IMAGE_CHIP_ID=0,this.EFUSE_RD_REG_BASE=1073061888,this.DR_REG_SYSCON_BASE=1073111040,this.UART_CLKDIV_REG=1072955412,this.UART_CLKDIV_MASK=1048575,this.UART_DATE_REG_ADDR=1610612856,this.XTAL_CLK_DIVIDER=1,this.FLASH_SIZES={"1MB":0,"2MB":16,"4MB":32,"8MB":48,"16MB":64},this.FLASH_WRITE_SIZE=1024,this.BOOTLOADER_FLASH_OFFSET=4096,this.SPI_REG_BASE=1072963584,this.SPI_USR_OFFS=28,this.SPI_USR1_OFFS=32,this.SPI_USR2_OFFS=36,this.SPI_W0_OFFS=128,this.SPI_MOSI_DLEN_OFFS=40,this.SPI_MISO_DLEN_OFFS=44}async readEfuse(A,t){const e=this.EFUSE_RD_REG_BASE+4*t;return A.debug("Read efuse "+e),await A.readReg(e)}async getPkgVersion(A){const t=await this.readEfuse(A,3);let e=t>>9&7;return e+=(t>>2&1)<<3,e}async getChipRevision(A){const t=await this.readEfuse(A,3),e=await this.readEfuse(A,5),i=await A.readReg(this.DR_REG_SYSCON_BASE+124);return 0!=(t>>15&1)?0!=(e>>20&1)?0!=(i>>31&1)?3:2:1:0}async getChipDescription(A){const t=["ESP32-D0WDQ6","ESP32-D0WD","ESP32-D2WD","","ESP32-U4WDH","ESP32-PICO-D4","ESP32-PICO-V3-02"];let e="";const i=await this.getPkgVersion(A),s=await this.getChipRevision(A),a=3==s;return 0!=(1&await this.readEfuse(A,3))&&(t[0]="ESP32-S0WDQ6",t[1]="ESP32-S0WD"),a&&(t[5]="ESP32-PICO-V3"),e=i>=0&&i<=6?t[i]:"Unknown ESP32",!a||0!==i&&1!==i||(e+="-V3"),e+" (revision "+s+")"}async getChipFeatures(A){const t=["Wi-Fi"],e=await this.readEfuse(A,3);0===(2&e)&&t.push(" BT");0!==(1&e)?t.push(" Single Core"):t.push(" Dual Core");if(0!==(8192&e)){0!==(4096&e)?t.push(" 160MHz"):t.push(" 240MHz")}const i=await this.getPkgVersion(A);-1!==[2,4,5,6].indexOf(i)&&t.push(" Embedded Flash"),6===i&&t.push(" Embedded PSRAM");0!==(await this.readEfuse(A,4)>>8&31)&&t.push(" VRef calibration in efuse");0!==(e>>14&1)&&t.push(" BLK3 partially reserved");const s=3&await this.readEfuse(A,6);return t.push(" Coding Scheme "+["None","3/4","Repeat (UNSUPPORTED)","Invalid"][s]),t}async getCrystalFreq(A){const t=await A.readReg(this.UART_CLKDIV_REG)&this.UART_CLKDIV_MASK,e=A.transport.baudrate*t/1e6/this.XTAL_CLK_DIVIDER;let i;return i=e>33?40:26,Math.abs(i-e)>1&&A.info("WARNING: Unsupported crystal in use"),i}_d2h(A){const t=(+A).toString(16);return 1===t.length?"0"+t:t}async readMac(A){let t=await this.readEfuse(A,1);t>>>=0;let e=await this.readEfuse(A,2);e>>>=0;const i=new Uint8Array(6);return i[0]=e>>8&255,i[1]=255&e,i[2]=t>>24&255,i[3]=t>>16&255,i[4]=t>>8&255,i[5]=255&t,this._d2h(i[0])+":"+this._d2h(i[1])+":"+this._d2h(i[2])+":"+this._d2h(i[3])+":"+this._d2h(i[4])+":"+this._d2h(i[5])}}var Bs=Object.freeze({__proto__:null,ESP32ROM:os});class ws extends Ue{constructor(){super(...arguments),this.CHIP_NAME="ESP32-C3",this.IMAGE_CHIP_ID=5,this.EFUSE_BASE=1610647552,this.MAC_EFUSE_REG=this.EFUSE_BASE+68,this.UART_CLKDIV_REG=1072955412,this.UART_CLKDIV_MASK=1048575,this.UART_DATE_REG_ADDR=1610612860,this.FLASH_WRITE_SIZE=1024,this.BOOTLOADER_FLASH_OFFSET=0,this.FLASH_SIZES={"1MB":0,"2MB":16,"4MB":32,"8MB":48,"16MB":64},this.SPI_REG_BASE=1610620928,this.SPI_USR_OFFS=24,this.SPI_USR1_OFFS=28,this.SPI_USR2_OFFS=32,this.SPI_MOSI_DLEN_OFFS=36,this.SPI_MISO_DLEN_OFFS=40,this.SPI_W0_OFFS=88}async getPkgVersion(A){const t=this.EFUSE_BASE+68+12;return await A.readReg(t)>>21&7}async getChipRevision(A){const t=this.EFUSE_BASE+68+12;return(await A.readReg(t)&7<<18)>>18}async getMinorChipVersion(A){const t=this.EFUSE_BASE+68+20,e=await A.readReg(t)>>23&1,i=this.EFUSE_BASE+68+12;return(e<<3)+(await A.readReg(i)>>18&7)}async getMajorChipVersion(A){const t=this.EFUSE_BASE+68+20;return await A.readReg(t)>>24&3}async getChipDescription(A){const t=await this.getPkgVersion(A),e=await this.getMajorChipVersion(A),i=await this.getMinorChipVersion(A);return`${{0:"ESP32-C3 (QFN32)",1:"ESP8685 (QFN28)",2:"ESP32-C3 AZ (QFN32)",3:"ESP8686 (QFN24)"}[t]||"Unknown ESP32-C3"} (revision v${e}.${i})`}async getFlashCap(A){const t=this.EFUSE_BASE+68+12;return await A.readReg(t)>>27&7}async getFlashVendor(A){const t=this.EFUSE_BASE+68+16;return{1:"XMC",2:"GD",3:"FM",4:"TT",5:"ZBIT"}[7&await A.readReg(t)]||""}async getChipFeatures(A){const t=["Wi-Fi","BLE"],e=await this.getFlashCap(A),i=await this.getFlashVendor(A),s={0:null,1:"Embedded Flash 4MB",2:"Embedded Flash 2MB",3:"Embedded Flash 1MB",4:"Embedded Flash 8MB"}[e],a=void 0!==s?s:"Unknown Embedded Flash";return null!==s&&t.push(`${a} (${i})`),t}async getCrystalFreq(A){return 40}_d2h(A){const t=(+A).toString(16);return 1===t.length?"0"+t:t}async readMac(A){let t=await A.readReg(this.MAC_EFUSE_REG);t>>>=0;let e=await A.readReg(this.MAC_EFUSE_REG+4);e=e>>>0&65535;const i=new Uint8Array(6);return i[0]=e>>8&255,i[1]=255&e,i[2]=t>>24&255,i[3]=t>>16&255,i[4]=t>>8&255,i[5]=255&t,this._d2h(i[0])+":"+this._d2h(i[1])+":"+this._d2h(i[2])+":"+this._d2h(i[3])+":"+this._d2h(i[4])+":"+this._d2h(i[5])}getEraseSize(A,t){return t}}var cs=Object.freeze({__proto__:null,ESP32C3ROM:ws});var Cs=Object.freeze({__proto__:null,ESP32C2ROM:class extends ws{constructor(){super(...arguments),this.CHIP_NAME="ESP32-C2",this.IMAGE_CHIP_ID=12,this.EFUSE_BASE=1610647552,this.MAC_EFUSE_REG=this.EFUSE_BASE+64,this.UART_CLKDIV_REG=1610612756,this.UART_CLKDIV_MASK=1048575,this.UART_DATE_REG_ADDR=1610612860,this.XTAL_CLK_DIVIDER=1,this.FLASH_WRITE_SIZE=1024,this.BOOTLOADER_FLASH_OFFSET=0,this.FLASH_SIZES={"1MB":0,"2MB":16,"4MB":32,"8MB":48,"16MB":64},this.SPI_REG_BASE=1610620928,this.SPI_USR_OFFS=24,this.SPI_USR1_OFFS=28,this.SPI_USR2_OFFS=32,this.SPI_MOSI_DLEN_OFFS=36,this.SPI_MISO_DLEN_OFFS=40,this.SPI_W0_OFFS=88}async getPkgVersion(A){const t=this.EFUSE_BASE+64+4;return await A.readReg(t)>>22&7}async getChipRevision(A){const t=this.EFUSE_BASE+64+4;return(await A.readReg(t)&3<<20)>>20}async getChipDescription(A){let t;const e=await this.getPkgVersion(A);t=0===e||1===e?"ESP32-C2":"unknown ESP32-C2";return t+=" (revision "+await this.getChipRevision(A)+")",t}async getChipFeatures(A){return["Wi-Fi","BLE"]}async getCrystalFreq(A){const t=await A.readReg(this.UART_CLKDIV_REG)&this.UART_CLKDIV_MASK,e=A.transport.baudrate*t/1e6/this.XTAL_CLK_DIVIDER;let i;return i=e>33?40:26,Math.abs(i-e)>1&&A.info("WARNING: Unsupported crystal in use"),i}async changeBaudRate(A){26===await this.getCrystalFreq(A)&&A.changeBaud()}_d2h(A){const t=(+A).toString(16);return 1===t.length?"0"+t:t}async readMac(A){let t=await A.readReg(this.MAC_EFUSE_REG);t>>>=0;let e=await A.readReg(this.MAC_EFUSE_REG+4);e=e>>>0&65535;const i=new Uint8Array(6);return i[0]=e>>8&255,i[1]=255&e,i[2]=t>>24&255,i[3]=t>>16&255,i[4]=t>>8&255,i[5]=255&t,this._d2h(i[0])+":"+this._d2h(i[1])+":"+this._d2h(i[2])+":"+this._d2h(i[3])+":"+this._d2h(i[4])+":"+this._d2h(i[5])}getEraseSize(A,t){return t}}});class _s extends Ue{constructor(){super(...arguments),this.CHIP_NAME="ESP32-C6",this.IMAGE_CHIP_ID=13,this.EFUSE_BASE=1611335680,this.EFUSE_BLOCK1_ADDR=this.EFUSE_BASE+68,this.MAC_EFUSE_REG=this.EFUSE_BASE+68,this.UART_CLKDIV_REG=1072955412,this.UART_CLKDIV_MASK=1048575,this.UART_DATE_REG_ADDR=1610612860,this.FLASH_WRITE_SIZE=1024,this.BOOTLOADER_FLASH_OFFSET=0,this.FLASH_SIZES={"1MB":0,"2MB":16,"4MB":32,"8MB":48,"16MB":64},this.SPI_REG_BASE=1610620928,this.SPI_USR_OFFS=24,this.SPI_USR1_OFFS=28,this.SPI_USR2_OFFS=32,this.SPI_MOSI_DLEN_OFFS=36,this.SPI_MISO_DLEN_OFFS=40,this.SPI_W0_OFFS=88}async getPkgVersion(A){const t=this.EFUSE_BASE+68+12;return await A.readReg(t)>>21&7}async getChipRevision(A){const t=this.EFUSE_BASE+68+12;return(await A.readReg(t)&7<<18)>>18}async getChipDescription(A){let t;t=0===await this.getPkgVersion(A)?"ESP32-C6":"unknown ESP32-C6";return t+=" (revision "+await this.getChipRevision(A)+")",t}async getChipFeatures(A){return["Wi-Fi 6","BT 5","IEEE802.15.4"]}async getCrystalFreq(A){return 40}_d2h(A){const t=(+A).toString(16);return 1===t.length?"0"+t:t}async readMac(A){let t=await A.readReg(this.MAC_EFUSE_REG);t>>>=0;let e=await A.readReg(this.MAC_EFUSE_REG+4);e=e>>>0&65535;const i=new Uint8Array(6);return i[0]=e>>8&255,i[1]=255&e,i[2]=t>>24&255,i[3]=t>>16&255,i[4]=t>>8&255,i[5]=255&t,this._d2h(i[0])+":"+this._d2h(i[1])+":"+this._d2h(i[2])+":"+this._d2h(i[3])+":"+this._d2h(i[4])+":"+this._d2h(i[5])}getEraseSize(A,t){return t}}var Is=Object.freeze({__proto__:null,ESP32C6ROM:_s});var ls=Object.freeze({__proto__:null,ESP32C61ROM:class extends _s{constructor(){super(...arguments),this.CHIP_NAME="ESP32-C61",this.IMAGE_CHIP_ID=20,this.CHIP_DETECT_MAGIC_VALUE=[871374959,606167151],this.UART_DATE_REG_ADDR=1610612860,this.EFUSE_BASE=1611352064,this.EFUSE_BLOCK1_ADDR=this.EFUSE_BASE+68,this.MAC_EFUSE_REG=this.EFUSE_BASE+68,this.EFUSE_RD_REG_BASE=this.EFUSE_BASE+48,this.EFUSE_PURPOSE_KEY0_REG=this.EFUSE_BASE+52,this.EFUSE_PURPOSE_KEY0_SHIFT=0,this.EFUSE_PURPOSE_KEY1_REG=this.EFUSE_BASE+52,this.EFUSE_PURPOSE_KEY1_SHIFT=4,this.EFUSE_PURPOSE_KEY2_REG=this.EFUSE_BASE+52,this.EFUSE_PURPOSE_KEY2_SHIFT=8,this.EFUSE_PURPOSE_KEY3_REG=this.EFUSE_BASE+52,this.EFUSE_PURPOSE_KEY3_SHIFT=12,this.EFUSE_PURPOSE_KEY4_REG=this.EFUSE_BASE+52,this.EFUSE_PURPOSE_KEY4_SHIFT=16,this.EFUSE_PURPOSE_KEY5_REG=this.EFUSE_BASE+52,this.EFUSE_PURPOSE_KEY5_SHIFT=20,this.EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT_REG=this.EFUSE_RD_REG_BASE,this.EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT=1<<20,this.EFUSE_SPI_BOOT_CRYPT_CNT_REG=this.EFUSE_BASE+48,this.EFUSE_SPI_BOOT_CRYPT_CNT_MASK=7<<23,this.EFUSE_SECURE_BOOT_EN_REG=this.EFUSE_BASE+52,this.EFUSE_SECURE_BOOT_EN_MASK=1<<26,this.FLASH_FREQUENCY={"80m":15,"40m":0,"20m":2},this.MEMORY_MAP=[[0,65536,"PADDING"],[1098907648,1107296256,"DROM"],[1082130432,1082523648,"DRAM"],[1082130432,1082523648,"BYTE_ACCESSIBLE"],[1074048e3,1074069504,"DROM_MASK"],[1073741824,1074048e3,"IROM_MASK"],[1090519040,1098907648,"IROM"],[1082130432,1082523648,"IRAM"],[1342177280,1342193664,"RTC_IRAM"],[1342177280,1342193664,"RTC_DRAM"],[1611653120,1611661312,"MEM_INTERNAL2"]],this.UF2_FAMILY_ID=2010665156,this.EFUSE_MAX_KEY=5,this.KEY_PURPOSES={0:"USER/EMPTY",1:"ECDSA_KEY",2:"XTS_AES_256_KEY_1",3:"XTS_AES_256_KEY_2",4:"XTS_AES_128_KEY",5:"HMAC_DOWN_ALL",6:"HMAC_DOWN_JTAG",7:"HMAC_DOWN_DIGITAL_SIGNATURE",8:"HMAC_UP",9:"SECURE_BOOT_DIGEST0",10:"SECURE_BOOT_DIGEST1",11:"SECURE_BOOT_DIGEST2",12:"KM_INIT_KEY",13:"XTS_AES_256_KEY_1_PSRAM",14:"XTS_AES_256_KEY_2_PSRAM",15:"XTS_AES_128_KEY_PSRAM"}}async getPkgVersion(A){return await A.readReg(this.EFUSE_BLOCK1_ADDR+8)>>26&7}async getMinorChipVersion(A){return 15&await A.readReg(this.EFUSE_BLOCK1_ADDR+8)}async getMajorChipVersion(A){return await A.readReg(this.EFUSE_BLOCK1_ADDR+8)>>4&3}async getChipDescription(A){let t;t=0===await this.getPkgVersion(A)?"ESP32-C61":"unknown ESP32-C61";return`${t} (revision v${await this.getMajorChipVersion(A)}.${await this.getMinorChipVersion(A)})`}async getChipFeatures(A){return["WiFi 6","BT 5"]}async readMac(A){let t=await A.readReg(this.MAC_EFUSE_REG);t>>>=0;let e=await A.readReg(this.MAC_EFUSE_REG+4);e=e>>>0&65535;const i=new Uint8Array(6);return i[0]=e>>8&255,i[1]=255&e,i[2]=t>>24&255,i[3]=t>>16&255,i[4]=t>>8&255,i[5]=255&t,this._d2h(i[0])+":"+this._d2h(i[1])+":"+this._d2h(i[2])+":"+this._d2h(i[3])+":"+this._d2h(i[4])+":"+this._d2h(i[5])}}});var ds=Object.freeze({__proto__:null,ESP32C5ROM:class extends _s{constructor(){super(...arguments),this.CHIP_NAME="ESP32-C5",this.IMAGE_CHIP_ID=23,this.BOOTLOADER_FLASH_OFFSET=8192,this.EFUSE_BASE=1611352064,this.EFUSE_BLOCK1_ADDR=this.EFUSE_BASE+68,this.MAC_EFUSE_REG=this.EFUSE_BASE+68,this.UART_CLKDIV_REG=1610612756,this.EFUSE_RD_REG_BASE=this.EFUSE_BASE+48,this.EFUSE_PURPOSE_KEY0_REG=this.EFUSE_BASE+52,this.EFUSE_PURPOSE_KEY0_SHIFT=24,this.EFUSE_PURPOSE_KEY1_REG=this.EFUSE_BASE+52,this.EFUSE_PURPOSE_KEY1_SHIFT=28,this.EFUSE_PURPOSE_KEY2_REG=this.EFUSE_BASE+56,this.EFUSE_PURPOSE_KEY2_SHIFT=0,this.EFUSE_PURPOSE_KEY3_REG=this.EFUSE_BASE+56,this.EFUSE_PURPOSE_KEY3_SHIFT=4,this.EFUSE_PURPOSE_KEY4_REG=this.EFUSE_BASE+56,this.EFUSE_PURPOSE_KEY4_SHIFT=8,this.EFUSE_PURPOSE_KEY5_REG=this.EFUSE_BASE+56,this.EFUSE_PURPOSE_KEY5_SHIFT=12,this.EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT_REG=this.EFUSE_RD_REG_BASE,this.EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT=1<<20,this.EFUSE_SPI_BOOT_CRYPT_CNT_REG=this.EFUSE_BASE+52,this.EFUSE_SPI_BOOT_CRYPT_CNT_MASK=7<<18,this.EFUSE_SECURE_BOOT_EN_REG=this.EFUSE_BASE+56,this.EFUSE_SECURE_BOOT_EN_MASK=1<<20,this.IROM_MAP_START=1107296256,this.IROM_MAP_END=1115684864,this.DROM_MAP_START=1115684864,this.DROM_MAP_END=1124073472,this.PCR_SYSCLK_CONF_REG=1611227408,this.PCR_SYSCLK_XTAL_FREQ_V=127<<24,this.PCR_SYSCLK_XTAL_FREQ_S=24,this.XTAL_CLK_DIVIDER=1,this.UARTDEV_BUF_NO=1082520860,this.CHIP_DETECT_MAGIC_VALUE=[285294703,1675706479,1607549039],this.FLASH_FREQUENCY={"80m":15,"40m":0,"20m":2},this.MEMORY_MAP=[[0,65536,"PADDING"],[1115684864,1124073472,"DROM"],[1082130432,1082523648,"DRAM"],[1082130432,1082523648,"BYTE_ACCESSIBLE"],[1073979392,1074003968,"DROM_MASK"],[1073741824,1073979392,"IROM_MASK"],[1107296256,1115684864,"IROM"],[1082130432,1082523648,"IRAM"],[1342177280,1342193664,"RTC_IRAM"],[1342177280,1342193664,"RTC_DRAM"],[1611653120,1611661312,"MEM_INTERNAL2"]],this.UF2_FAMILY_ID=4145808195,this.EFUSE_MAX_KEY=5,this.KEY_PURPOSES={0:"USER/EMPTY",1:"ECDSA_KEY",2:"XTS_AES_256_KEY_1",3:"XTS_AES_256_KEY_2",4:"XTS_AES_128_KEY",5:"HMAC_DOWN_ALL",6:"HMAC_DOWN_JTAG",7:"HMAC_DOWN_DIGITAL_SIGNATURE",8:"HMAC_UP",9:"SECURE_BOOT_DIGEST0",10:"SECURE_BOOT_DIGEST1",11:"SECURE_BOOT_DIGEST2",12:"KM_INIT_KEY"}}async getPkgVersion(A){return await A.readReg(this.EFUSE_BLOCK1_ADDR+8)>>26&7}async getMinorChipVersion(A){return 15&await A.readReg(this.EFUSE_BLOCK1_ADDR+8)}async getMajorChipVersion(A){return await A.readReg(this.EFUSE_BLOCK1_ADDR+8)>>4&3}async getChipDescription(A){let t;t=0===await this.getPkgVersion(A)?"ESP32-C5":"unknown ESP32-C5";return`${t} (revision v${await this.getMajorChipVersion(A)}.${await this.getMinorChipVersion(A)})`}async getChipFeatures(A){return["Wi-Fi 6 (dual-band)","BT 5 (LE)"]}async getCrystalFreq(A){const t=await A.readReg(this.UART_CLKDIV_REG)&this.UART_CLKDIV_MASK,e=A.transport.baudrate*t/1e6/this.XTAL_CLK_DIVIDER;let i;return i=e>45?48:e>33?40:26,Math.abs(i-e)>1&&A.info("WARNING: Unsupported crystal in use"),i}async getCrystalFreqRomExpect(A){return(await A.readReg(this.PCR_SYSCLK_CONF_REG)&this.PCR_SYSCLK_XTAL_FREQ_V)>>this.PCR_SYSCLK_XTAL_FREQ_S}}});var Ds=Object.freeze({__proto__:null,ESP32H2ROM:class extends Ue{constructor(){super(...arguments),this.CHIP_NAME="ESP32-H2",this.IMAGE_CHIP_ID=16,this.EFUSE_BASE=1611335680,this.EFUSE_BLOCK1_ADDR=this.EFUSE_BASE+68,this.MAC_EFUSE_REG=this.EFUSE_BASE+68,this.UART_CLKDIV_REG=1072955412,this.UART_CLKDIV_MASK=1048575,this.UART_DATE_REG_ADDR=1610612860,this.FLASH_WRITE_SIZE=1024,this.BOOTLOADER_FLASH_OFFSET=0,this.FLASH_SIZES={"1MB":0,"2MB":16,"4MB":32,"8MB":48,"16MB":64},this.SPI_REG_BASE=1610620928,this.SPI_USR_OFFS=24,this.SPI_USR1_OFFS=28,this.SPI_USR2_OFFS=32,this.SPI_MOSI_DLEN_OFFS=36,this.SPI_MISO_DLEN_OFFS=40,this.SPI_W0_OFFS=88,this.USB_RAM_BLOCK=2048,this.UARTDEV_BUF_NO_USB=3,this.UARTDEV_BUF_NO=1070526796}async getPkgVersion(A){return 7&await A.readReg(this.EFUSE_BLOCK1_ADDR+16)}async getMinorChipVersion(A){return await A.readReg(this.EFUSE_BLOCK1_ADDR+12)>>18&7}async getMajorChipVersion(A){return await A.readReg(this.EFUSE_BLOCK1_ADDR+12)>>21&3}async getChipDescription(A){let t;t=0===await this.getPkgVersion(A)?"ESP32-H2":"unknown ESP32-H2";return`${t} (revision v${await this.getMajorChipVersion(A)}.${await this.getMinorChipVersion(A)})`}async getChipFeatures(A){return["BT 5 (LE)","IEEE802.15.4","Single Core","96MHz"]}async getCrystalFreq(A){return 32}_d2h(A){const t=(+A).toString(16);return 1===t.length?"0"+t:t}async postConnect(A){const t=255&await A.readReg(this.UARTDEV_BUF_NO);A.debug("In _post_connect "+t),t==this.UARTDEV_BUF_NO_USB&&(A.ESP_RAM_BLOCK=this.USB_RAM_BLOCK)}async readMac(A){let t=await A.readReg(this.MAC_EFUSE_REG);t>>>=0;let e=await A.readReg(this.MAC_EFUSE_REG+4);e=e>>>0&65535;const i=new Uint8Array(6);return i[0]=e>>8&255,i[1]=255&e,i[2]=t>>24&255,i[3]=t>>16&255,i[4]=t>>8&255,i[5]=255&t,this._d2h(i[0])+":"+this._d2h(i[1])+":"+this._d2h(i[2])+":"+this._d2h(i[3])+":"+this._d2h(i[4])+":"+this._d2h(i[5])}getEraseSize(A,t){return t}}});var Ss=Object.freeze({__proto__:null,ESP32S3ROM:class extends Ue{constructor(){super(...arguments),this.CHIP_NAME="ESP32-S3",this.IMAGE_CHIP_ID=9,this.EFUSE_BASE=1610641408,this.MAC_EFUSE_REG=this.EFUSE_BASE+68,this.EFUSE_BLOCK1_ADDR=this.EFUSE_BASE+68,this.EFUSE_BLOCK2_ADDR=this.EFUSE_BASE+92,this.UART_CLKDIV_REG=1610612756,this.UART_CLKDIV_MASK=1048575,this.UART_DATE_REG_ADDR=1610612864,this.FLASH_WRITE_SIZE=1024,this.BOOTLOADER_FLASH_OFFSET=0,this.FLASH_SIZES={"1MB":0,"2MB":16,"4MB":32,"8MB":48,"16MB":64},this.SPI_REG_BASE=1610620928,this.SPI_USR_OFFS=24,this.SPI_USR1_OFFS=28,this.SPI_USR2_OFFS=32,this.SPI_MOSI_DLEN_OFFS=36,this.SPI_MISO_DLEN_OFFS=40,this.SPI_W0_OFFS=88,this.USB_RAM_BLOCK=2048,this.UARTDEV_BUF_NO_USB=3,this.UARTDEV_BUF_NO=1070526796}async getChipDescription(A){const t=await this.getMajorChipVersion(A),e=await this.getMinorChipVersion(A);return`${{0:"ESP32-S3 (QFN56)",1:"ESP32-S3-PICO-1 (LGA56)"}[await this.getPkgVersion(A)]||"unknown ESP32-S3"} (revision v${t}.${e})`}async getPkgVersion(A){return await A.readReg(this.EFUSE_BLOCK1_ADDR+12)>>21&7}async getRawMinorChipVersion(A){return((await A.readReg(this.EFUSE_BLOCK1_ADDR+20)>>23&1)<<3)+(await A.readReg(this.EFUSE_BLOCK1_ADDR+12)>>18&7)}async getMinorChipVersion(A){const t=await this.getRawMinorChipVersion(A);return await this.isEco0(A,t)?0:this.getRawMinorChipVersion(A)}async getRawMajorChipVersion(A){return await A.readReg(this.EFUSE_BLOCK1_ADDR+20)>>24&3}async getMajorChipVersion(A){const t=await this.getRawMinorChipVersion(A);return await this.isEco0(A,t)?0:this.getRawMajorChipVersion(A)}async getBlkVersionMajor(A){return 3&await A.readReg(this.EFUSE_BLOCK2_ADDR+16)}async getBlkVersionMinor(A){return await A.readReg(this.EFUSE_BLOCK1_ADDR+12)>>24&7}async isEco0(A,t){return!(7&t)&&1===await this.getBlkVersionMajor(A)&&1===await this.getBlkVersionMinor(A)}async getFlashCap(A){const t=this.EFUSE_BASE+68+12;return await A.readReg(t)>>27&7}async getFlashVendor(A){const t=this.EFUSE_BASE+68+16;return{1:"XMC",2:"GD",3:"FM",4:"TT",5:"BY"}[7&await A.readReg(t)]||""}async getPsramCap(A){const t=this.EFUSE_BASE+68+16;return await A.readReg(t)>>3&3}async getPsramVendor(A){const t=this.EFUSE_BASE+68+16;return{1:"AP_3v3",2:"AP_1v8"}[await A.readReg(t)>>7&3]||""}async getChipFeatures(A){const t=["Wi-Fi","BLE"],e=await this.getFlashCap(A),i=await this.getFlashVendor(A),s={0:null,1:"Embedded Flash 8MB",2:"Embedded Flash 4MB"}[e],a=void 0!==s?s:"Unknown Embedded Flash";null!==s&&t.push(`${a} (${i})`);const E=await this.getPsramCap(A),n=await this.getPsramVendor(A),r={0:null,1:"Embedded PSRAM 8MB",2:"Embedded PSRAM 2MB"}[E],h=void 0!==r?r:"Unknown Embedded PSRAM";return null!==r&&t.push(`${h} (${n})`),t}async getCrystalFreq(A){return 40}_d2h(A){const t=(+A).toString(16);return 1===t.length?"0"+t:t}async postConnect(A){const t=255&await A.readReg(this.UARTDEV_BUF_NO);A.debug("In _post_connect "+t),t==this.UARTDEV_BUF_NO_USB&&(A.ESP_RAM_BLOCK=this.USB_RAM_BLOCK)}async readMac(A){let t=await A.readReg(this.MAC_EFUSE_REG);t>>>=0;let e=await A.readReg(this.MAC_EFUSE_REG+4);e=e>>>0&65535;const i=new Uint8Array(6);return i[0]=e>>8&255,i[1]=255&e,i[2]=t>>24&255,i[3]=t>>16&255,i[4]=t>>8&255,i[5]=255&t,this._d2h(i[0])+":"+this._d2h(i[1])+":"+this._d2h(i[2])+":"+this._d2h(i[3])+":"+this._d2h(i[4])+":"+this._d2h(i[5])}getEraseSize(A,t){return t}}});var Rs=Object.freeze({__proto__:null,ESP32S2ROM:class extends Ue{constructor(){super(...arguments),this.CHIP_NAME="ESP32-S2",this.IMAGE_CHIP_ID=2,this.IROM_MAP_START=1074266112,this.IROM_MAP_END=1085800448,this.DROM_MAP_START=1056964608,this.DROM_MAP_END=1061093376,this.CHIP_DETECT_MAGIC_VALUE=[1990],this.SPI_REG_BASE=1061167104,this.SPI_USR_OFFS=24,this.SPI_USR1_OFFS=28,this.SPI_USR2_OFFS=32,this.SPI_MOSI_DLEN_OFFS=36,this.SPI_MISO_DLEN_OFFS=40,this.SPI_W0_OFFS=88,this.SPI_ADDR_REG_MSB=!1,this.MAC_EFUSE_REG=1061265476,this.UART_CLKDIV_REG=1061158932,this.SUPPORTS_ENCRYPTED_FLASH=!0,this.FLASH_ENCRYPTED_WRITE_ALIGN=16,this.EFUSE_BASE=1061265408,this.EFUSE_RD_REG_BASE=this.EFUSE_BASE+48,this.EFUSE_BLOCK1_ADDR=this.EFUSE_BASE+68,this.EFUSE_BLOCK2_ADDR=this.EFUSE_BASE+92,this.EFUSE_PURPOSE_KEY0_REG=this.EFUSE_BASE+52,this.EFUSE_PURPOSE_KEY0_SHIFT=24,this.EFUSE_PURPOSE_KEY1_REG=this.EFUSE_BASE+52,this.EFUSE_PURPOSE_KEY1_SHIFT=28,this.EFUSE_PURPOSE_KEY2_REG=this.EFUSE_BASE+56,this.EFUSE_PURPOSE_KEY2_SHIFT=0,this.EFUSE_PURPOSE_KEY3_REG=this.EFUSE_BASE+56,this.EFUSE_PURPOSE_KEY3_SHIFT=4,this.EFUSE_PURPOSE_KEY4_REG=this.EFUSE_BASE+56,this.EFUSE_PURPOSE_KEY4_SHIFT=8,this.EFUSE_PURPOSE_KEY5_REG=this.EFUSE_BASE+56,this.EFUSE_PURPOSE_KEY5_SHIFT=12,this.EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT_REG=this.EFUSE_RD_REG_BASE,this.EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT=1<<19,this.EFUSE_SPI_BOOT_CRYPT_CNT_REG=this.EFUSE_BASE+52,this.EFUSE_SPI_BOOT_CRYPT_CNT_MASK=7<<18,this.EFUSE_SECURE_BOOT_EN_REG=this.EFUSE_BASE+56,this.EFUSE_SECURE_BOOT_EN_MASK=1<<20,this.EFUSE_RD_REPEAT_DATA3_REG=this.EFUSE_BASE+60,this.EFUSE_RD_REPEAT_DATA3_REG_FLASH_TYPE_MASK=512,this.PURPOSE_VAL_XTS_AES256_KEY_1=2,this.PURPOSE_VAL_XTS_AES256_KEY_2=3,this.PURPOSE_VAL_XTS_AES128_KEY=4,this.UARTDEV_BUF_NO=1073741076,this.UARTDEV_BUF_NO_USB_OTG=2,this.USB_RAM_BLOCK=2048,this.GPIO_STRAP_REG=1061175352,this.GPIO_STRAP_SPI_BOOT_MASK=8,this.GPIO_STRAP_VDDSPI_MASK=16,this.RTC_CNTL_OPTION1_REG=1061191976,this.RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK=1,this.RTCCNTL_BASE_REG=1061191680,this.RTC_CNTL_WDTCONFIG0_REG=this.RTCCNTL_BASE_REG+148,this.RTC_CNTL_WDTCONFIG1_REG=this.RTCCNTL_BASE_REG+152,this.RTC_CNTL_WDTWPROTECT_REG=this.RTCCNTL_BASE_REG+172,this.RTC_CNTL_WDT_WKEY=1356348065,this.MEMORY_MAP=[[0,65536,"PADDING"],[1056964608,1073217536,"DROM"],[1062207488,1073217536,"EXTRAM_DATA"],[1073340416,1073348608,"RTC_DRAM"],[1073340416,1073741824,"BYTE_ACCESSIBLE"],[1073340416,1074208768,"MEM_INTERNAL"],[1073414144,1073741824,"DRAM"],[1073741824,1073848576,"IROM_MASK"],[1073872896,1074200576,"IRAM"],[1074200576,1074208768,"RTC_IRAM"],[1074266112,1082130432,"IROM"],[1342177280,1342185472,"RTC_DATA"]],this.EFUSE_VDD_SPI_REG=this.EFUSE_BASE+52,this.VDD_SPI_XPD=16,this.VDD_SPI_TIEH=32,this.VDD_SPI_FORCE=64,this.UF2_FAMILY_ID=3218951918,this.EFUSE_MAX_KEY=5,this.KEY_PURPOSES={0:"USER/EMPTY",1:"RESERVED",2:"XTS_AES_256_KEY_1",3:"XTS_AES_256_KEY_2",4:"XTS_AES_128_KEY",5:"HMAC_DOWN_ALL",6:"HMAC_DOWN_JTAG",7:"HMAC_DOWN_DIGITAL_SIGNATURE",8:"HMAC_UP",9:"SECURE_BOOT_DIGEST0",10:"SECURE_BOOT_DIGEST1",11:"SECURE_BOOT_DIGEST2"},this.UART_CLKDIV_MASK=1048575,this.UART_DATE_REG_ADDR=1610612856,this.FLASH_WRITE_SIZE=1024,this.BOOTLOADER_FLASH_OFFSET=4096,this.FLASH_SIZES={"1MB":0,"2MB":16,"4MB":32,"8MB":48,"16MB":64}}async getPkgVersion(A){const t=this.EFUSE_BLOCK1_ADDR+16;return 15&await A.readReg(t)}async getMinorChipVersion(A){return((await A.readReg(this.EFUSE_BLOCK1_ADDR+12)>>20&1)<<3)+(await A.readReg(this.EFUSE_BLOCK1_ADDR+16)>>4&7)}async getMajorChipVersion(A){return await A.readReg(this.EFUSE_BLOCK1_ADDR+12)>>18&3}async getFlashVersion(A){return await A.readReg(this.EFUSE_BLOCK1_ADDR+12)>>21&15}async getChipDescription(A){const t=await this.getFlashCap(A)+100*await this.getPsramCap(A),e=await this.getMajorChipVersion(A),i=await this.getMinorChipVersion(A);return`${{0:"ESP32-S2",1:"ESP32-S2FH2",2:"ESP32-S2FH4",102:"ESP32-S2FNR2",100:"ESP32-S2R2"}[t]||"unknown ESP32-S2"} (revision v${e}.${i})`}async getFlashCap(A){return await this.getFlashVersion(A)}async getPsramVersion(A){const t=this.EFUSE_BLOCK1_ADDR+12;return await A.readReg(t)>>28&15}async getPsramCap(A){return await this.getPsramVersion(A)}async getBlock2Version(A){const t=this.EFUSE_BLOCK2_ADDR+16;return await A.readReg(t)>>4&7}async getChipFeatures(A){const t=["Wi-Fi"],e={0:"No Embedded Flash",1:"Embedded Flash 2MB",2:"Embedded Flash 4MB"}[await this.getFlashCap(A)]||"Unknown Embedded Flash";t.push(e);const i={0:"No Embedded Flash",1:"Embedded PSRAM 2MB",2:"Embedded PSRAM 4MB"}[await this.getPsramCap(A)]||"Unknown Embedded PSRAM";t.push(i);const s={0:"No calibration in BLK2 of efuse",1:"ADC and temperature sensor calibration in BLK2 of efuse V1",2:"ADC and temperature sensor calibration in BLK2 of efuse V2"}[await this.getBlock2Version(A)]||"Unknown Calibration in BLK2";return t.push(s),t}async getCrystalFreq(A){return 40}_d2h(A){const t=(+A).toString(16);return 1===t.length?"0"+t:t}async readMac(A){let t=await A.readReg(this.MAC_EFUSE_REG);t>>>=0;let e=await A.readReg(this.MAC_EFUSE_REG+4);e=e>>>0&65535;const i=new Uint8Array(6);return i[0]=e>>8&255,i[1]=255&e,i[2]=t>>24&255,i[3]=t>>16&255,i[4]=t>>8&255,i[5]=255&t,this._d2h(i[0])+":"+this._d2h(i[1])+":"+this._d2h(i[2])+":"+this._d2h(i[3])+":"+this._d2h(i[4])+":"+this._d2h(i[5])}getEraseSize(A,t){return t}async usingUsbOtg(A){return(255&await A.readReg(this.UARTDEV_BUF_NO))===this.UARTDEV_BUF_NO_USB_OTG}async postConnect(A){const t=await this.usingUsbOtg(A);A.debug("In _post_connect using USB OTG ?"+t),t&&(A.ESP_RAM_BLOCK=this.USB_RAM_BLOCK)}}});var Ms=Object.freeze({__proto__:null,ESP8266ROM:class extends Ue{constructor(){super(...arguments),this.CHIP_NAME="ESP8266",this.CHIP_DETECT_MAGIC_VALUE=[4293968129],this.EFUSE_RD_REG_BASE=1072693328,this.UART_CLKDIV_REG=1610612756,this.UART_CLKDIV_MASK=1048575,this.XTAL_CLK_DIVIDER=2,this.FLASH_WRITE_SIZE=16384,this.BOOTLOADER_FLASH_OFFSET=0,this.UART_DATE_REG_ADDR=0,this.FLASH_SIZES={"512KB":0,"256KB":16,"1MB":32,"2MB":48,"4MB":64,"2MB-c1":80,"4MB-c1":96,"8MB":128,"16MB":144},this.SPI_REG_BASE=1610613248,this.SPI_USR_OFFS=28,this.SPI_USR1_OFFS=32,this.SPI_USR2_OFFS=36,this.SPI_MOSI_DLEN_OFFS=0,this.SPI_MISO_DLEN_OFFS=0,this.SPI_W0_OFFS=64,this.getChipFeatures=async A=>{const t=["WiFi"];return"ESP8285"==await this.getChipDescription(A)&&t.push("Embedded Flash"),t}}async readEfuse(A,t){const e=this.EFUSE_RD_REG_BASE+4*t;return A.debug("Read efuse "+e),await A.readReg(e)}async getChipDescription(A){const t=await this.readEfuse(A,2);return!!(16&await this.readEfuse(A,0)|65536&t)?"ESP8285":"ESP8266EX"}async getCrystalFreq(A){const t=await A.readReg(this.UART_CLKDIV_REG)&this.UART_CLKDIV_MASK,e=A.transport.baudrate*t/1e6/this.XTAL_CLK_DIVIDER;let i;return i=e>33?40:26,Math.abs(i-e)>1&&A.info("WARNING: Detected crystal freq "+e+"MHz is quite different to normalized freq "+i+"MHz. Unsupported crystal in use?"),i}_d2h(A){const t=(+A).toString(16);return 1===t.length?"0"+t:t}async readMac(A){let t=await this.readEfuse(A,0);t>>>=0;let e=await this.readEfuse(A,1);e>>>=0;let i=await this.readEfuse(A,3);i>>>=0;const s=new Uint8Array(6);return 0!=i?(s[0]=i>>16&255,s[1]=i>>8&255,s[2]=255&i):e>>16&255?1==(e>>16&255)?(s[0]=172,s[1]=208,s[2]=116):A.error("Unknown OUI"):(s[0]=24,s[1]=254,s[2]=52),s[3]=e>>8&255,s[4]=255&e,s[5]=t>>24&255,this._d2h(s[0])+":"+this._d2h(s[1])+":"+this._d2h(s[2])+":"+this._d2h(s[3])+":"+this._d2h(s[4])+":"+this._d2h(s[5])}getEraseSize(A,t){return t}}});var Qs=Object.freeze({__proto__:null,ESP32P4ROM:class extends os{constructor(){super(...arguments),this.CHIP_NAME="ESP32-P4",this.IMAGE_CHIP_ID=18,this.IROM_MAP_START=1073741824,this.IROM_MAP_END=1275068416,this.DROM_MAP_START=1073741824,this.DROM_MAP_END=1275068416,this.BOOTLOADER_FLASH_OFFSET=8192,this.CHIP_DETECT_MAGIC_VALUE=[0,182303440],this.UART_DATE_REG_ADDR=1343004812,this.EFUSE_BASE=1343410176,this.EFUSE_BLOCK1_ADDR=this.EFUSE_BASE+68,this.MAC_EFUSE_REG=this.EFUSE_BASE+68,this.SPI_REG_BASE=1342754816,this.SPI_USR_OFFS=24,this.SPI_USR1_OFFS=28,this.SPI_USR2_OFFS=32,this.SPI_MOSI_DLEN_OFFS=36,this.SPI_MISO_DLEN_OFFS=40,this.SPI_W0_OFFS=88,this.EFUSE_RD_REG_BASE=this.EFUSE_BASE+48,this.EFUSE_PURPOSE_KEY0_REG=this.EFUSE_BASE+52,this.EFUSE_PURPOSE_KEY0_SHIFT=24,this.EFUSE_PURPOSE_KEY1_REG=this.EFUSE_BASE+52,this.EFUSE_PURPOSE_KEY1_SHIFT=28,this.EFUSE_PURPOSE_KEY2_REG=this.EFUSE_BASE+56,this.EFUSE_PURPOSE_KEY2_SHIFT=0,this.EFUSE_PURPOSE_KEY3_REG=this.EFUSE_BASE+56,this.EFUSE_PURPOSE_KEY3_SHIFT=4,this.EFUSE_PURPOSE_KEY4_REG=this.EFUSE_BASE+56,this.EFUSE_PURPOSE_KEY4_SHIFT=8,this.EFUSE_PURPOSE_KEY5_REG=this.EFUSE_BASE+56,this.EFUSE_PURPOSE_KEY5_SHIFT=12,this.EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT_REG=this.EFUSE_RD_REG_BASE,this.EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT=1<<20,this.EFUSE_SPI_BOOT_CRYPT_CNT_REG=this.EFUSE_BASE+52,this.EFUSE_SPI_BOOT_CRYPT_CNT_MASK=7<<18,this.EFUSE_SECURE_BOOT_EN_REG=this.EFUSE_BASE+56,this.EFUSE_SECURE_BOOT_EN_MASK=1<<20,this.PURPOSE_VAL_XTS_AES256_KEY_1=2,this.PURPOSE_VAL_XTS_AES256_KEY_2=3,this.PURPOSE_VAL_XTS_AES128_KEY=4,this.SUPPORTS_ENCRYPTED_FLASH=!0,this.FLASH_ENCRYPTED_WRITE_ALIGN=16,this.MEMORY_MAP=[[0,65536,"PADDING"],[1073741824,1275068416,"DROM"],[1341128704,1341784064,"DRAM"],[1341128704,1341784064,"BYTE_ACCESSIBLE"],[1337982976,1338114048,"DROM_MASK"],[1337982976,1338114048,"IROM_MASK"],[1073741824,1275068416,"IROM"],[1341128704,1341784064,"IRAM"],[1343258624,1343291392,"RTC_IRAM"],[1343258624,1343291392,"RTC_DRAM"],[1611653120,1611661312,"MEM_INTERNAL2"]],this.UF2_FAMILY_ID=1026592404,this.EFUSE_MAX_KEY=5,this.KEY_PURPOSES={0:"USER/EMPTY",1:"ECDSA_KEY",2:"XTS_AES_256_KEY_1",3:"XTS_AES_256_KEY_2",4:"XTS_AES_128_KEY",5:"HMAC_DOWN_ALL",6:"HMAC_DOWN_JTAG",7:"HMAC_DOWN_DIGITAL_SIGNATURE",8:"HMAC_UP",9:"SECURE_BOOT_DIGEST0",10:"SECURE_BOOT_DIGEST1",11:"SECURE_BOOT_DIGEST2",12:"KM_INIT_KEY"}}async getPkgVersion(A){const t=this.EFUSE_BLOCK1_ADDR+8;return await A.readReg(t)>>27&7}async getMinorChipVersion(A){const t=this.EFUSE_BLOCK1_ADDR+8;return 15&await A.readReg(t)}async getMajorChipVersion(A){const t=this.EFUSE_BLOCK1_ADDR+8;return await A.readReg(t)>>4&3}async getChipDescription(A){return`${0===await this.getPkgVersion(A)?"ESP32-P4":"unknown ESP32-P4"} (revision v${await this.getMajorChipVersion(A)}.${await this.getMinorChipVersion(A)})`}async getChipFeatures(A){return["High-Performance MCU"]}async getCrystalFreq(A){return 40}async getFlashVoltage(A){}async overrideVddsdio(A){A.debug("VDD_SDIO overrides are not supported for ESP32-P4")}async readMac(A){let t=await A.readReg(this.MAC_EFUSE_REG);t>>>=0;let e=await A.readReg(this.MAC_EFUSE_REG+4);e=e>>>0&65535;const i=new Uint8Array(6);return i[0]=e>>8&255,i[1]=255&e,i[2]=t>>24&255,i[3]=t>>16&255,i[4]=t>>8&255,i[5]=255&t,this._d2h(i[0])+":"+this._d2h(i[1])+":"+this._d2h(i[2])+":"+this._d2h(i[3])+":"+this._d2h(i[4])+":"+this._d2h(i[5])}async getFlashCryptConfig(A){}async getSecureBootEnabled(A){return await A.readReg(this.EFUSE_SECURE_BOOT_EN_REG)&this.EFUSE_SECURE_BOOT_EN_MASK}async getKeyBlockPurpose(A,t){if(t<0||t>this.EFUSE_MAX_KEY)return void A.debug(`Valid key block numbers must be in range 0-${this.EFUSE_MAX_KEY}`);const e=[[this.EFUSE_PURPOSE_KEY0_REG,this.EFUSE_PURPOSE_KEY0_SHIFT],[this.EFUSE_PURPOSE_KEY1_REG,this.EFUSE_PURPOSE_KEY1_SHIFT],[this.EFUSE_PURPOSE_KEY2_REG,this.EFUSE_PURPOSE_KEY2_SHIFT],[this.EFUSE_PURPOSE_KEY3_REG,this.EFUSE_PURPOSE_KEY3_SHIFT],[this.EFUSE_PURPOSE_KEY4_REG,this.EFUSE_PURPOSE_KEY4_SHIFT],[this.EFUSE_PURPOSE_KEY5_REG,this.EFUSE_PURPOSE_KEY5_SHIFT]],[i,s]=e[t];return await A.readReg(i)>>s&15}async isFlashEncryptionKeyValid(A){const t=[];for(let e=0;e<=this.EFUSE_MAX_KEY;e++){const i=await this.getKeyBlockPurpose(A,e);t.push(i)}if(void 0!==typeof t.find((A=>A===this.PURPOSE_VAL_XTS_AES128_KEY)))return!0;const e=t.find((A=>A===this.PURPOSE_VAL_XTS_AES256_KEY_1)),i=t.find((A=>A===this.PURPOSE_VAL_XTS_AES256_KEY_2));return void 0!==typeof e&&void 0!==typeof i}}});export{Se as ClassicReset,Fe as CustomReset,Pe as ESPLoader,Me as HardReset,Ue as ROM,de as Transport,Re as UsbJtagSerialReset,Te as decodeBase64Data,fe as getStubJsonByChipName,Qe as validateCustomResetStringSequence}; From 7376e8263b91ddb2c1419b7cc15761a6c0a33915 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Mon, 17 Aug 2026 23:31:48 -0500 Subject: [PATCH 06/12] fix(usb_device): handle vendor zero-copy RX + reject rx_chunk_size==0 - tud_vendor_rx_cb: the TinyUSB zero-copy RX variant (CFG_TUD_VENDOR_RX_BUFSIZE==0) delivers bytes via the callback buffer, not the FIFO. Dispatch those directly instead of discarding them and reading an empty FIFO; the FIFO variant (the esp_tinyusb default, buffer==NULL) is unchanged. - initialize(): reject rx_chunk_size==0 for the CDC/vendor functions (invalid_argument). A zero-length RX buffer spins handle_cdc_rx()'s drain loop (while (0 == 0)) and stalls vendor reads. Addresses PR #720 review (usb_device.cpp:112, :319). Co-Authored-By: Claude Opus 4.8 --- components/usb_device/include/usb_device.hpp | 8 ++++-- components/usb_device/src/usb_device.cpp | 30 +++++++++++++++++--- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/components/usb_device/include/usb_device.hpp b/components/usb_device/include/usb_device.hpp index 501d1c4043..3661613d6f 100644 --- a/components/usb_device/include/usb_device.hpp +++ b/components/usb_device/include/usb_device.hpp @@ -247,8 +247,12 @@ class UsbDevice : public BaseComponent { /// @brief Internal: drain the CDC RX FIFO and dispatch to the CDC callback. void handle_cdc_rx(); - /// @brief Internal: drain the vendor RX FIFO and dispatch to the vendor callback. - void handle_vendor_rx(); + /// @brief Internal: dispatch received vendor bytes to the vendor callback. + /// @param 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. + /// @param bufsize Number of bytes at @p buffer (0 when @p buffer is null). + void handle_vendor_rx(const uint8_t *buffer = nullptr, size_t bufsize = 0); /// @brief Internal: pointer to the BOS descriptor bytes (nullptr if none). const uint8_t *bos_descriptor() const; diff --git a/components/usb_device/src/usb_device.cpp b/components/usb_device/src/usb_device.cpp index c19bc73ae4..6c1125dd7f 100644 --- a/components/usb_device/src/usb_device.cpp +++ b/components/usb_device/src/usb_device.cpp @@ -106,10 +106,10 @@ void tud_vendor_rx_cb(uint8_t itf, uint8_t const *buffer, uint16_t bufsize) { void tud_vendor_rx_cb(uint8_t itf, uint8_t const *buffer, uint32_t bufsize) { #endif (void)itf; - (void)buffer; - (void)bufsize; + // The FIFO variant calls this with buffer==NULL, bufsize==0 (drain via + // tud_vendor_read); the zero-copy variant passes the received bytes directly. if (s_device) - s_device->handle_vendor_rx(); + s_device->handle_vendor_rx(buffer, static_cast(bufsize)); } // Vendor control-transfer callback: answer the WebUSB URL and MS OS 2.0 @@ -243,7 +243,7 @@ void UsbDevice::handle_cdc_rx() { } while (rx_size == buf.size()); } -void UsbDevice::handle_vendor_rx() { +void UsbDevice::handle_vendor_rx(const uint8_t *buffer, size_t bufsize) { #if (CFG_TUD_VENDOR > 0) receive_callback_fn cb; { @@ -252,6 +252,14 @@ void UsbDevice::handle_vendor_rx() { } if (!cb || !config_.vendor) return; + // TinyUSB zero-copy RX variant (CFG_TUD_VENDOR_RX_BUFSIZE==0): the received + // bytes are delivered directly via the callback buffer and are NOT in a FIFO, + // so dispatch them here. Otherwise (FIFO variant, the esp_tinyusb default) + // buffer is null and we drain the FIFO via tud_vendor_read(). + if (buffer != nullptr && bufsize > 0) { + cb(std::span(buffer, bufsize)); + return; + } std::vector &buf = vendor_rx_buf_; while (tud_vendor_available()) { uint32_t count = tud_vendor_read(buf.data(), buf.size()); @@ -305,6 +313,20 @@ bool UsbDevice::initialize(std::error_code &ec) { #endif } + // A zero-length RX scratch buffer would make the RX drain loops spin without + // making progress (e.g. handle_cdc_rx()'s `while (rx_size == buf.size())` + // becomes `while (0 == 0)`), so require a positive chunk size. + if (config_.cdc && config_.cdc->rx_chunk_size == 0) { + logger_.error("CDC rx_chunk_size must be > 0"); + ec = std::make_error_code(std::errc::invalid_argument); + return false; + } + if (config_.vendor && config_.vendor->rx_chunk_size == 0) { + logger_.error("Vendor rx_chunk_size must be > 0"); + ec = std::make_error_code(std::errc::invalid_argument); + return false; + } + // --- Sequentially allocate interface numbers, endpoint addresses, strings --- uint8_t next_itf = 0; uint8_t next_ep = 1; // endpoint number (1..); IN uses 0x80|n, OUT uses n From 22aa7e3e0c6870e5785a628d5601ef4ee4ac53bb Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Mon, 17 Aug 2026 23:50:39 -0500 Subject: [PATCH 07/12] =?UTF-8?q?fix(usb=5Fdevice):=20re-review=20fixes=20?= =?UTF-8?q?=E2=80=94=20atomic=20instance=20routing,=20WebUSB=20wIndex,=20F?= =?UTF-8?q?IFO=20drain,=20retry=20errc?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - s_device is now std::atomic and every TinyUSB callback loads it ONCE into a local (fixes the cross-thread data race and the check-then-use TOCTOU against teardown); initialized_ likewise atomic. - WebUSB URL control branch now requires wIndex == 2 (WEBUSB_REQUEST_GET_URL) so it cannot shadow the MS-OS-2.0 request if the two vendor codes are configured equal. - handle_cdc_rx/handle_vendor_rx drain (and discard) the RX FIFO even when no receive callback is attached, so clearing the callback at runtime can no longer back-pressure/stall the host. - write_hid_report distinguishes transient backpressure (resource_unavailable_try_again when mounted but a report is in flight) from a real disconnect (not_connected via tud_mounted()). Co-Authored-By: Claude Opus 4.8 --- components/usb_device/include/usb_device.hpp | 3 +- components/usb_device/src/usb_device.cpp | 69 ++++++++++++++------ 2 files changed, 50 insertions(+), 22 deletions(-) diff --git a/components/usb_device/include/usb_device.hpp b/components/usb_device/include/usb_device.hpp index 3661613d6f..e25c49f5b3 100644 --- a/components/usb_device/include/usb_device.hpp +++ b/components/usb_device/include/usb_device.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -279,7 +280,7 @@ class UsbDevice : public BaseComponent { std::unique_ptr impl_; Config config_; - bool initialized_{false}; + std::atomic initialized_{false}; // read from the TinyUSB task via the write paths std::mutex cb_mutex_; receive_callback_fn on_cdc_receive_; diff --git a/components/usb_device/src/usb_device.cpp b/components/usb_device/src/usb_device.cpp index 6c1125dd7f..750ab0f018 100644 --- a/components/usb_device/src/usb_device.cpp +++ b/components/usb_device/src/usb_device.cpp @@ -1,6 +1,7 @@ #include "usb_device.hpp" #include +#include #include #include "tinyusb.h" @@ -12,8 +13,13 @@ namespace { // Only a single USB device exists on the chip; the BOS descriptor and the vendor // RX / control-request callbacks are global (no user pointer), so we route them -// through a file-scope pointer to the active instance. -espp::UsbDevice *s_device = nullptr; +// through a file-scope pointer to the active instance. It is atomic because the +// TinyUSB task reads it concurrently with initialize()/~UsbDevice() writes on +// the caller's thread; each callback loads it ONCE into a local. Teardown +// safety additionally relies on clearing it BEFORE tinyusb_driver_uninstall() +// (which quiesces the TinyUSB task) so no callback can begin using a +// destructing instance. +std::atomic s_device{nullptr}; // The CDC port this component uses. A single dedicated CDC-ACM interface. constexpr tinyusb_cdcacm_itf_t kCdcPort = TINYUSB_CDC_ACM_0; @@ -85,8 +91,10 @@ static void cdc_rx_trampoline(int itf, cdcacm_event_t *event) { (void)event; if (itf != (int)kCdcPort) return; - if (s_device) - s_device->handle_cdc_rx(); + // load once: the pointer must not be re-read between check and use + auto *dev = s_device.load(); + if (dev) + dev->handle_cdc_rx(); } extern "C" { @@ -94,7 +102,8 @@ extern "C" { // BOS descriptor (weak in TinyUSB core). Returns our WebUSB/MS-OS BOS when the // vendor+WebUSB function is enabled, otherwise NULL (no BOS). uint8_t const *tud_descriptor_bos_cb(void) { - return s_device ? s_device->bos_descriptor() : nullptr; + auto *dev = s_device.load(); + return dev ? dev->bos_descriptor() : nullptr; } #if (CFG_TUD_VENDOR > 0) @@ -108,8 +117,9 @@ void tud_vendor_rx_cb(uint8_t itf, uint8_t const *buffer, uint32_t bufsize) { (void)itf; // The FIFO variant calls this with buffer==NULL, bufsize==0 (drain via // tud_vendor_read); the zero-copy variant passes the received bytes directly. - if (s_device) - s_device->handle_vendor_rx(buffer, static_cast(bufsize)); + auto *dev = s_device.load(); + if (dev) + dev->handle_vendor_rx(buffer, static_cast(bufsize)); } // Vendor control-transfer callback: answer the WebUSB URL and MS OS 2.0 @@ -118,16 +128,20 @@ bool tud_vendor_control_xfer_cb(uint8_t rhport, uint8_t stage, tusb_control_request_t const *request) { if (stage != CONTROL_STAGE_SETUP) return true; // nothing to do on DATA / ACK stages - if (!s_device || !s_device->vendor_config().has_value()) + auto *dev = s_device.load(); + if (!dev || !dev->vendor_config().has_value()) return false; - const auto &vendor = *s_device->vendor_config(); + const auto &vendor = *dev->vendor_config(); switch (request->bmRequestType_bit.type) { case TUSB_REQ_TYPE_VENDOR: - if (request->bRequest == vendor.webusb_vendor_code) { + // wIndex 2 == WEBUSB_REQUEST_GET_URL; qualifying on it keeps this branch + // from shadowing the MS-OS request if the two vendor codes are configured + // to the same value. + if (request->bRequest == vendor.webusb_vendor_code && request->wIndex == 2) { // Return the WebUSB landing-page URL descriptor. uint8_t len = 0; - const uint8_t *url = s_device->webusb_url_descriptor(len); + const uint8_t *url = dev->webusb_url_descriptor(len); if (!url) return false; return tud_control_xfer(rhport, request, (void *)(uintptr_t)url, len); @@ -135,7 +149,7 @@ bool tud_vendor_control_xfer_cb(uint8_t rhport, uint8_t stage, if (request->bRequest == vendor.ms_os_vendor_code && request->wIndex == 7) { // Return the MS OS 2.0 descriptor set. uint16_t total_len = 0; - const uint8_t *ms = s_device->ms_os_20_descriptor(total_len); + const uint8_t *ms = dev->ms_os_20_descriptor(total_len); if (!ms) return false; return tud_control_xfer(rhport, request, (void *)(uintptr_t)ms, total_len); @@ -163,7 +177,8 @@ bool tud_vendor_control_xfer_cb(uint8_t rhport, uint8_t stage, // HID: return the application-supplied report descriptor for the given instance. uint8_t const *tud_hid_descriptor_report_cb(uint8_t instance) { (void)instance; - return s_device ? s_device->hid_report_descriptor() : nullptr; + auto *dev = s_device.load(); + return dev ? dev->hid_report_descriptor() : nullptr; } // HID GET_REPORT control request: this device is input-only, so nothing to do. @@ -227,8 +242,10 @@ void UsbDevice::handle_cdc_rx() { std::scoped_lock lk(cb_mutex_); cb = on_cdc_receive_; } - if (!cb || !config_.cdc) + if (!config_.cdc) return; + // NOTE: even with no callback attached we still drain (and discard) the FIFO + // below; leaving bytes in it would back-pressure/stall the host. std::vector &buf = cdc_rx_buf_; size_t rx_size = 0; do { @@ -238,7 +255,7 @@ void UsbDevice::handle_cdc_rx() { logger_.error("CDC read error: {}", esp_err_to_name(err)); break; } - if (rx_size > 0) + if (rx_size > 0 && cb) cb(std::span(buf.data(), rx_size)); } while (rx_size == buf.size()); } @@ -250,14 +267,17 @@ void UsbDevice::handle_vendor_rx(const uint8_t *buffer, size_t bufsize) { std::scoped_lock lk(cb_mutex_); cb = on_vendor_receive_; } - if (!cb || !config_.vendor) + if (!config_.vendor) return; // TinyUSB zero-copy RX variant (CFG_TUD_VENDOR_RX_BUFSIZE==0): the received // bytes are delivered directly via the callback buffer and are NOT in a FIFO, // so dispatch them here. Otherwise (FIFO variant, the esp_tinyusb default) - // buffer is null and we drain the FIFO via tud_vendor_read(). + // buffer is null and we drain the FIFO via tud_vendor_read(). With no + // callback attached, bytes are still consumed (discarded) so the FIFO cannot + // fill up and stall the host. if (buffer != nullptr && bufsize > 0) { - cb(std::span(buffer, bufsize)); + if (cb) + cb(std::span(buffer, bufsize)); return; } std::vector &buf = vendor_rx_buf_; @@ -265,7 +285,8 @@ void UsbDevice::handle_vendor_rx(const uint8_t *buffer, size_t bufsize) { uint32_t count = tud_vendor_read(buf.data(), buf.size()); if (count == 0) break; - cb(std::span(buf.data(), count)); + if (cb) + cb(std::span(buf.data(), count)); } #endif } @@ -819,11 +840,17 @@ bool UsbDevice::write_hid_report(uint8_t report_id, std::span rep ec = std::make_error_code(std::errc::not_connected); return false; } - if (!tud_hid_ready()) { - // Not mounted yet, or a previous report is still in flight. + if (!tud_mounted()) { + // Device not mounted (host not connected / not configured). ec = std::make_error_code(std::errc::not_connected); return false; } + if (!tud_hid_ready()) { + // Mounted but a previous report is still in flight -- transient + // backpressure, distinct from a disconnect so callers can retry. + ec = std::make_error_code(std::errc::resource_unavailable_try_again); + return false; + } if (!tud_hid_report(report_id, report.data(), static_cast(report.size()))) { logger_.warn_rate_limited("HID report send failed (report_id={})", report_id); ec = std::make_error_code(std::errc::io_error); From fe1235c3534593f287e9bf9212970ef48989b1d6 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Tue, 18 Aug 2026 10:03:40 -0500 Subject: [PATCH 08/12] fix(usb_device): distinct FS/HS config descriptors + strict flash-offset validation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - On high-speed builds (TUD_OPT_HIGH_SPEED, e.g. ESP32-P4) the single config descriptor with 512-byte bulk endpoints was installed for BOTH speeds, making the full-speed configuration invalid, and no device qualifier was provided. Build a 64-byte-bulk FS descriptor and a 512-byte-bulk HS descriptor, encode the HID bInterval as 2^(n-1) x 125us microframes for HS (choosing the largest exponent not slower than the requested ms), and install a device qualifier derived from the device descriptor. FS-only targets (S3/S2) are unchanged. - board_console: validate the WHOLE flash-offset string as hex before parseInt ("0x10000oops" no longer flashes at 0x10000) — flashing is destructive, so trailing garbage is now rejected with a clear error. Addresses PR #720 review (usb_device.cpp:730, board_console.html:668). esp32s3 example builds. Co-Authored-By: Claude Opus 4.8 --- components/usb_device/src/usb_device.cpp | 123 ++++++++++++------- components/usb_device/web/board_console.html | 8 +- 2 files changed, 87 insertions(+), 44 deletions(-) diff --git a/components/usb_device/src/usb_device.cpp b/components/usb_device/src/usb_device.cpp index 750ab0f018..a585364219 100644 --- a/components/usb_device/src/usb_device.cpp +++ b/components/usb_device/src/usb_device.cpp @@ -43,11 +43,13 @@ namespace espp { // of the driver. These must outlive tinyusb_driver_install(). struct UsbDevice::Impl { tusb_desc_device_t device_desc{}; - std::vector config_desc; - std::vector bos_desc; // BOS (WebUSB + MS OS 2.0), empty if unused - std::vector ms_os_20_desc; // MS OS 2.0 descriptor set, empty if unused - std::vector webusb_url_desc; // WebUSB URL descriptor, empty if unused - std::vector hid_report_desc; // HID report descriptor bytes, empty if unused + std::vector config_desc; // full-speed configuration (64-byte bulk) + std::vector hs_config_desc; // high-speed configuration (512-byte bulk), HS builds only + tusb_desc_device_qualifier_t qualifier_desc{}; // device qualifier, HS builds only + std::vector bos_desc; // BOS (WebUSB + MS OS 2.0), empty if unused + std::vector ms_os_20_desc; // MS OS 2.0 descriptor set, empty if unused + std::vector webusb_url_desc; // WebUSB URL descriptor, empty if unused + std::vector hid_report_desc; // HID report descriptor bytes, empty if unused // Owning strings + the pointer table TinyUSB reads (index 0 is the LANGID). std::array langid{{0x09, 0x04}}; @@ -352,7 +354,6 @@ bool UsbDevice::initialize(std::error_code &ec) { uint8_t next_itf = 0; uint8_t next_ep = 1; // endpoint number (1..); IN uses 0x80|n, OUT uses n uint8_t in_used = 0, out_used = 0; - const int ep_size = (TUD_OPT_HIGH_SPEED ? 512 : 64); // Preallocate the RX scratch buffers now so the TinyUSB-task RX handlers never // allocate on the hot path. @@ -470,49 +471,84 @@ bool UsbDevice::initialize(std::error_code &ec) { total_len + (config_.hid->has_out_endpoint ? TUD_HID_INOUT_DESC_LEN : TUD_HID_DESC_LEN)); } - impl_->config_desc.clear(); - auto append = [&](const uint8_t *p, size_t n) { - impl_->config_desc.insert(impl_->config_desc.end(), p, p + n); - }; - { - const uint8_t hdr[] = { - // config number, interface count, string index, total length, attribute, power (mA) - TUD_CONFIG_DESCRIPTOR(1, itf_count, 0, total_len, TUSB_DESC_CONFIG_ATT_REMOTE_WAKEUP, 100), - }; - append(hdr, sizeof(hdr)); - } - if (config_.cdc) { - const uint8_t d[] = { - TUD_CDC_DESCRIPTOR(cdc_itf, cdc_str, cdc_notif, 8, cdc_out, cdc_in, ep_size), - }; - append(d, sizeof(d)); - } - if (config_.vendor) { - const uint8_t d[] = { - TUD_VENDOR_DESCRIPTOR(vendor_itf, vendor_str, vendor_out, vendor_in, ep_size), - }; - append(d, sizeof(d)); - } - if (config_.hid) { - const uint16_t report_len = static_cast(impl_->hid_report_desc.size()); - const uint8_t poll = config_.hid->poll_interval_ms; - // Interrupt endpoints are full-speed (<=64 byte packets) even on high-speed - // parts; a 64-byte endpoint buffer comfortably fits the gamepad report. - constexpr uint8_t kHidEpSize = 64; - if (config_.hid->has_out_endpoint) { + // Build one configuration descriptor for a given bus speed. Bulk endpoints + // are 64 bytes at full speed and 512 at high speed; the HID interrupt + // bInterval is in 1-ms frames at FS but exponent-encoded (2^(n-1) x 125 us + // microframes) at HS. On HS-capable parts (e.g. ESP32-P4) BOTH descriptors + // are installed so the device is valid whichever speed the host negotiates. + auto build_config_desc = [&](std::vector &desc, int bulk_ep_size, + uint8_t hid_binterval) { + desc.clear(); + auto append = [&](const uint8_t *p, size_t n) { desc.insert(desc.end(), p, p + n); }; + { + const uint8_t hdr[] = { + // config number, interface count, string index, total length, attribute, power (mA) + TUD_CONFIG_DESCRIPTOR(1, itf_count, 0, total_len, TUSB_DESC_CONFIG_ATT_REMOTE_WAKEUP, + 100), + }; + append(hdr, sizeof(hdr)); + } + if (config_.cdc) { const uint8_t d[] = { - TUD_HID_INOUT_DESCRIPTOR(hid_itf, hid_str, HID_ITF_PROTOCOL_NONE, report_len, hid_out, - hid_in, kHidEpSize, poll), + TUD_CDC_DESCRIPTOR(cdc_itf, cdc_str, cdc_notif, 8, cdc_out, cdc_in, bulk_ep_size), }; append(d, sizeof(d)); - } else { + } + if (config_.vendor) { const uint8_t d[] = { - TUD_HID_DESCRIPTOR(hid_itf, hid_str, HID_ITF_PROTOCOL_NONE, report_len, hid_in, - kHidEpSize, poll), + TUD_VENDOR_DESCRIPTOR(vendor_itf, vendor_str, vendor_out, vendor_in, bulk_ep_size), }; append(d, sizeof(d)); } - } + if (config_.hid) { + const uint16_t report_len = static_cast(impl_->hid_report_desc.size()); + // Interrupt endpoints are <=64 byte packets at either speed; a 64-byte + // endpoint buffer comfortably fits the gamepad report. + constexpr uint8_t kHidEpSize = 64; + if (config_.hid->has_out_endpoint) { + const uint8_t d[] = { + TUD_HID_INOUT_DESCRIPTOR(hid_itf, hid_str, HID_ITF_PROTOCOL_NONE, report_len, hid_out, + hid_in, kHidEpSize, hid_binterval), + }; + append(d, sizeof(d)); + } else { + const uint8_t d[] = { + TUD_HID_DESCRIPTOR(hid_itf, hid_str, HID_ITF_PROTOCOL_NONE, report_len, hid_in, + kHidEpSize, hid_binterval), + }; + append(d, sizeof(d)); + } + } + }; + + const uint8_t hid_poll_ms = config_.hid ? config_.hid->poll_interval_ms : 0; + // Full-speed configuration: 64-byte bulk endpoints, bInterval in ms frames. + build_config_desc(impl_->config_desc, 64, hid_poll_ms); +#if (TUD_OPT_HIGH_SPEED) + // High-speed configuration: 512-byte bulk endpoints; HID bInterval is the + // exponent n in 2^(n-1) x 125 us microframes. Choose the largest n whose + // period does not exceed the requested ms (i.e. poll at least as often). + uint8_t hs_hid_binterval = 1; + { + const uint32_t microframes = static_cast(hid_poll_ms) * 8; // 125 us units + while (hs_hid_binterval < 16 && (1u << hs_hid_binterval) <= microframes) + ++hs_hid_binterval; // exits with 2^(n-1) <= microframes < 2^n + } + build_config_desc(impl_->hs_config_desc, 512, hs_hid_binterval); + // Device qualifier: required for a high-speed-capable device so the host can + // query the other-speed characteristics. + impl_->qualifier_desc = { + .bLength = sizeof(tusb_desc_device_qualifier_t), + .bDescriptorType = TUSB_DESC_DEVICE_QUALIFIER, + .bcdUSB = impl_->device_desc.bcdUSB, + .bDeviceClass = impl_->device_desc.bDeviceClass, + .bDeviceSubClass = impl_->device_desc.bDeviceSubClass, + .bDeviceProtocol = impl_->device_desc.bDeviceProtocol, + .bMaxPacketSize0 = impl_->device_desc.bMaxPacketSize0, + .bNumConfigurations = 1, + .bReserved = 0, + }; +#endif // --- WebUSB / MS OS 2.0 descriptors (only when the vendor+WebUSB is enabled) --- if (webusb) { @@ -727,7 +763,8 @@ bool UsbDevice::initialize(std::error_code &ec) { tusb_cfg.descriptor.string_count = static_cast(impl_->strings.size()); tusb_cfg.descriptor.full_speed_config = impl_->config_desc.data(); #if (TUD_OPT_HIGH_SPEED) - tusb_cfg.descriptor.high_speed_config = impl_->config_desc.data(); + tusb_cfg.descriptor.high_speed_config = impl_->hs_config_desc.data(); + tusb_cfg.descriptor.qualifier = &impl_->qualifier_desc; #endif // Register before installing so the BOS / vendor callbacks can find us. diff --git a/components/usb_device/web/board_console.html b/components/usb_device/web/board_console.html index 57c1aa06ff..1d58b8a4a7 100644 --- a/components/usb_device/web/board_console.html +++ b/components/usb_device/web/board_console.html @@ -664,7 +664,13 @@

ESP Flasher const offEl = row.querySelector(".offset-in"); const file = fileEl.files && fileEl.files[0]; if (!file) continue; - const addr = parseInt(offEl.value.trim(), 16); + // Flashing is destructive: validate the WHOLE offset string as hex + // (optionally 0x-prefixed) rather than parseInt alone, which accepts a + // valid prefix and ignores trailing garbage ("0x10000oops" -> 0x10000). + const offStr = offEl.value.trim(); + if (!/^(0[xX])?[0-9a-fA-F]+$/.test(offStr)) + throw new Error(`Invalid offset "${offEl.value}" for ${file.name} (expected hex, e.g. 0x10000)`); + const addr = parseInt(offStr, 16); if (!Number.isFinite(addr) || addr < 0) throw new Error(`Invalid offset "${offEl.value}" for ${file.name}`); const data = await readFileAsBinaryString(file); out.push({ data, address: addr, name: file.name, row }); From d745000035743f3a899fcb47523183afcddc0fec Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Tue, 18 Aug 2026 10:37:53 -0500 Subject: [PATCH 09/12] fix(usb_device): reject empty HID report descriptor + 32-bit flash-offset bound - initialize() now fails with invalid_argument when the HID function is enabled with an empty report_descriptor: proceeding emitted a HID interface with wDescriptorLength == 0 and a null report callback -- an invalid interface that reported success (PR review). - board_console: flash offsets are additionally bounded to the 32-bit address range after the strict-hex validation. Co-Authored-By: Claude Opus 4.8 --- components/usb_device/src/usb_device.cpp | 10 ++++++++++ components/usb_device/web/board_console.html | 3 ++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/components/usb_device/src/usb_device.cpp b/components/usb_device/src/usb_device.cpp index a585364219..e7f4fad672 100644 --- a/components/usb_device/src/usb_device.cpp +++ b/components/usb_device/src/usb_device.cpp @@ -334,6 +334,16 @@ bool UsbDevice::initialize(std::error_code &ec) { ec = std::make_error_code(std::errc::function_not_supported); return false; #endif + if (config_.hid->report_descriptor.empty()) { + // A default-constructed HidFunction has no report descriptor; proceeding + // would emit a HID interface with wDescriptorLength == 0 (and a null + // report callback) -- an invalid HID interface that "succeeds" here and + // then confuses the host. Reject it as an invalid configuration. + logger_.error("HID function enabled but report_descriptor is empty; supply the HID " + "report descriptor bytes (e.g. built with the hid-rp component)."); + ec = std::make_error_code(std::errc::invalid_argument); + return false; + } } // A zero-length RX scratch buffer would make the RX drain loops spin without diff --git a/components/usb_device/web/board_console.html b/components/usb_device/web/board_console.html index 1d58b8a4a7..5e38d4701d 100644 --- a/components/usb_device/web/board_console.html +++ b/components/usb_device/web/board_console.html @@ -671,7 +671,8 @@

ESP Flasher if (!/^(0[xX])?[0-9a-fA-F]+$/.test(offStr)) throw new Error(`Invalid offset "${offEl.value}" for ${file.name} (expected hex, e.g. 0x10000)`); const addr = parseInt(offStr, 16); - if (!Number.isFinite(addr) || addr < 0) throw new Error(`Invalid offset "${offEl.value}" for ${file.name}`); + if (!Number.isFinite(addr) || addr < 0 || addr > 0xFFFFFFFF) + throw new Error(`Offset "${offEl.value}" for ${file.name} is outside the 32-bit flash address range`); const data = await readFileAsBinaryString(file); out.push({ data, address: addr, name: file.name, row }); setRowProgress(row, 0); From ff18fa77664fb8f7d1e20d377fcc86e3576c8d57 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Tue, 18 Aug 2026 12:00:16 -0500 Subject: [PATCH 10/12] fix(usb_device): clear cppcheck unreachableCode in the HID validation The empty-report-descriptor check followed the CFG_TUD_HID==0 early return in the same block, which is unreachable when that preprocessor branch is active. Move it into the #else so each configuration contains only its own path. No behavior change; esp32s3 example builds. Co-Authored-By: Claude Opus 4.8 --- components/usb_device/src/usb_device.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/components/usb_device/src/usb_device.cpp b/components/usb_device/src/usb_device.cpp index e7f4fad672..c43171eb7d 100644 --- a/components/usb_device/src/usb_device.cpp +++ b/components/usb_device/src/usb_device.cpp @@ -333,7 +333,7 @@ bool UsbDevice::initialize(std::error_code &ec) { "CONFIG_TINYUSB_HID_COUNT>0 in sdkconfig."); ec = std::make_error_code(std::errc::function_not_supported); return false; -#endif +#else if (config_.hid->report_descriptor.empty()) { // A default-constructed HidFunction has no report descriptor; proceeding // would emit a HID interface with wDescriptorLength == 0 (and a null @@ -344,6 +344,7 @@ bool UsbDevice::initialize(std::error_code &ec) { ec = std::make_error_code(std::errc::invalid_argument); return false; } +#endif } // A zero-length RX scratch buffer would make the RX drain loops spin without From 72415a5970446dd97d38bfba295bc3f922780557 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Tue, 18 Aug 2026 12:35:32 -0500 Subject: [PATCH 11/12] fix(usb_device): atomically claim/release the singleton slot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit initialize() claimed s_device with a check-then-set: two threads (or two instances) could both observe nullptr and both proceed to install the TinyUSB driver. The registration is now a compare_exchange_strong just before tinyusb_driver_install() — exactly one initialize() wins, the loser backs out with device_or_resource_busy; the early null check remains as a documented fast-fail only. The destructor releases the slot with a matching compare_exchange (clears only if we still own it). PR #720 review. Co-Authored-By: Claude Opus 4.8 --- components/usb_device/src/usb_device.cpp | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/components/usb_device/src/usb_device.cpp b/components/usb_device/src/usb_device.cpp index c43171eb7d..06726c3e21 100644 --- a/components/usb_device/src/usb_device.cpp +++ b/components/usb_device/src/usb_device.cpp @@ -74,9 +74,11 @@ UsbDevice::~UsbDevice() { if (initialized_) { // Detach the global callback routing BEFORE tearing down the driver so a // TinyUSB callback that fires during deinit cannot dereference this - // destructing instance (use-after-free). - if (s_device == this) - s_device = nullptr; + // destructing instance (use-after-free). Atomic compare_exchange so the + // clear only happens if we still own the slot (mirrors the claim in + // initialize()). + UsbDevice *expected = this; + s_device.compare_exchange_strong(expected, nullptr); if (config_.cdc) tinyusb_cdcacm_deinit(kCdcPort); tinyusb_driver_uninstall(); @@ -303,6 +305,9 @@ bool UsbDevice::initialize(std::error_code &ec) { logger_.warn("Already initialized"); return true; } + // Fast-fail when another instance is already active. This check alone is + // check-then-act racy; the AUTHORITATIVE claim is the compare_exchange just + // before tinyusb_driver_install() below. if (s_device != nullptr) { logger_.error("Another UsbDevice/UsbCdc instance is already active"); ec = std::make_error_code(std::errc::device_or_resource_busy); @@ -779,7 +784,16 @@ bool UsbDevice::initialize(std::error_code &ec) { #endif // Register before installing so the BOS / vendor callbacks can find us. - s_device = this; + // Claim the singleton slot ATOMICALLY: the null check at the top of + // initialize() is only a fast-fail, so two threads (or two instances) that + // both passed it must be arbitrated here — exactly one compare_exchange + // wins and installs the driver; the loser backs out with "busy". + UsbDevice *expected = nullptr; + if (!s_device.compare_exchange_strong(expected, this)) { + logger_.error("Another UsbDevice/UsbCdc instance is already active"); + ec = std::make_error_code(std::errc::device_or_resource_busy); + return false; + } esp_err_t err = tinyusb_driver_install(&tusb_cfg); if (err != ESP_OK) { From 5b2a454ccb9ada95ffb3e888560bd8b5ed47182d Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Tue, 18 Aug 2026 13:30:55 -0500 Subject: [PATCH 12/12] docs(usb_device): annotate TinyUSB's (OUT, IN) argument order at the HID descriptor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The call is correct — TUD_HID_INOUT_DESCRIPTOR's signature is (..., _epout, _epin, ...) with OUT before IN, and hid_out/hid_in carry the right direction bits — but the ordering reads backwards without checking usbd.h, and a reviewer suggested 'fixing' it. Document it at the call site. Co-Authored-By: Claude Opus 4.8 --- components/usb_device/src/usb_device.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/components/usb_device/src/usb_device.cpp b/components/usb_device/src/usb_device.cpp index 06726c3e21..8db5f389e9 100644 --- a/components/usb_device/src/usb_device.cpp +++ b/components/usb_device/src/usb_device.cpp @@ -522,6 +522,9 @@ bool UsbDevice::initialize(std::error_code &ec) { // endpoint buffer comfortably fits the gamepad report. constexpr uint8_t kHidEpSize = 64; if (config_.hid->has_out_endpoint) { + // NOTE: TinyUSB's parameter order here is (..., _epout, _epin, ...) -- + // OUT before IN (see TUD_HID_INOUT_DESCRIPTOR in usbd.h). hid_out is + // the plain endpoint number and hid_in carries the 0x80 direction bit. const uint8_t d[] = { TUD_HID_INOUT_DESCRIPTOR(hid_itf, hid_str, HID_ITF_PROTOCOL_NONE, report_len, hid_out, hid_in, kHidEpSize, hid_binterval),