Add enable_loop_soon_any_context() for thread and ISR-safe loop enabling
What does this implement/fix?
This PR adds a new enable_loop_soon_any_context() method to the Component class, allowing components to safely request their loop() method to be re-enabled from interrupt service routines (ISRs), other threads, or any execution context.
Motivation
Currently, components can disable their loop() method to save CPU cycles when inactive using disable_loop(). However, there was no safe way to re-enable the loop from an ISR context (e.g., GPIO interrupts, timer callbacks). Calling enable_loop() directly from an ISR is unsafe because it:
- Performs read-modify-write operations on component state
- Calls into Application methods that manipulate data structures
- Could cause race conditions or crashes
Implementation
The solution introduces:
Component::enable_loop_soon_any_context()- A thread and ISR-safe method that defers the actualenable_loop()call to the main thread- Volatile flags - Uses simple volatile variable assignments that are atomic on all platforms
- Efficient processing - A global flag (
has_pending_enable_loop_requests_) avoids unnecessary iteration when no components have pending requests - Race condition handling - The global flag is cleared before processing to ensure no requests are lost
- Memory optimization - Component member variables reordered to eliminate padding on 32-bit systems (saves 8 bytes per component)
ISR Safety
The method is ISR-safe because it:
- Only performs simple assignments to volatile variables (atomic on all platforms)
- Contains no read-modify-write operations that could be interrupted
- Does no memory allocation, object construction, or function calls
- Is marked with
IRAM_ATTRto ensure code is in IRAM for ISR execution - Takes advantage of components never being destroyed in ESPHome
Design Decisions
- No
disable_loop_soon_any_context()- This is intentional. Disable operations would race against enable calls and synchronization would become too complex. Disabling should only be done from the main thread. - "soon" in the name - Clearly indicates the operation is deferred, not immediate
- Volatile over std::atomic - Ensures compatibility across all platforms (ESP8266, ESP32, LibreTiny, RP2040)
Example Usage
class ExampleISRComponent : public Component {
protected:
void IRAM_ATTR HOT gpio_interrupt_handler() {
// Request loop to be enabled on next main loop iteration
this->enable_loop_soon_any_context();
// The actual enable_loop() will happen safely in the main thread
}
};
Use Cases
This addresses the need for components that:
- Monitor hardware interrupts but don't need continuous loop() execution
- Want to save power by disabling loop() when idle
- Need to respond quickly to external events
Examples include interrupt-driven sensors, event-based communication protocols, and power-sensitive battery-operated devices.
Types of changes
- [ ] Bugfix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] Other
Related issue or feature (if applicable): fixes
N/A
Pull request in esphome-docs with documentation (if applicable): esphome/esphome-docs#
N/A - This is primarily an internal API for component developers. The method is documented inline with comprehensive docstrings.
Test Environment
- [ ] ESP32
- [x] ESP32 IDF
- [x] ESP8266
- [ ] RP2040
- [ ] BK72xx
- [ ] RTL87xx
Example entry for config.yaml:
# Not applicable - this is an internal API change
Checklist:
- [x] The code change is tested and works locally.
- [x] Tests have been added to verify that the new code works (under
tests/folder). - [ ] Documentation added/updated as needed.
- [x] I have read the contributing guide.