mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 07:03:55 +00:00 
			
		
		
		
	Add enable_loop_soon_from_isr
This commit is contained in:
		| @@ -97,6 +97,20 @@ void Application::loop() { | |||||||
|   // Feed WDT with time |   // Feed WDT with time | ||||||
|   this->feed_wdt(last_op_end_time); |   this->feed_wdt(last_op_end_time); | ||||||
|  |  | ||||||
|  |   // Process any pending enable_loop requests from ISRs | ||||||
|  |   // This must be done before marking in_loop_ = true to avoid race conditions | ||||||
|  |   if (this->has_pending_enable_loop_requests_) { | ||||||
|  |     // Clear flag BEFORE processing to avoid race condition | ||||||
|  |     // If ISR sets it during processing, we'll catch it next loop iteration | ||||||
|  |     // This is safe because: | ||||||
|  |     // 1. Each component has its own pending_enable_loop_ flag that we check | ||||||
|  |     // 2. If we can't process a component (wrong state), enable_pending_loops_() | ||||||
|  |     //    will set this flag back to true | ||||||
|  |     // 3. Any new ISR requests during processing will set the flag again | ||||||
|  |     this->has_pending_enable_loop_requests_ = false; | ||||||
|  |     this->enable_pending_loops_(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   // Mark that we're in the loop for safe reentrant modifications |   // Mark that we're in the loop for safe reentrant modifications | ||||||
|   this->in_loop_ = true; |   this->in_loop_ = true; | ||||||
|  |  | ||||||
| @@ -286,24 +300,82 @@ void Application::disable_component_loop_(Component *component) { | |||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| void Application::enable_component_loop_(Component *component) { | void Application::activate_looping_component_(uint16_t index) { | ||||||
|   // This method must be reentrant - components can re-enable themselves during their own loop() call |   // Helper to move component from inactive to active section | ||||||
|   // Single pass through all components to find and move if needed |   if (index != this->looping_components_active_end_) { | ||||||
|   // With typical 10-30 components, O(n) is faster than maintaining a map |     std::swap(this->looping_components_[index], this->looping_components_[this->looping_components_active_end_]); | ||||||
|   const uint16_t size = this->looping_components_.size(); |  | ||||||
|   for (uint16_t i = 0; i < size; i++) { |  | ||||||
|     if (this->looping_components_[i] == component) { |  | ||||||
|       if (i < this->looping_components_active_end_) { |  | ||||||
|         return;  // Already active |  | ||||||
|       } |  | ||||||
|       // Found in inactive section - move to active |  | ||||||
|       if (i != this->looping_components_active_end_) { |  | ||||||
|         std::swap(this->looping_components_[i], this->looping_components_[this->looping_components_active_end_]); |  | ||||||
|   } |   } | ||||||
|   this->looping_components_active_end_++; |   this->looping_components_active_end_++; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void Application::enable_component_loop_(Component *component) { | ||||||
|  |   // This method is only called when component state is LOOP_DONE, so we know | ||||||
|  |   // the component must be in the inactive section (if it exists in looping_components_) | ||||||
|  |   // Only search the inactive portion for better performance | ||||||
|  |   // With typical 0-5 inactive components, O(k) is much faster than O(n) | ||||||
|  |   const uint16_t size = this->looping_components_.size(); | ||||||
|  |   for (uint16_t i = this->looping_components_active_end_; i < size; i++) { | ||||||
|  |     if (this->looping_components_[i] == component) { | ||||||
|  |       // Found in inactive section - move to active | ||||||
|  |       this->activate_looping_component_(i); | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |   // Component not found in looping_components_ - this is normal for components | ||||||
|  |   // that don't have loop() or were not included in the partitioned vector | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void Application::enable_pending_loops_() { | ||||||
|  |   // Process components that requested enable_loop from ISR context | ||||||
|  |   // Only iterate through inactive looping_components_ (typically 0-5) instead of all components | ||||||
|  |   // | ||||||
|  |   // Race condition handling: | ||||||
|  |   // 1. We check if component is already in LOOP state first - if so, just clear the flag | ||||||
|  |   //    This handles reentrancy where enable_loop() was called between ISR and processing | ||||||
|  |   // 2. We only clear pending_enable_loop_ after checking state, preventing lost requests | ||||||
|  |   // 3. If any components aren't in LOOP_DONE state, we set has_pending_enable_loop_requests_ | ||||||
|  |   //    back to true to ensure we check again next iteration | ||||||
|  |   // 4. ISRs can safely set flags at any time - worst case is we process them next iteration | ||||||
|  |   // 5. The global flag (has_pending_enable_loop_requests_) is cleared before this method, | ||||||
|  |   //    so any ISR that fires during processing will be caught in the next loop | ||||||
|  |   const uint16_t size = this->looping_components_.size(); | ||||||
|  |   bool has_pending = false; | ||||||
|  |  | ||||||
|  |   for (uint16_t i = this->looping_components_active_end_; i < size; i++) { | ||||||
|  |     Component *component = this->looping_components_[i]; | ||||||
|  |     if (!component->pending_enable_loop_) { | ||||||
|  |       continue;  // Skip components without pending requests | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Check current state | ||||||
|  |     uint8_t state = component->component_state_ & COMPONENT_STATE_MASK; | ||||||
|  |  | ||||||
|  |     // If already in LOOP state, nothing to do - clear flag and continue | ||||||
|  |     if (state == COMPONENT_STATE_LOOP) { | ||||||
|  |       component->pending_enable_loop_ = false; | ||||||
|  |       continue; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // If not in LOOP_DONE state, can't enable yet - keep flag set | ||||||
|  |     if (state != COMPONENT_STATE_LOOP_DONE) { | ||||||
|  |       has_pending = true;  // Keep tracking this component | ||||||
|  |       continue;            // Keep the flag set - try again next iteration | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Clear the pending flag and enable the loop | ||||||
|  |     component->pending_enable_loop_ = false; | ||||||
|  |     ESP_LOGD(TAG, "%s loop enabled from ISR", component->get_component_source()); | ||||||
|  |     component->component_state_ &= ~COMPONENT_STATE_MASK; | ||||||
|  |     component->component_state_ |= COMPONENT_STATE_LOOP; | ||||||
|  |  | ||||||
|  |     // Move to active section | ||||||
|  |     this->activate_looping_component_(i); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // If we couldn't process some requests, ensure we check again next iteration | ||||||
|  |   if (has_pending) { | ||||||
|  |     this->has_pending_enable_loop_requests_ = true; | ||||||
|  |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| #ifdef USE_SOCKET_SELECT_SUPPORT | #ifdef USE_SOCKET_SELECT_SUPPORT | ||||||
|   | |||||||
| @@ -577,6 +577,8 @@ class Application { | |||||||
|   // to ensure component state is properly updated along with the loop partition |   // to ensure component state is properly updated along with the loop partition | ||||||
|   void disable_component_loop_(Component *component); |   void disable_component_loop_(Component *component); | ||||||
|   void enable_component_loop_(Component *component); |   void enable_component_loop_(Component *component); | ||||||
|  |   void enable_pending_loops_(); | ||||||
|  |   void activate_looping_component_(uint16_t index); | ||||||
|  |  | ||||||
|   void feed_wdt_arch_(); |   void feed_wdt_arch_(); | ||||||
|  |  | ||||||
| @@ -682,6 +684,7 @@ class Application { | |||||||
|   uint32_t loop_interval_{16}; |   uint32_t loop_interval_{16}; | ||||||
|   size_t dump_config_at_{SIZE_MAX}; |   size_t dump_config_at_{SIZE_MAX}; | ||||||
|   uint8_t app_state_{0}; |   uint8_t app_state_{0}; | ||||||
|  |   volatile bool has_pending_enable_loop_requests_{false}; | ||||||
|   Component *current_component_{nullptr}; |   Component *current_component_{nullptr}; | ||||||
|   uint32_t loop_component_start_time_{0}; |   uint32_t loop_component_start_time_{0}; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -148,10 +148,12 @@ void Component::mark_failed() { | |||||||
|   App.disable_component_loop_(this); |   App.disable_component_loop_(this); | ||||||
| } | } | ||||||
| void Component::disable_loop() { | void Component::disable_loop() { | ||||||
|  |   if ((this->component_state_ & COMPONENT_STATE_MASK) != COMPONENT_STATE_LOOP_DONE) { | ||||||
|     ESP_LOGD(TAG, "%s loop disabled", this->get_component_source()); |     ESP_LOGD(TAG, "%s loop disabled", this->get_component_source()); | ||||||
|     this->component_state_ &= ~COMPONENT_STATE_MASK; |     this->component_state_ &= ~COMPONENT_STATE_MASK; | ||||||
|     this->component_state_ |= COMPONENT_STATE_LOOP_DONE; |     this->component_state_ |= COMPONENT_STATE_LOOP_DONE; | ||||||
|     App.disable_component_loop_(this); |     App.disable_component_loop_(this); | ||||||
|  |   } | ||||||
| } | } | ||||||
| void Component::enable_loop() { | void Component::enable_loop() { | ||||||
|   if ((this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP_DONE) { |   if ((this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP_DONE) { | ||||||
| @@ -161,6 +163,19 @@ void Component::enable_loop() { | |||||||
|     App.enable_component_loop_(this); |     App.enable_component_loop_(this); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | void IRAM_ATTR HOT Component::enable_loop_soon_from_isr() { | ||||||
|  |   // This method is ISR-safe because: | ||||||
|  |   // 1. Only performs simple assignments to volatile variables (atomic on all platforms) | ||||||
|  |   // 2. No read-modify-write operations that could be interrupted | ||||||
|  |   // 3. No memory allocation, object construction, or function calls | ||||||
|  |   // 4. IRAM_ATTR ensures code is in IRAM, not flash (required for ISR execution) | ||||||
|  |   // 5. Components are never destroyed, so no use-after-free concerns | ||||||
|  |   // 6. App is guaranteed to be initialized before any ISR could fire | ||||||
|  |   // 7. Multiple ISR calls are safe - just sets the same flags to true | ||||||
|  |   // 8. Race condition with main loop is handled by clearing flag before processing | ||||||
|  |   this->pending_enable_loop_ = true; | ||||||
|  |   App.has_pending_enable_loop_requests_ = true; | ||||||
|  | } | ||||||
| void Component::reset_to_construction_state() { | void Component::reset_to_construction_state() { | ||||||
|   if ((this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED) { |   if ((this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED) { | ||||||
|     ESP_LOGI(TAG, "Component %s is being reset to construction state", this->get_component_source()); |     ESP_LOGI(TAG, "Component %s is being reset to construction state", this->get_component_source()); | ||||||
|   | |||||||
| @@ -171,6 +171,27 @@ class Component { | |||||||
|    */ |    */ | ||||||
|   void enable_loop(); |   void enable_loop(); | ||||||
|  |  | ||||||
|  |   /** ISR-safe version of enable_loop() that can be called from interrupt context. | ||||||
|  |    * | ||||||
|  |    * This method defers the actual enable via enable_pending_loops_ to the main loop, | ||||||
|  |    * making it safe to call from ISR handlers, timer callbacks, or other | ||||||
|  |    * interrupt contexts. | ||||||
|  |    * | ||||||
|  |    * @note The actual loop enabling will happen on the next main loop iteration. | ||||||
|  |    * @note Only one pending enable request is tracked per component. | ||||||
|  |    * @note There is no disable_loop_soon_from_isr() on purpose - it would race | ||||||
|  |    *       against enable calls and synchronization would get too complex | ||||||
|  |    *       to provide a safe version that would work for each component. | ||||||
|  |    * | ||||||
|  |    *       Use disable_loop() from the main thread only. | ||||||
|  |    * | ||||||
|  |    *       If you need to disable the loop from ISR, carefully implement | ||||||
|  |    *       it in the component itself, with an ISR safe approach, and call | ||||||
|  |    *       disable_loop() in its next ::loop() iteration. Implementations | ||||||
|  |    *       will need to carefully consider all possible race conditions. | ||||||
|  |    */ | ||||||
|  |   void enable_loop_soon_from_isr(); | ||||||
|  |  | ||||||
|   bool is_failed() const; |   bool is_failed() const; | ||||||
|  |  | ||||||
|   bool is_ready() const; |   bool is_ready() const; | ||||||
| @@ -331,16 +352,18 @@ class Component { | |||||||
|   /// Cancel a defer callback using the specified name, name must not be empty. |   /// Cancel a defer callback using the specified name, name must not be empty. | ||||||
|   bool cancel_defer(const std::string &name);  // NOLINT |   bool cancel_defer(const std::string &name);  // NOLINT | ||||||
|  |  | ||||||
|  |   // Ordered for optimal packing on 32-bit systems | ||||||
|  |   float setup_priority_override_{NAN}; | ||||||
|  |   const char *component_source_{nullptr}; | ||||||
|  |   const char *error_message_{nullptr}; | ||||||
|  |   uint16_t warn_if_blocking_over_{WARN_IF_BLOCKING_OVER_MS};  ///< Warn if blocked for this many ms (max 65.5s) | ||||||
|   /// State of this component - each bit has a purpose: |   /// State of this component - each bit has a purpose: | ||||||
|   /// Bits 0-1: Component state (0x00=CONSTRUCTION, 0x01=SETUP, 0x02=LOOP, 0x03=FAILED) |   /// Bits 0-1: Component state (0x00=CONSTRUCTION, 0x01=SETUP, 0x02=LOOP, 0x03=FAILED) | ||||||
|   /// Bit 2: STATUS_LED_WARNING |   /// Bit 2: STATUS_LED_WARNING | ||||||
|   /// Bit 3: STATUS_LED_ERROR |   /// Bit 3: STATUS_LED_ERROR | ||||||
|   /// Bits 4-7: Unused - reserved for future expansion (50% of the bits are free) |   /// Bits 4-7: Unused - reserved for future expansion (50% of the bits are free) | ||||||
|   uint8_t component_state_{0x00}; |   uint8_t component_state_{0x00}; | ||||||
|   float setup_priority_override_{NAN}; |   volatile bool pending_enable_loop_{false};  ///< ISR-safe flag for enable_loop_soon_from_isr | ||||||
|   const char *component_source_{nullptr}; |  | ||||||
|   uint16_t warn_if_blocking_over_{WARN_IF_BLOCKING_OVER_MS};  ///< Warn if blocked for this many ms (max 65.5s) |  | ||||||
|   const char *error_message_{nullptr}; |  | ||||||
| }; | }; | ||||||
|  |  | ||||||
| /** This class simplifies creating components that periodically check a state. | /** This class simplifies creating components that periodically check a state. | ||||||
|   | |||||||
| @@ -7,9 +7,13 @@ CODEOWNERS = ["@esphome/tests"] | |||||||
|  |  | ||||||
| loop_test_component_ns = cg.esphome_ns.namespace("loop_test_component") | loop_test_component_ns = cg.esphome_ns.namespace("loop_test_component") | ||||||
| LoopTestComponent = loop_test_component_ns.class_("LoopTestComponent", cg.Component) | LoopTestComponent = loop_test_component_ns.class_("LoopTestComponent", cg.Component) | ||||||
|  | LoopTestISRComponent = loop_test_component_ns.class_( | ||||||
|  |     "LoopTestISRComponent", cg.Component | ||||||
|  | ) | ||||||
|  |  | ||||||
| CONF_DISABLE_AFTER = "disable_after" | CONF_DISABLE_AFTER = "disable_after" | ||||||
| CONF_TEST_REDUNDANT_OPERATIONS = "test_redundant_operations" | CONF_TEST_REDUNDANT_OPERATIONS = "test_redundant_operations" | ||||||
|  | CONF_ISR_COMPONENTS = "isr_components" | ||||||
|  |  | ||||||
| COMPONENT_CONFIG_SCHEMA = cv.Schema( | COMPONENT_CONFIG_SCHEMA = cv.Schema( | ||||||
|     { |     { | ||||||
| @@ -20,10 +24,18 @@ COMPONENT_CONFIG_SCHEMA = cv.Schema( | |||||||
|     } |     } | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | ISR_COMPONENT_CONFIG_SCHEMA = cv.Schema( | ||||||
|  |     { | ||||||
|  |         cv.GenerateID(): cv.declare_id(LoopTestISRComponent), | ||||||
|  |         cv.Required(CONF_NAME): cv.string, | ||||||
|  |     } | ||||||
|  | ) | ||||||
|  |  | ||||||
| CONFIG_SCHEMA = cv.Schema( | CONFIG_SCHEMA = cv.Schema( | ||||||
|     { |     { | ||||||
|         cv.GenerateID(): cv.declare_id(LoopTestComponent), |         cv.GenerateID(): cv.declare_id(LoopTestComponent), | ||||||
|         cv.Required(CONF_COMPONENTS): cv.ensure_list(COMPONENT_CONFIG_SCHEMA), |         cv.Required(CONF_COMPONENTS): cv.ensure_list(COMPONENT_CONFIG_SCHEMA), | ||||||
|  |         cv.Optional(CONF_ISR_COMPONENTS): cv.ensure_list(ISR_COMPONENT_CONFIG_SCHEMA), | ||||||
|     } |     } | ||||||
| ).extend(cv.COMPONENT_SCHEMA) | ).extend(cv.COMPONENT_SCHEMA) | ||||||
|  |  | ||||||
| @@ -76,3 +88,9 @@ async def to_code(config): | |||||||
|                 comp_config[CONF_TEST_REDUNDANT_OPERATIONS] |                 comp_config[CONF_TEST_REDUNDANT_OPERATIONS] | ||||||
|             ) |             ) | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|  |     # Create ISR test components | ||||||
|  |     for isr_config in config.get(CONF_ISR_COMPONENTS, []): | ||||||
|  |         var = cg.new_Pvariable(isr_config[CONF_ID]) | ||||||
|  |         await cg.register_component(var, isr_config) | ||||||
|  |         cg.add(var.set_name(isr_config[CONF_NAME])) | ||||||
|   | |||||||
| @@ -0,0 +1,80 @@ | |||||||
|  | #include "loop_test_isr_component.h" | ||||||
|  | #include "esphome/core/hal.h" | ||||||
|  | #include "esphome/core/application.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace loop_test_component { | ||||||
|  |  | ||||||
|  | static const char *const ISR_TAG = "loop_test_isr_component"; | ||||||
|  |  | ||||||
|  | void LoopTestISRComponent::setup() { | ||||||
|  |   ESP_LOGI(ISR_TAG, "[%s] ISR component setup called", this->name_.c_str()); | ||||||
|  |   this->last_check_time_ = millis(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void LoopTestISRComponent::loop() { | ||||||
|  |   this->loop_count_++; | ||||||
|  |   ESP_LOGI(ISR_TAG, "[%s] ISR component loop count: %d", this->name_.c_str(), this->loop_count_); | ||||||
|  |  | ||||||
|  |   // Disable after 5 loops | ||||||
|  |   if (this->loop_count_ == 5) { | ||||||
|  |     ESP_LOGI(ISR_TAG, "[%s] Disabling after 5 loops", this->name_.c_str()); | ||||||
|  |     this->disable_loop(); | ||||||
|  |     this->last_disable_time_ = millis(); | ||||||
|  |     // Simulate ISR after disabling | ||||||
|  |     this->set_timeout("simulate_isr_1", 50, [this]() { | ||||||
|  |       ESP_LOGI(ISR_TAG, "[%s] Simulating ISR enable", this->name_.c_str()); | ||||||
|  |       this->simulate_isr_enable(); | ||||||
|  |       // Test reentrancy - call enable_loop() directly after ISR | ||||||
|  |       // This simulates another thread calling enable_loop while processing ISR enables | ||||||
|  |       this->set_timeout("test_reentrant", 10, [this]() { | ||||||
|  |         ESP_LOGI(ISR_TAG, "[%s] Testing reentrancy - calling enable_loop() directly", this->name_.c_str()); | ||||||
|  |         this->enable_loop(); | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // If we get here after being disabled, it means ISR re-enabled us | ||||||
|  |   if (this->loop_count_ > 5 && this->loop_count_ < 10) { | ||||||
|  |     ESP_LOGI(ISR_TAG, "[%s] Running after ISR re-enable! ISR was called %d times", this->name_.c_str(), | ||||||
|  |              this->isr_call_count_); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // Disable again after 10 loops to test multiple ISR enables | ||||||
|  |   if (this->loop_count_ == 10) { | ||||||
|  |     ESP_LOGI(ISR_TAG, "[%s] Disabling again after 10 loops", this->name_.c_str()); | ||||||
|  |     this->disable_loop(); | ||||||
|  |     this->last_disable_time_ = millis(); | ||||||
|  |  | ||||||
|  |     // Test pure ISR enable without any main loop enable | ||||||
|  |     this->set_timeout("simulate_isr_2", 50, [this]() { | ||||||
|  |       ESP_LOGI(ISR_TAG, "[%s] Testing pure ISR enable (no main loop enable)", this->name_.c_str()); | ||||||
|  |       this->simulate_isr_enable(); | ||||||
|  |       // DO NOT call enable_loop() - test that ISR alone works | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // Log when we're running after second ISR enable | ||||||
|  |   if (this->loop_count_ > 10) { | ||||||
|  |     ESP_LOGI(ISR_TAG, "[%s] Running after pure ISR re-enable! ISR was called %d times total", this->name_.c_str(), | ||||||
|  |              this->isr_call_count_); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void IRAM_ATTR LoopTestISRComponent::simulate_isr_enable() { | ||||||
|  |   // This simulates what would happen in a real ISR | ||||||
|  |   // In a real scenario, this would be called from an actual interrupt handler | ||||||
|  |  | ||||||
|  |   this->isr_call_count_++; | ||||||
|  |  | ||||||
|  |   // Call enable_loop_soon_from_isr multiple times to test that it's safe | ||||||
|  |   this->enable_loop_soon_from_isr(); | ||||||
|  |   this->enable_loop_soon_from_isr();  // Test multiple calls | ||||||
|  |   this->enable_loop_soon_from_isr();  // Should be idempotent | ||||||
|  |  | ||||||
|  |   // Note: In a real ISR, we cannot use ESP_LOG* macros as they're not ISR-safe | ||||||
|  |   // For testing, we'll track the call count and log it from the main loop | ||||||
|  | } | ||||||
|  |  | ||||||
|  | }  // namespace loop_test_component | ||||||
|  | }  // namespace esphome | ||||||
| @@ -0,0 +1,32 @@ | |||||||
|  | #pragma once | ||||||
|  |  | ||||||
|  | #include "esphome/core/component.h" | ||||||
|  | #include "esphome/core/log.h" | ||||||
|  | #include "esphome/core/hal.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace loop_test_component { | ||||||
|  |  | ||||||
|  | class LoopTestISRComponent : public Component { | ||||||
|  |  public: | ||||||
|  |   void set_name(const std::string &name) { this->name_ = name; } | ||||||
|  |  | ||||||
|  |   void setup() override; | ||||||
|  |   void loop() override; | ||||||
|  |  | ||||||
|  |   // Simulates an ISR calling enable_loop_soon_from_isr | ||||||
|  |   void simulate_isr_enable(); | ||||||
|  |  | ||||||
|  |   float get_setup_priority() const override { return setup_priority::DATA; } | ||||||
|  |  | ||||||
|  |  protected: | ||||||
|  |   std::string name_; | ||||||
|  |   int loop_count_{0}; | ||||||
|  |   uint32_t last_disable_time_{0}; | ||||||
|  |   uint32_t last_check_time_{0}; | ||||||
|  |   bool isr_enable_pending_{false}; | ||||||
|  |   int isr_call_count_{0}; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | }  // namespace loop_test_component | ||||||
|  | }  // namespace esphome | ||||||
| @@ -35,6 +35,11 @@ loop_test_component: | |||||||
|       test_redundant_operations: true |       test_redundant_operations: true | ||||||
|       disable_after: 10 |       disable_after: 10 | ||||||
|  |  | ||||||
|  |   # ISR test component that uses enable_loop_soon_from_isr | ||||||
|  |   isr_components: | ||||||
|  |     - id: isr_test | ||||||
|  |       name: "isr_test" | ||||||
|  |  | ||||||
| # Interval to re-enable the self_disable_10 component after some time | # Interval to re-enable the self_disable_10 component after some time | ||||||
| interval: | interval: | ||||||
|   - interval: 0.5s |   - interval: 0.5s | ||||||
|   | |||||||
| @@ -41,17 +41,25 @@ async def test_loop_disable_enable( | |||||||
|     redundant_disable_tested = asyncio.Event() |     redundant_disable_tested = asyncio.Event() | ||||||
|     # Event fired when self_disable_10 component is re-enabled and runs again (count > 10) |     # Event fired when self_disable_10 component is re-enabled and runs again (count > 10) | ||||||
|     self_disable_10_re_enabled = asyncio.Event() |     self_disable_10_re_enabled = asyncio.Event() | ||||||
|  |     # Events for ISR component testing | ||||||
|  |     isr_component_disabled = asyncio.Event() | ||||||
|  |     isr_component_re_enabled = asyncio.Event() | ||||||
|  |     isr_component_pure_re_enabled = asyncio.Event() | ||||||
|  |  | ||||||
|     # Track loop counts for components |     # Track loop counts for components | ||||||
|     self_disable_10_counts: list[int] = [] |     self_disable_10_counts: list[int] = [] | ||||||
|     normal_component_counts: list[int] = [] |     normal_component_counts: list[int] = [] | ||||||
|  |     isr_component_counts: list[int] = [] | ||||||
|  |  | ||||||
|     def on_log_line(line: str) -> None: |     def on_log_line(line: str) -> None: | ||||||
|         """Process each log line from the process output.""" |         """Process each log line from the process output.""" | ||||||
|         # Strip ANSI color codes |         # Strip ANSI color codes | ||||||
|         clean_line = re.sub(r"\x1b\[[0-9;]*m", "", line) |         clean_line = re.sub(r"\x1b\[[0-9;]*m", "", line) | ||||||
|  |  | ||||||
|         if "loop_test_component" not in clean_line: |         if ( | ||||||
|  |             "loop_test_component" not in clean_line | ||||||
|  |             and "loop_test_isr_component" not in clean_line | ||||||
|  |         ): | ||||||
|             return |             return | ||||||
|  |  | ||||||
|         log_messages.append(clean_line) |         log_messages.append(clean_line) | ||||||
| @@ -92,6 +100,18 @@ async def test_loop_disable_enable( | |||||||
|         ): |         ): | ||||||
|             redundant_disable_tested.set() |             redundant_disable_tested.set() | ||||||
|  |  | ||||||
|  |         # ISR component events | ||||||
|  |         elif "[isr_test]" in clean_line: | ||||||
|  |             if "ISR component loop count:" in clean_line: | ||||||
|  |                 count = int(clean_line.split("ISR component loop count: ")[1]) | ||||||
|  |                 isr_component_counts.append(count) | ||||||
|  |             elif "Disabling after 5 loops" in clean_line: | ||||||
|  |                 isr_component_disabled.set() | ||||||
|  |             elif "Running after ISR re-enable!" in clean_line: | ||||||
|  |                 isr_component_re_enabled.set() | ||||||
|  |             elif "Running after pure ISR re-enable!" in clean_line: | ||||||
|  |                 isr_component_pure_re_enabled.set() | ||||||
|  |  | ||||||
|     # Write, compile and run the ESPHome device with log callback |     # Write, compile and run the ESPHome device with log callback | ||||||
|     async with ( |     async with ( | ||||||
|         run_compiled(yaml_config, line_callback=on_log_line), |         run_compiled(yaml_config, line_callback=on_log_line), | ||||||
| @@ -148,3 +168,40 @@ async def test_loop_disable_enable( | |||||||
|         assert later_self_disable_counts, ( |         assert later_self_disable_counts, ( | ||||||
|             "self_disable_10 was re-enabled but did not run additional times" |             "self_disable_10 was re-enabled but did not run additional times" | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|  |         # Test ISR component functionality | ||||||
|  |         # Wait for ISR component to disable itself after 5 loops | ||||||
|  |         try: | ||||||
|  |             await asyncio.wait_for(isr_component_disabled.wait(), timeout=3.0) | ||||||
|  |         except asyncio.TimeoutError: | ||||||
|  |             pytest.fail("ISR component did not disable itself within 3 seconds") | ||||||
|  |  | ||||||
|  |         # Verify it ran exactly 5 times before disabling | ||||||
|  |         first_run_counts = [c for c in isr_component_counts if c <= 5] | ||||||
|  |         assert len(first_run_counts) == 5, ( | ||||||
|  |             f"Expected 5 loops before disable, got {first_run_counts}" | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # Wait for component to be re-enabled by periodic ISR simulation and run again | ||||||
|  |         try: | ||||||
|  |             await asyncio.wait_for(isr_component_re_enabled.wait(), timeout=2.0) | ||||||
|  |         except asyncio.TimeoutError: | ||||||
|  |             pytest.fail("ISR component was not re-enabled after ISR call") | ||||||
|  |  | ||||||
|  |         # Verify it's running again after ISR enable | ||||||
|  |         count_after_isr = len(isr_component_counts) | ||||||
|  |         assert count_after_isr > 5, ( | ||||||
|  |             f"Component didn't run after ISR enable: got {count_after_isr} counts total" | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # Wait for pure ISR enable (no main loop enable) to work | ||||||
|  |         try: | ||||||
|  |             await asyncio.wait_for(isr_component_pure_re_enabled.wait(), timeout=2.0) | ||||||
|  |         except asyncio.TimeoutError: | ||||||
|  |             pytest.fail("ISR component was not re-enabled by pure ISR call") | ||||||
|  |  | ||||||
|  |         # Verify it ran after pure ISR enable | ||||||
|  |         final_count = len(isr_component_counts) | ||||||
|  |         assert final_count > 10, ( | ||||||
|  |             f"Component didn't run after pure ISR enable: got {final_count} counts total" | ||||||
|  |         ) | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user