From 360a7ba9c79f8d55c0583bead4fea0075ff44888 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Feb 2026 19:29:39 -0600 Subject: [PATCH 1/3] =?UTF-8?q?[libretiny]=20Fix=20TCP=5FWND=20to=20show?= =?UTF-8?q?=203/10=C3=97MSS=20for=20BK=20reduced/default=20plans?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BK SDK sets TCP_SND_BUF=10×MSS on both plans but TCP_WND varies: 3×MSS (reduced plan) vs 10×MSS (default plan). Also update stale minimum counts in docstring (8 TCP / 6 UDP). Co-Authored-By: Claude Opus 4.6 --- esphome/components/libretiny/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/libretiny/__init__.py b/esphome/components/libretiny/__init__.py index 6b6b1abe1cc..f6679e24560 100644 --- a/esphome/components/libretiny/__init__.py +++ b/esphome/components/libretiny/__init__.py @@ -290,7 +290,7 @@ def _configure_lwip(config: dict) -> None: Setting ESP8266 ESP32 BK SDK RTL SDK LN SDK New ──────────────────────────────────────────────────────────────────────────── TCP_SND_BUF 2×MSS 4×MSS 10×MSS 5×MSS 7×MSS 4×MSS - TCP_WND 4×MSS 4×MSS 10×MSS 2×MSS 3×MSS 4×MSS + TCP_WND 4×MSS 4×MSS 3/10×MSS 2×MSS 3×MSS 4×MSS MEM_LIBC_MALLOC 1 1 0 0 1 1 MEMP_MEM_MALLOC 1 1 0 0 0 1 MEM_SIZE N/A* N/A* 16/32KB 5KB N/A* N/A* BK @@ -313,7 +313,7 @@ def _configure_lwip(config: dict) -> None: **** RTL/LN LT overlay overrides to flat 7. ***** Not defined in RTL SDK — lwIP opt.h defaults shown. "dynamic" = auto-calculated from component socket registrations via - socket.get_socket_counts() with minimums of 10 TCP / 8 UDP. + socket.get_socket_counts() with minimums of 8 TCP / 6 UDP. """ from esphome.components.socket import ( MIN_TCP_SOCKETS, From 30a2af0d54c9a3db71510b960a1dcd7e117cd406 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Feb 2026 19:39:14 -0600 Subject: [PATCH 2/3] [socket] Add MIN_TCP_LISTEN_SOCKETS constant for consistency - Clarify MIN_TCP_SOCKETS comment: covers minimal configs (api-only); when web_server is present its 5 sockets push past the minimum. - Add MIN_TCP_LISTEN_SOCKETS = 2 alongside MIN_TCP/MIN_UDP for consistency instead of hardcoding the value in libretiny. Co-Authored-By: Claude Opus 4.6 --- esphome/components/libretiny/__init__.py | 5 +++-- esphome/components/socket/__init__.py | 8 +++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/esphome/components/libretiny/__init__.py b/esphome/components/libretiny/__init__.py index f6679e24560..1a424b653fe 100644 --- a/esphome/components/libretiny/__init__.py +++ b/esphome/components/libretiny/__init__.py @@ -316,6 +316,7 @@ def _configure_lwip(config: dict) -> None: socket.get_socket_counts() with minimums of 8 TCP / 6 UDP. """ from esphome.components.socket import ( + MIN_TCP_LISTEN_SOCKETS, MIN_TCP_SOCKETS, MIN_UDP_SOCKETS, get_socket_counts, @@ -328,8 +329,8 @@ def _configure_lwip(config: dict) -> None: tcp_sockets = max(MIN_TCP_SOCKETS, raw_tcp) udp_sockets = max(MIN_UDP_SOCKETS, raw_udp) # Listening sockets — registered by components (api, ota, web_server_base, etc.) - # Not all components register yet, so ensure a minimum of 2 (api + ota baseline). - listening_tcp = max(raw_tcp_listen, 2) + # Not all components register yet, so ensure a minimum for baseline operation. + listening_tcp = max(MIN_TCP_LISTEN_SOCKETS, raw_tcp_listen) # TCP_SND_BUF: ESPAsyncWebServer allocates malloc(tcp_sndbuf()) per # response chunk. At 10×MSS=14.6KB (BK default) this causes OOM (#14095). diff --git a/esphome/components/socket/__init__.py b/esphome/components/socket/__init__.py index b8f971fd055..572e7993b90 100644 --- a/esphome/components/socket/__init__.py +++ b/esphome/components/socket/__init__.py @@ -21,12 +21,14 @@ KEY_SOCKET_CONSUMERS_TCP = "socket_consumers_tcp" KEY_SOCKET_CONSUMERS_UDP = "socket_consumers_udp" KEY_SOCKET_CONSUMERS_TCP_LISTEN = "socket_consumers_tcp_listen" -# Recommended minimum socket counts to ensure headroom. +# Recommended minimum socket counts. # Platforms should apply these (or their own) on top of get_socket_counts(). -# TCP: api(3) = 3 base, +5 headroom for ota-transfer/web_server/other. -# UDP: dhcp(1) + dns(1) + mdns(2) + wake_loop(1) = 5 base, +1 headroom. +# These cover minimal configs (e.g. api-only without web_server). +# When web_server is present, its 5 registered sockets push past the TCP minimum. MIN_TCP_SOCKETS = 8 MIN_UDP_SOCKETS = 6 +# Minimum listening sockets — at least api + ota baseline. +MIN_TCP_LISTEN_SOCKETS = 2 # Wake loop threadsafe support tracking KEY_WAKE_LOOP_THREADSAFE_REQUIRED = "wake_loop_threadsafe_required" From d5efbfde5c44cb4ee173d70f444f0ae1a9859b55 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Feb 2026 20:03:08 -0600 Subject: [PATCH 3/3] [socket] Return SocketCounts dataclass from get_socket_counts() Replace the unwieldy 6-tuple with a frozen dataclass for clarity and named attribute access at call sites. Co-Authored-By: Claude Opus 4.6 --- esphome/components/esp32/__init__.py | 29 +++++++++--------------- esphome/components/libretiny/__init__.py | 22 ++++++++---------- esphome/components/socket/__init__.py | 21 +++++++++++++---- 3 files changed, 38 insertions(+), 34 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index f15acfe4a96..4c211b2f2ad 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -1265,15 +1265,8 @@ def _configure_lwip_max_sockets(conf: dict) -> None: # CONFIG_LWIP_MAX_SOCKETS is a single VFS socket pool shared by all socket # types (TCP clients, TCP listeners, and UDP). Include all three counts. - ( - tcp_sockets, - udp_sockets, - tcp_listen, - tcp_details, - udp_details, - tcp_listen_details, - ) = get_socket_counts() - total_sockets = tcp_sockets + udp_sockets + tcp_listen + sc = get_socket_counts() + total_sockets = sc.tcp + sc.udp + sc.tcp_listen # User specified their own value - respect it but warn if insufficient if user_max_sockets is not None: @@ -1294,9 +1287,9 @@ def _configure_lwip_max_sockets(conf: dict) -> None: "at least %d.", user_sockets_int, total_sockets, - tcp_sockets, - udp_sockets, - tcp_listen, + sc.tcp, + sc.udp, + sc.tcp_listen, total_sockets, ) # User's value already added via sdkconfig_options processing @@ -1314,12 +1307,12 @@ def _configure_lwip_max_sockets(conf: dict) -> None: "(TCP=%d [%s], UDP=%d [%s], TCP_LISTEN=%d [%s])", max_sockets, sock_min, - tcp_sockets, - tcp_details, - udp_sockets, - udp_details, - tcp_listen, - tcp_listen_details, + sc.tcp, + sc.tcp_details, + sc.udp, + sc.udp_details, + sc.tcp_listen, + sc.tcp_listen_details, ) add_idf_sdkconfig_option("CONFIG_LWIP_MAX_SOCKETS", max_sockets) diff --git a/esphome/components/libretiny/__init__.py b/esphome/components/libretiny/__init__.py index 1a424b653fe..2291114d9a0 100644 --- a/esphome/components/libretiny/__init__.py +++ b/esphome/components/libretiny/__init__.py @@ -322,15 +322,13 @@ def _configure_lwip(config: dict) -> None: get_socket_counts, ) - raw_tcp, raw_udp, raw_tcp_listen, tcp_details, udp_details, tcp_listen_details = ( - get_socket_counts() - ) + sc = get_socket_counts() # Apply platform minimums — ensure headroom for ESPHome's needs - tcp_sockets = max(MIN_TCP_SOCKETS, raw_tcp) - udp_sockets = max(MIN_UDP_SOCKETS, raw_udp) + tcp_sockets = max(MIN_TCP_SOCKETS, sc.tcp) + udp_sockets = max(MIN_UDP_SOCKETS, sc.udp) # Listening sockets — registered by components (api, ota, web_server_base, etc.) # Not all components register yet, so ensure a minimum for baseline operation. - listening_tcp = max(MIN_TCP_LISTEN_SOCKETS, raw_tcp_listen) + listening_tcp = max(MIN_TCP_LISTEN_SOCKETS, sc.tcp_listen) # TCP_SND_BUF: ESPAsyncWebServer allocates malloc(tcp_sndbuf()) per # response chunk. At 10×MSS=14.6KB (BK default) this causes OOM (#14095). @@ -399,20 +397,20 @@ def _configure_lwip(config: dict) -> None: if CORE.is_bk72xx: lwip_opts.append("PBUF_POOL_SIZE=10") - tcp_min = " (min)" if tcp_sockets > raw_tcp else "" - udp_min = " (min)" if udp_sockets > raw_udp else "" - listen_min = " (min)" if listening_tcp > raw_tcp_listen else "" + tcp_min = " (min)" if tcp_sockets > sc.tcp else "" + udp_min = " (min)" if udp_sockets > sc.udp else "" + listen_min = " (min)" if listening_tcp > sc.tcp_listen else "" _LOGGER.info( "Configuring lwIP: TCP=%d%s [%s], UDP=%d%s [%s], TCP_LISTEN=%d%s [%s]", tcp_sockets, tcp_min, - tcp_details, + sc.tcp_details, udp_sockets, udp_min, - udp_details, + sc.udp_details, listening_tcp, listen_min, - tcp_listen_details, + sc.tcp_listen_details, ) cg.add_platformio_option("custom_options.lwip", lwip_opts) diff --git a/esphome/components/socket/__init__.py b/esphome/components/socket/__init__.py index 572e7993b90..d82f0c7abad 100644 --- a/esphome/components/socket/__init__.py +++ b/esphome/components/socket/__init__.py @@ -1,4 +1,5 @@ from collections.abc import Callable, MutableMapping +from dataclasses import dataclass from enum import StrEnum import logging @@ -77,10 +78,20 @@ def _format_consumers(consumers: dict[str, int]) -> str: return ", ".join(f"{name}={count}" for name, count in sorted(consumers.items())) -def get_socket_counts() -> tuple[int, int, int, str, str, str]: - """Return socket counts and component details for platform configuration. +@dataclass(frozen=True) +class SocketCounts: + """Socket counts and component details for platform configuration.""" - Returns (tcp, udp, tcp_listen, tcp_details, udp_details, tcp_listen_details). + tcp: int + udp: int + tcp_listen: int + tcp_details: str + udp_details: str + tcp_listen_details: str + + +def get_socket_counts() -> SocketCounts: + """Return socket counts and component details for platform configuration. Platforms call this during code generation to configure lwIP socket limits. All components will have registered their needs by then. @@ -106,7 +117,9 @@ def get_socket_counts() -> tuple[int, int, int, str, str, str]: tcp_listen, tcp_listen_details, ) - return tcp, udp, tcp_listen, tcp_details, udp_details, tcp_listen_details + return SocketCounts( + tcp, udp, tcp_listen, tcp_details, udp_details, tcp_listen_details + ) def require_wake_loop_threadsafe() -> None: