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

This commit is contained in:
J. Nick Koston
2026-08-09 10:07:42 -05:00
6 changed files with 80 additions and 57 deletions
@@ -221,21 +221,23 @@ async def new_gatt_backend(
return backend
FILTER_SOURCE_FILES = filter_source_files_from_platform(
{
"bluetooth_connection_bluedroid.cpp": {
PlatformFramework.ESP32_ARDUINO,
PlatformFramework.ESP32_IDF,
},
# Every hub platform the proxy admits (the file compiles empty where
# USE_BLE_GATT_CLIENT is not defined), so a platform gaining a backend
# cannot hit a missing-symbol trap here.
"bluetooth_connection_hub.cpp": {
PlatformFramework.RP2_ARDUINO,
PlatformFramework.LN882X_ARDUINO,
PlatformFramework.ESP32_ARDUINO,
PlatformFramework.ESP32_IDF,
},
"bluetooth_connection_rp2.cpp": {PlatformFramework.RP2_ARDUINO},
}
)
# Named so tests can pin the hub entry against bluetooth_proxy's platform
# list (this module cannot import bluetooth_proxy to derive it).
SOURCE_FILE_FRAMEWORKS: dict[str, set[PlatformFramework]] = {
"bluetooth_connection_bluedroid.cpp": {
PlatformFramework.ESP32_ARDUINO,
PlatformFramework.ESP32_IDF,
},
# Every hub platform the proxy admits (the file compiles empty where
# USE_BLE_GATT_CLIENT is not defined), so a platform gaining a backend
# cannot hit a missing-symbol trap here.
"bluetooth_connection_hub.cpp": {
PlatformFramework.RP2_ARDUINO,
PlatformFramework.LN882X_ARDUINO,
PlatformFramework.ESP32_ARDUINO,
PlatformFramework.ESP32_IDF,
},
"bluetooth_connection_rp2.cpp": {PlatformFramework.RP2_ARDUINO},
}
FILTER_SOURCE_FILES = filter_source_files_from_platform(SOURCE_FILE_FRAMEWORKS)
@@ -64,14 +64,11 @@ void BluedroidGattClient::loop() {
}
// Do not wait for REG_EVT; a dropped event must not wedge the slot.
this->set_state(ClientState::IDLE);
} else if (st == ClientState::IDLE) {
// The loop only drives the bootstrap, the disconnect safety timeout and
// the pre-started-search flush.
this->disable_loop();
} else if (st != ClientState::DISCONNECTING) {
// Only the pre-started-search flush can need the loop here (a consumer
// requesting the finished search outside an event drain). Settle again -
// unless delivering it just started a teardown that needs the timer.
// The loop only drives the bootstrap, the disconnect safety timeout and
// the pre-started-search flush (a consumer requesting the finished
// search outside an event drain; set_idle_() cleared any stale latch).
// Settle - unless delivering just started a teardown needing the timer.
this->deliver_pending_search_();
if (this->state() != ClientState::DISCONNECTING) {
this->disable_loop();
@@ -185,11 +182,11 @@ int BluedroidGattClient::discover_services() {
if (this->conn_id_ == UNSET_CONN_ID) {
return ble_device_base::GATT_ERR_NOT_CONNECTED;
}
this->search_requested_ = true;
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_) {
this->enable_loop();
}
@@ -197,8 +194,8 @@ int BluedroidGattClient::discover_services() {
}
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_ = false;
if (err == 0) {
this->search_requested_ = true;
}
return err;
}
@@ -506,12 +503,11 @@ int BluedroidGattClient::handle_search_cmpl_() {
// Reports a completed search once its consumer has asked for it; the
// pre-started search must stay silent until then.
bool BluedroidGattClient::deliver_pending_search_() {
void BluedroidGattClient::deliver_pending_search_() {
if (!this->search_requested_ || !this->search_done_)
return false;
return;
this->search_requested_ = false;
this->listener_->on_service_discovery_done(this->search_status_);
return true;
}
#ifdef BLUETOOTH_CONNECTION_SERVES_PROXY
@@ -554,10 +550,7 @@ void BluedroidGattClient::stream_service_batch(BluetoothConnection &conn) {
if (svc_status != ESP_GATT_OK || svc_count == 0) {
ESP_LOGE(TAG, "[%d] [%s] Service walk failed (service %d), aborting stream", conn.connection_index_,
conn.address_str_, conn.send_service_);
// Latch the real cause for the disconnect report.
conn.pending_error_ = svc_status != ESP_GATT_OK ? svc_status : ESP_GATT_NOT_FOUND;
conn.send_service_ = DONE_SENDING_SERVICES;
conn.disconnect();
conn.abort_service_stream(svc_status != ESP_GATT_OK ? svc_status : ESP_GATT_NOT_FOUND);
return;
}
uint16_t total_char_count = 0;
@@ -566,9 +559,7 @@ void BluedroidGattClient::stream_service_batch(BluetoothConnection &conn) {
service_result.start_handle, service_result.end_handle, 0, &total_char_count);
if (char_count_status != ESP_GATT_OK) {
this->log_gattc_warning_("esp_ble_gattc_get_attr_count", char_count_status);
conn.pending_error_ = char_count_status;
conn.send_service_ = DONE_SENDING_SERVICES;
conn.disconnect();
conn.abort_service_stream(char_count_status);
return;
}
@@ -600,9 +591,7 @@ void BluedroidGattClient::stream_service_batch(BluetoothConnection &conn) {
if (char_status != ESP_GATT_OK || cc == 0) {
if (char_status != ESP_GATT_OK) {
this->log_gattc_warning_("esp_ble_gattc_get_all_char", char_status);
conn.pending_error_ = char_status;
conn.send_service_ = DONE_SENDING_SERVICES;
conn.disconnect();
conn.abort_service_stream(char_status);
return;
}
break;
@@ -621,9 +610,7 @@ void BluedroidGattClient::stream_service_batch(BluetoothConnection &conn) {
// Abort rather than stream the characteristic descriptor-less: a
// missing CCCD in a cached database breaks notifications for good.
this->log_gattc_warning_("esp_ble_gattc_get_attr_count", desc_count_status);
conn.pending_error_ = desc_count_status;
conn.send_service_ = DONE_SENDING_SERVICES;
conn.disconnect();
conn.abort_service_stream(desc_count_status);
return;
}
if (total_desc_count > 0) {
@@ -640,9 +627,7 @@ void BluedroidGattClient::stream_service_batch(BluetoothConnection &conn) {
if (desc_status != ESP_GATT_OK || dc == 0) {
if (desc_status != ESP_GATT_OK) {
this->log_gattc_warning_("esp_ble_gattc_get_all_descr", desc_status);
conn.pending_error_ = desc_status;
conn.send_service_ = DONE_SENDING_SERVICES;
conn.disconnect();
conn.abort_service_stream(desc_status);
return;
}
break;
@@ -717,11 +702,9 @@ void BluedroidGattClient::handle_open_evt_(esp_ble_gattc_cb_param_t *param) {
// Discovery-bound connection: start the search now so it overlaps the
// MTU exchange. On a refusal fall back to the serialized path - the
// consumer's own discover_services() call retries the real search.
auto ret = esp_ble_gattc_search_service(this->gattc_if_, this->conn_id_, nullptr);
if (ret == ESP_OK) {
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;
} else {
this->log_gattc_warning_("esp_ble_gattc_search_service", ret);
}
}
}
@@ -817,16 +800,18 @@ bool BluedroidGattClient::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga
return false;
ESP_LOGI(TAG, "[%d] Service discovery complete", this->connection_index_);
this->search_done_ = true;
this->search_status_ = this->handle_search_cmpl_();
if (this->state() == ClientState::DISCONNECTING) {
// Teardown already owns the link: keep its state and safety timer;
// the terminal connected=false report settles the consumer.
// Teardown already owns the link: keep its state and safety timer,
// and skip the param/count work - the result is never delivered (the
// terminal connected=false report settles the consumer).
break;
}
this->search_status_ = this->handle_search_cmpl_();
this->set_state(ClientState::ESTABLISHED);
this->deliver_pending_search_();
if (this->state() != ClientState::DISCONNECTING) {
// Settled; a failed count started a teardown that needs the loop.
// Settled - unless delivering a failed status just started a
// teardown that needs the loop for its timer.
this->disable_loop();
}
break;
@@ -87,7 +87,7 @@ class BluedroidGattClient final : public esp32_ble_tracker::ESPBTClient, public
void handle_open_evt_(esp_ble_gattc_cb_param_t *param);
void handle_disconnect_evt_(esp_ble_gattc_cb_param_t *param);
int handle_search_cmpl_();
bool deliver_pending_search_();
void deliver_pending_search_();
void unconditional_disconnect_();
void set_idle_();
void set_disconnecting_();
@@ -41,6 +41,17 @@ class BluetoothConnection final : public ble_device_base::GattClientListener {
conn_err_t notify_characteristic(uint16_t handle, bool enable);
conn_err_t update_connection_params(uint16_t min_interval, uint16_t max_interval, uint16_t latency, uint16_t timeout);
/// Streamer abort: latch the GATT cause for the disconnect report (first
/// cause wins, matching disconnect()), park the stream cursor, and tear
/// the connection down.
void abort_service_stream(conn_err_t err) {
if (this->pending_error_ == 0) {
this->pending_error_ = err;
}
this->send_service_ = DONE_SENDING_SERVICES;
this->disconnect();
}
/// Start connecting: record the API address type (BLE_ADDR_TYPE_* code
/// space) and open the connection through the backend. Failures report
/// through the same reset path a failed open takes.
+11 -1
View File
@@ -1,4 +1,4 @@
from collections.abc import Callable
from collections.abc import Callable, Collection
from esphome.const import (
CONF_LEVEL,
@@ -98,6 +98,16 @@ def merge_config(old, new):
return new
def frameworks_for_platforms(platforms: Collection[str]) -> set[PlatformFramework]:
"""All PlatformFramework members whose platform is in `platforms`.
For FILTER_SOURCE_FILES maps that must stay in sync with a platform
registry: deriving the framework set here means a platform added to the
registry cannot validate and then fail at link on a filtered-out file.
"""
return {pf for pf in PlatformFramework if pf.value[0].value in platforms}
def filter_source_files_from_platform(
files_map: dict[str, set[PlatformFramework]],
) -> Callable[[], list[str]]:
@@ -9,11 +9,13 @@ import pytest
from esphome import config_validation as cv
from esphome.components import ble_device_base, bluetooth_connection, bluetooth_proxy
from esphome.config_helpers import frameworks_for_platforms
from esphome.const import (
CONF_ACTIVE,
KEY_CORE,
KEY_TARGET_FRAMEWORK,
KEY_TARGET_PLATFORM,
PLATFORM_ESP32,
PLATFORM_LN882X,
PLATFORM_RP2,
PlatformFramework,
@@ -177,6 +179,19 @@ def test_rp2_rejects_esp32_only_keys_by_name(
bluetooth_proxy.CONFIG_SCHEMA({"connections": [{}]})
def test_hub_source_filter_covers_every_hub_platform() -> None:
# bluetooth_connection cannot import this module to derive the hub.cpp
# framework set, so pin it here: a platform admitted to the proxy but
# missing from the filter would validate, then fail at link.
expected = frameworks_for_platforms(
[*bluetooth_proxy._HUB_PLATFORMS, PLATFORM_ESP32]
)
hub_frameworks = bluetooth_connection.SOURCE_FILE_FRAMEWORKS[
"bluetooth_connection_hub.cpp"
]
assert expected <= hub_frameworks
def test_bluetooth_connection_auto_load_covers_its_includes() -> None:
# The backend registers with its platform BLE stack (and the Bluedroid
# header includes the tracker's), so that closure lives here and