diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index f86a90d607..a1645219b1 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -60,6 +60,10 @@ void Component::set_interval(const std::string &name, uint32_t interval, std::fu App.scheduler.set_interval(this, name, interval, std::move(f)); } +void Component::set_interval(const char *name, uint32_t interval, std::function &&f) { // NOLINT + App.scheduler.set_interval(this, name, interval, std::move(f)); +} + bool Component::cancel_interval(const std::string &name) { // NOLINT return App.scheduler.cancel_interval(this, name); } @@ -77,6 +81,10 @@ void Component::set_timeout(const std::string &name, uint32_t timeout, std::func App.scheduler.set_timeout(this, name, timeout, std::move(f)); } +void Component::set_timeout(const char *name, uint32_t timeout, std::function &&f) { // NOLINT + App.scheduler.set_timeout(this, name, timeout, std::move(f)); +} + bool Component::cancel_timeout(const std::string &name) { // NOLINT return App.scheduler.cancel_timeout(this, name); } diff --git a/esphome/core/component.h b/esphome/core/component.h index 7f2bdd8414..900db27e29 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -260,6 +260,22 @@ class Component { */ void set_interval(const std::string &name, uint32_t interval, std::function &&f); // NOLINT + /** Set an interval function with a const char* name. + * + * IMPORTANT: The provided name pointer must remain valid for the lifetime of the scheduler item. + * This means the name should be: + * - A string literal (e.g., "update") + * - A static const char* variable + * - A pointer with lifetime >= the scheduled task + * + * For dynamic strings, use the std::string overload instead. + * + * @param name The identifier for this interval function (must have static lifetime) + * @param interval The interval in ms + * @param f The function to call + */ + void set_interval(const char *name, uint32_t interval, std::function &&f); // NOLINT + void set_interval(uint32_t interval, std::function &&f); // NOLINT /** Cancel an interval function. @@ -328,6 +344,22 @@ class Component { */ void set_timeout(const std::string &name, uint32_t timeout, std::function &&f); // NOLINT + /** Set a timeout function with a const char* name. + * + * IMPORTANT: The provided name pointer must remain valid for the lifetime of the scheduler item. + * This means the name should be: + * - A string literal (e.g., "init") + * - A static const char* variable + * - A pointer with lifetime >= the timeout duration + * + * For dynamic strings, use the std::string overload instead. + * + * @param name The identifier for this timeout function (must have static lifetime) + * @param timeout The timeout in ms + * @param f The function to call + */ + void set_timeout(const char *name, uint32_t timeout, std::function &&f); // NOLINT + void set_timeout(uint32_t timeout, std::function &&f); // NOLINT /** Cancel a timeout function. diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index a701147d32..30c4cb8137 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -17,6 +17,41 @@ static const uint32_t MAX_LOGICALLY_DELETED_ITEMS = 10; // Uncomment to debug scheduler // #define ESPHOME_DEBUG_SCHEDULER +#ifdef ESPHOME_DEBUG_SCHEDULER +// Helper to validate that a pointer looks like it's in static memory +static void validate_static_string(const char *name) { + if (name == nullptr) + return; + + // This is a heuristic check - stack and heap pointers are typically + // much higher in memory than static data + uintptr_t addr = reinterpret_cast(name); + + // Create a stack variable to compare against + int stack_var; + uintptr_t stack_addr = reinterpret_cast(&stack_var); + + // If the string pointer is near our stack variable, it's likely on the stack + // Using 8KB range as ESP32 main task stack is typically 8192 bytes + if (addr > (stack_addr - 0x2000) && addr < (stack_addr + 0x2000)) { + ESP_LOGW(TAG, + "WARNING: Scheduler name '%s' at %p appears to be on the stack - this is unsafe!\n" + " Stack reference at %p", + name, name, &stack_var); + } + + // Also check if it might be on the heap by seeing if it's in a very different range + // This is platform-specific but generally heap is allocated far from static memory + static const char *static_str = "test"; + uintptr_t static_addr = reinterpret_cast(static_str); + + // If the address is very far from known static memory, it might be heap + if (addr > static_addr + 0x100000 || (static_addr > 0x100000 && addr < static_addr - 0x100000)) { + ESP_LOGW(TAG, "WARNING: Scheduler name '%s' at %p might be on heap (static ref at %p)", name, name, static_str); + } +} +#endif + // A note on locking: the `lock_` lock protects the `items_` and `to_add_` containers. It must be taken when writing to // them (i.e. when adding/removing items, but not when changing items). As items are only deleted from the loop task, // iterating over them from the loop task is fine; but iterating from any other context requires the lock to be held to @@ -50,6 +85,12 @@ void HOT Scheduler::set_timeout_impl_(Component *component, const NameType &name item->set_name(name.c_str(), make_copy); } else { item->set_name(name, make_copy); +#ifdef ESPHOME_DEBUG_SCHEDULER + // Validate static strings in debug mode + if (!make_copy && name != nullptr) { + validate_static_string(name); + } +#endif } item->type = SchedulerItem::TIMEOUT; @@ -118,6 +159,12 @@ void HOT Scheduler::set_interval_impl_(Component *component, const NameType &nam item->set_name(name.c_str(), make_copy); } else { item->set_name(name, make_copy); +#ifdef ESPHOME_DEBUG_SCHEDULER + // Validate static strings in debug mode + if (!make_copy && name != nullptr) { + validate_static_string(name); + } +#endif } item->type = SchedulerItem::INTERVAL; @@ -238,9 +285,10 @@ void HOT Scheduler::call() { this->pop_raw_(); this->lock_.unlock(); + const char *name = item->get_name(); ESP_LOGD(TAG, " %s '%s/%s' interval=%" PRIu32 " next_execution in %" PRIu64 "ms at %" PRIu64, - item->get_type_str(), item->get_source(), item->get_name(), item->interval, item->next_execution_ - now, - item->next_execution_); + item->get_type_str(), item->get_source(), name ? name : "(null)", item->interval, + item->next_execution_ - now, item->next_execution_); old_items.push_back(std::move(item)); } diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index ca437e690c..73940d7fff 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -14,10 +14,33 @@ class Scheduler { public: // Public API - accepts std::string for backward compatibility void set_timeout(Component *component, const std::string &name, uint32_t timeout, std::function func); + + /** Set a timeout with a const char* name. + * + * IMPORTANT: The provided name pointer must remain valid for the lifetime of the scheduler item. + * This means the name should be: + * - A string literal (e.g., "update") + * - A static const char* variable + * - A pointer with lifetime >= the scheduled task + * + * For dynamic strings, use the std::string overload instead. + */ void set_timeout(Component *component, const char *name, uint32_t timeout, std::function func); bool cancel_timeout(Component *component, const std::string &name); + void set_interval(Component *component, const std::string &name, uint32_t interval, std::function func); + + /** Set an interval with a const char* name. + * + * IMPORTANT: The provided name pointer must remain valid for the lifetime of the scheduler item. + * This means the name should be: + * - A string literal (e.g., "update") + * - A static const char* variable + * - A pointer with lifetime >= the scheduled task + * + * For dynamic strings, use the std::string overload instead. + */ void set_interval(Component *component, const char *name, uint32_t interval, std::function func); bool cancel_interval(Component *component, const std::string &name);