diff --git a/esphome/components/bluetooth_proxy/bluetooth_connection.cpp b/esphome/components/bluetooth_proxy/bluetooth_connection.cpp index 1d6f7e23b3..60f56fda54 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_connection.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_connection.cpp @@ -135,8 +135,8 @@ void BluetoothConnection::loop() { // - For V3_WITH_CACHE: Services are never sent, disable after INIT state // - For V3_WITHOUT_CACHE: Disable only after service discovery is complete // (send_service_ == DONE_SENDING_SERVICES, which is only set after services are sent) - if (this->state_ != espbt::ClientState::INIT && (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE || - this->send_service_ == DONE_SENDING_SERVICES)) { + if (this->state() != espbt::ClientState::INIT && (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE || + this->send_service_ == DONE_SENDING_SERVICES)) { this->disable_loop(); } } diff --git a/esphome/components/esp32_ble_client/ble_client_base.cpp b/esphome/components/esp32_ble_client/ble_client_base.cpp index 01f79156a9..c464c89390 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.cpp +++ b/esphome/components/esp32_ble_client/ble_client_base.cpp @@ -50,7 +50,7 @@ void BLEClientBase::loop() { this->set_state(espbt::ClientState::INIT); return; } - if (this->state_ == espbt::ClientState::INIT) { + if (this->state() == espbt::ClientState::INIT) { auto ret = esp_ble_gattc_app_register(this->app_id); if (ret) { ESP_LOGE(TAG, "gattc app register failed. app_id=%d code=%d", this->app_id, ret); @@ -60,7 +60,7 @@ void BLEClientBase::loop() { } // If idle, we can disable the loop as connect() // will enable it again when a connection is needed. - else if (this->state_ == espbt::ClientState::IDLE) { + else if (this->state() == espbt::ClientState::IDLE) { this->disable_loop(); } } @@ -86,7 +86,7 @@ bool BLEClientBase::parse_device(const espbt::ESPBTDevice &device) { return false; if (this->address_ == 0 || device.address_uint64() != this->address_) return false; - if (this->state_ != espbt::ClientState::IDLE) + if (this->state() != espbt::ClientState::IDLE) return false; this->log_event_("Found device"); @@ -102,10 +102,10 @@ bool BLEClientBase::parse_device(const espbt::ESPBTDevice &device) { void BLEClientBase::connect() { // Prevent duplicate connection attempts - if (this->state_ == espbt::ClientState::CONNECTING || this->state_ == espbt::ClientState::CONNECTED || - this->state_ == espbt::ClientState::ESTABLISHED) { + if (this->state() == espbt::ClientState::CONNECTING || this->state() == espbt::ClientState::CONNECTED || + this->state() == espbt::ClientState::ESTABLISHED) { ESP_LOGW(TAG, "[%d] [%s] Connection already in progress, state=%s", this->connection_index_, this->address_str_, - espbt::client_state_to_string(this->state_)); + espbt::client_state_to_string(this->state())); return; } ESP_LOGI(TAG, "[%d] [%s] 0x%02x Connecting", this->connection_index_, this->address_str_, this->remote_addr_type_); @@ -133,12 +133,12 @@ void BLEClientBase::connect() { esp_err_t BLEClientBase::pair() { return esp_ble_set_encryption(this->remote_bda_, ESP_BLE_SEC_ENCRYPT); } void BLEClientBase::disconnect() { - if (this->state_ == espbt::ClientState::IDLE || this->state_ == espbt::ClientState::DISCONNECTING) { + if (this->state() == espbt::ClientState::IDLE || this->state() == espbt::ClientState::DISCONNECTING) { ESP_LOGI(TAG, "[%d] [%s] Disconnect requested, but already %s", this->connection_index_, this->address_str_, - espbt::client_state_to_string(this->state_)); + espbt::client_state_to_string(this->state())); return; } - if (this->state_ == espbt::ClientState::CONNECTING || this->conn_id_ == UNSET_CONN_ID) { + if (this->state() == espbt::ClientState::CONNECTING || this->conn_id_ == UNSET_CONN_ID) { ESP_LOGD(TAG, "[%d] [%s] Disconnect before connected, disconnect scheduled", this->connection_index_, this->address_str_); this->want_disconnect_ = true; @@ -150,7 +150,7 @@ void BLEClientBase::disconnect() { void BLEClientBase::unconditional_disconnect() { // Disconnect without checking the state. ESP_LOGI(TAG, "[%d] [%s] Disconnecting (conn_id: %d).", this->connection_index_, this->address_str_, this->conn_id_); - if (this->state_ == espbt::ClientState::DISCONNECTING) { + if (this->state() == espbt::ClientState::DISCONNECTING) { this->log_error_("Already disconnecting"); return; } @@ -170,7 +170,7 @@ void BLEClientBase::unconditional_disconnect() { this->log_gattc_warning_("esp_ble_gattc_close", err); } - if (this->state_ == espbt::ClientState::DISCOVERED) { + if (this->state() == espbt::ClientState::DISCOVERED) { this->set_address(0); this->set_state(espbt::ClientState::IDLE); } else { @@ -295,18 +295,18 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ // ESP-IDF's BLE stack may send ESP_GATTC_OPEN_EVT after esp_ble_gattc_open() returns an // error, if the error occurred at the BTA/GATT layer. This can result in the event // arriving after we've already transitioned to IDLE state. - if (this->state_ == espbt::ClientState::IDLE) { + if (this->state() == espbt::ClientState::IDLE) { ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_OPEN_EVT in IDLE state (status=%d), ignoring", this->connection_index_, this->address_str_, param->open.status); break; } - if (this->state_ != espbt::ClientState::CONNECTING) { + if (this->state() != espbt::ClientState::CONNECTING) { // This should not happen but lets log it in case it does // because it means we have a bad assumption about how the // ESP BT stack works. ESP_LOGE(TAG, "[%d] [%s] ESP_GATTC_OPEN_EVT in %s state (status=%d)", this->connection_index_, - this->address_str_, espbt::client_state_to_string(this->state_), param->open.status); + this->address_str_, espbt::client_state_to_string(this->state()), param->open.status); } if (param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN) { this->log_gattc_warning_("Connection open", param->open.status); @@ -327,7 +327,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) { // Cached connections already connected with medium parameters, no update needed // only set our state, subclients might have more stuff to do yet. - this->state_ = espbt::ClientState::ESTABLISHED; + this->set_state_internal_(espbt::ClientState::ESTABLISHED); break; } // For V3_WITHOUT_CACHE, we already set fast params before connecting @@ -356,7 +356,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ return false; // Check if we were disconnected while waiting for service discovery if (param->disconnect.reason == ESP_GATT_CONN_TERMINATE_PEER_USER && - this->state_ == espbt::ClientState::CONNECTED) { + this->state() == espbt::ClientState::CONNECTED) { this->log_warning_("Remote closed during discovery"); } else { ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_DISCONNECT_EVT, reason 0x%02x", this->connection_index_, this->address_str_, @@ -433,7 +433,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ #endif } ESP_LOGI(TAG, "[%d] [%s] Service discovery complete", this->connection_index_, this->address_str_); - this->state_ = espbt::ClientState::ESTABLISHED; + this->set_state_internal_(espbt::ClientState::ESTABLISHED); break; } case ESP_GATTC_READ_DESCR_EVT: { diff --git a/esphome/components/esp32_ble_client/ble_client_base.h b/esphome/components/esp32_ble_client/ble_client_base.h index c52f0e5d2d..c2336b2349 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.h +++ b/esphome/components/esp32_ble_client/ble_client_base.h @@ -44,7 +44,7 @@ class BLEClientBase : public espbt::ESPBTClient, public Component { void unconditional_disconnect(); void release_services(); - bool connected() { return this->state_ == espbt::ClientState::ESTABLISHED; } + bool connected() { return this->state() == espbt::ClientState::ESTABLISHED; } void set_auto_connect(bool auto_connect) { this->auto_connect_ = auto_connect; } diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index 995755ac84..48398f1f63 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -105,36 +105,32 @@ void ESP32BLETracker::loop() { } // Check for scan timeout - moved here from scheduler to avoid false reboots - // when the loop is blocked - if (this->scanner_state_ == ScannerState::RUNNING) { - switch (this->scan_timeout_state_) { - case ScanTimeoutState::MONITORING: { - uint32_t now = App.get_loop_component_start_time(); - uint32_t timeout_ms = this->scan_duration_ * 2000; - // Robust time comparison that handles rollover correctly - // This works because unsigned arithmetic wraps around predictably - if ((now - this->scan_start_time_) > timeout_ms) { - // First time we've seen the timeout exceeded - wait one more loop iteration - // This ensures all components have had a chance to process pending events - // This is because esp32_ble may not have run yet and called - // gap_scan_event_handler yet when the loop unblocks - ESP_LOGW(TAG, "Scan timeout exceeded"); - this->scan_timeout_state_ = ScanTimeoutState::EXCEEDED_WAIT; - } - break; - } - case ScanTimeoutState::EXCEEDED_WAIT: - // We've waited at least one full loop iteration, and scan is still running - ESP_LOGE(TAG, "Scan never terminated, rebooting"); - App.reboot(); - break; - - case ScanTimeoutState::INACTIVE: - // This case should be unreachable - scanner and timeout states are always synchronized - break; + // when the loop is blocked. This must run every iteration for safety. + if (this->scanner_state_ == ScannerState::RUNNING && this->scan_timeout_state_ == ScanTimeoutState::MONITORING) { + // Robust time comparison that handles rollover correctly + // This works because unsigned arithmetic wraps around predictably + if ((App.get_loop_component_start_time() - this->scan_start_time_) > this->scan_timeout_ms_) { + // First time we've seen the timeout exceeded - wait one more loop iteration + // This ensures all components have had a chance to process pending events + // This is because esp32_ble may not have run yet and called + // gap_scan_event_handler yet when the loop unblocks + ESP_LOGW(TAG, "Scan timeout exceeded"); + this->scan_timeout_state_ = ScanTimeoutState::EXCEEDED_WAIT; } + } else if (this->scan_timeout_state_ == ScanTimeoutState::EXCEEDED_WAIT) { + // We've waited at least one full loop iteration, and scan is still running + ESP_LOGE(TAG, "Scan never terminated, rebooting"); + App.reboot(); } + // Fast path: skip expensive client state counting and processing + // if no state has changed since last loop iteration + if (this->state_version_ == this->last_processed_version_) { + return; + } + this->last_processed_version_ = this->state_version_; + + // State changed - do full processing ClientStateCounts counts = this->count_client_states_(); if (counts != this->client_state_counts_) { this->client_state_counts_ = counts; @@ -236,6 +232,7 @@ void ESP32BLETracker::start_scan_(bool first) { // Start timeout monitoring in loop() instead of using scheduler // This prevents false reboots when the loop is blocked this->scan_start_time_ = App.get_loop_component_start_time(); + this->scan_timeout_ms_ = this->scan_duration_ * 2000; this->scan_timeout_state_ = ScanTimeoutState::MONITORING; esp_err_t err = esp_ble_gap_set_scan_params(&this->scan_params_); @@ -253,6 +250,7 @@ void ESP32BLETracker::start_scan_(bool first) { void ESP32BLETracker::register_client(ESPBTClient *client) { #ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT client->app_id = ++this->app_id_; + client->set_tracker_state_version(&this->state_version_); this->clients_.push_back(client); this->recalculate_advertisement_parser_types(); #endif @@ -382,6 +380,7 @@ void ESP32BLETracker::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_i void ESP32BLETracker::set_scanner_state_(ScannerState state) { this->scanner_state_ = state; + this->state_version_++; for (auto *listener : this->scanner_state_listeners_) { listener->on_scanner_state(state); } diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h index f538a0eddc..55c7ae57fa 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -226,25 +226,39 @@ class ESPBTClient : public ESPBTDeviceListener { bool disconnect_pending() const { return this->want_disconnect_; } void cancel_pending_disconnect() { this->want_disconnect_ = false; } virtual void set_state(ClientState st) { - this->state_ = st; + this->set_state_internal_(st); if (st == ClientState::IDLE) { this->want_disconnect_ = false; } } - ClientState state() const { return state_; } + ClientState state() const { return this->state_; } + + /// Set the tracker's state version pointer for change notification + void set_tracker_state_version(uint8_t *version) { this->tracker_state_version_ = version; } // Memory optimized layout uint8_t app_id; // App IDs are small integers assigned sequentially protected: - // Group 1: 1-byte types - ClientState state_{ClientState::INIT}; + /// Set state without IDLE handling - use for direct state transitions + void set_state_internal_(ClientState st) { + this->state_ = st; + if (this->tracker_state_version_ != nullptr) { + (*this->tracker_state_version_)++; + } + } + // want_disconnect_ is set to true when a disconnect is requested // while the client is connecting. This is used to disconnect the // client as soon as we get the connection id (conn_id_) from the // ESP_GATTC_OPEN_EVT event. bool want_disconnect_{false}; - // 2 bytes used, 2 bytes padding + + private: + ClientState state_{ClientState::INIT}; + /// Pointer to tracker's state_version_ counter, incremented on state changes + /// to enable fast-path loop optimization. Set by ESP32BLETracker::register_client(). + uint8_t *tracker_state_version_{nullptr}; }; class ESP32BLETracker : public Component, @@ -380,6 +394,11 @@ class ESP32BLETracker : public Component, // Group 4: 1-byte types (enums, uint8_t, bool) uint8_t app_id_{0}; uint8_t scan_start_fail_count_{0}; + /// Version counter incremented on any state change (scanner or client) + /// Used for fast-path optimization in loop() to skip work when nothing changed. + uint8_t state_version_{0}; + /// Last state version that was fully processed in loop() + uint8_t last_processed_version_{0}; ScannerState scanner_state_{ScannerState::IDLE}; bool scan_continuous_; bool scan_active_; @@ -396,6 +415,8 @@ class ESP32BLETracker : public Component, EXCEEDED_WAIT, // Timeout exceeded, waiting one loop before reboot }; uint32_t scan_start_time_{0}; + /// Precomputed timeout value: scan_duration_ * 2000 + uint32_t scan_timeout_ms_{0}; ScanTimeoutState scan_timeout_state_{ScanTimeoutState::INACTIVE}; };