This commit is contained in:
J. Nick Koston
2026-02-23 21:20:38 -06:00
parent 3fd24b779c
commit 29416061ea
3 changed files with 55 additions and 15 deletions

View File

@@ -147,14 +147,15 @@ void Application::setup() {
clear_setup_priority_overrides();
#endif
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
#ifdef USE_ESP32
// Initialize fast select: saves main loop task handle for xTaskNotifyGive wake
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_ESP32)
// Initialize fast select: saves main loop task handle for xTaskNotifyGive wake.
// Always init on ESP32 — the fast path (rcvevent reads + ulTaskNotifyTake) is used
// unconditionally when USE_SOCKET_SELECT_SUPPORT is enabled.
esphome_lwip_fast_select_init();
#else
// Set up wake socket for waking main loop from tasks
this->setup_wake_loop_threadsafe_();
#endif
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_ESP32)
// Set up wake socket for waking main loop from tasks (non-ESP32 only)
this->setup_wake_loop_threadsafe_();
#endif
this->schedule_dump_config();
@@ -642,7 +643,9 @@ void Application::yield_with_select_(uint32_t delay_ms) {
ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(delay_ms));
#elif defined(USE_SOCKET_SELECT_SUPPORT)
// Non-ESP32 platforms (LibreTiny bk72xx/rtl87xx): use select()
// Non-ESP32 select() path (LibreTiny bk72xx/rtl87xx, host platform).
// ESP32 is excluded by the #if above — both BSD_SOCKETS and LWIP_SOCKETS on ESP32
// use LwIP under the hood, so the fast path handles all ESP32 socket implementations.
if (!this->socket_fds_.empty()) [[likely]] {
// Update fd_set if socket list has changed
if (this->socket_fds_changed_) [[unlikely]] {
@@ -700,7 +703,7 @@ Application App; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
#ifdef USE_ESP32
void Application::wake_loop_threadsafe() {
// Direct FreeRTOS task notification — ISR-safe, <1 us
// Direct FreeRTOS task notification — <1 us, task context only (NOT ISR-safe)
esphome_lwip_wake_main_loop();
}
#else // !USE_ESP32

View File

@@ -130,11 +130,17 @@ void esphome_lwip_hook_socket(int fd) {
if (sock == NULL || sock->conn == NULL)
return;
// Save original callback once — all LwIP sockets share the same event_callback.
// Save original callback once — all LwIP sockets share the same static event_callback
// (DEFAULT_SOCKET_EVENTCB in sockets.c, used for SOCK_RAW, SOCK_DGRAM, and SOCK_STREAM).
if (s_original_callback == NULL) {
s_original_callback = sock->conn->callback;
}
// Verify assumption: if we already have the original, this socket should have the same one.
// If this fires, LwIP changed to use per-socket callbacks and we need a per-fd array.
LWIP_ASSERT("all sockets must share the same event_callback",
sock->conn->callback == s_original_callback || sock->conn->callback == esphome_socket_event_callback);
// Replace with our wrapper. Atomic on ESP32 (32-bit aligned pointer write).
// TCP/IP thread sees either old or new pointer — both are valid.
sock->conn->callback = esphome_socket_event_callback;

View File

@@ -1,16 +1,21 @@
from esphome.components import socket
from esphome.const import KEY_CORE, KEY_TARGET_PLATFORM, PLATFORM_ESP8266
from esphome.const import (
KEY_CORE,
KEY_TARGET_PLATFORM,
PLATFORM_ESP32,
PLATFORM_ESP8266,
)
from esphome.core import CORE
def _setup_non_esp32_platform() -> None:
"""Set up CORE.data with a non-ESP32 platform for testing."""
CORE.data[KEY_CORE] = {KEY_TARGET_PLATFORM: PLATFORM_ESP8266}
def _setup_platform(platform=PLATFORM_ESP8266) -> None:
"""Set up CORE.data with a platform for testing."""
CORE.data[KEY_CORE] = {KEY_TARGET_PLATFORM: platform}
def test_require_wake_loop_threadsafe__first_call() -> None:
"""Test that first call sets up define and consumes socket."""
_setup_non_esp32_platform()
_setup_platform()
CORE.config = {"wifi": True}
socket.require_wake_loop_threadsafe()
@@ -39,7 +44,7 @@ def test_require_wake_loop_threadsafe__idempotent() -> None:
def test_require_wake_loop_threadsafe__multiple_calls() -> None:
"""Test that multiple calls only set up once."""
_setup_non_esp32_platform()
_setup_platform()
# Call three times
CORE.config = {"openthread": True}
socket.require_wake_loop_threadsafe()
@@ -83,3 +88,29 @@ def test_require_wake_loop_threadsafe__no_networking_does_not_consume_socket() -
udp_consumers = CORE.data.get(socket.KEY_SOCKET_CONSUMERS_UDP, {})
assert "socket.wake_loop_threadsafe" not in udp_consumers
assert udp_consumers == initial_udp
def test_require_wake_loop_threadsafe__esp32_no_udp_socket() -> None:
"""Test that ESP32 uses task notifications instead of UDP socket."""
_setup_platform(PLATFORM_ESP32)
CORE.config = {"wifi": True}
socket.require_wake_loop_threadsafe()
# Verify the define was added
assert CORE.data[socket.KEY_WAKE_LOOP_THREADSAFE_REQUIRED] is True
assert any(d.name == "USE_WAKE_LOOP_THREADSAFE" for d in CORE.defines)
# Verify no UDP socket was consumed (ESP32 uses FreeRTOS task notifications)
udp_consumers = CORE.data.get(socket.KEY_SOCKET_CONSUMERS_UDP, {})
assert "socket.wake_loop_threadsafe" not in udp_consumers
def test_require_wake_loop_threadsafe__non_esp32_consumes_udp_socket() -> None:
"""Test that non-ESP32 platforms consume a UDP socket for wake notifications."""
_setup_platform(PLATFORM_ESP8266)
CORE.config = {"wifi": True}
socket.require_wake_loop_threadsafe()
# Verify UDP socket was consumed
udp_consumers = CORE.data.get(socket.KEY_SOCKET_CONSUMERS_UDP, {})
assert udp_consumers.get("socket.wake_loop_threadsafe") == 1