[core] Split wake.{h,cpp} into per-platform files (#15978)

This commit is contained in:
J. Nick Koston
2026-04-28 08:48:13 -05:00
committed by GitHub
parent 8921e3bb3f
commit 0759a3c681
17 changed files with 632 additions and 303 deletions

View File

@@ -792,6 +792,29 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform(
PlatformFramework.RTL87XX_ARDUINO,
PlatformFramework.LN882X_ARDUINO,
},
# Per-platform wake implementations — wake.h dispatches to exactly one of
# these based on USE_*, so the others can be skipped at the source level
# too. Header files next to each .cpp are always copied (the dispatcher
# #include's them) but compile to empty TUs on the wrong platform anyway.
"wake/wake_freertos.cpp": {
PlatformFramework.ESP32_ARDUINO,
PlatformFramework.ESP32_IDF,
PlatformFramework.BK72XX_ARDUINO,
PlatformFramework.RTL87XX_ARDUINO,
PlatformFramework.LN882X_ARDUINO,
},
"wake/wake_esp8266.cpp": {
PlatformFramework.ESP8266_ARDUINO,
},
"wake/wake_rp2040.cpp": {
PlatformFramework.RP2040_ARDUINO,
},
"wake/wake_host.cpp": {
PlatformFramework.HOST_NATIVE,
},
"wake/wake_generic.cpp": {
PlatformFramework.NRF52_ZEPHYR,
},
# Note: lock_free_queue.h and event_pool.h are header files and don't need to be filtered
# as they are only included when needed by the preprocessor
}

View File

@@ -17,8 +17,21 @@
#define ESPHOME_DEBUG_SCHEDULER
#define ESPHOME_DEBUG_API
// Default threading model for static analysis (ESP32 is multi-threaded with atomics)
// Threading model for static analysis. Match what the real codegen picks per
// platform (see esphome/components/<platform>/__init__.py ThreadModel.*):
// USE_ESP8266 / USE_RP2040 / USE_NRF52 → SINGLE
// USE_BK72XX (ARMv5TE, no LDREX/STREX) → MULTI_NO_ATOMICS
// everything else (ESP32, host, RTL87XX, LN882X) → MULTI_ATOMICS
// Without this the clang-tidy envs end up with USE_<single-threaded platform>
// + MULTI_ATOMICS simultaneously, a combination that can never occur in a
// real build.
#if defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_NRF52)
#define ESPHOME_THREAD_SINGLE
#elif defined(USE_BK72XX)
#define ESPHOME_THREAD_MULTI_NO_ATOMICS
#else
#define ESPHOME_THREAD_MULTI_ATOMICS
#endif
// logger
#define ESPHOME_LOG_LEVEL ESPHOME_LOG_LEVEL_VERY_VERBOSE

View File

@@ -22,8 +22,8 @@ static const char *const TAG = "time_64";
#ifdef ESPHOME_THREAD_SINGLE
// Storage for Millis64Impl inline compute() — defined here so all TUs share one copy.
uint32_t Millis64Impl::last_millis_{0};
uint16_t Millis64Impl::millis_major_{0};
uint32_t Millis64Impl::last_millis{0};
uint16_t Millis64Impl::millis_major{0};
#else
uint64_t Millis64Impl::compute(uint32_t now) {

View File

@@ -21,8 +21,8 @@ class Millis64Impl {
#ifdef ESPHOME_THREAD_SINGLE
// Storage defined in time_64.cpp — declared here so the inline body can access them.
static uint32_t last_millis_;
static uint16_t millis_major_;
static uint32_t last_millis;
static uint16_t millis_major;
static inline uint64_t ESPHOME_ALWAYS_INLINE compute(uint32_t now) {
// Half the 32-bit range - used to detect rollovers vs normal time progression
@@ -30,17 +30,17 @@ class Millis64Impl {
// Single-core platforms have no concurrency, so this is a simple implementation
// that just tracks 32-bit rollover (every 49.7 days) without any locking or atomics.
uint16_t major = millis_major_;
uint32_t last = last_millis_;
uint16_t major = millis_major;
uint32_t last = last_millis;
// Check for rollover
if (now < last && (last - now) > HALF_MAX_UINT32) {
millis_major_++;
millis_major++;
major++;
last_millis_ = now;
last_millis = now;
} else if (now > last) {
// Only update if time moved forward
last_millis_ = now;
last_millis = now;
}
// Combine major (high 32 bits) and now (low 32 bits) into 64-bit time

View File

@@ -3,6 +3,10 @@
/// @file wake.h
/// Platform-specific main loop wake primitives.
/// Always available on all platforms — no opt-in needed.
///
/// The public API for callers lives here; the per-platform implementations
/// live under esphome/core/wake/ and are included at the bottom of this file
/// based on the active USE_* platform define.
#include "esphome/core/defines.h"
#include "esphome/core/hal.h"
@@ -11,21 +15,6 @@
#include <atomic>
#endif
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
#include "esphome/core/main_task.h"
#endif
#ifdef USE_ESP8266
#include <coredecls.h>
#elif defined(USE_RP2040)
#include <hardware/sync.h>
#include <pico/time.h>
#endif
#ifdef USE_HOST
#include <sys/select.h>
#include <sys/socket.h>
#endif
namespace esphome {
// === Wake flag for ESP8266/RP2040 ===
@@ -67,184 +56,19 @@ __attribute__((always_inline)) inline bool wake_request_take() {
}
#endif
// === ESP32 / LibreTiny (FreeRTOS) ===
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
/// Wake the main loop from any context (ISR or task).
/// always_inline so callers placed in IRAM keep the whole wake path in IRAM.
__attribute__((always_inline)) inline void wake_main_task_any_context() {
// Set the wake-requested flag BEFORE the task notification so the consumer
// (Application::loop() gate) is guaranteed to see it on its next gate check.
wake_request_set();
if (in_isr_context()) {
BaseType_t px_higher_priority_task_woken = pdFALSE;
esphome_main_task_notify_from_isr(&px_higher_priority_task_woken);
#ifdef portYIELD_FROM_ISR
portYIELD_FROM_ISR(px_higher_priority_task_woken);
#else
// ARM9 FreeRTOS port (BK72xx) does not define portYIELD_FROM_ISR; the IRQ
// exit sequence performs the context switch if one was requested.
(void) px_higher_priority_task_woken;
#endif
} else {
esphome_main_task_notify();
}
}
/// IRAM_ATTR entry points — defined in wake.cpp.
void wake_loop_isrsafe(BaseType_t *px_higher_priority_task_woken);
void wake_loop_any_context();
inline void wake_loop_threadsafe() {
wake_request_set();
esphome_main_task_notify();
}
namespace internal {
inline void ESPHOME_ALWAYS_INLINE wakeable_delay(uint32_t ms) {
// Fast path (with USE_LWIP_FAST_SELECT): FreeRTOS task notifications posted by the lwip
// event_callback wrapper (see lwip_fast_select.c) are the single source of truth for
// socket wake-ups. Every NETCONN_EVT_RCVPLUS posts an xTaskNotifyGive, so any notification
// that lands between wakes keeps the counter non-zero (next ulTaskNotifyTake returns
// immediately) or wakes a blocked Take directly. Additional wake sources:
// wake_loop_threadsafe() from background tasks, and the ms timeout.
if (ms == 0) [[unlikely]] {
yield();
return;
}
ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(ms));
}
} // namespace internal
// === ESP8266 ===
#elif defined(USE_ESP8266)
/// Inline implementation — IRAM callers inline this directly.
inline void ESPHOME_ALWAYS_INLINE wake_loop_impl() {
// Set the wake-requested flag BEFORE esp_schedule so the consumer is
// guaranteed to see it on its next gate check.
wake_request_set();
g_main_loop_woke = true;
esp_schedule();
}
/// IRAM_ATTR entry point for ISR callers — defined in wake.cpp.
void wake_loop_any_context();
/// Non-ISR: always inline.
inline void wake_loop_threadsafe() { wake_loop_impl(); }
/// ISR-safe: no task_woken arg because ESP8266 has no FreeRTOS. Caller must be IRAM_ATTR.
inline void ESPHOME_ALWAYS_INLINE wake_loop_isrsafe() { wake_loop_impl(); }
namespace internal {
inline void ESPHOME_ALWAYS_INLINE wakeable_delay(uint32_t ms) {
if (ms == 0) [[unlikely]] {
delay(0);
return;
}
if (g_main_loop_woke) {
g_main_loop_woke = false;
return;
}
esp_delay(ms, []() { return !g_main_loop_woke; });
}
} // namespace internal
// === RP2040 ===
#elif defined(USE_RP2040)
inline void wake_loop_any_context() {
// Set the wake-requested flag BEFORE the SEV so the consumer is guaranteed
// to see it on its next gate check.
wake_request_set();
g_main_loop_woke = true;
__sev();
}
inline void wake_loop_threadsafe() { wake_loop_any_context(); }
/// RP2040 wakeable delay uses file-scope state (alarm callback + flag) — defined in wake.cpp.
namespace internal {
void wakeable_delay(uint32_t ms);
} // namespace internal
// === Host / Zephyr / other ===
#else
#ifdef USE_HOST
/// Host: wakes select() via UDP loopback socket. Defined in wake.cpp.
void wake_loop_threadsafe();
/// Register a socket file descriptor with the host select() loop. Not
/// thread-safe — main loop only. Returns false if fd is invalid or
/// >= FD_SETSIZE.
bool wake_register_fd(int fd);
/// Unregister a socket file descriptor. Not thread-safe — main loop only.
void wake_unregister_fd(int fd);
/// One-time setup of the loopback wake socket. Called from Application::setup().
void wake_setup();
// wake_fd_ready() and wake_drain_notifications() are defined inline at the
// bottom of this file — they need internal::g_read_fds / g_wake_socket_fd in
// scope, which depend on USE_HOST-only includes pulled in above.
#else
/// Zephyr is currently the only platform without a wake mechanism.
/// wake_loop_threadsafe() is a no-op and wakeable_delay() falls back to delay().
/// TODO: implement proper Zephyr wake using k_poll / k_sem or similar.
inline void wake_loop_threadsafe() {}
#endif
inline void wake_loop_any_context() { wake_loop_threadsafe(); }
namespace internal {
#ifdef USE_HOST
/// Host wakeable_delay uses select() over the registered fds — defined in wake.cpp.
void wakeable_delay(uint32_t ms);
#else
inline void ESPHOME_ALWAYS_INLINE wakeable_delay(uint32_t ms) {
if (ms == 0) [[unlikely]] {
yield();
return;
}
delay(ms);
}
#endif
} // namespace internal
#endif
#ifdef USE_HOST
namespace internal {
// File-scope state owned by wake.cpp. Accessed inline by wake_drain_notifications()
// and wake_fd_ready() so the hot path stays in the header.
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
extern int g_wake_socket_fd;
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
extern fd_set g_read_fds;
} // namespace internal
inline bool ESPHOME_ALWAYS_INLINE wake_fd_ready(int fd) { return FD_ISSET(fd, &internal::g_read_fds); }
// Small buffer for draining wake notification bytes (1 byte sent per wake).
// Sized to drain multiple notifications per recvfrom() without wasting stack.
inline constexpr size_t WAKE_NOTIFY_DRAIN_BUFFER_SIZE = 16;
inline void ESPHOME_ALWAYS_INLINE wake_drain_notifications() {
// Called from main loop to drain any pending wake notifications.
// Must check wake_fd_ready() to avoid blocking on empty socket.
if (internal::g_wake_socket_fd >= 0 && wake_fd_ready(internal::g_wake_socket_fd)) {
char buffer[WAKE_NOTIFY_DRAIN_BUFFER_SIZE];
// Drain all pending notifications with non-blocking reads. Multiple wake events
// may have triggered multiple writes, so drain until EWOULDBLOCK. We control
// both ends of this loopback socket (always 1 byte per wake), so no error
// checking — any error indicates catastrophic system failure.
while (::recvfrom(internal::g_wake_socket_fd, buffer, sizeof(buffer), 0, nullptr, nullptr) > 0) {
}
}
}
#endif // USE_HOST
} // namespace esphome
// Per-platform implementations. Each header re-enters namespace esphome {} and
// guards its body with the matching USE_* check, so only one contributes code
// for the active target.
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
#include "esphome/core/wake/wake_freertos.h"
#elif defined(USE_ESP8266)
#include "esphome/core/wake/wake_esp8266.h"
#elif defined(USE_RP2040)
#include "esphome/core/wake/wake_rp2040.h"
#elif defined(USE_HOST)
#include "esphome/core/wake/wake_host.h"
#else
#include "esphome/core/wake/wake_generic.h"
#endif

View File

@@ -0,0 +1,21 @@
#include "esphome/core/defines.h"
#ifdef USE_ESP8266
#include "esphome/core/hal.h"
#include "esphome/core/wake.h"
namespace esphome {
// === Wake-requested flag + main-loop woke flag storage ===
// ESP8266 is always ESPHOME_THREAD_SINGLE.
// NOLINTBEGIN(cppcoreguidelines-avoid-non-const-global-variables)
volatile uint8_t g_wake_requested = 0;
volatile bool g_main_loop_woke = false;
// NOLINTEND(cppcoreguidelines-avoid-non-const-global-variables)
void IRAM_ATTR wake_loop_any_context() { wake_loop_impl(); }
} // namespace esphome
#endif // USE_ESP8266

View File

@@ -0,0 +1,47 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_ESP8266
#include "esphome/core/hal.h"
#include <coredecls.h>
namespace esphome {
/// Inline implementation — IRAM callers inline this directly.
inline void ESPHOME_ALWAYS_INLINE wake_loop_impl() {
// Set the wake-requested flag BEFORE esp_schedule so the consumer is
// guaranteed to see it on its next gate check.
wake_request_set();
g_main_loop_woke = true;
esp_schedule();
}
/// IRAM_ATTR entry point for ISR callers — defined in wake_esp8266.cpp.
void wake_loop_any_context();
/// Non-ISR: always inline.
inline void wake_loop_threadsafe() { wake_loop_impl(); }
/// ISR-safe: no task_woken arg because ESP8266 has no FreeRTOS. Caller must be IRAM_ATTR.
inline void ESPHOME_ALWAYS_INLINE wake_loop_isrsafe() { wake_loop_impl(); }
namespace internal {
inline void ESPHOME_ALWAYS_INLINE wakeable_delay(uint32_t ms) {
if (ms == 0) [[unlikely]] {
delay(0);
return;
}
if (g_main_loop_woke) {
g_main_loop_woke = false;
return;
}
esp_delay(ms, []() { return !g_main_loop_woke; });
}
} // namespace internal
} // namespace esphome
#endif // USE_ESP8266

View File

@@ -0,0 +1,33 @@
#include "esphome/core/defines.h"
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
#include "esphome/core/hal.h"
#include "esphome/core/wake.h"
namespace esphome {
// === Wake-requested flag storage ===
// ESP32 is always MULTI_ATOMICS; LibreTiny is MULTI_ATOMICS on chips with
// proper atomics (e.g. RTL8720) and MULTI_NO_ATOMICS on others (e.g. BK72XX).
#ifdef ESPHOME_THREAD_MULTI_ATOMICS
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
std::atomic<uint8_t> g_wake_requested{0};
#else
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
volatile uint8_t g_wake_requested = 0;
#endif
void IRAM_ATTR wake_loop_isrsafe(BaseType_t *px_higher_priority_task_woken) {
// ISR-safe: set flag before notify so the wake is visible on the next gate
// check. wake_request_set() is just an aligned 8-bit store / atomic store
// and is safe from IRAM.
wake_request_set();
esphome_main_task_notify_from_isr(px_higher_priority_task_woken);
}
void IRAM_ATTR wake_loop_any_context() { wake_main_task_any_context(); }
} // namespace esphome
#endif // USE_ESP32 || USE_LIBRETINY

View File

@@ -0,0 +1,60 @@
#pragma once
#include "esphome/core/defines.h"
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
#include "esphome/core/hal.h"
#include "esphome/core/main_task.h"
namespace esphome {
/// Wake the main loop from any context (ISR or task).
/// always_inline so callers placed in IRAM keep the whole wake path in IRAM.
__attribute__((always_inline)) inline void wake_main_task_any_context() {
// Set the wake-requested flag BEFORE the task notification so the consumer
// (Application::loop() gate) is guaranteed to see it on its next gate check.
wake_request_set();
if (in_isr_context()) {
BaseType_t px_higher_priority_task_woken = pdFALSE;
esphome_main_task_notify_from_isr(&px_higher_priority_task_woken);
#ifdef portYIELD_FROM_ISR
portYIELD_FROM_ISR(px_higher_priority_task_woken);
#else
// ARM9 FreeRTOS port (BK72xx) does not define portYIELD_FROM_ISR; the IRQ
// exit sequence performs the context switch if one was requested.
(void) px_higher_priority_task_woken;
#endif
} else {
esphome_main_task_notify();
}
}
/// IRAM_ATTR entry points — defined in wake_freertos.cpp.
void wake_loop_isrsafe(BaseType_t *px_higher_priority_task_woken);
void wake_loop_any_context();
inline void wake_loop_threadsafe() {
wake_request_set();
esphome_main_task_notify();
}
namespace internal {
inline void ESPHOME_ALWAYS_INLINE wakeable_delay(uint32_t ms) {
// Fast path (with USE_LWIP_FAST_SELECT): FreeRTOS task notifications posted by the lwip
// event_callback wrapper (see lwip_fast_select.c) are the single source of truth for
// socket wake-ups. Every NETCONN_EVT_RCVPLUS posts an xTaskNotifyGive, so any notification
// that lands between wakes keeps the counter non-zero (next ulTaskNotifyTake returns
// immediately) or wakes a blocked Take directly. Additional wake sources:
// wake_loop_threadsafe() from background tasks, and the ms timeout.
if (ms == 0) [[unlikely]] {
yield();
return;
}
ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(ms));
}
} // namespace internal
} // namespace esphome
#endif // USE_ESP32 || USE_LIBRETINY

View File

@@ -0,0 +1,17 @@
#include "esphome/core/defines.h"
#if !defined(USE_ESP32) && !defined(USE_LIBRETINY) && !defined(USE_ESP8266) && !defined(USE_RP2040) && \
!defined(USE_HOST)
#include "esphome/core/wake.h"
namespace esphome {
// === Wake-requested flag storage ===
// Fallback platforms (currently only Zephyr/NRF52) are ESPHOME_THREAD_SINGLE.
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
volatile uint8_t g_wake_requested = 0;
} // namespace esphome
#endif // fallback guard

View File

@@ -0,0 +1,31 @@
#pragma once
#include "esphome/core/defines.h"
#if !defined(USE_ESP32) && !defined(USE_LIBRETINY) && !defined(USE_ESP8266) && !defined(USE_RP2040) && \
!defined(USE_HOST)
#include "esphome/core/hal.h"
namespace esphome {
/// Zephyr is currently the only platform without a wake mechanism.
/// wake_loop_threadsafe() is a no-op and wakeable_delay() falls back to delay().
/// TODO: implement proper Zephyr wake using k_poll / k_sem or similar.
inline void wake_loop_threadsafe() {}
inline void wake_loop_any_context() { wake_loop_threadsafe(); }
namespace internal {
inline void ESPHOME_ALWAYS_INLINE wakeable_delay(uint32_t ms) {
if (ms == 0) [[unlikely]] {
yield();
return;
}
delay(ms);
}
} // namespace internal
} // namespace esphome
#endif // fallback guard

View File

@@ -1,12 +1,11 @@
#include "esphome/core/wake.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#ifdef USE_ESP8266
#include <coredecls.h>
#endif
#include "esphome/core/defines.h"
#ifdef USE_HOST
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "esphome/core/wake.h"
#include <arpa/inet.h>
#include <cerrno>
#include <fcntl.h>
@@ -15,88 +14,19 @@
#include <sys/socket.h>
#include <unistd.h>
#include <vector>
#endif
namespace esphome {
// === Wake-requested flag storage ===
#ifdef ESPHOME_THREAD_MULTI_ATOMICS
// Host is always ESPHOME_THREAD_MULTI_ATOMICS.
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
std::atomic<uint8_t> g_wake_requested{0};
#else
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
volatile uint8_t g_wake_requested = 0;
#endif
// === ESP32 / LibreTiny — IRAM_ATTR entry points ===
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
void IRAM_ATTR wake_loop_isrsafe(BaseType_t *px_higher_priority_task_woken) {
// ISR-safe: set flag before notify so the wake is visible on the next gate
// check. wake_request_set() is just an aligned 8-bit store / atomic store
// and is safe from IRAM.
wake_request_set();
esphome_main_task_notify_from_isr(px_higher_priority_task_woken);
}
void IRAM_ATTR wake_loop_any_context() { wake_main_task_any_context(); }
#endif
// === ESP8266 / RP2040 ===
#if defined(USE_ESP8266) || defined(USE_RP2040)
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
volatile bool g_main_loop_woke = false;
#endif
#ifdef USE_ESP8266
void IRAM_ATTR wake_loop_any_context() { wake_loop_impl(); }
#endif
// === RP2040 — wakeable_delay (needs file-scope state for alarm callback) ===
#ifdef USE_RP2040
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
static volatile bool s_delay_expired = false;
static int64_t alarm_callback_(alarm_id_t id, void *user_data) {
(void) id;
(void) user_data;
s_delay_expired = true;
__sev();
return 0;
}
namespace internal {
void wakeable_delay(uint32_t ms) {
if (ms == 0) [[unlikely]] {
yield();
return;
}
if (g_main_loop_woke) {
g_main_loop_woke = false;
return;
}
s_delay_expired = false;
alarm_id_t alarm = add_alarm_in_ms(ms, alarm_callback_, nullptr, true);
if (alarm <= 0) {
delay(ms);
return;
}
while (!g_main_loop_woke && !s_delay_expired) {
__wfe();
}
if (!s_delay_expired)
cancel_alarm(alarm);
g_main_loop_woke = false;
}
} // namespace internal
#endif // USE_RP2040
// === Host (UDP loopback socket + select() based fd watcher) ===
#ifdef USE_HOST
static const char *const TAG = "wake";
namespace internal {
// File-scope state — referenced inline by wake_drain_notifications() and
// wake_fd_ready() in wake.h, and by the bodies in this file.
// wake_fd_ready() in wake_host.h, and by the bodies in this file.
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
int g_wake_socket_fd = -1;
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
@@ -271,6 +201,7 @@ void wake_setup() {
return;
}
}
#endif // USE_HOST
} // namespace esphome
#endif // USE_HOST

View File

@@ -0,0 +1,64 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_HOST
#include "esphome/core/hal.h"
#include <sys/select.h>
#include <sys/socket.h>
namespace esphome {
/// Host: wakes select() via UDP loopback socket. Defined in wake_host.cpp.
void wake_loop_threadsafe();
/// Register a socket file descriptor with the host select() loop. Not
/// thread-safe — main loop only. Returns false if fd is invalid or
/// >= FD_SETSIZE.
bool wake_register_fd(int fd);
/// Unregister a socket file descriptor. Not thread-safe — main loop only.
void wake_unregister_fd(int fd);
/// One-time setup of the loopback wake socket. Called from Application::setup().
void wake_setup();
inline void wake_loop_any_context() { wake_loop_threadsafe(); }
namespace internal {
/// Host wakeable_delay uses select() over the registered fds — defined in wake_host.cpp.
void wakeable_delay(uint32_t ms);
// File-scope state owned by wake_host.cpp. Accessed inline by
// wake_drain_notifications() and wake_fd_ready() so the hot path stays in the header.
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
extern int g_wake_socket_fd;
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
extern fd_set g_read_fds;
} // namespace internal
inline bool ESPHOME_ALWAYS_INLINE wake_fd_ready(int fd) { return FD_ISSET(fd, &internal::g_read_fds); }
// Small buffer for draining wake notification bytes (1 byte sent per wake).
// Sized to drain multiple notifications per recvfrom() without wasting stack.
inline constexpr size_t WAKE_NOTIFY_DRAIN_BUFFER_SIZE = 16;
inline void ESPHOME_ALWAYS_INLINE wake_drain_notifications() {
// Called from main loop to drain any pending wake notifications.
// Must check wake_fd_ready() to avoid blocking on empty socket.
if (internal::g_wake_socket_fd >= 0 && wake_fd_ready(internal::g_wake_socket_fd)) {
char buffer[WAKE_NOTIFY_DRAIN_BUFFER_SIZE];
// Drain all pending notifications with non-blocking reads. Multiple wake events
// may have triggered multiple writes, so drain until EWOULDBLOCK. We control
// both ends of this loopback socket (always 1 byte per wake), so no error
// checking — any error indicates catastrophic system failure.
while (::recvfrom(internal::g_wake_socket_fd, buffer, sizeof(buffer), 0, nullptr, nullptr) > 0) {
}
}
}
} // namespace esphome
#endif // USE_HOST

View File

@@ -0,0 +1,58 @@
#include "esphome/core/defines.h"
#ifdef USE_RP2040
#include "esphome/core/hal.h"
#include "esphome/core/wake.h"
#include <hardware/sync.h>
#include <pico/time.h>
namespace esphome {
// === Wake-requested flag + main-loop woke flag storage ===
// RP2040 is always ESPHOME_THREAD_SINGLE.
// NOLINTBEGIN(cppcoreguidelines-avoid-non-const-global-variables)
volatile uint8_t g_wake_requested = 0;
volatile bool g_main_loop_woke = false;
// NOLINTEND(cppcoreguidelines-avoid-non-const-global-variables)
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
static volatile bool s_delay_expired = false;
static int64_t alarm_callback_(alarm_id_t id, void *user_data) {
(void) id;
(void) user_data;
s_delay_expired = true;
__sev();
return 0;
}
namespace internal {
void wakeable_delay(uint32_t ms) {
if (ms == 0) [[unlikely]] {
yield();
return;
}
if (g_main_loop_woke) {
g_main_loop_woke = false;
return;
}
s_delay_expired = false;
alarm_id_t alarm = add_alarm_in_ms(ms, alarm_callback_, nullptr, true);
if (alarm <= 0) {
delay(ms);
return;
}
while (!g_main_loop_woke && !s_delay_expired) {
__wfe();
}
if (!s_delay_expired)
cancel_alarm(alarm);
g_main_loop_woke = false;
}
} // namespace internal
} // namespace esphome
#endif // USE_RP2040

View File

@@ -0,0 +1,31 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_RP2040
#include "esphome/core/hal.h"
#include <hardware/sync.h>
#include <pico/time.h>
namespace esphome {
inline void wake_loop_any_context() {
// Set the wake-requested flag BEFORE the SEV so the consumer is guaranteed
// to see it on its next gate check.
wake_request_set();
g_main_loop_woke = true;
__sev();
}
inline void wake_loop_threadsafe() { wake_loop_any_context(); }
/// RP2040 wakeable delay uses file-scope state (alarm callback + flag) — defined in wake_rp2040.cpp.
namespace internal {
void wakeable_delay(uint32_t ms);
} // namespace internal
} // namespace esphome
#endif // USE_RP2040

View File

@@ -31,8 +31,9 @@ class FileResource:
class ComponentManifest:
def __init__(self, module: ModuleType):
def __init__(self, module: ModuleType, recursive_sources: bool = False):
self.module = module
self.recursive_sources = recursive_sources
@property
def package(self) -> str:
@@ -108,8 +109,10 @@ class ComponentManifest:
def resources(self) -> list[FileResource]:
"""Return a list of all file resources defined in the package of this component.
This will return all cpp source files that are located in the same folder as the
loaded .py file (does not look through subdirectories)
By default only files directly in the package directory are returned. Manifests
constructed with ``recursive_sources=True`` also descend into non-subpackage
subdirectories (subdirectories without an ``__init__.py``), so core code can
live under ``esphome/core/<group>/`` without every component paying the cost.
"""
ret: list[FileResource] = []
@@ -121,23 +124,30 @@ class ComponentManifest:
set(filter_source_files_func()) if filter_source_files_func else set()
)
# Process all resources
for resource in (
r.name
for r in importlib.resources.files(self.package).iterdir()
if r.is_file()
):
if Path(resource).suffix not in SOURCE_FILE_EXTENSIONS:
continue
if not importlib.resources.files(self.package).joinpath(resource).is_file():
# Not a resource = this is a directory (yeah this is confusing)
continue
root = importlib.resources.files(self.package)
# Skip excluded files
if resource in excluded_files:
continue
for child in root.iterdir():
name = child.name
if child.is_file():
if Path(name).suffix not in SOURCE_FILE_EXTENSIONS:
continue
if name in excluded_files:
continue
ret.append(FileResource(self.package, name))
elif self.recursive_sources and child.is_dir() and name != "__pycache__":
# Skip Python subpackages — they load as their own components.
if child.joinpath("__init__.py").is_file():
continue
for sub in child.iterdir():
if not sub.is_file():
continue
if Path(sub.name).suffix not in SOURCE_FILE_EXTENSIONS:
continue
resource = f"{name}/{sub.name}"
if resource in excluded_files:
continue
ret.append(FileResource(self.package, resource))
ret.append(FileResource(self.package, resource))
return ret
@@ -237,7 +247,9 @@ def get_platform(domain: str, platform: str) -> ComponentManifest | None:
_COMPONENT_CACHE: dict[str, ComponentManifest] = {}
CORE_COMPONENTS_PATH = (Path(__file__).parent / "components").resolve()
_COMPONENT_CACHE["esphome"] = ComponentManifest(esphome.core.config)
_COMPONENT_CACHE["esphome"] = ComponentManifest(
esphome.core.config, recursive_sources=True
)
def _replace_component_manifest(domain: str, manifest: ComponentManifest) -> None:

View File

@@ -158,3 +158,167 @@ def test_component_manifest_resources_with_filter_source_files() -> None:
# Verify the correct number of resources
assert len(resources) == 3 # test.cpp, test.h, common.cpp
# ---------------------------------------------------------------------------
# recursive_sources — used only by the core "esphome" manifest so that files
# in esphome/core/<subdir>/*.cpp (e.g. esphome/core/wake/wake_host.cpp) are
# discovered without promoting <subdir>/ to a Python subpackage.
# ---------------------------------------------------------------------------
def _mock_file(filename: str) -> MagicMock:
m = MagicMock()
m.name = filename
m.is_file.return_value = True
m.is_dir.return_value = False
return m
def _mock_dir(dirname: str, children: list, has_init: bool = False) -> MagicMock:
"""Mock a directory entry with an iterdir() and joinpath('__init__.py')."""
d = MagicMock()
d.name = dirname
d.is_file.return_value = False
d.is_dir.return_value = True
d.iterdir.return_value = children
init_marker = MagicMock()
init_marker.is_file.return_value = has_init
d.joinpath.return_value = init_marker
return d
def test_component_manifest_resources_non_recursive_skips_subdirs() -> None:
"""Default (recursive_sources=False) does not descend into subdirectories."""
mock_module = MagicMock()
mock_module.__package__ = "esphome.components.test_component"
# No FILTER_SOURCE_FILES.
del mock_module.FILTER_SOURCE_FILES
manifest = ComponentManifest(mock_module) # recursive_sources defaults to False
top_level = [
_mock_file("top.cpp"),
_mock_dir("subdir", [_mock_file("nested.cpp")]),
]
with patch("importlib.resources.files") as mock_files_func:
pkg = MagicMock()
pkg.iterdir.return_value = top_level
mock_files_func.return_value = pkg
names = [r.resource for r in manifest.resources]
assert names == ["top.cpp"]
def test_component_manifest_resources_recursive_walks_non_subpackage_subdirs() -> None:
"""With recursive_sources=True, a subdir without __init__.py is walked."""
mock_module = MagicMock()
mock_module.__package__ = "esphome.core"
del mock_module.FILTER_SOURCE_FILES
manifest = ComponentManifest(mock_module, recursive_sources=True)
wake_dir = _mock_dir(
"wake",
[
_mock_file("wake_host.cpp"),
_mock_file("wake_host.h"),
_mock_file("README.md"), # wrong suffix, excluded
],
has_init=False,
)
top_level = [
_mock_file("wake.h"),
wake_dir,
]
with patch("importlib.resources.files") as mock_files_func:
pkg = MagicMock()
pkg.iterdir.return_value = top_level
mock_files_func.return_value = pkg
names = sorted(r.resource for r in manifest.resources)
assert names == ["wake.h", "wake/wake_host.cpp", "wake/wake_host.h"]
def test_component_manifest_resources_recursive_skips_subpackages() -> None:
"""Subdirectories that ARE Python subpackages (contain __init__.py) are
skipped even with recursive_sources=True — those load as their own
ComponentManifest and would otherwise be double-counted."""
mock_module = MagicMock()
mock_module.__package__ = "esphome.components.haier"
del mock_module.FILTER_SOURCE_FILES
manifest = ComponentManifest(mock_module, recursive_sources=True)
button_pkg = _mock_dir(
"button",
[_mock_file("self_cleaning.cpp")],
has_init=True, # Python subpackage — must be skipped.
)
top_level = [
_mock_file("haier.cpp"),
button_pkg,
]
with patch("importlib.resources.files") as mock_files_func:
pkg = MagicMock()
pkg.iterdir.return_value = top_level
mock_files_func.return_value = pkg
names = [r.resource for r in manifest.resources]
assert names == ["haier.cpp"]
def test_component_manifest_resources_recursive_skips_pycache() -> None:
"""__pycache__ inside a recursive walk must never be descended into."""
mock_module = MagicMock()
mock_module.__package__ = "esphome.core"
del mock_module.FILTER_SOURCE_FILES
manifest = ComponentManifest(mock_module, recursive_sources=True)
# __pycache__ is_dir=True but must be skipped without checking __init__.py
# or calling iterdir (would yield compiled artifacts).
pycache = _mock_dir("__pycache__", [_mock_file("wake.cpython-314.pyc")])
top_level = [
_mock_file("wake.h"),
pycache,
]
with patch("importlib.resources.files") as mock_files_func:
pkg = MagicMock()
pkg.iterdir.return_value = top_level
mock_files_func.return_value = pkg
names = [r.resource for r in manifest.resources]
assert names == ["wake.h"]
def test_component_manifest_resources_recursive_filter_source_files_supports_subpaths() -> (
None
):
"""FILTER_SOURCE_FILES entries using '/'-joined subpaths exclude files
inside a recursively-walked subdir."""
mock_module = MagicMock()
mock_module.__package__ = "esphome.core"
mock_module.FILTER_SOURCE_FILES = lambda: ["wake/wake_host.cpp"]
manifest = ComponentManifest(mock_module, recursive_sources=True)
wake_dir = _mock_dir(
"wake",
[
_mock_file("wake_host.cpp"), # excluded
_mock_file("wake_freertos.cpp"), # kept
],
)
with patch("importlib.resources.files") as mock_files_func:
pkg = MagicMock()
pkg.iterdir.return_value = [wake_dir]
mock_files_func.return_value = pkg
names = [r.resource for r in manifest.resources]
assert names == ["wake/wake_freertos.cpp"]