diff --git a/esphome/components/esp32_ble_server/__init__.py b/esphome/components/esp32_ble_server/__init__.py index f53c9450f4..109b942750 100644 --- a/esphome/components/esp32_ble_server/__init__.py +++ b/esphome/components/esp32_ble_server/__init__.py @@ -12,17 +12,92 @@ DEPENDENCIES = ["esp32"] CONF_MANUFACTURER = "manufacturer" CONF_MANUFACTURER_DATA = "manufacturer_data" +CONF_SERVICES = "services" +CONF_UUID = "uuid" +CONF_ADVERTISE = "advertise" +CONF_NUM_HANDLES = "num_handles" +CONF_CHARACTERISTICS = "characteristics" +CONF_PROPERTIES = "properties" +CONF_VALUE = "value" +CONF_DESCRIPTORS = "descriptors" +CONF_MAX_LENGTH = "max_length" esp32_ble_server_ns = cg.esphome_ns.namespace("esp32_ble_server") +ESPBTUUID_ns = cg.esphome_ns.namespace("esp32_ble").namespace("ESPBTUUID") +BLECharacteristic_ns = esp32_ble_server_ns.namespace("BLECharacteristic") BLEServer = esp32_ble_server_ns.class_( "BLEServer", cg.Component, esp32_ble.GATTsEventHandler, cg.Parented.template(esp32_ble.ESP32BLE), ) -BLEServiceComponent = esp32_ble_server_ns.class_("BLEServiceComponent") +BLEDescriptor = esp32_ble_server_ns.class_("BLEDescriptor") +BLECharacteristic = esp32_ble_server_ns.class_("BLECharacteristic") +BLEService = esp32_ble_server_ns.class_("BLEService") +def validate_uuid(value): + if len(value) != 36: + raise cv.Invalid("UUID must be exactly 36 characters long") + return value + + +# Define a schema for the properties +PROPERTIES_SCHEMA = cv.All( + cv.ensure_list(cv.one_of( + "READ", + "WRITE", + "NOTIFY", + "BROADCAST", + "INDICATE", + "WRITE_NR", + upper=True, + )), + cv.Length(min=1) +) + +UUID_SCHEMA = cv.Any(cv.All(cv.string, validate_uuid), cv.hex_uint32_t) + +CHARACTERISTIC_VALUE_SCHEMA = cv.Any( + cv.All(cv.ensure_list(cv.hex_uint8_t), cv.Length(min=1)), + cv.string, + cv.hex_uint8_t, + cv.hex_uint16_t, + cv.hex_uint32_t, + cv.int_, + cv.float_, + cv.boolean, +) + +SERVICE_CHARACTERISTIC_DESCRIPTOR_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(BLEDescriptor), + cv.Required(CONF_UUID): UUID_SCHEMA, + cv.Optional(CONF_MAX_LENGTH, default=0): cv.int_, + cv.Required(CONF_VALUE): CHARACTERISTIC_VALUE_SCHEMA, + } +) + +SERVICE_CHARACTERISTIC_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(BLECharacteristic), + cv.Required(CONF_UUID): UUID_SCHEMA, + cv.Required(CONF_PROPERTIES): PROPERTIES_SCHEMA, + cv.Optional(CONF_VALUE): CHARACTERISTIC_VALUE_SCHEMA, + cv.Optional(CONF_DESCRIPTORS, default=[]): cv.ensure_list(SERVICE_CHARACTERISTIC_DESCRIPTOR_SCHEMA), + } +) + +SERVICE_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(BLEService), + cv.Required(CONF_UUID): UUID_SCHEMA, + cv.Optional(CONF_ADVERTISE, default=False): cv.boolean, + cv.Optional(CONF_NUM_HANDLES, default=0): cv.int_, + cv.Optional(CONF_CHARACTERISTICS, default=[]): cv.ensure_list(SERVICE_CHARACTERISTIC_SCHEMA), + } +) + CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(BLEServer), @@ -30,10 +105,48 @@ CONFIG_SCHEMA = cv.Schema( cv.Optional(CONF_MANUFACTURER, default="ESPHome"): cv.string, cv.Optional(CONF_MANUFACTURER_DATA): cv.Schema([cv.hex_uint8_t]), cv.Optional(CONF_MODEL): cv.string, + cv.Optional(CONF_SERVICES, default=[]): cv.ensure_list(SERVICE_SCHEMA), } ).extend(cv.COMPONENT_SCHEMA) +def parse_properties(properties): + result = 0 + for prop in properties: + if prop == "READ": + result = result | BLECharacteristic_ns.PROPERTY_READ + elif prop == "WRITE": + result = result | BLECharacteristic_ns.PROPERTY_WRITE + elif prop == "NOTIFY": + result = result | BLECharacteristic_ns.PROPERTY_NOTIFY + elif prop == "BROADCAST": + result = result | BLECharacteristic_ns.PROPERTY_BROADCAST + elif prop == "INDICATE": + result = result | BLECharacteristic_ns.PROPERTY_INDICATE + elif prop == "WRITE_NR": + result = result | BLECharacteristic_ns.PROPERTY_WRITE_NR + return result + +def parse_uuid(uuid): + # If the UUID is a string, use from_raw + if isinstance(uuid, str): + return ESPBTUUID_ns.from_raw(uuid) + # Otherwise, use from_uint32 + return ESPBTUUID_ns.from_uint32(uuid) + +def parse_value(value): + if isinstance(value, list): + return cg.std_vector.template(cg.uint8)(value) + return value + +def calculate_num_handles(service_config): + total = 1 + for characteristic in service_config[CONF_CHARACTERISTICS]: + total += 2 # One for the characteristic itself and one for the value + for _ in characteristic[CONF_DESCRIPTORS]: + total += 1 + return total + async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) @@ -43,13 +156,36 @@ async def to_code(config): cg.add(parent.register_gatts_event_handler(var)) cg.add(parent.register_ble_status_event_handler(var)) cg.add(var.set_parent(parent)) - cg.add(var.set_manufacturer(config[CONF_MANUFACTURER])) if CONF_MANUFACTURER_DATA in config: cg.add(var.set_manufacturer_data(config[CONF_MANUFACTURER_DATA])) if CONF_MODEL in config: cg.add(var.set_model(config[CONF_MODEL])) + for service_config in config[CONF_SERVICES]: + num_handles = service_config[CONF_NUM_HANDLES] + # If num_handles is 0, calculate the optimal number of handles based on the number of characteristics and descriptors + if num_handles == 0: + num_handles = calculate_num_handles(service_config) + service_var = cg.Pvariable(service_config[CONF_ID], var.create_service( + parse_uuid(service_config[CONF_UUID]), + service_config[CONF_ADVERTISE], + num_handles, + )) + for characteristic in service_config[CONF_CHARACTERISTICS]: + char_var = cg.Pvariable(characteristic[CONF_ID], service_var.create_characteristic( + parse_uuid(characteristic[CONF_UUID]), + parse_properties(characteristic[CONF_PROPERTIES]) + )) + if CONF_VALUE in characteristic: + cg.add(char_var.set_value(parse_value(characteristic[CONF_VALUE]))) + for descriptor in characteristic[CONF_DESCRIPTORS]: + max_length = descriptor[CONF_MAX_LENGTH] + # If max_length is 0, calculate the optimal length based on the value + if max_length == 0: + max_length = len(parse_value(descriptor[CONF_VALUE])) + desc_var = cg.new_Pvariable(descriptor[CONF_ID], parse_uuid(descriptor[CONF_UUID]), max_length) + if CONF_VALUE in descriptor: + cg.add(desc_var.set_value(parse_value(descriptor[CONF_VALUE]))) cg.add_define("USE_ESP32_BLE_SERVER") - if CORE.using_esp_idf: add_idf_sdkconfig_option("CONFIG_BT_ENABLED", True) diff --git a/esphome/components/esp32_ble_server/ble_descriptor.cpp b/esphome/components/esp32_ble_server/ble_descriptor.cpp index bfb6224335..2240871689 100644 --- a/esphome/components/esp32_ble_server/ble_descriptor.cpp +++ b/esphome/components/esp32_ble_server/ble_descriptor.cpp @@ -38,7 +38,52 @@ void BLEDescriptor::do_create(BLECharacteristic *characteristic) { this->state_ = CREATING; } -void BLEDescriptor::set_value(const std::string &value) { this->set_value((uint8_t *) value.data(), value.length()); } +void BLEDescriptor::set_value(std::vector value) { + this->set_value(value.data(), value.size()); +} +void BLEDescriptor::set_value(const std::string &value) { + this->set_value((uint8_t *) value.data(), value.length()); +} +void BLEDescriptor::set_value(uint8_t &data) { + uint8_t temp[1]; + temp[0] = data; + this->set_value(temp, 1); +} +void BLEDescriptor::set_value(uint16_t &data) { + uint8_t temp[2]; + temp[0] = data; + temp[1] = data >> 8; + this->set_value(temp, 2); +} +void BLEDescriptor::set_value(uint32_t &data) { + uint8_t temp[4]; + temp[0] = data; + temp[1] = data >> 8; + temp[2] = data >> 16; + temp[3] = data >> 24; + this->set_value(temp, 4); +} +void BLEDescriptor::set_value(int &data) { + uint8_t temp[4]; + temp[0] = data; + temp[1] = data >> 8; + temp[2] = data >> 16; + temp[3] = data >> 24; + this->set_value(temp, 4); +} +void BLEDescriptor::set_value(float &data) { + float temp = data; + this->set_value((uint8_t *) &temp, 4); +} +void BLEDescriptor::set_value(double &data) { + double temp = data; + this->set_value((uint8_t *) &temp, 8); +} +void BLEDescriptor::set_value(bool &data) { + uint8_t temp[1]; + temp[0] = data; + this->set_value(temp, 1); +} void BLEDescriptor::set_value(const uint8_t *data, size_t length) { if (length > this->value_.attr_max_len) { ESP_LOGE(TAG, "Size %d too large, must be no bigger than %d", length, this->value_.attr_max_len); diff --git a/esphome/components/esp32_ble_server/ble_descriptor.h b/esphome/components/esp32_ble_server/ble_descriptor.h index 4b8fb345c3..d60721e3cf 100644 --- a/esphome/components/esp32_ble_server/ble_descriptor.h +++ b/esphome/components/esp32_ble_server/ble_descriptor.h @@ -20,8 +20,16 @@ class BLEDescriptor { virtual ~BLEDescriptor(); void do_create(BLECharacteristic *characteristic); - void set_value(const std::string &value); void set_value(const uint8_t *data, size_t length); + void set_value(const std::string &value); + void set_value(std::vector value); + void set_value(uint8_t &data); + void set_value(uint16_t &data); + void set_value(uint32_t &data); + void set_value(int &data); + void set_value(float &data); + void set_value(double &data); + void set_value(bool &data); void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param); diff --git a/esphome/components/esp32_ble_server/ble_server.cpp b/esphome/components/esp32_ble_server/ble_server.cpp index 338413f64e..72edaf1026 100644 --- a/esphome/components/esp32_ble_server/ble_server.cpp +++ b/esphome/components/esp32_ble_server/ble_server.cpp @@ -58,9 +58,7 @@ void BLEServer::loop() { pair.second->do_create(this); } if (this->device_information_service_ == nullptr) { - this->create_service(ESPBTUUID::from_uint16(DEVICE_INFORMATION_SERVICE_UUID)); - this->device_information_service_ = - this->get_service(ESPBTUUID::from_uint16(DEVICE_INFORMATION_SERVICE_UUID)); + this->device_information_service_ = this->create_service(ESPBTUUID::from_uint16(DEVICE_INFORMATION_SERVICE_UUID), false, 7); this->create_device_characteristics_(); } this->state_ = STARTING_SERVICE; @@ -115,39 +113,51 @@ bool BLEServer::create_device_characteristics_() { return true; } -void BLEServer::create_service(ESPBTUUID uuid, bool advertise, uint16_t num_handles, uint8_t inst_id) { +BLEService *BLEServer::create_service(ESPBTUUID uuid, bool advertise, uint16_t num_handles) { ESP_LOGV(TAG, "Creating BLE service - %s", uuid.to_string().c_str()); - // If the service already exists, do nothing - BLEService *service = this->get_service(uuid); - if (service != nullptr) { - ESP_LOGW(TAG, "BLE service %s already exists", uuid.to_string().c_str()); - return; + // 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; + } } - service = new BLEService(uuid, num_handles, inst_id, advertise); // NOLINT(cppcoreguidelines-owning-memory) - this->services_.emplace(uuid.to_string(), service); - service->do_create(this); + if (inst_id == 0xFF) { + ESP_LOGW(TAG, "Could not create BLE service %s, too many instances", uuid.to_string().c_str()); + return nullptr; + } + BLEService *service = new BLEService(uuid, num_handles, inst_id, advertise); // NOLINT(cppcoreguidelines-owning-memory) + this->services_.emplace(BLEServer::get_service_key(uuid, inst_id), service); + if (this->is_running()) { + service->do_create(this); + } + return service; } -void BLEServer::remove_service(ESPBTUUID uuid) { - ESP_LOGV(TAG, "Removing BLE service - %s", uuid.to_string().c_str()); - BLEService *service = this->get_service(uuid); +void BLEServer::remove_service(ESPBTUUID uuid, uint8_t inst_id) { + ESP_LOGV(TAG, "Removing BLE service - %s %d", uuid.to_string().c_str(), inst_id); + BLEService *service = this->get_service(uuid, inst_id); if (service == nullptr) { - ESP_LOGW(TAG, "BLE service %s not found", uuid.to_string().c_str()); + ESP_LOGW(TAG, "BLE service %s %d does not exist", uuid.to_string().c_str(), inst_id); return; } service->do_delete(); delete service; // NOLINT(cppcoreguidelines-owning-memory) - this->services_.erase(uuid.to_string()); + this->services_.erase(BLEServer::get_service_key(uuid, inst_id)); } -BLEService *BLEServer::get_service(ESPBTUUID uuid) { +BLEService *BLEServer::get_service(ESPBTUUID uuid, uint8_t inst_id) { BLEService *service = nullptr; - if (this->services_.count(uuid.to_string()) > 0) { - service = this->services_.at(uuid.to_string()); + if (this->services_.count(BLEServer::get_service_key(uuid, inst_id)) > 0) { + service = this->services_.at(BLEServer::get_service_key(uuid, inst_id)); } return service; } +std::string BLEServer::get_service_key(ESPBTUUID uuid, uint8_t inst_id) { + return uuid.to_string() + std::to_string(inst_id); +} + void BLEServer::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param) { switch (event) { diff --git a/esphome/components/esp32_ble_server/ble_server.h b/esphome/components/esp32_ble_server/ble_server.h index e379e67296..b4273f6acb 100644 --- a/esphome/components/esp32_ble_server/ble_server.h +++ b/esphome/components/esp32_ble_server/ble_server.h @@ -51,9 +51,9 @@ class BLEServer : public Component, public GATTsEventHandler, public BLEStatusEv this->restart_advertising_(); } - void create_service(ESPBTUUID uuid, bool advertise = false, uint16_t num_handles = 15, uint8_t inst_id = 0); - void remove_service(ESPBTUUID uuid); - BLEService *get_service(ESPBTUUID uuid); + BLEService *create_service(ESPBTUUID uuid, bool advertise = false, uint16_t num_handles = 15); + void remove_service(ESPBTUUID uuid, uint8_t inst_id = 0); + BLEService *get_service(ESPBTUUID uuid, uint8_t inst_id = 0); esp_gatt_if_t get_gatts_if() { return this->gatts_if_; } uint32_t get_connected_client_count() { return this->connected_clients_; } @@ -67,6 +67,7 @@ class BLEServer : public Component, public GATTsEventHandler, public BLEStatusEv void register_service_component(BLEServiceComponent *component) { this->service_components_.push_back(component); } protected: + static std::string get_service_key(ESPBTUUID uuid, uint8_t inst_id); bool create_device_characteristics_(); void restart_advertising_(); diff --git a/esphome/components/esp32_ble_server/ble_service.h b/esphome/components/esp32_ble_server/ble_service.h index 5e5883b6bf..74aacc2889 100644 --- a/esphome/components/esp32_ble_server/ble_service.h +++ b/esphome/components/esp32_ble_server/ble_service.h @@ -32,6 +32,7 @@ class BLEService { BLECharacteristic *create_characteristic(ESPBTUUID uuid, esp_gatt_char_prop_t properties); ESPBTUUID get_uuid() { return this->uuid_; } + uint8_t get_inst_id() { return this->inst_id_; } BLECharacteristic *get_last_created_characteristic() { return this->last_created_characteristic_; } uint16_t get_handle() { return this->handle_; } diff --git a/esphome/components/esp32_improv/__init__.py b/esphome/components/esp32_improv/__init__.py index 49d95d89e5..550c6df570 100644 --- a/esphome/components/esp32_improv/__init__.py +++ b/esphome/components/esp32_improv/__init__.py @@ -17,8 +17,9 @@ CONF_STATUS_INDICATOR = "status_indicator" CONF_WIFI_TIMEOUT = "wifi_timeout" esp32_improv_ns = cg.esphome_ns.namespace("esp32_improv") +BLEServiceComponent = cg.esphome_ns.namespace("esp32_ble_server").class_("BLEServiceComponent") ESP32ImprovComponent = esp32_improv_ns.class_( - "ESP32ImprovComponent", cg.Component, esp32_ble_server.BLEServiceComponent + "ESP32ImprovComponent", cg.Component, BLEServiceComponent ) diff --git a/esphome/components/esp32_improv/esp32_improv_component.cpp b/esphome/components/esp32_improv/esp32_improv_component.cpp index d90eaac3b6..e0f60329cf 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.cpp +++ b/esphome/components/esp32_improv/esp32_improv_component.cpp @@ -75,8 +75,7 @@ void ESP32ImprovComponent::loop() { if (this->service_ == nullptr) { // Setup the service ESP_LOGD(TAG, "Creating Improv service"); - global_ble_server->create_service(ESPBTUUID::from_raw(improv::SERVICE_UUID), true); - this->service_ = global_ble_server->get_service(ESPBTUUID::from_raw(improv::SERVICE_UUID)); + this->service_ = global_ble_server->create_service(ESPBTUUID::from_raw(improv::SERVICE_UUID), true); this->setup_characteristics(); }