diff --git a/esphome/components/ble_client/ble_client.cpp b/esphome/components/ble_client/ble_client.cpp index 9c01dfea2d..45c306c23f 100644 --- a/esphome/components/ble_client/ble_client.cpp +++ b/esphome/components/ble_client/ble_client.cpp @@ -2,6 +2,7 @@ #include "esphome/components/esp32_ble_client/ble_client_base.h" #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" #include "esphome/core/application.h" +#include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" @@ -16,6 +17,13 @@ namespace esphome::ble_client { static const char *const TAG = "ble_client"; +#ifdef USE_BLE_CLIENT_GATT_NODES +// Hold-off step per consecutive materializer failure, capped so a flapping +// peer retries within a minute at worst (neutral-engine parity). +static const uint32_t GATT_FAILURE_HOLD_OFF_STEP_MS = 10000; +static const uint8_t GATT_FAILURE_HOLD_OFF_MAX_STEPS = 6; +#endif + void BLEClient::setup() { BLEClientBase::setup(); this->enabled = true; @@ -35,6 +43,11 @@ void BLEClient::dump_config() { bool BLEClient::parse_device(const espbt::ESPBTDevice &device) { if (!this->enabled) return false; +#ifdef USE_BLE_CLIENT_GATT_NODES + if (this->gatt_hold_off_ms_ != 0 && millis() - this->gatt_hold_off_start_ < this->gatt_hold_off_ms_ && + device.address_uint64() == this->address_) + return false; +#endif return BLEClientBase::parse_device(device); } @@ -202,6 +215,7 @@ void BLEClient::handle_gatt_search_cmpl_(esp_gatt_status_t status) { // Distinguishes a failed search/count from a genuinely service-less peer. ESP_LOGW(TAG, "[%s] Service table unavailable (status=%d, services=%u); treating as failed discovery", this->address_str(), status, service_total); + this->register_gatt_failure_(); this->disconnect(); return; } @@ -220,6 +234,17 @@ void BLEClient::handle_gatt_search_cmpl_(esp_gatt_status_t status) { // nodes just made hold it via the pending notify-registration count. for (auto *node : this->gatt_nodes_) node->node_state = espbt::ClientState::ESTABLISHED; + this->gatt_consecutive_failures_ = 0; + this->gatt_hold_off_ms_ = 0; +} + +void BLEClient::register_gatt_failure_() { + if (this->gatt_consecutive_failures_ < GATT_FAILURE_HOLD_OFF_MAX_STEPS) + this->gatt_consecutive_failures_++; + this->gatt_hold_off_start_ = millis(); + this->gatt_hold_off_ms_ = this->gatt_consecutive_failures_ * GATT_FAILURE_HOLD_OFF_STEP_MS; + ESP_LOGW(TAG, "[%s] Holding off reconnect for %u s", this->address_str(), + this->gatt_consecutive_failures_ * (GATT_FAILURE_HOLD_OFF_STEP_MS / 1000)); } void BLEClient::on_disconnect_complete(esp_err_t reason) { diff --git a/esphome/components/ble_client/ble_client.h b/esphome/components/ble_client/ble_client.h index 5a5ef540ba..bd48594ea8 100644 --- a/esphome/components/ble_client/ble_client.h +++ b/esphome/components/ble_client/ble_client.h @@ -70,6 +70,7 @@ class BLEClient final : public BLEClientBase { void maybe_release_services_(); #ifdef USE_BLE_CLIENT_GATT_NODES int check_gatt_op_(const char *operation, esp_err_t err); + void register_gatt_failure_(); void dispatch_gatt_event_(esp_gattc_cb_event_t event, esp_ble_gattc_cb_param_t *param); void handle_gatt_search_cmpl_(esp_gatt_status_t status); bool take_pending_gatt_reg_(uint16_t handle); @@ -87,6 +88,12 @@ class BLEClient final : public BLEClientBase { uint8_t pending_gatt_reg_count_{0}; // on_connected fan-out started; on_disconnected is owed at teardown. bool gatt_connected_{false}; + // Backoff after materializer failures so a peer that reliably fails + // discovery cannot produce an unbounded reconnect loop (neutral-engine + // parity; wrap-safe start+duration pair). + uint32_t gatt_hold_off_start_{0}; + uint32_t gatt_hold_off_ms_{0}; + uint8_t gatt_consecutive_failures_{0}; #endif };