From d7fa131a8a31b3dab42acfe9685532b4b72ec141 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Tue, 11 Nov 2025 19:43:06 -0500 Subject: [PATCH 1/8] [network, psram, speaker wifi] Use CORE.data to enable high performance networking (#11812) --- esphome/components/network/__init__.py | 123 ++++++++++++++++++ esphome/components/psram/__init__.py | 37 +++++- .../speaker/media_player/__init__.py | 36 +++-- esphome/components/wifi/__init__.py | 58 ++++++++- tests/components/network/test.esp32-idf.yaml | 3 + 5 files changed, 235 insertions(+), 22 deletions(-) diff --git a/esphome/components/network/__init__.py b/esphome/components/network/__init__.py index 1d62b661ca..d7a51fb0c6 100644 --- a/esphome/components/network/__init__.py +++ b/esphome/components/network/__init__.py @@ -1,7 +1,9 @@ import ipaddress +import logging import esphome.codegen as cg from esphome.components.esp32 import add_idf_sdkconfig_option +from esphome.components.psram import is_guaranteed as psram_is_guaranteed import esphome.config_validation as cv from esphome.const import CONF_ENABLE_IPV6, CONF_MIN_IPV6_ADDR_COUNT from esphome.core import CORE, CoroPriority, coroutine_with_priority @@ -9,6 +11,13 @@ from esphome.core import CORE, CoroPriority, coroutine_with_priority CODEOWNERS = ["@esphome/core"] AUTO_LOAD = ["mdns"] +_LOGGER = logging.getLogger(__name__) + +# High performance networking tracking infrastructure +# Components can request high performance networking and this configures lwip and WiFi settings +KEY_HIGH_PERFORMANCE_NETWORKING = "high_performance_networking" +CONF_ENABLE_HIGH_PERFORMANCE = "enable_high_performance" + network_ns = cg.esphome_ns.namespace("network") IPAddress = network_ns.class_("IPAddress") @@ -47,6 +56,55 @@ def ip_address_literal(ip: str | int | None) -> cg.MockObj: return IPAddress(str(ip)) +def require_high_performance_networking() -> None: + """Request high performance networking for network and WiFi. + + Call this from components that need optimized network performance for streaming + or high-throughput data transfer. This enables high performance mode which + configures both lwip TCP settings and WiFi driver settings for improved + network performance. + + Settings applied (ESP-IDF only): + - lwip: Larger TCP buffers, windows, and mailbox sizes + - WiFi: Increased RX/TX buffers, AMPDU aggregation, PSRAM allocation (set by wifi component) + + Configuration is PSRAM-aware: + - With PSRAM guaranteed: Aggressive settings (512 RX buffers, 512KB TCP windows) + - Without PSRAM: Conservative optimized settings (64 buffers, 65KB TCP windows) + + Example: + from esphome.components import network + + def _request_high_performance_networking(config): + network.require_high_performance_networking() + return config + + CONFIG_SCHEMA = cv.All( + ..., + _request_high_performance_networking, + ) + """ + # Only set up once (idempotent - multiple components can call this) + if not CORE.data.get(KEY_HIGH_PERFORMANCE_NETWORKING, False): + CORE.data[KEY_HIGH_PERFORMANCE_NETWORKING] = True + + +def has_high_performance_networking() -> bool: + """Check if high performance networking mode is enabled. + + Returns True when high performance networking has been requested by a + component or explicitly enabled in the network configuration. This indicates + that lwip and WiFi will use optimized buffer sizes and settings. + + This function should be called during code generation (to_code phase) by + components that need to apply performance-related settings. + + Returns: + bool: True if high performance networking is enabled, False otherwise + """ + return CORE.data.get(KEY_HIGH_PERFORMANCE_NETWORKING, False) + + CONFIG_SCHEMA = cv.Schema( { cv.SplitDefault( @@ -71,6 +129,7 @@ CONFIG_SCHEMA = cv.Schema( ), ), cv.Optional(CONF_MIN_IPV6_ADDR_COUNT, default=0): cv.positive_int, + cv.Optional(CONF_ENABLE_HIGH_PERFORMANCE): cv.All(cv.boolean, cv.only_on_esp32), } ) @@ -80,6 +139,70 @@ async def to_code(config): cg.add_define("USE_NETWORK") if CORE.using_arduino and CORE.is_esp32: cg.add_library("Networking", None) + + # Apply high performance networking settings + # Config can explicitly enable/disable, or default to component-driven behavior + enable_high_perf = config.get(CONF_ENABLE_HIGH_PERFORMANCE) + component_requested = CORE.data.get(KEY_HIGH_PERFORMANCE_NETWORKING, False) + + # Explicit config overrides component request + should_enable = ( + enable_high_perf if enable_high_perf is not None else component_requested + ) + + # Log when user explicitly disables but a component requested it + if enable_high_perf is False and component_requested: + _LOGGER.info( + "High performance networking disabled by user configuration (overriding component request)" + ) + + if CORE.is_esp32 and CORE.using_esp_idf and should_enable: + # Check if PSRAM is guaranteed (set by psram component during final validation) + psram_guaranteed = psram_is_guaranteed() + + if psram_guaranteed: + _LOGGER.info( + "Applying high-performance lwip settings (PSRAM guaranteed): 512KB TCP windows, 512 mailbox sizes" + ) + # PSRAM is guaranteed - use aggressive settings + # Higher maximum values are allowed because CONFIG_LWIP_WND_SCALE is set to true + # CONFIG_LWIP_WND_SCALE can only be enabled if CONFIG_SPIRAM_IGNORE_NOTFOUND isn't set + # Based on https://github.com/espressif/esp-adf/issues/297#issuecomment-783811702 + + # Enable window scaling for much larger TCP windows + add_idf_sdkconfig_option("CONFIG_LWIP_WND_SCALE", True) + add_idf_sdkconfig_option("CONFIG_LWIP_TCP_RCV_SCALE", 3) + + # Large TCP buffers and windows (requires PSRAM) + add_idf_sdkconfig_option("CONFIG_LWIP_TCP_SND_BUF_DEFAULT", 65534) + add_idf_sdkconfig_option("CONFIG_LWIP_TCP_WND_DEFAULT", 512000) + + # Large mailboxes for high throughput + add_idf_sdkconfig_option("CONFIG_LWIP_TCPIP_RECVMBOX_SIZE", 512) + add_idf_sdkconfig_option("CONFIG_LWIP_TCP_RECVMBOX_SIZE", 512) + + # TCP connection limits + add_idf_sdkconfig_option("CONFIG_LWIP_MAX_ACTIVE_TCP", 16) + add_idf_sdkconfig_option("CONFIG_LWIP_MAX_LISTENING_TCP", 16) + + # TCP optimizations + add_idf_sdkconfig_option("CONFIG_LWIP_TCP_MAXRTX", 12) + add_idf_sdkconfig_option("CONFIG_LWIP_TCP_SYNMAXRTX", 6) + add_idf_sdkconfig_option("CONFIG_LWIP_TCP_MSS", 1436) + add_idf_sdkconfig_option("CONFIG_LWIP_TCP_MSL", 60000) + add_idf_sdkconfig_option("CONFIG_LWIP_TCP_OVERSIZE_MSS", True) + add_idf_sdkconfig_option("CONFIG_LWIP_TCP_QUEUE_OOSEQ", True) + else: + _LOGGER.info( + "Applying optimized lwip settings: 65KB TCP windows, 64 mailbox sizes" + ) + # PSRAM not guaranteed - use more conservative, but still optimized settings + # Based on https://github.com/espressif/esp-idf/blob/release/v5.4/examples/wifi/iperf/sdkconfig.defaults.esp32 + add_idf_sdkconfig_option("CONFIG_LWIP_TCP_SND_BUF_DEFAULT", 65534) + add_idf_sdkconfig_option("CONFIG_LWIP_TCP_WND_DEFAULT", 65534) + add_idf_sdkconfig_option("CONFIG_LWIP_TCP_RECVMBOX_SIZE", 64) + add_idf_sdkconfig_option("CONFIG_LWIP_TCPIP_RECVMBOX_SIZE", 64) + if (enable_ipv6 := config.get(CONF_ENABLE_IPV6, None)) is not None: cg.add_define("USE_NETWORK_IPV6", enable_ipv6) if enable_ipv6: diff --git a/esphome/components/psram/__init__.py b/esphome/components/psram/__init__.py index 11c238c1bf..c50c599855 100644 --- a/esphome/components/psram/__init__.py +++ b/esphome/components/psram/__init__.py @@ -35,6 +35,9 @@ DOMAIN = "psram" DEPENDENCIES = [PLATFORM_ESP32] +# PSRAM availability tracking for cross-component coordination +KEY_PSRAM_GUARANTEED = "psram_guaranteed" + _LOGGER = logging.getLogger(__name__) psram_ns = cg.esphome_ns.namespace(DOMAIN) @@ -71,6 +74,23 @@ def supported() -> bool: return variant in SPIRAM_MODES +def is_guaranteed() -> bool: + """Check if PSRAM is guaranteed to be available. + + Returns True when PSRAM is configured with both 'disabled: false' and + 'ignore_not_found: false', meaning the device will fail to boot if PSRAM + is not found. This ensures safe use of high buffer configurations that + depend on PSRAM. + + This function should be called during code generation (to_code phase) by + components that need to know PSRAM availability for configuration decisions. + + Returns: + bool: True if PSRAM is guaranteed, False otherwise + """ + return CORE.data.get(KEY_PSRAM_GUARANTEED, False) + + def validate_psram_mode(config): esp32_config = fv.full_config.get()[PLATFORM_ESP32] if config[CONF_SPEED] == "120MHZ": @@ -131,7 +151,22 @@ def get_config_schema(config): CONFIG_SCHEMA = get_config_schema -FINAL_VALIDATE_SCHEMA = validate_psram_mode + +def _store_psram_guaranteed(config): + """Store PSRAM guaranteed status in CORE.data for other components. + + PSRAM is "guaranteed" when it will fail if not found, ensuring safe use + of high buffer configurations in network/wifi components. + + Called during final validation to ensure the flag is available + before any to_code() functions run. + """ + psram_guaranteed = not config[CONF_DISABLED] and not config[CONF_IGNORE_NOT_FOUND] + CORE.data[KEY_PSRAM_GUARANTEED] = psram_guaranteed + return config + + +FINAL_VALIDATE_SCHEMA = cv.All(validate_psram_mode, _store_psram_guaranteed) async def to_code(config): diff --git a/esphome/components/speaker/media_player/__init__.py b/esphome/components/speaker/media_player/__init__.py index e50656e723..062bff92f8 100644 --- a/esphome/components/speaker/media_player/__init__.py +++ b/esphome/components/speaker/media_player/__init__.py @@ -6,7 +6,7 @@ from pathlib import Path from esphome import automation, external_files import esphome.codegen as cg -from esphome.components import audio, esp32, media_player, psram, speaker +from esphome.components import audio, esp32, media_player, network, psram, speaker import esphome.config_validation as cv from esphome.const import ( CONF_BUFFER_SIZE, @@ -32,6 +32,7 @@ _LOGGER = logging.getLogger(__name__) AUTO_LOAD = ["audio"] +DEPENDENCIES = ["network"] CODEOWNERS = ["@kahrendt", "@synesthesiam"] DOMAIN = "media_player" @@ -280,6 +281,18 @@ PIPELINE_SCHEMA = cv.Schema( } ) + +def _request_high_performance_networking(config): + """Request high performance networking for streaming media. + + Speaker media player streams audio data, so it always benefits from + optimized WiFi and lwip settings regardless of codec support. + Called during config validation to ensure flags are set before to_code(). + """ + network.require_high_performance_networking() + return config + + CONFIG_SCHEMA = cv.All( media_player.media_player_schema(SpeakerMediaPlayer).extend( { @@ -304,6 +317,7 @@ CONFIG_SCHEMA = cv.All( ), cv.only_with_esp_idf, _validate_repeated_speaker, + _request_high_performance_networking, ) @@ -321,28 +335,10 @@ FINAL_VALIDATE_SCHEMA = cv.All( async def to_code(config): if CORE.data[DOMAIN][config[CONF_ID].id][CONF_CODEC_SUPPORT_ENABLED]: - # Compile all supported audio codecs and optimize the wifi settings - + # Compile all supported audio codecs cg.add_define("USE_AUDIO_FLAC_SUPPORT", True) cg.add_define("USE_AUDIO_MP3_SUPPORT", True) - # Based on https://github.com/espressif/esp-idf/blob/release/v5.4/examples/wifi/iperf/sdkconfig.defaults.esp32 - esp32.add_idf_sdkconfig_option("CONFIG_ESP_WIFI_STATIC_RX_BUFFER_NUM", 16) - esp32.add_idf_sdkconfig_option("CONFIG_ESP_WIFI_DYNAMIC_RX_BUFFER_NUM", 64) - esp32.add_idf_sdkconfig_option("CONFIG_ESP_WIFI_DYNAMIC_TX_BUFFER_NUM", 64) - esp32.add_idf_sdkconfig_option("CONFIG_ESP_WIFI_AMPDU_TX_ENABLED", True) - esp32.add_idf_sdkconfig_option("CONFIG_ESP_WIFI_TX_BA_WIN", 32) - esp32.add_idf_sdkconfig_option("CONFIG_ESP_WIFI_AMPDU_RX_ENABLED", True) - esp32.add_idf_sdkconfig_option("CONFIG_ESP_WIFI_RX_BA_WIN", 32) - - esp32.add_idf_sdkconfig_option("CONFIG_LWIP_TCP_SND_BUF_DEFAULT", 65534) - esp32.add_idf_sdkconfig_option("CONFIG_LWIP_TCP_WND_DEFAULT", 65534) - esp32.add_idf_sdkconfig_option("CONFIG_LWIP_TCP_RECVMBOX_SIZE", 64) - esp32.add_idf_sdkconfig_option("CONFIG_LWIP_TCPIP_RECVMBOX_SIZE", 64) - - # Allocate wifi buffers in PSRAM - esp32.add_idf_sdkconfig_option("CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP", True) - var = await media_player.new_media_player(config) await cg.register_component(var, config) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 28db698a43..4dbb425e4b 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -5,7 +5,11 @@ from esphome.automation import Condition import esphome.codegen as cg from esphome.components.const import CONF_USE_PSRAM from esphome.components.esp32 import add_idf_sdkconfig_option, const, get_esp32_variant -from esphome.components.network import ip_address_literal +from esphome.components.network import ( + has_high_performance_networking, + ip_address_literal, +) +from esphome.components.psram import is_guaranteed as psram_is_guaranteed from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv from esphome.config_validation import only_with_esp_idf @@ -56,6 +60,8 @@ _LOGGER = logging.getLogger(__name__) AUTO_LOAD = ["network"] +_LOGGER = logging.getLogger(__name__) + NO_WIFI_VARIANTS = [const.VARIANT_ESP32H2, const.VARIANT_ESP32P4] CONF_SAVE = "save" CONF_MIN_AUTH_MODE = "min_auth_mode" @@ -496,6 +502,56 @@ async def to_code(config): if config.get(CONF_USE_PSRAM): add_idf_sdkconfig_option("CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP", True) + + # Apply high performance WiFi settings if high performance networking is enabled + if CORE.is_esp32 and CORE.using_esp_idf and has_high_performance_networking(): + # Check if PSRAM is guaranteed (set by psram component during final validation) + psram_guaranteed = psram_is_guaranteed() + + # Always allocate WiFi buffers in PSRAM if available + add_idf_sdkconfig_option("CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP", True) + + if psram_guaranteed: + _LOGGER.info( + "Applying high-performance WiFi settings (PSRAM guaranteed): 512 RX buffers, 32 TX buffers" + ) + # PSRAM is guaranteed - use aggressive settings + # Higher maximum values are allowed because CONFIG_LWIP_WND_SCALE is set to true in networking component + # Based on https://github.com/espressif/esp-adf/issues/297#issuecomment-783811702 + + # Large dynamic RX buffers (requires PSRAM) + add_idf_sdkconfig_option("CONFIG_ESP_WIFI_STATIC_RX_BUFFER_NUM", 16) + add_idf_sdkconfig_option("CONFIG_ESP_WIFI_DYNAMIC_RX_BUFFER_NUM", 512) + + # Static TX buffers for better performance + add_idf_sdkconfig_option("CONFIG_ESP_WIFI_STATIC_TX_BUFFER", True) + add_idf_sdkconfig_option("CONFIG_ESP_WIFI_TX_BUFFER_TYPE", 0) + add_idf_sdkconfig_option("CONFIG_ESP_WIFI_CACHE_TX_BUFFER_NUM", 32) + add_idf_sdkconfig_option("CONFIG_ESP_WIFI_STATIC_TX_BUFFER_NUM", 8) + + # AMPDU settings optimized for PSRAM + add_idf_sdkconfig_option("CONFIG_ESP_WIFI_AMPDU_TX_ENABLED", True) + add_idf_sdkconfig_option("CONFIG_ESP_WIFI_TX_BA_WIN", 16) + add_idf_sdkconfig_option("CONFIG_ESP_WIFI_AMPDU_RX_ENABLED", True) + add_idf_sdkconfig_option("CONFIG_ESP_WIFI_RX_BA_WIN", 32) + else: + _LOGGER.info( + "Applying optimized WiFi settings: 64 RX buffers, 64 TX buffers" + ) + # PSRAM not guaranteed - use more conservative, but still optimized settings + # Based on https://github.com/espressif/esp-idf/blob/release/v5.4/examples/wifi/iperf/sdkconfig.defaults.esp32 + + # Standard buffer counts + add_idf_sdkconfig_option("CONFIG_ESP_WIFI_STATIC_RX_BUFFER_NUM", 16) + add_idf_sdkconfig_option("CONFIG_ESP_WIFI_DYNAMIC_RX_BUFFER_NUM", 64) + add_idf_sdkconfig_option("CONFIG_ESP_WIFI_DYNAMIC_TX_BUFFER_NUM", 64) + + # Standard AMPDU settings + add_idf_sdkconfig_option("CONFIG_ESP_WIFI_AMPDU_TX_ENABLED", True) + add_idf_sdkconfig_option("CONFIG_ESP_WIFI_TX_BA_WIN", 32) + add_idf_sdkconfig_option("CONFIG_ESP_WIFI_AMPDU_RX_ENABLED", True) + add_idf_sdkconfig_option("CONFIG_ESP_WIFI_RX_BA_WIN", 32) + cg.add_define("USE_WIFI") # must register before OTA safe mode check diff --git a/tests/components/network/test.esp32-idf.yaml b/tests/components/network/test.esp32-idf.yaml index dade44d145..7c01bafa0d 100644 --- a/tests/components/network/test.esp32-idf.yaml +++ b/tests/components/network/test.esp32-idf.yaml @@ -1 +1,4 @@ <<: !include common.yaml + +network: + enable_high_performance: true From a93887a79041b7f66806e27e944c90c8a2a0a860 Mon Sep 17 00:00:00 2001 From: Stuart Parmenter Date: Tue, 14 Oct 2025 21:29:41 -0700 Subject: [PATCH 2/8] [const] Add CONF_ROWS (#11249) --- esphome/components/lvgl/defines.py | 1 - esphome/components/lvgl/widgets/buttonmatrix.py | 3 +-- esphome/components/matrix_keypad/__init__.py | 3 +-- esphome/const.py | 1 + 4 files changed, 3 insertions(+), 5 deletions(-) diff --git a/esphome/components/lvgl/defines.py b/esphome/components/lvgl/defines.py index d2b0977e89..7fbb6de071 100644 --- a/esphome/components/lvgl/defines.py +++ b/esphome/components/lvgl/defines.py @@ -504,7 +504,6 @@ CONF_RESUME_ON_INPUT = "resume_on_input" CONF_RIGHT_BUTTON = "right_button" CONF_ROLLOVER = "rollover" CONF_ROOT_BACK_BTN = "root_back_btn" -CONF_ROWS = "rows" CONF_SCALE_LINES = "scale_lines" CONF_SCROLLBAR_MODE = "scrollbar_mode" CONF_SELECTED_INDEX = "selected_index" diff --git a/esphome/components/lvgl/widgets/buttonmatrix.py b/esphome/components/lvgl/widgets/buttonmatrix.py index c6b6d2440f..baeb1c8e3e 100644 --- a/esphome/components/lvgl/widgets/buttonmatrix.py +++ b/esphome/components/lvgl/widgets/buttonmatrix.py @@ -2,7 +2,7 @@ from esphome import automation import esphome.codegen as cg from esphome.components.key_provider import KeyProvider import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_ITEMS, CONF_TEXT, CONF_WIDTH +from esphome.const import CONF_ID, CONF_ITEMS, CONF_ROWS, CONF_TEXT, CONF_WIDTH from esphome.cpp_generator import MockObj from ..automation import action_to_code @@ -15,7 +15,6 @@ from ..defines import ( CONF_ONE_CHECKED, CONF_PAD_COLUMN, CONF_PAD_ROW, - CONF_ROWS, CONF_SELECTED, ) from ..helpers import lvgl_components_required diff --git a/esphome/components/matrix_keypad/__init__.py b/esphome/components/matrix_keypad/__init__.py index f7a1d622a1..2e123323a0 100644 --- a/esphome/components/matrix_keypad/__init__.py +++ b/esphome/components/matrix_keypad/__init__.py @@ -2,7 +2,7 @@ from esphome import automation, pins import esphome.codegen as cg from esphome.components import key_provider import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_ON_KEY, CONF_PIN, CONF_TRIGGER_ID +from esphome.const import CONF_ID, CONF_ON_KEY, CONF_PIN, CONF_ROWS, CONF_TRIGGER_ID CODEOWNERS = ["@ssieb"] @@ -19,7 +19,6 @@ MatrixKeyTrigger = matrix_keypad_ns.class_( ) CONF_KEYPAD_ID = "keypad_id" -CONF_ROWS = "rows" CONF_COLUMNS = "columns" CONF_KEYS = "keys" CONF_DEBOUNCE_TIME = "debounce_time" diff --git a/esphome/const.py b/esphome/const.py index 9e8ec487b5..bfd772c539 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -837,6 +837,7 @@ CONF_RMT_CHANNEL = "rmt_channel" CONF_RMT_SYMBOLS = "rmt_symbols" CONF_ROTATION = "rotation" CONF_ROW = "row" +CONF_ROWS = "rows" CONF_RS_PIN = "rs_pin" CONF_RTD_NOMINAL_RESISTANCE = "rtd_nominal_resistance" CONF_RTD_WIRES = "rtd_wires" From 9326d78439b4ceec645ba85423abafab610d5e19 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sun, 26 Oct 2025 09:00:08 -0400 Subject: [PATCH 3/8] [core] Don't allow python 3.14 (#11527) --- pyproject.toml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b7b4a48d7e..49598d434d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,7 +19,9 @@ classifiers = [ "Programming Language :: Python :: 3", "Topic :: Home Automation", ] -requires-python = ">=3.11.0" + +# Python 3.14 is currently not supported by IDF <= 5.5.1, see https://github.com/esphome/esphome/issues/11502 +requires-python = ">=3.11.0,<3.14" dynamic = ["dependencies", "optional-dependencies", "version"] From 87f79290ba2fea8d0c58de18d7a44c2adf6ee389 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Tue, 4 Nov 2025 09:23:45 +1000 Subject: [PATCH 4/8] [usb_uart] Fixes for transfer queue allocation (#11548) --- esphome/components/usb_host/usb_host.h | 12 ++--- .../components/usb_host/usb_host_client.cpp | 54 +++++++++---------- esphome/components/usb_uart/usb_uart.cpp | 22 +++++--- tests/components/usb_uart/common.yaml | 3 ++ 4 files changed, 51 insertions(+), 40 deletions(-) diff --git a/esphome/components/usb_host/usb_host.h b/esphome/components/usb_host/usb_host.h index 43b24a54a5..31bdde2df8 100644 --- a/esphome/components/usb_host/usb_host.h +++ b/esphome/components/usb_host/usb_host.h @@ -55,7 +55,7 @@ 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 = USB_HOST_MAX_REQUESTS; // maximum number of outstanding requests possible. +static constexpr 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. @@ -65,6 +65,7 @@ static_assert(MAX_REQUESTS >= 1 && MAX_REQUESTS <= 32, "MAX_REQUESTS must be bet // 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 trq_bitmask_t ALL_REQUESTS_IN_USE = MAX_REQUESTS == 32 ? ~0 : (1 << MAX_REQUESTS) - 1; 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) @@ -133,11 +134,11 @@ class USBClient : public Component { float get_setup_priority() const override { return setup_priority::IO; } void on_opened(uint8_t addr); void on_removed(usb_device_handle_t handle); - void control_transfer_callback(const usb_transfer_t *xfer) const; - void transfer_in(uint8_t ep_address, const transfer_cb_t &callback, uint16_t length); - void transfer_out(uint8_t ep_address, const transfer_cb_t &callback, const uint8_t *data, uint16_t length); + bool transfer_in(uint8_t ep_address, const transfer_cb_t &callback, uint16_t length); + bool transfer_out(uint8_t ep_address, const transfer_cb_t &callback, const uint8_t *data, uint16_t length); void dump_config() override; void release_trq(TransferRequest *trq); + trq_bitmask_t get_trq_in_use() const { return trq_in_use_; } bool control_transfer(uint8_t type, uint8_t request, uint16_t value, uint16_t index, const transfer_cb_t &callback, const std::vector &data = {}); @@ -147,7 +148,6 @@ class USBClient : public Component { EventPool event_pool; protected: - bool register_(); TransferRequest *get_trq_(); // Lock-free allocation using atomic bitmask (multi-consumer safe) virtual void disconnect(); virtual void on_connected() {} @@ -158,7 +158,7 @@ class USBClient : public Component { // USB task management static void usb_task_fn(void *arg); - void usb_task_loop(); + [[noreturn]] void usb_task_loop() const; TaskHandle_t usb_task_handle_{nullptr}; diff --git a/esphome/components/usb_host/usb_host_client.cpp b/esphome/components/usb_host/usb_host_client.cpp index 2139ed869a..dc216a209d 100644 --- a/esphome/components/usb_host/usb_host_client.cpp +++ b/esphome/components/usb_host/usb_host_client.cpp @@ -188,9 +188,9 @@ void USBClient::setup() { } // Pre-allocate USB transfer buffers for all slots at startup // This avoids any dynamic allocation during runtime - for (size_t i = 0; i < MAX_REQUESTS; i++) { - usb_host_transfer_alloc(64, 0, &this->requests_[i].transfer); - this->requests_[i].client = this; // Set once, never changes + for (auto &request : this->requests_) { + usb_host_transfer_alloc(64, 0, &request.transfer); + request.client = this; // Set once, never changes } // Create and start USB task @@ -210,8 +210,7 @@ void USBClient::usb_task_fn(void *arg) { auto *client = static_cast(arg); client->usb_task_loop(); } - -void USBClient::usb_task_loop() { +void USBClient::usb_task_loop() const { while (true) { usb_host_client_handle_events(this->handle_, portMAX_DELAY); } @@ -334,22 +333,23 @@ 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_() { - trq_bitmask_t mask = this->trq_in_use_.load(std::memory_order_relaxed); + trq_bitmask_t mask = this->trq_in_use_.load(std::memory_order_acquire); // 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 & (static_cast(1) << i)) { - // Slot is in use, move to next slot - i++; - continue; + for (;;) { + if (mask == ALL_REQUESTS_IN_USE) { + ESP_LOGE(TAG, "All %zu transfer slots in use", MAX_REQUESTS); + return nullptr; } + // find the least significant zero bit + trq_bitmask_t lsb = ~mask & (mask + 1); // Slot i appears available, try to claim it atomically - trq_bitmask_t desired = mask | (static_cast(1) << i); // Set bit i to mark as in-use + trq_bitmask_t desired = mask | lsb; - if (this->trq_in_use_.compare_exchange_weak(mask, desired, std::memory_order_acquire, std::memory_order_relaxed)) { + if (this->trq_in_use_.compare_exchange_weak(mask, desired, std::memory_order::acquire)) { + auto i = __builtin_ctz(lsb); // count trailing zeroes // Successfully claimed slot i - prepare the TransferRequest auto *trq = &this->requests_[i]; trq->transfer->context = trq; @@ -358,13 +358,9 @@ TransferRequest *USBClient::get_trq_() { } // CAS failed - another thread modified the bitmask // mask was already updated by compare_exchange_weak with the current value - // No need to reload - the CAS already did that for us - i = 0; } - - ESP_LOGE(TAG, "All %zu transfer slots in use", MAX_REQUESTS); - return nullptr; } + void USBClient::disconnect() { this->on_disconnected(); auto err = usb_host_device_close(this->handle_, this->device_handle_); @@ -446,11 +442,11 @@ static void transfer_callback(usb_transfer_t *xfer) { * * @throws None. */ -void USBClient::transfer_in(uint8_t ep_address, const transfer_cb_t &callback, uint16_t length) { +bool USBClient::transfer_in(uint8_t ep_address, const transfer_cb_t &callback, uint16_t length) { auto *trq = this->get_trq_(); if (trq == nullptr) { ESP_LOGE(TAG, "Too many requests queued"); - return; + return false; } trq->callback = callback; trq->transfer->callback = transfer_callback; @@ -460,7 +456,9 @@ void USBClient::transfer_in(uint8_t ep_address, const transfer_cb_t &callback, u if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to submit transfer, address=%x, length=%d, err=%x", ep_address, length, err); this->release_trq(trq); + return false; } + return true; } /** @@ -476,11 +474,11 @@ void USBClient::transfer_in(uint8_t ep_address, const transfer_cb_t &callback, u * * @throws None. */ -void USBClient::transfer_out(uint8_t ep_address, const transfer_cb_t &callback, const uint8_t *data, uint16_t length) { +bool USBClient::transfer_out(uint8_t ep_address, const transfer_cb_t &callback, const uint8_t *data, uint16_t length) { auto *trq = this->get_trq_(); if (trq == nullptr) { ESP_LOGE(TAG, "Too many requests queued"); - return; + return false; } trq->callback = callback; trq->transfer->callback = transfer_callback; @@ -491,7 +489,9 @@ void USBClient::transfer_out(uint8_t ep_address, const transfer_cb_t &callback, if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to submit transfer, address=%x, length=%d, err=%x", ep_address, length, err); this->release_trq(trq); + return false; } + return true; } void USBClient::dump_config() { ESP_LOGCONFIG(TAG, @@ -505,7 +505,7 @@ void USBClient::dump_config() { // - Main loop: When transfer submission fails // // THREAD SAFETY: Lock-free using atomic AND to clear bit -// Thread-safe atomic operation allows multi-threaded deallocation +// Thread-safe atomic operation allows multithreaded deallocation void USBClient::release_trq(TransferRequest *trq) { if (trq == nullptr) return; @@ -517,10 +517,10 @@ void USBClient::release_trq(TransferRequest *trq) { return; } - // Atomically clear bit i to mark slot as available + // Atomically clear the bit to mark slot as available // fetch_and with inverted bitmask clears the bit atomically - trq_bitmask_t bit = static_cast(1) << index; - this->trq_in_use_.fetch_and(static_cast(~bit), std::memory_order_release); + trq_bitmask_t mask = ~(static_cast(1) << index); + this->trq_in_use_.fetch_and(mask, std::memory_order_release); } } // namespace usb_host diff --git a/esphome/components/usb_uart/usb_uart.cpp b/esphome/components/usb_uart/usb_uart.cpp index 29003e071e..c24fffb11d 100644 --- a/esphome/components/usb_uart/usb_uart.cpp +++ b/esphome/components/usb_uart/usb_uart.cpp @@ -214,7 +214,7 @@ void USBUartComponent::dump_config() { } } void USBUartComponent::start_input(USBUartChannel *channel) { - if (!channel->initialised_.load() || channel->input_started_.load()) + if (!channel->initialised_.load()) return; // THREAD CONTEXT: Called from both USB task and main loop threads // - USB task: Immediate restart after successful transfer for continuous data flow @@ -226,12 +226,18 @@ void USBUartComponent::start_input(USBUartChannel *channel) { // // The underlying transfer_in() uses lock-free atomic allocation from the // TransferRequest pool, making this multi-threaded access safe + + // if already started, don't restart. A spurious failure in compare_exchange_weak + // is not a problem, as it will be retried on the next read_array() + auto started = false; + if (!channel->input_started_.compare_exchange_weak(started, true)) + return; const auto *ep = channel->cdc_dev_.in_ep; // CALLBACK CONTEXT: This lambda is executed in USB task via transfer_callback auto callback = [this, channel](const usb_host::TransferStatus &status) { ESP_LOGV(TAG, "Transfer result: length: %u; status %X", status.data_len, status.error_code); if (!status.success) { - ESP_LOGE(TAG, "Control transfer failed, status=%s", esp_err_to_name(status.error_code)); + ESP_LOGE(TAG, "Input transfer failed, status=%s", esp_err_to_name(status.error_code)); // On failure, don't restart - let next read_array() trigger it channel->input_started_.store(false); return; @@ -263,8 +269,9 @@ void USBUartComponent::start_input(USBUartChannel *channel) { channel->input_started_.store(false); this->start_input(channel); }; - channel->input_started_.store(true); - this->transfer_in(ep->bEndpointAddress, callback, ep->wMaxPacketSize); + if (!this->transfer_in(ep->bEndpointAddress, callback, ep->wMaxPacketSize)) { + channel->input_started_.store(false); + } } void USBUartComponent::start_output(USBUartChannel *channel) { @@ -357,11 +364,12 @@ void USBUartTypeCdcAcm::on_disconnected() { usb_host_endpoint_flush(this->device_handle_, channel->cdc_dev_.notify_ep->bEndpointAddress); } usb_host_interface_release(this->handle_, this->device_handle_, channel->cdc_dev_.bulk_interface_number); - channel->initialised_.store(false); - channel->input_started_.store(false); - channel->output_started_.store(false); + // Reset the input and output started flags to their initial state to avoid the possibility of spurious restarts + channel->input_started_.store(true); + channel->output_started_.store(true); channel->input_buffer_.clear(); channel->output_buffer_.clear(); + channel->initialised_.store(false); } USBClient::on_disconnected(); } diff --git a/tests/components/usb_uart/common.yaml b/tests/components/usb_uart/common.yaml index 46ad6291f9..474c3f5c8d 100644 --- a/tests/components/usb_uart/common.yaml +++ b/tests/components/usb_uart/common.yaml @@ -1,3 +1,6 @@ +usb_host: + max_transfer_requests: 32 + usb_uart: - id: uart_0 type: cdc_acm From 58ad4759f0b9f0e924bd093eba77e7d0db7b27a3 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Tue, 4 Nov 2025 13:43:27 +1000 Subject: [PATCH 5/8] [lvgl] Fix rotation with unusual width (#11680) --- esphome/components/const/__init__.py | 1 + esphome/components/lvgl/lvgl_esphome.cpp | 24 ++++++++++--------- .../components/lvgl/widgets/buttonmatrix.py | 3 ++- esphome/components/matrix_keypad/__init__.py | 3 ++- esphome/const.py | 1 - 5 files changed, 18 insertions(+), 14 deletions(-) diff --git a/esphome/components/const/__init__.py b/esphome/components/const/__init__.py index 19924f0da7..2b88bb43a8 100644 --- a/esphome/components/const/__init__.py +++ b/esphome/components/const/__init__.py @@ -11,4 +11,5 @@ CONF_DRAW_ROUNDING = "draw_rounding" CONF_ON_RECEIVE = "on_receive" CONF_ON_STATE_CHANGE = "on_state_change" CONF_REQUEST_HEADERS = "request_headers" +CONF_ROWS = "rows" CONF_USE_PSRAM = "use_psram" diff --git a/esphome/components/lvgl/lvgl_esphome.cpp b/esphome/components/lvgl/lvgl_esphome.cpp index 7a32691b53..6e18adccfe 100644 --- a/esphome/components/lvgl/lvgl_esphome.cpp +++ b/esphome/components/lvgl/lvgl_esphome.cpp @@ -156,6 +156,7 @@ bool LvPageType::is_showing() const { return this->parent_->get_current_page() = void LvglComponent::draw_buffer_(const lv_area_t *area, lv_color_t *ptr) { auto width = lv_area_get_width(area); auto height = lv_area_get_height(area); + auto height_rounded = (height + this->draw_rounding - 1) / this->draw_rounding * this->draw_rounding; auto x1 = area->x1; auto y1 = area->y1; lv_color_t *dst = this->rotate_buf_; @@ -163,13 +164,13 @@ void LvglComponent::draw_buffer_(const lv_area_t *area, lv_color_t *ptr) { case display::DISPLAY_ROTATION_90_DEGREES: for (lv_coord_t x = height; x-- != 0;) { for (lv_coord_t y = 0; y != width; y++) { - dst[y * height + x] = *ptr++; + dst[y * height_rounded + x] = *ptr++; } } y1 = x1; x1 = this->disp_drv_.ver_res - area->y1 - height; - width = height; - height = lv_area_get_width(area); + height = width; + width = height_rounded; break; case display::DISPLAY_ROTATION_180_DEGREES: @@ -185,13 +186,13 @@ void LvglComponent::draw_buffer_(const lv_area_t *area, lv_color_t *ptr) { case display::DISPLAY_ROTATION_270_DEGREES: for (lv_coord_t x = 0; x != height; x++) { for (lv_coord_t y = width; y-- != 0;) { - dst[y * height + x] = *ptr++; + dst[y * height_rounded + x] = *ptr++; } } x1 = y1; y1 = this->disp_drv_.hor_res - area->x1 - width; - width = height; - height = lv_area_get_width(area); + height = width; + width = height_rounded; break; default: @@ -435,8 +436,10 @@ LvglComponent::LvglComponent(std::vector displays, float buf void LvglComponent::setup() { auto *display = this->displays_[0]; - auto width = display->get_width(); - auto height = display->get_height(); + auto rounding = this->draw_rounding; + // cater for displays with dimensions that don't divide by the required rounding + auto width = (display->get_width() + rounding - 1) / rounding * rounding; + auto height = (display->get_height() + rounding - 1) / rounding * rounding; auto frac = this->buffer_frac_; if (frac == 0) frac = 1; @@ -461,9 +464,8 @@ void LvglComponent::setup() { } this->buffer_frac_ = frac; lv_disp_draw_buf_init(&this->draw_buf_, buffer, nullptr, buffer_pixels); - this->disp_drv_.hor_res = width; - this->disp_drv_.ver_res = height; - // this->setup_driver_(display->get_width(), display->get_height()); + this->disp_drv_.hor_res = display->get_width(); + this->disp_drv_.ver_res = display->get_height(); lv_disp_drv_update(this->disp_, &this->disp_drv_); this->rotation = display->get_rotation(); if (this->rotation != display::DISPLAY_ROTATION_0_DEGREES) { diff --git a/esphome/components/lvgl/widgets/buttonmatrix.py b/esphome/components/lvgl/widgets/buttonmatrix.py index baeb1c8e3e..fe421aa477 100644 --- a/esphome/components/lvgl/widgets/buttonmatrix.py +++ b/esphome/components/lvgl/widgets/buttonmatrix.py @@ -1,8 +1,9 @@ from esphome import automation import esphome.codegen as cg +from esphome.components.const import CONF_ROWS from esphome.components.key_provider import KeyProvider import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_ITEMS, CONF_ROWS, CONF_TEXT, CONF_WIDTH +from esphome.const import CONF_ID, CONF_ITEMS, CONF_TEXT, CONF_WIDTH from esphome.cpp_generator import MockObj from ..automation import action_to_code diff --git a/esphome/components/matrix_keypad/__init__.py b/esphome/components/matrix_keypad/__init__.py index 2e123323a0..868b149211 100644 --- a/esphome/components/matrix_keypad/__init__.py +++ b/esphome/components/matrix_keypad/__init__.py @@ -1,8 +1,9 @@ from esphome import automation, pins import esphome.codegen as cg from esphome.components import key_provider +from esphome.components.const import CONF_ROWS import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_ON_KEY, CONF_PIN, CONF_ROWS, CONF_TRIGGER_ID +from esphome.const import CONF_ID, CONF_ON_KEY, CONF_PIN, CONF_TRIGGER_ID CODEOWNERS = ["@ssieb"] diff --git a/esphome/const.py b/esphome/const.py index bfd772c539..9e8ec487b5 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -837,7 +837,6 @@ CONF_RMT_CHANNEL = "rmt_channel" CONF_RMT_SYMBOLS = "rmt_symbols" CONF_ROTATION = "rotation" CONF_ROW = "row" -CONF_ROWS = "rows" CONF_RS_PIN = "rs_pin" CONF_RTD_NOMINAL_RESISTANCE = "rtd_nominal_resistance" CONF_RTD_WIRES = "rtd_wires" From a59888224c62ad1c0435f147baf360ba7f2269f9 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 11 Nov 2025 19:44:37 -0500 Subject: [PATCH 6/8] Bump version to 2025.10.5 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index 4f72970e24..1390761da5 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 2025.10.4 +PROJECT_NUMBER = 2025.10.5 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/esphome/const.py b/esphome/const.py index 9e8ec487b5..ddf9f28618 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2025.10.4" +__version__ = "2025.10.5" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 7806eb980f3caff0c1a7d73d8ec7bce4b0e2286d Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 11 Nov 2025 19:50:47 -0500 Subject: [PATCH 7/8] Bump version to 2025.12.0-dev --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index 034fa3fa37..a19120b9da 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 2025.11.0-dev +PROJECT_NUMBER = 2025.12.0-dev # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/esphome/const.py b/esphome/const.py index d0d94ed283..a25114d80e 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2025.11.0-dev" +__version__ = "2025.12.0-dev" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From e104103366ccba2e6b0bce232c914a31f5aa862d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 11 Nov 2025 20:33:20 -0600 Subject: [PATCH 8/8] two more --- esphome/components/wifi/wifi_component.cpp | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index e2cd22870f..e33cd7cf2d 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -787,6 +787,8 @@ const LogString *get_signal_bars(int8_t rssi) { void WiFiComponent::print_connect_params_() { bssid_t bssid = wifi_bssid(); + char bssid_s[18]; + format_mac_addr_upper(bssid.data(), bssid_s); ESP_LOGCONFIG(TAG, " Local MAC: %s", get_mac_address_pretty().c_str()); if (this->is_disabled()) { @@ -809,9 +811,9 @@ void WiFiComponent::print_connect_params_() { " Gateway: %s\n" " DNS1: %s\n" " DNS2: %s", - wifi_ssid().c_str(), format_mac_address_pretty(bssid.data()).c_str(), App.get_name().c_str(), rssi, - LOG_STR_ARG(get_signal_bars(rssi)), get_wifi_channel(), wifi_subnet_mask_().str().c_str(), - wifi_gateway_ip_().str().c_str(), wifi_dns_ip_(0).str().c_str(), wifi_dns_ip_(1).str().c_str()); + wifi_ssid().c_str(), bssid_s, App.get_name().c_str(), rssi, LOG_STR_ARG(get_signal_bars(rssi)), + get_wifi_channel(), wifi_subnet_mask_().str().c_str(), wifi_gateway_ip_().str().c_str(), + wifi_dns_ip_(0).str().c_str(), wifi_dns_ip_(1).str().c_str()); #ifdef ESPHOME_LOG_HAS_VERBOSE if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_bssid().has_value()) { ESP_LOGV(TAG, " Priority: %d", this->get_sta_priority(*config->get_bssid())); @@ -1390,8 +1392,10 @@ void WiFiComponent::log_and_adjust_priority_for_failed_connect_() { (old_priority > std::numeric_limits::min()) ? (old_priority - 1) : std::numeric_limits::min(); this->set_sta_priority(failed_bssid.value(), new_priority); } - ESP_LOGD(TAG, "Failed " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") ", priority %d → %d", ssid.c_str(), - format_mac_address_pretty(failed_bssid.value().data()).c_str(), old_priority, new_priority); + char bssid_s[18]; + format_mac_addr_upper(failed_bssid.value().data(), bssid_s); + ESP_LOGD(TAG, "Failed " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") ", priority %d → %d", ssid.c_str(), bssid_s, + old_priority, new_priority); // After adjusting priority, check if all priorities are now at minimum // If so, clear the vector to save memory and reset for fresh start