mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 07:03:55 +00:00 
			
		
		
		
	[esp32_ble_server] Create custom services, characteristics and descriptors (#7009)
Co-authored-by: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
This commit is contained in:
		| @@ -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 | ||||
|   | ||||
| @@ -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" | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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: | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
| @@ -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 | ||||
| @@ -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<uint8_t> value) { | ||||
| void BLECharacteristic::set_value(ByteBuffer buffer) { this->set_value(buffer.get_data()); } | ||||
|  | ||||
| void BLECharacteristic::set_value(const std::vector<uint8_t> &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<uint8_t>(value.begin(), value.end())); | ||||
| } | ||||
| void BLECharacteristic::set_value(const uint8_t *data, size_t length) { | ||||
|   this->set_value(std::vector<uint8_t>(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<uint8_t>(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<uint8_t> &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<BLECharacteristicEvt::EmptyEvt, uint16_t>::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<BLECharacteristicEvt::VectorEvt, std::vector<uint8_t>, 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<BLECharacteristicEvt::VectorEvt, std::vector<uint8_t>, 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); | ||||
|   | ||||
| @@ -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 <vector> | ||||
| #include <unordered_map> | ||||
|  | ||||
| #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<BLECharacteristicEvt::VectorEvt, std::vector<uint8_t>, uint16_t>, | ||||
|                           public EventEmitter<BLECharacteristicEvt::EmptyEvt, uint16_t> { | ||||
|  public: | ||||
|   BLECharacteristic(ESPBTUUID uuid, uint32_t properties); | ||||
|   ~BLECharacteristic(); | ||||
|  | ||||
|   void set_value(const uint8_t *data, size_t length); | ||||
|   void set_value(std::vector<uint8_t> 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<uint8_t> &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<void(const std::vector<uint8_t> &)> &&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<BLEDescriptor *> descriptors_; | ||||
|  | ||||
|   std::function<void(const std::vector<uint8_t> &)> on_write_; | ||||
|   std::unordered_map<uint16_t, bool> clients_to_notify_; | ||||
|  | ||||
|   esp_gatt_perm_t permissions_ = ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE; | ||||
|  | ||||
|   | ||||
| @@ -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<uint8_t> 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<uint8_t> 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<uint8_t>(param->write.value, param->write.value + param->write.len), | ||||
|                   param->write.conn_id); | ||||
|       break; | ||||
|     } | ||||
|     default: | ||||
|   | ||||
| @@ -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<BLEDescriptorEvt::VectorEvt, std::vector<uint8_t>, 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<uint8_t> 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, | ||||
|   | ||||
| @@ -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: { | ||||
|   | ||||
| @@ -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 <memory> | ||||
| #include <vector> | ||||
| #include <unordered_map> | ||||
| #include <unordered_set> | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
|  | ||||
| #include <esp_gap_ble_api.h> | ||||
| #include <esp_gatts_api.h> | ||||
|  | ||||
| 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<ESP32BLE> { | ||||
| class BLEServer : public Component, | ||||
|                   public GATTsEventHandler, | ||||
|                   public BLEStatusEventHandler, | ||||
|                   public Parented<ESP32BLE>, | ||||
|                   public EventEmitter<BLEServerEvt::EmptyEvt, uint16_t> { | ||||
|  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<uint8_t> &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<uint16_t, void *> &get_clients() { return this->clients_; } | ||||
|   uint32_t get_connected_client_count() { return this->clients_.size(); } | ||||
|   const std::unordered_set<uint16_t> &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<std::string> model_; | ||||
|   std::vector<uint8_t> manufacturer_data_; | ||||
|   std::vector<uint8_t> manufacturer_data_{}; | ||||
|   esp_gatt_if_t gatts_if_{0}; | ||||
|   bool registered_{false}; | ||||
|  | ||||
|   uint32_t connected_clients_{0}; | ||||
|   std::unordered_map<uint16_t, void *> clients_; | ||||
|   std::unordered_map<std::string, BLEService *> services_; | ||||
|   BLEService *device_information_service_; | ||||
|  | ||||
|   std::vector<BLEServiceComponent *> service_components_; | ||||
|   std::unordered_set<uint16_t> clients_; | ||||
|   std::unordered_map<std::string, BLEService *> services_{}; | ||||
|   std::vector<BLEService *> services_to_start_{}; | ||||
|   BLEService *device_information_service_{}; | ||||
|  | ||||
|   enum State : uint8_t { | ||||
|     INIT = 0x00, | ||||
|   | ||||
| @@ -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<std::vector<uint8_t>, uint16_t> *BLETriggers::create_characteristic_on_write_trigger( | ||||
|     BLECharacteristic *characteristic) { | ||||
|   Trigger<std::vector<uint8_t>, uint16_t> *on_write_trigger =  // NOLINT(cppcoreguidelines-owning-memory) | ||||
|       new Trigger<std::vector<uint8_t>, uint16_t>(); | ||||
|   characteristic->EventEmitter<BLECharacteristicEvt::VectorEvt, std::vector<uint8_t>, uint16_t>::on( | ||||
|       BLECharacteristicEvt::VectorEvt::ON_WRITE, | ||||
|       [on_write_trigger](const std::vector<uint8_t> &data, uint16_t id) { on_write_trigger->trigger(data, id); }); | ||||
|   return on_write_trigger; | ||||
| } | ||||
|  | ||||
| Trigger<std::vector<uint8_t>, uint16_t> *BLETriggers::create_descriptor_on_write_trigger(BLEDescriptor *descriptor) { | ||||
|   Trigger<std::vector<uint8_t>, uint16_t> *on_write_trigger =  // NOLINT(cppcoreguidelines-owning-memory) | ||||
|       new Trigger<std::vector<uint8_t>, uint16_t>(); | ||||
|   descriptor->on( | ||||
|       BLEDescriptorEvt::VectorEvt::ON_WRITE, | ||||
|       [on_write_trigger](const std::vector<uint8_t> &data, uint16_t id) { on_write_trigger->trigger(data, id); }); | ||||
|   return on_write_trigger; | ||||
| } | ||||
|  | ||||
| Trigger<uint16_t> *BLETriggers::create_server_on_connect_trigger(BLEServer *server) { | ||||
|   Trigger<uint16_t> *on_connect_trigger = new Trigger<uint16_t>();  // 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<uint16_t> *BLETriggers::create_server_on_disconnect_trigger(BLEServer *server) { | ||||
|   Trigger<uint16_t> *on_disconnect_trigger = new Trigger<uint16_t>();  // 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<void()> &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<BLECharacteristicEvt::EmptyEvt, uint16_t>::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 | ||||
							
								
								
									
										115
									
								
								esphome/components/esp32_ble_server/ble_server_automations.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										115
									
								
								esphome/components/esp32_ble_server/ble_server_automations.h
									
									
									
									
									
										Normal file
									
								
							| @@ -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 <vector> | ||||
| #include <unordered_map> | ||||
| #include <functional> | ||||
|  | ||||
| #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<std::vector<uint8_t>, uint16_t> *create_characteristic_on_write_trigger( | ||||
|       BLECharacteristic *characteristic); | ||||
|   static Trigger<std::vector<uint8_t>, uint16_t> *create_descriptor_on_write_trigger(BLEDescriptor *descriptor); | ||||
|   static Trigger<uint16_t> *create_server_on_connect_trigger(BLEServer *server); | ||||
|   static Trigger<uint16_t> *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<BLECharacteristicSetValueActionEvt, BLECharacteristic *> { | ||||
|  public: | ||||
|   // Singleton pattern | ||||
|   static BLECharacteristicSetValueActionManager *get_instance() { | ||||
|     static BLECharacteristicSetValueActionManager instance; | ||||
|     return &instance; | ||||
|   } | ||||
|   void set_listener(BLECharacteristic *characteristic, EventEmitterListenerID listener_id, | ||||
|                     const std::function<void()> &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<BLECharacteristic *, std::pair<EventEmitterListenerID, EventEmitterListenerID>> listeners_; | ||||
| }; | ||||
|  | ||||
| template<typename... Ts> class BLECharacteristicSetValueAction : public Action<Ts...> { | ||||
|  public: | ||||
|   BLECharacteristicSetValueAction(BLECharacteristic *characteristic) : parent_(characteristic) {} | ||||
|   TEMPLATABLE_VALUE(std::vector<uint8_t>, 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<BLECharacteristicEvt::EmptyEvt, uint16_t>::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<typename... Ts> class BLECharacteristicNotifyAction : public Action<Ts...> { | ||||
|  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<typename... Ts> class BLEDescriptorSetValueAction : public Action<Ts...> { | ||||
|  public: | ||||
|   BLEDescriptorSetValueAction(BLEDescriptor *descriptor) : parent_(descriptor) {} | ||||
|   TEMPLATABLE_VALUE(std::vector<uint8_t>, 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 | ||||
| @@ -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; | ||||
|     } | ||||
|   | ||||
| @@ -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<BLECharacteristic *> 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 | ||||
|   | ||||
| @@ -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") | ||||
|  | ||||
|   | ||||
| @@ -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") | ||||
|  | ||||
|   | ||||
| @@ -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<uint8_t> &data) { | ||||
|     if (!data.empty()) { | ||||
|       this->incoming_data_.insert(this->incoming_data_.end(), data.begin(), data.end()); | ||||
|     } | ||||
|   }); | ||||
|   this->rpc_->EventEmitter<BLECharacteristicEvt::VectorEvt, std::vector<uint8_t>, uint16_t>::on( | ||||
|       BLECharacteristicEvt::VectorEvt::ON_WRITE, [this](const std::vector<uint8_t> &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<uint8_t>(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<uint8_t>(error))); | ||||
|     if (this->state_ != improv::STATE_STOPPED) | ||||
|       this->error_->notify(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void ESP32ImprovComponent::send_response_(std::vector<uint8_t> &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 | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
							
								
								
									
										5
									
								
								esphome/components/event_emitter/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								esphome/components/event_emitter/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| CODEOWNERS = ["@Rapsssito"] | ||||
|  | ||||
| # Allows event_emitter to be configured in yaml, to allow use of the C++ api. | ||||
|  | ||||
| CONFIG_SCHEMA = {} | ||||
							
								
								
									
										14
									
								
								esphome/components/event_emitter/event_emitter.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								esphome/components/event_emitter/event_emitter.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
							
								
								
									
										63
									
								
								esphome/components/event_emitter/event_emitter.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								esphome/components/event_emitter/event_emitter.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,63 @@ | ||||
| #pragma once | ||||
| #include <unordered_map> | ||||
| #include <vector> | ||||
| #include <functional> | ||||
| #include <limits> | ||||
|  | ||||
| #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<typename EvtType, typename... Args> class EventEmitter { | ||||
|  public: | ||||
|   EventEmitterListenerID on(EvtType event, std::function<void(Args...)> 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<EventEmitterListenerID>::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<EventEmitterListenerID>::max(); | ||||
|     while (listeners_[event].count(next_id) > 0) { | ||||
|       next_id = (next_id + 1) % std::numeric_limits<EventEmitterListenerID>::max(); | ||||
|     } | ||||
|     current_id_ = next_id; | ||||
|     return current_id_; | ||||
|   } | ||||
|  | ||||
|  private: | ||||
|   std::unordered_map<EvtType, std::unordered_map<EventEmitterListenerID, std::function<void(Args...)>>> listeners_; | ||||
|   EventEmitterListenerID current_id_ = 0; | ||||
| }; | ||||
|  | ||||
| }  // namespace event_emitter | ||||
| }  // namespace esphome | ||||
| @@ -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" | ||||
|   | ||||
| @@ -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(); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user