diff --git a/CODEOWNERS b/CODEOWNERS index aa24b6cb82..cdf4ab7a99 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -148,6 +148,7 @@ esphome/components/esp32_rmt_led_strip/* @jesserockz esphome/components/esp8266/* @esphome/core esphome/components/ethernet_info/* @gtjadsonsantos esphome/components/event/* @nohat +esphome/components/event_emitter/* @Rapsssito esphome/components/exposure_notifications/* @OttoWinter esphome/components/ezo/* @ssieb esphome/components/ezo_pmp/* @carlos-sarmiento diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 684540ffa6..534098e5fd 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -1381,6 +1381,7 @@ message BluetoothConnectionsFreeResponse { uint32 free = 1; uint32 limit = 2; + repeated uint64 allocated = 3; } message BluetoothGATTErrorResponse { diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 8df152881c..41016e510f 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -6430,6 +6430,10 @@ bool BluetoothConnectionsFreeResponse::decode_varint(uint32_t field_id, ProtoVar this->limit = value.as_uint32(); return true; } + case 3: { + this->allocated.push_back(value.as_uint64()); + return true; + } default: return false; } @@ -6437,6 +6441,9 @@ bool BluetoothConnectionsFreeResponse::decode_varint(uint32_t field_id, ProtoVar void BluetoothConnectionsFreeResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(1, this->free); buffer.encode_uint32(2, this->limit); + for (auto &it : this->allocated) { + buffer.encode_uint64(3, it, true); + } } #ifdef HAS_PROTO_MESSAGE_DUMP void BluetoothConnectionsFreeResponse::dump_to(std::string &out) const { @@ -6451,6 +6458,13 @@ void BluetoothConnectionsFreeResponse::dump_to(std::string &out) const { sprintf(buffer, "%" PRIu32, this->limit); out.append(buffer); out.append("\n"); + + for (const auto &it : this->allocated) { + out.append(" allocated: "); + sprintf(buffer, "%llu", it); + out.append(buffer); + out.append("\n"); + } out.append("}"); } #endif diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 063c217bf7..a3fccbc641 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -1624,6 +1624,7 @@ class BluetoothConnectionsFreeResponse : public ProtoMessage { public: uint32_t free{0}; uint32_t limit{0}; + std::vector allocated{}; void encode(ProtoWriteBuffer buffer) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; diff --git a/esphome/components/ble_client/sensor/__init__.py b/esphome/components/ble_client/sensor/__init__.py index 0c48902a90..960410a5cc 100644 --- a/esphome/components/ble_client/sensor/__init__.py +++ b/esphome/components/ble_client/sensor/__init__.py @@ -11,6 +11,7 @@ from esphome.const import ( DEVICE_CLASS_SIGNAL_STRENGTH, STATE_CLASS_MEASUREMENT, UNIT_DECIBEL_MILLIWATT, + CONF_NOTIFY, ) from .. import ble_client_ns @@ -19,7 +20,6 @@ DEPENDENCIES = ["ble_client"] CONF_DESCRIPTOR_UUID = "descriptor_uuid" -CONF_NOTIFY = "notify" CONF_ON_NOTIFY = "on_notify" TYPE_CHARACTERISTIC = "characteristic" TYPE_RSSI = "rssi" diff --git a/esphome/components/ble_client/text_sensor/__init__.py b/esphome/components/ble_client/text_sensor/__init__.py index 479af1a57e..a6672e68f5 100644 --- a/esphome/components/ble_client/text_sensor/__init__.py +++ b/esphome/components/ble_client/text_sensor/__init__.py @@ -6,6 +6,7 @@ from esphome.const import ( CONF_CHARACTERISTIC_UUID, CONF_ID, CONF_SERVICE_UUID, + CONF_NOTIFY, CONF_TRIGGER_ID, ) @@ -15,7 +16,6 @@ DEPENDENCIES = ["ble_client"] CONF_DESCRIPTOR_UUID = "descriptor_uuid" -CONF_NOTIFY = "notify" CONF_ON_NOTIFY = "on_notify" adv_data_t = cg.std_vector.template(cg.uint8) diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp index bd1c8b7ea4..a263aca456 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp @@ -475,6 +475,11 @@ void BluetoothProxy::send_connections_free() { api::BluetoothConnectionsFreeResponse call; call.free = this->get_bluetooth_connections_free(); call.limit = this->get_bluetooth_connections_limit(); + for (auto *connection : this->connections_) { + if (connection->address_ != 0) { + call.allocated.push_back(connection->address_); + } + } this->api_connection_->send_bluetooth_connections_free_response(call); } diff --git a/esphome/components/esp32_ble/__init__.py b/esphome/components/esp32_ble/__init__.py index 08e30c9247..37b4900a03 100644 --- a/esphome/components/esp32_ble/__init__.py +++ b/esphome/components/esp32_ble/__init__.py @@ -1,3 +1,5 @@ +import re + from esphome import automation import esphome.codegen as cg from esphome.components.esp32 import add_idf_sdkconfig_option, const, get_esp32_variant @@ -64,6 +66,43 @@ CONFIG_SCHEMA = cv.Schema( ).extend(cv.COMPONENT_SCHEMA) +bt_uuid16_format = "XXXX" +bt_uuid32_format = "XXXXXXXX" +bt_uuid128_format = "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" + + +def bt_uuid(value): + in_value = cv.string_strict(value) + value = in_value.upper() + + if len(value) == len(bt_uuid16_format): + pattern = re.compile("^[A-F|0-9]{4,}$") + if not pattern.match(value): + raise cv.Invalid( + f"Invalid hexadecimal value for 16 bit UUID format: '{in_value}'" + ) + return value + if len(value) == len(bt_uuid32_format): + pattern = re.compile("^[A-F|0-9]{8,}$") + if not pattern.match(value): + raise cv.Invalid( + f"Invalid hexadecimal value for 32 bit UUID format: '{in_value}'" + ) + return value + if len(value) == len(bt_uuid128_format): + pattern = re.compile( + "^[A-F|0-9]{8,}-[A-F|0-9]{4,}-[A-F|0-9]{4,}-[A-F|0-9]{4,}-[A-F|0-9]{12,}$" + ) + if not pattern.match(value): + raise cv.Invalid( + f"Invalid hexadecimal value for 128 UUID format: '{in_value}'" + ) + return value + raise cv.Invalid( + f"Bluetooth UUID must be in 16 bit '{bt_uuid16_format}', 32 bit '{bt_uuid32_format}', or 128 bit '{bt_uuid128_format}' format" + ) + + def validate_variant(_): variant = get_esp32_variant() if variant in NO_BLUETOOTH_VARIANTS: diff --git a/esphome/components/esp32_ble_server/__init__.py b/esphome/components/esp32_ble_server/__init__.py index 9da7d13999..ab8e27ec43 100644 --- a/esphome/components/esp32_ble_server/__init__.py +++ b/esphome/components/esp32_ble_server/__init__.py @@ -1,37 +1,526 @@ +import encodings + +from esphome import automation import esphome.codegen as cg from esphome.components import esp32_ble from esphome.components.esp32 import add_idf_sdkconfig_option +from esphome.components.esp32_ble import bt_uuid import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_MODEL +from esphome.config_validation import UNDEFINED +from esphome.const import ( + CONF_DATA, + CONF_ESPHOME, + CONF_ID, + CONF_MAX_LENGTH, + CONF_MODEL, + CONF_NOTIFY, + CONF_ON_CONNECT, + CONF_ON_DISCONNECT, + CONF_PROJECT, + CONF_SERVICES, + CONF_TYPE, + CONF_UUID, + CONF_VALUE, + __version__ as ESPHOME_VERSION, +) from esphome.core import CORE +from esphome.schema_extractors import SCHEMA_EXTRACT -AUTO_LOAD = ["esp32_ble"] +AUTO_LOAD = ["esp32_ble", "bytebuffer", "event_emitter"] CODEOWNERS = ["@jesserockz", "@clydebarrow", "@Rapsssito"] DEPENDENCIES = ["esp32"] +DOMAIN = "esp32_ble_server" +CONF_ADVERTISE = "advertise" +CONF_BROADCAST = "broadcast" +CONF_CHARACTERISTICS = "characteristics" +CONF_DESCRIPTION = "description" +CONF_DESCRIPTORS = "descriptors" +CONF_ENDIANNESS = "endianness" +CONF_FIRMWARE_VERSION = "firmware_version" +CONF_INDICATE = "indicate" CONF_MANUFACTURER = "manufacturer" CONF_MANUFACTURER_DATA = "manufacturer_data" +CONF_ON_WRITE = "on_write" +CONF_READ = "read" +CONF_STRING = "string" +CONF_STRING_ENCODING = "string_encoding" +CONF_WRITE = "write" +CONF_WRITE_NO_RESPONSE = "write_no_response" + +# Internal configuration keys +CONF_CHAR_VALUE_ACTION_ID_ = "char_value_action_id_" + +# BLE reserverd UUIDs +CCCD_DESCRIPTOR_UUID = 0x2902 +CUD_DESCRIPTOR_UUID = 0x2901 +DEVICE_INFORMATION_SERVICE_UUID = 0x180A +MANUFACTURER_NAME_CHARACTERISTIC_UUID = 0x2A29 +MODEL_CHARACTERISTIC_UUID = 0x2A24 +FIRMWARE_VERSION_CHARACTERISTIC_UUID = 0x2A26 + +# Core key to store the global configuration +KEY_NOTIFY_REQUIRED = "notify_required" +KEY_SET_VALUE = "set_value" 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") +esp32_ble_server_automations_ns = esp32_ble_server_ns.namespace( + "esp32_ble_server_automations" +) +BLETriggers_ns = esp32_ble_server_automations_ns.namespace("BLETriggers") +BLEDescriptor = esp32_ble_server_ns.class_("BLEDescriptor") +BLECharacteristic = esp32_ble_server_ns.class_("BLECharacteristic") +BLEService = esp32_ble_server_ns.class_("BLEService") +BLECharacteristicSetValueAction = esp32_ble_server_automations_ns.class_( + "BLECharacteristicSetValueAction", automation.Action +) +BLEDescriptorSetValueAction = esp32_ble_server_automations_ns.class_( + "BLEDescriptorSetValueAction", automation.Action +) +BLECharacteristicNotifyAction = esp32_ble_server_automations_ns.class_( + "BLECharacteristicNotifyAction", automation.Action +) +bytebuffer_ns = cg.esphome_ns.namespace("bytebuffer") +Endianness_ns = bytebuffer_ns.namespace("Endian") +ByteBuffer_ns = bytebuffer_ns.namespace("ByteBuffer") +ByteBuffer = bytebuffer_ns.class_("ByteBuffer") +PROPERTY_MAP = { + CONF_READ: BLECharacteristic_ns.PROPERTY_READ, + CONF_WRITE: BLECharacteristic_ns.PROPERTY_WRITE, + CONF_NOTIFY: BLECharacteristic_ns.PROPERTY_NOTIFY, + CONF_BROADCAST: BLECharacteristic_ns.PROPERTY_BROADCAST, + CONF_INDICATE: BLECharacteristic_ns.PROPERTY_INDICATE, + CONF_WRITE_NO_RESPONSE: BLECharacteristic_ns.PROPERTY_WRITE_NR, +} + + +class ValueType: + def __init__(self, type_, validator, length): + self.type_ = type_ + self.validator = validator + self.length = length + + def validate(self, value, encoding): + value = self.validator(value) + if self.type_ == "string": + try: + value.encode(encoding) + except UnicodeEncodeError as e: + raise cv.Invalid(str(e)) from e + return value + + +VALUE_TYPES = { + type_name: ValueType(type_name, validator, length) + for type_name, validator, length in ( + ("uint8_t", cv.uint8_t, 1), + ("uint16_t", cv.uint16_t, 2), + ("uint32_t", cv.uint32_t, 4), + ("uint64_t", cv.uint64_t, 8), + ("int8_t", cv.int_range(-128, 127), 1), + ("int16_t", cv.int_range(-32768, 32767), 2), + ("int32_t", cv.int_range(-2147483648, 2147483647), 4), + ("int64_t", cv.int_range(-9223372036854775808, 9223372036854775807), 8), + ("float", cv.float_, 4), + ("double", cv.float_, 8), + ("string", cv.string_strict, None), # Length is variable + ) +} + + +def validate_char_on_write(char_config): + if CONF_ON_WRITE in char_config: + if not char_config[CONF_WRITE] and not char_config[CONF_WRITE_NO_RESPONSE]: + raise cv.Invalid( + f"{CONF_ON_WRITE} requires the {CONF_WRITE} or {CONF_WRITE_NO_RESPONSE} property to be set" + ) + return char_config + + +def validate_descriptor(desc_config): + if CONF_ON_WRITE in desc_config: + if not desc_config[CONF_WRITE]: + raise cv.Invalid( + f"{CONF_ON_WRITE} requires the {CONF_WRITE} property to be set" + ) + if CONF_MAX_LENGTH not in desc_config: + value = desc_config[CONF_VALUE][CONF_DATA] + if cg.is_template(value): + raise cv.Invalid( + f"Descriptor {desc_config[CONF_UUID]} has a templatable value and the {CONF_MAX_LENGTH} property is not set" + ) + if isinstance(value, list): + desc_config[CONF_MAX_LENGTH] = len(value) + elif isinstance(value, str): + desc_config[CONF_MAX_LENGTH] = len( + value.encode(desc_config[CONF_VALUE][CONF_STRING_ENCODING]) + ) + else: + desc_config[CONF_MAX_LENGTH] = VALUE_TYPES[ + desc_config[CONF_VALUE][CONF_TYPE] + ].length + return desc_config + + +def validate_notify_action(config): + # Store the characteristic ID in the global data for the final validation + data = CORE.data.setdefault(DOMAIN, {}).setdefault(KEY_NOTIFY_REQUIRED, set()) + data.add(config[CONF_ID]) + return config + + +def validate_set_value_action(config): + # Store the characteristic ID in the global data for the final validation + data = CORE.data.setdefault(DOMAIN, {}).setdefault(KEY_SET_VALUE, set()) + data.add(config[CONF_ID]) + return config + + +def create_description_cud(char_config): + if CONF_DESCRIPTION not in char_config: + return char_config + # If the config displays a description, there cannot be a descriptor with the CUD UUID + for desc in char_config[CONF_DESCRIPTORS]: + if desc[CONF_UUID] == CUD_DESCRIPTOR_UUID: + raise cv.Invalid( + f"Characteristic {char_config[CONF_UUID]} has a description, but a CUD descriptor is already present" + ) + # Manually add the CUD descriptor + char_config[CONF_DESCRIPTORS].append( + DESCRIPTOR_SCHEMA( + { + CONF_UUID: CUD_DESCRIPTOR_UUID, + CONF_READ: True, + CONF_WRITE: False, + CONF_VALUE: char_config[CONF_DESCRIPTION], + } + ) + ) + return char_config + + +def create_notify_cccd(char_config): + if not char_config[CONF_NOTIFY] and not char_config[CONF_INDICATE]: + return char_config + # If the CCCD descriptor is already present, return the config + for desc in char_config[CONF_DESCRIPTORS]: + if desc[CONF_UUID] == CCCD_DESCRIPTOR_UUID: + # Check if the WRITE property is set + if not desc[CONF_WRITE]: + raise cv.Invalid( + f"Characteristic {char_config[CONF_UUID]} has notify actions, but the CCCD descriptor does not have the {CONF_WRITE} property set" + ) + return char_config + # Manually add the CCCD descriptor + char_config[CONF_DESCRIPTORS].append( + DESCRIPTOR_SCHEMA( + { + CONF_UUID: CCCD_DESCRIPTOR_UUID, + CONF_READ: True, + CONF_WRITE: True, + CONF_MAX_LENGTH: 2, + CONF_VALUE: [0, 0], + } + ) + ) + return char_config + + +def create_device_information_service(config): + # If there is already a device information service, + # there cannot be CONF_MODEL, CONF_MANUFACTURER or CONF_FIRMWARE_VERSION properties + for service in config[CONF_SERVICES]: + if service[CONF_UUID] == DEVICE_INFORMATION_SERVICE_UUID: + if ( + CONF_MODEL in config + or CONF_MANUFACTURER in config + or CONF_FIRMWARE_VERSION in config + ): + raise cv.Invalid( + "Device information service already present, cannot add manufacturer, model or firmware version" + ) + return config + project = CORE.raw_config[CONF_ESPHOME].get(CONF_PROJECT, {}) + model = config.get(CONF_MODEL, project.get("name", CORE.data["esp32"]["board"])) + version = config.get( + CONF_FIRMWARE_VERSION, project.get("version", "ESPHome " + ESPHOME_VERSION) + ) + # Manually add the device information service + config[CONF_SERVICES].append( + SERVICE_SCHEMA( + { + CONF_UUID: DEVICE_INFORMATION_SERVICE_UUID, + CONF_CHARACTERISTICS: [ + { + CONF_UUID: MANUFACTURER_NAME_CHARACTERISTIC_UUID, + CONF_READ: True, + CONF_VALUE: config.get(CONF_MANUFACTURER, "ESPHome"), + }, + { + CONF_UUID: MODEL_CHARACTERISTIC_UUID, + CONF_READ: True, + CONF_VALUE: model, + }, + { + CONF_UUID: FIRMWARE_VERSION_CHARACTERISTIC_UUID, + CONF_READ: True, + CONF_VALUE: version, + }, + ], + } + ) + ) + return config + + +def final_validate_config(config): + # Check if all characteristics that require notifications have the notify property set + for char_id in CORE.data.get(DOMAIN, {}).get(KEY_NOTIFY_REQUIRED, set()): + # Look for the characteristic in the configuration + char_config = [ + char_conf + for service_conf in config[CONF_SERVICES] + for char_conf in service_conf[CONF_CHARACTERISTICS] + if char_conf[CONF_ID] == char_id + ][0] + if not char_config[CONF_NOTIFY]: + raise cv.Invalid( + f"Characteristic {char_config[CONF_UUID]} has notify actions and the {CONF_NOTIFY} property is not set" + ) + for char_id in CORE.data.get(DOMAIN, {}).get(KEY_SET_VALUE, set()): + # Look for the characteristic in the configuration + char_config = [ + char_conf + for service_conf in config[CONF_SERVICES] + for char_conf in service_conf[CONF_CHARACTERISTICS] + if char_conf[CONF_ID] == char_id + ][0] + if isinstance(char_config.get(CONF_VALUE, {}).get(CONF_DATA), cv.Lambda): + raise cv.Invalid( + f"Characteristic {char_config[CONF_UUID]} has both a set_value action and a templated value" + ) + return config + + +def validate_value_type(value_config): + # If the value is a not a templatable, the type must be set + value = value_config[CONF_DATA] + + if type_ := value_config.get(CONF_TYPE): + if cg.is_template(value): + raise cv.Invalid( + f'The "{CONF_TYPE}" property is not allowed for templatable values' + ) + value_config[CONF_DATA] = VALUE_TYPES[type_].validate( + value, value_config[CONF_STRING_ENCODING] + ) + elif isinstance(value, (float, int)): + raise cv.Invalid( + f'The "{CONF_TYPE}" property is required for the value "{value}"' + ) + return value_config + + +def validate_encoding(value): + if value == SCHEMA_EXTRACT: + return cv.one_of("utf-8", "latin-1", "ascii", "utf-16", "utf-32") + value = encodings.normalize_encoding(value) + if not value: + raise cv.Invalid("Invalid encoding") + return value + + +def value_schema(default_type=UNDEFINED, templatable=True): + data_validators = [ + cv.string_strict, + cv.int_, + cv.float_, + cv.All([cv.uint8_t], cv.Length(min=1)), + ] + if templatable: + data_validators.append(cv.returning_lambda) + + return cv.maybe_simple_value( + cv.All( + { + cv.Required(CONF_DATA): cv.Any(*data_validators), + cv.Optional(CONF_TYPE, default=default_type): cv.one_of( + *VALUE_TYPES, lower=True + ), + cv.Optional(CONF_STRING_ENCODING, default="utf_8"): validate_encoding, + cv.Optional(CONF_ENDIANNESS, default="LITTLE"): cv.enum( + { + "LITTLE": Endianness_ns.LITTLE, + "BIG": Endianness_ns.BIG, + } + ), + }, + validate_value_type, + ), + key=CONF_DATA, + ) + + +DESCRIPTOR_SCHEMA = cv.All( + { + cv.GenerateID(): cv.declare_id(BLEDescriptor), + cv.Required(CONF_UUID): cv.Any(bt_uuid, cv.hex_uint32_t), + cv.Optional(CONF_READ, default=True): cv.boolean, + cv.Optional(CONF_WRITE, default=True): cv.boolean, + cv.Optional(CONF_ON_WRITE): automation.validate_automation(single=True), + cv.Required(CONF_VALUE): value_schema(templatable=False), + cv.Optional(CONF_MAX_LENGTH): cv.uint16_t, + }, + validate_descriptor, +) + +CHARACTERISTIC_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(BLECharacteristic), + cv.Required(CONF_UUID): cv.Any(bt_uuid, cv.hex_uint32_t), + cv.Optional(CONF_VALUE): value_schema(templatable=True), + cv.GenerateID(CONF_CHAR_VALUE_ACTION_ID_): cv.declare_id( + BLECharacteristicSetValueAction + ), + cv.Optional(CONF_DESCRIPTORS, default=[]): cv.ensure_list(DESCRIPTOR_SCHEMA), + cv.Optional(CONF_ON_WRITE): automation.validate_automation(single=True), + cv.Optional(CONF_DESCRIPTION): value_schema( + default_type="string", templatable=False + ), + }, + extra_schemas=[ + validate_char_on_write, + create_description_cud, + create_notify_cccd, + ], +).extend({cv.Optional(k, default=False): cv.boolean for k in PROPERTY_MAP}) + +SERVICE_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(BLEService), + cv.Required(CONF_UUID): cv.Any(bt_uuid, cv.hex_uint32_t), + cv.Optional(CONF_ADVERTISE, default=False): cv.boolean, + cv.Optional(CONF_CHARACTERISTICS, default=[]): cv.ensure_list( + CHARACTERISTIC_SCHEMA + ), + } +) + CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(BLEServer), cv.GenerateID(esp32_ble.CONF_BLE_ID): cv.use_id(esp32_ble.ESP32BLE), - 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_MANUFACTURER): value_schema("string", templatable=False), + cv.Optional(CONF_MODEL): value_schema("string", templatable=False), + cv.Optional(CONF_FIRMWARE_VERSION): value_schema("string", templatable=False), + cv.Optional(CONF_MANUFACTURER_DATA): cv.Schema([cv.uint8_t]), + cv.Optional(CONF_SERVICES, default=[]): cv.ensure_list(SERVICE_SCHEMA), + cv.Optional(CONF_ON_CONNECT): automation.validate_automation(single=True), + cv.Optional(CONF_ON_DISCONNECT): automation.validate_automation(single=True), + }, + extra_schemas=[create_device_information_service], ).extend(cv.COMPONENT_SCHEMA) +FINAL_VALIDATE_SCHEMA = final_validate_config + + +def parse_properties(char_conf): + return sum( + (PROPERTY_MAP[k] for k in char_conf if k in PROPERTY_MAP and char_conf[k]), + start=0, + ) + + +def parse_uuid(uuid): + # If the UUID is a int, use from_uint32 + if isinstance(uuid, int): + return ESPBTUUID_ns.from_uint32(uuid) + # Otherwise, use ESPBTUUID_ns.from_raw + return ESPBTUUID_ns.from_raw(uuid) + + +async def parse_value(value_config, args): + value = value_config[CONF_DATA] + if isinstance(value, cv.Lambda): + return await cg.templatable(value, args, cg.std_vector.template(cg.uint8)) + + if isinstance(value, str): + value = list(value.encode(value_config[CONF_STRING_ENCODING])) + if isinstance(value, list): + return cg.std_vector.template(cg.uint8)(value) + val = cg.RawExpression(f"{value_config[CONF_TYPE]}({cg.safe_exp(value)})") + return ByteBuffer_ns.wrap(val, value_config[CONF_ENDIANNESS]) + + +def calculate_num_handles(service_config): + total = 1 + len(service_config[CONF_CHARACTERISTICS]) * 2 + total += sum( + len(char_conf[CONF_DESCRIPTORS]) + for char_conf in service_config[CONF_CHARACTERISTICS] + ) + return total + + +async def to_code_descriptor(descriptor_conf, char_var): + value = await parse_value(descriptor_conf[CONF_VALUE], {}) + desc_var = cg.new_Pvariable( + descriptor_conf[CONF_ID], + parse_uuid(descriptor_conf[CONF_UUID]), + descriptor_conf[CONF_MAX_LENGTH], + descriptor_conf[CONF_READ], + descriptor_conf[CONF_WRITE], + ) + cg.add(char_var.add_descriptor(desc_var)) + cg.add(desc_var.set_value(value)) + if CONF_ON_WRITE in descriptor_conf: + on_write_conf = descriptor_conf[CONF_ON_WRITE] + await automation.build_automation( + BLETriggers_ns.create_descriptor_on_write_trigger(desc_var), + [(cg.std_vector.template(cg.uint8), "x"), (cg.uint16, "id")], + on_write_conf, + ) + + +async def to_code_characteristic(service_var, char_conf): + char_var = cg.Pvariable( + char_conf[CONF_ID], + service_var.create_characteristic( + parse_uuid(char_conf[CONF_UUID]), + parse_properties(char_conf), + ), + ) + if CONF_ON_WRITE in char_conf: + on_write_conf = char_conf[CONF_ON_WRITE] + await automation.build_automation( + BLETriggers_ns.create_characteristic_on_write_trigger(char_var), + [(cg.std_vector.template(cg.uint8), "x"), (cg.uint16, "id")], + on_write_conf, + ) + if CONF_VALUE in char_conf: + action_conf = { + CONF_ID: char_conf[CONF_ID], + CONF_VALUE: char_conf[CONF_VALUE], + } + value_action = await ble_server_characteristic_set_value( + action_conf, + char_conf[CONF_CHAR_VALUE_ACTION_ID_], + cg.TemplateArguments(), + {}, + ) + cg.add(value_action.play()) + for descriptor_conf in char_conf[CONF_DESCRIPTORS]: + await to_code_descriptor(descriptor_conf, char_var) + async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) @@ -42,13 +531,94 @@ 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]: + # 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_config[CONF_ID], + var.create_service( + parse_uuid(service_config[CONF_UUID]), + service_config[CONF_ADVERTISE], + num_handles, + ), + ) + for char_conf in service_config[CONF_CHARACTERISTICS]: + await to_code_characteristic(service_var, char_conf) + if service_config[CONF_UUID] == DEVICE_INFORMATION_SERVICE_UUID: + cg.add(var.set_device_information_service(service_var)) + else: + cg.add(var.enqueue_start_service(service_var)) + if CONF_ON_CONNECT in config: + await automation.build_automation( + BLETriggers_ns.create_server_on_connect_trigger(var), + [(cg.uint16, "id")], + config[CONF_ON_CONNECT], + ) + if CONF_ON_DISCONNECT in config: + await automation.build_automation( + BLETriggers_ns.create_server_on_disconnect_trigger(var), + [(cg.uint16, "id")], + config[CONF_ON_DISCONNECT], + ) cg.add_define("USE_ESP32_BLE_SERVER") - if CORE.using_esp_idf: add_idf_sdkconfig_option("CONFIG_BT_ENABLED", True) + + +@automation.register_action( + "ble_server.characteristic.set_value", + BLECharacteristicSetValueAction, + cv.All( + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(BLECharacteristic), + cv.Required(CONF_VALUE): value_schema(), + } + ), + validate_set_value_action, + ), +) +async def ble_server_characteristic_set_value(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + value = await parse_value(config[CONF_VALUE], args) + cg.add(var.set_buffer(value)) + return var + + +@automation.register_action( + "ble_server.descriptor.set_value", + BLEDescriptorSetValueAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(BLEDescriptor), + cv.Required(CONF_VALUE): value_schema(), + } + ), +) +async def ble_server_descriptor_set_value(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + value = await parse_value(config[CONF_VALUE], args) + cg.add(var.set_buffer(value)) + return var + + +@automation.register_action( + "ble_server.characteristic.notify", + BLECharacteristicNotifyAction, + cv.All( + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(BLECharacteristic), + } + ), + validate_notify_action, + ), +) +async def ble_server_characteristic_notify(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + return var diff --git a/esphome/components/esp32_ble_server/ble_2901.cpp b/esphome/components/esp32_ble_server/ble_2901.cpp deleted file mode 100644 index ee0808d2c4..0000000000 --- a/esphome/components/esp32_ble_server/ble_2901.cpp +++ /dev/null @@ -1,18 +0,0 @@ -#include "ble_2901.h" -#include "esphome/components/esp32_ble/ble_uuid.h" - -#ifdef USE_ESP32 - -namespace esphome { -namespace esp32_ble_server { - -BLE2901::BLE2901(const std::string &value) : BLE2901((uint8_t *) value.data(), value.length()) {} -BLE2901::BLE2901(const uint8_t *data, size_t length) : BLEDescriptor(esp32_ble::ESPBTUUID::from_uint16(0x2901)) { - this->set_value(data, length); - this->permissions_ = ESP_GATT_PERM_READ; -} - -} // namespace esp32_ble_server -} // namespace esphome - -#endif diff --git a/esphome/components/esp32_ble_server/ble_2901.h b/esphome/components/esp32_ble_server/ble_2901.h deleted file mode 100644 index 60f53e55b2..0000000000 --- a/esphome/components/esp32_ble_server/ble_2901.h +++ /dev/null @@ -1,19 +0,0 @@ -#pragma once - -#include "ble_descriptor.h" - -#ifdef USE_ESP32 - -namespace esphome { -namespace esp32_ble_server { - -class BLE2901 : public BLEDescriptor { - public: - BLE2901(const std::string &value); - BLE2901(const uint8_t *data, size_t length); -}; - -} // namespace esp32_ble_server -} // namespace esphome - -#endif diff --git a/esphome/components/esp32_ble_server/ble_characteristic.cpp b/esphome/components/esp32_ble_server/ble_characteristic.cpp index 6ff7d615f9..15739d60bb 100644 --- a/esphome/components/esp32_ble_server/ble_characteristic.cpp +++ b/esphome/components/esp32_ble_server/ble_characteristic.cpp @@ -32,70 +32,36 @@ BLECharacteristic::BLECharacteristic(const ESPBTUUID uuid, uint32_t properties) this->set_write_no_response_property((properties & PROPERTY_WRITE_NR) != 0); } -void BLECharacteristic::set_value(std::vector value) { +void BLECharacteristic::set_value(ByteBuffer buffer) { this->set_value(buffer.get_data()); } + +void BLECharacteristic::set_value(const std::vector &buffer) { xSemaphoreTake(this->set_value_lock_, 0L); - this->value_ = std::move(value); + this->value_ = buffer; xSemaphoreGive(this->set_value_lock_); } -void BLECharacteristic::set_value(const std::string &value) { - this->set_value(std::vector(value.begin(), value.end())); -} -void BLECharacteristic::set_value(const uint8_t *data, size_t length) { - this->set_value(std::vector(data, data + length)); -} -void BLECharacteristic::set_value(uint8_t &data) { - uint8_t temp[1]; - temp[0] = data; - this->set_value(temp, 1); -} -void BLECharacteristic::set_value(uint16_t &data) { - uint8_t temp[2]; - temp[0] = data; - temp[1] = data >> 8; - this->set_value(temp, 2); -} -void BLECharacteristic::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 BLECharacteristic::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 BLECharacteristic::set_value(float &data) { - float temp = data; - this->set_value((uint8_t *) &temp, 4); -} -void BLECharacteristic::set_value(double &data) { - double temp = data; - this->set_value((uint8_t *) &temp, 8); -} -void BLECharacteristic::set_value(bool &data) { - uint8_t temp[1]; - temp[0] = data; - this->set_value(temp, 1); +void BLECharacteristic::set_value(const std::string &buffer) { + this->set_value(std::vector(buffer.begin(), buffer.end())); } -void BLECharacteristic::notify(bool notification) { - if (!notification) { - ESP_LOGW(TAG, "notification=false is not yet supported"); - // TODO: Handle when notification=false - } - if (this->service_->get_server()->get_connected_client_count() == 0) +void BLECharacteristic::notify() { + if (this->service_ == nullptr || this->service_->get_server() == nullptr || + this->service_->get_server()->get_connected_client_count() == 0) return; for (auto &client : this->service_->get_server()->get_clients()) { size_t length = this->value_.size(); - esp_err_t err = esp_ble_gatts_send_indicate(this->service_->get_server()->get_gatts_if(), client.first, - this->handle_, length, this->value_.data(), false); + // If the client is not in the list of clients to notify, skip it + if (this->clients_to_notify_.count(client) == 0) + continue; + // If the client is in the list of clients to notify, check if it requires an ack (i.e. INDICATE) + bool require_ack = this->clients_to_notify_[client]; + // TODO: Remove this block when INDICATE acknowledgment is supported + if (require_ack) { + ESP_LOGW(TAG, "INDICATE acknowledgment is not yet supported (i.e. it works as a NOTIFY)"); + require_ack = false; + } + esp_err_t err = esp_ble_gatts_send_indicate(this->service_->get_server()->get_gatts_if(), client, this->handle_, + length, this->value_.data(), require_ack); if (err != ESP_OK) { ESP_LOGE(TAG, "esp_ble_gatts_send_indicate failed %d", err); return; @@ -103,7 +69,24 @@ void BLECharacteristic::notify(bool notification) { } } -void BLECharacteristic::add_descriptor(BLEDescriptor *descriptor) { this->descriptors_.push_back(descriptor); } +void BLECharacteristic::add_descriptor(BLEDescriptor *descriptor) { + // 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(BLEDescriptorEvt::VectorEvt::ON_WRITE, [this](const std::vector &value, uint16_t conn_id) { + if (value.size() != 2) + return; + uint16_t cccd = encode_uint16(value[1], value[0]); + bool notify = (cccd & 1) != 0; + bool indicate = (cccd & 2) != 0; + if (notify || indicate) { + this->clients_to_notify_[conn_id] = indicate; + } else { + this->clients_to_notify_.erase(conn_id); + } + }); + } + this->descriptors_.push_back(descriptor); +} void BLECharacteristic::remove_descriptor(BLEDescriptor *descriptor) { this->descriptors_.erase(std::remove(this->descriptors_.begin(), this->descriptors_.end(), descriptor), @@ -223,6 +206,9 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt if (!param->read.need_rsp) break; // For some reason you can request a read but not want a response + this->EventEmitter::emit_(BLECharacteristicEvt::EmptyEvt::ON_READ, + param->read.conn_id); + uint16_t max_offset = 22; esp_gatt_rsp_t response; @@ -262,13 +248,13 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt } case ESP_GATTS_WRITE_EVT: { if (this->handle_ != param->write.handle) - return; + break; if (param->write.is_prep) { this->value_.insert(this->value_.end(), param->write.value, param->write.value + param->write.len); this->write_event_ = true; } else { - this->set_value(param->write.value, param->write.len); + this->set_value(ByteBuffer::wrap(param->write.value, param->write.len)); } if (param->write.need_rsp) { @@ -289,7 +275,8 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt } if (!param->write.is_prep) { - this->on_write_(this->value_); + this->EventEmitter, uint16_t>::emit_( + BLECharacteristicEvt::VectorEvt::ON_WRITE, this->value_, param->write.conn_id); } break; @@ -300,7 +287,8 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt break; this->write_event_ = false; if (param->exec_write.exec_write_flag == ESP_GATT_PREP_WRITE_EXEC) { - this->on_write_(this->value_); + this->EventEmitter, uint16_t>::emit_( + BLECharacteristicEvt::VectorEvt::ON_WRITE, this->value_, param->exec_write.conn_id); } esp_err_t err = esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id, ESP_GATT_OK, nullptr); diff --git a/esphome/components/esp32_ble_server/ble_characteristic.h b/esphome/components/esp32_ble_server/ble_characteristic.h index 8837c796a5..3698b8c4aa 100644 --- a/esphome/components/esp32_ble_server/ble_characteristic.h +++ b/esphome/components/esp32_ble_server/ble_characteristic.h @@ -2,8 +2,11 @@ #include "ble_descriptor.h" #include "esphome/components/esp32_ble/ble_uuid.h" +#include "esphome/components/event_emitter/event_emitter.h" +#include "esphome/components/bytebuffer/bytebuffer.h" #include +#include #ifdef USE_ESP32 @@ -19,24 +22,30 @@ namespace esphome { namespace esp32_ble_server { using namespace esp32_ble; +using namespace bytebuffer; +using namespace event_emitter; class BLEService; -class BLECharacteristic { +namespace BLECharacteristicEvt { +enum VectorEvt { + ON_WRITE, +}; + +enum EmptyEvt { + ON_READ, +}; +} // namespace BLECharacteristicEvt + +class BLECharacteristic : public EventEmitter, uint16_t>, + public EventEmitter { public: BLECharacteristic(ESPBTUUID uuid, uint32_t properties); ~BLECharacteristic(); - void set_value(const uint8_t *data, size_t length); - void set_value(std::vector value); - void set_value(const std::string &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 set_value(ByteBuffer buffer); + void set_value(const std::vector &buffer); + void set_value(const std::string &buffer); void set_broadcast_property(bool value); void set_indicate_property(bool value); @@ -45,13 +54,12 @@ class BLECharacteristic { void set_write_property(bool value); void set_write_no_response_property(bool value); - void notify(bool notification = true); + void notify(); void do_create(BLEService *service); + void do_delete() { this->clients_to_notify_.clear(); } void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param); - void on_write(const std::function &)> &&func) { this->on_write_ = func; } - void add_descriptor(BLEDescriptor *descriptor); void remove_descriptor(BLEDescriptor *descriptor); @@ -71,7 +79,7 @@ class BLECharacteristic { protected: bool write_event_{false}; - BLEService *service_; + BLEService *service_{}; ESPBTUUID uuid_; esp_gatt_char_prop_t properties_; uint16_t handle_{0xFFFF}; @@ -81,8 +89,7 @@ class BLECharacteristic { SemaphoreHandle_t set_value_lock_; std::vector descriptors_; - - std::function &)> on_write_; + std::unordered_map clients_to_notify_; esp_gatt_perm_t permissions_ = ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE; diff --git a/esphome/components/esp32_ble_server/ble_descriptor.cpp b/esphome/components/esp32_ble_server/ble_descriptor.cpp index bfb6224335..afbe579513 100644 --- a/esphome/components/esp32_ble_server/ble_descriptor.cpp +++ b/esphome/components/esp32_ble_server/ble_descriptor.cpp @@ -12,11 +12,19 @@ namespace esp32_ble_server { static const char *const TAG = "esp32_ble_server.descriptor"; -BLEDescriptor::BLEDescriptor(ESPBTUUID uuid, uint16_t max_len) { +static RAMAllocator descriptor_allocator{}; // NOLINT + +BLEDescriptor::BLEDescriptor(ESPBTUUID uuid, uint16_t max_len, bool read, bool write) { this->uuid_ = uuid; this->value_.attr_len = 0; this->value_.attr_max_len = max_len; - this->value_.attr_value = (uint8_t *) malloc(max_len); // NOLINT + this->value_.attr_value = descriptor_allocator.allocate(max_len); + if (read) { + this->permissions_ |= ESP_GATT_PERM_READ; + } + if (write) { + this->permissions_ |= ESP_GATT_PERM_WRITE; + } } BLEDescriptor::~BLEDescriptor() { free(this->value_.attr_value); } // NOLINT @@ -38,14 +46,15 @@ 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(const uint8_t *data, size_t length) { +void BLEDescriptor::set_value(std::vector buffer) { + size_t length = buffer.size(); + 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); return; } this->value_.attr_len = length; - memcpy(this->value_.attr_value, data, length); + memcpy(this->value_.attr_value, buffer.data(), length); } void BLEDescriptor::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, @@ -61,10 +70,13 @@ void BLEDescriptor::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_ break; } case ESP_GATTS_WRITE_EVT: { - if (this->handle_ == param->write.handle) { - this->value_.attr_len = param->write.len; - memcpy(this->value_.attr_value, param->write.value, param->write.len); - } + if (this->handle_ != param->write.handle) + break; + this->value_.attr_len = param->write.len; + memcpy(this->value_.attr_value, param->write.value, param->write.len); + this->emit_(BLEDescriptorEvt::VectorEvt::ON_WRITE, + std::vector(param->write.value, param->write.value + param->write.len), + param->write.conn_id); break; } default: diff --git a/esphome/components/esp32_ble_server/ble_descriptor.h b/esphome/components/esp32_ble_server/ble_descriptor.h index 4b8fb345c3..8d3c22c5a1 100644 --- a/esphome/components/esp32_ble_server/ble_descriptor.h +++ b/esphome/components/esp32_ble_server/ble_descriptor.h @@ -1,6 +1,8 @@ #pragma once #include "esphome/components/esp32_ble/ble_uuid.h" +#include "esphome/components/event_emitter/event_emitter.h" +#include "esphome/components/bytebuffer/bytebuffer.h" #ifdef USE_ESP32 @@ -11,17 +13,26 @@ namespace esphome { namespace esp32_ble_server { using namespace esp32_ble; +using namespace bytebuffer; +using namespace event_emitter; class BLECharacteristic; -class BLEDescriptor { +namespace BLEDescriptorEvt { +enum VectorEvt { + ON_WRITE, +}; +} // namespace BLEDescriptorEvt + +class BLEDescriptor : public EventEmitter, uint16_t> { public: - BLEDescriptor(ESPBTUUID uuid, uint16_t max_len = 100); + BLEDescriptor(ESPBTUUID uuid, uint16_t max_len = 100, bool read = true, bool write = true); virtual ~BLEDescriptor(); void do_create(BLECharacteristic *characteristic); + ESPBTUUID get_uuid() const { return this->uuid_; } - void set_value(const std::string &value); - void set_value(const uint8_t *data, size_t length); + void set_value(std::vector buffer); + void set_value(ByteBuffer buffer) { this->set_value(buffer.get_data()); } void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param); @@ -33,9 +44,9 @@ class BLEDescriptor { ESPBTUUID uuid_; uint16_t handle_{0xFFFF}; - esp_attr_value_t value_; + esp_attr_value_t value_{}; - esp_gatt_perm_t permissions_ = ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE; + esp_gatt_perm_t permissions_{}; enum State : uint8_t { FAILED = 0x00, diff --git a/esphome/components/esp32_ble_server/ble_server.cpp b/esphome/components/esp32_ble_server/ble_server.cpp index 338413f64e..5339bf8aed 100644 --- a/esphome/components/esp32_ble_server/ble_server.cpp +++ b/esphome/components/esp32_ble_server/ble_server.cpp @@ -19,11 +19,6 @@ namespace esp32_ble_server { static const char *const TAG = "esp32_ble_server"; -static const uint16_t DEVICE_INFORMATION_SERVICE_UUID = 0x180A; -static const uint16_t MODEL_UUID = 0x2A24; -static const uint16_t VERSION_UUID = 0x2A26; -static const uint16_t MANUFACTURER_UUID = 0x2A29; - void BLEServer::setup() { if (this->parent_->is_failed()) { this->mark_failed(); @@ -38,9 +33,27 @@ void BLEServer::loop() { return; } switch (this->state_) { - case RUNNING: - return; - + case RUNNING: { + // Start all services that are pending to start + if (!this->services_to_start_.empty()) { + uint16_t index_to_remove = 0; + // Iterate over the services to start + for (unsigned i = 0; i < this->services_to_start_.size(); i++) { + BLEService *service = this->services_to_start_[i]; + if (service->is_created()) { + service->start(); // Needs to be called once per characteristic in the service + } else { + index_to_remove = i + 1; + } + } + // Remove the services that have been started + if (index_to_remove > 0) { + this->services_to_start_.erase(this->services_to_start_.begin(), + this->services_to_start_.begin() + index_to_remove - 1); + } + } + break; + } case INIT: { esp_err_t err = esp_ble_gatts_app_register(0); if (err != ESP_OK) { @@ -53,29 +66,26 @@ void BLEServer::loop() { } case REGISTERING: { if (this->registered_) { + // Create the device information service first so + // it is at the top of the GATT table + this->device_information_service_->do_create(this); // Create all services previously created for (auto &pair : this->services_) { + if (pair.second == this->device_information_service_) { + continue; + } 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->create_device_characteristics_(); - } this->state_ = STARTING_SERVICE; } break; } case STARTING_SERVICE: { - if (!this->device_information_service_->is_created()) { - break; - } if (this->device_information_service_->is_running()) { this->state_ = RUNNING; this->restart_advertising_(); ESP_LOGD(TAG, "BLE server setup successfully"); - } else if (!this->device_information_service_->is_starting()) { + } else if (this->device_information_service_->is_created()) { this->device_information_service_->start(); } break; @@ -93,81 +103,66 @@ void BLEServer::restart_advertising_() { } } -bool BLEServer::create_device_characteristics_() { - if (this->model_.has_value()) { - BLECharacteristic *model = - this->device_information_service_->create_characteristic(MODEL_UUID, BLECharacteristic::PROPERTY_READ); - model->set_value(this->model_.value()); - } else { - BLECharacteristic *model = - this->device_information_service_->create_characteristic(MODEL_UUID, BLECharacteristic::PROPERTY_READ); - model->set_value(ESPHOME_BOARD); - } - - BLECharacteristic *version = - this->device_information_service_->create_characteristic(VERSION_UUID, BLECharacteristic::PROPERTY_READ); - version->set_value("ESPHome " ESPHOME_VERSION); - - BLECharacteristic *manufacturer = - this->device_information_service_->create_characteristic(MANUFACTURER_UUID, BLECharacteristic::PROPERTY_READ); - manufacturer->set_value(this->manufacturer_); - - 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 = // NOLINT(cppcoreguidelines-owning-memory) + new BLEService(uuid, num_handles, inst_id, advertise); + this->services_.emplace(BLEServer::get_service_key(uuid, inst_id), service); + if (this->parent_->is_active() && this->registered_) { + 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) { case ESP_GATTS_CONNECT_EVT: { ESP_LOGD(TAG, "BLE Client connected"); - this->add_client_(param->connect.conn_id, (void *) this); - this->connected_clients_++; - for (auto *component : this->service_components_) { - component->on_client_connect(); - } + this->add_client_(param->connect.conn_id); + this->emit_(BLEServerEvt::EmptyEvt::ON_CONNECT, param->connect.conn_id); break; } case ESP_GATTS_DISCONNECT_EVT: { ESP_LOGD(TAG, "BLE Client disconnected"); - if (this->remove_client_(param->disconnect.conn_id)) - this->connected_clients_--; + this->remove_client_(param->disconnect.conn_id); this->parent_->advertising_start(); - for (auto *component : this->service_components_) { - component->on_client_disconnect(); - } + this->emit_(BLEServerEvt::EmptyEvt::ON_DISCONNECT, param->disconnect.conn_id); break; } case ESP_GATTS_REG_EVT: { diff --git a/esphome/components/esp32_ble_server/ble_server.h b/esphome/components/esp32_ble_server/ble_server.h index e379e67296..43599438f3 100644 --- a/esphome/components/esp32_ble_server/ble_server.h +++ b/esphome/components/esp32_ble_server/ble_server.h @@ -4,36 +4,38 @@ #include "ble_characteristic.h" #include "esphome/components/esp32_ble/ble.h" -#include "esphome/components/esp32_ble/ble_advertising.h" #include "esphome/components/esp32_ble/ble_uuid.h" -#include "esphome/components/esp32_ble/queue.h" +#include "esphome/components/bytebuffer/bytebuffer.h" #include "esphome/core/component.h" #include "esphome/core/helpers.h" -#include "esphome/core/preferences.h" #include #include #include +#include #ifdef USE_ESP32 -#include #include namespace esphome { namespace esp32_ble_server { using namespace esp32_ble; +using namespace bytebuffer; -class BLEServiceComponent { - public: - virtual void on_client_connect(){}; - virtual void on_client_disconnect(){}; - virtual void start(); - virtual void stop(); +namespace BLEServerEvt { +enum EmptyEvt { + ON_CONNECT, + ON_DISCONNECT, }; +} // namespace BLEServerEvt -class BLEServer : public Component, public GATTsEventHandler, public BLEStatusEventHandler, public Parented { +class BLEServer : public Component, + public GATTsEventHandler, + public BLEStatusEventHandler, + public Parented, + public EventEmitter { public: void setup() override; void loop() override; @@ -44,47 +46,41 @@ class BLEServer : public Component, public GATTsEventHandler, public BLEStatusEv void teardown(); bool is_running(); - void set_manufacturer(const std::string &manufacturer) { this->manufacturer_ = manufacturer; } - void set_model(const std::string &model) { this->model_ = model; } void set_manufacturer_data(const std::vector &data) { this->manufacturer_data_ = data; 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); + void enqueue_start_service(BLEService *service) { this->services_to_start_.push_back(service); } + void set_device_information_service(BLEService *service) { this->device_information_service_ = service; } esp_gatt_if_t get_gatts_if() { return this->gatts_if_; } - uint32_t get_connected_client_count() { return this->connected_clients_; } - const std::unordered_map &get_clients() { return this->clients_; } + uint32_t get_connected_client_count() { return this->clients_.size(); } + const std::unordered_set &get_clients() { return this->clients_; } void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param) override; void ble_before_disabled_event_handler() override; - void register_service_component(BLEServiceComponent *component) { this->service_components_.push_back(component); } - protected: - bool create_device_characteristics_(); + static std::string get_service_key(ESPBTUUID uuid, uint8_t inst_id); void restart_advertising_(); - void add_client_(uint16_t conn_id, void *client) { this->clients_.emplace(conn_id, client); } - bool remove_client_(uint16_t conn_id) { return this->clients_.erase(conn_id) > 0; } + void add_client_(uint16_t conn_id) { this->clients_.insert(conn_id); } + void remove_client_(uint16_t conn_id) { this->clients_.erase(conn_id); } - std::string manufacturer_; - optional model_; - std::vector manufacturer_data_; + std::vector manufacturer_data_{}; esp_gatt_if_t gatts_if_{0}; bool registered_{false}; - uint32_t connected_clients_{0}; - std::unordered_map clients_; - std::unordered_map services_; - BLEService *device_information_service_; - - std::vector service_components_; + std::unordered_set clients_; + std::unordered_map services_{}; + std::vector services_to_start_{}; + BLEService *device_information_service_{}; enum State : uint8_t { INIT = 0x00, diff --git a/esphome/components/esp32_ble_server/ble_server_automations.cpp b/esphome/components/esp32_ble_server/ble_server_automations.cpp new file mode 100644 index 0000000000..41ef2b8bfe --- /dev/null +++ b/esphome/components/esp32_ble_server/ble_server_automations.cpp @@ -0,0 +1,77 @@ +#include "ble_server_automations.h" + +#ifdef USE_ESP32 + +namespace esphome { +namespace esp32_ble_server { +// Interface to interact with ESPHome automations and triggers +namespace esp32_ble_server_automations { + +using namespace esp32_ble; + +Trigger, uint16_t> *BLETriggers::create_characteristic_on_write_trigger( + BLECharacteristic *characteristic) { + Trigger, uint16_t> *on_write_trigger = // NOLINT(cppcoreguidelines-owning-memory) + new Trigger, uint16_t>(); + characteristic->EventEmitter, uint16_t>::on( + BLECharacteristicEvt::VectorEvt::ON_WRITE, + [on_write_trigger](const std::vector &data, uint16_t id) { on_write_trigger->trigger(data, id); }); + return on_write_trigger; +} + +Trigger, uint16_t> *BLETriggers::create_descriptor_on_write_trigger(BLEDescriptor *descriptor) { + Trigger, uint16_t> *on_write_trigger = // NOLINT(cppcoreguidelines-owning-memory) + new Trigger, uint16_t>(); + descriptor->on( + BLEDescriptorEvt::VectorEvt::ON_WRITE, + [on_write_trigger](const std::vector &data, uint16_t id) { on_write_trigger->trigger(data, id); }); + return on_write_trigger; +} + +Trigger *BLETriggers::create_server_on_connect_trigger(BLEServer *server) { + Trigger *on_connect_trigger = new Trigger(); // NOLINT(cppcoreguidelines-owning-memory) + server->on(BLEServerEvt::EmptyEvt::ON_CONNECT, + [on_connect_trigger](uint16_t conn_id) { on_connect_trigger->trigger(conn_id); }); + return on_connect_trigger; +} + +Trigger *BLETriggers::create_server_on_disconnect_trigger(BLEServer *server) { + Trigger *on_disconnect_trigger = new Trigger(); // NOLINT(cppcoreguidelines-owning-memory) + server->on(BLEServerEvt::EmptyEvt::ON_DISCONNECT, + [on_disconnect_trigger](uint16_t conn_id) { on_disconnect_trigger->trigger(conn_id); }); + return on_disconnect_trigger; +} + +void BLECharacteristicSetValueActionManager::set_listener(BLECharacteristic *characteristic, + EventEmitterListenerID listener_id, + const std::function &pre_notify_listener) { + // Check if there is already a listener for this characteristic + if (this->listeners_.count(characteristic) > 0) { + // Unpack the pair listener_id, pre_notify_listener_id + auto listener_pairs = this->listeners_[characteristic]; + EventEmitterListenerID old_listener_id = listener_pairs.first; + EventEmitterListenerID old_pre_notify_listener_id = listener_pairs.second; + // Remove the previous listener + characteristic->EventEmitter::off(BLECharacteristicEvt::EmptyEvt::ON_READ, + old_listener_id); + // Remove the pre-notify listener + this->off(BLECharacteristicSetValueActionEvt::PRE_NOTIFY, old_pre_notify_listener_id); + } + // Create a new listener for the pre-notify event + EventEmitterListenerID pre_notify_listener_id = + this->on(BLECharacteristicSetValueActionEvt::PRE_NOTIFY, + [pre_notify_listener, characteristic](const BLECharacteristic *evt_characteristic) { + // Only call the pre-notify listener if the characteristic is the one we are interested in + if (characteristic == evt_characteristic) { + pre_notify_listener(); + } + }); + // Save the pair listener_id, pre_notify_listener_id to the map + this->listeners_[characteristic] = std::make_pair(listener_id, pre_notify_listener_id); +} + +} // namespace esp32_ble_server_automations +} // namespace esp32_ble_server +} // namespace esphome + +#endif diff --git a/esphome/components/esp32_ble_server/ble_server_automations.h b/esphome/components/esp32_ble_server/ble_server_automations.h new file mode 100644 index 0000000000..eab6b05f05 --- /dev/null +++ b/esphome/components/esp32_ble_server/ble_server_automations.h @@ -0,0 +1,115 @@ +#pragma once + +#include "ble_server.h" +#include "ble_characteristic.h" +#include "ble_descriptor.h" + +#include "esphome/components/event_emitter/event_emitter.h" +#include "esphome/core/automation.h" + +#include +#include +#include + +#ifdef USE_ESP32 + +namespace esphome { +namespace esp32_ble_server { +// Interface to interact with ESPHome actions and triggers +namespace esp32_ble_server_automations { + +using namespace esp32_ble; +using namespace event_emitter; + +class BLETriggers { + public: + static Trigger, uint16_t> *create_characteristic_on_write_trigger( + BLECharacteristic *characteristic); + static Trigger, uint16_t> *create_descriptor_on_write_trigger(BLEDescriptor *descriptor); + static Trigger *create_server_on_connect_trigger(BLEServer *server); + static Trigger *create_server_on_disconnect_trigger(BLEServer *server); +}; + +enum BLECharacteristicSetValueActionEvt { + PRE_NOTIFY, +}; + +// Class to make sure only one BLECharacteristicSetValueAction is active at a time for each characteristic +class BLECharacteristicSetValueActionManager + : public EventEmitter { + public: + // Singleton pattern + static BLECharacteristicSetValueActionManager *get_instance() { + static BLECharacteristicSetValueActionManager instance; + return &instance; + } + void set_listener(BLECharacteristic *characteristic, EventEmitterListenerID listener_id, + const std::function &pre_notify_listener); + EventEmitterListenerID get_listener(BLECharacteristic *characteristic) { + return this->listeners_[characteristic].first; + } + void emit_pre_notify(BLECharacteristic *characteristic) { + this->emit_(BLECharacteristicSetValueActionEvt::PRE_NOTIFY, characteristic); + } + + private: + std::unordered_map> listeners_; +}; + +template class BLECharacteristicSetValueAction : public Action { + public: + BLECharacteristicSetValueAction(BLECharacteristic *characteristic) : parent_(characteristic) {} + TEMPLATABLE_VALUE(std::vector, buffer) + void set_buffer(ByteBuffer buffer) { this->set_buffer(buffer.get_data()); } + void play(Ts... x) override { + // If the listener is already set, do nothing + if (BLECharacteristicSetValueActionManager::get_instance()->get_listener(this->parent_) == this->listener_id_) + return; + // Set initial value + this->parent_->set_value(this->buffer_.value(x...)); + // Set the listener for read events + this->listener_id_ = this->parent_->EventEmitter::on( + BLECharacteristicEvt::EmptyEvt::ON_READ, [this, x...](uint16_t id) { + // Set the value of the characteristic every time it is read + this->parent_->set_value(this->buffer_.value(x...)); + }); + // Set the listener in the global manager so only one BLECharacteristicSetValueAction is set for each characteristic + BLECharacteristicSetValueActionManager::get_instance()->set_listener( + this->parent_, this->listener_id_, [this, x...]() { this->parent_->set_value(this->buffer_.value(x...)); }); + } + + protected: + BLECharacteristic *parent_; + EventEmitterListenerID listener_id_; +}; + +template class BLECharacteristicNotifyAction : public Action { + public: + BLECharacteristicNotifyAction(BLECharacteristic *characteristic) : parent_(characteristic) {} + void play(Ts... x) override { + // Call the pre-notify event + BLECharacteristicSetValueActionManager::get_instance()->emit_pre_notify(this->parent_); + // Notify the characteristic + this->parent_->notify(); + } + + protected: + BLECharacteristic *parent_; +}; + +template class BLEDescriptorSetValueAction : public Action { + public: + BLEDescriptorSetValueAction(BLEDescriptor *descriptor) : parent_(descriptor) {} + TEMPLATABLE_VALUE(std::vector, buffer) + void set_buffer(ByteBuffer buffer) { this->set_buffer(buffer.get_data()); } + void play(Ts... x) override { this->parent_->set_value(this->buffer_.value(x...)); } + + protected: + BLEDescriptor *parent_; +}; + +} // namespace esp32_ble_server_automations +} // namespace esp32_ble_server +} // namespace esphome + +#endif diff --git a/esphome/components/esp32_ble_server/ble_service.cpp b/esphome/components/esp32_ble_server/ble_service.cpp index 368f03fb52..96fedf2346 100644 --- a/esphome/components/esp32_ble_server/ble_service.cpp +++ b/esphome/components/esp32_ble_server/ble_service.cpp @@ -52,18 +52,21 @@ void BLEService::do_create(BLEServer *server) { esp_err_t err = esp_ble_gatts_create_service(server->get_gatts_if(), &srvc_id, this->num_handles_); if (err != ESP_OK) { ESP_LOGE(TAG, "esp_ble_gatts_create_service failed: %d", err); - this->init_state_ = FAILED; + this->state_ = FAILED; return; } - this->init_state_ = CREATING; + this->state_ = CREATING; } void BLEService::do_delete() { - if (this->init_state_ == DELETING || this->init_state_ == DELETED) + if (this->state_ == DELETING || this->state_ == DELETED) return; - this->init_state_ = DELETING; + this->state_ = DELETING; this->created_characteristic_count_ = 0; this->last_created_characteristic_ = nullptr; + // Call all characteristics to delete + for (auto *characteristic : this->characteristics_) + characteristic->do_delete(); this->stop_(); esp_err_t err = esp_ble_gatts_delete_service(this->handle_); if (err != ESP_OK) { @@ -91,6 +94,7 @@ void BLEService::start() { return; should_start_ = true; + this->state_ = STARTING; esp_err_t err = esp_ble_gatts_start_service(this->handle_); if (err != ESP_OK) { ESP_LOGE(TAG, "esp_ble_gatts_start_service failed: %d", err); @@ -98,7 +102,6 @@ void BLEService::start() { } if (this->advertise_) esp32_ble::global_ble->advertising_add_service_uuid(this->uuid_); - this->running_state_ = STARTING; } void BLEService::stop() { @@ -107,9 +110,9 @@ void BLEService::stop() { } void BLEService::stop_() { - if (this->running_state_ == STOPPING || this->running_state_ == STOPPED) + if (this->state_ == STOPPING || this->state_ == STOPPED) return; - this->running_state_ = STOPPING; + this->state_ = STOPPING; esp_err_t err = esp_ble_gatts_stop_service(this->handle_); if (err != ESP_OK) { ESP_LOGE(TAG, "esp_ble_gatts_stop_service failed: %d", err); @@ -119,17 +122,16 @@ void BLEService::stop_() { esp32_ble::global_ble->advertising_remove_service_uuid(this->uuid_); } -bool BLEService::is_created() { return this->init_state_ == CREATED; } bool BLEService::is_failed() { - if (this->init_state_ == FAILED) + if (this->state_ == FAILED) return true; bool failed = false; for (auto *characteristic : this->characteristics_) failed |= characteristic->is_failed(); if (failed) - this->init_state_ = FAILED; - return this->init_state_ == FAILED; + this->state_ = FAILED; + return this->state_ == FAILED; } void BLEService::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, @@ -139,7 +141,7 @@ void BLEService::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t g if (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->init_state_ = CREATED; + this->state_ = CREATED; if (this->should_start_) this->start(); } @@ -147,18 +149,18 @@ void BLEService::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t g } case ESP_GATTS_DELETE_EVT: if (param->del.service_handle == this->handle_) { - this->init_state_ = DELETED; + this->state_ = DELETED; } break; case ESP_GATTS_START_EVT: { if (param->start.service_handle == this->handle_) { - this->running_state_ = RUNNING; + this->state_ = RUNNING; } break; } case ESP_GATTS_STOP_EVT: { if (param->start.service_handle == this->handle_) { - this->running_state_ = STOPPED; + this->state_ = STOPPED; } break; } diff --git a/esphome/components/esp32_ble_server/ble_service.h b/esphome/components/esp32_ble_server/ble_service.h index 5e5883b6bf..dcfad5f501 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_; } @@ -44,18 +45,17 @@ class BLEService { void start(); void stop(); - bool is_created(); bool is_failed(); - - bool is_running() { return this->running_state_ == RUNNING; } - bool is_starting() { return this->running_state_ == STARTING; } - bool is_deleted() { return this->init_state_ == DELETED; } + bool is_created() { return this->state_ == CREATED; } + bool is_running() { return this->state_ == RUNNING; } + bool is_starting() { return this->state_ == STARTING; } + bool is_deleted() { return this->state_ == DELETED; } protected: std::vector characteristics_; BLECharacteristic *last_created_characteristic_{nullptr}; uint32_t created_characteristic_count_{0}; - BLEServer *server_; + BLEServer *server_ = nullptr; ESPBTUUID uuid_; uint16_t num_handles_; uint16_t handle_{0xFFFF}; @@ -66,22 +66,18 @@ class BLEService { bool do_create_characteristics_(); void stop_(); - enum InitState : uint8_t { + enum State : uint8_t { FAILED = 0x00, INIT, CREATING, - CREATING_DEPENDENTS, CREATED, - DELETING, - DELETED, - } init_state_{INIT}; - - enum RunningState : uint8_t { STARTING, RUNNING, STOPPING, STOPPED, - } running_state_{STOPPED}; + DELETING, + DELETED, + } state_{INIT}; }; } // namespace esp32_ble_server diff --git a/esphome/components/esp32_ble_tracker/__init__.py b/esphome/components/esp32_ble_tracker/__init__.py index 0aa8eadd0a..e762c89b94 100644 --- a/esphome/components/esp32_ble_tracker/__init__.py +++ b/esphome/components/esp32_ble_tracker/__init__.py @@ -1,9 +1,13 @@ -import re - from esphome import automation import esphome.codegen as cg from esphome.components import esp32_ble from esphome.components.esp32 import add_idf_sdkconfig_option +from esphome.components.esp32_ble import ( + bt_uuid, + bt_uuid16_format, + bt_uuid32_format, + bt_uuid128_format, +) import esphome.config_validation as cv from esphome.const import ( CONF_ACTIVE, @@ -86,43 +90,6 @@ def validate_scan_parameters(config): return config -bt_uuid16_format = "XXXX" -bt_uuid32_format = "XXXXXXXX" -bt_uuid128_format = "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" - - -def bt_uuid(value): - in_value = cv.string_strict(value) - value = in_value.upper() - - if len(value) == len(bt_uuid16_format): - pattern = re.compile("^[A-F|0-9]{4,}$") - if not pattern.match(value): - raise cv.Invalid( - f"Invalid hexadecimal value for 16 bit UUID format: '{in_value}'" - ) - return value - if len(value) == len(bt_uuid32_format): - pattern = re.compile("^[A-F|0-9]{8,}$") - if not pattern.match(value): - raise cv.Invalid( - f"Invalid hexadecimal value for 32 bit UUID format: '{in_value}'" - ) - return value - if len(value) == len(bt_uuid128_format): - pattern = re.compile( - "^[A-F|0-9]{8,}-[A-F|0-9]{4,}-[A-F|0-9]{4,}-[A-F|0-9]{4,}-[A-F|0-9]{12,}$" - ) - if not pattern.match(value): - raise cv.Invalid( - f"Invalid hexadecimal value for 128 UUID format: '{in_value}'" - ) - return value - raise cv.Invalid( - f"Service UUID must be in 16 bit '{bt_uuid16_format}', 32 bit '{bt_uuid32_format}', or 128 bit '{bt_uuid128_format}' format" - ) - - def as_hex(value): return cg.RawExpression(f"0x{value}ULL") diff --git a/esphome/components/esp32_improv/__init__.py b/esphome/components/esp32_improv/__init__.py index ecc07d4c91..ca39c1cd36 100644 --- a/esphome/components/esp32_improv/__init__.py +++ b/esphome/components/esp32_improv/__init__.py @@ -1,6 +1,6 @@ from esphome import automation import esphome.codegen as cg -from esphome.components import binary_sensor, esp32_ble_server, output +from esphome.components import binary_sensor, output import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_ON_STATE, CONF_TRIGGER_ID @@ -24,9 +24,7 @@ Error = improv_ns.enum("Error") State = improv_ns.enum("State") esp32_improv_ns = cg.esphome_ns.namespace("esp32_improv") -ESP32ImprovComponent = esp32_improv_ns.class_( - "ESP32ImprovComponent", cg.Component, esp32_ble_server.BLEServiceComponent -) +ESP32ImprovComponent = esp32_improv_ns.class_("ESP32ImprovComponent", cg.Component) ESP32ImprovProvisionedTrigger = esp32_improv_ns.class_( "ESP32ImprovProvisionedTrigger", automation.Trigger.template() ) @@ -47,7 +45,6 @@ ESP32ImprovStoppedTrigger = esp32_improv_ns.class_( CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(ESP32ImprovComponent), - cv.GenerateID(CONF_BLE_SERVER_ID): cv.use_id(esp32_ble_server.BLEServer), cv.Required(CONF_AUTHORIZER): cv.Any( cv.none, cv.use_id(binary_sensor.BinarySensor) ), @@ -100,9 +97,6 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) - ble_server = await cg.get_variable(config[CONF_BLE_SERVER_ID]) - cg.add(ble_server.register_service_component(var)) - cg.add_define("USE_IMPROV") cg.add_library("improv/Improv", "1.2.4") diff --git a/esphome/components/esp32_improv/esp32_improv_component.cpp b/esphome/components/esp32_improv/esp32_improv_component.cpp index c67431077c..b720425506 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.cpp +++ b/esphome/components/esp32_improv/esp32_improv_component.cpp @@ -4,12 +4,15 @@ #include "esphome/components/esp32_ble_server/ble_2902.h" #include "esphome/core/application.h" #include "esphome/core/log.h" +#include "esphome/components/bytebuffer/bytebuffer.h" #ifdef USE_ESP32 namespace esphome { namespace esp32_improv { +using namespace bytebuffer; + static const char *const TAG = "esp32_improv.component"; static const char *const ESPHOME_MY_LINK = "https://my.home-assistant.io/redirect/config_flow_start?domain=esphome"; @@ -26,6 +29,8 @@ void ESP32ImprovComponent::setup() { }); } #endif + global_ble_server->on(BLEServerEvt::EmptyEvt::ON_DISCONNECT, + [this](uint16_t conn_id) { this->set_error_(improv::ERROR_NONE); }); } void ESP32ImprovComponent::setup_characteristics() { @@ -40,11 +45,12 @@ void ESP32ImprovComponent::setup_characteristics() { this->error_->add_descriptor(error_descriptor); this->rpc_ = this->service_->create_characteristic(improv::RPC_COMMAND_UUID, BLECharacteristic::PROPERTY_WRITE); - this->rpc_->on_write([this](const std::vector &data) { - if (!data.empty()) { - this->incoming_data_.insert(this->incoming_data_.end(), data.begin(), data.end()); - } - }); + this->rpc_->EventEmitter, uint16_t>::on( + BLECharacteristicEvt::VectorEvt::ON_WRITE, [this](const std::vector &data, uint16_t id) { + if (!data.empty()) { + this->incoming_data_.insert(this->incoming_data_.end(), data.begin(), data.end()); + } + }); BLEDescriptor *rpc_descriptor = new BLE2902(); this->rpc_->add_descriptor(rpc_descriptor); @@ -62,7 +68,7 @@ void ESP32ImprovComponent::setup_characteristics() { if (this->status_indicator_ != nullptr) capabilities |= improv::CAPABILITY_IDENTIFY; #endif - this->capabilities_->set_value(capabilities); + this->capabilities_->set_value(ByteBuffer::wrap(capabilities)); this->setup_complete_ = true; } @@ -80,8 +86,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(); } @@ -93,15 +98,15 @@ void ESP32ImprovComponent::loop() { case improv::STATE_STOPPED: this->set_status_indicator_state_(false); - if (this->service_->is_created() && this->should_start_ && this->setup_complete_) { - if (this->service_->is_running()) { + if (this->should_start_ && this->setup_complete_) { + if (this->service_->is_created()) { + this->service_->start(); + } else if (this->service_->is_running()) { esp32_ble::global_ble->advertising_start(); this->set_state_(improv::STATE_AWAITING_AUTHORIZATION); this->set_error_(improv::ERROR_NONE); ESP_LOGD(TAG, "Service started!"); - } else { - this->service_->start(); } } break; @@ -199,8 +204,7 @@ void ESP32ImprovComponent::set_state_(improv::State state) { ESP_LOGV(TAG, "Setting state: %d", state); this->state_ = state; if (this->status_->get_value().empty() || this->status_->get_value()[0] != state) { - uint8_t data[1]{state}; - this->status_->set_value(data, 1); + this->status_->set_value(ByteBuffer::wrap(static_cast(state))); if (state != improv::STATE_STOPPED) this->status_->notify(); } @@ -232,15 +236,14 @@ void ESP32ImprovComponent::set_error_(improv::Error error) { ESP_LOGE(TAG, "Error: %d", error); } if (this->error_->get_value().empty() || this->error_->get_value()[0] != error) { - uint8_t data[1]{error}; - this->error_->set_value(data, 1); + this->error_->set_value(ByteBuffer::wrap(static_cast(error))); if (this->state_ != improv::STATE_STOPPED) this->error_->notify(); } } void ESP32ImprovComponent::send_response_(std::vector &response) { - this->rpc_response_->set_value(response); + this->rpc_response_->set_value(ByteBuffer::wrap(response)); if (this->state_ != improv::STATE_STOPPED) this->rpc_response_->notify(); } @@ -339,8 +342,6 @@ void ESP32ImprovComponent::on_wifi_connect_timeout_() { wifi::global_wifi_component->clear_sta(); } -void ESP32ImprovComponent::on_client_disconnect() { this->set_error_(improv::ERROR_NONE); }; - ESP32ImprovComponent *global_improv_component = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) } // namespace esp32_improv diff --git a/esphome/components/esp32_improv/esp32_improv_component.h b/esphome/components/esp32_improv/esp32_improv_component.h index 062b3f585b..87cec23876 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.h +++ b/esphome/components/esp32_improv/esp32_improv_component.h @@ -32,18 +32,17 @@ namespace esp32_improv { using namespace esp32_ble_server; -class ESP32ImprovComponent : public Component, public BLEServiceComponent { +class ESP32ImprovComponent : public Component { public: ESP32ImprovComponent(); void dump_config() override; void loop() override; void setup() override; void setup_characteristics(); - void on_client_disconnect() override; float get_setup_priority() const override; - void start() override; - void stop() override; + void start(); + void stop(); bool is_active() const { return this->state_ != improv::STATE_STOPPED; } #ifdef USE_ESP32_IMPROV_STATE_CALLBACK diff --git a/esphome/components/event_emitter/__init__.py b/esphome/components/event_emitter/__init__.py new file mode 100644 index 0000000000..fcbbf26f02 --- /dev/null +++ b/esphome/components/event_emitter/__init__.py @@ -0,0 +1,5 @@ +CODEOWNERS = ["@Rapsssito"] + +# Allows event_emitter to be configured in yaml, to allow use of the C++ api. + +CONFIG_SCHEMA = {} diff --git a/esphome/components/event_emitter/event_emitter.cpp b/esphome/components/event_emitter/event_emitter.cpp new file mode 100644 index 0000000000..8487e19c2f --- /dev/null +++ b/esphome/components/event_emitter/event_emitter.cpp @@ -0,0 +1,14 @@ +#include "event_emitter.h" + +namespace esphome { +namespace event_emitter { + +static const char *const TAG = "event_emitter"; + +void raise_event_emitter_full_error() { + ESP_LOGE(TAG, "EventEmitter has reached the maximum number of listeners for event"); + ESP_LOGW(TAG, "Removing listener to make space for new listener"); +} + +} // namespace event_emitter +} // namespace esphome diff --git a/esphome/components/event_emitter/event_emitter.h b/esphome/components/event_emitter/event_emitter.h new file mode 100644 index 0000000000..3876a2cc14 --- /dev/null +++ b/esphome/components/event_emitter/event_emitter.h @@ -0,0 +1,63 @@ +#pragma once +#include +#include +#include +#include + +#include "esphome/core/log.h" + +namespace esphome { +namespace event_emitter { + +using EventEmitterListenerID = uint32_t; +void raise_event_emitter_full_error(); + +// EventEmitter class that can emit events with a specific name (it is highly recommended to use an enum class for this) +// and a list of arguments. Supports multiple listeners for each event. +template class EventEmitter { + public: + EventEmitterListenerID on(EvtType event, std::function listener) { + EventEmitterListenerID listener_id = get_next_id_(event); + listeners_[event][listener_id] = listener; + return listener_id; + } + + void off(EvtType event, EventEmitterListenerID id) { + if (listeners_.count(event) == 0) + return; + listeners_[event].erase(id); + } + + protected: + void emit_(EvtType event, Args... args) { + if (listeners_.count(event) == 0) + return; + for (const auto &listener : listeners_[event]) { + listener.second(args...); + } + } + + EventEmitterListenerID get_next_id_(EvtType event) { + // Check if the map is full + if (listeners_[event].size() == std::numeric_limits::max()) { + // Raise an error if the map is full + raise_event_emitter_full_error(); + off(event, 0); + return 0; + } + // Get the next ID for the given event. + EventEmitterListenerID next_id = (current_id_ + 1) % std::numeric_limits::max(); + while (listeners_[event].count(next_id) > 0) { + next_id = (next_id + 1) % std::numeric_limits::max(); + } + current_id_ = next_id; + return current_id_; + } + + private: + std::unordered_map>> listeners_; + EventEmitterListenerID current_id_ = 0; +}; + +} // namespace event_emitter +} // namespace esphome diff --git a/esphome/components/mdns/__init__.py b/esphome/components/mdns/__init__.py index dd68fbb93c..1bc290b582 100644 --- a/esphome/components/mdns/__init__.py +++ b/esphome/components/mdns/__init__.py @@ -91,7 +91,7 @@ async def to_code(config): add_idf_component( name="mdns", repo="https://github.com/espressif/esp-protocols.git", - ref="mdns-v1.3.2", + ref="mdns-v1.5.1", path="components/mdns", ) diff --git a/esphome/const.py b/esphome/const.py index 95bf6afc02..ab41d8cbc2 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -530,6 +530,7 @@ CONF_NETWORKS = "networks" CONF_NEW_PASSWORD = "new_password" CONF_NITROGEN_DIOXIDE = "nitrogen_dioxide" CONF_NOISE_LEVEL = "noise_level" +CONF_NOTIFY = "notify" CONF_NUM_ATTEMPTS = "num_attempts" CONF_NUM_CHANNELS = "num_channels" CONF_NUM_CHIPS = "num_chips" diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index c79ba1b0ed..bd5bcda2fe 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -7,7 +7,7 @@ dependencies: version: v2.0.9 mdns: git: https://github.com/espressif/esp-protocols.git - version: mdns-v1.3.2 + version: mdns-v1.5.1 path: components/mdns rules: - if: "idf_version >=5.0" diff --git a/tests/components/esp32_ble_server/common.yaml b/tests/components/esp32_ble_server/common.yaml index 29a5407f84..696f4ea8fe 100644 --- a/tests/components/esp32_ble_server/common.yaml +++ b/tests/components/esp32_ble_server/common.yaml @@ -1,3 +1,66 @@ esp32_ble_server: - id: ble + id: ble_server manufacturer_data: [0x72, 0x4, 0x00, 0x23] + manufacturer: ESPHome + model: Test + on_connect: + - lambda: |- + ESP_LOGD("BLE", "Connection from %d", id); + on_disconnect: + - lambda: |- + ESP_LOGD("BLE", "Disconnection from %d", id); + services: + - uuid: 2a24b789-7aab-4535-af3e-ee76a35cc12d + advertise: false + characteristics: + - id: test_notify_characteristic + description: "Notify characteristic" + uuid: cad48e28-7fbe-41cf-bae9-d77a6c233423 + read: true + notify: true + value: [1, 2, 3, 4] + descriptors: + - uuid: cad48e28-7fbe-41cf-bae9-d77a6c111111 + on_write: + logger.log: + format: "Descriptor write id %u, data %s" + args: [id, 'format_hex_pretty(x.data(), x.size()).c_str()'] + value: + data: "123.1" + type: float + endianness: BIG + - uuid: 2a24b789-7a1b-4535-af3e-ee76a35cc42d + advertise: false + characteristics: + - id: test_change_characteristic + uuid: 2a24b789-7a1b-4535-af3e-ee76a35cc11c + read: true + value: + data: "Initial" + string_encoding: utf-8 + description: Change characteristic + descriptors: + - uuid: 0x4414 + id: test_change_descriptor + value: "Initial descriptor value" + - uuid: 0x2312 + value: + data: 0x12 + type: uint16_t + on_write: + - lambda: |- + ESP_LOGD("BLE", "Descriptor received: %s from %d", std::string(x.begin(), x.end()).c_str(), id); + - uuid: 2a24b789-7a1b-4535-af3e-ee76a35cc99a + write: true + on_write: + then: + - lambda: |- + ESP_LOGD("BLE", "Characteristic received: %s from %d", std::string(x.begin(), x.end()).c_str(), id); + - ble_server.characteristic.set_value: + id: test_change_characteristic + value: !lambda 'return bytebuffer::ByteBuffer::wrap({0x00, 0x01, 0x02}).get_data();' + - ble_server.characteristic.notify: + id: test_notify_characteristic + - ble_server.descriptor.set_value: + id: test_change_descriptor + value: !lambda return bytebuffer::ByteBuffer::wrap({0x03, 0x04, 0x05}).get_data();