From e7750250e068175389f9f0df880281c1e16a5ee4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 29 Sep 2025 17:55:26 -0500 Subject: [PATCH] wip --- .../components/esp32_ble_server/__init__.py | 194 ++++++++++++++++++ .../esp32_ble_server/ble_characteristic.cpp | 2 +- .../esp32_ble_server/ble_characteristic.h | 2 +- .../esp32_ble_server/ble_descriptor.h | 2 +- esphome/components/esp32_improv/__init__.py | 11 +- 5 files changed, 207 insertions(+), 4 deletions(-) diff --git a/esphome/components/esp32_ble_server/__init__.py b/esphome/components/esp32_ble_server/__init__.py index 7bd3a0e585..f8f913b8e9 100644 --- a/esphome/components/esp32_ble_server/__init__.py +++ b/esphome/components/esp32_ble_server/__init__.py @@ -591,6 +591,12 @@ async def to_code_characteristic(service_var, char_conf): parse_properties(char_conf), ), ) + + # If this characteristic has notify or indicate, it will get a CCCD descriptor (0x2902) + # and ble_characteristic.cpp will register a listener for it + if char_conf.get(CONF_NOTIFY, False) or char_conf.get(CONF_INDICATE, False): + allocate_descriptor_event_listener(0x2902, "WRITE", "esp32_ble_server", 1) + if CONF_ON_WRITE in char_conf: on_write_conf = char_conf[CONF_ON_WRITE] cg.add_define("USE_ESP32_BLE_SERVER_CHARACTERISTIC_ON_WRITE") @@ -670,6 +676,194 @@ async def to_code(config): [(cg.uint16, "id")], config[CONF_ON_DISCONNECT], ) + + # Generate defines for BLECharacteristicSetValueActionManager + set_value_action_count = sum( + count for comp, count in _LISTENER_ALLOCATIONS["server_connect"] + ) + if set_value_action_count > 0: + cg.add_define("BLE_SET_VALUE_ACTION_MAX_LISTENERS", str(set_value_action_count)) + + # Generate defines and specialized classes for server events + server_connect_count = sum( + count for comp, count in _LISTENER_ALLOCATIONS["server_connect"] + ) + server_disconnect_count = sum( + count for comp, count in _LISTENER_ALLOCATIONS["server_disconnect"] + ) + + if server_connect_count > 0 or server_disconnect_count > 0: + # Generate the specialized BLEServer class with EventEmitter support + cg.add_define( + "BLE_SERVER_CONNECT_MAX_LISTENERS", str(max(server_connect_count, 1)) + ) + cg.add_define( + "BLE_SERVER_DISCONNECT_MAX_LISTENERS", + str(max(server_disconnect_count, 1)), + ) + # TODO: Generate specialized BLEServer class with EventEmitter mixins + + # Generate defines and specialized classes for characteristics + # Group allocations by UUID + char_write_by_uuid: dict[int | str, int] = {} + for comp, uuid, count in _LISTENER_ALLOCATIONS["characteristic_write"]: + char_write_by_uuid[uuid] = char_write_by_uuid.get(uuid, 0) + count + + char_read_by_uuid: dict[int | str, int] = {} + for comp, uuid, count in _LISTENER_ALLOCATIONS["characteristic_read"]: + char_read_by_uuid[uuid] = char_read_by_uuid.get(uuid, 0) + count + + # Generate defines for each UUID + for uuid, count in char_write_by_uuid.items(): + uuid_id = sanitize_uuid_for_identifier(uuid) + cg.add_define(f"BLE_CHAR_{uuid_id}_WRITE_MAX_LISTENERS", str(count)) + + for uuid, count in char_read_by_uuid.items(): + uuid_id = sanitize_uuid_for_identifier(uuid) + cg.add_define(f"BLE_CHAR_{uuid_id}_READ_MAX_LISTENERS", str(count)) + + # Generate defines and specialized classes for descriptors + descriptor_write_by_uuid: dict[int | str, int] = {} + for comp, uuid, count in _LISTENER_ALLOCATIONS["descriptor_write"]: + descriptor_write_by_uuid[uuid] = descriptor_write_by_uuid.get(uuid, 0) + count + + for uuid, count in descriptor_write_by_uuid.items(): + uuid_id = sanitize_uuid_for_identifier(uuid) + cg.add_define(f"BLE_DESC_{uuid_id}_WRITE_MAX_LISTENERS", str(count)) + + # Generate specialized characteristic classes with EventEmitter support + for uuid in set(list(char_write_by_uuid.keys()) + list(char_read_by_uuid.keys())): + uuid_id = sanitize_uuid_for_identifier(uuid) + write_count = char_write_by_uuid.get(uuid, 0) + read_count = char_read_by_uuid.get(uuid, 0) + + # Generate specialized class that inherits from BLECharacteristic and adds EventEmitter support + class_name = f"BLECharacteristic_{uuid_id}" + + # Build the class header with appropriate EventEmitter base classes + base_classes = ["BLECharacteristic"] + template_params = [] + + if write_count > 0: + base_classes.append( + f"EventEmitter, uint16_t>" + ) + template_params.append("write") + if read_count > 0: + base_classes.append( + f"EventEmitter" + ) + template_params.append("read") + + cg.add_global( + cg.RawExpression(f""" +class {class_name} : public {", public ".join(base_classes)} {{ + public: + {class_name}(ESPBTUUID uuid, uint32_t properties, uint16_t max_len = 100) + : BLECharacteristic(uuid, properties, max_len) {{}} + + // Override virtual methods to provide EventEmitter functionality + {"EventEmitterListenerID on_write(std::function, uint16_t)> &&listener) override {" if write_count > 0 else ""} + {" return this->EventEmitter, uint16_t>::on(BLECharacteristicEvt::SpanEvt::ON_WRITE, std::move(listener));" if write_count > 0 else ""} + {"}" if write_count > 0 else ""} + + {"void off_write(EventEmitterListenerID id) override {" if write_count > 0 else ""} + {" this->EventEmitter, uint16_t>::off(BLECharacteristicEvt::SpanEvt::ON_WRITE, id);" if write_count > 0 else ""} + {"}" if write_count > 0 else ""} + + {"EventEmitterListenerID on_read(std::function &&listener) override {" if read_count > 0 else ""} + {" return this->EventEmitter::on(BLECharacteristicEvt::EmptyEvt::ON_READ, std::move(listener));" if read_count > 0 else ""} + {"}" if read_count > 0 else ""} + + {"void off_read(EventEmitterListenerID id) override {" if read_count > 0 else ""} + {" this->EventEmitter::off(BLECharacteristicEvt::EmptyEvt::ON_READ, id);" if read_count > 0 else ""} + {"}" if read_count > 0 else ""} + + protected: + {"void emit_on_write_(std::span value, uint16_t conn_id) override {" if write_count > 0 else ""} + {" this->EventEmitter, uint16_t>::emit_(BLECharacteristicEvt::SpanEvt::ON_WRITE, value, conn_id);" if write_count > 0 else ""} + {"}" if write_count > 0 else ""} + + {"void emit_on_read_(uint16_t conn_id) override {" if read_count > 0 else ""} + {" this->EventEmitter::emit_(BLECharacteristicEvt::EmptyEvt::ON_READ, conn_id);" if read_count > 0 else ""} + {"}" if read_count > 0 else ""} +}}; +""") + ) + + # Generate specialized descriptor classes with EventEmitter support + for uuid, count in descriptor_write_by_uuid.items(): + uuid_id = sanitize_uuid_for_identifier(uuid) + class_name = f"BLEDescriptor_{uuid_id}" + + cg.add_global( + cg.RawExpression(f""" +class {class_name} : public BLEDescriptor, + public EventEmitter, uint16_t> {{ + public: + {class_name}(ESPBTUUID uuid, uint16_t max_len = 100, bool read = true, bool write = true) + : BLEDescriptor(uuid, max_len, read, write) {{}} + + EventEmitterListenerID on_write(std::function, uint16_t)> &&listener) override {{ + return this->EventEmitter, uint16_t>::on(BLEDescriptorEvt::SpanEvt::ON_WRITE, std::move(listener)); + }} + + void off_write(EventEmitterListenerID id) override {{ + this->EventEmitter, uint16_t>::off(BLEDescriptorEvt::SpanEvt::ON_WRITE, id); + }} + + protected: + void emit_on_write_(std::span value, uint16_t conn_id) override {{ + this->EventEmitter, uint16_t>::emit_(BLEDescriptorEvt::SpanEvt::ON_WRITE, value, conn_id); + }} +}}; +""") + ) + + # Generate specialized BLEServer class if needed + if server_connect_count > 0 or server_disconnect_count > 0: + base_classes = ["BLEServer"] + if server_connect_count > 0: + base_classes.append( + "EventEmitter" + ) + if server_disconnect_count > 0: + base_classes.append( + "EventEmitter" + ) + + cg.add_global( + cg.RawExpression(f""" +class BLEServerWithEvents : public {", public ".join(base_classes)} {{ + public: + {"EventEmitterListenerID on_connect(std::function &&listener) override {" if server_connect_count > 0 else ""} + {" return this->EventEmitter::on(BLEServerEvt::EmptyEvt::ON_CONNECT, std::move(listener));" if server_connect_count > 0 else ""} + {"}" if server_connect_count > 0 else ""} + + {"void off_connect(EventEmitterListenerID id) override {" if server_connect_count > 0 else ""} + {" this->EventEmitter::off(BLEServerEvt::EmptyEvt::ON_CONNECT, id);" if server_connect_count > 0 else ""} + {"}" if server_connect_count > 0 else ""} + + {"EventEmitterListenerID on_disconnect(std::function &&listener) override {" if server_disconnect_count > 0 else ""} + {" return this->EventEmitter::on(BLEServerEvt::EmptyEvt::ON_DISCONNECT, std::move(listener));" if server_disconnect_count > 0 else ""} + {"}" if server_disconnect_count > 0 else ""} + + {"void off_disconnect(EventEmitterListenerID id) override {" if server_disconnect_count > 0 else ""} + {" this->EventEmitter::off(BLEServerEvt::EmptyEvt::ON_DISCONNECT, id);" if server_disconnect_count > 0 else ""} + {"}" if server_disconnect_count > 0 else ""} + + protected: + {"void emit_on_connect_(uint16_t conn_id) override {" if server_connect_count > 0 else ""} + {" this->EventEmitter::emit_(BLEServerEvt::EmptyEvt::ON_CONNECT, conn_id);" if server_connect_count > 0 else ""} + {"}" if server_connect_count > 0 else ""} + + {"void emit_on_disconnect_(uint16_t conn_id) override {" if server_disconnect_count > 0 else ""} + {" this->EventEmitter::emit_(BLEServerEvt::EmptyEvt::ON_DISCONNECT, conn_id);" if server_disconnect_count > 0 else ""} + {"}" if server_disconnect_count > 0 else ""} +}}; +""") + ) + cg.add_define("USE_ESP32_BLE_SERVER") cg.add_define("USE_ESP32_BLE_ADVERTISING") add_idf_sdkconfig_option("CONFIG_BT_ENABLED", True) diff --git a/esphome/components/esp32_ble_server/ble_characteristic.cpp b/esphome/components/esp32_ble_server/ble_characteristic.cpp index efa9c80b30..d3fde4ec25 100644 --- a/esphome/components/esp32_ble_server/ble_characteristic.cpp +++ b/esphome/components/esp32_ble_server/ble_characteristic.cpp @@ -73,7 +73,7 @@ void BLECharacteristic::notify() { void BLECharacteristic::add_descriptor(BLEDescriptor *descriptor) { // If the descriptor is the CCCD descriptor, listen to its write event to know if the client wants to be notified if (descriptor->get_uuid() == ESPBTUUID::from_uint16(ESP_GATT_UUID_CHAR_CLIENT_CONFIG)) { - descriptor->on(BLEDescriptorEvt::VectorEvt::ON_WRITE, [this](const std::vector &value, uint16_t conn_id) { + descriptor->on_write([this](std::span value, uint16_t conn_id) { if (value.size() != 2) return; uint16_t cccd = encode_uint16(value[1], value[0]); diff --git a/esphome/components/esp32_ble_server/ble_characteristic.h b/esphome/components/esp32_ble_server/ble_characteristic.h index 6b322bc7ff..a64610d73f 100644 --- a/esphome/components/esp32_ble_server/ble_characteristic.h +++ b/esphome/components/esp32_ble_server/ble_characteristic.h @@ -28,7 +28,7 @@ using namespace event_emitter; class BLEService; namespace BLECharacteristicEvt { -enum VectorEvt { +enum SpanEvt { ON_WRITE, }; diff --git a/esphome/components/esp32_ble_server/ble_descriptor.h b/esphome/components/esp32_ble_server/ble_descriptor.h index 1b787f7a35..af1cbfd18d 100644 --- a/esphome/components/esp32_ble_server/ble_descriptor.h +++ b/esphome/components/esp32_ble_server/ble_descriptor.h @@ -20,7 +20,7 @@ using namespace event_emitter; class BLECharacteristic; namespace BLEDescriptorEvt { -enum VectorEvt { +enum SpanEvt { ON_WRITE, }; } // namespace BLEDescriptorEvt diff --git a/esphome/components/esp32_improv/__init__.py b/esphome/components/esp32_improv/__init__.py index fa33bd947a..43377326bd 100644 --- a/esphome/components/esp32_improv/__init__.py +++ b/esphome/components/esp32_improv/__init__.py @@ -1,6 +1,6 @@ from esphome import automation import esphome.codegen as cg -from esphome.components import binary_sensor, esp32_ble, output +from esphome.components import binary_sensor, esp32_ble, esp32_ble_server, output from esphome.components.esp32_ble import BTLoggers import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_ON_STATE, CONF_TRIGGER_ID @@ -98,6 +98,15 @@ async def to_code(config): # Register the loggers this component needs esp32_ble.register_bt_logger(BTLoggers.GATT, BTLoggers.SMP) + # Allocate event listeners for esp32_improv + # Need 1 listener for server disconnect event + esp32_ble_server.allocate_server_event_listener("DISCONNECT", "esp32_improv", 1) + # The RPC characteristic UUID comes from the Improv library (0x00467768-6228-2272-4663-277478268000 + 0x01) + # We need 1 listener for the RPC write event + esp32_ble_server.allocate_characteristic_event_listener( + "00467768-6228-2272-4663-277478268001", "WRITE", "esp32_improv", 1 + ) + var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config)