From 8f68ce0ae87bc57d767ead160d48480af2b658f7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 21 Aug 2026 17:13:05 -0500 Subject: [PATCH] [network] Add tcp_send_buffer option --- esphome/components/network/__init__.py | 19 ++++++ .../network/config/tcp_send_buffer.yaml | 14 ++++ .../config/tcp_send_buffer_high_perf.yaml | 15 +++++ .../network/test_tcp_send_buffer.py | 66 +++++++++++++++++++ .../test-tcp-send-buffer.esp32-idf.yaml | 6 ++ 5 files changed, 120 insertions(+) create mode 100644 tests/component_tests/network/config/tcp_send_buffer.yaml create mode 100644 tests/component_tests/network/config/tcp_send_buffer_high_perf.yaml create mode 100644 tests/component_tests/network/test_tcp_send_buffer.py create mode 100644 tests/components/network/test-tcp-send-buffer.esp32-idf.yaml diff --git a/esphome/components/network/__init__.py b/esphome/components/network/__init__.py index 3544fb2647..29dced46c6 100644 --- a/esphome/components/network/__init__.py +++ b/esphome/components/network/__init__.py @@ -26,6 +26,15 @@ _LOGGER = logging.getLogger(__name__) # 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" +CONF_TCP_SEND_BUFFER = "tcp_send_buffer" + +# 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 +# default 1440 byte MSS: at least 2 x MSS, at most 65535 without window +# scaling. +TCP_SEND_BUFFER_MIN = 2880 +TCP_SEND_BUFFER_MAX = 65535 # Network priority tracking infrastructure # Components can query this to determine their relative setup priority. @@ -306,6 +315,11 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_ENABLE_HIGH_PERFORMANCE): cv.All( cv.boolean, cv.only_on_esp32 ), + cv.Optional(CONF_TCP_SEND_BUFFER): cv.All( + cv.validate_bytes, + cv.int_range(min=TCP_SEND_BUFFER_MIN, max=TCP_SEND_BUFFER_MAX), + cv.only_on_esp32, + ), cv.Optional(CONF_PRIORITY): _validate_priority_list, } ), @@ -446,6 +460,11 @@ async def to_code(config): add_idf_sdkconfig_option("CONFIG_LWIP_TCP_RECVMBOX_SIZE", 64) add_idf_sdkconfig_option("CONFIG_LWIP_TCPIP_RECVMBOX_SIZE", 64) + # After the high performance block so an explicit size wins over the + # bundle's 65534 (last write wins in the sdkconfig store). + if (tcp_send_buffer := config.get(CONF_TCP_SEND_BUFFER)) is not None: + add_idf_sdkconfig_option("CONFIG_LWIP_TCP_SND_BUF_DEFAULT", tcp_send_buffer) + if CORE.is_nrf52: zephyr_add_prj_conf("NETWORKING", True) zephyr_add_prj_conf("NET_IPV6", True) diff --git a/tests/component_tests/network/config/tcp_send_buffer.yaml b/tests/component_tests/network/config/tcp_send_buffer.yaml new file mode 100644 index 0000000000..59c3a29fd2 --- /dev/null +++ b/tests/component_tests/network/config/tcp_send_buffer.yaml @@ -0,0 +1,14 @@ +esphome: + name: test + +esp32: + board: esp32dev + framework: + type: esp-idf + +wifi: + ssid: "test_ssid" + password: "test_password" + +network: + tcp_send_buffer: 32kB diff --git a/tests/component_tests/network/config/tcp_send_buffer_high_perf.yaml b/tests/component_tests/network/config/tcp_send_buffer_high_perf.yaml new file mode 100644 index 0000000000..18dd1fe5fd --- /dev/null +++ b/tests/component_tests/network/config/tcp_send_buffer_high_perf.yaml @@ -0,0 +1,15 @@ +esphome: + name: test + +esp32: + board: esp32dev + framework: + type: esp-idf + +wifi: + ssid: "test_ssid" + password: "test_password" + +network: + enable_high_performance: true + tcp_send_buffer: 16384 diff --git a/tests/component_tests/network/test_tcp_send_buffer.py b/tests/component_tests/network/test_tcp_send_buffer.py new file mode 100644 index 0000000000..e193030fca --- /dev/null +++ b/tests/component_tests/network/test_tcp_send_buffer.py @@ -0,0 +1,66 @@ +"""Tests for the ``network: tcp_send_buffer:`` option. + +The option sets lwIP's per-socket TCP send buffer +(CONFIG_LWIP_TCP_SND_BUF_DEFAULT) on ESP-IDF. The stock default (5744 bytes) +stalls bursty senders such as a Bluetooth proxy streaming GATT notifications; +until now the only way to raise it was the all-or-nothing +``enable_high_performance`` bundle. +""" + +from collections.abc import Callable +from pathlib import Path + +import pytest +from voluptuous import Invalid + +from esphome import config_validation as cv +from esphome.components.esp32.const import KEY_VARIANT, VARIANT_ESP32 +from esphome.components.network import CONFIG_SCHEMA +from esphome.const import KEY_FRAMEWORK_VERSION, PlatformFramework +from esphome.core import CORE +from tests.component_tests.types import SetCoreConfigCallable + + +def _sdkconfig_option(name: str) -> int | None: + from esphome.components.esp32.const import KEY_ESP32, KEY_SDKCONFIG_OPTIONS + + 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 + + +def test_tcp_send_buffer_overrides_high_performance( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> 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 + + +@pytest.mark.parametrize("value", ["1kB", "128kB"]) +def test_out_of_range_rejected( + set_core_config: SetCoreConfigCallable, value: str +) -> None: + set_core_config( + PlatformFramework.ESP32_IDF, + core_data={KEY_FRAMEWORK_VERSION: cv.Version(5, 5, 5)}, + platform_data={KEY_VARIANT: VARIANT_ESP32}, + ) + with pytest.raises(Invalid): + CONFIG_SCHEMA({"tcp_send_buffer": value}) + + +def test_rejected_on_esp8266(set_core_config: SetCoreConfigCallable) -> None: + set_core_config( + PlatformFramework.ESP8266_ARDUINO, + core_data={KEY_FRAMEWORK_VERSION: cv.Version(3, 1, 2)}, + ) + with pytest.raises(Invalid, match="esp32"): + CONFIG_SCHEMA({"tcp_send_buffer": "32kB"}) diff --git a/tests/components/network/test-tcp-send-buffer.esp32-idf.yaml b/tests/components/network/test-tcp-send-buffer.esp32-idf.yaml new file mode 100644 index 0000000000..b83b60a68e --- /dev/null +++ b/tests/components/network/test-tcp-send-buffer.esp32-idf.yaml @@ -0,0 +1,6 @@ +wifi: + ssid: MySSID + password: password1 + +network: + tcp_send_buffer: 32kB