From 9d7f606a39cb8e1def666e28b201e5e0b555338d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 3 Sep 2025 16:34:47 -0500 Subject: [PATCH] explain why its safe --- esphome/core/scheduler.h | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index 4633c202a0..1743557339 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -94,13 +94,15 @@ 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 (stored as 64-bit + // for compatibility). 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 @@ -188,12 +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"; }