mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 15:12:06 +00:00 
			
		
		
		
	Merge branch 'scheduler_retiny' into integration
This commit is contained in:
		
							
								
								
									
										1
									
								
								.github/PULL_REQUEST_TEMPLATE.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/PULL_REQUEST_TEMPLATE.md
									
									
									
									
										vendored
									
									
								
							| @@ -26,6 +26,7 @@ | ||||
| - [ ] RP2040 | ||||
| - [ ] BK72xx | ||||
| - [ ] RTL87xx | ||||
| - [ ] nRF52840 | ||||
|  | ||||
| ## Example entry for `config.yaml`: | ||||
|  | ||||
|   | ||||
| @@ -17,6 +17,7 @@ from esphome.const import ( | ||||
|     CONF_MODE, | ||||
|     CONF_NUMBER, | ||||
|     CONF_ON_VALUE, | ||||
|     CONF_SWITCH, | ||||
|     CONF_TEXT, | ||||
|     CONF_TRIGGER_ID, | ||||
|     CONF_TYPE, | ||||
| @@ -33,7 +34,6 @@ CONF_LABEL = "label" | ||||
| CONF_MENU = "menu" | ||||
| CONF_BACK = "back" | ||||
| CONF_SELECT = "select" | ||||
| CONF_SWITCH = "switch" | ||||
| CONF_ON_TEXT = "on_text" | ||||
| CONF_OFF_TEXT = "off_text" | ||||
| CONF_VALUE_LAMBDA = "value_lambda" | ||||
|   | ||||
| @@ -1,9 +1,9 @@ | ||||
| from esphome.const import CONF_SWITCH | ||||
|  | ||||
| from ..defines import CONF_INDICATOR, CONF_KNOB, CONF_MAIN | ||||
| from ..types import LvBoolean | ||||
| from . import WidgetType | ||||
|  | ||||
| CONF_SWITCH = "switch" | ||||
|  | ||||
|  | ||||
| class SwitchType(WidgetType): | ||||
|     def __init__(self): | ||||
|   | ||||
| @@ -8,6 +8,7 @@ | ||||
| #include "esphome/core/log.h" | ||||
| #include "esphome/core/time.h" | ||||
| #include "esphome/components/network/util.h" | ||||
| #include "esphome/core/helpers.h" | ||||
|  | ||||
| #include <esp_wireguard.h> | ||||
| #include <esp_wireguard_err.h> | ||||
| @@ -42,7 +43,10 @@ void Wireguard::setup() { | ||||
|  | ||||
|   this->publish_enabled_state(); | ||||
|  | ||||
|   this->wg_initialized_ = esp_wireguard_init(&(this->wg_config_), &(this->wg_ctx_)); | ||||
|   { | ||||
|     LwIPLock lock; | ||||
|     this->wg_initialized_ = esp_wireguard_init(&(this->wg_config_), &(this->wg_ctx_)); | ||||
|   } | ||||
|  | ||||
|   if (this->wg_initialized_ == ESP_OK) { | ||||
|     ESP_LOGI(TAG, "Initialized"); | ||||
| @@ -249,7 +253,10 @@ void Wireguard::start_connection_() { | ||||
|   } | ||||
|  | ||||
|   ESP_LOGD(TAG, "Starting connection"); | ||||
|   this->wg_connected_ = esp_wireguard_connect(&(this->wg_ctx_)); | ||||
|   { | ||||
|     LwIPLock lock; | ||||
|     this->wg_connected_ = esp_wireguard_connect(&(this->wg_ctx_)); | ||||
|   } | ||||
|  | ||||
|   if (this->wg_connected_ == ESP_OK) { | ||||
|     ESP_LOGI(TAG, "Connection started"); | ||||
| @@ -280,7 +287,10 @@ void Wireguard::start_connection_() { | ||||
| void Wireguard::stop_connection_() { | ||||
|   if (this->wg_initialized_ == ESP_OK && this->wg_connected_ == ESP_OK) { | ||||
|     ESP_LOGD(TAG, "Stopping connection"); | ||||
|     esp_wireguard_disconnect(&(this->wg_ctx_)); | ||||
|     { | ||||
|       LwIPLock lock; | ||||
|       esp_wireguard_disconnect(&(this->wg_ctx_)); | ||||
|     } | ||||
|     this->wg_connected_ = ESP_FAIL; | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -922,6 +922,7 @@ CONF_SWING_MODE_COMMAND_TOPIC = "swing_mode_command_topic" | ||||
| CONF_SWING_MODE_STATE_TOPIC = "swing_mode_state_topic" | ||||
| CONF_SWING_OFF_ACTION = "swing_off_action" | ||||
| CONF_SWING_VERTICAL_ACTION = "swing_vertical_action" | ||||
| CONF_SWITCH = "switch" | ||||
| CONF_SWITCH_DATAPOINT = "switch_datapoint" | ||||
| CONF_SWITCHES = "switches" | ||||
| CONF_SYNC = "sync" | ||||
|   | ||||
| @@ -14,6 +14,8 @@ namespace esphome { | ||||
| static const char *const TAG = "scheduler"; | ||||
|  | ||||
| static const uint32_t MAX_LOGICALLY_DELETED_ITEMS = 10; | ||||
| // Half the 32-bit range - used to detect rollovers vs normal time progression | ||||
| static const uint32_t HALF_MAX_UINT32 = 0x80000000UL; | ||||
|  | ||||
| // Uncomment to debug scheduler | ||||
| // #define ESPHOME_DEBUG_SCHEDULER | ||||
| @@ -91,7 +93,8 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type | ||||
|   } | ||||
| #endif | ||||
|  | ||||
|   const auto now = this->millis_64_(millis()); | ||||
|   // Get fresh timestamp for new timer/interval - ensures accurate scheduling | ||||
|   const auto now = this->millis_64_(millis());  // Fresh millis() call | ||||
|  | ||||
|   // Type-specific setup | ||||
|   if (type == SchedulerItem::INTERVAL) { | ||||
| @@ -220,7 +223,8 @@ optional<uint32_t> HOT Scheduler::next_schedule_in(uint32_t now) { | ||||
|   if (this->empty_()) | ||||
|     return {}; | ||||
|   auto &item = this->items_[0]; | ||||
|   const auto now_64 = this->millis_64_(now); | ||||
|   // 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 | ||||
|   if (item->next_execution_ < now_64) | ||||
|     return 0; | ||||
|   return item->next_execution_ - now_64; | ||||
| @@ -259,7 +263,8 @@ void HOT Scheduler::call(uint32_t now) { | ||||
|   } | ||||
| #endif | ||||
|  | ||||
|   const auto now_64 = this->millis_64_(now); | ||||
|   // 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() | ||||
|   this->process_to_add(); | ||||
|  | ||||
| #ifdef ESPHOME_DEBUG_SCHEDULER | ||||
| @@ -268,8 +273,13 @@ void HOT Scheduler::call(uint32_t now) { | ||||
|   if (now_64 - last_print > 2000) { | ||||
|     last_print = now_64; | ||||
|     std::vector<std::unique_ptr<SchedulerItem>> old_items; | ||||
| #if !defined(USE_ESP8266) && !defined(USE_RP2040) && !defined(USE_LIBRETINY) | ||||
|     ESP_LOGD(TAG, "Items: count=%zu, now=%" PRIu64 " (%u, %" PRIu32 ")", this->items_.size(), now_64, | ||||
|              this->millis_major_, this->last_millis_.load(std::memory_order_relaxed)); | ||||
| #else | ||||
|     ESP_LOGD(TAG, "Items: count=%zu, now=%" PRIu64 " (%u, %" PRIu32 ")", this->items_.size(), now_64, | ||||
|              this->millis_major_, this->last_millis_); | ||||
| #endif | ||||
|     while (!this->empty_()) { | ||||
|       std::unique_ptr<SchedulerItem> item; | ||||
|       { | ||||
| @@ -483,16 +493,111 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_c | ||||
| } | ||||
|  | ||||
| uint64_t Scheduler::millis_64_(uint32_t now) { | ||||
|   // Check for rollover by comparing with last value | ||||
|   if (now < this->last_millis_) { | ||||
|     // Detected rollover (happens every ~49.7 days) | ||||
|   // THREAD SAFETY NOTE: | ||||
|   // This function can be called from multiple threads simultaneously on ESP32/LibreTiny. | ||||
|   // On single-threaded platforms (ESP8266, RP2040), atomics are not needed. | ||||
|   // | ||||
|   // IMPORTANT: Always pass fresh millis() values to this function. The implementation | ||||
|   // handles out-of-order timestamps between threads, but minimizing time differences | ||||
|   // helps maintain accuracy. | ||||
|   // | ||||
|   // The implementation handles the 32-bit rollover (every 49.7 days) by: | ||||
|   // 1. Using a lock when detecting rollover to ensure atomic update | ||||
|   // 2. Restricting normal updates to forward movement within the same epoch | ||||
|   // This prevents race conditions at the rollover boundary without requiring | ||||
|   // 64-bit atomics or locking on every call. | ||||
|  | ||||
| #ifdef USE_LIBRETINY | ||||
|   // LibreTiny: Multi-threaded but lacks atomic operation support | ||||
|   // TODO: If LibreTiny ever adds atomic support, remove this entire block and | ||||
|   // let it fall through to the atomic-based implementation below | ||||
|   // We need to use a lock when near the rollover boundary to prevent races | ||||
|   uint32_t last = this->last_millis_; | ||||
|  | ||||
|   // Define a safe window around the rollover point (10 seconds) | ||||
|   // This covers any reasonable scheduler delays or thread preemption | ||||
|   static const uint32_t ROLLOVER_WINDOW = 10000;  // 10 seconds in milliseconds | ||||
|  | ||||
|   // Check if we're near the rollover boundary (close to 0xFFFFFFFF or just past 0) | ||||
|   bool near_rollover = (last > (0xFFFFFFFF - ROLLOVER_WINDOW)) || (now < ROLLOVER_WINDOW); | ||||
|  | ||||
|   if (near_rollover || (now < last && (last - now) > HALF_MAX_UINT32)) { | ||||
|     // Near rollover or detected a rollover - need lock for safety | ||||
|     LockGuard guard{this->lock_}; | ||||
|     // Re-read with lock held | ||||
|     last = this->last_millis_; | ||||
|  | ||||
|     if (now < last && (last - now) > HALF_MAX_UINT32) { | ||||
|       // True rollover detected (happens every ~49.7 days) | ||||
|       this->millis_major_++; | ||||
| #ifdef ESPHOME_DEBUG_SCHEDULER | ||||
|       ESP_LOGD(TAG, "Detected true 32-bit rollover at %" PRIu32 "ms (was %" PRIu32 ")", now, last); | ||||
| #endif | ||||
|     } | ||||
|     // Update last_millis_ while holding lock | ||||
|     this->last_millis_ = now; | ||||
|   } else if (now > last) { | ||||
|     // Normal case: Not near rollover and time moved forward | ||||
|     // Update without lock. While this may cause minor races (microseconds of | ||||
|     // backwards time movement), they're acceptable because: | ||||
|     // 1. The scheduler operates at millisecond resolution, not microsecond | ||||
|     // 2. We've already prevented the critical rollover race condition | ||||
|     // 3. Any backwards movement is orders of magnitude smaller than scheduler delays | ||||
|     this->last_millis_ = now; | ||||
|   } | ||||
|   // If now <= last and we're not near rollover, don't update | ||||
|   // This minimizes backwards time movement | ||||
|  | ||||
| #elif !defined(USE_ESP8266) && !defined(USE_RP2040) | ||||
|   // Multi-threaded platforms with atomic support (ESP32) | ||||
|   uint32_t last = this->last_millis_.load(std::memory_order_relaxed); | ||||
|  | ||||
|   // If we might be near a rollover (large backwards jump), take the lock for the entire operation | ||||
|   // This ensures rollover detection and last_millis_ update are atomic together | ||||
|   if (now < last && (last - now) > HALF_MAX_UINT32) { | ||||
|     // Potential rollover - need lock for atomic rollover detection + update | ||||
|     LockGuard guard{this->lock_}; | ||||
|     // Re-read with lock held | ||||
|     last = this->last_millis_.load(std::memory_order_relaxed); | ||||
|  | ||||
|     if (now < last && (last - now) > HALF_MAX_UINT32) { | ||||
|       // True rollover detected (happens every ~49.7 days) | ||||
|       this->millis_major_++; | ||||
| #ifdef ESPHOME_DEBUG_SCHEDULER | ||||
|       ESP_LOGD(TAG, "Detected true 32-bit rollover at %" PRIu32 "ms (was %" PRIu32 ")", now, last); | ||||
| #endif | ||||
|     } | ||||
|     // Update last_millis_ while holding lock to prevent races | ||||
|     this->last_millis_.store(now, std::memory_order_relaxed); | ||||
|   } else { | ||||
|     // Normal case: Try lock-free update, but only allow forward movement within same epoch | ||||
|     // This prevents accidentally moving backwards across a rollover boundary | ||||
|     while (now > last && (now - last) < HALF_MAX_UINT32) { | ||||
|       if (this->last_millis_.compare_exchange_weak(last, now, std::memory_order_relaxed)) { | ||||
|         break; | ||||
|       } | ||||
|       // last is automatically updated by compare_exchange_weak if it fails | ||||
|     } | ||||
|   } | ||||
|  | ||||
| #else | ||||
|   // Single-threaded platforms (ESP8266, RP2040): No atomics needed | ||||
|   uint32_t last = this->last_millis_; | ||||
|  | ||||
|   // Check for rollover | ||||
|   if (now < last && (last - now) > HALF_MAX_UINT32) { | ||||
|     this->millis_major_++; | ||||
| #ifdef ESPHOME_DEBUG_SCHEDULER | ||||
|     ESP_LOGD(TAG, "Incrementing scheduler major at %" PRIu64 "ms", | ||||
|              now + (static_cast<uint64_t>(this->millis_major_) << 32)); | ||||
|     ESP_LOGD(TAG, "Detected true 32-bit rollover at %" PRIu32 "ms (was %" PRIu32 ")", now, last); | ||||
| #endif | ||||
|   } | ||||
|   this->last_millis_ = now; | ||||
|  | ||||
|   // Only update if time moved forward | ||||
|   if (now > last) { | ||||
|     this->last_millis_ = now; | ||||
|   } | ||||
| #endif | ||||
|  | ||||
|   // Combine major (high 32 bits) and now (low 32 bits) into 64-bit time | ||||
|   return now + (static_cast<uint64_t>(this->millis_major_) << 32); | ||||
| } | ||||
|   | ||||
| @@ -4,6 +4,9 @@ | ||||
| #include <memory> | ||||
| #include <cstring> | ||||
| #include <deque> | ||||
| #if !defined(USE_ESP8266) && !defined(USE_RP2040) && !defined(USE_LIBRETINY) | ||||
| #include <atomic> | ||||
| #endif | ||||
|  | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/helpers.h" | ||||
| @@ -52,8 +55,12 @@ class Scheduler { | ||||
|                  std::function<RetryResult(uint8_t)> func, float backoff_increase_factor = 1.0f); | ||||
|   bool cancel_retry(Component *component, const std::string &name); | ||||
|  | ||||
|   // Calculate when the next scheduled item should run | ||||
|   // @param now Fresh timestamp from millis() - must not be stale/cached | ||||
|   optional<uint32_t> next_schedule_in(uint32_t now); | ||||
|  | ||||
|   // Execute all scheduled items that are ready | ||||
|   // @param now Fresh timestamp from millis() - must not be stale/cached | ||||
|   void call(uint32_t now); | ||||
|  | ||||
|   void process_to_add(); | ||||
| @@ -203,7 +210,15 @@ class Scheduler { | ||||
|   // Both platforms save 40 bytes of RAM by excluding this | ||||
|   std::deque<std::unique_ptr<SchedulerItem>> defer_queue_;  // FIFO queue for defer() calls | ||||
| #endif | ||||
| #if !defined(USE_ESP8266) && !defined(USE_RP2040) && !defined(USE_LIBRETINY) | ||||
|   // Multi-threaded platforms with atomic support: last_millis_ needs atomic for lock-free updates | ||||
|   std::atomic<uint32_t> last_millis_{0}; | ||||
| #else | ||||
|   // Platforms without atomic support or single-threaded platforms | ||||
|   uint32_t last_millis_{0}; | ||||
| #endif | ||||
|   // millis_major_ is protected by lock when incrementing, volatile ensures | ||||
|   // reads outside the lock see fresh values (not cached in registers) | ||||
|   uint16_t millis_major_{0}; | ||||
|   uint32_t to_remove_{0}; | ||||
| }; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user