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/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/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index d2c5fd1180..4facb4a6f0 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())); @@ -1391,8 +1393,10 @@ void WiFiComponent::log_and_adjust_priority_for_failed_connect_() { 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 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 = ( 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