Merge branch 'esp32-gatt-backend' into neutral-ble-client

This commit is contained in:
J. Nick Koston
2026-08-09 10:56:31 -05:00
2 changed files with 44 additions and 36 deletions
@@ -121,11 +121,9 @@ void BluedroidGattClient::tracker_connect_() {
this->services_released_ = false;
this->seen_mtu_ = false;
// Per-attempt reset: the stack-down path in loop() reaches IDLE through
// set_state() without set_idle_(), and a stale done latch would complete
// this connection's discovery with the previous connection's result.
this->search_prestarted_ = false;
this->search_done_ = false;
this->search_requested_ = false;
// set_state() without set_idle_(), and a stale completed search would
// satisfy this connection's discovery with the previous one's result.
this->search_state_ = SearchState::NONE;
this->search_status_ = 0;
this->enable_loop();
this->set_state(ClientState::CONNECTING);
@@ -189,20 +187,27 @@ int BluedroidGattClient::discover_services() {
if (this->conn_id_ == UNSET_CONN_ID) {
return ble_device_base::GATT_ERR_NOT_CONNECTED;
}
if (this->search_prestarted_) {
// Completion comes from the pre-started search: the pending SEARCH_CMPL,
// or - when it already landed - the flush after the connected report
// (loop() covers a request made outside that event drain).
this->search_requested_ = true;
if (this->search_done_) {
switch (this->search_state_) {
case SearchState::PRESTARTED:
// The pending SEARCH_CMPL reports once it lands.
this->search_state_ = SearchState::CLAIMED;
return 0;
case SearchState::PRESTART_DONE:
// Already landed: the flush after the connected report delivers
// (loop() covers a claim made outside that event drain).
this->search_state_ = SearchState::REPORT_PENDING;
this->enable_loop();
}
return 0;
return 0;
case SearchState::CLAIMED:
case SearchState::REPORT_PENDING:
return 0; // One completion is already owed to this claimant.
case SearchState::NONE:
break;
}
int err = this->check_and_log_error_("esp_ble_gattc_search_service",
esp_ble_gattc_search_service(this->gattc_if_, this->conn_id_, nullptr));
if (err == 0) {
this->search_requested_ = true;
this->search_state_ = SearchState::CLAIMED;
}
return err;
}
@@ -446,9 +451,7 @@ bool BluedroidGattClient::check_addr_(const esp_bd_addr_t &addr) const {
void BluedroidGattClient::set_idle_() {
this->set_state(ClientState::IDLE);
this->conn_id_ = UNSET_CONN_ID;
this->search_prestarted_ = false;
this->search_done_ = false;
this->search_requested_ = false;
this->search_state_ = SearchState::NONE;
this->search_status_ = 0;
}
@@ -508,16 +511,14 @@ int BluedroidGattClient::handle_search_cmpl_() {
return 0;
}
// Reports a completed search once its consumer has asked for it; the
// pre-started search must stay silent until then. Delivery consumes the
// whole latch, so a later discover_services() on the same connection issues
// a real search instead of re-reporting this result.
// Reports a completed search once it has a claimant; a pre-started search
// stays silent until claimed. Delivery consumes the state, so a later
// discover_services() on the same connection issues a real search instead
// of re-reporting this result.
void BluedroidGattClient::deliver_pending_search_() {
if (!this->search_requested_ || !this->search_done_)
if (this->search_state_ != SearchState::REPORT_PENDING)
return;
this->search_requested_ = false;
this->search_prestarted_ = false;
this->search_done_ = false;
this->search_state_ = SearchState::NONE;
this->listener_->on_service_discovery_done(this->search_status_);
}
@@ -715,7 +716,7 @@ void BluedroidGattClient::handle_open_evt_(esp_ble_gattc_cb_param_t *param) {
// consumer's own discover_services() call retries the real search.
if (this->check_and_log_error_("esp_ble_gattc_search_service",
esp_ble_gattc_search_service(this->gattc_if_, this->conn_id_, nullptr)) == 0) {
this->search_prestarted_ = true;
this->search_state_ = SearchState::PRESTARTED;
}
}
}
@@ -810,7 +811,6 @@ bool BluedroidGattClient::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga
if (this->conn_id_ != param->search_cmpl.conn_id)
return false;
ESP_LOGI(TAG, "[%d] Service discovery complete", this->connection_index_);
this->search_done_ = true;
if (this->state() == ClientState::DISCONNECTING) {
// Teardown already owns the link: keep its state and safety timer,
// and skip the param/count work - the result is never delivered (the
@@ -818,6 +818,8 @@ bool BluedroidGattClient::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga
break;
}
this->search_status_ = this->handle_search_cmpl_();
this->search_state_ =
this->search_state_ == SearchState::CLAIMED ? SearchState::REPORT_PENDING : SearchState::PRESTART_DONE;
this->set_state(ClientState::ESTABLISHED);
this->deliver_pending_search_();
if (this->state() != ClientState::DISCONNECTING) {
@@ -33,6 +33,15 @@ class BluetoothConnection;
// void disconnect() cannot overload with an int-returning twin.
class BluedroidGattClient final : public esp32_ble_tracker::ESPBTClient, public Component {
public:
// Lifecycle of one connection attempt's service search.
enum class SearchState : uint8_t {
NONE, // no search this attempt
PRESTARTED, // issued at OPEN_EVT, no claimant yet
PRESTART_DONE, // completed with search_status_ latched, no claimant yet
CLAIMED, // in flight with a claimant (pre-started or direct)
REPORT_PENDING // completed and claimed: deliver on the next flush
};
void setup() override;
void loop() override;
void dump_config() override;
@@ -137,15 +146,12 @@ class BluedroidGattClient final : public esp32_ble_tracker::ESPBTClient, public
// The connected report waits for the MTU exchange; OPEN_EVT alone would
// hand HA the default 23.
bool seen_mtu_ : 1 {false};
// Pre-started discovery latch: the search is issued at OPEN_EVT so it
// overlaps the MTU exchange (the replaced class's timing) and the
// consumer's discover_services() completes from it instead of paying a
// serialized ATT round trip. requested/done meet in
// deliver_pending_search_().
bool search_prestarted_ : 1 {false};
bool search_done_ : 1 {false};
bool search_requested_ : 1 {false};
// esp_gatt_status_t of the completed search, held until requested.
// Pre-started discovery: the search is issued at OPEN_EVT so it overlaps
// the MTU exchange (the replaced class's timing) and the consumer's
// discover_services() completes from it instead of paying a serialized
// ATT round trip. Reset per attempt and on idle.
SearchState search_state_ : 4 {SearchState::NONE};
// esp_gatt_status_t of the completed search, held until claimed.
uint8_t search_status_{0};
};