[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:
J. Nick Koston
2026-02-22 18:52:46 -06:00
committed by GitHub
parent b539a5aa51
commit ded457c2c1
13 changed files with 327 additions and 59 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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(

View File

@@ -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

View File

@@ -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

View File

@@ -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,
)

View File

@@ -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,
)

View File

@@ -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