mirror of
https://github.com/esphome/esphome.git
synced 2026-06-24 13:45:15 +00:00
[core] Split wake.{h,cpp} into per-platform files (#15978)
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
21
esphome/core/wake/wake_esp8266.cpp
Normal file
21
esphome/core/wake/wake_esp8266.cpp
Normal 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
|
||||
47
esphome/core/wake/wake_esp8266.h
Normal file
47
esphome/core/wake/wake_esp8266.h
Normal 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
|
||||
33
esphome/core/wake/wake_freertos.cpp
Normal file
33
esphome/core/wake/wake_freertos.cpp
Normal 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
|
||||
60
esphome/core/wake/wake_freertos.h
Normal file
60
esphome/core/wake/wake_freertos.h
Normal 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
|
||||
17
esphome/core/wake/wake_generic.cpp
Normal file
17
esphome/core/wake/wake_generic.cpp
Normal 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
|
||||
31
esphome/core/wake/wake_generic.h
Normal file
31
esphome/core/wake/wake_generic.h
Normal 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
|
||||
@@ -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
|
||||
64
esphome/core/wake/wake_host.h
Normal file
64
esphome/core/wake/wake_host.h
Normal 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
|
||||
58
esphome/core/wake/wake_rp2040.cpp
Normal file
58
esphome/core/wake/wake_rp2040.cpp
Normal 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
|
||||
31
esphome/core/wake/wake_rp2040.h
Normal file
31
esphome/core/wake/wake_rp2040.h
Normal 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
|
||||
@@ -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:
|
||||
|
||||
@@ -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"]
|
||||
|
||||
Reference in New Issue
Block a user