Compare commits

...
Author SHA1 Message Date
J. Nick Koston 3dc121a199 Merge remote-tracking branch 'origin/ble-server-caller-owned' into HEAD 2026-11-14 08:40:02 -06:00
J. Nick Koston f245927b49 Merge remote-tracking branch 'origin/dev' into HEAD 2026-11-14 08:36:58 -06:00
J. Nick Koston 058a5b0358 Merge remote-tracking branch 'origin/ble-server-caller-owned' into ble-server-yaml-static 2026-11-12 00:30:50 -06:00
J. Nick Koston deb8ff7747 Delete only unclaimed services created through this server's GATT interface 2026-11-12 00:30:38 -06:00
J. Nick Koston 1cc5593038 Merge remote-tracking branch 'origin/ble-server-caller-owned' into ble-server-yaml-static 2026-11-12 00:15:10 -06:00
J. Nick Koston be7ebd4502 Claim ADD_CHAR_EVT only while creating, in the characteristic's own service 2026-11-12 00:14:57 -06:00
J. Nick Koston 2b6abf538b Merge remote-tracking branch 'origin/ble-server-caller-owned' into ble-server-yaml-static 2026-11-11 23:51:41 -06:00
J. Nick Koston 8441f30da4 Delete a service whose create arrives after it was removed 2026-11-11 23:51:36 -06:00
J. Nick Koston d5622826f6 Merge remote-tracking branch 'origin/ble-server-caller-owned' into ble-server-yaml-static 2026-11-11 23:43:10 -06:00
J. Nick Koston a1ede4e5c6 Skip the stack calls when a service is removed before it was created 2026-11-11 23:42:59 -06:00
J. Nick Koston 5208ddc5f7 Merge remote-tracking branch 'origin/ble-server-caller-owned' into ble-server-yaml-static 2026-11-11 23:39:40 -06:00
J. Nick Koston 93efaca879 Forget a deleted characteristic's handles until it is created again 2026-11-11 23:38:31 -06:00
J. Nick Koston b7d59ccf7d Merge branch 'dev' into ble-server-caller-owned 2026-11-11 23:24:22 -06:00
J. Nick Koston ae79c13c8a Merge remote-tracking branch 'origin/ble-server-caller-owned' into ble-server-yaml-static 2026-11-11 23:09:35 -06:00
J. Nick Koston f55557f581 Bind each parent only when adding, share one error message and trim comments 2026-11-11 23:09:30 -06:00
J. Nick Koston 6eac98162d Merge remote-tracking branch 'origin/ble-server-caller-owned' into ble-server-yaml-static 2026-11-11 23:06:49 -06:00
J. Nick Koston b78f3df6c1 Refuse a service, characteristic or descriptor that is already registered
Each object is bound to its parent when added and unbound when removed or when the
parent is destroyed, so none can be in two lists, be freed twice or keep a stale parent.
2026-11-11 23:05:09 -06:00
J. Nick Koston ced25b128b Dereference the static service and characteristic with an expression instead of an f-string 2026-11-11 22:26:14 -06:00
J. Nick Koston 9cde6881c9 Merge remote-tracking branch 'origin/ble-server-caller-owned' into ble-server-yaml-static 2026-11-11 22:25:50 -06:00
J. Nick Koston 507f754c78 Use std::erase, guard the descriptor's characteristic and dereference with an expression 2026-11-11 22:25:46 -06:00
J. Nick Koston 3b7e4a9970 Merge remote-tracking branch 'origin/ble-server-caller-owned' into ble-server-yaml-static 2026-11-11 22:23:51 -06:00
J. Nick Koston e465f754c6 Remove every copy of a descriptor and clear only a characteristic's own service 2026-11-11 22:22:06 -06:00
J. Nick Koston 2c67c6726c Merge remote-tracking branch 'origin/ble-server-caller-owned' into ble-server-yaml-static 2026-11-11 21:49:03 -06:00
J. Nick Koston 4e09edb59a Unqueue removed services, free only member descriptors and bind added characteristics 2026-11-11 21:48:28 -06:00
J. Nick Koston 2009d19ad6 Merge remote-tracking branch 'upstream/ble-server-caller-owned' into ble-server-yaml-static
# Conflicts:
#	esphome/components/esp32_ble_server/__init__.py
2026-11-09 21:01:57 -08:00
J. Nick Koston 4e6f994b7c Merge remote-tracking branch 'upstream/dev' into ble-server-caller-owned 2026-11-09 20:59:46 -08:00
J. Nick Koston 61fcee3320 Merge branch 'ble-server-caller-owned' into ble-server-yaml-static 2026-11-09 02:23:25 -08:00
J. Nick Koston 195d16eb27 Document that a caller-owned service is added once 2026-11-09 02:23:20 -08:00
J. Nick Koston c60e5d9c54 Merge branch 'ble-server-caller-owned' into ble-server-yaml-static 2026-11-09 02:18:53 -08:00
J. Nick Koston fab9d004cc Point the caller-owned API comments at component members 2026-11-09 02:18:41 -08:00
J. Nick Koston 215f65fcd5 Merge branch 'ble-server-caller-owned' into ble-server-yaml-static 2026-11-09 02:09:24 -08:00
J. Nick Koston d4ffa31c4a Pick the instance id before allocating so create_service never deletes 2026-11-09 02:09:13 -08:00
J. Nick Koston 0df4082d36 Merge branch 'ble-server-caller-owned' into ble-server-yaml-static
# Conflicts:
#	tests/component_tests/esp32_ble_server/test_esp32_ble_server.py
2026-11-09 01:52:31 -08:00
J. Nick Koston 1296f3bc63 Test caller-owned registration and YAML descriptors added by reference 2026-11-09 01:52:06 -08:00
J. Nick Koston b1af3e65d2 Merge branch 'ble-server-caller-owned' into ble-server-yaml-static 2026-11-09 01:43:26 -08:00
J. Nick Koston e40165dd84 Clear ownership on remove_descriptor and parent pointers of caller-owned children 2026-11-09 01:43:12 -08:00
J. Nick Koston fbbab7178a Test that YAML services and characteristics are static and registered 2026-11-09 01:15:55 -08:00
J. Nick Koston ae81f6bd47 Merge branch 'ble-server-caller-owned' into ble-server-yaml-static 2026-11-09 00:35:40 -08:00
J. Nick Koston 9579244ef3 Shorten the BLEService constructor comment 2026-11-09 00:35:39 -08:00
J. Nick Koston a80c24842f [esp32_ble_server] Keep YAML services and characteristics in static storage 2026-11-09 00:33:54 -08:00
J. Nick Koston 60812d8055 [esp32_ble_server] Accept services, characteristics and descriptors the caller keeps alive 2026-11-09 00:22:41 -08:00
11 changed files with 228 additions and 58 deletions

No files matched your search

+13 -14
View File
@@ -41,7 +41,7 @@ from esphome.const import (
__version__ as ESPHOME_VERSION,
)
from esphome.core import CORE, ID, MACAddress
from esphome.cpp_generator import MockObj, StaticCastExpression
from esphome.cpp_generator import MockObj, StaticCastExpression, UnaryOpExpression
import esphome.final_validate as fv
from esphome.schema_extractors import SCHEMA_EXTRACT
from esphome.types import ConfigType, TemplateArgsType
@@ -704,21 +704,21 @@ async def to_code_descriptor(descriptor_conf, char_conf, char_var):
descriptor_conf[CONF_MAX_LENGTH],
descriptor_permissions(descriptor_conf, char_conf),
)
cg.add(char_var.add_descriptor(desc_var))
# Static storage, so the characteristic must not free it
cg.add(char_var.add_descriptor(UnaryOpExpression("*", desc_var)))
cg.add(desc_var.set_value(value))
if (on_write_conf := descriptor_conf.get(CONF_ON_WRITE)) is not None:
await _build_on_write_automation(desc_var, on_write_conf)
async def to_code_characteristic(service_var, char_conf):
char_var = cg.Pvariable(
char_var = cg.new_Pvariable(
char_conf[CONF_ID],
service_var.create_characteristic(
parse_uuid(char_conf[CONF_UUID]),
parse_properties(char_conf),
characteristic_permissions(char_conf),
),
parse_uuid(char_conf[CONF_UUID]),
parse_properties(char_conf),
characteristic_permissions(char_conf),
)
cg.add(service_var.add_characteristic(UnaryOpExpression("*", char_var)))
if (on_write_conf := char_conf.get(CONF_ON_WRITE)) is not None:
await _build_on_write_automation(char_var, on_write_conf)
if CONF_VALUE in char_conf:
@@ -823,14 +823,13 @@ async def to_code(config):
for service_config in config[CONF_SERVICES]:
# Calculate the optimal number of handles based on the number of characteristics and descriptors
num_handles = calculate_num_handles(service_config)
service_var = cg.Pvariable(
service_var = cg.new_Pvariable(
service_config[CONF_ID],
var.create_service(
parse_uuid(service_config[CONF_UUID]),
service_config[CONF_ADVERTISE],
num_handles,
),
parse_uuid(service_config[CONF_UUID]),
num_handles,
service_config[CONF_ADVERTISE],
)
cg.add(var.add_service(UnaryOpExpression("*", service_var)))
for char_conf in service_config[CONF_CHARACTERISTICS]:
await to_code_characteristic(service_var, char_conf)
if uuid_is(service_config[CONF_UUID], DEVICE_INFORMATION_SERVICE_UUID):
@@ -2,6 +2,8 @@
#include "ble_server.h"
#include "ble_service.h"
#include <vector>
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
@@ -13,7 +15,12 @@ ESPHOME_LOG_TAG(TAG, "esp32_ble_server.characteristic");
BLECharacteristic::~BLECharacteristic() {
for (auto *descriptor : this->descriptors_) {
delete descriptor; // NOLINT(cppcoreguidelines-owning-memory)
if (descriptor->owned_) {
delete descriptor; // NOLINT(cppcoreguidelines-owning-memory)
} else if (descriptor->characteristic_ == this) {
// A caller-owned descriptor outlives this characteristic, and may already belong to another one
descriptor->characteristic_ = nullptr;
}
}
}
@@ -79,10 +86,27 @@ void BLECharacteristic::notify() {
}
}
void BLECharacteristic::add_descriptor(BLEDescriptor *descriptor) {
void BLECharacteristic::do_delete() {
this->clients_to_notify_.clear();
// The stack may reuse the handles, so a deleted characteristic must not match events until it is created again
this->handle_ = 0xFFFF;
this->state_ = INIT;
for (auto *descriptor : this->descriptors_) {
descriptor->handle_ = 0xFFFF;
descriptor->state_ = BLEDescriptor::INIT;
}
}
bool BLECharacteristic::add_descriptor(BLEDescriptor &descriptor) {
// Bound until removed or the parent is destroyed
if (descriptor.characteristic_ != nullptr) {
ESP_LOGE(TAG, "Already added");
return false;
}
descriptor.characteristic_ = this;
// If the descriptor is the CCCD descriptor, listen to its write event to know if the client wants to be notified
if (descriptor->get_uuid() == ESPBTUUID::from_uint16(ESP_GATT_UUID_CHAR_CLIENT_CONFIG)) {
descriptor->on_write([this](std::span<const uint8_t> value, uint16_t conn_id) {
if (descriptor.get_uuid() == ESPBTUUID::from_uint16(ESP_GATT_UUID_CHAR_CLIENT_CONFIG)) {
descriptor.on_write([this](std::span<const uint8_t> value, uint16_t conn_id) {
if (value.size() != 2)
return;
uint16_t cccd = encode_uint16(value[1], value[0]);
@@ -96,16 +120,18 @@ void BLECharacteristic::add_descriptor(BLEDescriptor *descriptor) {
}
});
}
this->descriptors_.push_back(descriptor);
this->descriptors_.push_back(&descriptor);
return true;
}
void BLECharacteristic::remove_descriptor(BLEDescriptor *descriptor) {
this->descriptors_.erase(std::remove(this->descriptors_.begin(), this->descriptors_.end(), descriptor),
this->descriptors_.end());
if (std::erase(this->descriptors_, descriptor) == 0)
return;
descriptor->owned_ = false; // the caller owns it again
descriptor->characteristic_ = nullptr;
}
void BLECharacteristic::do_create(BLEService *service) {
this->service_ = service;
void BLECharacteristic::do_create() {
esp_attr_control_t control;
control.auto_rsp = ESP_GATT_RSP_BY_APP;
@@ -116,8 +142,9 @@ void BLECharacteristic::do_create(BLEService *service) {
#endif
esp_bt_uuid_t uuid = this->uuid_.get_uuid();
esp_err_t err = esp_ble_gatts_add_char(service->get_handle(), &uuid, static_cast<esp_gatt_perm_t>(this->permissions_),
this->properties_, nullptr, &control);
esp_err_t err =
esp_ble_gatts_add_char(this->service_->get_handle(), &uuid, static_cast<esp_gatt_perm_t>(this->permissions_),
this->properties_, nullptr, &control);
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_ble_gatts_add_char failed: %d", err);
@@ -188,11 +215,15 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt
break;
}
case ESP_GATTS_ADD_CHAR_EVT: {
if (this->uuid_ == ESPBTUUID::from_uuid(param->add_char.char_uuid)) {
// Only while creating, in its own service: a late event for a removed service or a same-UUID
// characteristic elsewhere must not set the handle or start the descriptors
if (this->state_ == CREATING && this->service_ != nullptr &&
this->service_->get_handle() == param->add_char.service_handle &&
this->uuid_ == ESPBTUUID::from_uuid(param->add_char.char_uuid)) {
this->handle_ = param->add_char.attr_handle;
for (auto *descriptor : this->descriptors_) {
descriptor->do_create(this);
descriptor->do_create();
}
this->state_ = CREATING_DEPENDENTS;
@@ -25,6 +25,8 @@ using namespace bytebuffer;
class BLEService;
class BLECharacteristic final {
friend class BLEService;
public:
BLECharacteristic(ESPBTUUID uuid, uint32_t properties, esp_gatt_perm_t permissions);
~BLECharacteristic();
@@ -44,11 +46,18 @@ class BLECharacteristic final {
void notify();
void do_create(BLEService *service);
void do_delete() { this->clients_to_notify_.clear(); }
void do_create();
void do_delete();
void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param);
void add_descriptor(BLEDescriptor *descriptor);
/// Takes ownership of a heap-allocated descriptor and frees it with this characteristic.
void add_descriptor(BLEDescriptor *descriptor) {
if (this->add_descriptor(*descriptor))
descriptor->owned_ = true;
}
/// Adds a descriptor the caller keeps alive (such as a component member); it is never freed here.
/// False when it already belongs to a characteristic.
bool add_descriptor(BLEDescriptor &descriptor);
void remove_descriptor(BLEDescriptor *descriptor);
BLEService *get_service() { return this->service_; }
@@ -106,6 +115,8 @@ class BLECharacteristic final {
CREATING_DEPENDENTS,
CREATED,
} state_{INIT};
// Set by BLEService::create_characteristic(), which allocates it; the service frees it
bool owned_{false};
};
} // namespace esphome::esp32_ble_server
@@ -27,8 +27,7 @@ BLEDescriptor::BLEDescriptor(ESPBTUUID uuid, uint16_t max_len, esp_gatt_perm_t p
BLEDescriptor::~BLEDescriptor() { free(this->value_.attr_value); } // NOLINT
void BLEDescriptor::do_create(BLECharacteristic *characteristic) {
this->characteristic_ = characteristic;
void BLEDescriptor::do_create() {
esp_attr_control_t control;
control.auto_rsp = ESP_GATT_AUTO_RSP;
@@ -66,7 +65,8 @@ void BLEDescriptor::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_
esp_ble_gatts_cb_param_t *param) {
switch (event) {
case ESP_GATTS_ADD_CHAR_DESCR_EVT: {
if (this->characteristic_ != nullptr && this->uuid_ == ESPBTUUID::from_uuid(param->add_char_descr.descr_uuid) &&
// Bound by add_descriptor(), so only a descriptor do_create() is creating may claim the event
if (this->state_ == CREATING && this->uuid_ == ESPBTUUID::from_uuid(param->add_char_descr.descr_uuid) &&
this->characteristic_->get_service()->get_handle() == param->add_char_descr.service_handle &&
this->characteristic_ == this->characteristic_->get_service()->get_last_created_characteristic()) {
this->handle_ = param->add_char_descr.attr_handle;
@@ -20,11 +20,13 @@ class BLECharacteristic;
// Base class for BLE descriptors
class BLEDescriptor {
friend class BLECharacteristic;
public:
BLEDescriptor(ESPBTUUID uuid, uint16_t max_len = 100, bool read = true, bool write = true);
BLEDescriptor(ESPBTUUID uuid, uint16_t max_len, esp_gatt_perm_t permissions);
virtual ~BLEDescriptor();
void do_create(BLECharacteristic *characteristic);
void do_create();
ESPBTUUID get_uuid() const { return this->uuid_; }
void set_value(const uint8_t *data, size_t length);
@@ -60,6 +62,8 @@ class BLEDescriptor {
CREATING,
CREATED,
} state_{INIT};
// Set when the characteristic took ownership through add_descriptor(BLEDescriptor *) and frees it
bool owned_{false};
};
} // namespace esphome::esp32_ble_server
@@ -1,5 +1,7 @@
#include "ble_server.h"
#include <vector>
#include "esphome/components/ble_device_base/ble_device.h"
#include "esphome/components/esp32_ble/ble.h"
#include "esphome/core/log.h"
@@ -123,31 +125,52 @@ void BLEServer::release_advertising_() {
}
BLEService *BLEServer::create_service(ESPBTUUID uuid, bool advertise, uint16_t num_handles) {
// Checked before allocating, so a failure never deletes a service and pulls in its destructor
const uint8_t inst_id = this->free_inst_id_(uuid);
if (inst_id == INVALID_INST_ID)
return nullptr;
auto *service = new BLEService(uuid, num_handles, advertise); // NOLINT(cppcoreguidelines-owning-memory)
this->register_service_(*service, inst_id, true);
return service;
}
bool BLEServer::add_service(BLEService &service) {
// Adding it twice would free or start it twice
for (auto &entry : this->services_) {
if (entry.service == &service) {
ESP_LOGE(TAG, "Already added");
return false;
}
}
const uint8_t inst_id = this->free_inst_id_(service.get_uuid());
if (inst_id == INVALID_INST_ID)
return false;
this->register_service_(service, inst_id, false);
return true;
}
uint8_t BLEServer::free_inst_id_(const ESPBTUUID &uuid) {
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
char uuid_buf[esp32_ble::UUID_STR_LEN];
uuid.to_str(uuid_buf);
ESP_LOGV(TAG, "Creating BLE service - %s", uuid_buf);
ESP_LOGV(TAG, "Adding BLE service - %s", uuid_buf);
#endif
// Calculate the inst_id for the service
uint8_t inst_id = 0;
for (; inst_id < 0xFF; inst_id++) {
if (this->get_service(uuid, inst_id) == nullptr) {
break;
}
for (uint8_t inst_id = 0; inst_id < INVALID_INST_ID; inst_id++) {
if (this->get_service(uuid, inst_id) == nullptr)
return inst_id;
}
if (inst_id == 0xFF) {
char warn_uuid_buf[esp32_ble::UUID_STR_LEN];
uuid.to_str(warn_uuid_buf);
ESP_LOGW(TAG, "Could not create BLE service %s, too many instances", warn_uuid_buf);
return nullptr;
}
BLEService *service = // NOLINT(cppcoreguidelines-owning-memory)
new BLEService(uuid, num_handles, inst_id, advertise);
this->services_.push_back({uuid, inst_id, service});
char warn_uuid_buf[esp32_ble::UUID_STR_LEN];
uuid.to_str(warn_uuid_buf);
ESP_LOGW(TAG, "Could not create BLE service %s, too many instances", warn_uuid_buf);
return INVALID_INST_ID;
}
void BLEServer::register_service_(BLEService &service, uint8_t inst_id, bool owned) {
service.inst_id_ = inst_id;
this->services_.push_back({service.get_uuid(), inst_id, owned, &service});
if (this->parent_->is_active() && this->registered_) {
service->do_create(this);
service.do_create(this);
}
return service;
}
void BLEServer::remove_service(ESPBTUUID uuid, uint8_t inst_id) {
@@ -159,7 +182,10 @@ void BLEServer::remove_service(ESPBTUUID uuid, uint8_t inst_id) {
for (auto it = this->services_.begin(); it != this->services_.end(); ++it) {
if (it->uuid == uuid && it->inst_id == inst_id) {
it->service->do_delete();
delete it->service; // NOLINT(cppcoreguidelines-owning-memory)
// A service still waiting to start must not stay queued once it is unregistered or freed
std::erase(this->services_to_start_, it->service);
if (it->owned)
delete it->service; // NOLINT(cppcoreguidelines-owning-memory)
this->services_.erase(it);
return;
}
@@ -218,6 +244,15 @@ void BLEServer::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t ga
for (auto &entry : this->services_) {
entry.service->gatts_event_handler(event, gatts_if, param);
}
// A service this server asked for but removed while the stack was still creating it is deleted once its handle
// arrives; services of other GATT applications are left alone
if (event == ESP_GATTS_CREATE_EVT && gatts_if == this->gatts_if_ && param->create.status == ESP_GATT_OK) {
for (auto &entry : this->services_) {
if (entry.service->get_handle() == param->create.service_handle)
return;
}
esp_ble_gatts_delete_service(param->create.service_handle);
}
}
int8_t BLEServer::find_client_index_(uint16_t conn_id) const {
@@ -49,6 +49,10 @@ class BLEServer final : public Component, public Parented<ESP32BLE> {
uint8_t get_max_clients() const { return this->max_clients_; }
BLEService *create_service(ESPBTUUID uuid, bool advertise = false, uint16_t num_handles = 15);
/// Adds a service the caller keeps alive (such as a component member); remove_service() never frees it.
/// Do not add it again after remove_service(). Returns false when it is already added or the UUID already has
/// 255 instances.
bool add_service(BLEService &service);
void remove_service(ESPBTUUID uuid, uint8_t inst_id = 0);
BLEService *get_service(ESPBTUUID uuid, uint8_t inst_id = 0);
void enqueue_start_service(BLEService *service) { this->services_to_start_.push_back(service); }
@@ -114,9 +118,13 @@ class BLEServer final : public Component, public Parented<ESP32BLE> {
struct ServiceEntry {
ESPBTUUID uuid;
uint8_t inst_id;
bool owned; // allocated by create_service(), freed by remove_service()
BLEService *service;
};
static constexpr uint8_t INVALID_INST_ID = 0xFF;
uint8_t free_inst_id_(const ESPBTUUID &uuid);
void register_service_(BLEService &service, uint8_t inst_id, bool owned);
void restart_advertising_();
void request_advertising_();
void release_advertising_();
@@ -12,8 +12,25 @@ BLEService::BLEService(ESPBTUUID uuid, uint16_t num_handles, uint8_t inst_id, bo
: uuid_(uuid), num_handles_(num_handles), inst_id_(inst_id), advertise_(advertise) {}
BLEService::~BLEService() {
for (auto &chr : this->characteristics_)
delete chr; // NOLINT(cppcoreguidelines-owning-memory)
for (auto *chr : this->characteristics_) {
if (chr->owned_) {
delete chr; // NOLINT(cppcoreguidelines-owning-memory)
} else if (chr->service_ == this) {
// A caller-owned characteristic outlives this service, and may already belong to another one
chr->service_ = nullptr;
}
}
}
bool BLEService::add_characteristic(BLECharacteristic &characteristic) {
// Bound until the parent is destroyed
if (characteristic.service_ != nullptr) {
ESP_LOGE(TAG, "Already added");
return false;
}
characteristic.service_ = this;
this->characteristics_.push_back(&characteristic);
return true;
}
BLECharacteristic *BLEService::get_characteristic(ESPBTUUID uuid) {
@@ -39,7 +56,8 @@ BLECharacteristic *BLEService::create_characteristic(ESPBTUUID uuid, esp_gatt_ch
esp_gatt_perm_t permissions) {
// NOLINTNEXTLINE(cppcoreguidelines-owning-memory)
BLECharacteristic *characteristic = new BLECharacteristic(uuid, properties, permissions);
this->characteristics_.push_back(characteristic);
characteristic->owned_ = true;
this->add_characteristic(*characteristic);
return characteristic;
}
@@ -69,8 +87,14 @@ void BLEService::do_delete() {
// Call all characteristics to delete
for (auto *characteristic : this->characteristics_)
characteristic->do_delete();
// Not created on the stack yet; if a create is in flight, the server deletes it when its handle arrives
if (this->handle_ == 0xFFFF) {
this->state_ = DELETED;
return;
}
this->stop_();
esp_err_t err = esp_ble_gatts_delete_service(this->handle_);
this->handle_ = 0xFFFF; // a later add creates it again with a new handle
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_ble_gatts_delete_service failed: %d", err);
return;
@@ -87,7 +111,7 @@ bool BLEService::do_create_characteristics_() {
auto *characteristic = this->characteristics_[this->created_characteristic_count_++];
this->last_created_characteristic_ = characteristic;
characteristic->do_create(this);
characteristic->do_create();
return true;
}
@@ -140,7 +164,7 @@ void BLEService::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t g
esp_ble_gatts_cb_param_t *param) {
switch (event) {
case ESP_GATTS_CREATE_EVT: {
if (this->uuid_ == ESPBTUUID::from_uuid(param->create.service_id.id.uuid) &&
if (this->state_ == CREATING && this->uuid_ == ESPBTUUID::from_uuid(param->create.service_id.id.uuid) &&
this->inst_id_ == param->create.service_id.id.inst_id) {
this->handle_ = param->create.service_handle;
this->state_ = CREATED;
@@ -20,8 +20,12 @@ class BLEServer;
using namespace esp32_ble;
class BLEService final {
friend class BLEServer;
public:
BLEService(ESPBTUUID uuid, uint16_t num_handles, uint8_t inst_id, bool advertise);
/// The server picks the instance id when the service is added.
BLEService(ESPBTUUID uuid, uint16_t num_handles, bool advertise) : BLEService(uuid, num_handles, 0, advertise) {}
~BLEService();
BLECharacteristic *get_characteristic(ESPBTUUID uuid);
BLECharacteristic *get_characteristic(uint16_t uuid);
@@ -32,6 +36,8 @@ class BLEService final {
esp_gatt_perm_t permissions = ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE);
BLECharacteristic *create_characteristic(ESPBTUUID uuid, esp_gatt_char_prop_t properties,
esp_gatt_perm_t permissions = ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE);
/// Adds a characteristic the caller keeps alive; it is never freed here. False when it already belongs to a service.
bool add_characteristic(BLECharacteristic &characteristic);
ESPBTUUID get_uuid() { return this->uuid_; }
uint8_t get_inst_id() { return this->inst_id_; }
@@ -4,6 +4,7 @@ from __future__ import annotations
from collections.abc import Callable
from pathlib import Path
import re
import pytest
@@ -147,6 +148,42 @@ def test_set_value_constants_share_a_flash_table(
assert "ByteBuffer::wrap" not in main_cpp
def test_yaml_services_and_characteristics_are_static(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
main_cpp = generate_main(component_config_path("own_service.yaml"))
# Placement new into static storage, with handle count then advertise
service = re.search(
r"new\((\w+)\) esp32_ble_server::BLEService\("
r'esp32_ble::ESPBTUUID::from_raw\("2A24B789-7AAB-4535-AF3E-EE76A35CC12D"\), \d+, (?:true|false)\);',
main_cpp,
)
assert service is not None
assert f"ble_server->add_service((*{service[1]}));" in main_cpp
characteristic = re.search(
r"new\((\w+)\) esp32_ble_server::BLECharacteristic\("
r'esp32_ble::ESPBTUUID::from_raw\("CAD48E28-7FBE-41CF-BAE9-D77A6C233423"\)',
main_cpp,
)
assert characteristic is not None
assert f"{service[1]}->add_characteristic((*{characteristic[1]}));" in main_cpp
assert "create_service(" not in main_cpp
assert "create_characteristic(" not in main_cpp
def test_yaml_descriptors_are_added_by_reference(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
main_cpp = generate_main(component_config_path("set_value.yaml"))
# Descriptors live in static storage, so the characteristic must not take ownership
assert "->add_descriptor((*test_descriptor));" in main_cpp
assert "->add_descriptor(test_descriptor);" not in main_cpp
def test_float32_rejects_out_of_range() -> None:
for value in (1e40, -1e40):
with pytest.raises(cv.Invalid, match="out of range for a float"):
@@ -1,3 +1,18 @@
esphome:
on_boot:
- lambda: |-
// Objects the caller keeps alive, registered without heap allocation
static esp32_ble_server::BLEService battery(esp32_ble::ESPBTUUID::from_uint16(0x180F), 6, false);
static esp32_ble_server::BLECharacteristic level(
esp32_ble::ESPBTUUID::from_uint16(0x2A19),
esp32_ble_server::BLECharacteristic::PROPERTY_READ | esp32_ble_server::BLECharacteristic::PROPERTY_NOTIFY,
ESP_GATT_PERM_READ);
static esp32_ble_server::BLE2902 level_cccd;
level.add_descriptor(level_cccd);
battery.add_characteristic(level);
if (id(ble_server).add_service(battery))
id(ble_server).enqueue_start_service(&battery);
esp32_ble_server:
id: ble_server
manufacturer_data: [0x72, 0x4, 0x00, 0x23]