From b0698fce4a7dc7184879e02154c470024d744cca Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Tue, 15 Sep 2026 09:39:57 -0500 Subject: [PATCH 1/4] feat(usb_device): configurable bcdDevice, bMaxPower and remote-wakeup attribute UsbDevice::Config gains bcd_device (default 0x0100, the previous hard-coded value), max_power_ma (default 100 mA, as before; clamped to 500) and remote_wakeup (default true, as before). Some hosts compare these against the device they expect: the Nintendo Switch is happier with a Pro Controller that reports bcdDevice 2.10 and 500 mA, which the esp-usb-ble-hid dongle could not express until now. An XInput-only device keeps reporting the Xbox 360 bcdDevice. The configuration descriptor's attribute and power bytes are patched in after TUD_CONFIG_DESCRIPTOR since the macro's arithmetic on a runtime value is a narrowing conversion inside the braced initializer. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01DFjjnq3XCTRAXENSxsCxJU --- components/usb_device/include/usb_device.hpp | 7 +++++++ components/usb_device/src/usb_device.cpp | 10 ++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/components/usb_device/include/usb_device.hpp b/components/usb_device/include/usb_device.hpp index 107401d55..0bfa29207 100644 --- a/components/usb_device/include/usb_device.hpp +++ b/components/usb_device/include/usb_device.hpp @@ -229,6 +229,13 @@ class UsbDevice : public BaseComponent { 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. */ + uint16_t bcd_device{ + 0x0100}; /**< bcdDevice (device release, BCD) in the device descriptor. Ignored + for an XInput-only device (which reports the Xbox 360 value). */ + uint16_t max_power_ma{100}; /**< bMaxPower in the configuration descriptor, in mA (0..500). Some + hosts compare it against the device they expect (e.g. a Switch + expects a Pro Controller's 500 mA). */ + bool remote_wakeup{true}; /**< Advertise remote wakeup in the configuration attributes. */ std::optional cdc{}; /**< Enable a CDC-ACM function. */ std::optional vendor{}; /**< Enable a vendor-specific / WebUSB function. */ diff --git a/components/usb_device/src/usb_device.cpp b/components/usb_device/src/usb_device.cpp index f8001377e..50409cab2 100644 --- a/components/usb_device/src/usb_device.cpp +++ b/components/usb_device/src/usb_device.cpp @@ -1,5 +1,6 @@ #include "usb_device.hpp" +#include #include #include #include @@ -888,7 +889,7 @@ bool UsbDevice::initialize(std::error_code &ec) { impl_->device_desc.bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE; impl_->device_desc.idVendor = xinput_only ? config_.xinput->vid : config_.vid; impl_->device_desc.idProduct = xinput_only ? config_.xinput->pid : config_.pid; - impl_->device_desc.bcdDevice = xinput_only ? espp::xinput::kDefaultBcdDevice : 0x0100; + impl_->device_desc.bcdDevice = xinput_only ? espp::xinput::kDefaultBcdDevice : config_.bcd_device; impl_->device_desc.iManufacturer = 0x01; impl_->device_desc.iProduct = 0x02; impl_->device_desc.iSerialNumber = 0x03; @@ -927,11 +928,16 @@ bool UsbDevice::initialize(std::error_code &ec) { desc.clear(); auto append = [&](const uint8_t *p, size_t n) { desc.insert(desc.end(), p, p + n); }; { - const uint8_t hdr[] = { + 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), }; + // bmAttributes (byte 7) and bMaxPower (byte 8, 2 mA units) come from the + // config; patched in after the macro since its arithmetic is not constant + hdr[7] = static_cast( + TU_BIT(7) | (config_.remote_wakeup ? TUSB_DESC_CONFIG_ATT_REMOTE_WAKEUP : 0)); + hdr[8] = static_cast(std::min(config_.max_power_ma, 500) / 2); append(hdr, sizeof(hdr)); } #if (CFG_TUD_CDC > 0) From 908a1e67aede3c75974cb12c11d25381fc10d6f4 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Tue, 15 Sep 2026 10:06:10 -0500 Subject: [PATCH 2/4] fix(usb_device): write the configuration descriptor header explicitly; round bMaxPower up The TUD_CONFIG_DESCRIPTOR macro cannot be expanded by cppcheck (it saw a 1-byte array patched at [7]/[8]) and hid which arguments were real: the 9-byte header is now spelled out field by field. bMaxPower (2 mA units) is clamped to 500 mA and rounded up so an odd request is never under-reported. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01DFjjnq3XCTRAXENSxsCxJU --- components/usb_device/src/usb_device.cpp | 27 ++++++++++++++++-------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/components/usb_device/src/usb_device.cpp b/components/usb_device/src/usb_device.cpp index 50409cab2..84c211b4e 100644 --- a/components/usb_device/src/usb_device.cpp +++ b/components/usb_device/src/usb_device.cpp @@ -928,16 +928,25 @@ bool UsbDevice::initialize(std::error_code &ec) { desc.clear(); auto append = [&](const uint8_t *p, size_t n) { desc.insert(desc.end(), p, p + n); }; { - 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), - }; - // bmAttributes (byte 7) and bMaxPower (byte 8, 2 mA units) come from the - // config; patched in after the macro since its arithmetic is not constant - hdr[7] = static_cast( + // The 9-byte configuration descriptor header, written out explicitly + // (TUD_CONFIG_DESCRIPTOR's arithmetic on runtime values is a narrowing + // conversion inside a braced initializer). bMaxPower is in 2 mA units: + // clamp to the USB 2.0 maximum (500 mA) and round UP so an odd request is + // never under-reported (1 mA -> 2 mA). + const uint16_t power_ma = std::min(config_.max_power_ma, 500); + const uint8_t attributes = static_cast( TU_BIT(7) | (config_.remote_wakeup ? TUSB_DESC_CONFIG_ATT_REMOTE_WAKEUP : 0)); - hdr[8] = static_cast(std::min(config_.max_power_ma, 500) / 2); + const uint8_t hdr[9] = { + 9, // bLength + static_cast(TUSB_DESC_CONFIGURATION), // bDescriptorType + static_cast(total_len & 0xFF), // wTotalLength (LE) + static_cast(total_len >> 8), + static_cast(itf_count), // bNumInterfaces + 1, // bConfigurationValue + 0, // iConfiguration + attributes, // bmAttributes + static_cast((power_ma + 1) / 2), // bMaxPower (2 mA units) + }; append(hdr, sizeof(hdr)); } #if (CFG_TUD_CDC > 0) From 63c7839da402ff47e6e5de2f60d08f6b9a1650be Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Tue, 15 Sep 2026 11:51:38 -0500 Subject: [PATCH 3/4] fix(usb_device): append the descriptor fields after log_level; document them bcd_device / max_power_ma / remote_wakeup now come last in UsbDevice::Config so positional aggregate initializers written against earlier releases keep their member positions. The README and doc/en/buses/usb_cdc.rst feature lists and usage snippets describe the three fields, the 500 mA clamp and the 2 mA round-up. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01DFjjnq3XCTRAXENSxsCxJU --- components/usb_device/README.md | 9 +++++- components/usb_device/include/usb_device.hpp | 29 +++++++++++--------- doc/en/buses/usb_cdc.rst | 10 ++++++- 3 files changed, 33 insertions(+), 15 deletions(-) diff --git a/components/usb_device/README.md b/components/usb_device/README.md index a6d44ae51..42c4dbacb 100644 --- a/components/usb_device/README.md +++ b/components/usb_device/README.md @@ -70,7 +70,9 @@ for back-compatibility. - **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. + strings, plus the descriptor details some hosts check: `bcd_device` + (device release), `max_power_ma` (bMaxPower, clamped to 500 mA and rounded up + to the next 2 mA unit) and `remote_wakeup`. - **Idiomatic espp**: no exceptions; `initialize()` reports failures via `std::error_code`. - **Safe marshaling**: the TinyUSB RX callbacks (TinyUSB task context) are drained @@ -85,6 +87,11 @@ Composite CDC + vendor/WebUSB device (both interfaces carry the same raw stream) espp::UsbDevice::Config cfg; cfg.vid = 0x1209; // pid.codes VID (ODrive uses this) cfg.pid = 0x0d32; // ODrive-like PID +// optional descriptor details (defaults: 0x0100, 100 mA, remote wakeup on); +// e.g. a Nintendo Switch expects a Pro Controller to report 0x0210 and 500 mA +cfg.bcd_device = 0x0100; +cfg.max_power_ma = 100; // clamped to 500, rounded up to a 2 mA unit +cfg.remote_wakeup = true; espp::UsbDevice::CdcFunction cdc; cdc.on_receive = [&](std::span data) { /* serial rx */ }; diff --git a/components/usb_device/include/usb_device.hpp b/components/usb_device/include/usb_device.hpp index 0bfa29207..374c2fb51 100644 --- a/components/usb_device/include/usb_device.hpp +++ b/components/usb_device/include/usb_device.hpp @@ -229,21 +229,24 @@ class UsbDevice : public BaseComponent { 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. */ - uint16_t bcd_device{ - 0x0100}; /**< bcdDevice (device release, BCD) in the device descriptor. Ignored - for an XInput-only device (which reports the Xbox 360 value). */ - uint16_t max_power_ma{100}; /**< bMaxPower in the configuration descriptor, in mA (0..500). Some - hosts compare it against the device they expect (e.g. a Switch - expects a Pro Controller's 500 mA). */ - bool remote_wakeup{true}; /**< Advertise remote wakeup in the configuration attributes. */ - - std::optional cdc{}; /**< Enable a CDC-ACM function. */ - std::optional vendor{}; /**< Enable a vendor-specific / WebUSB function. */ - std::optional hid{}; /**< Enable a HID function. */ - std::optional xinput{}; /**< Enable an X-Input (Xbox 360) function. */ - std::optional msc{}; /**< (Future) enable an MSC function. */ + std::optional cdc{}; /**< Enable a CDC-ACM function. */ + std::optional vendor{}; /**< Enable a vendor-specific / WebUSB function. */ + std::optional hid{}; /**< Enable a HID function. */ + std::optional xinput{}; /**< Enable an X-Input (Xbox 360) function. */ + std::optional msc{}; /**< (Future) enable an MSC function. */ espp::Logger::Verbosity log_level{espp::Logger::Verbosity::WARN}; /**< Logger verbosity. */ + + // Descriptor details (appended after the original members so positional + // aggregate initializers of earlier releases keep compiling). + uint16_t bcd_device{0x0100}; /**< bcdDevice (device release, BCD) in the device descriptor. + Ignored for an XInput-only device (which reports the Xbox + 360 value). */ + uint16_t max_power_ma{100}; /**< bMaxPower in the configuration descriptor, in mA; clamped + to 500 and rounded up to the next 2 mA unit. Some hosts + compare it against the device they expect (e.g. a Switch + expects a Pro Controller's 500 mA). */ + bool remote_wakeup{true}; /**< Advertise remote wakeup in the configuration attributes. */ }; /** diff --git a/doc/en/buses/usb_cdc.rst b/doc/en/buses/usb_cdc.rst index 2d318a749..f78c0416b 100644 --- a/doc/en/buses/usb_cdc.rst +++ b/doc/en/buses/usb_cdc.rst @@ -59,7 +59,10 @@ Features - 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 +- Configurable VID, PID, and manufacturer / product / serial / interface strings, + plus the descriptor details some hosts check: ``bcd_device`` (device release), + ``max_power_ma`` (bMaxPower, clamped to 500 mA and rounded up to the next 2 mA + unit) and ``remote_wakeup`` - No exceptions; ``initialize()`` reports failures via ``std::error_code`` - Safely marshals the TinyUSB RX callbacks (TinyUSB task context) into per-function user callbacks @@ -75,6 +78,11 @@ stream: espp::UsbDevice::Config cfg; cfg.vid = 0x1209; // pid.codes VID (ODrive uses this) cfg.pid = 0x0d32; // ODrive-like PID + // optional descriptor details (defaults: 0x0100, 100 mA, remote wakeup on); + // e.g. a Nintendo Switch expects a Pro Controller to report 0x0210 and 500 mA + cfg.bcd_device = 0x0100; + cfg.max_power_ma = 100; // clamped to 500, rounded up to a 2 mA unit + cfg.remote_wakeup = true; espp::UsbDevice::CdcFunction cdc; cdc.on_receive = [&](std::span data) { /* handle serial rx */ }; From 5842cae79d5bafc624fc422f9b0a382ead640fc1 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Tue, 15 Sep 2026 11:53:18 -0500 Subject: [PATCH 4/4] refactor(usb_device): group the descriptor fields with the device identity; log_level stays last espp configs keep log_level as the final member and only promise named (designated) initializer compatibility, so bcd_device / max_power_ma / remote_wakeup sit with vid / pid / strings where they belong. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01DFjjnq3XCTRAXENSxsCxJU --- components/usb_device/include/usb_device.hpp | 24 +++++++++----------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/components/usb_device/include/usb_device.hpp b/components/usb_device/include/usb_device.hpp index 374c2fb51..0240a674c 100644 --- a/components/usb_device/include/usb_device.hpp +++ b/components/usb_device/include/usb_device.hpp @@ -229,24 +229,22 @@ class UsbDevice : public BaseComponent { 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{}; /**< Enable a HID function. */ - std::optional xinput{}; /**< Enable an X-Input (Xbox 360) function. */ - std::optional msc{}; /**< (Future) enable an MSC function. */ - - espp::Logger::Verbosity log_level{espp::Logger::Verbosity::WARN}; /**< Logger verbosity. */ - - // Descriptor details (appended after the original members so positional - // aggregate initializers of earlier releases keep compiling). + // Descriptor details some hosts check (e.g. a Switch expects a Pro Controller + // to report bcdDevice 0x0210 and 500 mA). uint16_t bcd_device{0x0100}; /**< bcdDevice (device release, BCD) in the device descriptor. Ignored for an XInput-only device (which reports the Xbox 360 value). */ uint16_t max_power_ma{100}; /**< bMaxPower in the configuration descriptor, in mA; clamped - to 500 and rounded up to the next 2 mA unit. Some hosts - compare it against the device they expect (e.g. a Switch - expects a Pro Controller's 500 mA). */ + to 500 and rounded up to the next 2 mA unit. */ bool remote_wakeup{true}; /**< Advertise remote wakeup in the configuration attributes. */ + + std::optional cdc{}; /**< Enable a CDC-ACM function. */ + std::optional vendor{}; /**< Enable a vendor-specific / WebUSB function. */ + std::optional hid{}; /**< Enable a HID function. */ + std::optional xinput{}; /**< Enable an X-Input (Xbox 360) function. */ + std::optional msc{}; /**< (Future) enable an MSC function. */ + + espp::Logger::Verbosity log_level{espp::Logger::Verbosity::WARN}; /**< Logger verbosity. */ }; /**