diff --git a/esphome/components/network/__init__.py b/esphome/components/network/__init__.py index 9a47679ba3..decee27bcf 100644 --- a/esphome/components/network/__init__.py +++ b/esphome/components/network/__init__.py @@ -9,9 +9,11 @@ from esphome.components.zephyr import zephyr_add_prj_conf import esphome.config_validation as cv from esphome.const import ( CONF_ENABLE_IPV6, + CONF_ETHERNET, CONF_ID, CONF_MIN_IPV6_ADDR_COUNT, CONF_PRIORITY, + CONF_WIFI, ) from esphome.core import CORE, CoroPriority, coroutine_with_priority import esphome.final_validate as fv @@ -28,6 +30,16 @@ KEY_HIGH_PERFORMANCE_NETWORKING = "high_performance_networking" CONF_ENABLE_HIGH_PERFORMANCE = "enable_high_performance" CONF_TCP_SEND_BUFFER = "tcp_send_buffer" +# TCP receive window of the optimized lwip tier (PSRAM not guaranteed) +TCP_WND_OPTIMIZED = 65534 +# Ethernet drivers keep every queued frame in internal RAM, so ethernet-only builds +# get a window sized for a LAN round trip and the stock lwip input mailbox. Dual +# wifi + ethernet builds keep wifi's sizes; the ethernet component moves their +# received frames to PSRAM instead. +TCP_WND_ETHERNET = 16384 +TCPIP_RECVMBOX_ETHERNET = 32 +TCPIP_RECVMBOX_OPTIMIZED = 64 + # lwIP queues at most this many unsent/unacked bytes per TCP socket; the # stock ESP-IDF default (5744 bytes) stalls bursty senders like a Bluetooth # proxy streaming GATT notifications. Bounds follow the lwIP guidance for the @@ -166,6 +178,8 @@ def require_high_performance_networking() -> None: 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) + - Ethernet only: 16KB TCP windows regardless of PSRAM, because ESP-IDF + ethernet drivers keep received frames in internal RAM Example: from esphome.components import network @@ -417,8 +431,11 @@ async def to_code(config: ConfigType) -> None: if CORE.is_esp32 and should_enable: # Check if PSRAM is guaranteed (set by psram component during final validation) psram_guaranteed = psram_is_guaranteed() + # ESP-IDF ethernet drivers malloc() received frames into internal RAM, so + # lwip is never sized for PSRAM on an ethernet-only build. + ethernet_only = CONF_ETHERNET in CORE.config and CONF_WIFI not in CORE.config - if psram_guaranteed: + if psram_guaranteed and not ethernet_only: _LOGGER.info( "Applying high-performance lwip settings (PSRAM guaranteed): 512KB TCP windows, 512 mailbox sizes" ) @@ -451,15 +468,25 @@ async def to_code(config: ConfigType) -> None: add_idf_sdkconfig_option("CONFIG_LWIP_TCP_OVERSIZE_MSS", True) add_idf_sdkconfig_option("CONFIG_LWIP_TCP_QUEUE_OOSEQ", True) else: + # Every queued byte is internal RAM on ethernet and a LAN round trip needs + # little window; wifi keeps the larger sizes. + if ethernet_only: + tcp_window, tcpip_mailbox = TCP_WND_ETHERNET, TCPIP_RECVMBOX_ETHERNET + else: + tcp_window, tcpip_mailbox = TCP_WND_OPTIMIZED, TCPIP_RECVMBOX_OPTIMIZED _LOGGER.info( - "Applying optimized lwip settings: 65KB TCP windows, 64 mailbox sizes" + "Applying optimized lwip settings: %dKB TCP windows, %d entry input mailbox", + tcp_window // 1000, + tcpip_mailbox, ) # 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 + # The send buffer stays at 65534 on ethernet too: it only fills under outbound + # load, and bursty senders such as a Bluetooth proxy stall on the IDF default. 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_WND_DEFAULT", tcp_window) add_idf_sdkconfig_option("CONFIG_LWIP_TCP_RECVMBOX_SIZE", 64) - add_idf_sdkconfig_option("CONFIG_LWIP_TCPIP_RECVMBOX_SIZE", 64) + add_idf_sdkconfig_option("CONFIG_LWIP_TCPIP_RECVMBOX_SIZE", tcpip_mailbox) # After the high performance block so an explicit size wins over the # bundle's 65534 (last write wins in the sdkconfig store). diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index c9616a6c29..1ba8946841 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -771,7 +771,7 @@ async def to_code(config): "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 + # Higher maximum values are allowed because CONFIG_LWIP_WND_SCALE may be set to true in networking component # Based on https://github.com/espressif/esp-adf/issues/297#issuecomment-783811702 # Large dynamic RX buffers (requires PSRAM) diff --git a/tests/component_tests/network/__init__.py b/tests/component_tests/network/__init__.py index e69de29bb2..b4ed442ff8 100644 --- a/tests/component_tests/network/__init__.py +++ b/tests/component_tests/network/__init__.py @@ -0,0 +1,11 @@ +"""Shared helpers for the network component tests.""" + +from esphome.components.esp32.const import KEY_ESP32, KEY_SDKCONFIG_OPTIONS +from esphome.core import CORE + + +def sdkconfig_option(name: str) -> int | bool | None: + """Return a generated sdkconfig value, or None when unset or not ESP32.""" + if KEY_ESP32 not in CORE.data: + return None + return CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS].get(name) diff --git a/tests/component_tests/network/config/high_perf_ethernet_no_psram.yaml b/tests/component_tests/network/config/high_perf_ethernet_no_psram.yaml new file mode 100644 index 0000000000..31858c3fc5 --- /dev/null +++ b/tests/component_tests/network/config/high_perf_ethernet_no_psram.yaml @@ -0,0 +1,20 @@ +esphome: + name: test + +esp32: + board: esp32dev + framework: + type: esp-idf + +ethernet: + type: LAN8720 + mdc_pin: 23 + mdio_pin: 32 + clk: + pin: 0 + mode: CLK_EXT_IN + phy_addr: 0 + power_pin: 33 + +network: + enable_high_performance: true diff --git a/tests/component_tests/network/config/high_perf_ethernet_psram.yaml b/tests/component_tests/network/config/high_perf_ethernet_psram.yaml new file mode 100644 index 0000000000..3407fd5ffe --- /dev/null +++ b/tests/component_tests/network/config/high_perf_ethernet_psram.yaml @@ -0,0 +1,23 @@ +esphome: + name: test + +esp32: + variant: esp32s3 + framework: + type: esp-idf + +psram: + mode: octal + ignore_not_found: false + +ethernet: + type: W5500 + clk_pin: GPIO12 + mosi_pin: GPIO11 + miso_pin: GPIO13 + cs_pin: GPIO10 + interrupt_pin: GPIO14 + reset_pin: GPIO9 + +network: + enable_high_performance: true diff --git a/tests/component_tests/network/config/high_perf_wifi_ethernet_psram.yaml b/tests/component_tests/network/config/high_perf_wifi_ethernet_psram.yaml new file mode 100644 index 0000000000..b9b49c825d --- /dev/null +++ b/tests/component_tests/network/config/high_perf_wifi_ethernet_psram.yaml @@ -0,0 +1,30 @@ +esphome: + name: test + +esp32: + variant: esp32s3 + framework: + type: esp-idf + +psram: + mode: octal + ignore_not_found: false + +wifi: + ssid: "test_ssid" + password: "test_password" + +ethernet: + type: W5500 + clk_pin: GPIO12 + mosi_pin: GPIO11 + miso_pin: GPIO13 + cs_pin: GPIO10 + interrupt_pin: GPIO14 + reset_pin: GPIO9 + +network: + priority: + - wifi + - ethernet + enable_high_performance: true diff --git a/tests/component_tests/network/config/high_perf_wifi_no_psram.yaml b/tests/component_tests/network/config/high_perf_wifi_no_psram.yaml new file mode 100644 index 0000000000..c24e4b6253 --- /dev/null +++ b/tests/component_tests/network/config/high_perf_wifi_no_psram.yaml @@ -0,0 +1,14 @@ +esphome: + name: test + +esp32: + board: esp32dev + framework: + type: esp-idf + +wifi: + ssid: "test_ssid" + password: "test_password" + +network: + enable_high_performance: true diff --git a/tests/component_tests/network/config/high_perf_wifi_psram.yaml b/tests/component_tests/network/config/high_perf_wifi_psram.yaml new file mode 100644 index 0000000000..efdad282a4 --- /dev/null +++ b/tests/component_tests/network/config/high_perf_wifi_psram.yaml @@ -0,0 +1,18 @@ +esphome: + name: test + +esp32: + variant: esp32s3 + framework: + type: esp-idf + +psram: + mode: octal + ignore_not_found: false + +wifi: + ssid: "test_ssid" + password: "test_password" + +network: + enable_high_performance: true diff --git a/tests/component_tests/network/test_high_performance.py b/tests/component_tests/network/test_high_performance.py new file mode 100644 index 0000000000..c291806ac4 --- /dev/null +++ b/tests/component_tests/network/test_high_performance.py @@ -0,0 +1,54 @@ +"""Tests for the ``network: enable_high_performance:`` lwip tier selection. + +Ethernet drivers keep received frames in internal RAM, so an ethernet-only +build must keep lwip off the PSRAM tier even when PSRAM is guaranteed. +""" + +from collections.abc import Callable +from pathlib import Path + +import pytest + +from esphome.components.network import require_high_performance_networking +from tests.component_tests.network import sdkconfig_option + + +@pytest.mark.parametrize( + ( + "fixture", + "window", + "tcp_mailbox", + "tcpip_mailbox", + "window_scale", + "wifi_rx_buffers", + ), + [ + ("high_perf_wifi_psram.yaml", 512000, 512, 512, True, 512), + ("high_perf_wifi_no_psram.yaml", 65534, 64, 64, None, 64), + # dual builds keep wifi's sizes; the ethernet component moves frames to PSRAM + ("high_perf_wifi_ethernet_psram.yaml", 512000, 512, 512, True, 512), + ("high_perf_ethernet_no_psram.yaml", 16384, 64, 32, None, None), + # ethernet only with PSRAM guaranteed: the cell the guard exists for + ("high_perf_ethernet_psram.yaml", 16384, 64, 32, None, None), + ], +) +def test_lwip_tier( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], + fixture: str, + window: int, + tcp_mailbox: int, + tcpip_mailbox: int, + window_scale: bool | None, + wifi_rx_buffers: int | None, +) -> None: + # The wifi component only reacts to a component request, so request it the + # way sendspin does; the config key alone decides the lwip tier. + require_high_performance_networking() + generate_main(component_config_path(fixture)) + assert sdkconfig_option("CONFIG_LWIP_TCP_WND_DEFAULT") == window + assert sdkconfig_option("CONFIG_LWIP_TCP_RECVMBOX_SIZE") == tcp_mailbox + assert sdkconfig_option("CONFIG_LWIP_TCPIP_RECVMBOX_SIZE") == tcpip_mailbox + assert sdkconfig_option("CONFIG_LWIP_WND_SCALE") is window_scale + # Wifi RX buffers really go to PSRAM, so the wifi tier is never downgraded + assert sdkconfig_option("CONFIG_ESP_WIFI_DYNAMIC_RX_BUFFER_NUM") == wifi_rx_buffers diff --git a/tests/component_tests/network/test_priority.py b/tests/component_tests/network/test_priority.py index 041b358dda..57e412ea19 100644 --- a/tests/component_tests/network/test_priority.py +++ b/tests/component_tests/network/test_priority.py @@ -19,6 +19,7 @@ from esphome.components.network import ( from esphome.const import CONF_PRIORITY, PlatformFramework from esphome.core import CORE import esphome.final_validate as fv +from tests.component_tests.network import sdkconfig_option from tests.component_tests.types import SetCoreConfigCallable @@ -217,13 +218,7 @@ def test_no_primary_interface_define_without_priority( def _dns_per_default_netif_option() -> bool | None: - from esphome.components.esp32.const import KEY_ESP32, KEY_SDKCONFIG_OPTIONS - - if KEY_ESP32 not in CORE.data: # non-ESP32 configs have no sdkconfig at all - return None - return CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS].get( - "CONFIG_ESP_NETIF_SET_DNS_PER_DEFAULT_NETIF" - ) + return sdkconfig_option("CONFIG_ESP_NETIF_SET_DNS_PER_DEFAULT_NETIF") @pytest.mark.parametrize( diff --git a/tests/component_tests/network/test_tcp_send_buffer.py b/tests/component_tests/network/test_tcp_send_buffer.py index 6498921243..bf68c7c11c 100644 --- a/tests/component_tests/network/test_tcp_send_buffer.py +++ b/tests/component_tests/network/test_tcp_send_buffer.py @@ -14,32 +14,24 @@ import pytest from voluptuous import Invalid from esphome import config_validation as cv -from esphome.components.esp32.const import ( - KEY_SDKCONFIG_OPTIONS, - KEY_VARIANT, - VARIANT_ESP32, -) +from esphome.components.esp32.const import KEY_VARIANT, VARIANT_ESP32 from esphome.components.network import ( CONF_TCP_SEND_BUFFER, CONFIG_SCHEMA, TCP_SEND_BUFFER_MAX, TCP_SEND_BUFFER_MIN, ) -from esphome.const import KEY_ESP32, KEY_FRAMEWORK_VERSION, PlatformFramework -from esphome.core import CORE +from esphome.const import KEY_FRAMEWORK_VERSION, PlatformFramework +from tests.component_tests.network import sdkconfig_option from tests.component_tests.types import SetCoreConfigCallable -def _sdkconfig_option(name: str) -> int | None: - return CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS].get(name) - - def test_tcp_send_buffer_sets_sdkconfig( generate_main: Callable[[str | Path], str], component_config_path: Callable[[str], Path], ) -> None: generate_main(component_config_path("tcp_send_buffer.yaml")) - assert _sdkconfig_option("CONFIG_LWIP_TCP_SND_BUF_DEFAULT") == 32000 + assert sdkconfig_option("CONFIG_LWIP_TCP_SND_BUF_DEFAULT") == 32000 def test_tcp_send_buffer_overrides_high_performance( @@ -48,7 +40,7 @@ def test_tcp_send_buffer_overrides_high_performance( ) -> None: """An explicit size wins over the high performance bundle's 65534.""" generate_main(component_config_path("tcp_send_buffer_high_perf.yaml")) - assert _sdkconfig_option("CONFIG_LWIP_TCP_SND_BUF_DEFAULT") == 16384 + assert sdkconfig_option("CONFIG_LWIP_TCP_SND_BUF_DEFAULT") == 16384 @pytest.mark.parametrize("value", [TCP_SEND_BUFFER_MIN, TCP_SEND_BUFFER_MAX]) diff --git a/tests/components/network/test-priority.esp32-idf.yaml b/tests/components/network/test-priority.esp32-idf.yaml index baa821a234..ccafd74543 100644 --- a/tests/components/network/test-priority.esp32-idf.yaml +++ b/tests/components/network/test-priority.esp32-idf.yaml @@ -3,6 +3,11 @@ # it must actually compile (not just validate) to guard the reconciler wiring. # WiFi is listed first so the build also exercises the wifi-primary branch in # network/util.cpp (the ethernet-primary branch matches the legacy order). +# PSRAM guaranteed plus high performance networking: a dual build keeps wifi's +# PSRAM tier, so this compiles the sdkconfig a wifi + ethernet audio device gets. +psram: + ignore_not_found: false + wifi: ssid: MySSID password: password1 @@ -21,3 +26,4 @@ network: priority: - wifi - ethernet + enable_high_performance: true