Skip to content

Commit 8e795fd

Browse files
authored
Add component loop control documentation (#55)
1 parent fbd6ecb commit 8e795fd

File tree

3 files changed

+266
-2
lines changed

3 files changed

+266
-2
lines changed
Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
# Advanced Topics
2+
3+
This section covers advanced component development topics in ESPHome. These features are typically used in more complex components or when optimizing for performance and resource usage.
4+
5+
## Component Loop Control
6+
7+
ESPHome's main loop runs approximately every 7ms (~7000 times per minute), calling each component's `loop()` method. This high frequency ensures responsive behavior but can waste CPU cycles for components that don't need continuous updates. The loop control API allows components to dynamically enable or disable their participation in the main loop.
8+
9+
On platforms with socket select support (ESP32, Host, and LibreTiny-based chips like BK72xx/RTL87xx), the loop also wakes up immediately when there is new data on monitored sockets (such as API connections and OTA updates), ensuring low-latency network communication without polling. ESP8266 and RP2040 platforms use a simpler TCP implementation without select support.
10+
11+
### Overview
12+
13+
Components can control their loop execution using three methods:
14+
15+
- **`disable_loop()`** - Removes the component from active loop execution
16+
- **`enable_loop()`** - Re-adds the component to active loop execution
17+
- **`enable_loop_soon_any_context()`** - Thread-safe version that can be called from ISRs, timers, or other threads
18+
19+
### When to Use Loop Control
20+
21+
Loop control is beneficial for:
22+
23+
1. **Event-driven components** - Components that respond to interrupts or callbacks rather than polling
24+
2. **Conditional components** - Components that only need to run under specific conditions
25+
3. **One-time operations** - Components that complete their work and no longer need updates
26+
4. **Power optimization** - Reducing CPU usage for battery-powered devices
27+
28+
### Basic Usage
29+
30+
#### Disabling the Loop
31+
32+
Components typically disable their loop when they have no work to do:
33+
34+
```cpp
35+
void MyComponent::loop() {
36+
if (!this->has_work()) {
37+
this->disable_loop();
38+
return;
39+
}
40+
41+
// Do actual work
42+
this->process_data();
43+
}
44+
```
45+
46+
#### Re-enabling the Loop
47+
48+
Components re-enable their loop when new work arrives:
49+
50+
```cpp
51+
void MyComponent::on_data_received() {
52+
this->data_available_ = true;
53+
this->enable_loop(); // Resume loop processing
54+
}
55+
```
56+
57+
#### Thread-Safe Enabling from ISRs
58+
59+
When called from interrupt handlers, timers, or other threads, use the thread-safe version:
60+
61+
```cpp
62+
void IRAM_ATTR MyComponent::gpio_isr_handler() {
63+
// ISR context - cannot use enable_loop() directly
64+
this->enable_loop_soon_any_context();
65+
}
66+
```
67+
68+
### Real-World Examples
69+
70+
#### 1. Interrupt-Driven GPIO Binary Sensor
71+
72+
```cpp
73+
void GPIOBinarySensor::loop() {
74+
if (this->use_interrupt_) {
75+
if (this->interrupt_triggered_) {
76+
// Process the interrupt
77+
bool state = this->pin_->digital_read();
78+
this->publish_state(state);
79+
this->interrupt_triggered_ = false;
80+
} else {
81+
// No interrupt, disable loop until next one
82+
this->disable_loop();
83+
}
84+
} else {
85+
// Polling mode - always check
86+
this->publish_state(this->pin_->digital_read());
87+
}
88+
}
89+
90+
// ISR handler
91+
void IRAM_ATTR gpio_interrupt_handler(void *arg) {
92+
GPIOBinarySensor *sensor = static_cast<GPIOBinarySensor *>(arg);
93+
sensor->interrupt_triggered_ = true;
94+
sensor->enable_loop_soon_any_context();
95+
}
96+
```
97+
98+
#### 2. State-Based Component (BLE Client)
99+
100+
```cpp
101+
void BLEClientBase::loop() {
102+
if (!esp32_ble::global_ble->is_active()) {
103+
this->set_state(espbt::ClientState::INIT);
104+
return;
105+
}
106+
107+
if (this->state_ == espbt::ClientState::INIT) {
108+
// Register with BLE stack
109+
auto ret = esp_ble_gattc_app_register(this->app_id);
110+
if (ret) {
111+
ESP_LOGE(TAG, "gattc app register failed. app_id=%d code=%d", this->app_id, ret);
112+
this->mark_failed();
113+
}
114+
this->set_state(espbt::ClientState::IDLE);
115+
}
116+
// READY_TO_CONNECT means we have discovered the device
117+
else if (this->state_ == espbt::ClientState::READY_TO_CONNECT) {
118+
this->connect();
119+
}
120+
// If idle, disable loop as set_state will enable it again when needed
121+
else if (this->state_ == espbt::ClientState::IDLE) {
122+
this->disable_loop();
123+
}
124+
}
125+
126+
void BLEClientBase::set_state(espbt::ClientState st) {
127+
ESP_LOGV(TAG, "[%d] [%s] Set state %d", this->connection_index_, this->address_str_.c_str(), (int) st);
128+
ESPBTClient::set_state(st);
129+
if (st == espbt::ClientState::READY_TO_CONNECT) {
130+
// Enable loop when we need to connect
131+
this->enable_loop();
132+
}
133+
}
134+
```
135+
136+
#### 3. Network Event Handler (Ethernet)
137+
138+
```cpp
139+
void EthernetComponent::eth_event_handler(void *arg, esp_event_base_t event_base,
140+
int32_t event, void *event_data) {
141+
switch (event) {
142+
case ETHERNET_EVENT_START:
143+
global_eth_component->started_ = true;
144+
global_eth_component->enable_loop_soon_any_context();
145+
break;
146+
case ETHERNET_EVENT_DISCONNECTED:
147+
global_eth_component->connected_ = false;
148+
global_eth_component->enable_loop_soon_any_context();
149+
break;
150+
}
151+
}
152+
```
153+
154+
#### 4. One-Time Task (Safe Mode)
155+
156+
```cpp
157+
void SafeModeComponent::loop() {
158+
if (!this->boot_successful_ &&
159+
(millis() - this->safe_mode_start_time_) > this->safe_mode_boot_is_good_after_) {
160+
// Boot successful, no need to monitor anymore
161+
ESP_LOGI(TAG, "Boot successful; disabling safe mode checks");
162+
this->clean_rtc();
163+
this->boot_successful_ = true;
164+
this->disable_loop(); // Never need to check again
165+
}
166+
}
167+
```
168+
169+
### Implementation Details
170+
171+
#### Component State Management
172+
173+
Components track their loop state using internal flags:
174+
175+
- `COMPONENT_STATE_LOOP` - Component is actively looping
176+
- `COMPONENT_STATE_LOOP_DONE` - Component has disabled its loop
177+
178+
#### Performance Considerations
179+
180+
With the main loop running every ~7ms:
181+
182+
- An idle component with an empty `loop()` still consumes CPU cycles
183+
- 10 disabled components save ~70,000 function calls per minute
184+
- Critical for ESP8266/ESP32 devices with limited CPU resources
185+
186+
#### Thread Safety
187+
188+
The `enable_loop_soon_any_context()` method is specifically designed for cross-thread usage:
189+
190+
- Sets volatile flags that are checked by the main loop
191+
- No memory allocation or complex operations
192+
- Safe to call from ISRs, timer callbacks, or FreeRTOS tasks
193+
- Multiple calls are idempotent (safe to call repeatedly)
194+
195+
### Best Practices
196+
197+
1. **Only disable your own loop** - Components should only call loop control methods on themselves, never on other components
198+
199+
2. **Clear state before disabling** - Ensure the component is in a clean state before disabling the loop
200+
201+
3. **Document loop dependencies** - If your component requires continuous looping, document why
202+
203+
4. **Prefer event-driven design** - Use interrupts, callbacks, or timers instead of polling when possible
204+
205+
5. **Test edge cases** - Verify behavior when rapidly enabling/disabling the loop
206+
207+
### Common Patterns
208+
209+
#### Conditional Compilation
210+
211+
Some components disable their loop based on compile-time configuration:
212+
213+
```cpp
214+
void CaptivePortal::loop() {
215+
#ifdef USE_ARDUINO
216+
if (this->dns_server_ != nullptr) {
217+
this->dns_server_->processNextRequest();
218+
} else {
219+
this->disable_loop();
220+
}
221+
#else
222+
// No DNS server on ESP-IDF
223+
this->disable_loop();
224+
#endif
225+
}
226+
```
227+
228+
#### BLE Components
229+
230+
Most BLE components inherit from parent classes (like `BLEClientNode`) that have a `loop()` method, but don't actually need it. They disable their loop immediately since all work is done through BLE callbacks and periodic `update()` calls:
231+
232+
```cpp
233+
void Anova::loop() {
234+
// Parent BLEClientNode has a loop() method, but this component uses
235+
// polling via update() and BLE callbacks so loop isn't needed
236+
this->disable_loop();
237+
}
238+
```
239+
240+
### Debugging
241+
242+
To debug loop control issues:
243+
244+
1. Add logging when enabling/disabling:
245+
```cpp
246+
ESP_LOGV(TAG, "%s loop disabled", this->get_component_source());
247+
```
248+
249+
2. Check if your component is in the loop state:
250+
```cpp
251+
bool is_looping = this->is_in_loop_state();
252+
```
253+
254+
3. Use the Logger component's async support as a reference implementation
255+
256+
### See Also
257+
258+
- Source: [`esphome/core/component.h`](https://github.com/esphome/esphome/blob/dev/esphome/core/component.h) and [`esphome/core/component.cpp`](https://github.com/esphome/esphome/blob/dev/esphome/core/component.cpp)

docs/architecture/components/index.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -276,8 +276,9 @@ There are several methods `Component` defines which components typically impleme
276276
- `setup()`: This method is called once as ESPHome starts up to perform initialization of the component. This may mean
277277
simply initializing some memory/variables or performing a series of read/write calls to look for and initialize
278278
some (sensor, display, etc.) hardware connected via some bus (I2C, SPI, serial/UART, one-wire, etc.).
279-
- `loop()`: This method is called at each iteration of ESPHome's main application loop. Typically this is every 16
280-
milliseconds, but there may be some variance as other components consume cycles to perform their own tasks.
279+
- `loop()`: This method is called at each iteration of ESPHome's main application loop. The loop runs approximately
280+
every 7ms (~7000 times per minute), but there may be some variance as other components consume cycles to perform
281+
their own tasks. Components can dynamically control their participation in the main loop (see [Component Loop Control](advanced.md#component-loop-control) in the Advanced Topics page).
281282

282283
#### Other important methods
283284

@@ -382,6 +383,10 @@ void ExampleComponent::on_powerdown() {
382383
}
383384
```
384385

386+
## Advanced Topics
387+
388+
For more advanced component development topics, including Component Loop Control which allows components to dynamically enable or disable their participation in the main loop for performance optimization, see the [Advanced Topics](advanced.md) page.
389+
385390
## Going further
386391

387392
- To help you get started, we have a number of ["starter" components](https://github.com/esphome/starter-components).

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ nav:
8383
- SPI: architecture/components/spi.md
8484
- Serial (UART): architecture/components/uart.md
8585
- Automations: architecture/components/automations.md
86+
- Advanced Topics: architecture/components/advanced.md
8687
- API:
8788
- Overview: architecture/api/index.md
8889
- Protocol Details: architecture/api/protocol_details.md

0 commit comments

Comments
 (0)