mirror of
https://github.com/esphome/esphome.git
synced 2026-02-25 12:55:30 -07:00
[libretiny] Tune oversized lwIP defaults for ESPHome (#14186)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -233,8 +233,8 @@ def _consume_api_sockets(config: ConfigType) -> ConfigType:
|
||||
|
||||
# API needs 1 listening socket + typically 3 concurrent client connections
|
||||
# (not max_connections, which is the upper limit rarely reached)
|
||||
sockets_needed = 1 + 3
|
||||
socket.consume_sockets(sockets_needed, "api")(config)
|
||||
socket.consume_sockets(3, "api")(config)
|
||||
socket.consume_sockets(1, "api", socket.SocketType.TCP_LISTEN)(config)
|
||||
return config
|
||||
|
||||
|
||||
|
||||
@@ -76,13 +76,15 @@ def _final_validate(config: ConfigType) -> ConfigType:
|
||||
|
||||
# Register socket needs for DNS server and additional HTTP connections
|
||||
# - 1 UDP socket for DNS server
|
||||
# - 3 additional TCP sockets for captive portal detection probes + configuration requests
|
||||
# - 3 TCP sockets for captive portal detection probes + configuration requests
|
||||
# OS captive portal detection makes multiple probe requests that stay in TIME_WAIT.
|
||||
# Need headroom for actual user configuration requests.
|
||||
# LRU purging will reclaim idle sockets to prevent exhaustion from repeated attempts.
|
||||
# The listening socket is registered by web_server_base (shared HTTP server).
|
||||
from esphome.components import socket
|
||||
|
||||
socket.consume_sockets(4, "captive_portal")(config)
|
||||
socket.consume_sockets(3, "captive_portal")(config)
|
||||
socket.consume_sockets(1, "captive_portal", socket.SocketType.UDP)(config)
|
||||
|
||||
return config
|
||||
|
||||
|
||||
@@ -1258,21 +1258,15 @@ def _configure_lwip_max_sockets(conf: dict) -> None:
|
||||
This function runs in to_code() after all components have registered their socket needs.
|
||||
User-provided sdkconfig_options take precedence.
|
||||
"""
|
||||
from esphome.components.socket import KEY_SOCKET_CONSUMERS
|
||||
from esphome.components.socket import get_socket_counts
|
||||
|
||||
# Check if user manually specified CONFIG_LWIP_MAX_SOCKETS
|
||||
user_max_sockets = conf[CONF_SDKCONFIG_OPTIONS].get("CONFIG_LWIP_MAX_SOCKETS")
|
||||
|
||||
socket_consumers: dict[str, int] = CORE.data.get(KEY_SOCKET_CONSUMERS, {})
|
||||
total_sockets = sum(socket_consumers.values())
|
||||
|
||||
# Early return if no sockets registered and no user override
|
||||
if total_sockets == 0 and user_max_sockets is None:
|
||||
return
|
||||
|
||||
components_list = ", ".join(
|
||||
f"{name}={count}" for name, count in sorted(socket_consumers.items())
|
||||
)
|
||||
# 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.
|
||||
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:
|
||||
@@ -1281,22 +1275,23 @@ def _configure_lwip_max_sockets(conf: dict) -> None:
|
||||
user_max_sockets,
|
||||
)
|
||||
|
||||
# Warn if user's value is less than what components need
|
||||
if total_sockets > 0:
|
||||
user_sockets_int = 0
|
||||
with contextlib.suppress(ValueError, TypeError):
|
||||
user_sockets_int = int(user_max_sockets)
|
||||
user_sockets_int = 0
|
||||
with contextlib.suppress(ValueError, TypeError):
|
||||
user_sockets_int = int(user_max_sockets)
|
||||
|
||||
if user_sockets_int < total_sockets:
|
||||
_LOGGER.warning(
|
||||
"CONFIG_LWIP_MAX_SOCKETS is set to %d but your configuration "
|
||||
"needs %d sockets (registered: %s). You may experience socket "
|
||||
"exhaustion errors. Consider increasing to at least %d.",
|
||||
user_sockets_int,
|
||||
total_sockets,
|
||||
components_list,
|
||||
total_sockets,
|
||||
)
|
||||
if user_sockets_int < total_sockets:
|
||||
_LOGGER.warning(
|
||||
"CONFIG_LWIP_MAX_SOCKETS is set to %d but your configuration "
|
||||
"needs %d sockets (%d TCP + %d UDP + %d TCP_LISTEN). You may "
|
||||
"experience socket exhaustion errors. Consider increasing to "
|
||||
"at least %d.",
|
||||
user_sockets_int,
|
||||
total_sockets,
|
||||
sc.tcp,
|
||||
sc.udp,
|
||||
sc.tcp_listen,
|
||||
total_sockets,
|
||||
)
|
||||
# User's value already added via sdkconfig_options processing
|
||||
return
|
||||
|
||||
@@ -1305,11 +1300,19 @@ def _configure_lwip_max_sockets(conf: dict) -> None:
|
||||
max_sockets = max(DEFAULT_MAX_SOCKETS, total_sockets)
|
||||
|
||||
log_level = logging.INFO if max_sockets > DEFAULT_MAX_SOCKETS else logging.DEBUG
|
||||
sock_min = " (min)" if max_sockets > total_sockets else ""
|
||||
_LOGGER.log(
|
||||
log_level,
|
||||
"Setting CONFIG_LWIP_MAX_SOCKETS to %d (registered: %s)",
|
||||
"Setting CONFIG_LWIP_MAX_SOCKETS to %d%s "
|
||||
"(TCP=%d [%s], UDP=%d [%s], TCP_LISTEN=%d [%s])",
|
||||
max_sockets,
|
||||
components_list,
|
||||
sock_min,
|
||||
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)
|
||||
|
||||
@@ -20,8 +20,10 @@ def _consume_camera_web_server_sockets(config: ConfigType) -> ConfigType:
|
||||
from esphome.components import socket
|
||||
|
||||
# Each camera web server instance needs 1 listening socket + 2 client connections
|
||||
sockets_needed = 3
|
||||
socket.consume_sockets(sockets_needed, "esp32_camera_web_server")(config)
|
||||
socket.consume_sockets(2, "esp32_camera_web_server")(config)
|
||||
socket.consume_sockets(1, "esp32_camera_web_server", socket.SocketType.TCP_LISTEN)(
|
||||
config
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
|
||||
@@ -97,8 +97,9 @@ def _consume_ota_sockets(config: ConfigType) -> ConfigType:
|
||||
"""Register socket needs for OTA component."""
|
||||
from esphome.components import socket
|
||||
|
||||
# OTA needs 1 listening socket (client connections are temporary during updates)
|
||||
socket.consume_sockets(1, "ota")(config)
|
||||
# OTA needs 1 listening socket. The active transfer connection during an update
|
||||
# uses a TCP PCB from the general pool, covered by MIN_TCP_SOCKETS headroom.
|
||||
socket.consume_sockets(1, "ota", socket.SocketType.TCP_LISTEN)(config)
|
||||
return config
|
||||
|
||||
|
||||
|
||||
@@ -275,6 +275,146 @@ BASE_SCHEMA.add_extra(_detect_variant)
|
||||
BASE_SCHEMA.add_extra(_update_core_data)
|
||||
|
||||
|
||||
def _configure_lwip(config: dict) -> None:
|
||||
"""Configure lwIP options for LibreTiny platforms.
|
||||
|
||||
The BK/RTL/LN SDKs each ship different lwIP defaults. BK72XX defaults are
|
||||
wildly oversized for ESPHome's IoT use case, causing OOM on BK7231N.
|
||||
RTL87XX and LN882H have more conservative defaults but still need tuning
|
||||
for ESPHome's socket usage patterns.
|
||||
|
||||
See https://github.com/esphome/esphome/issues/14095
|
||||
|
||||
Comparison of SDK defaults vs ESPHome targets (TCP_MSS=1460 on all LT):
|
||||
|
||||
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 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
|
||||
PBUF_POOL_SIZE 10 16 3/10 20 20 10 BK
|
||||
MAX_SOCKETS_TCP 5 16 12 —** —** dynamic
|
||||
MAX_SOCKETS_UDP 4 16 22 —** —** dynamic
|
||||
TCP_SND_QUEUELEN ~8 17 20 20 35 17
|
||||
MEMP_NUM_TCP_SEG 10 16 40 20 =qlen 17
|
||||
MEMP_NUM_TCP_PCB 5 16 12 10 8 =TCP
|
||||
MEMP_NUM_TCP_PCB_LISTEN 4 16 4 5 3 dynamic
|
||||
MEMP_NUM_UDP_PCB 4 16 25*** 7**** 7**** =UDP
|
||||
MEMP_NUM_NETCONN 0 10 38 4***** =sum =sum
|
||||
MEMP_NUM_NETBUF 0 2 16 2***** 8 4
|
||||
MEMP_NUM_TCPIP_MSG_INPKT 4 8 16 8***** 12 8
|
||||
|
||||
* ESP8266/ESP32/LN882H use MEM_LIBC_MALLOC=1 (system heap, no dedicated pool).
|
||||
ESP8266/ESP32 also use MEMP_MEM_MALLOC=1 (MEMP pools from heap, not static).
|
||||
** RTL/LN SDKs don't define MAX_SOCKETS_TCP/UDP (LibreTiny-specific).
|
||||
*** BK LT overlay: MAX_SOCKETS_UDP+2+1 = 25.
|
||||
**** 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 8 TCP / 6 UDP.
|
||||
"""
|
||||
from esphome.components.socket import (
|
||||
MIN_TCP_LISTEN_SOCKETS,
|
||||
MIN_TCP_SOCKETS,
|
||||
MIN_UDP_SOCKETS,
|
||||
get_socket_counts,
|
||||
)
|
||||
|
||||
sc = get_socket_counts()
|
||||
# Apply platform minimums — ensure headroom for ESPHome's needs
|
||||
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, 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).
|
||||
# 4×MSS=5,840 matches ESP32. RTL(5×) and LN(7×) are close already.
|
||||
tcp_snd_buf = "(4*TCP_MSS)" # BK: 10×MSS, RTL: 5×MSS, LN: 7×MSS
|
||||
|
||||
# TCP_WND: receive window. 4×MSS matches ESP32.
|
||||
# RTL SDK uses only 2×MSS; increasing to 4× is safe and improves throughput.
|
||||
tcp_wnd = "(4*TCP_MSS)" # BK: 10×MSS, RTL: 2×MSS, LN: 3×MSS
|
||||
|
||||
# TCP_SND_QUEUELEN: max pbufs queued for send buffer
|
||||
# ESP-IDF formula: (4 * TCP_SND_BUF + (TCP_MSS - 1)) / TCP_MSS
|
||||
# With 4×MSS: (4*5840 + 1459) / 1460 = 17 — match ESP32
|
||||
tcp_snd_queuelen = 17 # BK: 20, RTL: 20, LN: 35
|
||||
# MEMP_NUM_TCP_SEG: segment pool, must be >= TCP_SND_QUEUELEN (lwIP sanity check)
|
||||
memp_num_tcp_seg = tcp_snd_queuelen # BK: 40, RTL: 20, LN: =qlen
|
||||
|
||||
lwip_opts: list[str] = [
|
||||
# Disable statistics — not needed for production, saves RAM
|
||||
"LWIP_STATS=0", # BK: 1, RTL: 0 already, LN: 0 already
|
||||
"MEM_STATS=0",
|
||||
"MEMP_STATS=0",
|
||||
# TCP send buffer — 4×MSS matches ESP32
|
||||
f"TCP_SND_BUF={tcp_snd_buf}",
|
||||
# TCP receive window — 4×MSS matches ESP32
|
||||
f"TCP_WND={tcp_wnd}",
|
||||
# Socket counts — auto-calculated from component registrations
|
||||
f"MAX_SOCKETS_TCP={tcp_sockets}",
|
||||
f"MAX_SOCKETS_UDP={udp_sockets}",
|
||||
# Listening sockets — BK SDK uses this to derive MEMP_NUM_TCP_PCB_LISTEN;
|
||||
# RTL/LN don't use it, but we set MEMP_NUM_TCP_PCB_LISTEN explicitly below.
|
||||
f"MAX_LISTENING_SOCKETS_TCP={listening_tcp}",
|
||||
# Queued segment limits — derived from 4×MSS buffer size
|
||||
f"TCP_SND_QUEUELEN={tcp_snd_queuelen}",
|
||||
f"MEMP_NUM_TCP_SEG={memp_num_tcp_seg}", # must be >= queuelen
|
||||
# PCB pools — active connections + listening sockets
|
||||
f"MEMP_NUM_TCP_PCB={tcp_sockets}", # BK: 12, RTL: 10, LN: 8
|
||||
f"MEMP_NUM_TCP_PCB_LISTEN={listening_tcp}", # BK: =MAX_LISTENING, RTL: 5, LN: 3
|
||||
# UDP PCB pool — includes wifi.lwip_internal (DHCP + DNS)
|
||||
f"MEMP_NUM_UDP_PCB={udp_sockets}", # BK: 25, RTL/LN: 7 via LT
|
||||
# Netconn pool — each socket (active + listening) needs a netconn
|
||||
f"MEMP_NUM_NETCONN={tcp_sockets + udp_sockets + listening_tcp}",
|
||||
# Netbuf pool
|
||||
"MEMP_NUM_NETBUF=4", # BK: 16, RTL: 2 (opt.h), LN: 8
|
||||
# Inbound message pool
|
||||
"MEMP_NUM_TCPIP_MSG_INPKT=8", # BK: 16, RTL: 8 (opt.h), LN: 12
|
||||
]
|
||||
|
||||
# Use system heap for all lwIP allocations on all LibreTiny platforms.
|
||||
# - MEM_LIBC_MALLOC=1: Use system heap instead of dedicated lwIP heap.
|
||||
# LN882H already ships with this. BK SDK defaults to a 16/32KB dedicated
|
||||
# pool that fragments during OTA. RTL SDK defaults to a 5KB pool.
|
||||
# All three SDKs wire malloc → pvPortMalloc (FreeRTOS thread-safe heap).
|
||||
# - MEMP_MEM_MALLOC=1: Allocate MEMP pools from heap on demand instead
|
||||
# of static arrays. Saves ~20KB RAM on BK72XX. Safe because WiFi
|
||||
# receive paths run in task context, not ISR context. ESP32 and ESP8266
|
||||
# both ship with MEMP_MEM_MALLOC=1.
|
||||
lwip_opts.append("MEM_LIBC_MALLOC=1")
|
||||
lwip_opts.append("MEMP_MEM_MALLOC=1")
|
||||
|
||||
# BK72XX-specific: PBUF_POOL_SIZE override
|
||||
# BK SDK "reduced plan" sets this to only 3 — too few for multiple
|
||||
# concurrent connections (API + web_server + OTA). BK default plan
|
||||
# uses 10; match that. RTL(20) and LN(20) need no override.
|
||||
# With MEMP_MEM_MALLOC=1, this is a max count (allocated on demand).
|
||||
if CORE.is_bk72xx:
|
||||
lwip_opts.append("PBUF_POOL_SIZE=10")
|
||||
|
||||
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,
|
||||
sc.tcp_details,
|
||||
udp_sockets,
|
||||
udp_min,
|
||||
sc.udp_details,
|
||||
listening_tcp,
|
||||
listen_min,
|
||||
sc.tcp_listen_details,
|
||||
)
|
||||
cg.add_platformio_option("custom_options.lwip", lwip_opts)
|
||||
|
||||
|
||||
# pylint: disable=use-dict-literal
|
||||
async def component_to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
@@ -389,11 +529,12 @@ async def component_to_code(config):
|
||||
"custom_options.sys_config#h", _BK7231N_SYS_CONFIG_OPTIONS
|
||||
)
|
||||
|
||||
# Disable LWIP statistics to save RAM - not needed in production
|
||||
# Must explicitly disable all sub-stats to avoid redefinition warnings
|
||||
cg.add_platformio_option(
|
||||
"custom_options.lwip",
|
||||
["LWIP_STATS=0", "MEM_STATS=0", "MEMP_STATS=0"],
|
||||
)
|
||||
# Tune lwIP for ESPHome's actual needs.
|
||||
# The SDK defaults (TCP_SND_BUF=10*MSS, MAX_SOCKETS_TCP=12, MEM_SIZE=32KB)
|
||||
# are wildly oversized for an IoT device. ESPAsyncWebServer allocates
|
||||
# malloc(tcp_sndbuf()) per response chunk — at 14.6KB this causes silent
|
||||
# OOM on memory-constrained chips like BK7231N.
|
||||
# See https://github.com/esphome/esphome/issues/14095
|
||||
_configure_lwip(config)
|
||||
|
||||
await cg.register_component(var, config)
|
||||
|
||||
@@ -56,7 +56,7 @@ def _consume_mdns_sockets(config: ConfigType) -> ConfigType:
|
||||
from esphome.components import socket
|
||||
|
||||
# mDNS needs 2 sockets (IPv4 + IPv6 multicast)
|
||||
socket.consume_sockets(2, "mdns")(config)
|
||||
socket.consume_sockets(2, "mdns", socket.SocketType.UDP)(config)
|
||||
return config
|
||||
|
||||
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
from collections.abc import Callable, MutableMapping
|
||||
from dataclasses import dataclass
|
||||
from enum import StrEnum
|
||||
import logging
|
||||
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.core import CORE
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
|
||||
CONF_IMPLEMENTATION = "implementation"
|
||||
@@ -13,33 +18,110 @@ IMPLEMENTATION_BSD_SOCKETS = "bsd_sockets"
|
||||
|
||||
# Socket tracking infrastructure
|
||||
# Components register their socket needs and platforms read this to configure appropriately
|
||||
KEY_SOCKET_CONSUMERS = "socket_consumers"
|
||||
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.
|
||||
# Platforms should apply these (or their own) on top of get_socket_counts().
|
||||
# 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"
|
||||
|
||||
|
||||
class SocketType(StrEnum):
|
||||
TCP = "tcp"
|
||||
UDP = "udp"
|
||||
TCP_LISTEN = "tcp_listen"
|
||||
|
||||
|
||||
_SOCKET_TYPE_KEYS = {
|
||||
SocketType.TCP: KEY_SOCKET_CONSUMERS_TCP,
|
||||
SocketType.UDP: KEY_SOCKET_CONSUMERS_UDP,
|
||||
SocketType.TCP_LISTEN: KEY_SOCKET_CONSUMERS_TCP_LISTEN,
|
||||
}
|
||||
|
||||
|
||||
def consume_sockets(
|
||||
value: int, consumer: str
|
||||
value: int, consumer: str, socket_type: SocketType = SocketType.TCP
|
||||
) -> Callable[[MutableMapping], MutableMapping]:
|
||||
"""Register socket usage for a component.
|
||||
|
||||
Args:
|
||||
value: Number of sockets needed by the component
|
||||
consumer: Name of the component consuming the sockets
|
||||
socket_type: Type of socket (SocketType.TCP, SocketType.UDP, or SocketType.TCP_LISTEN)
|
||||
|
||||
Returns:
|
||||
A validator function that records the socket usage
|
||||
"""
|
||||
typed_key = _SOCKET_TYPE_KEYS[socket_type]
|
||||
|
||||
def _consume_sockets(config: MutableMapping) -> MutableMapping:
|
||||
consumers: dict[str, int] = CORE.data.setdefault(KEY_SOCKET_CONSUMERS, {})
|
||||
consumers: dict[str, int] = CORE.data.setdefault(typed_key, {})
|
||||
consumers[consumer] = consumers.get(consumer, 0) + value
|
||||
return config
|
||||
|
||||
return _consume_sockets
|
||||
|
||||
|
||||
def _format_consumers(consumers: dict[str, int]) -> str:
|
||||
"""Format consumer dict as 'name=count, ...' or 'none'."""
|
||||
if not consumers:
|
||||
return "none"
|
||||
return ", ".join(f"{name}={count}" for name, count in sorted(consumers.items()))
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SocketCounts:
|
||||
"""Socket counts and component details for platform configuration."""
|
||||
|
||||
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.
|
||||
|
||||
Platforms should apply their own minimums on top of these values.
|
||||
"""
|
||||
tcp_consumers = CORE.data.get(KEY_SOCKET_CONSUMERS_TCP, {})
|
||||
udp_consumers = CORE.data.get(KEY_SOCKET_CONSUMERS_UDP, {})
|
||||
tcp_listen_consumers = CORE.data.get(KEY_SOCKET_CONSUMERS_TCP_LISTEN, {})
|
||||
tcp = sum(tcp_consumers.values())
|
||||
udp = sum(udp_consumers.values())
|
||||
tcp_listen = sum(tcp_listen_consumers.values())
|
||||
|
||||
tcp_details = _format_consumers(tcp_consumers)
|
||||
udp_details = _format_consumers(udp_consumers)
|
||||
tcp_listen_details = _format_consumers(tcp_listen_consumers)
|
||||
_LOGGER.debug(
|
||||
"Socket counts: TCP=%d (%s), UDP=%d (%s), TCP_LISTEN=%d (%s)",
|
||||
tcp,
|
||||
tcp_details,
|
||||
udp,
|
||||
udp_details,
|
||||
tcp_listen,
|
||||
tcp_listen_details,
|
||||
)
|
||||
return SocketCounts(
|
||||
tcp, udp, tcp_listen, tcp_details, udp_details, tcp_listen_details
|
||||
)
|
||||
|
||||
|
||||
def require_wake_loop_threadsafe() -> None:
|
||||
"""Mark that wake_loop_threadsafe support is required by a component.
|
||||
|
||||
@@ -66,7 +148,7 @@ def require_wake_loop_threadsafe() -> None:
|
||||
CORE.data[KEY_WAKE_LOOP_THREADSAFE_REQUIRED] = True
|
||||
cg.add_define("USE_WAKE_LOOP_THREADSAFE")
|
||||
# Consume 1 socket for the shared wake notification socket
|
||||
consume_sockets(1, "socket.wake_loop_threadsafe")({})
|
||||
consume_sockets(1, "socket.wake_loop_threadsafe", SocketType.UDP)({})
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
|
||||
@@ -73,7 +73,7 @@ def _consume_udp_sockets(config: ConfigType) -> ConfigType:
|
||||
|
||||
# UDP uses up to 2 sockets: 1 broadcast + 1 listen
|
||||
# Whether each is used depends on code generation, so register worst case
|
||||
socket.consume_sockets(2, "udp")(config)
|
||||
socket.consume_sockets(2, "udp", socket.SocketType.UDP)(config)
|
||||
return config
|
||||
|
||||
|
||||
|
||||
@@ -144,11 +144,11 @@ def _consume_web_server_sockets(config: ConfigType) -> ConfigType:
|
||||
"""Register socket needs for web_server component."""
|
||||
from esphome.components import socket
|
||||
|
||||
# Web server needs 1 listening socket + typically 5 concurrent client connections
|
||||
# Web server needs typically 5 concurrent client connections
|
||||
# (browser opens connections for page resources, SSE event stream, and POST
|
||||
# requests for entity control which may linger before closing)
|
||||
sockets_needed = 6
|
||||
socket.consume_sockets(sockets_needed, "web_server")(config)
|
||||
# The listening socket is registered by web_server_base (shared with captive_portal)
|
||||
socket.consume_sockets(5, "web_server")(config)
|
||||
return config
|
||||
|
||||
|
||||
|
||||
@@ -23,10 +23,27 @@ web_server_base_ns = cg.esphome_ns.namespace("web_server_base")
|
||||
WebServerBase = web_server_base_ns.class_("WebServerBase")
|
||||
|
||||
CONF_WEB_SERVER_BASE_ID = "web_server_base_id"
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(WebServerBase),
|
||||
}
|
||||
|
||||
|
||||
def _consume_web_server_base_sockets(config):
|
||||
"""Register the shared listening socket for the HTTP server.
|
||||
|
||||
web_server_base is the shared HTTP server used by web_server and captive_portal.
|
||||
The listening socket is registered here rather than in each consumer.
|
||||
"""
|
||||
from esphome.components import socket
|
||||
|
||||
socket.consume_sockets(1, "web_server_base", socket.SocketType.TCP_LISTEN)(config)
|
||||
return config
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(WebServerBase),
|
||||
}
|
||||
),
|
||||
_consume_web_server_base_sockets,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -58,6 +58,7 @@ from esphome.const import (
|
||||
)
|
||||
from esphome.core import CORE, CoroPriority, HexInt, coroutine_with_priority
|
||||
import esphome.final_validate as fv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import wpa2_eap
|
||||
|
||||
@@ -269,9 +270,28 @@ def final_validate(config):
|
||||
)
|
||||
|
||||
|
||||
def _consume_wifi_sockets(config: ConfigType) -> ConfigType:
|
||||
"""Register UDP PCBs used internally by lwIP for DHCP and DNS.
|
||||
|
||||
Only needed on LibreTiny where we directly set MEMP_NUM_UDP_PCB (the raw
|
||||
PCB pool shared by both application sockets and lwIP internals like DHCP/DNS).
|
||||
On ESP32, CONFIG_LWIP_MAX_SOCKETS only controls the POSIX socket layer —
|
||||
DHCP/DNS use raw udp_new() which bypasses it entirely.
|
||||
"""
|
||||
if not (CORE.is_bk72xx or CORE.is_rtl87xx or CORE.is_ln882x):
|
||||
return config
|
||||
from esphome.components import socket
|
||||
|
||||
# lwIP allocates UDP PCBs for DHCP client and DNS resolver internally.
|
||||
# These are not application sockets but consume MEMP_NUM_UDP_PCB pool entries.
|
||||
socket.consume_sockets(2, "wifi.lwip_internal", socket.SocketType.UDP)(config)
|
||||
return config
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = cv.All(
|
||||
final_validate,
|
||||
validate_variant,
|
||||
_consume_wifi_sockets,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -66,12 +66,12 @@ def test_require_wake_loop_threadsafe__no_networking_does_not_consume_socket() -
|
||||
CORE.config = {"logger": {}}
|
||||
|
||||
# Track initial socket consumer state
|
||||
initial_consumers = CORE.data.get(socket.KEY_SOCKET_CONSUMERS, {})
|
||||
initial_udp = CORE.data.get(socket.KEY_SOCKET_CONSUMERS_UDP, {})
|
||||
|
||||
# Call require_wake_loop_threadsafe
|
||||
socket.require_wake_loop_threadsafe()
|
||||
|
||||
# Verify no socket was consumed
|
||||
consumers = CORE.data.get(socket.KEY_SOCKET_CONSUMERS, {})
|
||||
assert "socket.wake_loop_threadsafe" not in consumers
|
||||
assert consumers == initial_consumers
|
||||
udp_consumers = CORE.data.get(socket.KEY_SOCKET_CONSUMERS_UDP, {})
|
||||
assert "socket.wake_loop_threadsafe" not in udp_consumers
|
||||
assert udp_consumers == initial_udp
|
||||
|
||||
Reference in New Issue
Block a user