|
| 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) |
0 commit comments