diff --git a/esphome/components/esp32/core.cpp b/esphome/components/esp32/core.cpp index 202d929ab9..b9ae871abf 100644 --- a/esphome/components/esp32/core.cpp +++ b/esphome/components/esp32/core.cpp @@ -23,6 +23,7 @@ namespace esphome { void HOT yield() { vPortYield(); } uint32_t IRAM_ATTR HOT millis() { return (uint32_t) (esp_timer_get_time() / 1000ULL); } +uint64_t HOT millis_64() { return static_cast(esp_timer_get_time()) / 1000ULL; } void HOT delay(uint32_t ms) { vTaskDelay(ms / portTICK_PERIOD_MS); } uint32_t IRAM_ATTR HOT micros() { return (uint32_t) esp_timer_get_time(); } void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); } diff --git a/esphome/components/esp8266/core.cpp b/esphome/components/esp8266/core.cpp index 236d3022be..497e99b61f 100644 --- a/esphome/components/esp8266/core.cpp +++ b/esphome/components/esp8266/core.cpp @@ -3,6 +3,7 @@ #include "core.h" #include "esphome/core/defines.h" #include "esphome/core/hal.h" +#include "esphome/core/application.h" #include "esphome/core/helpers.h" #include "preferences.h" #include @@ -16,6 +17,7 @@ namespace esphome { void HOT yield() { ::yield(); } uint32_t IRAM_ATTR HOT millis() { return ::millis(); } +uint64_t millis_64() { return App.scheduler.millis_64_impl_(::millis()); } void HOT delay(uint32_t ms) { ::delay(ms); } uint32_t IRAM_ATTR HOT micros() { return ::micros(); } void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); } diff --git a/esphome/components/host/core.cpp b/esphome/components/host/core.cpp index c20a33fa37..5c08717ac9 100644 --- a/esphome/components/host/core.cpp +++ b/esphome/components/host/core.cpp @@ -1,5 +1,6 @@ #ifdef USE_HOST +#include "esphome/core/application.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "preferences.h" @@ -19,6 +20,7 @@ uint32_t IRAM_ATTR HOT millis() { uint32_t ms = round(spec.tv_nsec / 1e6); return ((uint32_t) seconds) * 1000U + ms; } +uint64_t millis_64() { return App.scheduler.millis_64_impl_(millis()); } void HOT delay(uint32_t ms) { struct timespec ts; ts.tv_sec = ms / 1000; diff --git a/esphome/components/libretiny/core.cpp b/esphome/components/libretiny/core.cpp index 4dda7c3856..6cbc81938d 100644 --- a/esphome/components/libretiny/core.cpp +++ b/esphome/components/libretiny/core.cpp @@ -3,6 +3,7 @@ #include "core.h" #include "esphome/core/defines.h" #include "esphome/core/hal.h" +#include "esphome/core/application.h" #include "esphome/core/helpers.h" #include "preferences.h" @@ -13,6 +14,7 @@ namespace esphome { void HOT yield() { ::yield(); } uint32_t IRAM_ATTR HOT millis() { return ::millis(); } +uint64_t millis_64() { return App.scheduler.millis_64_impl_(::millis()); } uint32_t IRAM_ATTR HOT micros() { return ::micros(); } void HOT delay(uint32_t ms) { ::delay(ms); } void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { ::delayMicroseconds(us); } diff --git a/esphome/components/rp2040/core.cpp b/esphome/components/rp2040/core.cpp index 37378d88bb..52c6f1185c 100644 --- a/esphome/components/rp2040/core.cpp +++ b/esphome/components/rp2040/core.cpp @@ -3,6 +3,7 @@ #include "core.h" #include "esphome/core/defines.h" #include "esphome/core/hal.h" +#include "esphome/core/application.h" #include "esphome/core/helpers.h" #include "hardware/watchdog.h" @@ -11,6 +12,7 @@ namespace esphome { void HOT yield() { ::yield(); } uint32_t IRAM_ATTR HOT millis() { return ::millis(); } +uint64_t millis_64() { return App.scheduler.millis_64_impl_(::millis()); } void HOT delay(uint32_t ms) { ::delay(ms); } uint32_t IRAM_ATTR HOT micros() { return ::micros(); } void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); } diff --git a/esphome/components/uptime/sensor/uptime_seconds_sensor.cpp b/esphome/components/uptime/sensor/uptime_seconds_sensor.cpp index 20e8ed8fda..552e4e5da4 100644 --- a/esphome/components/uptime/sensor/uptime_seconds_sensor.cpp +++ b/esphome/components/uptime/sensor/uptime_seconds_sensor.cpp @@ -1,6 +1,6 @@ #include "uptime_seconds_sensor.h" -#include "esphome/core/application.h" +#include "esphome/core/hal.h" #include "esphome/core/log.h" namespace esphome::uptime { @@ -8,7 +8,7 @@ namespace esphome::uptime { static const char *const TAG = "uptime.sensor"; void UptimeSecondsSensor::update() { - const uint64_t uptime = App.scheduler.millis_64(); + const uint64_t uptime = millis_64(); const uint64_t seconds_int = uptime / 1000ULL; const float seconds = float(seconds_int) + (uptime % 1000ULL) / 1000.0f; this->publish_state(seconds); diff --git a/esphome/components/uptime/text_sensor/uptime_text_sensor.cpp b/esphome/components/uptime/text_sensor/uptime_text_sensor.cpp index 88ae53fbfc..c89d23672e 100644 --- a/esphome/components/uptime/text_sensor/uptime_text_sensor.cpp +++ b/esphome/components/uptime/text_sensor/uptime_text_sensor.cpp @@ -1,6 +1,6 @@ #include "uptime_text_sensor.h" -#include "esphome/core/application.h" +#include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" @@ -19,7 +19,7 @@ static void append_unit(char *buf, size_t buf_size, size_t &pos, const char *sep void UptimeTextSensor::setup() { this->update(); } void UptimeTextSensor::update() { - uint32_t uptime = static_cast(App.scheduler.millis_64() / 1000); + uint32_t uptime = static_cast(millis_64() / 1000); unsigned interval = this->get_update_interval() / 1000; // Calculate all time units diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index e2f9c21331..16674321c9 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -385,7 +385,7 @@ json::SerializationBuffer<> WebServer::get_config_json() { #endif root[ESPHOME_F("log")] = this->expose_log_; root[ESPHOME_F("lang")] = "en"; - root[ESPHOME_F("uptime")] = static_cast(App.scheduler.millis_64() / 1000); + root[ESPHOME_F("uptime")] = static_cast(millis_64() / 1000); return builder.serialize(); } @@ -414,7 +414,7 @@ void WebServer::setup() { // getting a lot of events this->set_interval(10000, [this]() { char buf[32]; - auto uptime = static_cast(App.scheduler.millis_64() / 1000); + auto uptime = static_cast(millis_64() / 1000); buf_append_printf(buf, sizeof(buf), 0, "{\"uptime\":%u}", uptime); this->events_.try_send_nodefer(buf, "ping", millis(), 30000); }); diff --git a/esphome/components/zephyr/core.cpp b/esphome/components/zephyr/core.cpp index d7027b33f5..7f5d6d44fa 100644 --- a/esphome/components/zephyr/core.cpp +++ b/esphome/components/zephyr/core.cpp @@ -4,6 +4,7 @@ #include #include #include +#include "esphome/core/application.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "esphome/core/defines.h" @@ -17,6 +18,7 @@ static const device *const WDT = DEVICE_DT_GET(DT_ALIAS(watchdog0)); void yield() { ::k_yield(); } uint32_t millis() { return k_ticks_to_ms_floor32(k_uptime_ticks()); } +uint64_t millis_64() { return App.scheduler.millis_64_impl_(millis()); } uint32_t micros() { return k_ticks_to_us_floor32(k_uptime_ticks()); } void delayMicroseconds(uint32_t us) { ::k_usleep(us); } void delay(uint32_t ms) { ::k_msleep(ms); } diff --git a/esphome/core/hal.h b/esphome/core/hal.h index 1a4230e421..fa5ca646f2 100644 --- a/esphome/core/hal.h +++ b/esphome/core/hal.h @@ -32,6 +32,7 @@ namespace esphome { void yield(); uint32_t millis(); +uint64_t millis_64(); uint32_t micros(); void delay(uint32_t ms); void delayMicroseconds(uint32_t us); // NOLINT(readability-identifier-naming) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index d9c66b2000..06c0bb8b1b 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -28,8 +28,10 @@ static constexpr size_t MAX_POOL_SIZE = 5; // Set to 5 to match the pool size - when we have as many cancelled items as our // pool can hold, it's time to clean up and recycle them. static constexpr uint32_t MAX_LOGICALLY_DELETED_ITEMS = 5; +#ifndef USE_ESP32 // Half the 32-bit range - used to detect rollovers vs normal time progression static constexpr uint32_t HALF_MAX_UINT32 = std::numeric_limits::max() / 2; +#endif // max delay to start an interval sequence static constexpr uint32_t MAX_INTERVAL_DELAY = 5000; @@ -150,8 +152,8 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type return; } - // Get fresh timestamp BEFORE taking lock - millis_64_ may need to acquire lock itself - const uint64_t now = this->millis_64_(millis()); + // Get fresh 64-bit timestamp BEFORE taking lock + const uint64_t now_64 = millis_64(); // Take lock early to protect scheduler_item_pool_ access LockGuard guard{this->lock_}; @@ -184,7 +186,7 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type item->interval = delay; // first execution happens immediately after a random smallish offset uint32_t offset = this->calculate_interval_offset_(delay); - item->set_next_execution(now + offset); + item->set_next_execution(now_64 + offset); #ifdef ESPHOME_LOG_HAS_VERBOSE SchedulerNameLog name_log; ESP_LOGV(TAG, "Scheduler interval for %s is %" PRIu32 "ms, offset %" PRIu32 "ms", @@ -192,11 +194,11 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type #endif } else { item->interval = 0; - item->set_next_execution(now + delay); + item->set_next_execution(now_64 + delay); } #ifdef ESPHOME_DEBUG_SCHEDULER - this->debug_log_timer_(item.get(), name_type, static_name, hash_or_id, type, delay, now); + this->debug_log_timer_(item.get(), name_type, static_name, hash_or_id, type, delay, now_64); #endif /* ESPHOME_DEBUG_SCHEDULER */ // For retries, check if there's a cancelled timeout first @@ -399,8 +401,7 @@ optional HOT Scheduler::next_schedule_in(uint32_t now) { return {}; auto &item = this->items_[0]; - // Convert the fresh timestamp from caller (usually Application::loop()) to 64-bit - const auto now_64 = this->millis_64_(now); // 'now' from parameter - fresh from caller + const auto now_64 = this->millis_64_from_(now); const uint64_t next_exec = item->get_next_execution(); if (next_exec < now_64) return 0; @@ -461,8 +462,8 @@ void HOT Scheduler::call(uint32_t now) { this->process_defer_queue_(now); #endif /* not ESPHOME_THREAD_SINGLE */ - // Convert the fresh timestamp from main loop to 64-bit for scheduler operations - const auto now_64 = this->millis_64_(now); // 'now' from parameter - fresh from Application::loop() + // Extend the caller's 32-bit timestamp to 64-bit for scheduler operations + const auto now_64 = this->millis_64_from_(now); this->process_to_add(); // Track if any items were added to to_add_ during this call (intervals or from callbacks) @@ -474,15 +475,18 @@ void HOT Scheduler::call(uint32_t now) { if (now_64 - last_print > 2000) { last_print = now_64; std::vector old_items; -#ifdef ESPHOME_THREAD_MULTI_ATOMICS +#if !defined(USE_ESP32) && defined(ESPHOME_THREAD_MULTI_ATOMICS) const auto last_dbg = this->last_millis_.load(std::memory_order_relaxed); const auto major_dbg = this->millis_major_.load(std::memory_order_relaxed); ESP_LOGD(TAG, "Items: count=%zu, pool=%zu, now=%" PRIu64 " (%" PRIu16 ", %" PRIu32 ")", this->items_.size(), this->scheduler_item_pool_.size(), now_64, major_dbg, last_dbg); -#else /* not ESPHOME_THREAD_MULTI_ATOMICS */ +#elif !defined(USE_ESP32) ESP_LOGD(TAG, "Items: count=%zu, pool=%zu, now=%" PRIu64 " (%" PRIu16 ", %" PRIu32 ")", this->items_.size(), this->scheduler_item_pool_.size(), now_64, this->millis_major_, this->last_millis_); -#endif /* else ESPHOME_THREAD_MULTI_ATOMICS */ +#else + ESP_LOGD(TAG, "Items: count=%zu, pool=%zu, now=%" PRIu64, this->items_.size(), this->scheduler_item_pool_.size(), + now_64); +#endif // Cleanup before debug output this->cleanup_(); while (!this->items_.empty()) { @@ -710,9 +714,8 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, NameType name_type return total_cancelled > 0; } -uint64_t Scheduler::millis_64() { return this->millis_64_(millis()); } - -uint64_t Scheduler::millis_64_(uint32_t now) { +#ifndef USE_ESP32 +uint64_t Scheduler::millis_64_impl_(uint32_t now) { // THREAD SAFETY NOTE: // This function has three implementations, based on the precompiler flags // - ESPHOME_THREAD_SINGLE - Runs on single-threaded platforms (ESP8266, RP2040, etc.) @@ -869,6 +872,7 @@ uint64_t Scheduler::millis_64_(uint32_t now) { "No platform threading model defined. One of ESPHOME_THREAD_SINGLE, ESPHOME_THREAD_MULTI_NO_ATOMICS, or ESPHOME_THREAD_MULTI_ATOMICS must be defined." #endif } +#endif // not USE_ESP32 bool HOT Scheduler::SchedulerItem::cmp(const SchedulerItemPtr &a, const SchedulerItemPtr &b) { // High bits are almost always equal (change only on 32-bit rollover ~49 days) diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index 840ee7159a..c605325810 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -10,6 +10,7 @@ #endif #include "esphome/core/component.h" +#include "esphome/core/hal.h" #include "esphome/core/helpers.h" namespace esphome { @@ -117,12 +118,12 @@ class Scheduler { bool cancel_retry(Component *component, uint32_t id); /// Get 64-bit millisecond timestamp (handles 32-bit millis() rollover) - uint64_t millis_64(); + uint64_t millis_64() { return esphome::millis_64(); } - // Calculate when the next scheduled item should run - // @param now Fresh timestamp from millis() - must not be stale/cached - // Returns the time in milliseconds until the next scheduled item, or nullopt if no items - // This method performs cleanup of removed items before checking the schedule + // Calculate when the next scheduled item should run. + // @param now On ESP32, unused for 64-bit extension (native); on other platforms, extended to 64-bit via rollover. + // Returns the time in milliseconds until the next scheduled item, or nullopt if no items. + // This method performs cleanup of removed items before checking the schedule. // IMPORTANT: This method should only be called from the main thread (loop task). optional next_schedule_in(uint32_t now); @@ -282,7 +283,25 @@ class Scheduler { // Common implementation for cancel_retry bool cancel_retry_(Component *component, NameType name_type, const char *static_name, uint32_t hash_or_id); - uint64_t millis_64_(uint32_t now); + // Extend a 32-bit millis() value to 64-bit. Use when the caller already has a fresh now. + // On ESP32, ignores now and uses esp_timer_get_time() directly (native 64-bit). + // On non-ESP32, extends now to 64-bit using rollover tracking. + uint64_t millis_64_from_(uint32_t now) { +#ifdef USE_ESP32 + (void) now; + return millis_64(); +#else + return this->millis_64_impl_(now); +#endif + } + +#ifndef USE_ESP32 + // On non-ESP32 platforms, millis_64() HAL function delegates to this method + // which tracks 32-bit millis() rollover using millis_major_ and last_millis_. + // On ESP32, millis_64() uses esp_timer_get_time() directly. + friend uint64_t millis_64(); + uint64_t millis_64_impl_(uint32_t now); +#endif // Cleanup logically deleted items from the scheduler // Returns the number of items remaining after cleanup // IMPORTANT: This method should only be called from the main thread (loop task). @@ -549,6 +568,9 @@ class Scheduler { // to synchronize between tasks (see https://github.com/esphome/backlog/issues/52) std::vector scheduler_item_pool_; +#ifndef USE_ESP32 + // On ESP32, millis_64() uses esp_timer_get_time() directly; no rollover tracking needed. + // On other platforms, these fields track 32-bit millis() rollover for millis_64_impl_(). #ifdef ESPHOME_THREAD_MULTI_ATOMICS /* * Multi-threaded platforms with atomic support: last_millis_ needs atomic for lock-free updates @@ -577,6 +599,7 @@ class Scheduler { #else /* not ESPHOME_THREAD_MULTI_ATOMICS */ uint16_t millis_major_{0}; #endif /* else ESPHOME_THREAD_MULTI_ATOMICS */ +#endif /* not USE_ESP32 */ }; } // namespace esphome