mirror of
https://github.com/esphome/esphome.git
synced 2026-08-24 07:06:20 +00:00
Simplify: flash vtable sink, shared table walker, one proxy predicate, slot ledger
This commit is contained in:
@@ -97,60 +97,69 @@ concept GattClientEventSinkContract = requires(S sink, const uint8_t *data) {
|
||||
{ sink.on_pairing_result(int{}) } -> std::same_as<void>;
|
||||
};
|
||||
|
||||
/// Type-erased consumer handle a backend delivers events through: one
|
||||
/// instance pointer plus a trampoline per event. Built with make_gatt_sink()
|
||||
/// from any type satisfying GattClientEventSinkContract; call sites read the
|
||||
/// same as a direct listener call. No virtuals, no heap — the cost of
|
||||
/// supporting several consumer types in one build is one indirect call per
|
||||
/// event. Codegen wires the sink before setup(), so backends may call
|
||||
/// without a null check.
|
||||
/// One trampoline per event, shared by every instance of a consumer type.
|
||||
struct GattEventVTable {
|
||||
void (*connection_state)(void *, bool, uint16_t, int);
|
||||
void (*service_discovery_done)(void *, int);
|
||||
void (*read_result)(void *, uint16_t, const uint8_t *, uint16_t, int);
|
||||
void (*write_result)(void *, uint16_t, int);
|
||||
void (*notify_state)(void *, uint16_t, bool, int);
|
||||
void (*notify_data)(void *, uint16_t, const uint8_t *, uint16_t);
|
||||
void (*pairing_result)(void *, int);
|
||||
};
|
||||
|
||||
// The per-consumer-type table lives in flash (constexpr), so a sink costs
|
||||
// two pointers of RAM regardless of how many events the surface carries.
|
||||
template<typename T>
|
||||
inline constexpr GattEventVTable GATT_EVENT_VTABLE{
|
||||
[](void *p, bool connected, uint16_t mtu, int error) {
|
||||
static_cast<T *>(p)->on_connection_state(connected, mtu, error);
|
||||
},
|
||||
[](void *p, int error) { static_cast<T *>(p)->on_service_discovery_done(error); },
|
||||
[](void *p, uint16_t handle, const uint8_t *data, uint16_t len, int error) {
|
||||
static_cast<T *>(p)->on_read_result(handle, data, len, error);
|
||||
},
|
||||
[](void *p, uint16_t handle, int error) { static_cast<T *>(p)->on_write_result(handle, error); },
|
||||
[](void *p, uint16_t handle, bool enabled, int error) {
|
||||
static_cast<T *>(p)->on_notify_state(handle, enabled, error);
|
||||
},
|
||||
[](void *p, uint16_t handle, const uint8_t *data, uint16_t len) {
|
||||
static_cast<T *>(p)->on_notify_data(handle, data, len);
|
||||
},
|
||||
[](void *p, int status) { static_cast<T *>(p)->on_pairing_result(status); },
|
||||
};
|
||||
|
||||
/// Type-erased consumer handle a backend delivers events through: an
|
||||
/// instance pointer plus the consumer type's trampoline table. Built with
|
||||
/// make_gatt_sink() from any type satisfying GattClientEventSinkContract;
|
||||
/// call sites read the same as a direct listener call. No virtuals, no heap —
|
||||
/// the cost of supporting several consumer types in one build is one
|
||||
/// indirect call per event. Codegen wires the sink before setup(), so
|
||||
/// backends may call without a null check.
|
||||
struct GattEventSink {
|
||||
void *instance{nullptr};
|
||||
void (*connection_state)(void *, bool, uint16_t, int){nullptr};
|
||||
void (*service_discovery_done)(void *, int){nullptr};
|
||||
void (*read_result)(void *, uint16_t, const uint8_t *, uint16_t, int){nullptr};
|
||||
void (*write_result)(void *, uint16_t, int){nullptr};
|
||||
void (*notify_state)(void *, uint16_t, bool, int){nullptr};
|
||||
void (*notify_data)(void *, uint16_t, const uint8_t *, uint16_t){nullptr};
|
||||
void (*pairing_result)(void *, int){nullptr};
|
||||
const GattEventVTable *vtable{nullptr};
|
||||
|
||||
void on_connection_state(bool connected, uint16_t mtu, int error) const {
|
||||
this->connection_state(this->instance, connected, mtu, error);
|
||||
this->vtable->connection_state(this->instance, connected, mtu, error);
|
||||
}
|
||||
void on_service_discovery_done(int error) const { this->service_discovery_done(this->instance, error); }
|
||||
void on_service_discovery_done(int error) const { this->vtable->service_discovery_done(this->instance, error); }
|
||||
void on_read_result(uint16_t handle, const uint8_t *data, uint16_t len, int error) const {
|
||||
this->read_result(this->instance, handle, data, len, error);
|
||||
this->vtable->read_result(this->instance, handle, data, len, error);
|
||||
}
|
||||
void on_write_result(uint16_t handle, int error) const { this->write_result(this->instance, handle, error); }
|
||||
void on_write_result(uint16_t handle, int error) const { this->vtable->write_result(this->instance, handle, error); }
|
||||
void on_notify_state(uint16_t handle, bool enabled, int error) const {
|
||||
this->notify_state(this->instance, handle, enabled, error);
|
||||
this->vtable->notify_state(this->instance, handle, enabled, error);
|
||||
}
|
||||
void on_notify_data(uint16_t handle, const uint8_t *data, uint16_t len) const {
|
||||
this->notify_data(this->instance, handle, data, len);
|
||||
this->vtable->notify_data(this->instance, handle, data, len);
|
||||
}
|
||||
void on_pairing_result(int status) const { this->pairing_result(this->instance, status); }
|
||||
void on_pairing_result(int status) const { this->vtable->pairing_result(this->instance, status); }
|
||||
};
|
||||
|
||||
template<typename T> GattEventSink make_gatt_sink(T *consumer) {
|
||||
static_assert(GattClientEventSinkContract<T>, "the consumer is missing part of the event-sink surface");
|
||||
return GattEventSink{
|
||||
consumer,
|
||||
[](void *p, bool connected, uint16_t mtu, int error) {
|
||||
static_cast<T *>(p)->on_connection_state(connected, mtu, error);
|
||||
},
|
||||
[](void *p, int error) { static_cast<T *>(p)->on_service_discovery_done(error); },
|
||||
[](void *p, uint16_t handle, const uint8_t *data, uint16_t len, int error) {
|
||||
static_cast<T *>(p)->on_read_result(handle, data, len, error);
|
||||
},
|
||||
[](void *p, uint16_t handle, int error) { static_cast<T *>(p)->on_write_result(handle, error); },
|
||||
[](void *p, uint16_t handle, bool enabled, int error) {
|
||||
static_cast<T *>(p)->on_notify_state(handle, enabled, error);
|
||||
},
|
||||
[](void *p, uint16_t handle, const uint8_t *data, uint16_t len) {
|
||||
static_cast<T *>(p)->on_notify_data(handle, data, len);
|
||||
},
|
||||
[](void *p, int status) { static_cast<T *>(p)->on_pairing_result(status); },
|
||||
};
|
||||
return {consumer, &GATT_EVENT_VTABLE<T>};
|
||||
}
|
||||
|
||||
// The BLEGattConnection op surface, asserted where the alias binds
|
||||
|
||||
@@ -7,6 +7,8 @@ owning a dedicated backend (e.g. radon_eye_rd200) through
|
||||
gatt_client_schema() + new_gatt_backend().
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.config_helpers import filter_source_files_from_platform
|
||||
import esphome.config_validation as cv
|
||||
@@ -15,8 +17,10 @@ from esphome.core import CORE
|
||||
from esphome.schema_extractors import SCHEMA_EXTRACT
|
||||
from esphome.types import ConfigType
|
||||
|
||||
DOMAIN = "bluetooth_connection"
|
||||
|
||||
def AUTO_LOAD(config: ConfigType | None = None) -> list[str]:
|
||||
|
||||
def AUTO_LOAD() -> list[str]:
|
||||
"""ble_device_base plus the platform BLE stack the build's backend
|
||||
registers with, so consumers stay platform-blind. The platform-less arm
|
||||
serves tooling that resolves the manifest without a target."""
|
||||
@@ -42,7 +46,9 @@ RP2_MAX_CONNECTIONS = 1
|
||||
HUB_MAX_CONNECTIONS: dict[str, int] = {PLATFORM_RP2: RP2_MAX_CONNECTIONS}
|
||||
|
||||
# Every platform with a GATT backend; gates dedicated-backend consumers.
|
||||
GATT_CLIENT_PLATFORMS = [PLATFORM_ESP32, PLATFORM_RP2]
|
||||
# Derived from the hub registry so a platform gaining a backend is admitted
|
||||
# everywhere at once (esp32 is the non-hub arm).
|
||||
GATT_CLIENT_PLATFORMS = [PLATFORM_ESP32, *HUB_MAX_CONNECTIONS]
|
||||
|
||||
# The hub-platform wrapper and the rp2 BTstack backend codegen classes.
|
||||
HubBluetoothConnection = bluetooth_connection_ns.class_("BluetoothConnection")
|
||||
@@ -78,25 +84,79 @@ def gatt_client_schema() -> cv.Schema:
|
||||
)
|
||||
|
||||
|
||||
def gatt_client_config_schema(base_schema: cv.Schema, consumer: str) -> cv.All:
|
||||
"""Wrap a dedicated-backend consumer's schema so the consumer stays
|
||||
platform-blind: gates on the platforms with a backend, folds in
|
||||
gatt_client_schema(), and does the esp32 controller-slot bookkeeping.
|
||||
`consumer` names the component in slot-exhaustion errors."""
|
||||
def hub_connection_schema() -> cv.Schema:
|
||||
"""Per-slot schema for the proxy's connection wrappers: the wrapper id on
|
||||
top of the backend fragment. Same call-time constraint as
|
||||
gatt_client_schema()."""
|
||||
return gatt_client_schema().extend(
|
||||
{cv.GenerateID(): cv.declare_id(HubBluetoothConnection)}
|
||||
)
|
||||
|
||||
def apply(config: ConfigType) -> ConfigType:
|
||||
if config is SCHEMA_EXTRACT:
|
||||
# The language-schema dumper runs without a platform; expose the
|
||||
# consumer's own keys.
|
||||
return base_schema
|
||||
config = base_schema.extend(gatt_client_schema())(config)
|
||||
|
||||
@dataclass
|
||||
class _SlotLedger:
|
||||
"""GATT connection slots claimed this run, for the platform cap check."""
|
||||
|
||||
consumers: list[str] = field(default_factory=list)
|
||||
|
||||
|
||||
def _ledger() -> _SlotLedger:
|
||||
if DOMAIN not in CORE.data:
|
||||
CORE.data[DOMAIN] = _SlotLedger()
|
||||
return CORE.data[DOMAIN]
|
||||
|
||||
|
||||
def consume_gatt_slot(consumer: str):
|
||||
"""Validator claiming one GATT connection slot: the neutral ledger feeds
|
||||
the platform cap check in FINAL_VALIDATE_SCHEMA, and esp32 additionally
|
||||
charges the controller's connection budget."""
|
||||
|
||||
def validator(config: ConfigType) -> ConfigType:
|
||||
_ledger().consumers.append(consumer)
|
||||
if CORE.is_esp32:
|
||||
from esphome.components import esp32_ble
|
||||
|
||||
esp32_ble.consume_connection_slots(1, consumer)(config)
|
||||
return config
|
||||
|
||||
return cv.All(cv.only_on(GATT_CLIENT_PLATFORMS), apply)
|
||||
return validator
|
||||
|
||||
|
||||
def _validate_slot_totals(config: ConfigType) -> ConfigType:
|
||||
# esp32 has its own controller budget (esp32_ble); the hub platforms cap
|
||||
# at the prebuilt stack's client count, and nothing else counts claims
|
||||
# across components (e.g. a proxy plus a radon_eye_rd200 on rp2).
|
||||
if (cap := HUB_MAX_CONNECTIONS.get(CORE.target_platform)) is None:
|
||||
return config
|
||||
claimed = _ledger().consumers
|
||||
if len(claimed) > cap:
|
||||
raise cv.Invalid(
|
||||
f"{CORE.target_platform} supports at most {cap} GATT client "
|
||||
f"connection(s); {len(claimed)} requested by: {', '.join(claimed)}"
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _validate_slot_totals
|
||||
|
||||
|
||||
def gatt_client_config_schema(base_schema: cv.Schema, consumer: str):
|
||||
"""Wrap a dedicated-backend consumer's schema so the consumer stays
|
||||
platform-blind: gates on the platforms with a backend, folds in
|
||||
gatt_client_schema(), and claims the connection slot.
|
||||
`consumer` names the component in slot-exhaustion errors."""
|
||||
|
||||
def apply(config: ConfigType) -> ConfigType:
|
||||
if config is SCHEMA_EXTRACT:
|
||||
# The language-schema dumper runs without a platform; expose the
|
||||
# consumer's own keys. Checked before the platform gate so the
|
||||
# dumper is not rejected by only_on.
|
||||
return base_schema
|
||||
cv.only_on(GATT_CLIENT_PLATFORMS)(config)
|
||||
config = base_schema.extend(gatt_client_schema())(config)
|
||||
return consume_gatt_slot(consumer)(config)
|
||||
|
||||
return apply
|
||||
|
||||
|
||||
async def new_gatt_backend(config: ConfigType) -> cg.MockObj:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#include "bluetooth_connection.h"
|
||||
|
||||
#ifdef BLUETOOTH_CONNECTION_HAS_GATT
|
||||
#ifdef BLUETOOTH_CONNECTION_SERVES_PROXY
|
||||
|
||||
#include "esphome/components/api/api_pb2.h"
|
||||
#include "esphome/core/log.h"
|
||||
@@ -39,4 +39,4 @@ BatchClose close_service_batch(api::BluetoothGATTGetServicesResponse &resp, size
|
||||
|
||||
} // namespace esphome::bluetooth_connection
|
||||
|
||||
#endif // BLUETOOTH_CONNECTION_HAS_GATT
|
||||
#endif // BLUETOOTH_CONNECTION_SERVES_PROXY
|
||||
|
||||
@@ -16,14 +16,16 @@
|
||||
#include <esp_err.h>
|
||||
#endif
|
||||
|
||||
// The connection-aware API request handlers are compiled: a proxy is present
|
||||
// and a GATT backend is wired by codegen (one slot per connection).
|
||||
// The proxy-serving surface is compiled: a proxy is present and a GATT
|
||||
// backend is wired by codegen (one slot per connection). This is the single
|
||||
// spelling of that predicate - the hub wrapper, the connection-aware API
|
||||
// request handlers, and the Bluedroid in-place streamer all gate on it.
|
||||
// Advertisement-only builds get the clean-error handlers; address-scoped
|
||||
// maintenance (unpair, cache clear) still works there through the
|
||||
// per-platform free functions below. Backend-only builds (a dedicated-backend
|
||||
// consumer without bluetooth_proxy) compile none of this API surface.
|
||||
#if defined(USE_BLE_GATT_CLIENT) && defined(USE_BLUETOOTH_PROXY)
|
||||
#define BLUETOOTH_CONNECTION_HAS_GATT
|
||||
#define BLUETOOTH_CONNECTION_SERVES_PROXY
|
||||
#endif
|
||||
|
||||
namespace esphome::api {
|
||||
@@ -139,7 +141,7 @@ inline void fill_gatt_uuid(std::array<uint64_t, 2> &uuid_128, uint32_t &short_uu
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef BLUETOOTH_CONNECTION_HAS_GATT
|
||||
#ifdef BLUETOOTH_CONNECTION_SERVES_PROXY
|
||||
/// Result of close_service_batch: keep filling the batch or send it now.
|
||||
/// An oversized service is packed alone; a failed (backpressured) send is
|
||||
/// retried from the batch start, so no service is silently skipped.
|
||||
@@ -151,6 +153,6 @@ enum class BatchClose : uint8_t { CONTINUE, SEND };
|
||||
/// cannot drift.
|
||||
BatchClose close_service_batch(api::BluetoothGATTGetServicesResponse &resp, size_t ¤t_size, int16_t &send_service,
|
||||
uint8_t connection_index, const char *address_str);
|
||||
#endif // BLUETOOTH_CONNECTION_HAS_GATT
|
||||
#endif // BLUETOOTH_CONNECTION_SERVES_PROXY
|
||||
|
||||
} // namespace esphome::bluetooth_connection
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
|
||||
#if defined(USE_ESP32_BLE) && defined(USE_BLE_GATT_CLIENT)
|
||||
|
||||
#include "bluetooth_connection.h"
|
||||
|
||||
// The in-place streamer serves the proxy's service-discovery API; backend-only
|
||||
// builds compile without the proxy headers or the streamer.
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
#include "bluetooth_connection.h"
|
||||
#ifdef BLUETOOTH_CONNECTION_SERVES_PROXY
|
||||
#include "bluetooth_connection_hub.h"
|
||||
|
||||
#include "esphome/components/bluetooth_proxy/bluetooth_proxy.h"
|
||||
@@ -273,12 +274,11 @@ void BluedroidGattClient::free_service_table_() {
|
||||
this->table_desc_total_ = 0;
|
||||
}
|
||||
|
||||
bool BluedroidGattClient::build_service_table_() {
|
||||
// Pass 1: count characteristics and descriptors so one exact-size block
|
||||
// holds the whole table (descriptor counts need the characteristic handles,
|
||||
// so this pass already enumerates characteristics).
|
||||
uint16_t char_total = 0;
|
||||
uint16_t desc_total = 0;
|
||||
template<typename ServiceFn, typename CharFn, typename DescFn>
|
||||
bool BluedroidGattClient::walk_database_(ServiceFn &&on_service, CharFn &&on_char, DescFn &&on_desc) {
|
||||
// Shared enumeration for both table-build passes: an identical walk order
|
||||
// is what lets the counting pass size the block the filling pass fills.
|
||||
// INVALID_OFFSET/NOT_FOUND mean end-of-range; anything else is a failure.
|
||||
for (uint16_t s = 0; s < this->service_total_; s++) {
|
||||
esp_gattc_service_elem_t svc;
|
||||
uint16_t svc_count = 1;
|
||||
@@ -286,6 +286,9 @@ bool BluedroidGattClient::build_service_table_() {
|
||||
svc_count == 0) {
|
||||
return false;
|
||||
}
|
||||
if (!on_service(s, svc)) {
|
||||
return false;
|
||||
}
|
||||
uint16_t svc_chars = 0;
|
||||
if (esp_ble_gattc_get_attr_count(this->gattc_if_, this->conn_id_, ESP_GATT_DB_CHARACTERISTIC, svc.start_handle,
|
||||
svc.end_handle, 0, &svc_chars) != ESP_GATT_OK) {
|
||||
@@ -302,13 +305,45 @@ bool BluedroidGattClient::build_service_table_() {
|
||||
if (status != ESP_GATT_OK || char_count == 0) {
|
||||
return false;
|
||||
}
|
||||
char_total++;
|
||||
uint16_t chr_descs = 0;
|
||||
esp_ble_gattc_get_attr_count(this->gattc_if_, this->conn_id_, ESP_GATT_DB_DESCRIPTOR, 0, 0, chr.char_handle,
|
||||
&chr_descs);
|
||||
desc_total += chr_descs;
|
||||
if (!on_char(svc, chr)) {
|
||||
return false;
|
||||
}
|
||||
for (uint16_t d = 0;; d++) {
|
||||
esp_gattc_descr_elem_t desc;
|
||||
uint16_t desc_count = 1;
|
||||
auto desc_status =
|
||||
esp_ble_gattc_get_all_descr(this->gattc_if_, this->conn_id_, chr.char_handle, &desc, &desc_count, d);
|
||||
if (desc_status == ESP_GATT_INVALID_OFFSET || desc_status == ESP_GATT_NOT_FOUND) {
|
||||
break;
|
||||
}
|
||||
if (desc_status != ESP_GATT_OK || desc_count == 0) {
|
||||
return false;
|
||||
}
|
||||
if (!on_desc(chr, desc)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool BluedroidGattClient::build_service_table_() {
|
||||
// Pass 1: count, so one exact-size block holds the whole table.
|
||||
uint16_t char_total = 0;
|
||||
uint16_t desc_total = 0;
|
||||
bool counted = this->walk_database_([](uint16_t, const esp_gattc_service_elem_t &) { return true; },
|
||||
[&](const esp_gattc_service_elem_t &, const esp_gattc_char_elem_t &) {
|
||||
char_total++;
|
||||
return true;
|
||||
},
|
||||
[&](const esp_gattc_char_elem_t &, const esp_gattc_descr_elem_t &) {
|
||||
desc_total++;
|
||||
return true;
|
||||
});
|
||||
if (!counted) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// The arrays share one block; carving stays aligned because each struct's
|
||||
// strictest member is the UUID and array sizes are multiples of it.
|
||||
@@ -327,74 +362,55 @@ bool BluedroidGattClient::build_service_table_() {
|
||||
auto *descriptors =
|
||||
reinterpret_cast<ble_device_base::GattDescriptor *>(this->table_storage_ + svc_bytes + char_bytes);
|
||||
|
||||
// Pass 2: fill. Every write is bounded by the pass-1 totals, so a
|
||||
// misbehaving peripheral returning extra entries cannot overrun the block.
|
||||
// Pass 2: fill, bounded by the pass-1 totals. A bound trip or a shortfall
|
||||
// means the cached database changed between the passes; fail the build
|
||||
// rather than serve an inconsistent table (the consumer retries).
|
||||
uint16_t char_index = 0;
|
||||
uint16_t desc_index = 0;
|
||||
for (uint16_t s = 0; s < this->service_total_; s++) {
|
||||
esp_gattc_service_elem_t svc;
|
||||
uint16_t svc_count = 1;
|
||||
if (esp_ble_gattc_get_service(this->gattc_if_, this->conn_id_, nullptr, &svc, &svc_count, s) != ESP_GATT_OK ||
|
||||
svc_count == 0) {
|
||||
this->free_service_table_();
|
||||
return false;
|
||||
}
|
||||
auto &service = services[s];
|
||||
service.uuid = ble_device_base::ESPBTUUID::from_uuid(svc.uuid);
|
||||
service.start_handle = svc.start_handle;
|
||||
service.end_handle = svc.end_handle;
|
||||
service.first_characteristic = char_index;
|
||||
for (uint16_t c = 0; char_index < char_total; c++) {
|
||||
esp_gattc_char_elem_t chr;
|
||||
uint16_t char_count = 1;
|
||||
auto status = esp_ble_gattc_get_all_char(this->gattc_if_, this->conn_id_, svc.start_handle, svc.end_handle, &chr,
|
||||
&char_count, c);
|
||||
if (status == ESP_GATT_INVALID_OFFSET || status == ESP_GATT_NOT_FOUND) {
|
||||
break;
|
||||
}
|
||||
if (status != ESP_GATT_OK || char_count == 0) {
|
||||
this->free_service_table_();
|
||||
return false;
|
||||
}
|
||||
auto &characteristic = characteristics[char_index];
|
||||
characteristic.uuid = ble_device_base::ESPBTUUID::from_uuid(chr.uuid);
|
||||
characteristic.value_handle = chr.char_handle;
|
||||
// Bluedroid addresses descriptors by characteristic handle, so the
|
||||
// table's end_handle only needs the service-bounded upper bound.
|
||||
characteristic.end_handle = svc.end_handle;
|
||||
characteristic.properties = chr.properties;
|
||||
characteristic.first_descriptor = desc_index;
|
||||
uint16_t chr_descs = 0;
|
||||
esp_ble_gattc_get_attr_count(this->gattc_if_, this->conn_id_, ESP_GATT_DB_DESCRIPTOR, 0, 0, chr.char_handle,
|
||||
&chr_descs);
|
||||
for (uint16_t d = 0; d < chr_descs && desc_index < desc_total; d++) {
|
||||
esp_gattc_descr_elem_t desc;
|
||||
uint16_t desc_count = 1;
|
||||
auto desc_status =
|
||||
esp_ble_gattc_get_all_descr(this->gattc_if_, this->conn_id_, chr.char_handle, &desc, &desc_count, d);
|
||||
if (desc_status == ESP_GATT_INVALID_OFFSET || desc_status == ESP_GATT_NOT_FOUND) {
|
||||
break;
|
||||
ble_device_base::GattService *cur_service = nullptr;
|
||||
ble_device_base::GattCharacteristic *cur_char = nullptr;
|
||||
bool filled = this->walk_database_(
|
||||
[&](uint16_t s, const esp_gattc_service_elem_t &svc) {
|
||||
cur_service = &services[s];
|
||||
cur_service->uuid = ble_device_base::ESPBTUUID::from_uuid(svc.uuid);
|
||||
cur_service->start_handle = svc.start_handle;
|
||||
cur_service->end_handle = svc.end_handle;
|
||||
cur_service->first_characteristic = char_index;
|
||||
cur_service->characteristic_count = 0;
|
||||
return true;
|
||||
},
|
||||
[&](const esp_gattc_service_elem_t &svc, const esp_gattc_char_elem_t &chr) {
|
||||
if (char_index >= char_total) {
|
||||
return false;
|
||||
}
|
||||
if (desc_status != ESP_GATT_OK || desc_count == 0) {
|
||||
this->free_service_table_();
|
||||
cur_char = &characteristics[char_index++];
|
||||
cur_char->uuid = ble_device_base::ESPBTUUID::from_uuid(chr.uuid);
|
||||
cur_char->value_handle = chr.char_handle;
|
||||
// Bluedroid addresses descriptors by characteristic handle, so the
|
||||
// table's end_handle only needs the service-bounded upper bound.
|
||||
cur_char->end_handle = svc.end_handle;
|
||||
cur_char->properties = chr.properties;
|
||||
cur_char->first_descriptor = desc_index;
|
||||
cur_char->descriptor_count = 0;
|
||||
cur_service->characteristic_count++;
|
||||
return true;
|
||||
},
|
||||
[&](const esp_gattc_char_elem_t &, const esp_gattc_descr_elem_t &desc) {
|
||||
if (desc_index >= desc_total) {
|
||||
return false;
|
||||
}
|
||||
descriptors[desc_index].uuid = ble_device_base::ESPBTUUID::from_uuid(desc.uuid);
|
||||
descriptors[desc_index].handle = desc.handle;
|
||||
desc_index++;
|
||||
}
|
||||
characteristic.descriptor_count = desc_index - characteristic.first_descriptor;
|
||||
char_index++;
|
||||
}
|
||||
service.characteristic_count = char_index - service.first_characteristic;
|
||||
cur_char->descriptor_count++;
|
||||
return true;
|
||||
});
|
||||
if (!filled || char_index != char_total || desc_index != desc_total) {
|
||||
this->free_service_table_();
|
||||
return false;
|
||||
}
|
||||
if (char_index < char_total) {
|
||||
// Fewer characteristics enumerated than counted: close the gap so the
|
||||
// view's descriptor offset can be derived from the filled count alone.
|
||||
memmove(characteristics + char_index, descriptors, desc_index * sizeof(ble_device_base::GattDescriptor));
|
||||
}
|
||||
this->table_char_total_ = char_index;
|
||||
this->table_desc_total_ = desc_index;
|
||||
this->table_char_total_ = char_total;
|
||||
this->table_desc_total_ = desc_total;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -459,16 +475,14 @@ void BluedroidGattClient::handle_search_cmpl_() {
|
||||
// clients cache the streamed result permanently.
|
||||
auto status = primary_status != ESP_GATT_OK ? primary_status : secondary_status;
|
||||
this->log_gattc_warning_("esp_ble_gattc_get_attr_count", status);
|
||||
if (this->listener_ != nullptr) {
|
||||
this->listener_->on_service_discovery_done(status);
|
||||
}
|
||||
this->sink_.on_service_discovery_done(status);
|
||||
return;
|
||||
}
|
||||
this->service_total_ = primary + secondary;
|
||||
this->sink_.on_service_discovery_done(0);
|
||||
}
|
||||
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
#ifdef BLUETOOTH_CONNECTION_SERVES_PROXY
|
||||
void BluedroidGattClient::stream_service_batch(BluetoothConnection &conn) {
|
||||
if (this->services_released_ || conn.send_service_ >= this->service_total_) {
|
||||
conn.send_service_ = DONE_SENDING_SERVICES;
|
||||
@@ -607,7 +621,7 @@ void BluedroidGattClient::stream_service_batch(BluetoothConnection &conn) {
|
||||
conn.send_service_ = batch_start;
|
||||
}
|
||||
}
|
||||
#endif // USE_BLUETOOTH_PROXY
|
||||
#endif // BLUETOOTH_CONNECTION_SERVES_PROXY
|
||||
|
||||
// ---- events ----
|
||||
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
|
||||
#if defined(USE_ESP32_BLE) && defined(USE_BLE_GATT_CLIENT)
|
||||
|
||||
#include "bluetooth_connection.h"
|
||||
|
||||
#include "esphome/components/ble_device_base/ble_gatt_client.h"
|
||||
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
|
||||
#include "esphome/core/component.h"
|
||||
@@ -23,7 +25,7 @@
|
||||
namespace esphome::bluetooth_connection {
|
||||
|
||||
class BluedroidGattClient;
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
#ifdef BLUETOOTH_CONNECTION_SERVES_PROXY
|
||||
class BluetoothConnection;
|
||||
#endif
|
||||
|
||||
@@ -77,7 +79,7 @@ class BluedroidGattClient final : public Component {
|
||||
ble_device_base::GattServiceTable get_service_table();
|
||||
void release_services();
|
||||
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
#ifdef BLUETOOTH_CONNECTION_SERVES_PROXY
|
||||
/// In-place service streamer (the proxy wrapper detects and prefers it):
|
||||
/// builds one api response batch directly from Bluedroid's cached database,
|
||||
/// so the streaming peak is the response itself - the old esp32 model.
|
||||
@@ -108,6 +110,8 @@ class BluedroidGattClient final : public Component {
|
||||
const char *param_type);
|
||||
int check_and_log_error_(const char *operation, esp_err_t err);
|
||||
void log_gattc_warning_(const char *operation, int code);
|
||||
template<typename ServiceFn, typename CharFn, typename DescFn>
|
||||
bool walk_database_(ServiceFn &&on_service, CharFn &&on_char, DescFn &&on_desc);
|
||||
bool build_service_table_();
|
||||
void free_service_table_();
|
||||
ble_device_base::GattServiceTable table_view_() const;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Hub-platform connection wrapper (USE_RP2 hub builds today).
|
||||
#include "bluetooth_connection_hub.h"
|
||||
|
||||
#if defined(USE_BLE_GATT_CLIENT) && defined(USE_BLUETOOTH_PROXY)
|
||||
#ifdef BLUETOOTH_CONNECTION_SERVES_PROXY
|
||||
|
||||
#include "esphome/components/api/api_pb2.h"
|
||||
#include "esphome/components/bluetooth_proxy/bluetooth_proxy.h"
|
||||
@@ -436,4 +436,4 @@ void BluetoothConnection::send_service_for_discovery_() {
|
||||
|
||||
} // namespace esphome::bluetooth_connection
|
||||
|
||||
#endif // USE_BLE_GATT_CLIENT && USE_BLUETOOTH_PROXY
|
||||
#endif // BLUETOOTH_CONNECTION_SERVES_PROXY
|
||||
|
||||
@@ -5,14 +5,12 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#include "bluetooth_connection.h"
|
||||
|
||||
// The wrapper exists to serve the proxy's API surface; dedicated-backend
|
||||
// consumers (radon_eye_rd200) drive the backend directly, so backend-only
|
||||
// builds compile this header empty.
|
||||
#if defined(USE_BLE_GATT_CLIENT) && defined(USE_BLUETOOTH_PROXY)
|
||||
|
||||
#include "bluetooth_connection.h"
|
||||
#ifdef BLUETOOTH_CONNECTION_SERVES_PROXY
|
||||
|
||||
#include "esphome/components/ble_device_base/ble_client_state.h"
|
||||
#include "bluetooth_connection_gatt_backend.h"
|
||||
@@ -154,4 +152,4 @@ static_assert(ble_device_base::GattClientEventSinkContract<BluetoothConnection>,
|
||||
|
||||
} // namespace esphome::bluetooth_connection
|
||||
|
||||
#endif // USE_BLE_GATT_CLIENT && USE_BLUETOOTH_PROXY
|
||||
#endif // BLUETOOTH_CONNECTION_SERVES_PROXY
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#include "bluetooth_connection_rp2.h"
|
||||
|
||||
#include "bluetooth_connection_hub.h"
|
||||
#include "bluetooth_connection.h"
|
||||
|
||||
#if defined(USE_RP2040_BLE) && defined(USE_BLE_GATT_CLIENT)
|
||||
@@ -621,8 +620,7 @@ void RP2GattClient::handle_query_complete_(uint8_t att_status) {
|
||||
this->op_len_ > 0) {
|
||||
att_status = 0;
|
||||
}
|
||||
this->sink_.on_read_result(this->op_handle_, this->op_buffer_, att_status == 0 ? this->op_len_ : 0,
|
||||
att_status);
|
||||
this->sink_.on_read_result(this->op_handle_, this->op_buffer_, att_status == 0 ? this->op_len_ : 0, att_status);
|
||||
break;
|
||||
case OpType::WRITE_CHAR:
|
||||
case OpType::WRITE_DESC:
|
||||
|
||||
@@ -84,17 +84,7 @@ def _esp32_config_schema() -> cv.All:
|
||||
f"update _IDF_MAX_CONNECTIONS in bluetooth_proxy/__init__.py"
|
||||
)
|
||||
|
||||
CONNECTION_SCHEMA = (
|
||||
bluetooth_connection.gatt_client_schema()
|
||||
.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(
|
||||
bluetooth_connection.HubBluetoothConnection
|
||||
)
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
)
|
||||
CONNECTION_SCHEMA = bluetooth_connection.hub_connection_schema()
|
||||
|
||||
def validate_connections(config):
|
||||
if CONF_CONNECTIONS in config:
|
||||
@@ -157,15 +147,15 @@ def _rp2_config_schema() -> cv.All:
|
||||
"""Full proxy on the rp2 BLE hub: active connections through the BTstack
|
||||
GATT client backend in bluetooth_connection. The slot limit comes from the
|
||||
prebuilt BTstack library (one connection today); the code is built for N."""
|
||||
connection_schema = bluetooth_connection.gatt_client_schema().extend(
|
||||
{cv.GenerateID(): cv.declare_id(bluetooth_connection.HubBluetoothConnection)}
|
||||
)
|
||||
connection_schema = bluetooth_connection.hub_connection_schema()
|
||||
|
||||
def populate_connections(config: ConfigType) -> ConfigType:
|
||||
# One wrapper + backend pair per slot, declared during validation so
|
||||
# their ids exist for codegen (the esp32 arm's `connections` pattern).
|
||||
if not config[CONF_ACTIVE]:
|
||||
return config
|
||||
for _ in range(config[CONF_CONNECTION_SLOTS]):
|
||||
bluetooth_connection.consume_gatt_slot("bluetooth_proxy")(config)
|
||||
return {
|
||||
**config,
|
||||
CONF_CONNECTIONS: [
|
||||
|
||||
@@ -13,36 +13,32 @@ static const char *const TAG = "radon_eye_rd200";
|
||||
|
||||
using ble_device_base::ESPBTUUID;
|
||||
|
||||
// V1 (RD200 firmware < 2.0) exposes a vendor service; V2 (>= 2.0) moved to a
|
||||
// 16-bit-based service with a different command byte and payload layout.
|
||||
// V1 (RD200 firmware < 2.0) exposes a vendor service; V2 (>= 2.0) moved to
|
||||
// Bluetooth-base (16-bit) UUIDs with a different command byte and payload
|
||||
// layout.
|
||||
static const char *const SERVICE_UUID_V1 = "00001523-1212-efde-1523-785feabcd123";
|
||||
static const char *const WRITE_CHARACTERISTIC_UUID_V1 = "00001524-1212-efde-1523-785feabcd123";
|
||||
static const char *const READ_CHARACTERISTIC_UUID_V1 = "00001525-1212-efde-1523-785feabcd123";
|
||||
static const uint8_t WRITE_COMMAND_V1 = 0x50;
|
||||
|
||||
static const char *const SERVICE_UUID_V2 = "00001523-0000-1000-8000-00805f9b34fb";
|
||||
static const char *const WRITE_CHARACTERISTIC_UUID_V2 = "00001524-0000-1000-8000-00805f9b34fb";
|
||||
static const char *const READ_CHARACTERISTIC_UUID_V2 = "00001525-0000-1000-8000-00805f9b34fb";
|
||||
static const uint16_t SERVICE_UUID_V2 = 0x1523;
|
||||
static const uint16_t WRITE_CHARACTERISTIC_UUID_V2 = 0x1524;
|
||||
static const uint16_t READ_CHARACTERISTIC_UUID_V2 = 0x1525;
|
||||
static const uint8_t WRITE_COMMAND_V2 = 0x40;
|
||||
|
||||
// Minimum notification payload carrying all three measurements.
|
||||
static const uint16_t MESSAGE_MIN_LEN_V1 = 20;
|
||||
static const uint16_t MESSAGE_MIN_LEN_V2 = 68;
|
||||
|
||||
// BLE public address type (shared code space with the API/backends).
|
||||
static const uint8_t BLE_ADDR_TYPE_PUBLIC = 0;
|
||||
|
||||
void RadonEyeRD200::update() {
|
||||
if (this->busy_) {
|
||||
ESP_LOGW(TAG, "Connection in progress");
|
||||
// The backends refuse a connect on a non-idle slot, so a poll landing
|
||||
// mid-sequence just logs and retries next interval.
|
||||
int err = this->backend_->connect(this->address_, ble_device_base::BLE_ADDR_TYPE_PUBLIC);
|
||||
if (err != 0) {
|
||||
ESP_LOGW(TAG, "Connection in progress (err=%d)", err);
|
||||
return;
|
||||
}
|
||||
ESP_LOGD(TAG, "Connecting");
|
||||
if (this->backend_->connect(this->address_, BLE_ADDR_TYPE_PUBLIC) == 0) {
|
||||
this->busy_ = true;
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Connect request rejected, will retry");
|
||||
}
|
||||
}
|
||||
|
||||
void RadonEyeRD200::on_connection_state(bool connected, uint16_t mtu, int error) {
|
||||
@@ -50,82 +46,84 @@ void RadonEyeRD200::on_connection_state(bool connected, uint16_t mtu, int error)
|
||||
if (error != 0) {
|
||||
ESP_LOGW(TAG, "Disconnected, status=%d", error);
|
||||
}
|
||||
this->busy_ = false;
|
||||
return;
|
||||
}
|
||||
ESP_LOGI(TAG, "Connected successfully!");
|
||||
if (this->backend_->discover_services() != 0) {
|
||||
this->abort_connection_();
|
||||
this->backend_->disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
void RadonEyeRD200::on_service_discovery_done(int error) {
|
||||
if (error != 0) {
|
||||
ESP_LOGW(TAG, "Service discovery failed, status=%d", error);
|
||||
this->abort_connection_();
|
||||
this->backend_->disconnect();
|
||||
return;
|
||||
}
|
||||
bool resolved = this->resolve_handles_();
|
||||
// The table is backend-owned transient storage; release before continuing.
|
||||
this->backend_->release_services();
|
||||
if (!resolved) {
|
||||
this->abort_connection_();
|
||||
this->backend_->disconnect();
|
||||
return;
|
||||
}
|
||||
// Local notification registration; the CCCD write follows in
|
||||
// on_notify_state (the contract leaves the CCCD to the client).
|
||||
if (this->backend_->notify_characteristic(this->read_handle_, true) != 0) {
|
||||
this->abort_connection_();
|
||||
this->backend_->disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
bool RadonEyeRD200::resolve_handles_() {
|
||||
auto table = this->backend_->get_service_table();
|
||||
const char *write_uuid;
|
||||
const char *read_uuid;
|
||||
const ble_device_base::GattService *service;
|
||||
if ((service = ble_device_base::find_service(table, ESPBTUUID::from_raw(SERVICE_UUID_V1))) != nullptr) {
|
||||
write_uuid = WRITE_CHARACTERISTIC_UUID_V1;
|
||||
read_uuid = READ_CHARACTERISTIC_UUID_V1;
|
||||
this->write_command_ = WRITE_COMMAND_V1;
|
||||
} else if ((service = ble_device_base::find_service(table, ESPBTUUID::from_raw(SERVICE_UUID_V2))) != nullptr) {
|
||||
write_uuid = WRITE_CHARACTERISTIC_UUID_V2;
|
||||
read_uuid = READ_CHARACTERISTIC_UUID_V2;
|
||||
this->write_command_ = WRITE_COMMAND_V2;
|
||||
} else {
|
||||
ESP_LOGW(TAG, "No supported device has been found, disconnecting");
|
||||
return false;
|
||||
struct Variant {
|
||||
ESPBTUUID service;
|
||||
ESPBTUUID write_chr;
|
||||
ESPBTUUID read_chr;
|
||||
uint8_t command;
|
||||
};
|
||||
// Built on the stack per (cold) discovery so the UUID objects stay out of
|
||||
// static RAM; the V1 strings live in flash.
|
||||
const Variant variants[] = {
|
||||
{ESPBTUUID::from_raw(SERVICE_UUID_V1), ESPBTUUID::from_raw(WRITE_CHARACTERISTIC_UUID_V1),
|
||||
ESPBTUUID::from_raw(READ_CHARACTERISTIC_UUID_V1), WRITE_COMMAND_V1},
|
||||
{ESPBTUUID::from_uint16(SERVICE_UUID_V2), ESPBTUUID::from_uint16(WRITE_CHARACTERISTIC_UUID_V2),
|
||||
ESPBTUUID::from_uint16(READ_CHARACTERISTIC_UUID_V2), WRITE_COMMAND_V2},
|
||||
};
|
||||
for (const auto &variant : variants) {
|
||||
const auto *service = ble_device_base::find_service(table, variant.service);
|
||||
if (service == nullptr) {
|
||||
continue;
|
||||
}
|
||||
const auto *read_chr = ble_device_base::find_characteristic(table, *service, variant.read_chr);
|
||||
const auto *write_chr = ble_device_base::find_characteristic(table, *service, variant.write_chr);
|
||||
if (read_chr == nullptr || write_chr == nullptr) {
|
||||
ESP_LOGW(TAG, "Service found but a sensor characteristic is missing");
|
||||
return false;
|
||||
}
|
||||
this->cccd_handle_ = ble_device_base::find_cccd(table, *read_chr);
|
||||
if (this->cccd_handle_ == 0) {
|
||||
ESP_LOGW(TAG, "Sensor read characteristic has no CCCD");
|
||||
return false;
|
||||
}
|
||||
this->read_handle_ = read_chr->value_handle;
|
||||
this->write_handle_ = write_chr->value_handle;
|
||||
this->write_command_ = variant.command;
|
||||
return true;
|
||||
}
|
||||
|
||||
const auto *read_chr = ble_device_base::find_characteristic(table, *service, ESPBTUUID::from_raw(read_uuid));
|
||||
if (read_chr == nullptr) {
|
||||
ESP_LOGW(TAG, "No sensor read characteristic found at char %s", read_uuid);
|
||||
return false;
|
||||
}
|
||||
const auto *write_chr = ble_device_base::find_characteristic(table, *service, ESPBTUUID::from_raw(write_uuid));
|
||||
if (write_chr == nullptr) {
|
||||
ESP_LOGW(TAG, "No sensor write characteristic found at char %s", write_uuid);
|
||||
return false;
|
||||
}
|
||||
this->cccd_handle_ = ble_device_base::find_cccd(table, *read_chr);
|
||||
if (this->cccd_handle_ == 0) {
|
||||
ESP_LOGW(TAG, "Sensor read characteristic has no CCCD");
|
||||
return false;
|
||||
}
|
||||
this->read_handle_ = read_chr->value_handle;
|
||||
this->write_handle_ = write_chr->value_handle;
|
||||
return true;
|
||||
ESP_LOGW(TAG, "No supported device has been found, disconnecting");
|
||||
return false;
|
||||
}
|
||||
|
||||
void RadonEyeRD200::on_notify_state(uint16_t handle, bool enabled, int error) {
|
||||
if (error != 0) {
|
||||
ESP_LOGW(TAG, "Error registering for sensor notify, status=%d", error);
|
||||
this->abort_connection_();
|
||||
this->backend_->disconnect();
|
||||
return;
|
||||
}
|
||||
static const uint8_t enable_notify[2] = {0x01, 0x00};
|
||||
if (this->backend_->write_descriptor(this->cccd_handle_, enable_notify, sizeof(enable_notify)) != 0) {
|
||||
this->abort_connection_();
|
||||
this->backend_->disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,14 +135,14 @@ void RadonEyeRD200::on_write_result(uint16_t handle, int error) {
|
||||
}
|
||||
if (error != 0) {
|
||||
ESP_LOGE(TAG, "write descr failed, error status = %x", error);
|
||||
this->abort_connection_();
|
||||
this->backend_->disconnect();
|
||||
return;
|
||||
}
|
||||
ESP_LOGV(TAG, "Write descr success, writing 0x%02X at write_handle=%d", this->write_command_, this->write_handle_);
|
||||
if (this->backend_->write_characteristic(this->write_handle_, &this->write_command_, sizeof(this->write_command_),
|
||||
false) != 0) {
|
||||
ESP_LOGW(TAG, "Error writing 0x%02x command", this->write_command_);
|
||||
this->abort_connection_();
|
||||
this->backend_->disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,8 +154,6 @@ void RadonEyeRD200::on_notify_data(uint16_t handle, const uint8_t *data, uint16_
|
||||
this->backend_->disconnect();
|
||||
}
|
||||
|
||||
void RadonEyeRD200::abort_connection_() { this->backend_->disconnect(); }
|
||||
|
||||
void RadonEyeRD200::read_sensors_(const uint8_t *value, uint16_t value_len) {
|
||||
if (value_len < 1) {
|
||||
ESP_LOGW(TAG, "Unexpected empty message");
|
||||
|
||||
@@ -25,7 +25,7 @@ namespace esphome::radon_eye_rd200 {
|
||||
|
||||
class RadonEyeRD200 final : public PollingComponent {
|
||||
public:
|
||||
RadonEyeRD200(ble_device_base::BLEGattConnection *backend, uint64_t address) : address_(address), backend_(backend) {
|
||||
RadonEyeRD200(ble_device_base::BLEGattConnection *backend, uint64_t address) : backend_(backend), address_(address) {
|
||||
backend->set_sink(ble_device_base::make_gatt_sink(this));
|
||||
}
|
||||
|
||||
@@ -47,16 +47,16 @@ class RadonEyeRD200 final : public PollingComponent {
|
||||
protected:
|
||||
bool resolve_handles_();
|
||||
void read_sensors_(const uint8_t *value, uint16_t value_len);
|
||||
void abort_connection_();
|
||||
|
||||
// Group 1: 8-byte types
|
||||
uint64_t address_;
|
||||
|
||||
// Group 2: pointers
|
||||
// Group 1: pointers first - PollingComponent's size is 4 mod 8, so three
|
||||
// pointers bring the 8-byte address to a naturally aligned offset.
|
||||
ble_device_base::BLEGattConnection *backend_;
|
||||
sensor::Sensor *radon_sensor_{nullptr};
|
||||
sensor::Sensor *radon_long_term_sensor_{nullptr};
|
||||
|
||||
// Group 2: 8-byte types
|
||||
uint64_t address_;
|
||||
|
||||
// Group 3: 2-byte types
|
||||
uint16_t read_handle_{0};
|
||||
uint16_t write_handle_{0};
|
||||
@@ -64,9 +64,6 @@ class RadonEyeRD200 final : public PollingComponent {
|
||||
|
||||
// Group 4: 1-byte types
|
||||
uint8_t write_command_{0};
|
||||
// A connect was accepted and the link is not torn down yet; cleared by
|
||||
// on_connection_state(false).
|
||||
bool busy_{false};
|
||||
};
|
||||
|
||||
} // namespace esphome::radon_eye_rd200
|
||||
|
||||
@@ -177,7 +177,7 @@ def test_rp2_rejects_esp32_only_keys_by_name(
|
||||
bluetooth_proxy.CONFIG_SCHEMA({"connections": [{}]})
|
||||
|
||||
|
||||
def test_bluetooth_connection_auto_load_covers_its_includes() -> None:
|
||||
def test_bluetooth_connection_auto_load_matches_the_platform_stack() -> None:
|
||||
# The backend registers with its platform BLE stack, so that dependency
|
||||
# lives here and consumers (proxy, radon_eye_rd200) stay platform-blind;
|
||||
# the platform-less arm is the union for manifest-resolving tooling.
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# The tracker and the Bluedroid backend are auto-loaded through
|
||||
# bluetooth_connection; no bus package or hub config is needed.
|
||||
packages:
|
||||
ble: !include ../../test_build_components/common/ble/esp32-idf.yaml
|
||||
radon_eye_rd200: !include common.yaml
|
||||
|
||||
Reference in New Issue
Block a user