From a8b07af2a356f4642e93312645ceb8e159b12600 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 17 Jan 2026 16:12:51 -1000 Subject: [PATCH 1/2] fixes --- .../esp32_ble_tracker/esp32_ble_tracker.cpp | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index 92fd04f77a..87436f4d22 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -106,21 +106,23 @@ void ESP32BLETracker::loop() { // Check for scan timeout - moved here from scheduler to avoid false reboots // 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; + if (this->scanner_state_ == ScannerState::RUNNING) { + if (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(); } - } 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 From 4549e375c120ed7545094b6850f581619650aa20 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 17 Jan 2026 16:16:58 -1000 Subject: [PATCH 2/2] adjust --- .../esp32_ble_tracker/esp32_ble_tracker.cpp | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index 87436f4d22..ad491885ec 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -126,7 +126,16 @@ void ESP32BLETracker::loop() { } // Fast path: skip expensive client state counting and processing - // if no state has changed since last loop iteration + // if no state has changed since last loop iteration. + // + // How state changes ensure we reach the code below: + // - handle_scanner_failure_(): scanner_state_ set via set_scanner_state_() increments version + // - start_scan_()/update_coex_preference_(): scanner_state_ becomes IDLE via set_scanner_state_() + // - try_promote_discovered_clients_(): client enters DISCOVERED via set_state(), or + // connecting client finishes (state change), or scanner reaches RUNNING/IDLE + // + // All conditions that affect the logic below are tied to state changes that increment + // state_version_, so the fast path is safe. if (this->state_version_ == this->last_processed_version_) { return; } @@ -140,6 +149,7 @@ void ESP32BLETracker::loop() { this->client_state_counts_.discovered, this->client_state_counts_.disconnecting); } + // Scanner failure: reached when set_scanner_state_(FAILED) or scan_set_param_failed_ set if (this->scanner_state_ == ScannerState::FAILED || (this->scan_set_param_failed_ && this->scanner_state_ == ScannerState::RUNNING)) { this->handle_scanner_failure_(); @@ -158,6 +168,8 @@ void ESP32BLETracker::loop() { */ + // Start scan: reached when scanner_state_ becomes IDLE (via set_scanner_state_()) and + // all clients are idle (their state changes increment version when they finish) if (this->scanner_state_ == ScannerState::IDLE && !counts.connecting && !counts.disconnecting && !counts.discovered) { #ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE this->update_coex_preference_(false); @@ -166,8 +178,9 @@ void ESP32BLETracker::loop() { this->start_scan_(false); // first = false } } - // If there is a discovered client and no connecting - // clients, then promote the discovered client to ready to connect. + // Promote discovered clients: reached when a client's state becomes DISCOVERED (via set_state()), + // or when a blocking condition clears (connecting client finishes, scanner reaches RUNNING/IDLE). + // All these trigger state_version_ increment, so we'll process and check promotion eligibility. // We check both RUNNING and IDLE states because: // - RUNNING: gap_scan_event_handler initiates stop_scan_() but promotion can happen immediately // - IDLE: Scanner has already stopped (naturally or by gap_scan_event_handler)