From 51fbc4f7a3296b7177486db6d80443b621dc0b93 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 12 Oct 2025 08:12:52 +0000 Subject: [PATCH 1/7] Bump aioesphomeapi from 41.13.0 to 41.14.0 (#11188) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 8cc2b4ed45..64946263ea 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile esptool==5.1.0 click==8.1.7 esphome-dashboard==20251009.0 -aioesphomeapi==41.13.0 +aioesphomeapi==41.14.0 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.18.15 # dashboard_import From 660adccda32b4603629cea1d7b0669b7219669f9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 12 Oct 2025 02:58:56 -1000 Subject: [PATCH 2/7] [mipi_rgb] Fix pin conflicts introduced by shared SPI bus in #11134 (#11185) --- tests/components/mipi_rgb/test.esp32-s3-idf.yaml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/components/mipi_rgb/test.esp32-s3-idf.yaml b/tests/components/mipi_rgb/test.esp32-s3-idf.yaml index 29f833c235..642292f7c4 100644 --- a/tests/components/mipi_rgb/test.esp32-s3-idf.yaml +++ b/tests/components/mipi_rgb/test.esp32-s3-idf.yaml @@ -40,9 +40,7 @@ display: - number: 17 blue: - number: 47 - allow_other_uses: true - - number: 41 - allow_other_uses: true + - number: 1 - number: 0 ignore_strapping_warning: true - number: 42 @@ -53,7 +51,7 @@ display: number: 45 ignore_strapping_warning: true hsync_pin: - number: 40 + number: 38 vsync_pin: number: 48 data_rate: 1000000.0 From cad747c672da80ab5484199fe7261843066d10e7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 12 Oct 2025 08:25:35 -1000 Subject: [PATCH 3/7] [ci] Dynamic runner allocation: 8 for releases, 4 for dev (#11191) --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f692b1f7d0..0363b5afdf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -433,7 +433,7 @@ jobs: if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) >= 100 strategy: fail-fast: false - max-parallel: 5 + max-parallel: ${{ (github.base_ref == 'beta' || github.base_ref == 'release') && 8 || 4 }} matrix: components: ${{ fromJson(needs.test-build-components-splitter.outputs.matrix) }} steps: From 9b6e8b4b414ede5043ca0cbd0f766b4ca446f9bf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 12 Oct 2025 08:26:28 -1000 Subject: [PATCH 4/7] [wifi] Fix missed string literal in flash on ESP8266 (#11187) --- esphome/components/wifi/wifi_component.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 2e083d4c68..71ee4271ba 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -576,8 +576,9 @@ __attribute__((noinline)) static void log_scan_result(const WiFiScanResult &res) format_mac_addr_upper(bssid.data(), bssid_s); if (res.get_matches()) { - ESP_LOGI(TAG, "- '%s' %s" LOG_SECRET("(%s) ") "%s", res.get_ssid().c_str(), res.get_is_hidden() ? "(HIDDEN) " : "", - bssid_s, LOG_STR_ARG(get_signal_bars(res.get_rssi()))); + ESP_LOGI(TAG, "- '%s' %s" LOG_SECRET("(%s) ") "%s", res.get_ssid().c_str(), + res.get_is_hidden() ? LOG_STR_LITERAL("(HIDDEN) ") : LOG_STR_LITERAL(""), bssid_s, + LOG_STR_ARG(get_signal_bars(res.get_rssi()))); ESP_LOGD(TAG, " Channel: %u\n" " RSSI: %d dB", From 6bc9ed08107b89e75a3df7efe5166c7c01c8b90c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 12 Oct 2025 08:27:43 -1000 Subject: [PATCH 5/7] [ota] Increase handshake timeout to 20s now that auth is non-blocking (#11186) --- esphome/components/esphome/ota/ota_esphome.cpp | 2 +- esphome/espota2.py | 2 +- tests/unit_tests/test_espota2.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index b65bfc5ab8..569268ea15 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -29,7 +29,7 @@ namespace esphome { static const char *const TAG = "esphome.ota"; static constexpr uint16_t OTA_BLOCK_SIZE = 8192; static constexpr size_t OTA_BUFFER_SIZE = 1024; // buffer size for OTA data transfer -static constexpr uint32_t OTA_SOCKET_TIMEOUT_HANDSHAKE = 10000; // milliseconds for initial handshake +static constexpr uint32_t OTA_SOCKET_TIMEOUT_HANDSHAKE = 20000; // milliseconds for initial handshake static constexpr uint32_t OTA_SOCKET_TIMEOUT_DATA = 90000; // milliseconds for data transfer #ifdef USE_OTA_PASSWORD diff --git a/esphome/espota2.py b/esphome/espota2.py index 2712d00127..17a1da8235 100644 --- a/esphome/espota2.py +++ b/esphome/espota2.py @@ -410,7 +410,7 @@ def run_ota_impl_( af, socktype, _, _, sa = r _LOGGER.info("Connecting to %s port %s...", sa[0], sa[1]) sock = socket.socket(af, socktype) - sock.settimeout(10.0) + sock.settimeout(20.0) try: sock.connect(sa) except OSError as err: diff --git a/tests/unit_tests/test_espota2.py b/tests/unit_tests/test_espota2.py index bd1a6bde81..52c72291d6 100644 --- a/tests/unit_tests/test_espota2.py +++ b/tests/unit_tests/test_espota2.py @@ -493,7 +493,7 @@ def test_run_ota_impl_successful( assert result_host == "192.168.1.100" # Verify socket was configured correctly - mock_socket.settimeout.assert_called_with(10.0) + mock_socket.settimeout.assert_called_with(20.0) mock_socket.connect.assert_called_once_with(("192.168.1.100", 3232)) mock_socket.close.assert_called_once() From 9ebfa9aaa82b813784b61c397ac364557fb5d47e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 12 Oct 2025 08:30:58 -1000 Subject: [PATCH 6/7] [esp32_improv] Fix state not transitioning to PROVISIONED when WiFi configured via captive portal (#11181) --- .../esp32_improv/esp32_improv_component.cpp | 54 +++++++++++-------- .../esp32_improv/esp32_improv_component.h | 1 + 2 files changed, 34 insertions(+), 21 deletions(-) diff --git a/esphome/components/esp32_improv/esp32_improv_component.cpp b/esphome/components/esp32_improv/esp32_improv_component.cpp index f773083890..d83caf931b 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.cpp +++ b/esphome/components/esp32_improv/esp32_improv_component.cpp @@ -143,6 +143,7 @@ void ESP32ImprovComponent::loop() { #else this->set_state_(improv::STATE_AUTHORIZED); #endif + this->check_wifi_connection_(); break; } case improv::STATE_AUTHORIZED: { @@ -156,31 +157,12 @@ void ESP32ImprovComponent::loop() { if (!this->check_identify_()) { this->set_status_indicator_state_((now % 1000) < 500); } + this->check_wifi_connection_(); break; } case improv::STATE_PROVISIONING: { this->set_status_indicator_state_((now % 200) < 100); - if (wifi::global_wifi_component->is_connected()) { - wifi::global_wifi_component->save_wifi_sta(this->connecting_sta_.get_ssid(), - this->connecting_sta_.get_password()); - this->connecting_sta_ = {}; - this->cancel_timeout("wifi-connect-timeout"); - this->set_state_(improv::STATE_PROVISIONED); - - std::vector urls = {ESPHOME_MY_LINK}; -#ifdef USE_WEBSERVER - for (auto &ip : wifi::global_wifi_component->wifi_sta_ip_addresses()) { - if (ip.is_ip4()) { - std::string webserver_url = "http://" + ip.str() + ":" + to_string(USE_WEBSERVER_PORT); - urls.push_back(webserver_url); - break; - } - } -#endif - std::vector data = improv::build_rpc_response(improv::WIFI_SETTINGS, urls); - this->send_response_(data); - this->stop(); - } + this->check_wifi_connection_(); break; } case improv::STATE_PROVISIONED: { @@ -392,6 +374,36 @@ void ESP32ImprovComponent::on_wifi_connect_timeout_() { wifi::global_wifi_component->clear_sta(); } +void ESP32ImprovComponent::check_wifi_connection_() { + if (!wifi::global_wifi_component->is_connected()) { + return; + } + + if (this->state_ == improv::STATE_PROVISIONING) { + wifi::global_wifi_component->save_wifi_sta(this->connecting_sta_.get_ssid(), this->connecting_sta_.get_password()); + this->connecting_sta_ = {}; + this->cancel_timeout("wifi-connect-timeout"); + + std::vector urls = {ESPHOME_MY_LINK}; +#ifdef USE_WEBSERVER + for (auto &ip : wifi::global_wifi_component->wifi_sta_ip_addresses()) { + if (ip.is_ip4()) { + std::string webserver_url = "http://" + ip.str() + ":" + to_string(USE_WEBSERVER_PORT); + urls.push_back(webserver_url); + break; + } + } +#endif + std::vector data = improv::build_rpc_response(improv::WIFI_SETTINGS, urls); + this->send_response_(data); + } else if (this->is_active() && this->state_ != improv::STATE_PROVISIONED) { + ESP_LOGD(TAG, "WiFi provisioned externally"); + } + + this->set_state_(improv::STATE_PROVISIONED); + this->stop(); +} + void ESP32ImprovComponent::advertise_service_data_() { uint8_t service_data[IMPROV_SERVICE_DATA_SIZE] = {}; service_data[0] = IMPROV_PROTOCOL_ID_1; // PR diff --git a/esphome/components/esp32_improv/esp32_improv_component.h b/esphome/components/esp32_improv/esp32_improv_component.h index eb07e09dce..6782430ffe 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.h +++ b/esphome/components/esp32_improv/esp32_improv_component.h @@ -111,6 +111,7 @@ class ESP32ImprovComponent : public Component { void send_response_(std::vector &response); void process_incoming_data_(); void on_wifi_connect_timeout_(); + void check_wifi_connection_(); bool check_identify_(); void advertise_service_data_(); #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG From 1f13d44c1b9ebed5abd14aa43200f4ed269418e1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 12 Oct 2025 09:04:30 -1000 Subject: [PATCH 7/7] [usb_host] Fix transfer slot exhaustion at high data rates and add configurable max_transfer_requests (#11174) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- esphome/components/usb_host/__init__.py | 11 +++- esphome/components/usb_host/usb_host.h | 42 ++++++++------ .../components/usb_host/usb_host_client.cpp | 57 ++++++------------- esphome/core/defines.h | 1 + .../usb_host/test.esp32-s3-idf.yaml | 1 + 5 files changed, 54 insertions(+), 58 deletions(-) diff --git a/esphome/components/usb_host/__init__.py b/esphome/components/usb_host/__init__.py index de734bf425..d452e0e9fa 100644 --- a/esphome/components/usb_host/__init__.py +++ b/esphome/components/usb_host/__init__.py @@ -9,6 +9,7 @@ from esphome.components.esp32 import ( import esphome.config_validation as cv from esphome.const import CONF_DEVICES, CONF_ID from esphome.cpp_types import Component +from esphome.types import ConfigType AUTO_LOAD = ["bytebuffer"] CODEOWNERS = ["@clydebarrow"] @@ -20,6 +21,7 @@ USBClient = usb_host_ns.class_("USBClient", Component) CONF_VID = "vid" CONF_PID = "pid" CONF_ENABLE_HUBS = "enable_hubs" +CONF_MAX_TRANSFER_REQUESTS = "max_transfer_requests" def usb_device_schema(cls=USBClient, vid: int = None, pid: [int] = None) -> cv.Schema: @@ -44,6 +46,9 @@ CONFIG_SCHEMA = cv.All( { cv.GenerateID(): cv.declare_id(USBHost), cv.Optional(CONF_ENABLE_HUBS, default=False): cv.boolean, + cv.Optional(CONF_MAX_TRANSFER_REQUESTS, default=16): cv.int_range( + min=1, max=32 + ), cv.Optional(CONF_DEVICES): cv.ensure_list(usb_device_schema()), } ), @@ -58,10 +63,14 @@ async def register_usb_client(config): return var -async def to_code(config): +async def to_code(config: ConfigType) -> None: add_idf_sdkconfig_option("CONFIG_USB_HOST_CONTROL_TRANSFER_MAX_SIZE", 1024) if config.get(CONF_ENABLE_HUBS): add_idf_sdkconfig_option("CONFIG_USB_HOST_HUBS_SUPPORTED", True) + + max_requests = config[CONF_MAX_TRANSFER_REQUESTS] + cg.add_define("USB_HOST_MAX_REQUESTS", max_requests) + var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) for device in config.get(CONF_DEVICES) or (): diff --git a/esphome/components/usb_host/usb_host.h b/esphome/components/usb_host/usb_host.h index 4f8d2ec9a8..43b24a54a5 100644 --- a/esphome/components/usb_host/usb_host.h +++ b/esphome/components/usb_host/usb_host.h @@ -2,6 +2,7 @@ // Should not be needed, but it's required to pass CI clang-tidy checks #if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4) +#include "esphome/core/defines.h" #include "esphome/core/component.h" #include #include "usb/usb_host.h" @@ -16,23 +17,25 @@ namespace usb_host { // THREADING MODEL: // This component uses a dedicated USB task for event processing to prevent data loss. -// - USB Task (high priority): Handles USB events, executes transfer callbacks -// - Main Loop Task: Initiates transfers, processes completion events +// - USB Task (high priority): Handles USB events, executes transfer callbacks, releases transfer slots +// - Main Loop Task: Initiates transfers, processes device connect/disconnect events // // Thread-safe communication: // - Lock-free queues for USB task -> main loop events (SPSC pattern) -// - Lock-free TransferRequest pool using atomic bitmask (MCSP pattern) +// - Lock-free TransferRequest pool using atomic bitmask (MCMP pattern - multi-consumer, multi-producer) // // TransferRequest pool access pattern: // - get_trq_() [allocate]: Called from BOTH USB task and main loop threads // * USB task: via USB UART input callbacks that restart transfers immediately // * Main loop: for output transfers and flow-controlled input restarts -// - release_trq() [deallocate]: Called from main loop thread only +// - release_trq() [deallocate]: Called from BOTH USB task and main loop threads +// * USB task: immediately after transfer callback completes (critical for preventing slot exhaustion) +// * Main loop: when transfer submission fails // -// The multi-threaded allocation is intentional for performance: -// - USB task can immediately restart input transfers without context switching +// The multi-threaded allocation/deallocation is intentional for performance: +// - USB task can immediately restart input transfers and release slots without context switching // - Main loop controls backpressure by deciding when to restart after consuming data -// The atomic bitmask ensures thread-safe allocation without mutex blocking. +// The atomic bitmask ensures thread-safe allocation/deallocation without mutex blocking. static const char *const TAG = "usb_host"; @@ -52,8 +55,17 @@ static const uint8_t USB_DIR_IN = 1 << 7; static const uint8_t USB_DIR_OUT = 0; static const size_t SETUP_PACKET_SIZE = 8; -static const size_t MAX_REQUESTS = 16; // maximum number of outstanding requests possible. -static_assert(MAX_REQUESTS <= 16, "MAX_REQUESTS must be <= 16 to fit in uint16_t bitmask"); +static const size_t MAX_REQUESTS = USB_HOST_MAX_REQUESTS; // maximum number of outstanding requests possible. +static_assert(MAX_REQUESTS >= 1 && MAX_REQUESTS <= 32, "MAX_REQUESTS must be between 1 and 32"); + +// Select appropriate bitmask type for tracking allocation of TransferRequest slots. +// The bitmask must have at least as many bits as MAX_REQUESTS, so: +// - Use uint16_t for up to 16 requests (MAX_REQUESTS <= 16) +// - Use uint32_t for 17-32 requests (MAX_REQUESTS > 16) +// This is tied to the static_assert above, which enforces MAX_REQUESTS is between 1 and 32. +// If MAX_REQUESTS is increased above 32, this logic and the static_assert must be updated. +using trq_bitmask_t = std::conditional<(MAX_REQUESTS <= 16), uint16_t, uint32_t>::type; + static constexpr size_t USB_EVENT_QUEUE_SIZE = 32; // Size of event queue between USB task and main loop static constexpr size_t USB_TASK_STACK_SIZE = 4096; // Stack size for USB task (same as ESP-IDF USB examples) static constexpr UBaseType_t USB_TASK_PRIORITY = 5; // Higher priority than main loop (tskIDLE_PRIORITY + 5) @@ -83,8 +95,6 @@ struct TransferRequest { enum EventType : uint8_t { EVENT_DEVICE_NEW, EVENT_DEVICE_GONE, - EVENT_TRANSFER_COMPLETE, - EVENT_CONTROL_COMPLETE, }; struct UsbEvent { @@ -96,9 +106,6 @@ struct UsbEvent { struct { usb_device_handle_t handle; } device_gone; - struct { - TransferRequest *trq; - } transfer; } data; // Required for EventPool - no cleanup needed for POD types @@ -163,10 +170,9 @@ class USBClient : public Component { uint16_t pid_{}; // Lock-free pool management using atomic bitmask (no dynamic allocation) // Bit i = 1: requests_[i] is in use, Bit i = 0: requests_[i] is available - // Supports multiple concurrent consumers (both threads can allocate) - // Single producer for deallocation (main loop only) - // Limited to 16 slots by uint16_t size (enforced by static_assert) - std::atomic trq_in_use_; + // Supports multiple concurrent consumers and producers (both threads can allocate/deallocate) + // Bitmask type automatically selected: uint16_t for <= 16 slots, uint32_t for 17-32 slots + std::atomic trq_in_use_; TransferRequest requests_[MAX_REQUESTS]{}; }; class USBHost : public Component { diff --git a/esphome/components/usb_host/usb_host_client.cpp b/esphome/components/usb_host/usb_host_client.cpp index b26385a8ef..2139ed869a 100644 --- a/esphome/components/usb_host/usb_host_client.cpp +++ b/esphome/components/usb_host/usb_host_client.cpp @@ -228,12 +228,6 @@ void USBClient::loop() { case EVENT_DEVICE_GONE: this->on_removed(event->data.device_gone.handle); break; - case EVENT_TRANSFER_COMPLETE: - case EVENT_CONTROL_COMPLETE: { - auto *trq = event->data.transfer.trq; - this->release_trq(trq); - break; - } } // Return event to pool for reuse this->event_pool.release(event); @@ -313,25 +307,6 @@ void USBClient::on_removed(usb_device_handle_t handle) { } } -// Helper to queue transfer cleanup to main loop -static void queue_transfer_cleanup(TransferRequest *trq, EventType type) { - auto *client = trq->client; - - // Allocate event from pool - UsbEvent *event = client->event_pool.allocate(); - if (event == nullptr) { - // No events available - increment counter for periodic logging - client->event_queue.increment_dropped_count(); - return; - } - - event->type = type; - event->data.transfer.trq = trq; - - // Push to lock-free queue (always succeeds since pool size == queue size) - client->event_queue.push(event); -} - // CALLBACK CONTEXT: USB task (called from usb_host_client_handle_events in USB task) static void control_callback(const usb_transfer_t *xfer) { auto *trq = static_cast(xfer->context); @@ -346,8 +321,9 @@ static void control_callback(const usb_transfer_t *xfer) { trq->callback(trq->status); } - // Queue cleanup to main loop - queue_transfer_cleanup(trq, EVENT_CONTROL_COMPLETE); + // Release transfer slot immediately in USB task + // The release_trq() uses thread-safe atomic operations + trq->client->release_trq(trq); } // THREAD CONTEXT: Called from both USB task and main loop threads (multi-consumer) @@ -358,20 +334,20 @@ static void control_callback(const usb_transfer_t *xfer) { // This multi-threaded access is intentional for performance - USB task can // immediately restart transfers without waiting for main loop scheduling. TransferRequest *USBClient::get_trq_() { - uint16_t mask = this->trq_in_use_.load(std::memory_order_relaxed); + trq_bitmask_t mask = this->trq_in_use_.load(std::memory_order_relaxed); // Find first available slot (bit = 0) and try to claim it atomically // We use a while loop to allow retrying the same slot after CAS failure size_t i = 0; while (i != MAX_REQUESTS) { - if (mask & (1U << i)) { + if (mask & (static_cast(1) << i)) { // Slot is in use, move to next slot i++; continue; } // Slot i appears available, try to claim it atomically - uint16_t desired = mask | (1U << i); // Set bit i to mark as in-use + trq_bitmask_t desired = mask | (static_cast(1) << i); // Set bit i to mark as in-use if (this->trq_in_use_.compare_exchange_weak(mask, desired, std::memory_order_acquire, std::memory_order_relaxed)) { // Successfully claimed slot i - prepare the TransferRequest @@ -386,7 +362,7 @@ TransferRequest *USBClient::get_trq_() { i = 0; } - ESP_LOGE(TAG, "All %d transfer slots in use", MAX_REQUESTS); + ESP_LOGE(TAG, "All %zu transfer slots in use", MAX_REQUESTS); return nullptr; } void USBClient::disconnect() { @@ -452,8 +428,11 @@ static void transfer_callback(usb_transfer_t *xfer) { trq->callback(trq->status); } - // Queue cleanup to main loop - queue_transfer_cleanup(trq, EVENT_TRANSFER_COMPLETE); + // Release transfer slot AFTER callback completes to prevent slot exhaustion + // This is critical for high-throughput transfers (e.g., USB UART at 115200 baud) + // The callback has finished accessing xfer->data_buffer, so it's safe to release + // The release_trq() uses thread-safe atomic operations + trq->client->release_trq(trq); } /** * Performs a transfer input operation. @@ -521,12 +500,12 @@ void USBClient::dump_config() { " Product id %04X", this->vid_, this->pid_); } -// THREAD CONTEXT: Only called from main loop thread (single producer for deallocation) -// - Via event processing when handling EVENT_TRANSFER_COMPLETE/EVENT_CONTROL_COMPLETE -// - Directly when transfer submission fails +// THREAD CONTEXT: Called from both USB task and main loop threads +// - USB task: Immediately after transfer callback completes +// - Main loop: When transfer submission fails // // THREAD SAFETY: Lock-free using atomic AND to clear bit -// Single-producer pattern makes this simpler than allocation +// Thread-safe atomic operation allows multi-threaded deallocation void USBClient::release_trq(TransferRequest *trq) { if (trq == nullptr) return; @@ -540,8 +519,8 @@ void USBClient::release_trq(TransferRequest *trq) { // Atomically clear bit i to mark slot as available // fetch_and with inverted bitmask clears the bit atomically - uint16_t bit = 1U << index; - this->trq_in_use_.fetch_and(static_cast(~bit), std::memory_order_release); + trq_bitmask_t bit = static_cast(1) << index; + this->trq_in_use_.fetch_and(static_cast(~bit), std::memory_order_release); } } // namespace usb_host diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 955d0f987c..ae44e16624 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -193,6 +193,7 @@ #define USE_WEBSERVER_PORT 80 // NOLINT #define USE_WEBSERVER_SORTING #define USE_WIFI_11KV_SUPPORT +#define USB_HOST_MAX_REQUESTS 16 #ifdef USE_ARDUINO #define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 2, 1) diff --git a/tests/components/usb_host/test.esp32-s3-idf.yaml b/tests/components/usb_host/test.esp32-s3-idf.yaml index a2892872e5..5360d1f6ff 100644 --- a/tests/components/usb_host/test.esp32-s3-idf.yaml +++ b/tests/components/usb_host/test.esp32-s3-idf.yaml @@ -1,4 +1,5 @@ usb_host: + max_transfer_requests: 32 # Test uint32_t bitmask path (17-32 requests) devices: - id: device_1 vid: 0x1234