diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index 4633c202a0..68ad64b9b1 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -94,13 +94,16 @@ class Scheduler { char *dynamic_name; // For allocated strings } name_; uint32_t interval; - // Split 64-bit time to handle millis() rollover. The scheduler combines the 32-bit millis() - // with a 16-bit rollover counter to create a 64-bit time that won't roll over for - // billions of years. This ensures correct scheduling even when devices run for months. - // Split into two fields for better memory alignment on 32-bit systems. - uint32_t next_execution_low_; // Lower 32 bits of next execution time + // Split time to handle millis() rollover. The scheduler combines the 32-bit millis() + // with a 16-bit rollover counter to create a 48-bit time space (using 32+16 bits). + // This is intentionally limited to 48 bits, not stored as a full 64-bit value. + // With 49.7 days per 32-bit rollover, the 16-bit counter supports + // 49.7 days × 65536 = ~8900 years. This ensures correct scheduling + // even when devices run for months. Split into two fields for better memory + // alignment on 32-bit systems. + uint32_t next_execution_low_; // Lower 32 bits of execution time (millis value) std::function callback; - uint16_t next_execution_high_; // Upper 16 bits of next execution time + uint16_t next_execution_high_; // Upper 16 bits (millis_major counter) #ifdef ESPHOME_THREAD_MULTI_ATOMICS // Multi-threaded with atomics: use atomic for lock-free access @@ -154,7 +157,7 @@ class Scheduler { SchedulerItem &operator=(SchedulerItem &&) = delete; // Helper to get the name regardless of storage type - constexpr const char *get_name() const { return name_is_dynamic ? name_.dynamic_name : name_.static_name; } + const char *get_name() const { return name_is_dynamic ? name_.dynamic_name : name_.static_name; } // Helper to clear dynamic name if allocated void clear_dynamic_name() { @@ -187,13 +190,17 @@ class Scheduler { static bool cmp(const std::unique_ptr &a, const std::unique_ptr &b); - // Helper methods to work with split execution time (constexpr for optimization) + // Note: We use 48 bits total (32 + 16), stored in a 64-bit value for API compatibility. + // The upper 16 bits of the 64-bit value are always zero, which is fine since + // millis_major_ is also 16 bits and they must match. constexpr uint64_t get_next_execution() const { return (static_cast(next_execution_high_) << 32) | next_execution_low_; } constexpr void set_next_execution(uint64_t value) { next_execution_low_ = static_cast(value); + // Cast to uint16_t intentionally truncates to lower 16 bits of the upper 32 bits. + // This is correct because millis_major_ that creates these values is also 16 bits. next_execution_high_ = static_cast(value >> 32); } constexpr const char *get_type_str() const { return (type == TIMEOUT) ? "timeout" : "interval"; }