mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 07:03:55 +00:00 
			
		
		
		
	Merge branch 'dev' into disable_ethernet_loop
This commit is contained in:
		| @@ -17,7 +17,11 @@ void Anova::setup() { | ||||
|   this->current_request_ = 0; | ||||
| } | ||||
|  | ||||
| void Anova::loop() {} | ||||
| void Anova::loop() { | ||||
|   // Parent BLEClientNode has a loop() method, but this component uses | ||||
|   // polling via update() and BLE callbacks so loop isn't needed | ||||
|   this->disable_loop(); | ||||
| } | ||||
|  | ||||
| void Anova::control(const ClimateCall &call) { | ||||
|   if (call.get_mode().has_value()) { | ||||
|   | ||||
| @@ -1643,6 +1643,7 @@ enum VoiceAssistantEvent { | ||||
|   VOICE_ASSISTANT_STT_VAD_END = 12; | ||||
|   VOICE_ASSISTANT_TTS_STREAM_START = 98; | ||||
|   VOICE_ASSISTANT_TTS_STREAM_END = 99; | ||||
|   VOICE_ASSISTANT_INTENT_PROGRESS = 100; | ||||
| } | ||||
|  | ||||
| message VoiceAssistantEventData { | ||||
|   | ||||
| @@ -516,6 +516,8 @@ template<> const char *proto_enum_to_string<enums::VoiceAssistantEvent>(enums::V | ||||
|       return "VOICE_ASSISTANT_TTS_STREAM_START"; | ||||
|     case enums::VOICE_ASSISTANT_TTS_STREAM_END: | ||||
|       return "VOICE_ASSISTANT_TTS_STREAM_END"; | ||||
|     case enums::VOICE_ASSISTANT_INTENT_PROGRESS: | ||||
|       return "VOICE_ASSISTANT_INTENT_PROGRESS"; | ||||
|     default: | ||||
|       return "UNKNOWN"; | ||||
|   } | ||||
|   | ||||
| @@ -208,6 +208,7 @@ enum VoiceAssistantEvent : uint32_t { | ||||
|   VOICE_ASSISTANT_STT_VAD_END = 12, | ||||
|   VOICE_ASSISTANT_TTS_STREAM_START = 98, | ||||
|   VOICE_ASSISTANT_TTS_STREAM_END = 99, | ||||
|   VOICE_ASSISTANT_INTENT_PROGRESS = 100, | ||||
| }; | ||||
| enum VoiceAssistantTimerEvent : uint32_t { | ||||
|   VOICE_ASSISTANT_TIMER_STARTED = 0, | ||||
|   | ||||
| @@ -21,8 +21,8 @@ CONFIG_SCHEMA = cv.All( | ||||
| @coroutine_with_priority(200.0) | ||||
| async def to_code(config): | ||||
|     if CORE.is_esp32 or CORE.is_libretiny: | ||||
|         # https://github.com/esphome/AsyncTCP/blob/master/library.json | ||||
|         cg.add_library("esphome/AsyncTCP-esphome", "2.1.4") | ||||
|         # https://github.com/ESP32Async/AsyncTCP | ||||
|         cg.add_library("ESP32Async/AsyncTCP", "3.4.4") | ||||
|     elif CORE.is_esp8266: | ||||
|         # https://github.com/esphome/ESPAsyncTCP | ||||
|         cg.add_library("esphome/ESPAsyncTCP-esphome", "2.0.0") | ||||
|         # https://github.com/ESP32Async/ESPAsyncTCP | ||||
|         cg.add_library("ESP32Async/ESPAsyncTCP", "2.0.0") | ||||
|   | ||||
| @@ -480,7 +480,11 @@ void BedJetHub::set_clock(uint8_t hour, uint8_t minute) { | ||||
|  | ||||
| /* Internal */ | ||||
|  | ||||
| void BedJetHub::loop() {} | ||||
| void BedJetHub::loop() { | ||||
|   // Parent BLEClientNode has a loop() method, but this component uses | ||||
|   // polling via update() and BLE callbacks so loop isn't needed | ||||
|   this->disable_loop(); | ||||
| } | ||||
| void BedJetHub::update() { this->dispatch_status_(); } | ||||
|  | ||||
| void BedJetHub::dump_config() { | ||||
|   | ||||
| @@ -83,7 +83,11 @@ void BedJetClimate::reset_state_() { | ||||
|   this->publish_state(); | ||||
| } | ||||
|  | ||||
| void BedJetClimate::loop() {} | ||||
| void BedJetClimate::loop() { | ||||
|   // This component is controlled via the parent BedJetHub | ||||
|   // Empty loop not needed, disable to save CPU cycles | ||||
|   this->disable_loop(); | ||||
| } | ||||
|  | ||||
| void BedJetClimate::control(const ClimateCall &call) { | ||||
|   ESP_LOGD(TAG, "Received BedJetClimate::control"); | ||||
|   | ||||
| @@ -11,7 +11,11 @@ namespace ble_client { | ||||
|  | ||||
| static const char *const TAG = "ble_rssi_sensor"; | ||||
|  | ||||
| void BLEClientRSSISensor::loop() {} | ||||
| void BLEClientRSSISensor::loop() { | ||||
|   // Parent BLEClientNode has a loop() method, but this component uses | ||||
|   // polling via update() and BLE GAP callbacks so loop isn't needed | ||||
|   this->disable_loop(); | ||||
| } | ||||
|  | ||||
| void BLEClientRSSISensor::dump_config() { | ||||
|   LOG_SENSOR("", "BLE Client RSSI Sensor", this); | ||||
|   | ||||
| @@ -11,7 +11,11 @@ namespace ble_client { | ||||
|  | ||||
| static const char *const TAG = "ble_sensor"; | ||||
|  | ||||
| void BLESensor::loop() {} | ||||
| void BLESensor::loop() { | ||||
|   // Parent BLEClientNode has a loop() method, but this component uses | ||||
|   // polling via update() and BLE callbacks so loop isn't needed | ||||
|   this->disable_loop(); | ||||
| } | ||||
|  | ||||
| void BLESensor::dump_config() { | ||||
|   LOG_SENSOR("", "BLE Sensor", this); | ||||
|   | ||||
| @@ -14,7 +14,11 @@ static const char *const TAG = "ble_text_sensor"; | ||||
|  | ||||
| static const std::string EMPTY = ""; | ||||
|  | ||||
| void BLETextSensor::loop() {} | ||||
| void BLETextSensor::loop() { | ||||
|   // Parent BLEClientNode has a loop() method, but this component uses | ||||
|   // polling via update() and BLE callbacks so loop isn't needed | ||||
|   this->disable_loop(); | ||||
| } | ||||
|  | ||||
| void BLETextSensor::dump_config() { | ||||
|   LOG_TEXT_SENSOR("", "BLE Text Sensor", this); | ||||
|   | ||||
| @@ -12,8 +12,8 @@ from esphome.const import ( | ||||
|     CONF_OVERSAMPLING, | ||||
|     CONF_PRESSURE, | ||||
|     CONF_TEMPERATURE, | ||||
|     DEVICE_CLASS_HUMIDITY, | ||||
|     DEVICE_CLASS_ATMOSPHERIC_PRESSURE, | ||||
|     DEVICE_CLASS_HUMIDITY, | ||||
|     DEVICE_CLASS_TEMPERATURE, | ||||
|     ICON_GAS_CYLINDER, | ||||
|     STATE_CLASS_MEASUREMENT, | ||||
|   | ||||
| @@ -37,7 +37,12 @@ void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) { | ||||
|   request->redirect("/?save"); | ||||
| } | ||||
|  | ||||
| void CaptivePortal::setup() {} | ||||
| void CaptivePortal::setup() { | ||||
| #ifndef USE_ARDUINO | ||||
|   // No DNS server needed for non-Arduino frameworks | ||||
|   this->disable_loop(); | ||||
| #endif | ||||
| } | ||||
| void CaptivePortal::start() { | ||||
|   this->base_->init(); | ||||
|   if (!this->initialized_) { | ||||
| @@ -50,6 +55,8 @@ void CaptivePortal::start() { | ||||
|   this->dns_server_->setErrorReplyCode(DNSReplyCode::NoError); | ||||
|   network::IPAddress ip = wifi::global_wifi_component->wifi_soft_ap_ip(); | ||||
|   this->dns_server_->start(53, "*", ip); | ||||
|   // Re-enable loop() when DNS server is started | ||||
|   this->enable_loop(); | ||||
| #endif | ||||
|  | ||||
|   this->base_->get_server()->onNotFound([this](AsyncWebServerRequest *req) { | ||||
| @@ -68,7 +75,11 @@ void CaptivePortal::start() { | ||||
|  | ||||
| void CaptivePortal::handleRequest(AsyncWebServerRequest *req) { | ||||
|   if (req->url() == "/") { | ||||
| #ifndef USE_ESP8266 | ||||
|     auto *response = req->beginResponse(200, "text/html", INDEX_GZ, sizeof(INDEX_GZ)); | ||||
| #else | ||||
|     auto *response = req->beginResponse_P(200, "text/html", INDEX_GZ, sizeof(INDEX_GZ)); | ||||
| #endif | ||||
|     response->addHeader("Content-Encoding", "gzip"); | ||||
|     req->send(response); | ||||
|     return; | ||||
|   | ||||
| @@ -21,8 +21,11 @@ class CaptivePortal : public AsyncWebHandler, public Component { | ||||
|   void dump_config() override; | ||||
| #ifdef USE_ARDUINO | ||||
|   void loop() override { | ||||
|     if (this->dns_server_ != nullptr) | ||||
|     if (this->dns_server_ != nullptr) { | ||||
|       this->dns_server_->processNextRequest(); | ||||
|     } else { | ||||
|       this->disable_loop(); | ||||
|     } | ||||
|   } | ||||
| #endif | ||||
|   float get_setup_priority() const override; | ||||
| @@ -37,7 +40,7 @@ class CaptivePortal : public AsyncWebHandler, public Component { | ||||
| #endif | ||||
|   } | ||||
|  | ||||
|   bool canHandle(AsyncWebServerRequest *request) override { | ||||
|   bool canHandle(AsyncWebServerRequest *request) const override { | ||||
|     if (!this->active_) | ||||
|       return false; | ||||
|  | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| #ifdef USE_ESP32 | ||||
|  | ||||
| #include "ble.h" | ||||
| #include "ble_event_pool.h" | ||||
|  | ||||
| #include "esphome/core/application.h" | ||||
| #include "esphome/core/log.h" | ||||
| @@ -23,9 +24,6 @@ namespace esp32_ble { | ||||
|  | ||||
| static const char *const TAG = "esp32_ble"; | ||||
|  | ||||
| static RAMAllocator<BLEEvent> EVENT_ALLOCATOR(  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) | ||||
|     RAMAllocator<BLEEvent>::ALLOW_FAILURE | RAMAllocator<BLEEvent>::ALLOC_INTERNAL); | ||||
|  | ||||
| void ESP32BLE::setup() { | ||||
|   global_ble = this; | ||||
|   ESP_LOGCONFIG(TAG, "Running setup"); | ||||
| @@ -349,9 +347,8 @@ void ESP32BLE::loop() { | ||||
|       default: | ||||
|         break; | ||||
|     } | ||||
|     // Destructor will clean up external allocations for GATTC/GATTS | ||||
|     ble_event->~BLEEvent(); | ||||
|     EVENT_ALLOCATOR.deallocate(ble_event, 1); | ||||
|     // Return the event to the pool | ||||
|     this->ble_event_pool_.release(ble_event); | ||||
|     ble_event = this->ble_events_.pop(); | ||||
|   } | ||||
|   if (this->advertising_ != nullptr) { | ||||
| @@ -359,37 +356,41 @@ void ESP32BLE::loop() { | ||||
|   } | ||||
|  | ||||
|   // Log dropped events periodically | ||||
|   size_t dropped = this->ble_events_.get_and_reset_dropped_count(); | ||||
|   uint16_t dropped = this->ble_events_.get_and_reset_dropped_count(); | ||||
|   if (dropped > 0) { | ||||
|     ESP_LOGW(TAG, "Dropped %zu BLE events due to buffer overflow", dropped); | ||||
|     ESP_LOGW(TAG, "Dropped %u BLE events due to buffer overflow", dropped); | ||||
|   } | ||||
| } | ||||
|  | ||||
| // Helper function to load new event data based on type | ||||
| void load_ble_event(BLEEvent *event, esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) { | ||||
|   event->load_gap_event(e, p); | ||||
| } | ||||
|  | ||||
| void load_ble_event(BLEEvent *event, esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) { | ||||
|   event->load_gattc_event(e, i, p); | ||||
| } | ||||
|  | ||||
| void load_ble_event(BLEEvent *event, esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p) { | ||||
|   event->load_gatts_event(e, i, p); | ||||
| } | ||||
|  | ||||
| template<typename... Args> void enqueue_ble_event(Args... args) { | ||||
|   // Check if queue is full before allocating | ||||
|   if (global_ble->ble_events_.full()) { | ||||
|     // Queue is full, drop the event | ||||
|   // Allocate an event from the pool | ||||
|   BLEEvent *event = global_ble->ble_event_pool_.allocate(); | ||||
|   if (event == nullptr) { | ||||
|     // No events available - queue is full or we're out of memory | ||||
|     global_ble->ble_events_.increment_dropped_count(); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   BLEEvent *new_event = EVENT_ALLOCATOR.allocate(1); | ||||
|   if (new_event == nullptr) { | ||||
|     // Memory too fragmented to allocate new event. Can only drop it until memory comes back | ||||
|     global_ble->ble_events_.increment_dropped_count(); | ||||
|     return; | ||||
|   } | ||||
|   new (new_event) BLEEvent(args...); | ||||
|   // Load new event data (replaces previous event) | ||||
|   load_ble_event(event, args...); | ||||
|  | ||||
|   // Push the event - since we're the only producer and we checked full() above, | ||||
|   // this should always succeed unless we have a bug | ||||
|   if (!global_ble->ble_events_.push(new_event)) { | ||||
|     // This should not happen in SPSC queue with single producer | ||||
|     ESP_LOGE(TAG, "BLE queue push failed unexpectedly"); | ||||
|     new_event->~BLEEvent(); | ||||
|     EVENT_ALLOCATOR.deallocate(new_event, 1); | ||||
|   } | ||||
| }  // NOLINT(clang-analyzer-unix.Malloc) | ||||
|   // Push the event to the queue | ||||
|   global_ble->ble_events_.push(event); | ||||
|   // Push always succeeds because we're the only producer and the pool ensures we never exceed queue size | ||||
| } | ||||
|  | ||||
| // Explicit template instantiations for the friend function | ||||
| template void enqueue_ble_event(esp_gap_ble_cb_event_t, esp_ble_gap_cb_param_t *); | ||||
|   | ||||
| @@ -12,6 +12,7 @@ | ||||
| #include "esphome/core/helpers.h" | ||||
|  | ||||
| #include "ble_event.h" | ||||
| #include "ble_event_pool.h" | ||||
| #include "queue.h" | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
| @@ -148,6 +149,7 @@ class ESP32BLE : public Component { | ||||
|   BLEComponentState state_{BLE_COMPONENT_STATE_OFF}; | ||||
|  | ||||
|   LockFreeQueue<BLEEvent, MAX_BLE_QUEUE_SIZE> ble_events_; | ||||
|   BLEEventPool<MAX_BLE_QUEUE_SIZE> ble_event_pool_; | ||||
|   BLEAdvertising *advertising_{}; | ||||
|   esp_ble_io_cap_t io_cap_{ESP_IO_CAP_NONE}; | ||||
|   uint32_t advertising_cycle_time_{}; | ||||
|   | ||||
| @@ -51,6 +51,13 @@ static_assert(offsetof(esp_ble_gap_cb_param_t, scan_stop_cmpl.status) == | ||||
| // - GATTC/GATTS events: We heap-allocate and copy the entire param struct, ensuring | ||||
| //   the data remains valid even after the BLE callback returns. The original | ||||
| //   param pointer from ESP-IDF is only valid during the callback. | ||||
| // | ||||
| // CRITICAL DESIGN NOTE: | ||||
| // The heap allocations for GATTC/GATTS events are REQUIRED for memory safety. | ||||
| // DO NOT attempt to optimize by removing these allocations or storing pointers | ||||
| // to the original ESP-IDF data. The ESP-IDF callback data has a different lifetime | ||||
| // than our event processing, and accessing it after the callback returns would | ||||
| // result in use-after-free bugs and crashes. | ||||
| class BLEEvent { | ||||
|  public: | ||||
|   // NOLINTNEXTLINE(readability-identifier-naming) | ||||
| @@ -63,123 +70,72 @@ class BLEEvent { | ||||
|   // Constructor for GAP events - no external allocations needed | ||||
|   BLEEvent(esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) { | ||||
|     this->type_ = GAP; | ||||
|     this->event_.gap.gap_event = e; | ||||
|  | ||||
|     if (p == nullptr) { | ||||
|       return;  // Invalid event, but we can't log in header file | ||||
|     } | ||||
|  | ||||
|     // Only copy the data we actually use for each GAP event type | ||||
|     switch (e) { | ||||
|       case ESP_GAP_BLE_SCAN_RESULT_EVT: | ||||
|         // Copy only the fields we use from scan results | ||||
|         memcpy(this->event_.gap.scan_result.bda, p->scan_rst.bda, sizeof(esp_bd_addr_t)); | ||||
|         this->event_.gap.scan_result.ble_addr_type = p->scan_rst.ble_addr_type; | ||||
|         this->event_.gap.scan_result.rssi = p->scan_rst.rssi; | ||||
|         this->event_.gap.scan_result.adv_data_len = p->scan_rst.adv_data_len; | ||||
|         this->event_.gap.scan_result.scan_rsp_len = p->scan_rst.scan_rsp_len; | ||||
|         this->event_.gap.scan_result.search_evt = p->scan_rst.search_evt; | ||||
|         memcpy(this->event_.gap.scan_result.ble_adv, p->scan_rst.ble_adv, | ||||
|                ESP_BLE_ADV_DATA_LEN_MAX + ESP_BLE_SCAN_RSP_DATA_LEN_MAX); | ||||
|         break; | ||||
|  | ||||
|       case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: | ||||
|         this->event_.gap.scan_complete.status = p->scan_param_cmpl.status; | ||||
|         break; | ||||
|  | ||||
|       case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT: | ||||
|         this->event_.gap.scan_complete.status = p->scan_start_cmpl.status; | ||||
|         break; | ||||
|  | ||||
|       case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT: | ||||
|         this->event_.gap.scan_complete.status = p->scan_stop_cmpl.status; | ||||
|         break; | ||||
|  | ||||
|       default: | ||||
|         // We only handle 4 GAP event types, others are dropped | ||||
|         break; | ||||
|     } | ||||
|     this->init_gap_data_(e, p); | ||||
|   } | ||||
|  | ||||
|   // Constructor for GATTC events - uses heap allocation | ||||
|   // Creates a copy of the param struct since the original is only valid during the callback | ||||
|   // IMPORTANT: The heap allocation is REQUIRED and must not be removed as an optimization. | ||||
|   // The param pointer from ESP-IDF is only valid during the callback execution. | ||||
|   // Since BLE events are processed asynchronously in the main loop, we must create | ||||
|   // our own copy to ensure the data remains valid until the event is processed. | ||||
|   BLEEvent(esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) { | ||||
|     this->type_ = GATTC; | ||||
|     this->event_.gattc.gattc_event = e; | ||||
|     this->event_.gattc.gattc_if = i; | ||||
|  | ||||
|     if (p == nullptr) { | ||||
|       this->event_.gattc.gattc_param = nullptr; | ||||
|       this->event_.gattc.data = nullptr; | ||||
|       return;  // Invalid event, but we can't log in header file | ||||
|     } | ||||
|  | ||||
|     // Heap-allocate param and data | ||||
|     // Heap allocation is used because GATTC/GATTS events are rare (<1% of events) | ||||
|     // while GAP events (99%) are stored inline to minimize memory usage | ||||
|     this->event_.gattc.gattc_param = new esp_ble_gattc_cb_param_t(*p); | ||||
|  | ||||
|     // Copy data for events that need it | ||||
|     switch (e) { | ||||
|       case ESP_GATTC_NOTIFY_EVT: | ||||
|         this->event_.gattc.data = new std::vector<uint8_t>(p->notify.value, p->notify.value + p->notify.value_len); | ||||
|         this->event_.gattc.gattc_param->notify.value = this->event_.gattc.data->data(); | ||||
|         break; | ||||
|       case ESP_GATTC_READ_CHAR_EVT: | ||||
|       case ESP_GATTC_READ_DESCR_EVT: | ||||
|         this->event_.gattc.data = new std::vector<uint8_t>(p->read.value, p->read.value + p->read.value_len); | ||||
|         this->event_.gattc.gattc_param->read.value = this->event_.gattc.data->data(); | ||||
|         break; | ||||
|       default: | ||||
|         this->event_.gattc.data = nullptr; | ||||
|         break; | ||||
|     } | ||||
|     this->init_gattc_data_(e, i, p); | ||||
|   } | ||||
|  | ||||
|   // Constructor for GATTS events - uses heap allocation | ||||
|   // Creates a copy of the param struct since the original is only valid during the callback | ||||
|   // IMPORTANT: The heap allocation is REQUIRED and must not be removed as an optimization. | ||||
|   // The param pointer from ESP-IDF is only valid during the callback execution. | ||||
|   // Since BLE events are processed asynchronously in the main loop, we must create | ||||
|   // our own copy to ensure the data remains valid until the event is processed. | ||||
|   BLEEvent(esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p) { | ||||
|     this->type_ = GATTS; | ||||
|     this->event_.gatts.gatts_event = e; | ||||
|     this->event_.gatts.gatts_if = i; | ||||
|  | ||||
|     if (p == nullptr) { | ||||
|       this->event_.gatts.gatts_param = nullptr; | ||||
|       this->event_.gatts.data = nullptr; | ||||
|       return;  // Invalid event, but we can't log in header file | ||||
|     } | ||||
|  | ||||
|     // Heap-allocate param and data | ||||
|     // Heap allocation is used because GATTC/GATTS events are rare (<1% of events) | ||||
|     // while GAP events (99%) are stored inline to minimize memory usage | ||||
|     this->event_.gatts.gatts_param = new esp_ble_gatts_cb_param_t(*p); | ||||
|  | ||||
|     // Copy data for events that need it | ||||
|     switch (e) { | ||||
|       case ESP_GATTS_WRITE_EVT: | ||||
|         this->event_.gatts.data = new std::vector<uint8_t>(p->write.value, p->write.value + p->write.len); | ||||
|         this->event_.gatts.gatts_param->write.value = this->event_.gatts.data->data(); | ||||
|         break; | ||||
|       default: | ||||
|         this->event_.gatts.data = nullptr; | ||||
|         break; | ||||
|     } | ||||
|     this->init_gatts_data_(e, i, p); | ||||
|   } | ||||
|  | ||||
|   // Destructor to clean up heap allocations | ||||
|   ~BLEEvent() { | ||||
|     switch (this->type_) { | ||||
|       case GATTC: | ||||
|         delete this->event_.gattc.gattc_param; | ||||
|         delete this->event_.gattc.data; | ||||
|         break; | ||||
|       case GATTS: | ||||
|         delete this->event_.gatts.gatts_param; | ||||
|         delete this->event_.gatts.data; | ||||
|         break; | ||||
|       default: | ||||
|         break; | ||||
|   ~BLEEvent() { this->cleanup_heap_data(); } | ||||
|  | ||||
|   // Default constructor for pre-allocation in pool | ||||
|   BLEEvent() : type_(GAP) {} | ||||
|  | ||||
|   // Clean up any heap-allocated data | ||||
|   void cleanup_heap_data() { | ||||
|     if (this->type_ == GAP) { | ||||
|       return; | ||||
|     } | ||||
|     if (this->type_ == GATTC) { | ||||
|       delete this->event_.gattc.gattc_param; | ||||
|       delete this->event_.gattc.data; | ||||
|       this->event_.gattc.gattc_param = nullptr; | ||||
|       this->event_.gattc.data = nullptr; | ||||
|       return; | ||||
|     } | ||||
|     if (this->type_ == GATTS) { | ||||
|       delete this->event_.gatts.gatts_param; | ||||
|       delete this->event_.gatts.data; | ||||
|       this->event_.gatts.gatts_param = nullptr; | ||||
|       this->event_.gatts.data = nullptr; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // Load new event data for reuse (replaces previous event data) | ||||
|   void load_gap_event(esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) { | ||||
|     this->cleanup_heap_data(); | ||||
|     this->type_ = GAP; | ||||
|     this->init_gap_data_(e, p); | ||||
|   } | ||||
|  | ||||
|   void load_gattc_event(esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) { | ||||
|     this->cleanup_heap_data(); | ||||
|     this->type_ = GATTC; | ||||
|     this->init_gattc_data_(e, i, p); | ||||
|   } | ||||
|  | ||||
|   void load_gatts_event(esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p) { | ||||
|     this->cleanup_heap_data(); | ||||
|     this->type_ = GATTS; | ||||
|     this->init_gatts_data_(e, i, p); | ||||
|   } | ||||
|  | ||||
|   // Disable copy to prevent double-delete | ||||
| @@ -224,6 +180,119 @@ class BLEEvent { | ||||
|   esp_gap_ble_cb_event_t gap_event_type() const { return event_.gap.gap_event; } | ||||
|   const BLEScanResult &scan_result() const { return event_.gap.scan_result; } | ||||
|   esp_bt_status_t scan_complete_status() const { return event_.gap.scan_complete.status; } | ||||
|  | ||||
|  private: | ||||
|   // Initialize GAP event data | ||||
|   void init_gap_data_(esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) { | ||||
|     this->event_.gap.gap_event = e; | ||||
|  | ||||
|     if (p == nullptr) { | ||||
|       return;  // Invalid event, but we can't log in header file | ||||
|     } | ||||
|  | ||||
|     // Copy data based on event type | ||||
|     switch (e) { | ||||
|       case ESP_GAP_BLE_SCAN_RESULT_EVT: | ||||
|         memcpy(this->event_.gap.scan_result.bda, p->scan_rst.bda, sizeof(esp_bd_addr_t)); | ||||
|         this->event_.gap.scan_result.ble_addr_type = p->scan_rst.ble_addr_type; | ||||
|         this->event_.gap.scan_result.rssi = p->scan_rst.rssi; | ||||
|         this->event_.gap.scan_result.adv_data_len = p->scan_rst.adv_data_len; | ||||
|         this->event_.gap.scan_result.scan_rsp_len = p->scan_rst.scan_rsp_len; | ||||
|         this->event_.gap.scan_result.search_evt = p->scan_rst.search_evt; | ||||
|         memcpy(this->event_.gap.scan_result.ble_adv, p->scan_rst.ble_adv, | ||||
|                ESP_BLE_ADV_DATA_LEN_MAX + ESP_BLE_SCAN_RSP_DATA_LEN_MAX); | ||||
|         break; | ||||
|  | ||||
|       case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: | ||||
|         this->event_.gap.scan_complete.status = p->scan_param_cmpl.status; | ||||
|         break; | ||||
|  | ||||
|       case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT: | ||||
|         this->event_.gap.scan_complete.status = p->scan_start_cmpl.status; | ||||
|         break; | ||||
|  | ||||
|       case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT: | ||||
|         this->event_.gap.scan_complete.status = p->scan_stop_cmpl.status; | ||||
|         break; | ||||
|  | ||||
|       default: | ||||
|         // We only handle 4 GAP event types, others are dropped | ||||
|         break; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // Initialize GATTC event data | ||||
|   void init_gattc_data_(esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) { | ||||
|     this->event_.gattc.gattc_event = e; | ||||
|     this->event_.gattc.gattc_if = i; | ||||
|  | ||||
|     if (p == nullptr) { | ||||
|       this->event_.gattc.gattc_param = nullptr; | ||||
|       this->event_.gattc.data = nullptr; | ||||
|       return;  // Invalid event, but we can't log in header file | ||||
|     } | ||||
|  | ||||
|     // Heap-allocate param and data | ||||
|     // Heap allocation is used because GATTC/GATTS events are rare (<1% of events) | ||||
|     // while GAP events (99%) are stored inline to minimize memory usage | ||||
|     // IMPORTANT: This heap allocation provides clear ownership semantics: | ||||
|     // - The BLEEvent owns the allocated memory for its lifetime | ||||
|     // - The data remains valid from the BLE callback context until processed in the main loop | ||||
|     // - Without this copy, we'd have use-after-free bugs as ESP-IDF reuses the callback memory | ||||
|     this->event_.gattc.gattc_param = new esp_ble_gattc_cb_param_t(*p); | ||||
|  | ||||
|     // Copy data for events that need it | ||||
|     // The param struct contains pointers (e.g., notify.value) that point to temporary buffers. | ||||
|     // We must copy this data to ensure it remains valid when the event is processed later. | ||||
|     switch (e) { | ||||
|       case ESP_GATTC_NOTIFY_EVT: | ||||
|         this->event_.gattc.data = new std::vector<uint8_t>(p->notify.value, p->notify.value + p->notify.value_len); | ||||
|         this->event_.gattc.gattc_param->notify.value = this->event_.gattc.data->data(); | ||||
|         break; | ||||
|       case ESP_GATTC_READ_CHAR_EVT: | ||||
|       case ESP_GATTC_READ_DESCR_EVT: | ||||
|         this->event_.gattc.data = new std::vector<uint8_t>(p->read.value, p->read.value + p->read.value_len); | ||||
|         this->event_.gattc.gattc_param->read.value = this->event_.gattc.data->data(); | ||||
|         break; | ||||
|       default: | ||||
|         this->event_.gattc.data = nullptr; | ||||
|         break; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // Initialize GATTS event data | ||||
|   void init_gatts_data_(esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p) { | ||||
|     this->event_.gatts.gatts_event = e; | ||||
|     this->event_.gatts.gatts_if = i; | ||||
|  | ||||
|     if (p == nullptr) { | ||||
|       this->event_.gatts.gatts_param = nullptr; | ||||
|       this->event_.gatts.data = nullptr; | ||||
|       return;  // Invalid event, but we can't log in header file | ||||
|     } | ||||
|  | ||||
|     // Heap-allocate param and data | ||||
|     // Heap allocation is used because GATTC/GATTS events are rare (<1% of events) | ||||
|     // while GAP events (99%) are stored inline to minimize memory usage | ||||
|     // IMPORTANT: This heap allocation provides clear ownership semantics: | ||||
|     // - The BLEEvent owns the allocated memory for its lifetime | ||||
|     // - The data remains valid from the BLE callback context until processed in the main loop | ||||
|     // - Without this copy, we'd have use-after-free bugs as ESP-IDF reuses the callback memory | ||||
|     this->event_.gatts.gatts_param = new esp_ble_gatts_cb_param_t(*p); | ||||
|  | ||||
|     // Copy data for events that need it | ||||
|     // The param struct contains pointers (e.g., write.value) that point to temporary buffers. | ||||
|     // We must copy this data to ensure it remains valid when the event is processed later. | ||||
|     switch (e) { | ||||
|       case ESP_GATTS_WRITE_EVT: | ||||
|         this->event_.gatts.data = new std::vector<uint8_t>(p->write.value, p->write.value + p->write.len); | ||||
|         this->event_.gatts.gatts_param->write.value = this->event_.gatts.data->data(); | ||||
|         break; | ||||
|       default: | ||||
|         this->event_.gatts.data = nullptr; | ||||
|         break; | ||||
|     } | ||||
|   } | ||||
| }; | ||||
|  | ||||
| // BLEEvent total size: 84 bytes (80 byte union + 1 byte type + 3 bytes padding) | ||||
|   | ||||
							
								
								
									
										72
									
								
								esphome/components/esp32_ble/ble_event_pool.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								esphome/components/esp32_ble/ble_event_pool.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,72 @@ | ||||
| #pragma once | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
|  | ||||
| #include <atomic> | ||||
| #include <cstddef> | ||||
| #include "ble_event.h" | ||||
| #include "queue.h" | ||||
| #include "esphome/core/helpers.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace esp32_ble { | ||||
|  | ||||
| // BLE Event Pool - On-demand pool of BLEEvent objects to avoid heap fragmentation | ||||
| // Events are allocated on first use and reused thereafter, growing to peak usage | ||||
| template<uint8_t SIZE> class BLEEventPool { | ||||
|  public: | ||||
|   BLEEventPool() : total_created_(0) {} | ||||
|  | ||||
|   ~BLEEventPool() { | ||||
|     // Clean up any remaining events in the free list | ||||
|     BLEEvent *event; | ||||
|     while ((event = this->free_list_.pop()) != nullptr) { | ||||
|       delete event; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // Allocate an event from the pool | ||||
|   // Returns nullptr if pool is full | ||||
|   BLEEvent *allocate() { | ||||
|     // Try to get from free list first | ||||
|     BLEEvent *event = this->free_list_.pop(); | ||||
|     if (event != nullptr) | ||||
|       return event; | ||||
|  | ||||
|     // Need to create a new event | ||||
|     if (this->total_created_ >= SIZE) { | ||||
|       // Pool is at capacity | ||||
|       return nullptr; | ||||
|     } | ||||
|  | ||||
|     // Use internal RAM for better performance | ||||
|     RAMAllocator<BLEEvent> allocator(RAMAllocator<BLEEvent>::ALLOC_INTERNAL); | ||||
|     event = allocator.allocate(1); | ||||
|  | ||||
|     if (event == nullptr) { | ||||
|       // Memory allocation failed | ||||
|       return nullptr; | ||||
|     } | ||||
|  | ||||
|     // Placement new to construct the object | ||||
|     new (event) BLEEvent(); | ||||
|     this->total_created_++; | ||||
|     return event; | ||||
|   } | ||||
|  | ||||
|   // Return an event to the pool for reuse | ||||
|   void release(BLEEvent *event) { | ||||
|     if (event != nullptr) { | ||||
|       this->free_list_.push(event); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|  private: | ||||
|   LockFreeQueue<BLEEvent, SIZE> free_list_;  // Free events ready for reuse | ||||
|   uint8_t total_created_;                    // Total events created (high water mark) | ||||
| }; | ||||
|  | ||||
| }  // namespace esp32_ble | ||||
| }  // namespace esphome | ||||
|  | ||||
| #endif | ||||
| @@ -18,7 +18,7 @@ | ||||
| namespace esphome { | ||||
| namespace esp32_ble { | ||||
|  | ||||
| template<class T, size_t SIZE> class LockFreeQueue { | ||||
| template<class T, uint8_t SIZE> class LockFreeQueue { | ||||
|  public: | ||||
|   LockFreeQueue() : head_(0), tail_(0), dropped_count_(0) {} | ||||
|  | ||||
| @@ -26,8 +26,8 @@ template<class T, size_t SIZE> class LockFreeQueue { | ||||
|     if (element == nullptr) | ||||
|       return false; | ||||
|  | ||||
|     size_t current_tail = tail_.load(std::memory_order_relaxed); | ||||
|     size_t next_tail = (current_tail + 1) % SIZE; | ||||
|     uint8_t current_tail = tail_.load(std::memory_order_relaxed); | ||||
|     uint8_t next_tail = (current_tail + 1) % SIZE; | ||||
|  | ||||
|     if (next_tail == head_.load(std::memory_order_acquire)) { | ||||
|       // Buffer full | ||||
| @@ -41,7 +41,7 @@ template<class T, size_t SIZE> class LockFreeQueue { | ||||
|   } | ||||
|  | ||||
|   T *pop() { | ||||
|     size_t current_head = head_.load(std::memory_order_relaxed); | ||||
|     uint8_t current_head = head_.load(std::memory_order_relaxed); | ||||
|  | ||||
|     if (current_head == tail_.load(std::memory_order_acquire)) { | ||||
|       return nullptr;  // Empty | ||||
| @@ -53,27 +53,30 @@ template<class T, size_t SIZE> class LockFreeQueue { | ||||
|   } | ||||
|  | ||||
|   size_t size() const { | ||||
|     size_t tail = tail_.load(std::memory_order_acquire); | ||||
|     size_t head = head_.load(std::memory_order_acquire); | ||||
|     uint8_t tail = tail_.load(std::memory_order_acquire); | ||||
|     uint8_t head = head_.load(std::memory_order_acquire); | ||||
|     return (tail - head + SIZE) % SIZE; | ||||
|   } | ||||
|  | ||||
|   size_t get_and_reset_dropped_count() { return dropped_count_.exchange(0, std::memory_order_relaxed); } | ||||
|   uint16_t get_and_reset_dropped_count() { return dropped_count_.exchange(0, std::memory_order_relaxed); } | ||||
|  | ||||
|   void increment_dropped_count() { dropped_count_.fetch_add(1, std::memory_order_relaxed); } | ||||
|  | ||||
|   bool empty() const { return head_.load(std::memory_order_acquire) == tail_.load(std::memory_order_acquire); } | ||||
|  | ||||
|   bool full() const { | ||||
|     size_t next_tail = (tail_.load(std::memory_order_relaxed) + 1) % SIZE; | ||||
|     uint8_t next_tail = (tail_.load(std::memory_order_relaxed) + 1) % SIZE; | ||||
|     return next_tail == head_.load(std::memory_order_acquire); | ||||
|   } | ||||
|  | ||||
|  protected: | ||||
|   T *buffer_[SIZE]; | ||||
|   std::atomic<size_t> head_; | ||||
|   std::atomic<size_t> tail_; | ||||
|   std::atomic<size_t> dropped_count_; | ||||
|   // Atomic: written by producer (push/increment), read+reset by consumer (get_and_reset) | ||||
|   std::atomic<uint16_t> dropped_count_;  // 65535 max - more than enough for drop tracking | ||||
|   // Atomic: written by consumer (pop), read by producer (push) to check if full | ||||
|   std::atomic<uint8_t> head_; | ||||
|   // Atomic: written by producer (push), read by consumer (pop) to check if empty | ||||
|   std::atomic<uint8_t> tail_; | ||||
| }; | ||||
|  | ||||
| }  // namespace esp32_ble | ||||
|   | ||||
| @@ -22,6 +22,16 @@ void BLEClientBase::setup() { | ||||
|   this->connection_index_ = connection_index++; | ||||
| } | ||||
|  | ||||
| void BLEClientBase::set_state(espbt::ClientState st) { | ||||
|   ESP_LOGV(TAG, "[%d] [%s] Set state %d", this->connection_index_, this->address_str_.c_str(), (int) st); | ||||
|   ESPBTClient::set_state(st); | ||||
|  | ||||
|   if (st == espbt::ClientState::READY_TO_CONNECT) { | ||||
|     // Enable loop when we need to connect | ||||
|     this->enable_loop(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void BLEClientBase::loop() { | ||||
|   if (!esp32_ble::global_ble->is_active()) { | ||||
|     this->set_state(espbt::ClientState::INIT); | ||||
| @@ -37,9 +47,14 @@ void BLEClientBase::loop() { | ||||
|   } | ||||
|   // READY_TO_CONNECT means we have discovered the device | ||||
|   // and the scanner has been stopped by the tracker. | ||||
|   if (this->state_ == espbt::ClientState::READY_TO_CONNECT) { | ||||
|   else if (this->state_ == espbt::ClientState::READY_TO_CONNECT) { | ||||
|     this->connect(); | ||||
|   } | ||||
|   // If its idle, we can disable the loop as set_state | ||||
|   // will enable it again when we need to connect. | ||||
|   else if (this->state_ == espbt::ClientState::IDLE) { | ||||
|     this->disable_loop(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| float BLEClientBase::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH; } | ||||
|   | ||||
| @@ -93,6 +93,8 @@ class BLEClientBase : public espbt::ESPBTClient, public Component { | ||||
|  | ||||
|   bool check_addr(esp_bd_addr_t &addr) { return memcmp(addr, this->remote_bda_, sizeof(esp_bd_addr_t)) == 0; } | ||||
|  | ||||
|   void set_state(espbt::ClientState st) override; | ||||
|  | ||||
|  protected: | ||||
|   int gattc_if_; | ||||
|   esp_bd_addr_t remote_bda_; | ||||
|   | ||||
| @@ -1,25 +0,0 @@ | ||||
| #ifdef USE_ESP32 | ||||
| #include "esp32_hall.h" | ||||
| #include "esphome/core/log.h" | ||||
| #include "esphome/core/hal.h" | ||||
| #include <driver/adc.h> | ||||
|  | ||||
| namespace esphome { | ||||
| namespace esp32_hall { | ||||
|  | ||||
| static const char *const TAG = "esp32_hall"; | ||||
|  | ||||
| void ESP32HallSensor::update() { | ||||
|   adc1_config_width(ADC_WIDTH_BIT_12); | ||||
|   int value_int = hall_sensor_read(); | ||||
|   float value = (value_int / 4095.0f) * 10000.0f; | ||||
|   ESP_LOGD(TAG, "'%s': Got reading %.0f µT", this->name_.c_str(), value); | ||||
|   this->publish_state(value); | ||||
| } | ||||
| std::string ESP32HallSensor::unique_id() { return get_mac_address() + "-hall"; } | ||||
| void ESP32HallSensor::dump_config() { LOG_SENSOR("", "ESP32 Hall Sensor", this); } | ||||
|  | ||||
| }  // namespace esp32_hall | ||||
| }  // namespace esphome | ||||
|  | ||||
| #endif | ||||
| @@ -1,23 +0,0 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/components/sensor/sensor.h" | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
|  | ||||
| namespace esphome { | ||||
| namespace esp32_hall { | ||||
|  | ||||
| class ESP32HallSensor : public sensor::Sensor, public PollingComponent { | ||||
|  public: | ||||
|   void dump_config() override; | ||||
|  | ||||
|   void update() override; | ||||
|  | ||||
|   std::string unique_id() override; | ||||
| }; | ||||
|  | ||||
| }  // namespace esp32_hall | ||||
| }  // namespace esphome | ||||
|  | ||||
| #endif | ||||
| @@ -1,24 +0,0 @@ | ||||
| import esphome.codegen as cg | ||||
| from esphome.components import sensor | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import ICON_MAGNET, STATE_CLASS_MEASUREMENT, UNIT_MICROTESLA | ||||
|  | ||||
| DEPENDENCIES = ["esp32"] | ||||
|  | ||||
| esp32_hall_ns = cg.esphome_ns.namespace("esp32_hall") | ||||
| ESP32HallSensor = esp32_hall_ns.class_( | ||||
|     "ESP32HallSensor", sensor.Sensor, cg.PollingComponent | ||||
| ) | ||||
|  | ||||
| CONFIG_SCHEMA = sensor.sensor_schema( | ||||
|     ESP32HallSensor, | ||||
|     unit_of_measurement=UNIT_MICROTESLA, | ||||
|     icon=ICON_MAGNET, | ||||
|     accuracy_decimals=1, | ||||
|     state_class=STATE_CLASS_MEASUREMENT, | ||||
| ).extend(cv.polling_component_schema("60s")) | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     var = await sensor.new_sensor(config) | ||||
|     await cg.register_component(var, config) | ||||
| @@ -168,6 +168,8 @@ void ESP32ImprovComponent::loop() { | ||||
|     case improv::STATE_PROVISIONED: { | ||||
|       this->incoming_data_.clear(); | ||||
|       this->set_status_indicator_state_(false); | ||||
|       // Provisioning complete, no further loop execution needed | ||||
|       this->disable_loop(); | ||||
|       break; | ||||
|     } | ||||
|   } | ||||
| @@ -254,6 +256,7 @@ void ESP32ImprovComponent::start() { | ||||
|  | ||||
|   ESP_LOGD(TAG, "Setting Improv to start"); | ||||
|   this->should_start_ = true; | ||||
|   this->enable_loop(); | ||||
| } | ||||
|  | ||||
| void ESP32ImprovComponent::stop() { | ||||
|   | ||||
| @@ -125,6 +125,6 @@ async def to_code(config): | ||||
|     cg.add(var.set_max_temperature(config[CONF_MAX_TEMPERATURE])) | ||||
|     cg.add(var.set_min_temperature(config[CONF_MIN_TEMPERATURE])) | ||||
|  | ||||
|     cg.add_library("tonia/HeatpumpIR", "1.0.32") | ||||
|     cg.add_library("tonia/HeatpumpIR", "1.0.35") | ||||
|     if CORE.is_libretiny: | ||||
|         CORE.add_platformio_option("lib_ignore", "IRremoteESP8266") | ||||
|   | ||||
| @@ -116,5 +116,5 @@ async def to_code(config): | ||||
|  | ||||
|     cg.add_library("WiFiClientSecure", None) | ||||
|     cg.add_library("HTTPClient", None) | ||||
|     cg.add_library("esphome/ESP32-audioI2S", "2.2.0") | ||||
|     cg.add_library("esphome/ESP32-audioI2S", "2.3.0") | ||||
|     cg.add_build_flag("-DAUDIO_NO_SD_FS") | ||||
|   | ||||
| @@ -136,6 +136,7 @@ bool I2SAudioMicrophone::start_driver_() { | ||||
|   if (!this->parent_->try_lock()) { | ||||
|     return false;  // Waiting for another i2s to return lock | ||||
|   } | ||||
|   this->locked_driver_ = true; | ||||
|   esp_err_t err; | ||||
|  | ||||
| #ifdef USE_I2S_LEGACY | ||||
| @@ -340,7 +341,10 @@ void I2SAudioMicrophone::stop_driver_() { | ||||
|     this->rx_handle_ = nullptr; | ||||
|   } | ||||
| #endif | ||||
|   this->parent_->unlock(); | ||||
|   if (this->locked_driver_) { | ||||
|     this->parent_->unlock(); | ||||
|     this->locked_driver_ = false; | ||||
|   } | ||||
| } | ||||
|  | ||||
| void I2SAudioMicrophone::mic_task(void *params) { | ||||
| @@ -482,7 +486,8 @@ void I2SAudioMicrophone::loop() { | ||||
|       } | ||||
|  | ||||
|       if (!this->start_driver_()) { | ||||
|         this->status_momentary_error("Driver failed to start; retrying in 1 second", 1000); | ||||
|         ESP_LOGE(TAG, "Driver failed to start; retrying in 1 second"); | ||||
|         this->status_momentary_error("driver_fail", 1000); | ||||
|         this->stop_driver_();  // Stop/frees whatever possibly started | ||||
|         break; | ||||
|       } | ||||
| @@ -492,7 +497,8 @@ void I2SAudioMicrophone::loop() { | ||||
|                     &this->task_handle_); | ||||
|  | ||||
|         if (this->task_handle_ == nullptr) { | ||||
|           this->status_momentary_error("Task failed to start, retrying in 1 second", 1000); | ||||
|           ESP_LOGE(TAG, "Task failed to start, retrying in 1 second"); | ||||
|           this->status_momentary_error("task_fail", 1000); | ||||
|           this->stop_driver_();  // Stops the driver to return the lock; will be reloaded in next attempt | ||||
|         } | ||||
|       } | ||||
|   | ||||
| @@ -81,6 +81,7 @@ class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, pub | ||||
|   bool pdm_{false}; | ||||
|  | ||||
|   bool correct_dc_offset_; | ||||
|   bool locked_driver_{false}; | ||||
|   int32_t dc_offset_{0}; | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -173,9 +173,9 @@ def _notify_old_style(config): | ||||
|  | ||||
| # The dev and latest branches will be at *least* this version, which is what matters. | ||||
| ARDUINO_VERSIONS = { | ||||
|     "dev": (cv.Version(1, 7, 0), "https://github.com/libretiny-eu/libretiny.git"), | ||||
|     "latest": (cv.Version(1, 7, 0), "libretiny"), | ||||
|     "recommended": (cv.Version(1, 7, 0), None), | ||||
|     "dev": (cv.Version(1, 9, 1), "https://github.com/libretiny-eu/libretiny.git"), | ||||
|     "latest": (cv.Version(1, 9, 1), "libretiny"), | ||||
|     "recommended": (cv.Version(1, 9, 1), None), | ||||
| } | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -17,7 +17,7 @@ namespace light { | ||||
|  | ||||
| class LightOutput; | ||||
|  | ||||
| enum LightRestoreMode { | ||||
| enum LightRestoreMode : uint8_t { | ||||
|   LIGHT_RESTORE_DEFAULT_OFF, | ||||
|   LIGHT_RESTORE_DEFAULT_ON, | ||||
|   LIGHT_ALWAYS_OFF, | ||||
| @@ -212,12 +212,18 @@ class LightState : public EntityBase, public Component { | ||||
|  | ||||
|   /// Store the output to allow effects to have more access. | ||||
|   LightOutput *output_; | ||||
|   /// Value for storing the index of the currently active effect. 0 if no effect is active | ||||
|   uint32_t active_effect_index_{}; | ||||
|   /// The currently active transformer for this light (transition/flash). | ||||
|   std::unique_ptr<LightTransformer> transformer_{nullptr}; | ||||
|   /// Whether the light value should be written in the next cycle. | ||||
|   bool next_write_{true}; | ||||
|   /// List of effects for this light. | ||||
|   std::vector<LightEffect *> effects_; | ||||
|   /// Value for storing the index of the currently active effect. 0 if no effect is active | ||||
|   uint32_t active_effect_index_{}; | ||||
|   /// Default transition length for all transitions in ms. | ||||
|   uint32_t default_transition_length_{}; | ||||
|   /// Transition length to use for flash transitions. | ||||
|   uint32_t flash_transition_length_{}; | ||||
|   /// Gamma correction factor for the light. | ||||
|   float gamma_correct_{}; | ||||
|  | ||||
|   /// Object used to store the persisted values of the light. | ||||
|   ESPPreferenceObject rtc_; | ||||
| @@ -236,19 +242,13 @@ class LightState : public EntityBase, public Component { | ||||
|    */ | ||||
|   CallbackManager<void()> target_state_reached_callback_{}; | ||||
|  | ||||
|   /// Default transition length for all transitions in ms. | ||||
|   uint32_t default_transition_length_{}; | ||||
|   /// Transition length to use for flash transitions. | ||||
|   uint32_t flash_transition_length_{}; | ||||
|   /// Gamma correction factor for the light. | ||||
|   float gamma_correct_{}; | ||||
|   /// Restore mode of the light. | ||||
|   LightRestoreMode restore_mode_; | ||||
|   /// Initial state of the light. | ||||
|   optional<LightStateRTCState> initial_state_{}; | ||||
|   /// List of effects for this light. | ||||
|   std::vector<LightEffect *> effects_; | ||||
|  | ||||
|   /// Restore mode of the light. | ||||
|   LightRestoreMode restore_mode_; | ||||
|   /// Whether the light value should be written in the next cycle. | ||||
|   bool next_write_{true}; | ||||
|   // for effects, true if a transformer (transition) is active. | ||||
|   bool is_transformer_active_ = false; | ||||
| }; | ||||
|   | ||||
| @@ -178,18 +178,21 @@ void OnlineImage::update() { | ||||
|   if (this->format_ == ImageFormat::BMP) { | ||||
|     ESP_LOGD(TAG, "Allocating BMP decoder"); | ||||
|     this->decoder_ = make_unique<BmpDecoder>(this); | ||||
|     this->enable_loop(); | ||||
|   } | ||||
| #endif  // USE_ONLINE_IMAGE_BMP_SUPPORT | ||||
| #ifdef USE_ONLINE_IMAGE_JPEG_SUPPORT | ||||
|   if (this->format_ == ImageFormat::JPEG) { | ||||
|     ESP_LOGD(TAG, "Allocating JPEG decoder"); | ||||
|     this->decoder_ = esphome::make_unique<JpegDecoder>(this); | ||||
|     this->enable_loop(); | ||||
|   } | ||||
| #endif  // USE_ONLINE_IMAGE_JPEG_SUPPORT | ||||
| #ifdef USE_ONLINE_IMAGE_PNG_SUPPORT | ||||
|   if (this->format_ == ImageFormat::PNG) { | ||||
|     ESP_LOGD(TAG, "Allocating PNG decoder"); | ||||
|     this->decoder_ = make_unique<PngDecoder>(this); | ||||
|     this->enable_loop(); | ||||
|   } | ||||
| #endif  // USE_ONLINE_IMAGE_PNG_SUPPORT | ||||
|  | ||||
| @@ -212,6 +215,7 @@ void OnlineImage::update() { | ||||
| void OnlineImage::loop() { | ||||
|   if (!this->decoder_) { | ||||
|     // Not decoding at the moment => nothing to do. | ||||
|     this->disable_loop(); | ||||
|     return; | ||||
|   } | ||||
|   if (!this->downloader_ || this->decoder_->is_finished()) { | ||||
|   | ||||
| @@ -12,6 +12,8 @@ class IntervalSyncer : public Component { | ||||
|   void setup() override { | ||||
|     if (this->write_interval_ != 0) { | ||||
|       set_interval(this->write_interval_, []() { global_preferences->sync(); }); | ||||
|       // When using interval-based syncing, we don't need the loop | ||||
|       this->disable_loop(); | ||||
|     } | ||||
|   } | ||||
|   void loop() override { | ||||
|   | ||||
| @@ -40,7 +40,7 @@ class PrometheusHandler : public AsyncWebHandler, public Component { | ||||
|    */ | ||||
|   void add_label_name(EntityBase *obj, const std::string &value) { relabel_map_name_.insert({obj, value}); } | ||||
|  | ||||
|   bool canHandle(AsyncWebServerRequest *request) override { | ||||
|   bool canHandle(AsyncWebServerRequest *request) const override { | ||||
|     if (request->method() == HTTP_GET) { | ||||
|       if (request->url() == "/metrics") | ||||
|         return true; | ||||
|   | ||||
| @@ -142,8 +142,10 @@ void Rtttl::stop() { | ||||
| } | ||||
|  | ||||
| void Rtttl::loop() { | ||||
|   if (this->note_duration_ == 0 || this->state_ == State::STATE_STOPPED) | ||||
|   if (this->note_duration_ == 0 || this->state_ == State::STATE_STOPPED) { | ||||
|     this->disable_loop(); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
| #ifdef USE_SPEAKER | ||||
|   if (this->speaker_ != nullptr) { | ||||
| @@ -391,6 +393,11 @@ void Rtttl::set_state_(State state) { | ||||
|   this->state_ = state; | ||||
|   ESP_LOGV(TAG, "State changed from %s to %s", LOG_STR_ARG(state_to_string(old_state)), | ||||
|            LOG_STR_ARG(state_to_string(state))); | ||||
|  | ||||
|   // Clear loop_done when transitioning from STOPPED to any other state | ||||
|   if (old_state == State::STATE_STOPPED && state != State::STATE_STOPPED) { | ||||
|     this->enable_loop(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| }  // namespace rtttl | ||||
|   | ||||
| @@ -42,6 +42,8 @@ void SafeModeComponent::loop() { | ||||
|     ESP_LOGI(TAG, "Boot seems successful; resetting boot loop counter"); | ||||
|     this->clean_rtc(); | ||||
|     this->boot_successful_ = true; | ||||
|     // Disable loop since we no longer need to check | ||||
|     this->disable_loop(); | ||||
|   } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -67,6 +67,12 @@ void SNTPComponent::loop() { | ||||
|            time.minute, time.second); | ||||
|   this->time_sync_callback_.call(); | ||||
|   this->has_time_ = true; | ||||
|  | ||||
| #ifdef USE_ESP_IDF | ||||
|   // On ESP-IDF, time sync is permanent and update() doesn't force resync | ||||
|   // Time is now synchronized, no need to check anymore | ||||
|   this->disable_loop(); | ||||
| #endif | ||||
| } | ||||
|  | ||||
| }  // namespace sntp | ||||
|   | ||||
| @@ -3,7 +3,6 @@ | ||||
|  | ||||
| namespace esphome { | ||||
| namespace spi { | ||||
|  | ||||
| #ifdef USE_ARDUINO | ||||
|  | ||||
| static const char *const TAG = "spi-esp-arduino"; | ||||
| @@ -38,17 +37,31 @@ class SPIDelegateHw : public SPIDelegate { | ||||
|  | ||||
|   void write16(uint16_t data) override { this->channel_->transfer16(data); } | ||||
|  | ||||
| #ifdef USE_RP2040 | ||||
|   void write_array(const uint8_t *ptr, size_t length) override { | ||||
|     // avoid overwriting the supplied buffer | ||||
|     uint8_t *rxbuf = new uint8_t[length];  // NOLINT(cppcoreguidelines-owning-memory) | ||||
|     memcpy(rxbuf, ptr, length); | ||||
|     this->channel_->transfer((void *) rxbuf, length); | ||||
|     delete[] rxbuf;  // NOLINT(cppcoreguidelines-owning-memory) | ||||
|   } | ||||
|     if (length == 1) { | ||||
|       this->channel_->transfer(*ptr); | ||||
|       return; | ||||
|     } | ||||
| #ifdef USE_RP2040 | ||||
|     // avoid overwriting the supplied buffer. Use vector for automatic deallocation | ||||
|     auto rxbuf = std::vector<uint8_t>(length); | ||||
|     memcpy(rxbuf.data(), ptr, length); | ||||
|     this->channel_->transfer((void *) rxbuf.data(), length); | ||||
| #elif defined(USE_ESP8266) | ||||
|     // ESP8266 SPI library requires the pointer to be word aligned, but the data may not be | ||||
|     // so we need to copy the data to a temporary buffer | ||||
|     if (reinterpret_cast<uintptr_t>(ptr) & 0x3) { | ||||
|       ESP_LOGVV(TAG, "SPI write buffer not word aligned, copying to temporary buffer"); | ||||
|       auto txbuf = std::vector<uint8_t>(length); | ||||
|       memcpy(txbuf.data(), ptr, length); | ||||
|       this->channel_->writeBytes(txbuf.data(), length); | ||||
|     } else { | ||||
|       this->channel_->writeBytes(ptr, length); | ||||
|     } | ||||
| #else | ||||
|   void write_array(const uint8_t *ptr, size_t length) override { this->channel_->writeBytes(ptr, length); } | ||||
|     this->channel_->writeBytes(ptr, length); | ||||
| #endif | ||||
|   } | ||||
|  | ||||
|   void read_array(uint8_t *ptr, size_t length) override { this->channel_->transfer(ptr, length); } | ||||
|  | ||||
|   | ||||
| @@ -21,7 +21,7 @@ const int RESTORE_MODE_PERSISTENT_MASK = 0x02; | ||||
| const int RESTORE_MODE_INVERTED_MASK = 0x04; | ||||
| const int RESTORE_MODE_DISABLED_MASK = 0x08; | ||||
|  | ||||
| enum SwitchRestoreMode { | ||||
| enum SwitchRestoreMode : uint8_t { | ||||
|   SWITCH_ALWAYS_OFF = !RESTORE_MODE_ON_MASK, | ||||
|   SWITCH_ALWAYS_ON = RESTORE_MODE_ON_MASK, | ||||
|   SWITCH_RESTORE_DEFAULT_OFF = RESTORE_MODE_PERSISTENT_MASK, | ||||
| @@ -49,12 +49,12 @@ class Switch : public EntityBase, public EntityBase_DeviceClass { | ||||
|    */ | ||||
|   void publish_state(bool state); | ||||
|  | ||||
|   /// The current reported state of the binary sensor. | ||||
|   bool state; | ||||
|  | ||||
|   /// Indicates whether or not state is to be retrieved from flash and how | ||||
|   SwitchRestoreMode restore_mode{SWITCH_RESTORE_DEFAULT_OFF}; | ||||
|  | ||||
|   /// The current reported state of the binary sensor. | ||||
|   bool state; | ||||
|  | ||||
|   /** Turn this switch on. This is called by the front-end. | ||||
|    * | ||||
|    * For implementing switches, please override write_state. | ||||
| @@ -123,10 +123,16 @@ class Switch : public EntityBase, public EntityBase_DeviceClass { | ||||
|    */ | ||||
|   virtual void write_state(bool state) = 0; | ||||
|  | ||||
|   CallbackManager<void(bool)> state_callback_{}; | ||||
|   bool inverted_{false}; | ||||
|   Deduplicator<bool> publish_dedup_; | ||||
|   // Pointer first (4 bytes) | ||||
|   ESPPreferenceObject rtc_; | ||||
|  | ||||
|   // CallbackManager (12 bytes on 32-bit - contains vector) | ||||
|   CallbackManager<void(bool)> state_callback_{}; | ||||
|  | ||||
|   // Small types grouped together | ||||
|   Deduplicator<bool> publish_dedup_;  // 2 bytes (bool has_value_ + bool last_value_) | ||||
|   bool inverted_{false};              // 1 byte | ||||
|   // Total: 3 bytes, 1 byte padding | ||||
| }; | ||||
|  | ||||
| #define LOG_SWITCH(prefix, type, obj) log_switch((TAG), (prefix), LOG_STR_LITERAL(type), (obj)) | ||||
|   | ||||
| @@ -24,8 +24,10 @@ void TLC5971::dump_config() { | ||||
| } | ||||
|  | ||||
| void TLC5971::loop() { | ||||
|   if (!this->update_) | ||||
|   if (!this->update_) { | ||||
|     this->disable_loop(); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   uint32_t command; | ||||
|  | ||||
| @@ -93,6 +95,7 @@ void TLC5971::set_channel_value(uint16_t channel, uint16_t value) { | ||||
|     return; | ||||
|   if (this->pwm_amounts_[channel] != value) { | ||||
|     this->update_ = true; | ||||
|     this->enable_loop(); | ||||
|   } | ||||
|   this->pwm_amounts_[channel] = value; | ||||
| } | ||||
|   | ||||
| @@ -91,7 +91,7 @@ void DeferredUpdateEventSource::process_deferred_queue_() { | ||||
|   while (!deferred_queue_.empty()) { | ||||
|     DeferredEvent &de = deferred_queue_.front(); | ||||
|     std::string message = de.message_generator_(web_server_, de.source_); | ||||
|     if (this->try_send(message.c_str(), "state")) { | ||||
|     if (this->send(message.c_str(), "state") != DISCARDED) { | ||||
|       // O(n) but memory efficiency is more important than speed here which is why std::vector was chosen | ||||
|       deferred_queue_.erase(deferred_queue_.begin()); | ||||
|     } else { | ||||
| @@ -131,7 +131,7 @@ void DeferredUpdateEventSource::deferrable_send_state(void *source, const char * | ||||
|     deq_push_back_with_dedup_(source, message_generator); | ||||
|   } else { | ||||
|     std::string message = message_generator(web_server_, source); | ||||
|     if (!this->try_send(message.c_str(), "state")) { | ||||
|     if (this->send(message.c_str(), "state") == DISCARDED) { | ||||
|       deq_push_back_with_dedup_(source, message_generator); | ||||
|     } | ||||
|   } | ||||
| @@ -171,8 +171,8 @@ void DeferredUpdateEventSourceList::add_new_client(WebServer *ws, AsyncWebServer | ||||
|     ws->defer([this, ws, es]() { this->on_client_connect_(ws, es); }); | ||||
|   }); | ||||
|  | ||||
|   es->onDisconnect([this, ws](AsyncEventSource *source, AsyncEventSourceClient *client) { | ||||
|     ws->defer([this, source]() { this->on_client_disconnect_((DeferredUpdateEventSource *) source); }); | ||||
|   es->onDisconnect([this, ws, es](AsyncEventSourceClient *client) { | ||||
|     ws->defer([this, es]() { this->on_client_disconnect_((DeferredUpdateEventSource *) es); }); | ||||
|   }); | ||||
|  | ||||
|   es->handleRequest(request); | ||||
| @@ -291,14 +291,23 @@ float WebServer::get_setup_priority() const { return setup_priority::WIFI - 1.0f | ||||
|  | ||||
| #ifdef USE_WEBSERVER_LOCAL | ||||
| void WebServer::handle_index_request(AsyncWebServerRequest *request) { | ||||
| #ifndef USE_ESP8266 | ||||
|   AsyncWebServerResponse *response = request->beginResponse(200, "text/html", INDEX_GZ, sizeof(INDEX_GZ)); | ||||
| #else | ||||
|   AsyncWebServerResponse *response = request->beginResponse_P(200, "text/html", INDEX_GZ, sizeof(INDEX_GZ)); | ||||
| #endif | ||||
|   response->addHeader("Content-Encoding", "gzip"); | ||||
|   request->send(response); | ||||
| } | ||||
| #elif USE_WEBSERVER_VERSION >= 2 | ||||
| void WebServer::handle_index_request(AsyncWebServerRequest *request) { | ||||
| #ifndef USE_ESP8266 | ||||
|   AsyncWebServerResponse *response = | ||||
|       request->beginResponse(200, "text/html", ESPHOME_WEBSERVER_INDEX_HTML, ESPHOME_WEBSERVER_INDEX_HTML_SIZE); | ||||
| #else | ||||
|   AsyncWebServerResponse *response = | ||||
|       request->beginResponse_P(200, "text/html", ESPHOME_WEBSERVER_INDEX_HTML, ESPHOME_WEBSERVER_INDEX_HTML_SIZE); | ||||
| #endif | ||||
|   // No gzip header here because the HTML file is so small | ||||
|   request->send(response); | ||||
| } | ||||
| @@ -317,8 +326,13 @@ void WebServer::handle_pna_cors_request(AsyncWebServerRequest *request) { | ||||
|  | ||||
| #ifdef USE_WEBSERVER_CSS_INCLUDE | ||||
| void WebServer::handle_css_request(AsyncWebServerRequest *request) { | ||||
| #ifndef USE_ESP8266 | ||||
|   AsyncWebServerResponse *response = | ||||
|       request->beginResponse(200, "text/css", ESPHOME_WEBSERVER_CSS_INCLUDE, ESPHOME_WEBSERVER_CSS_INCLUDE_SIZE); | ||||
| #else | ||||
|   AsyncWebServerResponse *response = | ||||
|       request->beginResponse_P(200, "text/css", ESPHOME_WEBSERVER_CSS_INCLUDE, ESPHOME_WEBSERVER_CSS_INCLUDE_SIZE); | ||||
| #endif | ||||
|   response->addHeader("Content-Encoding", "gzip"); | ||||
|   request->send(response); | ||||
| } | ||||
| @@ -326,8 +340,13 @@ void WebServer::handle_css_request(AsyncWebServerRequest *request) { | ||||
|  | ||||
| #ifdef USE_WEBSERVER_JS_INCLUDE | ||||
| void WebServer::handle_js_request(AsyncWebServerRequest *request) { | ||||
| #ifndef USE_ESP8266 | ||||
|   AsyncWebServerResponse *response = | ||||
|       request->beginResponse(200, "text/javascript", ESPHOME_WEBSERVER_JS_INCLUDE, ESPHOME_WEBSERVER_JS_INCLUDE_SIZE); | ||||
| #else | ||||
|   AsyncWebServerResponse *response = | ||||
|       request->beginResponse_P(200, "text/javascript", ESPHOME_WEBSERVER_JS_INCLUDE, ESPHOME_WEBSERVER_JS_INCLUDE_SIZE); | ||||
| #endif | ||||
|   response->addHeader("Content-Encoding", "gzip"); | ||||
|   request->send(response); | ||||
| } | ||||
| @@ -1837,7 +1856,7 @@ std::string WebServer::update_json(update::UpdateEntity *obj, JsonDetail start_c | ||||
| } | ||||
| #endif | ||||
|  | ||||
| bool WebServer::canHandle(AsyncWebServerRequest *request) { | ||||
| bool WebServer::canHandle(AsyncWebServerRequest *request) const { | ||||
|   if (request->url() == "/") | ||||
|     return true; | ||||
|  | ||||
| @@ -1859,12 +1878,6 @@ bool WebServer::canHandle(AsyncWebServerRequest *request) { | ||||
|  | ||||
| #ifdef USE_WEBSERVER_PRIVATE_NETWORK_ACCESS | ||||
|   if (request->method() == HTTP_OPTIONS && request->hasHeader(HEADER_CORS_REQ_PNA)) { | ||||
| #ifdef USE_ARDUINO | ||||
|     // Header needs to be added to interesting header list for it to not be | ||||
|     // nuked by the time we handle the request later. | ||||
|     // Only required in Arduino framework. | ||||
|     request->addInterestingHeader(HEADER_CORS_REQ_PNA); | ||||
| #endif | ||||
|     return true; | ||||
|   } | ||||
| #endif | ||||
| @@ -2145,7 +2158,7 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) { | ||||
| #endif | ||||
| } | ||||
|  | ||||
| bool WebServer::isRequestHandlerTrivial() { return false; } | ||||
| bool WebServer::isRequestHandlerTrivial() const { return false; } | ||||
|  | ||||
| void WebServer::add_entity_config(EntityBase *entity, float weight, uint64_t group) { | ||||
|   this->sorting_entitys_[entity] = SortingComponents{weight, group}; | ||||
|   | ||||
| @@ -99,7 +99,7 @@ class DeferredUpdateEventSource : public AsyncEventSource { | ||||
|  protected: | ||||
|   // surface a couple methods from the base class | ||||
|   using AsyncEventSource::handleRequest; | ||||
|   using AsyncEventSource::try_send; | ||||
|   using AsyncEventSource::send; | ||||
|  | ||||
|   ListEntitiesIterator entities_iterator_; | ||||
|   // vector is used very specifically for its zero memory overhead even though items are popped from the front (memory | ||||
| @@ -468,11 +468,11 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { | ||||
| #endif | ||||
|  | ||||
|   /// Override the web handler's canHandle method. | ||||
|   bool canHandle(AsyncWebServerRequest *request) override; | ||||
|   bool canHandle(AsyncWebServerRequest *request) const override; | ||||
|   /// Override the web handler's handleRequest method. | ||||
|   void handleRequest(AsyncWebServerRequest *request) override; | ||||
|   /// This web handle is not trivial. | ||||
|   bool isRequestHandlerTrivial() override;  // NOLINT(readability-identifier-naming) | ||||
|   bool isRequestHandlerTrivial() const override;  // NOLINT(readability-identifier-naming) | ||||
|  | ||||
|   void add_entity_config(EntityBase *entity, float weight, uint64_t group); | ||||
|   void add_sorting_group(uint64_t group_id, const std::string &group_name, float weight); | ||||
|   | ||||
| @@ -36,5 +36,7 @@ async def to_code(config): | ||||
|             cg.add_library("WiFi", None) | ||||
|             cg.add_library("FS", None) | ||||
|             cg.add_library("Update", None) | ||||
|         # https://github.com/esphome/ESPAsyncWebServer/blob/master/library.json | ||||
|         cg.add_library("esphome/ESPAsyncWebServer-esphome", "3.3.0") | ||||
|         if CORE.is_esp8266: | ||||
|             cg.add_library("ESP8266WiFi", None) | ||||
|         # https://github.com/ESP32Async/ESPAsyncWebServer/blob/main/library.json | ||||
|         cg.add_library("ESP32Async/ESPAsyncWebServer", "3.7.8") | ||||
|   | ||||
| @@ -23,7 +23,7 @@ class MiddlewareHandler : public AsyncWebHandler { | ||||
|  public: | ||||
|   MiddlewareHandler(AsyncWebHandler *next) : next_(next) {} | ||||
|  | ||||
|   bool canHandle(AsyncWebServerRequest *request) override { return next_->canHandle(request); } | ||||
|   bool canHandle(AsyncWebServerRequest *request) const override { return next_->canHandle(request); } | ||||
|   void handleRequest(AsyncWebServerRequest *request) override { next_->handleRequest(request); } | ||||
|   void handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len, | ||||
|                     bool final) override { | ||||
| @@ -32,7 +32,7 @@ class MiddlewareHandler : public AsyncWebHandler { | ||||
|   void handleBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) override { | ||||
|     next_->handleBody(request, data, len, index, total); | ||||
|   } | ||||
|   bool isRequestHandlerTrivial() override { return next_->isRequestHandlerTrivial(); } | ||||
|   bool isRequestHandlerTrivial() const override { return next_->isRequestHandlerTrivial(); } | ||||
|  | ||||
|  protected: | ||||
|   AsyncWebHandler *next_; | ||||
| @@ -131,12 +131,12 @@ class OTARequestHandler : public AsyncWebHandler { | ||||
|   void handleRequest(AsyncWebServerRequest *request) override; | ||||
|   void handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len, | ||||
|                     bool final) override; | ||||
|   bool canHandle(AsyncWebServerRequest *request) override { | ||||
|   bool canHandle(AsyncWebServerRequest *request) const override { | ||||
|     return request->url() == "/update" && request->method() == HTTP_POST; | ||||
|   } | ||||
|  | ||||
|   // NOLINTNEXTLINE(readability-identifier-naming) | ||||
|   bool isRequestHandlerTrivial() override { return false; } | ||||
|   bool isRequestHandlerTrivial() const override { return false; } | ||||
|  | ||||
|  protected: | ||||
|   uint32_t last_ota_progress_{0}; | ||||
|   | ||||
| @@ -135,8 +135,8 @@ class AsyncWebServerRequest { | ||||
|     return res; | ||||
|   } | ||||
|   // NOLINTNEXTLINE(readability-identifier-naming) | ||||
|   AsyncWebServerResponse *beginResponse_P(int code, const char *content_type, const uint8_t *data, | ||||
|                                           const size_t data_size) { | ||||
|   AsyncWebServerResponse *beginResponse(int code, const char *content_type, const uint8_t *data, | ||||
|                                         const size_t data_size) { | ||||
|     auto *res = new AsyncWebServerResponseProgmem(this, data, data_size);  // NOLINT(cppcoreguidelines-owning-memory) | ||||
|     this->init_response_(res, code, content_type); | ||||
|     return res; | ||||
| @@ -211,7 +211,7 @@ class AsyncWebHandler { | ||||
|  public: | ||||
|   virtual ~AsyncWebHandler() {} | ||||
|   // NOLINTNEXTLINE(readability-identifier-naming) | ||||
|   virtual bool canHandle(AsyncWebServerRequest *request) { return false; } | ||||
|   virtual bool canHandle(AsyncWebServerRequest *request) const { return false; } | ||||
|   // NOLINTNEXTLINE(readability-identifier-naming) | ||||
|   virtual void handleRequest(AsyncWebServerRequest *request) {} | ||||
|   // NOLINTNEXTLINE(readability-identifier-naming) | ||||
| @@ -220,7 +220,7 @@ class AsyncWebHandler { | ||||
|   // NOLINTNEXTLINE(readability-identifier-naming) | ||||
|   virtual void handleBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) {} | ||||
|   // NOLINTNEXTLINE(readability-identifier-naming) | ||||
|   virtual bool isRequestHandlerTrivial() { return true; } | ||||
|   virtual bool isRequestHandlerTrivial() const { return true; } | ||||
| }; | ||||
|  | ||||
| #ifdef USE_WEBSERVER | ||||
| @@ -290,7 +290,7 @@ class AsyncEventSource : public AsyncWebHandler { | ||||
|   ~AsyncEventSource() override; | ||||
|  | ||||
|   // NOLINTNEXTLINE(readability-identifier-naming) | ||||
|   bool canHandle(AsyncWebServerRequest *request) override { | ||||
|   bool canHandle(AsyncWebServerRequest *request) const override { | ||||
|     return request->method() == HTTP_GET && request->url() == this->url_; | ||||
|   } | ||||
|   // NOLINTNEXTLINE(readability-identifier-naming) | ||||
|   | ||||
| @@ -91,6 +91,13 @@ bool parse_xiaomi_value(uint16_t value_type, const uint8_t *data, uint8_t value_ | ||||
|   // MiaoMiaoce humidity, 1 byte, 8-bit unsigned integer, 1 % | ||||
|   else if ((value_type == 0x4C02) && (value_length == 1)) { | ||||
|     result.humidity = data[0]; | ||||
|   } | ||||
|   // XMWSDJ04MMC humidity, 4 bytes, float, 0.1 °C | ||||
|   else if ((value_type == 0x4C08) && (value_length == 4)) { | ||||
|     const uint32_t int_number = encode_uint32(data[3], data[2], data[1], data[0]); | ||||
|     float humidity; | ||||
|     std::memcpy(&humidity, &int_number, sizeof(humidity)); | ||||
|     result.humidity = humidity; | ||||
|   } else { | ||||
|     return false; | ||||
|   } | ||||
| @@ -219,6 +226,11 @@ optional<XiaomiParseResult> parse_xiaomi_header(const esp32_ble_tracker::Service | ||||
|   } else if (device_uuid == 0x055b) {  // small square body, segment LCD, encrypted | ||||
|     result.type = XiaomiParseResult::TYPE_LYWSD03MMC; | ||||
|     result.name = "LYWSD03MMC"; | ||||
|   } else if (device_uuid == 0x1203) {  // small square body, e-ink display, encrypted | ||||
|     result.type = XiaomiParseResult::TYPE_XMWSDJ04MMC; | ||||
|     result.name = "XMWSDJ04MMC"; | ||||
|     if (raw.size() == 19) | ||||
|       result.raw_offset -= 6; | ||||
|   } else if (device_uuid == 0x07f6) {  // Xiaomi-Yeelight BLE nightlight | ||||
|     result.type = XiaomiParseResult::TYPE_MJYD02YLA; | ||||
|     result.name = "MJYD02YLA"; | ||||
|   | ||||
| @@ -20,6 +20,7 @@ struct XiaomiParseResult { | ||||
|     TYPE_LYWSD02MMC, | ||||
|     TYPE_CGG1, | ||||
|     TYPE_LYWSD03MMC, | ||||
|     TYPE_XMWSDJ04MMC, | ||||
|     TYPE_CGD1, | ||||
|     TYPE_CGDK2, | ||||
|     TYPE_JQJCY01YM, | ||||
|   | ||||
							
								
								
									
										77
									
								
								esphome/components/xiaomi_xmwsdj04mmc/sensor.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								esphome/components/xiaomi_xmwsdj04mmc/sensor.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,77 @@ | ||||
| import esphome.codegen as cg | ||||
| from esphome.components import esp32_ble_tracker, sensor | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
|     CONF_BATTERY_LEVEL, | ||||
|     CONF_BINDKEY, | ||||
|     CONF_HUMIDITY, | ||||
|     CONF_ID, | ||||
|     CONF_MAC_ADDRESS, | ||||
|     CONF_TEMPERATURE, | ||||
|     DEVICE_CLASS_BATTERY, | ||||
|     DEVICE_CLASS_HUMIDITY, | ||||
|     DEVICE_CLASS_TEMPERATURE, | ||||
|     ENTITY_CATEGORY_DIAGNOSTIC, | ||||
|     STATE_CLASS_MEASUREMENT, | ||||
|     UNIT_CELSIUS, | ||||
|     UNIT_PERCENT, | ||||
| ) | ||||
|  | ||||
| AUTO_LOAD = ["xiaomi_ble"] | ||||
| CODEOWNERS = ["@medusalix"] | ||||
| DEPENDENCIES = ["esp32_ble_tracker"] | ||||
|  | ||||
| xiaomi_xmwsdj04mmc_ns = cg.esphome_ns.namespace("xiaomi_xmwsdj04mmc") | ||||
| XiaomiXMWSDJ04MMC = xiaomi_xmwsdj04mmc_ns.class_( | ||||
|     "XiaomiXMWSDJ04MMC", esp32_ble_tracker.ESPBTDeviceListener, cg.Component | ||||
| ) | ||||
|  | ||||
| CONFIG_SCHEMA = ( | ||||
|     cv.Schema( | ||||
|         { | ||||
|             cv.GenerateID(): cv.declare_id(XiaomiXMWSDJ04MMC), | ||||
|             cv.Required(CONF_BINDKEY): cv.bind_key, | ||||
|             cv.Required(CONF_MAC_ADDRESS): cv.mac_address, | ||||
|             cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( | ||||
|                 unit_of_measurement=UNIT_CELSIUS, | ||||
|                 accuracy_decimals=1, | ||||
|                 device_class=DEVICE_CLASS_TEMPERATURE, | ||||
|                 state_class=STATE_CLASS_MEASUREMENT, | ||||
|             ), | ||||
|             cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( | ||||
|                 unit_of_measurement=UNIT_PERCENT, | ||||
|                 accuracy_decimals=0, | ||||
|                 device_class=DEVICE_CLASS_HUMIDITY, | ||||
|                 state_class=STATE_CLASS_MEASUREMENT, | ||||
|             ), | ||||
|             cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema( | ||||
|                 unit_of_measurement=UNIT_PERCENT, | ||||
|                 accuracy_decimals=0, | ||||
|                 device_class=DEVICE_CLASS_BATTERY, | ||||
|                 state_class=STATE_CLASS_MEASUREMENT, | ||||
|                 entity_category=ENTITY_CATEGORY_DIAGNOSTIC, | ||||
|             ), | ||||
|         } | ||||
|     ) | ||||
|     .extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA) | ||||
|     .extend(cv.COMPONENT_SCHEMA) | ||||
| ) | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     await cg.register_component(var, config) | ||||
|     await esp32_ble_tracker.register_ble_device(var, config) | ||||
|  | ||||
|     cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) | ||||
|     cg.add(var.set_bindkey(config[CONF_BINDKEY])) | ||||
|  | ||||
|     if temperature_config := config.get(CONF_TEMPERATURE): | ||||
|         sens = await sensor.new_sensor(temperature_config) | ||||
|         cg.add(var.set_temperature(sens)) | ||||
|     if humidity_config := config.get(CONF_HUMIDITY): | ||||
|         sens = await sensor.new_sensor(humidity_config) | ||||
|         cg.add(var.set_humidity(sens)) | ||||
|     if battery_level_config := config.get(CONF_BATTERY_LEVEL): | ||||
|         sens = await sensor.new_sensor(battery_level_config) | ||||
|         cg.add(var.set_battery_level(sens)) | ||||
							
								
								
									
										77
									
								
								esphome/components/xiaomi_xmwsdj04mmc/xiaomi_xmwsdj04mmc.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								esphome/components/xiaomi_xmwsdj04mmc/xiaomi_xmwsdj04mmc.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,77 @@ | ||||
| #include "xiaomi_xmwsdj04mmc.h" | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
|  | ||||
| namespace esphome { | ||||
| namespace xiaomi_xmwsdj04mmc { | ||||
|  | ||||
| static const char *const TAG = "xiaomi_xmwsdj04mmc"; | ||||
|  | ||||
| void XiaomiXMWSDJ04MMC::dump_config() { | ||||
|   ESP_LOGCONFIG(TAG, "Xiaomi XMWSDJ04MMC"); | ||||
|   ESP_LOGCONFIG(TAG, "  Bindkey: %s", format_hex_pretty(this->bindkey_, 16).c_str()); | ||||
|   LOG_SENSOR("  ", "Temperature", this->temperature_); | ||||
|   LOG_SENSOR("  ", "Humidity", this->humidity_); | ||||
|   LOG_SENSOR("  ", "Battery Level", this->battery_level_); | ||||
| } | ||||
|  | ||||
| bool XiaomiXMWSDJ04MMC::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { | ||||
|   if (device.address_uint64() != this->address_) { | ||||
|     ESP_LOGVV(TAG, "parse_device(): unknown MAC address."); | ||||
|     return false; | ||||
|   } | ||||
|   ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); | ||||
|  | ||||
|   bool success = false; | ||||
|   for (auto &service_data : device.get_service_datas()) { | ||||
|     auto res = xiaomi_ble::parse_xiaomi_header(service_data); | ||||
|     if (!res.has_value()) { | ||||
|       continue; | ||||
|     } | ||||
|     if (res->is_duplicate) { | ||||
|       continue; | ||||
|     } | ||||
|     if (res->has_encryption && | ||||
|         (!(xiaomi_ble::decrypt_xiaomi_payload(const_cast<std::vector<uint8_t> &>(service_data.data), this->bindkey_, | ||||
|                                               this->address_)))) { | ||||
|       continue; | ||||
|     } | ||||
|     if (!(xiaomi_ble::parse_xiaomi_message(service_data.data, *res))) { | ||||
|       continue; | ||||
|     } | ||||
|     if (res->humidity.has_value() && this->humidity_ != nullptr) { | ||||
|       // see https://github.com/custom-components/sensor.mitemp_bt/issues/7#issuecomment-595948254 | ||||
|       *res->humidity = trunc(*res->humidity); | ||||
|     } | ||||
|     if (!(xiaomi_ble::report_xiaomi_results(res, device.address_str()))) { | ||||
|       continue; | ||||
|     } | ||||
|     if (res->temperature.has_value() && this->temperature_ != nullptr) | ||||
|       this->temperature_->publish_state(*res->temperature); | ||||
|     if (res->humidity.has_value() && this->humidity_ != nullptr) | ||||
|       this->humidity_->publish_state(*res->humidity); | ||||
|     if (res->battery_level.has_value() && this->battery_level_ != nullptr) | ||||
|       this->battery_level_->publish_state(*res->battery_level); | ||||
|     success = true; | ||||
|   } | ||||
|  | ||||
|   return success; | ||||
| } | ||||
|  | ||||
| void XiaomiXMWSDJ04MMC::set_bindkey(const std::string &bindkey) { | ||||
|   memset(this->bindkey_, 0, 16); | ||||
|   if (bindkey.size() != 32) { | ||||
|     return; | ||||
|   } | ||||
|   char temp[3] = {0}; | ||||
|   for (int i = 0; i < 16; i++) { | ||||
|     strncpy(temp, &(bindkey.c_str()[i * 2]), 2); | ||||
|     this->bindkey_[i] = std::strtoul(temp, nullptr, 16); | ||||
|   } | ||||
| } | ||||
|  | ||||
| }  // namespace xiaomi_xmwsdj04mmc | ||||
| }  // namespace esphome | ||||
|  | ||||
| #endif | ||||
							
								
								
									
										37
									
								
								esphome/components/xiaomi_xmwsdj04mmc/xiaomi_xmwsdj04mmc.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								esphome/components/xiaomi_xmwsdj04mmc/xiaomi_xmwsdj04mmc.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,37 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/components/sensor/sensor.h" | ||||
| #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" | ||||
| #include "esphome/components/xiaomi_ble/xiaomi_ble.h" | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
|  | ||||
| namespace esphome { | ||||
| namespace xiaomi_xmwsdj04mmc { | ||||
|  | ||||
| class XiaomiXMWSDJ04MMC : public Component, public esp32_ble_tracker::ESPBTDeviceListener { | ||||
|  public: | ||||
|   void set_address(uint64_t address) { this->address_ = address; } | ||||
|   void set_bindkey(const std::string &bindkey); | ||||
|  | ||||
|   bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; | ||||
|  | ||||
|   void dump_config() override; | ||||
|   float get_setup_priority() const override { return setup_priority::DATA; } | ||||
|   void set_temperature(sensor::Sensor *temperature) { this->temperature_ = temperature; } | ||||
|   void set_humidity(sensor::Sensor *humidity) { this->humidity_ = humidity; } | ||||
|   void set_battery_level(sensor::Sensor *battery_level) { this->battery_level_ = battery_level; } | ||||
|  | ||||
|  protected: | ||||
|   uint64_t address_; | ||||
|   uint8_t bindkey_[16]; | ||||
|   sensor::Sensor *temperature_{nullptr}; | ||||
|   sensor::Sensor *humidity_{nullptr}; | ||||
|   sensor::Sensor *battery_level_{nullptr}; | ||||
| }; | ||||
|  | ||||
| }  // namespace xiaomi_xmwsdj04mmc | ||||
| }  // namespace esphome | ||||
|  | ||||
| #endif | ||||
| @@ -97,7 +97,13 @@ void Application::loop() { | ||||
|   // Feed WDT with time | ||||
|   this->feed_wdt(last_op_end_time); | ||||
|  | ||||
|   for (Component *component : this->looping_components_) { | ||||
|   // Mark that we're in the loop for safe reentrant modifications | ||||
|   this->in_loop_ = true; | ||||
|  | ||||
|   for (this->current_loop_index_ = 0; this->current_loop_index_ < this->looping_components_active_end_; | ||||
|        this->current_loop_index_++) { | ||||
|     Component *component = this->looping_components_[this->current_loop_index_]; | ||||
|  | ||||
|     // Update the cached time before each component runs | ||||
|     this->loop_component_start_time_ = last_op_end_time; | ||||
|  | ||||
| @@ -112,6 +118,8 @@ void Application::loop() { | ||||
|     this->app_state_ |= new_app_state; | ||||
|     this->feed_wdt(last_op_end_time); | ||||
|   } | ||||
|  | ||||
|   this->in_loop_ = false; | ||||
|   this->app_state_ = new_app_state; | ||||
|  | ||||
|   // Use the last component's end time instead of calling millis() again | ||||
| @@ -235,9 +243,66 @@ void Application::teardown_components(uint32_t timeout_ms) { | ||||
| } | ||||
|  | ||||
| void Application::calculate_looping_components_() { | ||||
|   // First add all active components | ||||
|   for (auto *obj : this->components_) { | ||||
|     if (obj->has_overridden_loop()) | ||||
|     if (obj->has_overridden_loop() && | ||||
|         (obj->get_component_state() & COMPONENT_STATE_MASK) != COMPONENT_STATE_LOOP_DONE) { | ||||
|       this->looping_components_.push_back(obj); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   this->looping_components_active_end_ = this->looping_components_.size(); | ||||
|  | ||||
|   // Then add all inactive (LOOP_DONE) components | ||||
|   // This handles components that called disable_loop() during setup, before this method runs | ||||
|   for (auto *obj : this->components_) { | ||||
|     if (obj->has_overridden_loop() && | ||||
|         (obj->get_component_state() & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP_DONE) { | ||||
|       this->looping_components_.push_back(obj); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| void Application::disable_component_loop_(Component *component) { | ||||
|   // This method must be reentrant - components can disable themselves during their own loop() call | ||||
|   // Linear search to find component in active section | ||||
|   // Most configs have 10-30 looping components (30 is on the high end) | ||||
|   // O(n) is acceptable here as we optimize for memory, not complexity | ||||
|   for (uint16_t i = 0; i < this->looping_components_active_end_; i++) { | ||||
|     if (this->looping_components_[i] == component) { | ||||
|       // Move last active component to this position | ||||
|       this->looping_components_active_end_--; | ||||
|       if (i != this->looping_components_active_end_) { | ||||
|         std::swap(this->looping_components_[i], this->looping_components_[this->looping_components_active_end_]); | ||||
|  | ||||
|         // If we're currently iterating and just swapped the current position | ||||
|         if (this->in_loop_ && i == this->current_loop_index_) { | ||||
|           // Decrement so we'll process the swapped component next | ||||
|           this->current_loop_index_--; | ||||
|         } | ||||
|       } | ||||
|       return; | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| void Application::enable_component_loop_(Component *component) { | ||||
|   // This method must be reentrant - components can re-enable themselves during their own loop() call | ||||
|   // Single pass through all components to find and move if needed | ||||
|   // With typical 10-30 components, O(n) is faster than maintaining a map | ||||
|   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_++; | ||||
|       return; | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -572,13 +572,41 @@ class Application { | ||||
|  | ||||
|   void calculate_looping_components_(); | ||||
|  | ||||
|   // These methods are called by Component::disable_loop() and Component::enable_loop() | ||||
|   // Components should not call these directly - use this->disable_loop() or this->enable_loop() | ||||
|   // to ensure component state is properly updated along with the loop partition | ||||
|   void disable_component_loop_(Component *component); | ||||
|   void enable_component_loop_(Component *component); | ||||
|  | ||||
|   void feed_wdt_arch_(); | ||||
|  | ||||
|   /// Perform a delay while also monitoring socket file descriptors for readiness | ||||
|   void yield_with_select_(uint32_t delay_ms); | ||||
|  | ||||
|   std::vector<Component *> components_{}; | ||||
|  | ||||
|   // Partitioned vector design for looping components | ||||
|   // ================================================= | ||||
|   // Components are partitioned into [active | inactive] sections: | ||||
|   // | ||||
|   // looping_components_: [A, B, C, D | E, F] | ||||
|   //                                  ^ | ||||
|   //                      looping_components_active_end_ (4) | ||||
|   // | ||||
|   // - Components A,B,C,D are active and will be called in loop() | ||||
|   // - Components E,F are inactive (disabled/failed) and won't be called | ||||
|   // - No flag checking needed during iteration - just loop 0 to active_end_ | ||||
|   // - When a component is disabled, it's swapped with the last active component | ||||
|   //   and active_end_ is decremented | ||||
|   // - When a component is enabled, it's swapped with the first inactive component | ||||
|   //   and active_end_ is incremented | ||||
|   // - This eliminates branch mispredictions from flag checking in the hot loop | ||||
|   std::vector<Component *> looping_components_{}; | ||||
|   uint16_t looping_components_active_end_{0}; | ||||
|  | ||||
|   // For safe reentrant modifications during iteration | ||||
|   uint16_t current_loop_index_{0}; | ||||
|   bool in_loop_{false}; | ||||
|  | ||||
| #ifdef USE_BINARY_SENSOR | ||||
|   std::vector<binary_sensor::BinarySensor *> binary_sensors_{}; | ||||
|   | ||||
| @@ -30,17 +30,18 @@ const float LATE = -100.0f; | ||||
|  | ||||
| }  // namespace setup_priority | ||||
|  | ||||
| // Component state uses bits 0-1 (4 states) | ||||
| const uint8_t COMPONENT_STATE_MASK = 0x03; | ||||
| // Component state uses bits 0-2 (8 states, 5 used) | ||||
| const uint8_t COMPONENT_STATE_MASK = 0x07; | ||||
| const uint8_t COMPONENT_STATE_CONSTRUCTION = 0x00; | ||||
| const uint8_t COMPONENT_STATE_SETUP = 0x01; | ||||
| const uint8_t COMPONENT_STATE_LOOP = 0x02; | ||||
| const uint8_t COMPONENT_STATE_FAILED = 0x03; | ||||
| // Status LED uses bits 2-3 | ||||
| const uint8_t STATUS_LED_MASK = 0x0C; | ||||
| const uint8_t COMPONENT_STATE_LOOP_DONE = 0x04; | ||||
| // Status LED uses bits 3-4 | ||||
| const uint8_t STATUS_LED_MASK = 0x18; | ||||
| const uint8_t STATUS_LED_OK = 0x00; | ||||
| const uint8_t STATUS_LED_WARNING = 0x04;  // Bit 2 | ||||
| const uint8_t STATUS_LED_ERROR = 0x08;    // Bit 3 | ||||
| const uint8_t STATUS_LED_WARNING = 0x08;  // Bit 3 | ||||
| const uint8_t STATUS_LED_ERROR = 0x10;    // Bit 4 | ||||
|  | ||||
| const uint16_t WARN_IF_BLOCKING_OVER_MS = 50U;       ///< Initial blocking time allowed without warning | ||||
| const uint16_t WARN_IF_BLOCKING_INCREMENT_MS = 10U;  ///< How long the blocking time must be larger to warn again | ||||
| @@ -113,6 +114,9 @@ void Component::call() { | ||||
|     case COMPONENT_STATE_FAILED:  // NOLINT(bugprone-branch-clone) | ||||
|       // State failed: Do nothing | ||||
|       break; | ||||
|     case COMPONENT_STATE_LOOP_DONE:  // NOLINT(bugprone-branch-clone) | ||||
|       // State loop done: Do nothing, component has finished its work | ||||
|       break; | ||||
|     default: | ||||
|       break; | ||||
|   } | ||||
| @@ -136,14 +140,30 @@ bool Component::should_warn_of_blocking(uint32_t blocking_time) { | ||||
|   return false; | ||||
| } | ||||
| void Component::mark_failed() { | ||||
|   ESP_LOGE(TAG, "Component %s was marked as failed.", this->get_component_source()); | ||||
|   ESP_LOGE(TAG, "Component %s was marked as failed", this->get_component_source()); | ||||
|   this->component_state_ &= ~COMPONENT_STATE_MASK; | ||||
|   this->component_state_ |= COMPONENT_STATE_FAILED; | ||||
|   this->status_set_error(); | ||||
|   // Also remove from loop since failed components shouldn't loop | ||||
|   App.disable_component_loop_(this); | ||||
| } | ||||
| void Component::disable_loop() { | ||||
|   ESP_LOGD(TAG, "%s loop disabled", this->get_component_source()); | ||||
|   this->component_state_ &= ~COMPONENT_STATE_MASK; | ||||
|   this->component_state_ |= COMPONENT_STATE_LOOP_DONE; | ||||
|   App.disable_component_loop_(this); | ||||
| } | ||||
| void Component::enable_loop() { | ||||
|   if ((this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP_DONE) { | ||||
|     ESP_LOGD(TAG, "%s loop enabled", this->get_component_source()); | ||||
|     this->component_state_ &= ~COMPONENT_STATE_MASK; | ||||
|     this->component_state_ |= COMPONENT_STATE_LOOP; | ||||
|     App.enable_component_loop_(this); | ||||
|   } | ||||
| } | ||||
| void Component::reset_to_construction_state() { | ||||
|   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()); | ||||
|     this->component_state_ &= ~COMPONENT_STATE_MASK; | ||||
|     this->component_state_ |= COMPONENT_STATE_CONSTRUCTION; | ||||
|     // Clear error status when resetting | ||||
| @@ -276,8 +296,8 @@ uint32_t WarnIfComponentBlockingGuard::finish() { | ||||
|   } | ||||
|   if (should_warn) { | ||||
|     const char *src = component_ == nullptr ? "<null>" : component_->get_component_source(); | ||||
|     ESP_LOGW(TAG, "Component %s took a long time for an operation (%" PRIu32 " ms).", src, blocking_time); | ||||
|     ESP_LOGW(TAG, "Components should block for at most 30 ms."); | ||||
|     ESP_LOGW(TAG, "Component %s took a long time for an operation (%" PRIu32 " ms)", src, blocking_time); | ||||
|     ESP_LOGW(TAG, "Components should block for at most 30 ms"); | ||||
|   } | ||||
|  | ||||
|   return curr_time; | ||||
|   | ||||
| @@ -58,6 +58,7 @@ extern const uint8_t COMPONENT_STATE_CONSTRUCTION; | ||||
| extern const uint8_t COMPONENT_STATE_SETUP; | ||||
| extern const uint8_t COMPONENT_STATE_LOOP; | ||||
| extern const uint8_t COMPONENT_STATE_FAILED; | ||||
| extern const uint8_t COMPONENT_STATE_LOOP_DONE; | ||||
| extern const uint8_t STATUS_LED_MASK; | ||||
| extern const uint8_t STATUS_LED_OK; | ||||
| extern const uint8_t STATUS_LED_WARNING; | ||||
| @@ -150,6 +151,26 @@ class Component { | ||||
|     this->mark_failed(); | ||||
|   } | ||||
|  | ||||
|   /** Disable this component's loop. The loop() method will no longer be called. | ||||
|    * | ||||
|    * This is useful for components that only need to run for a certain period of time | ||||
|    * or when inactive, saving CPU cycles. | ||||
|    * | ||||
|    * @note Components should call this->disable_loop() on themselves, not on other components. | ||||
|    *       This ensures the component's state is properly updated along with the loop partition. | ||||
|    */ | ||||
|   void disable_loop(); | ||||
|  | ||||
|   /** Enable this component's loop. The loop() method will be called normally. | ||||
|    * | ||||
|    * This is useful for components that transition between active and inactive states | ||||
|    * and need to re-enable their loop() method when becoming active again. | ||||
|    * | ||||
|    * @note Components should call this->enable_loop() on themselves, not on other components. | ||||
|    *       This ensures the component's state is properly updated along with the loop partition. | ||||
|    */ | ||||
|   void enable_loop(); | ||||
|  | ||||
|   bool is_failed() const; | ||||
|  | ||||
|   bool is_ready() const; | ||||
|   | ||||
| @@ -29,7 +29,9 @@ Component = esphome_ns.class_("Component") | ||||
| ComponentPtr = Component.operator("ptr") | ||||
| PollingComponent = esphome_ns.class_("PollingComponent", Component) | ||||
| Application = esphome_ns.class_("Application") | ||||
| optional = esphome_ns.class_("optional") | ||||
| # Create optional with explicit namespace to avoid ambiguity with std::optional | ||||
| # The generated code will use esphome::optional instead of just optional | ||||
| optional = global_ns.namespace("esphome").class_("optional") | ||||
| arduino_json_ns = global_ns.namespace("ArduinoJson") | ||||
| JsonObject = arduino_json_ns.class_("JsonObject") | ||||
| JsonObjectConst = arduino_json_ns.class_("JsonObjectConst") | ||||
|   | ||||
		Reference in New Issue
	
	Block a user