From fc847c1de8927871e783db1a35e967efabda9f04 Mon Sep 17 00:00:00 2001 From: guillempages Date: Thu, 23 Jan 2025 21:32:03 +0100 Subject: [PATCH 01/17] [online_image] Code Improvements (#8130) --- esphome/components/online_image/__init__.py | 18 +++++++++--------- .../components/online_image/image_decoder.h | 6 +++--- .../components/online_image/online_image.cpp | 17 ++++++++--------- esphome/components/online_image/png_image.cpp | 4 ++-- esphome/components/online_image/png_image.h | 2 +- 5 files changed, 23 insertions(+), 24 deletions(-) diff --git a/esphome/components/online_image/__init__.py b/esphome/components/online_image/__init__.py index fdb49fc493..eb6debf8eb 100644 --- a/esphome/components/online_image/__init__.py +++ b/esphome/components/online_image/__init__.py @@ -52,6 +52,14 @@ class Format: pass +class BMPFormat(Format): + def __init__(self): + super().__init__("BMP") + + def actions(self): + cg.add_define("USE_ONLINE_IMAGE_BMP_SUPPORT") + + class PNGFormat(Format): def __init__(self): super().__init__("PNG") @@ -61,20 +69,12 @@ class PNGFormat(Format): cg.add_library("pngle", "1.0.2") -class BMPFormat(Format): - def __init__(self): - super().__init__("BMP") - - def actions(self): - cg.add_define("USE_ONLINE_IMAGE_BMP_SUPPORT") - - # New formats can be added here. IMAGE_FORMATS = { x.image_type: x for x in ( - PNGFormat(), BMPFormat(), + PNGFormat(), ) } diff --git a/esphome/components/online_image/image_decoder.h b/esphome/components/online_image/image_decoder.h index 3b04824bb9..4e5dd7b229 100644 --- a/esphome/components/online_image/image_decoder.h +++ b/esphome/components/online_image/image_decoder.h @@ -30,7 +30,7 @@ class ImageDecoder { * * @param download_size The total number of bytes that need to be downloaded for the image. */ - virtual void prepare(uint32_t download_size) { this->download_size_ = download_size; } + virtual void prepare(size_t download_size) { this->download_size_ = download_size; } /** * @brief Decode a part of the image. It will try reading from the buffer. @@ -75,8 +75,8 @@ class ImageDecoder { OnlineImage *image_; // Initializing to 1, to ensure it is distinguishable from initial "decoded_bytes_". // Will be overwritten anyway once the download size is known. - uint32_t download_size_ = 1; - uint32_t decoded_bytes_ = 0; + size_t download_size_ = 1; + size_t decoded_bytes_ = 0; double x_scale_ = 1.0; double y_scale_ = 1.0; }; diff --git a/esphome/components/online_image/online_image.cpp b/esphome/components/online_image/online_image.cpp index 23fe61b534..c6499c24e4 100644 --- a/esphome/components/online_image/online_image.cpp +++ b/esphome/components/online_image/online_image.cpp @@ -6,13 +6,12 @@ static const char *const TAG = "online_image"; #include "image_decoder.h" -#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT -#include "png_image.h" -#endif - #ifdef USE_ONLINE_IMAGE_BMP_SUPPORT #include "bmp_image.h" #endif +#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT +#include "png_image.h" +#endif namespace esphome { namespace online_image { @@ -122,16 +121,16 @@ void OnlineImage::update() { ESP_LOGD(TAG, "Starting download"); size_t total_size = this->downloader_->content_length; -#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT - if (this->format_ == ImageFormat::PNG) { - this->decoder_ = make_unique(this); - } -#endif // ONLINE_IMAGE_PNG_SUPPORT #ifdef USE_ONLINE_IMAGE_BMP_SUPPORT if (this->format_ == ImageFormat::BMP) { this->decoder_ = make_unique(this); } #endif // ONLINE_IMAGE_BMP_SUPPORT +#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT + if (this->format_ == ImageFormat::PNG) { + this->decoder_ = make_unique(this); + } +#endif // ONLINE_IMAGE_PNG_SUPPORT if (!this->decoder_) { ESP_LOGE(TAG, "Could not instantiate decoder. Image format unsupported."); diff --git a/esphome/components/online_image/png_image.cpp b/esphome/components/online_image/png_image.cpp index 4c4c22f9b7..59c1ce6c7d 100644 --- a/esphome/components/online_image/png_image.cpp +++ b/esphome/components/online_image/png_image.cpp @@ -41,7 +41,7 @@ static void draw_callback(pngle_t *pngle, uint32_t x, uint32_t y, uint32_t w, ui decoder->draw(x, y, w, h, color); } -void PngDecoder::prepare(uint32_t download_size) { +void PngDecoder::prepare(size_t download_size) { ImageDecoder::prepare(download_size); pngle_set_user_data(this->pngle_, this); pngle_set_init_callback(this->pngle_, init_callback); @@ -51,7 +51,7 @@ void PngDecoder::prepare(uint32_t download_size) { int HOT PngDecoder::decode(uint8_t *buffer, size_t size) { if (!this->pngle_) { ESP_LOGE(TAG, "PNG decoder engine not initialized!"); - return -1; + return DECODE_ERROR_OUT_OF_MEMORY; } if (size < 256 && size < this->download_size_ - this->decoded_bytes_) { ESP_LOGD(TAG, "Waiting for data"); diff --git a/esphome/components/online_image/png_image.h b/esphome/components/online_image/png_image.h index d82ff93149..c137227907 100644 --- a/esphome/components/online_image/png_image.h +++ b/esphome/components/online_image/png_image.h @@ -21,7 +21,7 @@ class PngDecoder : public ImageDecoder { PngDecoder(OnlineImage *image) : ImageDecoder(image), pngle_(pngle_new()) {} ~PngDecoder() override { pngle_destroy(this->pngle_); } - void prepare(uint32_t download_size) override; + void prepare(size_t download_size) override; int HOT decode(uint8_t *buffer, size_t size) override; protected: From dbf4c2c4dae43ee929471a26823db88f373576d6 Mon Sep 17 00:00:00 2001 From: Jimmy Hedman Date: Mon, 27 Jan 2025 05:23:57 +0100 Subject: [PATCH 02/17] Update mdns for ESP-IDF (#8145) --- esphome/components/mdns/__init__.py | 2 +- esphome/idf_component.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/mdns/__init__.py b/esphome/components/mdns/__init__.py index dd68fbb93c..1bc290b582 100644 --- a/esphome/components/mdns/__init__.py +++ b/esphome/components/mdns/__init__.py @@ -91,7 +91,7 @@ async def to_code(config): add_idf_component( name="mdns", repo="https://github.com/espressif/esp-protocols.git", - ref="mdns-v1.3.2", + ref="mdns-v1.5.1", path="components/mdns", ) diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index c79ba1b0ed..bd5bcda2fe 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -7,7 +7,7 @@ dependencies: version: v2.0.9 mdns: git: https://github.com/espressif/esp-protocols.git - version: mdns-v1.3.2 + version: mdns-v1.5.1 path: components/mdns rules: - if: "idf_version >=5.0" From dd18a219db7e4d7306c4cbd088baecfd146a811d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 27 Jan 2025 16:57:52 -1000 Subject: [PATCH 03/17] Include Bluetooth connection slot allocations in connections free message (#8148) --- esphome/components/api/api.proto | 1 + esphome/components/api/api_pb2.cpp | 14 ++++++++++++++ esphome/components/api/api_pb2.h | 1 + .../components/bluetooth_proxy/bluetooth_proxy.cpp | 5 +++++ 4 files changed, 21 insertions(+) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 684540ffa6..534098e5fd 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -1381,6 +1381,7 @@ message BluetoothConnectionsFreeResponse { uint32 free = 1; uint32 limit = 2; + repeated uint64 allocated = 3; } message BluetoothGATTErrorResponse { diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 8df152881c..41016e510f 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -6430,6 +6430,10 @@ bool BluetoothConnectionsFreeResponse::decode_varint(uint32_t field_id, ProtoVar this->limit = value.as_uint32(); return true; } + case 3: { + this->allocated.push_back(value.as_uint64()); + return true; + } default: return false; } @@ -6437,6 +6441,9 @@ bool BluetoothConnectionsFreeResponse::decode_varint(uint32_t field_id, ProtoVar void BluetoothConnectionsFreeResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(1, this->free); buffer.encode_uint32(2, this->limit); + for (auto &it : this->allocated) { + buffer.encode_uint64(3, it, true); + } } #ifdef HAS_PROTO_MESSAGE_DUMP void BluetoothConnectionsFreeResponse::dump_to(std::string &out) const { @@ -6451,6 +6458,13 @@ void BluetoothConnectionsFreeResponse::dump_to(std::string &out) const { sprintf(buffer, "%" PRIu32, this->limit); out.append(buffer); out.append("\n"); + + for (const auto &it : this->allocated) { + out.append(" allocated: "); + sprintf(buffer, "%llu", it); + out.append(buffer); + out.append("\n"); + } out.append("}"); } #endif diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 063c217bf7..a3fccbc641 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -1624,6 +1624,7 @@ class BluetoothConnectionsFreeResponse : public ProtoMessage { public: uint32_t free{0}; uint32_t limit{0}; + std::vector allocated{}; void encode(ProtoWriteBuffer buffer) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp index bd1c8b7ea4..a263aca456 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp @@ -475,6 +475,11 @@ void BluetoothProxy::send_connections_free() { api::BluetoothConnectionsFreeResponse call; call.free = this->get_bluetooth_connections_free(); call.limit = this->get_bluetooth_connections_limit(); + for (auto *connection : this->connections_) { + if (connection->address_ != 0) { + call.allocated.push_back(connection->address_); + } + } this->api_connection_->send_bluetooth_connections_free_response(call); } From f7f8bf4da44b83796a301bc486d77fdb46d8ed82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Mart=C3=ADn?= Date: Tue, 28 Jan 2025 12:00:28 +0100 Subject: [PATCH 04/17] [esp32_ble_server] Create custom services, characteristics and descriptors (#7009) Co-authored-by: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> --- CODEOWNERS | 1 + .../components/ble_client/sensor/__init__.py | 2 +- .../ble_client/text_sensor/__init__.py | 2 +- esphome/components/esp32_ble/__init__.py | 39 ++ .../components/esp32_ble_server/__init__.py | 594 +++++++++++++++++- .../components/esp32_ble_server/ble_2901.cpp | 18 - .../components/esp32_ble_server/ble_2901.h | 19 - .../esp32_ble_server/ble_characteristic.cpp | 108 ++-- .../esp32_ble_server/ble_characteristic.h | 41 +- .../esp32_ble_server/ble_descriptor.cpp | 30 +- .../esp32_ble_server/ble_descriptor.h | 23 +- .../esp32_ble_server/ble_server.cpp | 129 ++-- .../components/esp32_ble_server/ble_server.h | 60 +- .../ble_server_automations.cpp | 77 +++ .../esp32_ble_server/ble_server_automations.h | 115 ++++ .../esp32_ble_server/ble_service.cpp | 32 +- .../components/esp32_ble_server/ble_service.h | 24 +- .../components/esp32_ble_tracker/__init__.py | 45 +- esphome/components/esp32_improv/__init__.py | 10 +- .../esp32_improv/esp32_improv_component.cpp | 39 +- .../esp32_improv/esp32_improv_component.h | 7 +- esphome/components/event_emitter/__init__.py | 5 + .../event_emitter/event_emitter.cpp | 14 + .../components/event_emitter/event_emitter.h | 63 ++ esphome/const.py | 1 + tests/components/esp32_ble_server/common.yaml | 65 +- 26 files changed, 1221 insertions(+), 342 deletions(-) delete mode 100644 esphome/components/esp32_ble_server/ble_2901.cpp delete mode 100644 esphome/components/esp32_ble_server/ble_2901.h create mode 100644 esphome/components/esp32_ble_server/ble_server_automations.cpp create mode 100644 esphome/components/esp32_ble_server/ble_server_automations.h create mode 100644 esphome/components/event_emitter/__init__.py create mode 100644 esphome/components/event_emitter/event_emitter.cpp create mode 100644 esphome/components/event_emitter/event_emitter.h diff --git a/CODEOWNERS b/CODEOWNERS index aa24b6cb82..cdf4ab7a99 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -148,6 +148,7 @@ esphome/components/esp32_rmt_led_strip/* @jesserockz esphome/components/esp8266/* @esphome/core esphome/components/ethernet_info/* @gtjadsonsantos esphome/components/event/* @nohat +esphome/components/event_emitter/* @Rapsssito esphome/components/exposure_notifications/* @OttoWinter esphome/components/ezo/* @ssieb esphome/components/ezo_pmp/* @carlos-sarmiento diff --git a/esphome/components/ble_client/sensor/__init__.py b/esphome/components/ble_client/sensor/__init__.py index 0c48902a90..960410a5cc 100644 --- a/esphome/components/ble_client/sensor/__init__.py +++ b/esphome/components/ble_client/sensor/__init__.py @@ -11,6 +11,7 @@ from esphome.const import ( DEVICE_CLASS_SIGNAL_STRENGTH, STATE_CLASS_MEASUREMENT, UNIT_DECIBEL_MILLIWATT, + CONF_NOTIFY, ) from .. import ble_client_ns @@ -19,7 +20,6 @@ DEPENDENCIES = ["ble_client"] CONF_DESCRIPTOR_UUID = "descriptor_uuid" -CONF_NOTIFY = "notify" CONF_ON_NOTIFY = "on_notify" TYPE_CHARACTERISTIC = "characteristic" TYPE_RSSI = "rssi" diff --git a/esphome/components/ble_client/text_sensor/__init__.py b/esphome/components/ble_client/text_sensor/__init__.py index 479af1a57e..a6672e68f5 100644 --- a/esphome/components/ble_client/text_sensor/__init__.py +++ b/esphome/components/ble_client/text_sensor/__init__.py @@ -6,6 +6,7 @@ from esphome.const import ( CONF_CHARACTERISTIC_UUID, CONF_ID, CONF_SERVICE_UUID, + CONF_NOTIFY, CONF_TRIGGER_ID, ) @@ -15,7 +16,6 @@ DEPENDENCIES = ["ble_client"] CONF_DESCRIPTOR_UUID = "descriptor_uuid" -CONF_NOTIFY = "notify" CONF_ON_NOTIFY = "on_notify" adv_data_t = cg.std_vector.template(cg.uint8) diff --git a/esphome/components/esp32_ble/__init__.py b/esphome/components/esp32_ble/__init__.py index 08e30c9247..37b4900a03 100644 --- a/esphome/components/esp32_ble/__init__.py +++ b/esphome/components/esp32_ble/__init__.py @@ -1,3 +1,5 @@ +import re + from esphome import automation import esphome.codegen as cg from esphome.components.esp32 import add_idf_sdkconfig_option, const, get_esp32_variant @@ -64,6 +66,43 @@ CONFIG_SCHEMA = cv.Schema( ).extend(cv.COMPONENT_SCHEMA) +bt_uuid16_format = "XXXX" +bt_uuid32_format = "XXXXXXXX" +bt_uuid128_format = "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" + + +def bt_uuid(value): + in_value = cv.string_strict(value) + value = in_value.upper() + + if len(value) == len(bt_uuid16_format): + pattern = re.compile("^[A-F|0-9]{4,}$") + if not pattern.match(value): + raise cv.Invalid( + f"Invalid hexadecimal value for 16 bit UUID format: '{in_value}'" + ) + return value + if len(value) == len(bt_uuid32_format): + pattern = re.compile("^[A-F|0-9]{8,}$") + if not pattern.match(value): + raise cv.Invalid( + f"Invalid hexadecimal value for 32 bit UUID format: '{in_value}'" + ) + return value + if len(value) == len(bt_uuid128_format): + pattern = re.compile( + "^[A-F|0-9]{8,}-[A-F|0-9]{4,}-[A-F|0-9]{4,}-[A-F|0-9]{4,}-[A-F|0-9]{12,}$" + ) + if not pattern.match(value): + raise cv.Invalid( + f"Invalid hexadecimal value for 128 UUID format: '{in_value}'" + ) + return value + raise cv.Invalid( + f"Bluetooth UUID must be in 16 bit '{bt_uuid16_format}', 32 bit '{bt_uuid32_format}', or 128 bit '{bt_uuid128_format}' format" + ) + + def validate_variant(_): variant = get_esp32_variant() if variant in NO_BLUETOOTH_VARIANTS: diff --git a/esphome/components/esp32_ble_server/__init__.py b/esphome/components/esp32_ble_server/__init__.py index 9da7d13999..ab8e27ec43 100644 --- a/esphome/components/esp32_ble_server/__init__.py +++ b/esphome/components/esp32_ble_server/__init__.py @@ -1,37 +1,526 @@ +import encodings + +from esphome import automation import esphome.codegen as cg from esphome.components import esp32_ble from esphome.components.esp32 import add_idf_sdkconfig_option +from esphome.components.esp32_ble import bt_uuid import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_MODEL +from esphome.config_validation import UNDEFINED +from esphome.const import ( + CONF_DATA, + CONF_ESPHOME, + CONF_ID, + CONF_MAX_LENGTH, + CONF_MODEL, + CONF_NOTIFY, + CONF_ON_CONNECT, + CONF_ON_DISCONNECT, + CONF_PROJECT, + CONF_SERVICES, + CONF_TYPE, + CONF_UUID, + CONF_VALUE, + __version__ as ESPHOME_VERSION, +) from esphome.core import CORE +from esphome.schema_extractors import SCHEMA_EXTRACT -AUTO_LOAD = ["esp32_ble"] +AUTO_LOAD = ["esp32_ble", "bytebuffer", "event_emitter"] CODEOWNERS = ["@jesserockz", "@clydebarrow", "@Rapsssito"] DEPENDENCIES = ["esp32"] +DOMAIN = "esp32_ble_server" +CONF_ADVERTISE = "advertise" +CONF_BROADCAST = "broadcast" +CONF_CHARACTERISTICS = "characteristics" +CONF_DESCRIPTION = "description" +CONF_DESCRIPTORS = "descriptors" +CONF_ENDIANNESS = "endianness" +CONF_FIRMWARE_VERSION = "firmware_version" +CONF_INDICATE = "indicate" CONF_MANUFACTURER = "manufacturer" CONF_MANUFACTURER_DATA = "manufacturer_data" +CONF_ON_WRITE = "on_write" +CONF_READ = "read" +CONF_STRING = "string" +CONF_STRING_ENCODING = "string_encoding" +CONF_WRITE = "write" +CONF_WRITE_NO_RESPONSE = "write_no_response" + +# Internal configuration keys +CONF_CHAR_VALUE_ACTION_ID_ = "char_value_action_id_" + +# BLE reserverd UUIDs +CCCD_DESCRIPTOR_UUID = 0x2902 +CUD_DESCRIPTOR_UUID = 0x2901 +DEVICE_INFORMATION_SERVICE_UUID = 0x180A +MANUFACTURER_NAME_CHARACTERISTIC_UUID = 0x2A29 +MODEL_CHARACTERISTIC_UUID = 0x2A24 +FIRMWARE_VERSION_CHARACTERISTIC_UUID = 0x2A26 + +# Core key to store the global configuration +KEY_NOTIFY_REQUIRED = "notify_required" +KEY_SET_VALUE = "set_value" esp32_ble_server_ns = cg.esphome_ns.namespace("esp32_ble_server") +ESPBTUUID_ns = cg.esphome_ns.namespace("esp32_ble").namespace("ESPBTUUID") +BLECharacteristic_ns = esp32_ble_server_ns.namespace("BLECharacteristic") BLEServer = esp32_ble_server_ns.class_( "BLEServer", cg.Component, esp32_ble.GATTsEventHandler, cg.Parented.template(esp32_ble.ESP32BLE), ) -BLEServiceComponent = esp32_ble_server_ns.class_("BLEServiceComponent") +esp32_ble_server_automations_ns = esp32_ble_server_ns.namespace( + "esp32_ble_server_automations" +) +BLETriggers_ns = esp32_ble_server_automations_ns.namespace("BLETriggers") +BLEDescriptor = esp32_ble_server_ns.class_("BLEDescriptor") +BLECharacteristic = esp32_ble_server_ns.class_("BLECharacteristic") +BLEService = esp32_ble_server_ns.class_("BLEService") +BLECharacteristicSetValueAction = esp32_ble_server_automations_ns.class_( + "BLECharacteristicSetValueAction", automation.Action +) +BLEDescriptorSetValueAction = esp32_ble_server_automations_ns.class_( + "BLEDescriptorSetValueAction", automation.Action +) +BLECharacteristicNotifyAction = esp32_ble_server_automations_ns.class_( + "BLECharacteristicNotifyAction", automation.Action +) +bytebuffer_ns = cg.esphome_ns.namespace("bytebuffer") +Endianness_ns = bytebuffer_ns.namespace("Endian") +ByteBuffer_ns = bytebuffer_ns.namespace("ByteBuffer") +ByteBuffer = bytebuffer_ns.class_("ByteBuffer") +PROPERTY_MAP = { + CONF_READ: BLECharacteristic_ns.PROPERTY_READ, + CONF_WRITE: BLECharacteristic_ns.PROPERTY_WRITE, + CONF_NOTIFY: BLECharacteristic_ns.PROPERTY_NOTIFY, + CONF_BROADCAST: BLECharacteristic_ns.PROPERTY_BROADCAST, + CONF_INDICATE: BLECharacteristic_ns.PROPERTY_INDICATE, + CONF_WRITE_NO_RESPONSE: BLECharacteristic_ns.PROPERTY_WRITE_NR, +} + + +class ValueType: + def __init__(self, type_, validator, length): + self.type_ = type_ + self.validator = validator + self.length = length + + def validate(self, value, encoding): + value = self.validator(value) + if self.type_ == "string": + try: + value.encode(encoding) + except UnicodeEncodeError as e: + raise cv.Invalid(str(e)) from e + return value + + +VALUE_TYPES = { + type_name: ValueType(type_name, validator, length) + for type_name, validator, length in ( + ("uint8_t", cv.uint8_t, 1), + ("uint16_t", cv.uint16_t, 2), + ("uint32_t", cv.uint32_t, 4), + ("uint64_t", cv.uint64_t, 8), + ("int8_t", cv.int_range(-128, 127), 1), + ("int16_t", cv.int_range(-32768, 32767), 2), + ("int32_t", cv.int_range(-2147483648, 2147483647), 4), + ("int64_t", cv.int_range(-9223372036854775808, 9223372036854775807), 8), + ("float", cv.float_, 4), + ("double", cv.float_, 8), + ("string", cv.string_strict, None), # Length is variable + ) +} + + +def validate_char_on_write(char_config): + if CONF_ON_WRITE in char_config: + if not char_config[CONF_WRITE] and not char_config[CONF_WRITE_NO_RESPONSE]: + raise cv.Invalid( + f"{CONF_ON_WRITE} requires the {CONF_WRITE} or {CONF_WRITE_NO_RESPONSE} property to be set" + ) + return char_config + + +def validate_descriptor(desc_config): + if CONF_ON_WRITE in desc_config: + if not desc_config[CONF_WRITE]: + raise cv.Invalid( + f"{CONF_ON_WRITE} requires the {CONF_WRITE} property to be set" + ) + if CONF_MAX_LENGTH not in desc_config: + value = desc_config[CONF_VALUE][CONF_DATA] + if cg.is_template(value): + raise cv.Invalid( + f"Descriptor {desc_config[CONF_UUID]} has a templatable value and the {CONF_MAX_LENGTH} property is not set" + ) + if isinstance(value, list): + desc_config[CONF_MAX_LENGTH] = len(value) + elif isinstance(value, str): + desc_config[CONF_MAX_LENGTH] = len( + value.encode(desc_config[CONF_VALUE][CONF_STRING_ENCODING]) + ) + else: + desc_config[CONF_MAX_LENGTH] = VALUE_TYPES[ + desc_config[CONF_VALUE][CONF_TYPE] + ].length + return desc_config + + +def validate_notify_action(config): + # Store the characteristic ID in the global data for the final validation + data = CORE.data.setdefault(DOMAIN, {}).setdefault(KEY_NOTIFY_REQUIRED, set()) + data.add(config[CONF_ID]) + return config + + +def validate_set_value_action(config): + # Store the characteristic ID in the global data for the final validation + data = CORE.data.setdefault(DOMAIN, {}).setdefault(KEY_SET_VALUE, set()) + data.add(config[CONF_ID]) + return config + + +def create_description_cud(char_config): + if CONF_DESCRIPTION not in char_config: + return char_config + # If the config displays a description, there cannot be a descriptor with the CUD UUID + for desc in char_config[CONF_DESCRIPTORS]: + if desc[CONF_UUID] == CUD_DESCRIPTOR_UUID: + raise cv.Invalid( + f"Characteristic {char_config[CONF_UUID]} has a description, but a CUD descriptor is already present" + ) + # Manually add the CUD descriptor + char_config[CONF_DESCRIPTORS].append( + DESCRIPTOR_SCHEMA( + { + CONF_UUID: CUD_DESCRIPTOR_UUID, + CONF_READ: True, + CONF_WRITE: False, + CONF_VALUE: char_config[CONF_DESCRIPTION], + } + ) + ) + return char_config + + +def create_notify_cccd(char_config): + if not char_config[CONF_NOTIFY] and not char_config[CONF_INDICATE]: + return char_config + # If the CCCD descriptor is already present, return the config + for desc in char_config[CONF_DESCRIPTORS]: + if desc[CONF_UUID] == CCCD_DESCRIPTOR_UUID: + # Check if the WRITE property is set + if not desc[CONF_WRITE]: + raise cv.Invalid( + f"Characteristic {char_config[CONF_UUID]} has notify actions, but the CCCD descriptor does not have the {CONF_WRITE} property set" + ) + return char_config + # Manually add the CCCD descriptor + char_config[CONF_DESCRIPTORS].append( + DESCRIPTOR_SCHEMA( + { + CONF_UUID: CCCD_DESCRIPTOR_UUID, + CONF_READ: True, + CONF_WRITE: True, + CONF_MAX_LENGTH: 2, + CONF_VALUE: [0, 0], + } + ) + ) + return char_config + + +def create_device_information_service(config): + # If there is already a device information service, + # there cannot be CONF_MODEL, CONF_MANUFACTURER or CONF_FIRMWARE_VERSION properties + for service in config[CONF_SERVICES]: + if service[CONF_UUID] == DEVICE_INFORMATION_SERVICE_UUID: + if ( + CONF_MODEL in config + or CONF_MANUFACTURER in config + or CONF_FIRMWARE_VERSION in config + ): + raise cv.Invalid( + "Device information service already present, cannot add manufacturer, model or firmware version" + ) + return config + project = CORE.raw_config[CONF_ESPHOME].get(CONF_PROJECT, {}) + model = config.get(CONF_MODEL, project.get("name", CORE.data["esp32"]["board"])) + version = config.get( + CONF_FIRMWARE_VERSION, project.get("version", "ESPHome " + ESPHOME_VERSION) + ) + # Manually add the device information service + config[CONF_SERVICES].append( + SERVICE_SCHEMA( + { + CONF_UUID: DEVICE_INFORMATION_SERVICE_UUID, + CONF_CHARACTERISTICS: [ + { + CONF_UUID: MANUFACTURER_NAME_CHARACTERISTIC_UUID, + CONF_READ: True, + CONF_VALUE: config.get(CONF_MANUFACTURER, "ESPHome"), + }, + { + CONF_UUID: MODEL_CHARACTERISTIC_UUID, + CONF_READ: True, + CONF_VALUE: model, + }, + { + CONF_UUID: FIRMWARE_VERSION_CHARACTERISTIC_UUID, + CONF_READ: True, + CONF_VALUE: version, + }, + ], + } + ) + ) + return config + + +def final_validate_config(config): + # Check if all characteristics that require notifications have the notify property set + for char_id in CORE.data.get(DOMAIN, {}).get(KEY_NOTIFY_REQUIRED, set()): + # Look for the characteristic in the configuration + char_config = [ + char_conf + for service_conf in config[CONF_SERVICES] + for char_conf in service_conf[CONF_CHARACTERISTICS] + if char_conf[CONF_ID] == char_id + ][0] + if not char_config[CONF_NOTIFY]: + raise cv.Invalid( + f"Characteristic {char_config[CONF_UUID]} has notify actions and the {CONF_NOTIFY} property is not set" + ) + for char_id in CORE.data.get(DOMAIN, {}).get(KEY_SET_VALUE, set()): + # Look for the characteristic in the configuration + char_config = [ + char_conf + for service_conf in config[CONF_SERVICES] + for char_conf in service_conf[CONF_CHARACTERISTICS] + if char_conf[CONF_ID] == char_id + ][0] + if isinstance(char_config.get(CONF_VALUE, {}).get(CONF_DATA), cv.Lambda): + raise cv.Invalid( + f"Characteristic {char_config[CONF_UUID]} has both a set_value action and a templated value" + ) + return config + + +def validate_value_type(value_config): + # If the value is a not a templatable, the type must be set + value = value_config[CONF_DATA] + + if type_ := value_config.get(CONF_TYPE): + if cg.is_template(value): + raise cv.Invalid( + f'The "{CONF_TYPE}" property is not allowed for templatable values' + ) + value_config[CONF_DATA] = VALUE_TYPES[type_].validate( + value, value_config[CONF_STRING_ENCODING] + ) + elif isinstance(value, (float, int)): + raise cv.Invalid( + f'The "{CONF_TYPE}" property is required for the value "{value}"' + ) + return value_config + + +def validate_encoding(value): + if value == SCHEMA_EXTRACT: + return cv.one_of("utf-8", "latin-1", "ascii", "utf-16", "utf-32") + value = encodings.normalize_encoding(value) + if not value: + raise cv.Invalid("Invalid encoding") + return value + + +def value_schema(default_type=UNDEFINED, templatable=True): + data_validators = [ + cv.string_strict, + cv.int_, + cv.float_, + cv.All([cv.uint8_t], cv.Length(min=1)), + ] + if templatable: + data_validators.append(cv.returning_lambda) + + return cv.maybe_simple_value( + cv.All( + { + cv.Required(CONF_DATA): cv.Any(*data_validators), + cv.Optional(CONF_TYPE, default=default_type): cv.one_of( + *VALUE_TYPES, lower=True + ), + cv.Optional(CONF_STRING_ENCODING, default="utf_8"): validate_encoding, + cv.Optional(CONF_ENDIANNESS, default="LITTLE"): cv.enum( + { + "LITTLE": Endianness_ns.LITTLE, + "BIG": Endianness_ns.BIG, + } + ), + }, + validate_value_type, + ), + key=CONF_DATA, + ) + + +DESCRIPTOR_SCHEMA = cv.All( + { + cv.GenerateID(): cv.declare_id(BLEDescriptor), + cv.Required(CONF_UUID): cv.Any(bt_uuid, cv.hex_uint32_t), + cv.Optional(CONF_READ, default=True): cv.boolean, + cv.Optional(CONF_WRITE, default=True): cv.boolean, + cv.Optional(CONF_ON_WRITE): automation.validate_automation(single=True), + cv.Required(CONF_VALUE): value_schema(templatable=False), + cv.Optional(CONF_MAX_LENGTH): cv.uint16_t, + }, + validate_descriptor, +) + +CHARACTERISTIC_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(BLECharacteristic), + cv.Required(CONF_UUID): cv.Any(bt_uuid, cv.hex_uint32_t), + cv.Optional(CONF_VALUE): value_schema(templatable=True), + cv.GenerateID(CONF_CHAR_VALUE_ACTION_ID_): cv.declare_id( + BLECharacteristicSetValueAction + ), + cv.Optional(CONF_DESCRIPTORS, default=[]): cv.ensure_list(DESCRIPTOR_SCHEMA), + cv.Optional(CONF_ON_WRITE): automation.validate_automation(single=True), + cv.Optional(CONF_DESCRIPTION): value_schema( + default_type="string", templatable=False + ), + }, + extra_schemas=[ + validate_char_on_write, + create_description_cud, + create_notify_cccd, + ], +).extend({cv.Optional(k, default=False): cv.boolean for k in PROPERTY_MAP}) + +SERVICE_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(BLEService), + cv.Required(CONF_UUID): cv.Any(bt_uuid, cv.hex_uint32_t), + cv.Optional(CONF_ADVERTISE, default=False): cv.boolean, + cv.Optional(CONF_CHARACTERISTICS, default=[]): cv.ensure_list( + CHARACTERISTIC_SCHEMA + ), + } +) + CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(BLEServer), cv.GenerateID(esp32_ble.CONF_BLE_ID): cv.use_id(esp32_ble.ESP32BLE), - cv.Optional(CONF_MANUFACTURER, default="ESPHome"): cv.string, - cv.Optional(CONF_MANUFACTURER_DATA): cv.Schema([cv.hex_uint8_t]), - cv.Optional(CONF_MODEL): cv.string, - } + cv.Optional(CONF_MANUFACTURER): value_schema("string", templatable=False), + cv.Optional(CONF_MODEL): value_schema("string", templatable=False), + cv.Optional(CONF_FIRMWARE_VERSION): value_schema("string", templatable=False), + cv.Optional(CONF_MANUFACTURER_DATA): cv.Schema([cv.uint8_t]), + cv.Optional(CONF_SERVICES, default=[]): cv.ensure_list(SERVICE_SCHEMA), + cv.Optional(CONF_ON_CONNECT): automation.validate_automation(single=True), + cv.Optional(CONF_ON_DISCONNECT): automation.validate_automation(single=True), + }, + extra_schemas=[create_device_information_service], ).extend(cv.COMPONENT_SCHEMA) +FINAL_VALIDATE_SCHEMA = final_validate_config + + +def parse_properties(char_conf): + return sum( + (PROPERTY_MAP[k] for k in char_conf if k in PROPERTY_MAP and char_conf[k]), + start=0, + ) + + +def parse_uuid(uuid): + # If the UUID is a int, use from_uint32 + if isinstance(uuid, int): + return ESPBTUUID_ns.from_uint32(uuid) + # Otherwise, use ESPBTUUID_ns.from_raw + return ESPBTUUID_ns.from_raw(uuid) + + +async def parse_value(value_config, args): + value = value_config[CONF_DATA] + if isinstance(value, cv.Lambda): + return await cg.templatable(value, args, cg.std_vector.template(cg.uint8)) + + if isinstance(value, str): + value = list(value.encode(value_config[CONF_STRING_ENCODING])) + if isinstance(value, list): + return cg.std_vector.template(cg.uint8)(value) + val = cg.RawExpression(f"{value_config[CONF_TYPE]}({cg.safe_exp(value)})") + return ByteBuffer_ns.wrap(val, value_config[CONF_ENDIANNESS]) + + +def calculate_num_handles(service_config): + total = 1 + len(service_config[CONF_CHARACTERISTICS]) * 2 + total += sum( + len(char_conf[CONF_DESCRIPTORS]) + for char_conf in service_config[CONF_CHARACTERISTICS] + ) + return total + + +async def to_code_descriptor(descriptor_conf, char_var): + value = await parse_value(descriptor_conf[CONF_VALUE], {}) + desc_var = cg.new_Pvariable( + descriptor_conf[CONF_ID], + parse_uuid(descriptor_conf[CONF_UUID]), + descriptor_conf[CONF_MAX_LENGTH], + descriptor_conf[CONF_READ], + descriptor_conf[CONF_WRITE], + ) + cg.add(char_var.add_descriptor(desc_var)) + cg.add(desc_var.set_value(value)) + if CONF_ON_WRITE in descriptor_conf: + on_write_conf = descriptor_conf[CONF_ON_WRITE] + await automation.build_automation( + BLETriggers_ns.create_descriptor_on_write_trigger(desc_var), + [(cg.std_vector.template(cg.uint8), "x"), (cg.uint16, "id")], + on_write_conf, + ) + + +async def to_code_characteristic(service_var, char_conf): + char_var = cg.Pvariable( + char_conf[CONF_ID], + service_var.create_characteristic( + parse_uuid(char_conf[CONF_UUID]), + parse_properties(char_conf), + ), + ) + if CONF_ON_WRITE in char_conf: + on_write_conf = char_conf[CONF_ON_WRITE] + await automation.build_automation( + BLETriggers_ns.create_characteristic_on_write_trigger(char_var), + [(cg.std_vector.template(cg.uint8), "x"), (cg.uint16, "id")], + on_write_conf, + ) + if CONF_VALUE in char_conf: + action_conf = { + CONF_ID: char_conf[CONF_ID], + CONF_VALUE: char_conf[CONF_VALUE], + } + value_action = await ble_server_characteristic_set_value( + action_conf, + char_conf[CONF_CHAR_VALUE_ACTION_ID_], + cg.TemplateArguments(), + {}, + ) + cg.add(value_action.play()) + for descriptor_conf in char_conf[CONF_DESCRIPTORS]: + await to_code_descriptor(descriptor_conf, char_var) + async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) @@ -42,13 +531,94 @@ async def to_code(config): cg.add(parent.register_gatts_event_handler(var)) cg.add(parent.register_ble_status_event_handler(var)) cg.add(var.set_parent(parent)) - - cg.add(var.set_manufacturer(config[CONF_MANUFACTURER])) if CONF_MANUFACTURER_DATA in config: cg.add(var.set_manufacturer_data(config[CONF_MANUFACTURER_DATA])) - if CONF_MODEL in config: - cg.add(var.set_model(config[CONF_MODEL])) + for service_config in config[CONF_SERVICES]: + # Calculate the optimal number of handles based on the number of characteristics and descriptors + num_handles = calculate_num_handles(service_config) + service_var = cg.Pvariable( + service_config[CONF_ID], + var.create_service( + parse_uuid(service_config[CONF_UUID]), + service_config[CONF_ADVERTISE], + num_handles, + ), + ) + for char_conf in service_config[CONF_CHARACTERISTICS]: + await to_code_characteristic(service_var, char_conf) + if service_config[CONF_UUID] == DEVICE_INFORMATION_SERVICE_UUID: + cg.add(var.set_device_information_service(service_var)) + else: + cg.add(var.enqueue_start_service(service_var)) + if CONF_ON_CONNECT in config: + await automation.build_automation( + BLETriggers_ns.create_server_on_connect_trigger(var), + [(cg.uint16, "id")], + config[CONF_ON_CONNECT], + ) + if CONF_ON_DISCONNECT in config: + await automation.build_automation( + BLETriggers_ns.create_server_on_disconnect_trigger(var), + [(cg.uint16, "id")], + config[CONF_ON_DISCONNECT], + ) cg.add_define("USE_ESP32_BLE_SERVER") - if CORE.using_esp_idf: add_idf_sdkconfig_option("CONFIG_BT_ENABLED", True) + + +@automation.register_action( + "ble_server.characteristic.set_value", + BLECharacteristicSetValueAction, + cv.All( + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(BLECharacteristic), + cv.Required(CONF_VALUE): value_schema(), + } + ), + validate_set_value_action, + ), +) +async def ble_server_characteristic_set_value(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + value = await parse_value(config[CONF_VALUE], args) + cg.add(var.set_buffer(value)) + return var + + +@automation.register_action( + "ble_server.descriptor.set_value", + BLEDescriptorSetValueAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(BLEDescriptor), + cv.Required(CONF_VALUE): value_schema(), + } + ), +) +async def ble_server_descriptor_set_value(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + value = await parse_value(config[CONF_VALUE], args) + cg.add(var.set_buffer(value)) + return var + + +@automation.register_action( + "ble_server.characteristic.notify", + BLECharacteristicNotifyAction, + cv.All( + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(BLECharacteristic), + } + ), + validate_notify_action, + ), +) +async def ble_server_characteristic_notify(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + return var diff --git a/esphome/components/esp32_ble_server/ble_2901.cpp b/esphome/components/esp32_ble_server/ble_2901.cpp deleted file mode 100644 index ee0808d2c4..0000000000 --- a/esphome/components/esp32_ble_server/ble_2901.cpp +++ /dev/null @@ -1,18 +0,0 @@ -#include "ble_2901.h" -#include "esphome/components/esp32_ble/ble_uuid.h" - -#ifdef USE_ESP32 - -namespace esphome { -namespace esp32_ble_server { - -BLE2901::BLE2901(const std::string &value) : BLE2901((uint8_t *) value.data(), value.length()) {} -BLE2901::BLE2901(const uint8_t *data, size_t length) : BLEDescriptor(esp32_ble::ESPBTUUID::from_uint16(0x2901)) { - this->set_value(data, length); - this->permissions_ = ESP_GATT_PERM_READ; -} - -} // namespace esp32_ble_server -} // namespace esphome - -#endif diff --git a/esphome/components/esp32_ble_server/ble_2901.h b/esphome/components/esp32_ble_server/ble_2901.h deleted file mode 100644 index 60f53e55b2..0000000000 --- a/esphome/components/esp32_ble_server/ble_2901.h +++ /dev/null @@ -1,19 +0,0 @@ -#pragma once - -#include "ble_descriptor.h" - -#ifdef USE_ESP32 - -namespace esphome { -namespace esp32_ble_server { - -class BLE2901 : public BLEDescriptor { - public: - BLE2901(const std::string &value); - BLE2901(const uint8_t *data, size_t length); -}; - -} // namespace esp32_ble_server -} // namespace esphome - -#endif diff --git a/esphome/components/esp32_ble_server/ble_characteristic.cpp b/esphome/components/esp32_ble_server/ble_characteristic.cpp index 6ff7d615f9..15739d60bb 100644 --- a/esphome/components/esp32_ble_server/ble_characteristic.cpp +++ b/esphome/components/esp32_ble_server/ble_characteristic.cpp @@ -32,70 +32,36 @@ BLECharacteristic::BLECharacteristic(const ESPBTUUID uuid, uint32_t properties) this->set_write_no_response_property((properties & PROPERTY_WRITE_NR) != 0); } -void BLECharacteristic::set_value(std::vector value) { +void BLECharacteristic::set_value(ByteBuffer buffer) { this->set_value(buffer.get_data()); } + +void BLECharacteristic::set_value(const std::vector &buffer) { xSemaphoreTake(this->set_value_lock_, 0L); - this->value_ = std::move(value); + this->value_ = buffer; xSemaphoreGive(this->set_value_lock_); } -void BLECharacteristic::set_value(const std::string &value) { - this->set_value(std::vector(value.begin(), value.end())); -} -void BLECharacteristic::set_value(const uint8_t *data, size_t length) { - this->set_value(std::vector(data, data + length)); -} -void BLECharacteristic::set_value(uint8_t &data) { - uint8_t temp[1]; - temp[0] = data; - this->set_value(temp, 1); -} -void BLECharacteristic::set_value(uint16_t &data) { - uint8_t temp[2]; - temp[0] = data; - temp[1] = data >> 8; - this->set_value(temp, 2); -} -void BLECharacteristic::set_value(uint32_t &data) { - uint8_t temp[4]; - temp[0] = data; - temp[1] = data >> 8; - temp[2] = data >> 16; - temp[3] = data >> 24; - this->set_value(temp, 4); -} -void BLECharacteristic::set_value(int &data) { - uint8_t temp[4]; - temp[0] = data; - temp[1] = data >> 8; - temp[2] = data >> 16; - temp[3] = data >> 24; - this->set_value(temp, 4); -} -void BLECharacteristic::set_value(float &data) { - float temp = data; - this->set_value((uint8_t *) &temp, 4); -} -void BLECharacteristic::set_value(double &data) { - double temp = data; - this->set_value((uint8_t *) &temp, 8); -} -void BLECharacteristic::set_value(bool &data) { - uint8_t temp[1]; - temp[0] = data; - this->set_value(temp, 1); +void BLECharacteristic::set_value(const std::string &buffer) { + this->set_value(std::vector(buffer.begin(), buffer.end())); } -void BLECharacteristic::notify(bool notification) { - if (!notification) { - ESP_LOGW(TAG, "notification=false is not yet supported"); - // TODO: Handle when notification=false - } - if (this->service_->get_server()->get_connected_client_count() == 0) +void BLECharacteristic::notify() { + if (this->service_ == nullptr || this->service_->get_server() == nullptr || + this->service_->get_server()->get_connected_client_count() == 0) return; for (auto &client : this->service_->get_server()->get_clients()) { size_t length = this->value_.size(); - esp_err_t err = esp_ble_gatts_send_indicate(this->service_->get_server()->get_gatts_if(), client.first, - this->handle_, length, this->value_.data(), false); + // If the client is not in the list of clients to notify, skip it + if (this->clients_to_notify_.count(client) == 0) + continue; + // If the client is in the list of clients to notify, check if it requires an ack (i.e. INDICATE) + bool require_ack = this->clients_to_notify_[client]; + // TODO: Remove this block when INDICATE acknowledgment is supported + if (require_ack) { + ESP_LOGW(TAG, "INDICATE acknowledgment is not yet supported (i.e. it works as a NOTIFY)"); + require_ack = false; + } + esp_err_t err = esp_ble_gatts_send_indicate(this->service_->get_server()->get_gatts_if(), client, this->handle_, + length, this->value_.data(), require_ack); if (err != ESP_OK) { ESP_LOGE(TAG, "esp_ble_gatts_send_indicate failed %d", err); return; @@ -103,7 +69,24 @@ void BLECharacteristic::notify(bool notification) { } } -void BLECharacteristic::add_descriptor(BLEDescriptor *descriptor) { this->descriptors_.push_back(descriptor); } +void BLECharacteristic::add_descriptor(BLEDescriptor *descriptor) { + // If the descriptor is the CCCD descriptor, listen to its write event to know if the client wants to be notified + if (descriptor->get_uuid() == ESPBTUUID::from_uint16(ESP_GATT_UUID_CHAR_CLIENT_CONFIG)) { + descriptor->on(BLEDescriptorEvt::VectorEvt::ON_WRITE, [this](const std::vector &value, uint16_t conn_id) { + if (value.size() != 2) + return; + uint16_t cccd = encode_uint16(value[1], value[0]); + bool notify = (cccd & 1) != 0; + bool indicate = (cccd & 2) != 0; + if (notify || indicate) { + this->clients_to_notify_[conn_id] = indicate; + } else { + this->clients_to_notify_.erase(conn_id); + } + }); + } + this->descriptors_.push_back(descriptor); +} void BLECharacteristic::remove_descriptor(BLEDescriptor *descriptor) { this->descriptors_.erase(std::remove(this->descriptors_.begin(), this->descriptors_.end(), descriptor), @@ -223,6 +206,9 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt if (!param->read.need_rsp) break; // For some reason you can request a read but not want a response + this->EventEmitter::emit_(BLECharacteristicEvt::EmptyEvt::ON_READ, + param->read.conn_id); + uint16_t max_offset = 22; esp_gatt_rsp_t response; @@ -262,13 +248,13 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt } case ESP_GATTS_WRITE_EVT: { if (this->handle_ != param->write.handle) - return; + break; if (param->write.is_prep) { this->value_.insert(this->value_.end(), param->write.value, param->write.value + param->write.len); this->write_event_ = true; } else { - this->set_value(param->write.value, param->write.len); + this->set_value(ByteBuffer::wrap(param->write.value, param->write.len)); } if (param->write.need_rsp) { @@ -289,7 +275,8 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt } if (!param->write.is_prep) { - this->on_write_(this->value_); + this->EventEmitter, uint16_t>::emit_( + BLECharacteristicEvt::VectorEvt::ON_WRITE, this->value_, param->write.conn_id); } break; @@ -300,7 +287,8 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt break; this->write_event_ = false; if (param->exec_write.exec_write_flag == ESP_GATT_PREP_WRITE_EXEC) { - this->on_write_(this->value_); + this->EventEmitter, uint16_t>::emit_( + BLECharacteristicEvt::VectorEvt::ON_WRITE, this->value_, param->exec_write.conn_id); } esp_err_t err = esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id, ESP_GATT_OK, nullptr); diff --git a/esphome/components/esp32_ble_server/ble_characteristic.h b/esphome/components/esp32_ble_server/ble_characteristic.h index 8837c796a5..3698b8c4aa 100644 --- a/esphome/components/esp32_ble_server/ble_characteristic.h +++ b/esphome/components/esp32_ble_server/ble_characteristic.h @@ -2,8 +2,11 @@ #include "ble_descriptor.h" #include "esphome/components/esp32_ble/ble_uuid.h" +#include "esphome/components/event_emitter/event_emitter.h" +#include "esphome/components/bytebuffer/bytebuffer.h" #include +#include #ifdef USE_ESP32 @@ -19,24 +22,30 @@ namespace esphome { namespace esp32_ble_server { using namespace esp32_ble; +using namespace bytebuffer; +using namespace event_emitter; class BLEService; -class BLECharacteristic { +namespace BLECharacteristicEvt { +enum VectorEvt { + ON_WRITE, +}; + +enum EmptyEvt { + ON_READ, +}; +} // namespace BLECharacteristicEvt + +class BLECharacteristic : public EventEmitter, uint16_t>, + public EventEmitter { public: BLECharacteristic(ESPBTUUID uuid, uint32_t properties); ~BLECharacteristic(); - void set_value(const uint8_t *data, size_t length); - void set_value(std::vector value); - void set_value(const std::string &value); - void set_value(uint8_t &data); - void set_value(uint16_t &data); - void set_value(uint32_t &data); - void set_value(int &data); - void set_value(float &data); - void set_value(double &data); - void set_value(bool &data); + void set_value(ByteBuffer buffer); + void set_value(const std::vector &buffer); + void set_value(const std::string &buffer); void set_broadcast_property(bool value); void set_indicate_property(bool value); @@ -45,13 +54,12 @@ class BLECharacteristic { void set_write_property(bool value); void set_write_no_response_property(bool value); - void notify(bool notification = true); + void notify(); void do_create(BLEService *service); + void do_delete() { this->clients_to_notify_.clear(); } void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param); - void on_write(const std::function &)> &&func) { this->on_write_ = func; } - void add_descriptor(BLEDescriptor *descriptor); void remove_descriptor(BLEDescriptor *descriptor); @@ -71,7 +79,7 @@ class BLECharacteristic { protected: bool write_event_{false}; - BLEService *service_; + BLEService *service_{}; ESPBTUUID uuid_; esp_gatt_char_prop_t properties_; uint16_t handle_{0xFFFF}; @@ -81,8 +89,7 @@ class BLECharacteristic { SemaphoreHandle_t set_value_lock_; std::vector descriptors_; - - std::function &)> on_write_; + std::unordered_map clients_to_notify_; esp_gatt_perm_t permissions_ = ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE; diff --git a/esphome/components/esp32_ble_server/ble_descriptor.cpp b/esphome/components/esp32_ble_server/ble_descriptor.cpp index bfb6224335..afbe579513 100644 --- a/esphome/components/esp32_ble_server/ble_descriptor.cpp +++ b/esphome/components/esp32_ble_server/ble_descriptor.cpp @@ -12,11 +12,19 @@ namespace esp32_ble_server { static const char *const TAG = "esp32_ble_server.descriptor"; -BLEDescriptor::BLEDescriptor(ESPBTUUID uuid, uint16_t max_len) { +static RAMAllocator descriptor_allocator{}; // NOLINT + +BLEDescriptor::BLEDescriptor(ESPBTUUID uuid, uint16_t max_len, bool read, bool write) { this->uuid_ = uuid; this->value_.attr_len = 0; this->value_.attr_max_len = max_len; - this->value_.attr_value = (uint8_t *) malloc(max_len); // NOLINT + this->value_.attr_value = descriptor_allocator.allocate(max_len); + if (read) { + this->permissions_ |= ESP_GATT_PERM_READ; + } + if (write) { + this->permissions_ |= ESP_GATT_PERM_WRITE; + } } BLEDescriptor::~BLEDescriptor() { free(this->value_.attr_value); } // NOLINT @@ -38,14 +46,15 @@ void BLEDescriptor::do_create(BLECharacteristic *characteristic) { this->state_ = CREATING; } -void BLEDescriptor::set_value(const std::string &value) { this->set_value((uint8_t *) value.data(), value.length()); } -void BLEDescriptor::set_value(const uint8_t *data, size_t length) { +void BLEDescriptor::set_value(std::vector buffer) { + size_t length = buffer.size(); + if (length > this->value_.attr_max_len) { ESP_LOGE(TAG, "Size %d too large, must be no bigger than %d", length, this->value_.attr_max_len); return; } this->value_.attr_len = length; - memcpy(this->value_.attr_value, data, length); + memcpy(this->value_.attr_value, buffer.data(), length); } void BLEDescriptor::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, @@ -61,10 +70,13 @@ void BLEDescriptor::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_ break; } case ESP_GATTS_WRITE_EVT: { - if (this->handle_ == param->write.handle) { - this->value_.attr_len = param->write.len; - memcpy(this->value_.attr_value, param->write.value, param->write.len); - } + if (this->handle_ != param->write.handle) + break; + this->value_.attr_len = param->write.len; + memcpy(this->value_.attr_value, param->write.value, param->write.len); + this->emit_(BLEDescriptorEvt::VectorEvt::ON_WRITE, + std::vector(param->write.value, param->write.value + param->write.len), + param->write.conn_id); break; } default: diff --git a/esphome/components/esp32_ble_server/ble_descriptor.h b/esphome/components/esp32_ble_server/ble_descriptor.h index 4b8fb345c3..8d3c22c5a1 100644 --- a/esphome/components/esp32_ble_server/ble_descriptor.h +++ b/esphome/components/esp32_ble_server/ble_descriptor.h @@ -1,6 +1,8 @@ #pragma once #include "esphome/components/esp32_ble/ble_uuid.h" +#include "esphome/components/event_emitter/event_emitter.h" +#include "esphome/components/bytebuffer/bytebuffer.h" #ifdef USE_ESP32 @@ -11,17 +13,26 @@ namespace esphome { namespace esp32_ble_server { using namespace esp32_ble; +using namespace bytebuffer; +using namespace event_emitter; class BLECharacteristic; -class BLEDescriptor { +namespace BLEDescriptorEvt { +enum VectorEvt { + ON_WRITE, +}; +} // namespace BLEDescriptorEvt + +class BLEDescriptor : public EventEmitter, uint16_t> { public: - BLEDescriptor(ESPBTUUID uuid, uint16_t max_len = 100); + BLEDescriptor(ESPBTUUID uuid, uint16_t max_len = 100, bool read = true, bool write = true); virtual ~BLEDescriptor(); void do_create(BLECharacteristic *characteristic); + ESPBTUUID get_uuid() const { return this->uuid_; } - void set_value(const std::string &value); - void set_value(const uint8_t *data, size_t length); + void set_value(std::vector buffer); + void set_value(ByteBuffer buffer) { this->set_value(buffer.get_data()); } void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param); @@ -33,9 +44,9 @@ class BLEDescriptor { ESPBTUUID uuid_; uint16_t handle_{0xFFFF}; - esp_attr_value_t value_; + esp_attr_value_t value_{}; - esp_gatt_perm_t permissions_ = ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE; + esp_gatt_perm_t permissions_{}; enum State : uint8_t { FAILED = 0x00, diff --git a/esphome/components/esp32_ble_server/ble_server.cpp b/esphome/components/esp32_ble_server/ble_server.cpp index 338413f64e..5339bf8aed 100644 --- a/esphome/components/esp32_ble_server/ble_server.cpp +++ b/esphome/components/esp32_ble_server/ble_server.cpp @@ -19,11 +19,6 @@ namespace esp32_ble_server { static const char *const TAG = "esp32_ble_server"; -static const uint16_t DEVICE_INFORMATION_SERVICE_UUID = 0x180A; -static const uint16_t MODEL_UUID = 0x2A24; -static const uint16_t VERSION_UUID = 0x2A26; -static const uint16_t MANUFACTURER_UUID = 0x2A29; - void BLEServer::setup() { if (this->parent_->is_failed()) { this->mark_failed(); @@ -38,9 +33,27 @@ void BLEServer::loop() { return; } switch (this->state_) { - case RUNNING: - return; - + case RUNNING: { + // Start all services that are pending to start + if (!this->services_to_start_.empty()) { + uint16_t index_to_remove = 0; + // Iterate over the services to start + for (unsigned i = 0; i < this->services_to_start_.size(); i++) { + BLEService *service = this->services_to_start_[i]; + if (service->is_created()) { + service->start(); // Needs to be called once per characteristic in the service + } else { + index_to_remove = i + 1; + } + } + // Remove the services that have been started + if (index_to_remove > 0) { + this->services_to_start_.erase(this->services_to_start_.begin(), + this->services_to_start_.begin() + index_to_remove - 1); + } + } + break; + } case INIT: { esp_err_t err = esp_ble_gatts_app_register(0); if (err != ESP_OK) { @@ -53,29 +66,26 @@ void BLEServer::loop() { } case REGISTERING: { if (this->registered_) { + // Create the device information service first so + // it is at the top of the GATT table + this->device_information_service_->do_create(this); // Create all services previously created for (auto &pair : this->services_) { + if (pair.second == this->device_information_service_) { + continue; + } pair.second->do_create(this); } - if (this->device_information_service_ == nullptr) { - this->create_service(ESPBTUUID::from_uint16(DEVICE_INFORMATION_SERVICE_UUID)); - this->device_information_service_ = - this->get_service(ESPBTUUID::from_uint16(DEVICE_INFORMATION_SERVICE_UUID)); - this->create_device_characteristics_(); - } this->state_ = STARTING_SERVICE; } break; } case STARTING_SERVICE: { - if (!this->device_information_service_->is_created()) { - break; - } if (this->device_information_service_->is_running()) { this->state_ = RUNNING; this->restart_advertising_(); ESP_LOGD(TAG, "BLE server setup successfully"); - } else if (!this->device_information_service_->is_starting()) { + } else if (this->device_information_service_->is_created()) { this->device_information_service_->start(); } break; @@ -93,81 +103,66 @@ void BLEServer::restart_advertising_() { } } -bool BLEServer::create_device_characteristics_() { - if (this->model_.has_value()) { - BLECharacteristic *model = - this->device_information_service_->create_characteristic(MODEL_UUID, BLECharacteristic::PROPERTY_READ); - model->set_value(this->model_.value()); - } else { - BLECharacteristic *model = - this->device_information_service_->create_characteristic(MODEL_UUID, BLECharacteristic::PROPERTY_READ); - model->set_value(ESPHOME_BOARD); - } - - BLECharacteristic *version = - this->device_information_service_->create_characteristic(VERSION_UUID, BLECharacteristic::PROPERTY_READ); - version->set_value("ESPHome " ESPHOME_VERSION); - - BLECharacteristic *manufacturer = - this->device_information_service_->create_characteristic(MANUFACTURER_UUID, BLECharacteristic::PROPERTY_READ); - manufacturer->set_value(this->manufacturer_); - - return true; -} - -void BLEServer::create_service(ESPBTUUID uuid, bool advertise, uint16_t num_handles, uint8_t inst_id) { +BLEService *BLEServer::create_service(ESPBTUUID uuid, bool advertise, uint16_t num_handles) { ESP_LOGV(TAG, "Creating BLE service - %s", uuid.to_string().c_str()); - // If the service already exists, do nothing - BLEService *service = this->get_service(uuid); - if (service != nullptr) { - ESP_LOGW(TAG, "BLE service %s already exists", uuid.to_string().c_str()); - return; + // Calculate the inst_id for the service + uint8_t inst_id = 0; + for (; inst_id < 0xFF; inst_id++) { + if (this->get_service(uuid, inst_id) == nullptr) { + break; + } } - service = new BLEService(uuid, num_handles, inst_id, advertise); // NOLINT(cppcoreguidelines-owning-memory) - this->services_.emplace(uuid.to_string(), service); - service->do_create(this); + if (inst_id == 0xFF) { + ESP_LOGW(TAG, "Could not create BLE service %s, too many instances", uuid.to_string().c_str()); + return nullptr; + } + BLEService *service = // NOLINT(cppcoreguidelines-owning-memory) + new BLEService(uuid, num_handles, inst_id, advertise); + this->services_.emplace(BLEServer::get_service_key(uuid, inst_id), service); + if (this->parent_->is_active() && this->registered_) { + service->do_create(this); + } + return service; } -void BLEServer::remove_service(ESPBTUUID uuid) { - ESP_LOGV(TAG, "Removing BLE service - %s", uuid.to_string().c_str()); - BLEService *service = this->get_service(uuid); +void BLEServer::remove_service(ESPBTUUID uuid, uint8_t inst_id) { + ESP_LOGV(TAG, "Removing BLE service - %s %d", uuid.to_string().c_str(), inst_id); + BLEService *service = this->get_service(uuid, inst_id); if (service == nullptr) { - ESP_LOGW(TAG, "BLE service %s not found", uuid.to_string().c_str()); + ESP_LOGW(TAG, "BLE service %s %d does not exist", uuid.to_string().c_str(), inst_id); return; } service->do_delete(); delete service; // NOLINT(cppcoreguidelines-owning-memory) - this->services_.erase(uuid.to_string()); + this->services_.erase(BLEServer::get_service_key(uuid, inst_id)); } -BLEService *BLEServer::get_service(ESPBTUUID uuid) { +BLEService *BLEServer::get_service(ESPBTUUID uuid, uint8_t inst_id) { BLEService *service = nullptr; - if (this->services_.count(uuid.to_string()) > 0) { - service = this->services_.at(uuid.to_string()); + if (this->services_.count(BLEServer::get_service_key(uuid, inst_id)) > 0) { + service = this->services_.at(BLEServer::get_service_key(uuid, inst_id)); } return service; } +std::string BLEServer::get_service_key(ESPBTUUID uuid, uint8_t inst_id) { + return uuid.to_string() + std::to_string(inst_id); +} + void BLEServer::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param) { switch (event) { case ESP_GATTS_CONNECT_EVT: { ESP_LOGD(TAG, "BLE Client connected"); - this->add_client_(param->connect.conn_id, (void *) this); - this->connected_clients_++; - for (auto *component : this->service_components_) { - component->on_client_connect(); - } + this->add_client_(param->connect.conn_id); + this->emit_(BLEServerEvt::EmptyEvt::ON_CONNECT, param->connect.conn_id); break; } case ESP_GATTS_DISCONNECT_EVT: { ESP_LOGD(TAG, "BLE Client disconnected"); - if (this->remove_client_(param->disconnect.conn_id)) - this->connected_clients_--; + this->remove_client_(param->disconnect.conn_id); this->parent_->advertising_start(); - for (auto *component : this->service_components_) { - component->on_client_disconnect(); - } + this->emit_(BLEServerEvt::EmptyEvt::ON_DISCONNECT, param->disconnect.conn_id); break; } case ESP_GATTS_REG_EVT: { diff --git a/esphome/components/esp32_ble_server/ble_server.h b/esphome/components/esp32_ble_server/ble_server.h index e379e67296..43599438f3 100644 --- a/esphome/components/esp32_ble_server/ble_server.h +++ b/esphome/components/esp32_ble_server/ble_server.h @@ -4,36 +4,38 @@ #include "ble_characteristic.h" #include "esphome/components/esp32_ble/ble.h" -#include "esphome/components/esp32_ble/ble_advertising.h" #include "esphome/components/esp32_ble/ble_uuid.h" -#include "esphome/components/esp32_ble/queue.h" +#include "esphome/components/bytebuffer/bytebuffer.h" #include "esphome/core/component.h" #include "esphome/core/helpers.h" -#include "esphome/core/preferences.h" #include #include #include +#include #ifdef USE_ESP32 -#include #include namespace esphome { namespace esp32_ble_server { using namespace esp32_ble; +using namespace bytebuffer; -class BLEServiceComponent { - public: - virtual void on_client_connect(){}; - virtual void on_client_disconnect(){}; - virtual void start(); - virtual void stop(); +namespace BLEServerEvt { +enum EmptyEvt { + ON_CONNECT, + ON_DISCONNECT, }; +} // namespace BLEServerEvt -class BLEServer : public Component, public GATTsEventHandler, public BLEStatusEventHandler, public Parented { +class BLEServer : public Component, + public GATTsEventHandler, + public BLEStatusEventHandler, + public Parented, + public EventEmitter { public: void setup() override; void loop() override; @@ -44,47 +46,41 @@ class BLEServer : public Component, public GATTsEventHandler, public BLEStatusEv void teardown(); bool is_running(); - void set_manufacturer(const std::string &manufacturer) { this->manufacturer_ = manufacturer; } - void set_model(const std::string &model) { this->model_ = model; } void set_manufacturer_data(const std::vector &data) { this->manufacturer_data_ = data; this->restart_advertising_(); } - void create_service(ESPBTUUID uuid, bool advertise = false, uint16_t num_handles = 15, uint8_t inst_id = 0); - void remove_service(ESPBTUUID uuid); - BLEService *get_service(ESPBTUUID uuid); + BLEService *create_service(ESPBTUUID uuid, bool advertise = false, uint16_t num_handles = 15); + void remove_service(ESPBTUUID uuid, uint8_t inst_id = 0); + BLEService *get_service(ESPBTUUID uuid, uint8_t inst_id = 0); + void enqueue_start_service(BLEService *service) { this->services_to_start_.push_back(service); } + void set_device_information_service(BLEService *service) { this->device_information_service_ = service; } esp_gatt_if_t get_gatts_if() { return this->gatts_if_; } - uint32_t get_connected_client_count() { return this->connected_clients_; } - const std::unordered_map &get_clients() { return this->clients_; } + uint32_t get_connected_client_count() { return this->clients_.size(); } + const std::unordered_set &get_clients() { return this->clients_; } void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param) override; void ble_before_disabled_event_handler() override; - void register_service_component(BLEServiceComponent *component) { this->service_components_.push_back(component); } - protected: - bool create_device_characteristics_(); + static std::string get_service_key(ESPBTUUID uuid, uint8_t inst_id); void restart_advertising_(); - void add_client_(uint16_t conn_id, void *client) { this->clients_.emplace(conn_id, client); } - bool remove_client_(uint16_t conn_id) { return this->clients_.erase(conn_id) > 0; } + void add_client_(uint16_t conn_id) { this->clients_.insert(conn_id); } + void remove_client_(uint16_t conn_id) { this->clients_.erase(conn_id); } - std::string manufacturer_; - optional model_; - std::vector manufacturer_data_; + std::vector manufacturer_data_{}; esp_gatt_if_t gatts_if_{0}; bool registered_{false}; - uint32_t connected_clients_{0}; - std::unordered_map clients_; - std::unordered_map services_; - BLEService *device_information_service_; - - std::vector service_components_; + std::unordered_set clients_; + std::unordered_map services_{}; + std::vector services_to_start_{}; + BLEService *device_information_service_{}; enum State : uint8_t { INIT = 0x00, diff --git a/esphome/components/esp32_ble_server/ble_server_automations.cpp b/esphome/components/esp32_ble_server/ble_server_automations.cpp new file mode 100644 index 0000000000..41ef2b8bfe --- /dev/null +++ b/esphome/components/esp32_ble_server/ble_server_automations.cpp @@ -0,0 +1,77 @@ +#include "ble_server_automations.h" + +#ifdef USE_ESP32 + +namespace esphome { +namespace esp32_ble_server { +// Interface to interact with ESPHome automations and triggers +namespace esp32_ble_server_automations { + +using namespace esp32_ble; + +Trigger, uint16_t> *BLETriggers::create_characteristic_on_write_trigger( + BLECharacteristic *characteristic) { + Trigger, uint16_t> *on_write_trigger = // NOLINT(cppcoreguidelines-owning-memory) + new Trigger, uint16_t>(); + characteristic->EventEmitter, uint16_t>::on( + BLECharacteristicEvt::VectorEvt::ON_WRITE, + [on_write_trigger](const std::vector &data, uint16_t id) { on_write_trigger->trigger(data, id); }); + return on_write_trigger; +} + +Trigger, uint16_t> *BLETriggers::create_descriptor_on_write_trigger(BLEDescriptor *descriptor) { + Trigger, uint16_t> *on_write_trigger = // NOLINT(cppcoreguidelines-owning-memory) + new Trigger, uint16_t>(); + descriptor->on( + BLEDescriptorEvt::VectorEvt::ON_WRITE, + [on_write_trigger](const std::vector &data, uint16_t id) { on_write_trigger->trigger(data, id); }); + return on_write_trigger; +} + +Trigger *BLETriggers::create_server_on_connect_trigger(BLEServer *server) { + Trigger *on_connect_trigger = new Trigger(); // NOLINT(cppcoreguidelines-owning-memory) + server->on(BLEServerEvt::EmptyEvt::ON_CONNECT, + [on_connect_trigger](uint16_t conn_id) { on_connect_trigger->trigger(conn_id); }); + return on_connect_trigger; +} + +Trigger *BLETriggers::create_server_on_disconnect_trigger(BLEServer *server) { + Trigger *on_disconnect_trigger = new Trigger(); // NOLINT(cppcoreguidelines-owning-memory) + server->on(BLEServerEvt::EmptyEvt::ON_DISCONNECT, + [on_disconnect_trigger](uint16_t conn_id) { on_disconnect_trigger->trigger(conn_id); }); + return on_disconnect_trigger; +} + +void BLECharacteristicSetValueActionManager::set_listener(BLECharacteristic *characteristic, + EventEmitterListenerID listener_id, + const std::function &pre_notify_listener) { + // Check if there is already a listener for this characteristic + if (this->listeners_.count(characteristic) > 0) { + // Unpack the pair listener_id, pre_notify_listener_id + auto listener_pairs = this->listeners_[characteristic]; + EventEmitterListenerID old_listener_id = listener_pairs.first; + EventEmitterListenerID old_pre_notify_listener_id = listener_pairs.second; + // Remove the previous listener + characteristic->EventEmitter::off(BLECharacteristicEvt::EmptyEvt::ON_READ, + old_listener_id); + // Remove the pre-notify listener + this->off(BLECharacteristicSetValueActionEvt::PRE_NOTIFY, old_pre_notify_listener_id); + } + // Create a new listener for the pre-notify event + EventEmitterListenerID pre_notify_listener_id = + this->on(BLECharacteristicSetValueActionEvt::PRE_NOTIFY, + [pre_notify_listener, characteristic](const BLECharacteristic *evt_characteristic) { + // Only call the pre-notify listener if the characteristic is the one we are interested in + if (characteristic == evt_characteristic) { + pre_notify_listener(); + } + }); + // Save the pair listener_id, pre_notify_listener_id to the map + this->listeners_[characteristic] = std::make_pair(listener_id, pre_notify_listener_id); +} + +} // namespace esp32_ble_server_automations +} // namespace esp32_ble_server +} // namespace esphome + +#endif diff --git a/esphome/components/esp32_ble_server/ble_server_automations.h b/esphome/components/esp32_ble_server/ble_server_automations.h new file mode 100644 index 0000000000..eab6b05f05 --- /dev/null +++ b/esphome/components/esp32_ble_server/ble_server_automations.h @@ -0,0 +1,115 @@ +#pragma once + +#include "ble_server.h" +#include "ble_characteristic.h" +#include "ble_descriptor.h" + +#include "esphome/components/event_emitter/event_emitter.h" +#include "esphome/core/automation.h" + +#include +#include +#include + +#ifdef USE_ESP32 + +namespace esphome { +namespace esp32_ble_server { +// Interface to interact with ESPHome actions and triggers +namespace esp32_ble_server_automations { + +using namespace esp32_ble; +using namespace event_emitter; + +class BLETriggers { + public: + static Trigger, uint16_t> *create_characteristic_on_write_trigger( + BLECharacteristic *characteristic); + static Trigger, uint16_t> *create_descriptor_on_write_trigger(BLEDescriptor *descriptor); + static Trigger *create_server_on_connect_trigger(BLEServer *server); + static Trigger *create_server_on_disconnect_trigger(BLEServer *server); +}; + +enum BLECharacteristicSetValueActionEvt { + PRE_NOTIFY, +}; + +// Class to make sure only one BLECharacteristicSetValueAction is active at a time for each characteristic +class BLECharacteristicSetValueActionManager + : public EventEmitter { + public: + // Singleton pattern + static BLECharacteristicSetValueActionManager *get_instance() { + static BLECharacteristicSetValueActionManager instance; + return &instance; + } + void set_listener(BLECharacteristic *characteristic, EventEmitterListenerID listener_id, + const std::function &pre_notify_listener); + EventEmitterListenerID get_listener(BLECharacteristic *characteristic) { + return this->listeners_[characteristic].first; + } + void emit_pre_notify(BLECharacteristic *characteristic) { + this->emit_(BLECharacteristicSetValueActionEvt::PRE_NOTIFY, characteristic); + } + + private: + std::unordered_map> listeners_; +}; + +template class BLECharacteristicSetValueAction : public Action { + public: + BLECharacteristicSetValueAction(BLECharacteristic *characteristic) : parent_(characteristic) {} + TEMPLATABLE_VALUE(std::vector, buffer) + void set_buffer(ByteBuffer buffer) { this->set_buffer(buffer.get_data()); } + void play(Ts... x) override { + // If the listener is already set, do nothing + if (BLECharacteristicSetValueActionManager::get_instance()->get_listener(this->parent_) == this->listener_id_) + return; + // Set initial value + this->parent_->set_value(this->buffer_.value(x...)); + // Set the listener for read events + this->listener_id_ = this->parent_->EventEmitter::on( + BLECharacteristicEvt::EmptyEvt::ON_READ, [this, x...](uint16_t id) { + // Set the value of the characteristic every time it is read + this->parent_->set_value(this->buffer_.value(x...)); + }); + // Set the listener in the global manager so only one BLECharacteristicSetValueAction is set for each characteristic + BLECharacteristicSetValueActionManager::get_instance()->set_listener( + this->parent_, this->listener_id_, [this, x...]() { this->parent_->set_value(this->buffer_.value(x...)); }); + } + + protected: + BLECharacteristic *parent_; + EventEmitterListenerID listener_id_; +}; + +template class BLECharacteristicNotifyAction : public Action { + public: + BLECharacteristicNotifyAction(BLECharacteristic *characteristic) : parent_(characteristic) {} + void play(Ts... x) override { + // Call the pre-notify event + BLECharacteristicSetValueActionManager::get_instance()->emit_pre_notify(this->parent_); + // Notify the characteristic + this->parent_->notify(); + } + + protected: + BLECharacteristic *parent_; +}; + +template class BLEDescriptorSetValueAction : public Action { + public: + BLEDescriptorSetValueAction(BLEDescriptor *descriptor) : parent_(descriptor) {} + TEMPLATABLE_VALUE(std::vector, buffer) + void set_buffer(ByteBuffer buffer) { this->set_buffer(buffer.get_data()); } + void play(Ts... x) override { this->parent_->set_value(this->buffer_.value(x...)); } + + protected: + BLEDescriptor *parent_; +}; + +} // namespace esp32_ble_server_automations +} // namespace esp32_ble_server +} // namespace esphome + +#endif diff --git a/esphome/components/esp32_ble_server/ble_service.cpp b/esphome/components/esp32_ble_server/ble_service.cpp index 368f03fb52..96fedf2346 100644 --- a/esphome/components/esp32_ble_server/ble_service.cpp +++ b/esphome/components/esp32_ble_server/ble_service.cpp @@ -52,18 +52,21 @@ void BLEService::do_create(BLEServer *server) { esp_err_t err = esp_ble_gatts_create_service(server->get_gatts_if(), &srvc_id, this->num_handles_); if (err != ESP_OK) { ESP_LOGE(TAG, "esp_ble_gatts_create_service failed: %d", err); - this->init_state_ = FAILED; + this->state_ = FAILED; return; } - this->init_state_ = CREATING; + this->state_ = CREATING; } void BLEService::do_delete() { - if (this->init_state_ == DELETING || this->init_state_ == DELETED) + if (this->state_ == DELETING || this->state_ == DELETED) return; - this->init_state_ = DELETING; + this->state_ = DELETING; this->created_characteristic_count_ = 0; this->last_created_characteristic_ = nullptr; + // Call all characteristics to delete + for (auto *characteristic : this->characteristics_) + characteristic->do_delete(); this->stop_(); esp_err_t err = esp_ble_gatts_delete_service(this->handle_); if (err != ESP_OK) { @@ -91,6 +94,7 @@ void BLEService::start() { return; should_start_ = true; + this->state_ = STARTING; esp_err_t err = esp_ble_gatts_start_service(this->handle_); if (err != ESP_OK) { ESP_LOGE(TAG, "esp_ble_gatts_start_service failed: %d", err); @@ -98,7 +102,6 @@ void BLEService::start() { } if (this->advertise_) esp32_ble::global_ble->advertising_add_service_uuid(this->uuid_); - this->running_state_ = STARTING; } void BLEService::stop() { @@ -107,9 +110,9 @@ void BLEService::stop() { } void BLEService::stop_() { - if (this->running_state_ == STOPPING || this->running_state_ == STOPPED) + if (this->state_ == STOPPING || this->state_ == STOPPED) return; - this->running_state_ = STOPPING; + this->state_ = STOPPING; esp_err_t err = esp_ble_gatts_stop_service(this->handle_); if (err != ESP_OK) { ESP_LOGE(TAG, "esp_ble_gatts_stop_service failed: %d", err); @@ -119,17 +122,16 @@ void BLEService::stop_() { esp32_ble::global_ble->advertising_remove_service_uuid(this->uuid_); } -bool BLEService::is_created() { return this->init_state_ == CREATED; } bool BLEService::is_failed() { - if (this->init_state_ == FAILED) + if (this->state_ == FAILED) return true; bool failed = false; for (auto *characteristic : this->characteristics_) failed |= characteristic->is_failed(); if (failed) - this->init_state_ = FAILED; - return this->init_state_ == FAILED; + this->state_ = FAILED; + return this->state_ == FAILED; } void BLEService::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, @@ -139,7 +141,7 @@ void BLEService::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t g if (this->uuid_ == ESPBTUUID::from_uuid(param->create.service_id.id.uuid) && this->inst_id_ == param->create.service_id.id.inst_id) { this->handle_ = param->create.service_handle; - this->init_state_ = CREATED; + this->state_ = CREATED; if (this->should_start_) this->start(); } @@ -147,18 +149,18 @@ void BLEService::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t g } case ESP_GATTS_DELETE_EVT: if (param->del.service_handle == this->handle_) { - this->init_state_ = DELETED; + this->state_ = DELETED; } break; case ESP_GATTS_START_EVT: { if (param->start.service_handle == this->handle_) { - this->running_state_ = RUNNING; + this->state_ = RUNNING; } break; } case ESP_GATTS_STOP_EVT: { if (param->start.service_handle == this->handle_) { - this->running_state_ = STOPPED; + this->state_ = STOPPED; } break; } diff --git a/esphome/components/esp32_ble_server/ble_service.h b/esphome/components/esp32_ble_server/ble_service.h index 5e5883b6bf..dcfad5f501 100644 --- a/esphome/components/esp32_ble_server/ble_service.h +++ b/esphome/components/esp32_ble_server/ble_service.h @@ -32,6 +32,7 @@ class BLEService { BLECharacteristic *create_characteristic(ESPBTUUID uuid, esp_gatt_char_prop_t properties); ESPBTUUID get_uuid() { return this->uuid_; } + uint8_t get_inst_id() { return this->inst_id_; } BLECharacteristic *get_last_created_characteristic() { return this->last_created_characteristic_; } uint16_t get_handle() { return this->handle_; } @@ -44,18 +45,17 @@ class BLEService { void start(); void stop(); - bool is_created(); bool is_failed(); - - bool is_running() { return this->running_state_ == RUNNING; } - bool is_starting() { return this->running_state_ == STARTING; } - bool is_deleted() { return this->init_state_ == DELETED; } + bool is_created() { return this->state_ == CREATED; } + bool is_running() { return this->state_ == RUNNING; } + bool is_starting() { return this->state_ == STARTING; } + bool is_deleted() { return this->state_ == DELETED; } protected: std::vector characteristics_; BLECharacteristic *last_created_characteristic_{nullptr}; uint32_t created_characteristic_count_{0}; - BLEServer *server_; + BLEServer *server_ = nullptr; ESPBTUUID uuid_; uint16_t num_handles_; uint16_t handle_{0xFFFF}; @@ -66,22 +66,18 @@ class BLEService { bool do_create_characteristics_(); void stop_(); - enum InitState : uint8_t { + enum State : uint8_t { FAILED = 0x00, INIT, CREATING, - CREATING_DEPENDENTS, CREATED, - DELETING, - DELETED, - } init_state_{INIT}; - - enum RunningState : uint8_t { STARTING, RUNNING, STOPPING, STOPPED, - } running_state_{STOPPED}; + DELETING, + DELETED, + } state_{INIT}; }; } // namespace esp32_ble_server diff --git a/esphome/components/esp32_ble_tracker/__init__.py b/esphome/components/esp32_ble_tracker/__init__.py index 0aa8eadd0a..e762c89b94 100644 --- a/esphome/components/esp32_ble_tracker/__init__.py +++ b/esphome/components/esp32_ble_tracker/__init__.py @@ -1,9 +1,13 @@ -import re - from esphome import automation import esphome.codegen as cg from esphome.components import esp32_ble from esphome.components.esp32 import add_idf_sdkconfig_option +from esphome.components.esp32_ble import ( + bt_uuid, + bt_uuid16_format, + bt_uuid32_format, + bt_uuid128_format, +) import esphome.config_validation as cv from esphome.const import ( CONF_ACTIVE, @@ -86,43 +90,6 @@ def validate_scan_parameters(config): return config -bt_uuid16_format = "XXXX" -bt_uuid32_format = "XXXXXXXX" -bt_uuid128_format = "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" - - -def bt_uuid(value): - in_value = cv.string_strict(value) - value = in_value.upper() - - if len(value) == len(bt_uuid16_format): - pattern = re.compile("^[A-F|0-9]{4,}$") - if not pattern.match(value): - raise cv.Invalid( - f"Invalid hexadecimal value for 16 bit UUID format: '{in_value}'" - ) - return value - if len(value) == len(bt_uuid32_format): - pattern = re.compile("^[A-F|0-9]{8,}$") - if not pattern.match(value): - raise cv.Invalid( - f"Invalid hexadecimal value for 32 bit UUID format: '{in_value}'" - ) - return value - if len(value) == len(bt_uuid128_format): - pattern = re.compile( - "^[A-F|0-9]{8,}-[A-F|0-9]{4,}-[A-F|0-9]{4,}-[A-F|0-9]{4,}-[A-F|0-9]{12,}$" - ) - if not pattern.match(value): - raise cv.Invalid( - f"Invalid hexadecimal value for 128 UUID format: '{in_value}'" - ) - return value - raise cv.Invalid( - f"Service UUID must be in 16 bit '{bt_uuid16_format}', 32 bit '{bt_uuid32_format}', or 128 bit '{bt_uuid128_format}' format" - ) - - def as_hex(value): return cg.RawExpression(f"0x{value}ULL") diff --git a/esphome/components/esp32_improv/__init__.py b/esphome/components/esp32_improv/__init__.py index ecc07d4c91..ca39c1cd36 100644 --- a/esphome/components/esp32_improv/__init__.py +++ b/esphome/components/esp32_improv/__init__.py @@ -1,6 +1,6 @@ from esphome import automation import esphome.codegen as cg -from esphome.components import binary_sensor, esp32_ble_server, output +from esphome.components import binary_sensor, output import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_ON_STATE, CONF_TRIGGER_ID @@ -24,9 +24,7 @@ Error = improv_ns.enum("Error") State = improv_ns.enum("State") esp32_improv_ns = cg.esphome_ns.namespace("esp32_improv") -ESP32ImprovComponent = esp32_improv_ns.class_( - "ESP32ImprovComponent", cg.Component, esp32_ble_server.BLEServiceComponent -) +ESP32ImprovComponent = esp32_improv_ns.class_("ESP32ImprovComponent", cg.Component) ESP32ImprovProvisionedTrigger = esp32_improv_ns.class_( "ESP32ImprovProvisionedTrigger", automation.Trigger.template() ) @@ -47,7 +45,6 @@ ESP32ImprovStoppedTrigger = esp32_improv_ns.class_( CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(ESP32ImprovComponent), - cv.GenerateID(CONF_BLE_SERVER_ID): cv.use_id(esp32_ble_server.BLEServer), cv.Required(CONF_AUTHORIZER): cv.Any( cv.none, cv.use_id(binary_sensor.BinarySensor) ), @@ -100,9 +97,6 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) - ble_server = await cg.get_variable(config[CONF_BLE_SERVER_ID]) - cg.add(ble_server.register_service_component(var)) - cg.add_define("USE_IMPROV") cg.add_library("improv/Improv", "1.2.4") diff --git a/esphome/components/esp32_improv/esp32_improv_component.cpp b/esphome/components/esp32_improv/esp32_improv_component.cpp index c67431077c..b720425506 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.cpp +++ b/esphome/components/esp32_improv/esp32_improv_component.cpp @@ -4,12 +4,15 @@ #include "esphome/components/esp32_ble_server/ble_2902.h" #include "esphome/core/application.h" #include "esphome/core/log.h" +#include "esphome/components/bytebuffer/bytebuffer.h" #ifdef USE_ESP32 namespace esphome { namespace esp32_improv { +using namespace bytebuffer; + static const char *const TAG = "esp32_improv.component"; static const char *const ESPHOME_MY_LINK = "https://my.home-assistant.io/redirect/config_flow_start?domain=esphome"; @@ -26,6 +29,8 @@ void ESP32ImprovComponent::setup() { }); } #endif + global_ble_server->on(BLEServerEvt::EmptyEvt::ON_DISCONNECT, + [this](uint16_t conn_id) { this->set_error_(improv::ERROR_NONE); }); } void ESP32ImprovComponent::setup_characteristics() { @@ -40,11 +45,12 @@ void ESP32ImprovComponent::setup_characteristics() { this->error_->add_descriptor(error_descriptor); this->rpc_ = this->service_->create_characteristic(improv::RPC_COMMAND_UUID, BLECharacteristic::PROPERTY_WRITE); - this->rpc_->on_write([this](const std::vector &data) { - if (!data.empty()) { - this->incoming_data_.insert(this->incoming_data_.end(), data.begin(), data.end()); - } - }); + this->rpc_->EventEmitter, uint16_t>::on( + BLECharacteristicEvt::VectorEvt::ON_WRITE, [this](const std::vector &data, uint16_t id) { + if (!data.empty()) { + this->incoming_data_.insert(this->incoming_data_.end(), data.begin(), data.end()); + } + }); BLEDescriptor *rpc_descriptor = new BLE2902(); this->rpc_->add_descriptor(rpc_descriptor); @@ -62,7 +68,7 @@ void ESP32ImprovComponent::setup_characteristics() { if (this->status_indicator_ != nullptr) capabilities |= improv::CAPABILITY_IDENTIFY; #endif - this->capabilities_->set_value(capabilities); + this->capabilities_->set_value(ByteBuffer::wrap(capabilities)); this->setup_complete_ = true; } @@ -80,8 +86,7 @@ void ESP32ImprovComponent::loop() { if (this->service_ == nullptr) { // Setup the service ESP_LOGD(TAG, "Creating Improv service"); - global_ble_server->create_service(ESPBTUUID::from_raw(improv::SERVICE_UUID), true); - this->service_ = global_ble_server->get_service(ESPBTUUID::from_raw(improv::SERVICE_UUID)); + this->service_ = global_ble_server->create_service(ESPBTUUID::from_raw(improv::SERVICE_UUID), true); this->setup_characteristics(); } @@ -93,15 +98,15 @@ void ESP32ImprovComponent::loop() { case improv::STATE_STOPPED: this->set_status_indicator_state_(false); - if (this->service_->is_created() && this->should_start_ && this->setup_complete_) { - if (this->service_->is_running()) { + if (this->should_start_ && this->setup_complete_) { + if (this->service_->is_created()) { + this->service_->start(); + } else if (this->service_->is_running()) { esp32_ble::global_ble->advertising_start(); this->set_state_(improv::STATE_AWAITING_AUTHORIZATION); this->set_error_(improv::ERROR_NONE); ESP_LOGD(TAG, "Service started!"); - } else { - this->service_->start(); } } break; @@ -199,8 +204,7 @@ void ESP32ImprovComponent::set_state_(improv::State state) { ESP_LOGV(TAG, "Setting state: %d", state); this->state_ = state; if (this->status_->get_value().empty() || this->status_->get_value()[0] != state) { - uint8_t data[1]{state}; - this->status_->set_value(data, 1); + this->status_->set_value(ByteBuffer::wrap(static_cast(state))); if (state != improv::STATE_STOPPED) this->status_->notify(); } @@ -232,15 +236,14 @@ void ESP32ImprovComponent::set_error_(improv::Error error) { ESP_LOGE(TAG, "Error: %d", error); } if (this->error_->get_value().empty() || this->error_->get_value()[0] != error) { - uint8_t data[1]{error}; - this->error_->set_value(data, 1); + this->error_->set_value(ByteBuffer::wrap(static_cast(error))); if (this->state_ != improv::STATE_STOPPED) this->error_->notify(); } } void ESP32ImprovComponent::send_response_(std::vector &response) { - this->rpc_response_->set_value(response); + this->rpc_response_->set_value(ByteBuffer::wrap(response)); if (this->state_ != improv::STATE_STOPPED) this->rpc_response_->notify(); } @@ -339,8 +342,6 @@ void ESP32ImprovComponent::on_wifi_connect_timeout_() { wifi::global_wifi_component->clear_sta(); } -void ESP32ImprovComponent::on_client_disconnect() { this->set_error_(improv::ERROR_NONE); }; - ESP32ImprovComponent *global_improv_component = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) } // namespace esp32_improv diff --git a/esphome/components/esp32_improv/esp32_improv_component.h b/esphome/components/esp32_improv/esp32_improv_component.h index 062b3f585b..87cec23876 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.h +++ b/esphome/components/esp32_improv/esp32_improv_component.h @@ -32,18 +32,17 @@ namespace esp32_improv { using namespace esp32_ble_server; -class ESP32ImprovComponent : public Component, public BLEServiceComponent { +class ESP32ImprovComponent : public Component { public: ESP32ImprovComponent(); void dump_config() override; void loop() override; void setup() override; void setup_characteristics(); - void on_client_disconnect() override; float get_setup_priority() const override; - void start() override; - void stop() override; + void start(); + void stop(); bool is_active() const { return this->state_ != improv::STATE_STOPPED; } #ifdef USE_ESP32_IMPROV_STATE_CALLBACK diff --git a/esphome/components/event_emitter/__init__.py b/esphome/components/event_emitter/__init__.py new file mode 100644 index 0000000000..fcbbf26f02 --- /dev/null +++ b/esphome/components/event_emitter/__init__.py @@ -0,0 +1,5 @@ +CODEOWNERS = ["@Rapsssito"] + +# Allows event_emitter to be configured in yaml, to allow use of the C++ api. + +CONFIG_SCHEMA = {} diff --git a/esphome/components/event_emitter/event_emitter.cpp b/esphome/components/event_emitter/event_emitter.cpp new file mode 100644 index 0000000000..8487e19c2f --- /dev/null +++ b/esphome/components/event_emitter/event_emitter.cpp @@ -0,0 +1,14 @@ +#include "event_emitter.h" + +namespace esphome { +namespace event_emitter { + +static const char *const TAG = "event_emitter"; + +void raise_event_emitter_full_error() { + ESP_LOGE(TAG, "EventEmitter has reached the maximum number of listeners for event"); + ESP_LOGW(TAG, "Removing listener to make space for new listener"); +} + +} // namespace event_emitter +} // namespace esphome diff --git a/esphome/components/event_emitter/event_emitter.h b/esphome/components/event_emitter/event_emitter.h new file mode 100644 index 0000000000..3876a2cc14 --- /dev/null +++ b/esphome/components/event_emitter/event_emitter.h @@ -0,0 +1,63 @@ +#pragma once +#include +#include +#include +#include + +#include "esphome/core/log.h" + +namespace esphome { +namespace event_emitter { + +using EventEmitterListenerID = uint32_t; +void raise_event_emitter_full_error(); + +// EventEmitter class that can emit events with a specific name (it is highly recommended to use an enum class for this) +// and a list of arguments. Supports multiple listeners for each event. +template class EventEmitter { + public: + EventEmitterListenerID on(EvtType event, std::function listener) { + EventEmitterListenerID listener_id = get_next_id_(event); + listeners_[event][listener_id] = listener; + return listener_id; + } + + void off(EvtType event, EventEmitterListenerID id) { + if (listeners_.count(event) == 0) + return; + listeners_[event].erase(id); + } + + protected: + void emit_(EvtType event, Args... args) { + if (listeners_.count(event) == 0) + return; + for (const auto &listener : listeners_[event]) { + listener.second(args...); + } + } + + EventEmitterListenerID get_next_id_(EvtType event) { + // Check if the map is full + if (listeners_[event].size() == std::numeric_limits::max()) { + // Raise an error if the map is full + raise_event_emitter_full_error(); + off(event, 0); + return 0; + } + // Get the next ID for the given event. + EventEmitterListenerID next_id = (current_id_ + 1) % std::numeric_limits::max(); + while (listeners_[event].count(next_id) > 0) { + next_id = (next_id + 1) % std::numeric_limits::max(); + } + current_id_ = next_id; + return current_id_; + } + + private: + std::unordered_map>> listeners_; + EventEmitterListenerID current_id_ = 0; +}; + +} // namespace event_emitter +} // namespace esphome diff --git a/esphome/const.py b/esphome/const.py index 95bf6afc02..ab41d8cbc2 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -530,6 +530,7 @@ CONF_NETWORKS = "networks" CONF_NEW_PASSWORD = "new_password" CONF_NITROGEN_DIOXIDE = "nitrogen_dioxide" CONF_NOISE_LEVEL = "noise_level" +CONF_NOTIFY = "notify" CONF_NUM_ATTEMPTS = "num_attempts" CONF_NUM_CHANNELS = "num_channels" CONF_NUM_CHIPS = "num_chips" diff --git a/tests/components/esp32_ble_server/common.yaml b/tests/components/esp32_ble_server/common.yaml index 29a5407f84..696f4ea8fe 100644 --- a/tests/components/esp32_ble_server/common.yaml +++ b/tests/components/esp32_ble_server/common.yaml @@ -1,3 +1,66 @@ esp32_ble_server: - id: ble + id: ble_server manufacturer_data: [0x72, 0x4, 0x00, 0x23] + manufacturer: ESPHome + model: Test + on_connect: + - lambda: |- + ESP_LOGD("BLE", "Connection from %d", id); + on_disconnect: + - lambda: |- + ESP_LOGD("BLE", "Disconnection from %d", id); + services: + - uuid: 2a24b789-7aab-4535-af3e-ee76a35cc12d + advertise: false + characteristics: + - id: test_notify_characteristic + description: "Notify characteristic" + uuid: cad48e28-7fbe-41cf-bae9-d77a6c233423 + read: true + notify: true + value: [1, 2, 3, 4] + descriptors: + - uuid: cad48e28-7fbe-41cf-bae9-d77a6c111111 + on_write: + logger.log: + format: "Descriptor write id %u, data %s" + args: [id, 'format_hex_pretty(x.data(), x.size()).c_str()'] + value: + data: "123.1" + type: float + endianness: BIG + - uuid: 2a24b789-7a1b-4535-af3e-ee76a35cc42d + advertise: false + characteristics: + - id: test_change_characteristic + uuid: 2a24b789-7a1b-4535-af3e-ee76a35cc11c + read: true + value: + data: "Initial" + string_encoding: utf-8 + description: Change characteristic + descriptors: + - uuid: 0x4414 + id: test_change_descriptor + value: "Initial descriptor value" + - uuid: 0x2312 + value: + data: 0x12 + type: uint16_t + on_write: + - lambda: |- + ESP_LOGD("BLE", "Descriptor received: %s from %d", std::string(x.begin(), x.end()).c_str(), id); + - uuid: 2a24b789-7a1b-4535-af3e-ee76a35cc99a + write: true + on_write: + then: + - lambda: |- + ESP_LOGD("BLE", "Characteristic received: %s from %d", std::string(x.begin(), x.end()).c_str(), id); + - ble_server.characteristic.set_value: + id: test_change_characteristic + value: !lambda 'return bytebuffer::ByteBuffer::wrap({0x00, 0x01, 0x02}).get_data();' + - ble_server.characteristic.notify: + id: test_notify_characteristic + - ble_server.descriptor.set_value: + id: test_change_descriptor + value: !lambda return bytebuffer::ByteBuffer::wrap({0x03, 0x04, 0x05}).get_data(); From 7dab1a6082a2b9938f0c7f28d6a9861df0ad718b Mon Sep 17 00:00:00 2001 From: guillempages Date: Wed, 29 Jan 2025 00:35:43 +0100 Subject: [PATCH 05/17] [online_image] Add JPEG support to online_image (#8127) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jimmy Hedman Co-authored-by: J. Nick Koston Co-authored-by: Rodrigo Martín Co-authored-by: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> --- esphome/components/online_image/__init__.py | 14 ++- .../components/online_image/image_decoder.cpp | 15 ++++ .../components/online_image/image_decoder.h | 2 + .../components/online_image/jpeg_image.cpp | 89 +++++++++++++++++++ esphome/components/online_image/jpeg_image.h | 34 +++++++ .../components/online_image/online_image.cpp | 22 ++++- .../components/online_image/online_image.h | 17 +++- esphome/components/online_image/png_image.cpp | 1 - esphome/core/defines.h | 1 + platformio.ini | 2 + tests/components/online_image/common.yaml | 8 ++ 11 files changed, 199 insertions(+), 6 deletions(-) create mode 100644 esphome/components/online_image/jpeg_image.cpp create mode 100644 esphome/components/online_image/jpeg_image.h diff --git a/esphome/components/online_image/__init__.py b/esphome/components/online_image/__init__.py index eb6debf8eb..c476270571 100644 --- a/esphome/components/online_image/__init__.py +++ b/esphome/components/online_image/__init__.py @@ -60,6 +60,15 @@ class BMPFormat(Format): cg.add_define("USE_ONLINE_IMAGE_BMP_SUPPORT") +class JPEGFormat(Format): + def __init__(self): + super().__init__("JPEG") + + def actions(self): + cg.add_define("USE_ONLINE_IMAGE_JPEG_SUPPORT") + cg.add_library("JPEGDEC", "1.6.2", "https://github.com/bitbank2/JPEGDEC") + + class PNGFormat(Format): def __init__(self): super().__init__("PNG") @@ -69,14 +78,15 @@ class PNGFormat(Format): cg.add_library("pngle", "1.0.2") -# New formats can be added here. IMAGE_FORMATS = { x.image_type: x for x in ( BMPFormat(), + JPEGFormat(), PNGFormat(), ) } +IMAGE_FORMATS.update({"JPG": IMAGE_FORMATS["JPEG"]}) OnlineImage = online_image_ns.class_("OnlineImage", cg.PollingComponent, Image_) @@ -116,7 +126,7 @@ ONLINE_IMAGE_SCHEMA = ( cv.Required(CONF_URL): cv.url, cv.Required(CONF_FORMAT): cv.one_of(*IMAGE_FORMATS, upper=True), cv.Optional(CONF_PLACEHOLDER): cv.use_id(Image_), - cv.Optional(CONF_BUFFER_SIZE, default=2048): cv.int_range(256, 65536), + cv.Optional(CONF_BUFFER_SIZE, default=65536): cv.int_range(256, 65536), cv.Optional(CONF_ON_DOWNLOAD_FINISHED): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( diff --git a/esphome/components/online_image/image_decoder.cpp b/esphome/components/online_image/image_decoder.cpp index d0d0495ba6..2958d8671d 100644 --- a/esphome/components/online_image/image_decoder.cpp +++ b/esphome/components/online_image/image_decoder.cpp @@ -41,5 +41,20 @@ size_t DownloadBuffer::read(size_t len) { return this->unread_; } +size_t DownloadBuffer::resize(size_t size) { + if (this->size_ == size) { + return size; + } + this->allocator_.deallocate(this->buffer_, this->size_); + this->size_ = size; + this->buffer_ = this->allocator_.allocate(size); + this->reset(); + if (this->buffer_) { + return size; + } else { + return 0; + } +} + } // namespace online_image } // namespace esphome diff --git a/esphome/components/online_image/image_decoder.h b/esphome/components/online_image/image_decoder.h index 4e5dd7b229..957af49ac9 100644 --- a/esphome/components/online_image/image_decoder.h +++ b/esphome/components/online_image/image_decoder.h @@ -106,6 +106,8 @@ class DownloadBuffer { void reset() { this->unread_ = 0; } + size_t resize(size_t size); + protected: RAMAllocator allocator_{}; uint8_t *buffer_; diff --git a/esphome/components/online_image/jpeg_image.cpp b/esphome/components/online_image/jpeg_image.cpp new file mode 100644 index 0000000000..773b85a2c4 --- /dev/null +++ b/esphome/components/online_image/jpeg_image.cpp @@ -0,0 +1,89 @@ +#include "jpeg_image.h" +#ifdef USE_ONLINE_IMAGE_JPEG_SUPPORT + +#include "esphome/components/display/display_buffer.h" +#include "esphome/core/application.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +#include "online_image.h" +static const char *const TAG = "online_image.jpeg"; + +namespace esphome { +namespace online_image { + +/** + * @brief Callback method that will be called by the JPEGDEC engine when a chunk + * of the image is decoded. + * + * @param jpeg The JPEGDRAW object, including the context data. + */ +static int draw_callback(JPEGDRAW *jpeg) { + ImageDecoder *decoder = (ImageDecoder *) jpeg->pUser; + + // Some very big images take too long to decode, so feed the watchdog on each callback + // to avoid crashing. + App.feed_wdt(); + size_t position = 0; + for (size_t y = 0; y < jpeg->iHeight; y++) { + for (size_t x = 0; x < jpeg->iWidth; x++) { + auto rg = decode_value(jpeg->pPixels[position++]); + auto ba = decode_value(jpeg->pPixels[position++]); + Color color(rg[1], rg[0], ba[1], ba[0]); + + if (!decoder) { + ESP_LOGE(TAG, "Decoder pointer is null!"); + return 0; + } + decoder->draw(jpeg->x + x, jpeg->y + y, 1, 1, color); + } + } + return 1; +} + +void JpegDecoder::prepare(size_t download_size) { + ImageDecoder::prepare(download_size); + auto size = this->image_->resize_download_buffer(download_size); + if (size < download_size) { + ESP_LOGE(TAG, "Resize failed!"); + // TODO: return an error code; + } +} + +int HOT JpegDecoder::decode(uint8_t *buffer, size_t size) { + if (size < this->download_size_) { + ESP_LOGV(TAG, "Download not complete. Size: %d/%d", size, this->download_size_); + return 0; + } + + if (!this->jpeg_.openRAM(buffer, size, draw_callback)) { + ESP_LOGE(TAG, "Could not open image for decoding."); + return DECODE_ERROR_INVALID_TYPE; + } + auto jpeg_type = this->jpeg_.getJPEGType(); + if (jpeg_type == JPEG_MODE_INVALID) { + ESP_LOGE(TAG, "Unsupported JPEG image"); + return DECODE_ERROR_INVALID_TYPE; + } else if (jpeg_type == JPEG_MODE_PROGRESSIVE) { + ESP_LOGE(TAG, "Progressive JPEG images not supported"); + return DECODE_ERROR_INVALID_TYPE; + } + ESP_LOGD(TAG, "Image size: %d x %d, bpp: %d", this->jpeg_.getWidth(), this->jpeg_.getHeight(), this->jpeg_.getBpp()); + + this->jpeg_.setUserPointer(this); + this->jpeg_.setPixelType(RGB8888); + this->set_size(this->jpeg_.getWidth(), this->jpeg_.getHeight()); + if (!this->jpeg_.decode(0, 0, 0)) { + ESP_LOGE(TAG, "Error while decoding."); + this->jpeg_.close(); + return DECODE_ERROR_UNSUPPORTED_FORMAT; + } + this->decoded_bytes_ = size; + this->jpeg_.close(); + return size; +} + +} // namespace online_image +} // namespace esphome + +#endif // USE_ONLINE_IMAGE_JPEG_SUPPORT diff --git a/esphome/components/online_image/jpeg_image.h b/esphome/components/online_image/jpeg_image.h new file mode 100644 index 0000000000..f04a35655a --- /dev/null +++ b/esphome/components/online_image/jpeg_image.h @@ -0,0 +1,34 @@ +#pragma once + +#include "image_decoder.h" +#include "esphome/core/defines.h" +#ifdef USE_ONLINE_IMAGE_JPEG_SUPPORT +#include + +namespace esphome { +namespace online_image { + +/** + * @brief Image decoder specialization for JPEG images. + */ +class JpegDecoder : public ImageDecoder { + public: + /** + * @brief Construct a new JPEG Decoder object. + * + * @param display The image to decode the stream into. + */ + JpegDecoder(OnlineImage *image) : ImageDecoder(image) {} + ~JpegDecoder() override {} + + void prepare(size_t download_size) override; + int HOT decode(uint8_t *buffer, size_t size) override; + + protected: + JPEGDEC jpeg_{}; +}; + +} // namespace online_image +} // namespace esphome + +#endif // USE_ONLINE_IMAGE_JPEG_SUPPORT diff --git a/esphome/components/online_image/online_image.cpp b/esphome/components/online_image/online_image.cpp index c6499c24e4..b08c14b721 100644 --- a/esphome/components/online_image/online_image.cpp +++ b/esphome/components/online_image/online_image.cpp @@ -9,6 +9,9 @@ static const char *const TAG = "online_image"; #ifdef USE_ONLINE_IMAGE_BMP_SUPPORT #include "bmp_image.h" #endif +#ifdef USE_ONLINE_IMAGE_JPEG_SUPPORT +#include "jpeg_image.h" +#endif #ifdef USE_ONLINE_IMAGE_PNG_SUPPORT #include "png_image.h" #endif @@ -32,6 +35,7 @@ OnlineImage::OnlineImage(const std::string &url, int width, int height, ImageFor : Image(nullptr, 0, 0, type, transparency), buffer_(nullptr), download_buffer_(download_buffer_size), + download_buffer_initial_size_(download_buffer_size), format_(format), fixed_width_(width), fixed_height_(height) { @@ -123,23 +127,32 @@ void OnlineImage::update() { #ifdef USE_ONLINE_IMAGE_BMP_SUPPORT if (this->format_ == ImageFormat::BMP) { + ESP_LOGD(TAG, "Allocating BMP decoder"); this->decoder_ = make_unique(this); } #endif // ONLINE_IMAGE_BMP_SUPPORT +#ifdef USE_ONLINE_IMAGE_JPEG_SUPPORT + if (this->format_ == ImageFormat::JPEG) { + ESP_LOGD(TAG, "Allocating JPEG decoder"); + this->decoder_ = esphome::make_unique(this); + } +#endif // USE_ONLINE_IMAGE_JPEG_SUPPORT #ifdef USE_ONLINE_IMAGE_PNG_SUPPORT if (this->format_ == ImageFormat::PNG) { + ESP_LOGD(TAG, "Allocating PNG decoder"); this->decoder_ = make_unique(this); } #endif // ONLINE_IMAGE_PNG_SUPPORT if (!this->decoder_) { - ESP_LOGE(TAG, "Could not instantiate decoder. Image format unsupported."); + ESP_LOGE(TAG, "Could not instantiate decoder. Image format unsupported: %d", this->format_); this->end_connection_(); this->download_error_callback_.call(); return; } this->decoder_->prepare(total_size); - ESP_LOGI(TAG, "Downloading image"); + ESP_LOGI(TAG, "Downloading image (Size: %d)", total_size); + this->start_time_ = ::time(nullptr); } void OnlineImage::loop() { @@ -153,6 +166,7 @@ void OnlineImage::loop() { this->height_ = buffer_height_; ESP_LOGD(TAG, "Image fully downloaded, read %zu bytes, width/height = %d/%d", this->downloader_->get_bytes_read(), this->width_, this->height_); + ESP_LOGD(TAG, "Total time: %lds", ::time(nullptr) - this->start_time_); this->end_connection_(); this->download_finished_callback_.call(); return; @@ -163,6 +177,10 @@ void OnlineImage::loop() { } size_t available = this->download_buffer_.free_capacity(); if (available) { + // Some decoders need to fully download the image before downloading. + // In case of huge images, don't wait blocking until the whole image has been downloaded, + // use smaller chunks + available = std::min(available, this->download_buffer_initial_size_); auto len = this->downloader_->read(this->download_buffer_.append(), available); if (len > 0) { this->download_buffer_.write(len); diff --git a/esphome/components/online_image/online_image.h b/esphome/components/online_image/online_image.h index 849f860ad5..4abc047083 100644 --- a/esphome/components/online_image/online_image.h +++ b/esphome/components/online_image/online_image.h @@ -23,7 +23,7 @@ using t_http_codes = enum { enum ImageFormat { /** Automatically detect from MIME type. Not supported yet. */ AUTO, - /** JPEG format. Not supported yet. */ + /** JPEG format. */ JPEG, /** PNG format. */ PNG, @@ -79,6 +79,13 @@ class OnlineImage : public PollingComponent, */ void release(); + /** + * Resize the download buffer + * + * @param size The new size for the download buffer. + */ + size_t resize_download_buffer(size_t size) { return this->download_buffer_.resize(size); } + void add_on_finished_callback(std::function &&callback); void add_on_error_callback(std::function &&callback); @@ -119,6 +126,12 @@ class OnlineImage : public PollingComponent, uint8_t *buffer_; DownloadBuffer download_buffer_; + /** + * This is the *initial* size of the download buffer, not the current size. + * The download buffer can be resized at runtime; the download_buffer_initial_size_ + * will *not* change even if the download buffer has been resized. + */ + size_t download_buffer_initial_size_; const ImageFormat format_; image::Image *placeholder_{nullptr}; @@ -148,6 +161,8 @@ class OnlineImage : public PollingComponent, */ int buffer_height_; + time_t start_time_; + friend bool ImageDecoder::set_size(int width, int height); friend void ImageDecoder::draw(int x, int y, int w, int h, const Color &color); }; diff --git a/esphome/components/online_image/png_image.cpp b/esphome/components/online_image/png_image.cpp index 59c1ce6c7d..6bc968c7ba 100644 --- a/esphome/components/online_image/png_image.cpp +++ b/esphome/components/online_image/png_image.cpp @@ -2,7 +2,6 @@ #ifdef USE_ONLINE_IMAGE_PNG_SUPPORT #include "esphome/components/display/display_buffer.h" -#include "esphome/core/application.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 074b19809f..211f3b8319 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -62,6 +62,7 @@ #define USE_NUMBER #define USE_ONLINE_IMAGE_BMP_SUPPORT #define USE_ONLINE_IMAGE_PNG_SUPPORT +#define USE_ONLINE_IMAGE_JPEG_SUPPORT #define USE_OTA #define USE_OTA_PASSWORD #define USE_OTA_STATE_CALLBACK diff --git a/platformio.ini b/platformio.ini index b9b80e933f..e91c06d86e 100644 --- a/platformio.ini +++ b/platformio.ini @@ -41,6 +41,8 @@ lib_deps = functionpointer/arduino-MLX90393@1.0.2 ; mlx90393 pavlodn/HaierProtocol@0.9.31 ; haier kikuchan98/pngle@1.0.2 ; online_image + ; Using the repository directly, otherwise ESP-IDF can't use the library + https://github.com/bitbank2/JPEGDEC.git#1.6.2 ; online_image ; This is using the repository until a new release is published to PlatformIO https://github.com/Sensirion/arduino-gas-index-algorithm.git#3.2.1 ; Sensirion Gas Index Algorithm Arduino Library lvgl/lvgl@8.4.0 ; lvgl diff --git a/tests/components/online_image/common.yaml b/tests/components/online_image/common.yaml index d75900adf9..69daa915c5 100644 --- a/tests/components/online_image/common.yaml +++ b/tests/components/online_image/common.yaml @@ -30,6 +30,14 @@ online_image: url: https://samples-files.com/samples/images/bmp/480-360-sample.bmp format: BMP type: BINARY + - id: online_jpeg_image + url: http://www.faqs.org/images/library.jpg + format: JPEG + type: RGB + - id: online_jpg_image + url: http://www.faqs.org/images/library.jpg + format: JPG + type: RGB565 # Check the set_url action esphome: From 2489f951071ae45743a0dc1e28aa354bfc1250bd Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Wed, 29 Jan 2025 10:58:06 +1100 Subject: [PATCH 06/17] [logger] Ensure PRIu32 and friends are available (#8155) --- esphome/core/log.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/core/log.h b/esphome/core/log.h index 99a68024c5..9ab80afe78 100644 --- a/esphome/core/log.h +++ b/esphome/core/log.h @@ -2,6 +2,8 @@ #include #include +// for PRIu32 and friends +#include #include #ifdef USE_STORE_LOG_STR_IN_FLASH From a23ce416ea150c1d0fda829664097f7a26253223 Mon Sep 17 00:00:00 2001 From: Stefan Rado <628587+kroimon@users.noreply.github.com> Date: Wed, 29 Jan 2025 04:54:10 +0100 Subject: [PATCH 07/17] Fix forgotten uses of use_transparency (#8115) --- esphome/components/image/__init__.py | 6 ++---- tests/components/lvgl/lvgl-package.yaml | 4 ++-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/esphome/components/image/__init__.py b/esphome/components/image/__init__.py index 7cdce757be..20b041a321 100644 --- a/esphome/components/image/__init__.py +++ b/esphome/components/image/__init__.py @@ -273,11 +273,9 @@ IMAGE_TYPE = { "GRAYSCALE": ImageGrayscale, "RGB565": ImageRGB565, "RGB": ImageRGB, - "TRANSPARENT_BINARY": ReplaceWith( - "'type: BINARY' and 'use_transparency: chroma_key'" - ), + "TRANSPARENT_BINARY": ReplaceWith("'type: BINARY' and 'transparency: chroma_key'"), "RGB24": ReplaceWith("'type: RGB'"), - "RGBA": ReplaceWith("'type: RGB' and 'use_transparency: alpha_channel'"), + "RGBA": ReplaceWith("'type: RGB' and 'transparency: alpha_channel'"), } TransparencyType = image_ns.enum("TransparencyType") diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index b3227bb96e..c51a3d03e7 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -834,12 +834,12 @@ image: resize: 256x48 file: $component_dir/logo-text.svg type: RGB565 - use_transparency: alpha_channel + transparency: alpha_channel - id: dog_image file: $component_dir/logo-text.svg resize: 256x48 type: BINARY - use_transparency: chroma_key + transparency: chroma_key color: - id: light_blue From 9957840dfc7544eec0369fd5cec9a4a190dc8ea5 Mon Sep 17 00:00:00 2001 From: Jimmy Hedman Date: Wed, 29 Jan 2025 11:00:18 +0100 Subject: [PATCH 08/17] Add multicast support to udp component (#8051) --- esphome/components/network/ip_address.h | 2 ++ esphome/components/udp/__init__.py | 6 ++++++ esphome/components/udp/udp_component.cpp | 18 ++++++++++++++++++ esphome/components/udp/udp_component.h | 3 +++ esphome/config_validation.py | 9 +++++++++ tests/components/udp/common.yaml | 1 + 6 files changed, 39 insertions(+) diff --git a/esphome/components/network/ip_address.h b/esphome/components/network/ip_address.h index 69d3788ca5..1598daf6f9 100644 --- a/esphome/components/network/ip_address.h +++ b/esphome/components/network/ip_address.h @@ -49,6 +49,7 @@ struct IPAddress { } IPAddress(const std::string &in_address) { inet_aton(in_address.c_str(), &ip_addr_); } IPAddress(const ip_addr_t *other_ip) { ip_addr_ = *other_ip; } + std::string str() const { return str_lower_case(inet_ntoa(ip_addr_)); } #else IPAddress() { ip_addr_set_zero(&ip_addr_); } IPAddress(uint8_t first, uint8_t second, uint8_t third, uint8_t fourth) { @@ -119,6 +120,7 @@ struct IPAddress { bool is_set() { return !ip_addr_isany(&ip_addr_); } // NOLINT(readability-simplify-boolean-expr) bool is_ip4() { return IP_IS_V4(&ip_addr_); } bool is_ip6() { return IP_IS_V6(&ip_addr_); } + bool is_multicast() { return ip_addr_ismulticast(&ip_addr_); } std::string str() const { return str_lower_case(ipaddr_ntoa(&ip_addr_)); } bool operator==(const IPAddress &other) const { return ip_addr_cmp(&ip_addr_, &other.ip_addr_); } bool operator!=(const IPAddress &other) const { return !ip_addr_cmp(&ip_addr_, &other.ip_addr_); } diff --git a/esphome/components/udp/__init__.py b/esphome/components/udp/__init__.py index e189975ade..5485663f1c 100644 --- a/esphome/components/udp/__init__.py +++ b/esphome/components/udp/__init__.py @@ -27,6 +27,7 @@ UDPComponent = udp_ns.class_("UDPComponent", cg.PollingComponent) CONF_BROADCAST = "broadcast" CONF_BROADCAST_ID = "broadcast_id" CONF_ADDRESSES = "addresses" +CONF_LISTEN_ADDRESS = "listen_address" CONF_PROVIDER = "provider" CONF_PROVIDERS = "providers" CONF_REMOTE_ID = "remote_id" @@ -84,6 +85,9 @@ CONFIG_SCHEMA = cv.All( { cv.GenerateID(): cv.declare_id(UDPComponent), cv.Optional(CONF_PORT, default=18511): cv.port, + cv.Optional( + CONF_LISTEN_ADDRESS, default="255.255.255.255" + ): cv.ipv4address_multi_broadcast, cv.Optional(CONF_ADDRESSES, default=["255.255.255.255"]): cv.ensure_list( cv.ipv4address, ), @@ -154,5 +158,7 @@ async def to_code(config): for provider in config.get(CONF_PROVIDERS, ()): name = provider[CONF_NAME] cg.add(var.add_provider(name)) + if (listen_address := str(config[CONF_LISTEN_ADDRESS])) != "255.255.255.255": + cg.add(var.set_listen_address(listen_address)) if encryption := provider.get(CONF_ENCRYPTION): cg.add(var.set_provider_encryption(name, hash_encryption_key(encryption))) diff --git a/esphome/components/udp/udp_component.cpp b/esphome/components/udp/udp_component.cpp index e29620fa9a..30f7356879 100644 --- a/esphome/components/udp/udp_component.cpp +++ b/esphome/components/udp/udp_component.cpp @@ -249,6 +249,21 @@ void UDPComponent::setup() { server.sin_addr.s_addr = ESPHOME_INADDR_ANY; server.sin_port = htons(this->port_); + if (this->listen_address_.has_value()) { + struct ip_mreq imreq = {}; + imreq.imr_interface.s_addr = ESPHOME_INADDR_ANY; + inet_aton(this->listen_address_.value().str().c_str(), &imreq.imr_multiaddr); + server.sin_addr.s_addr = imreq.imr_multiaddr.s_addr; + ESP_LOGV(TAG, "Join multicast %s", this->listen_address_.value().str().c_str()); + err = this->listen_socket_->setsockopt(IPPROTO_IP, IP_ADD_MEMBERSHIP, &imreq, sizeof(imreq)); + if (err < 0) { + ESP_LOGE(TAG, "Failed to set IP_ADD_MEMBERSHIP. Error %d", errno); + this->mark_failed(); + this->status_set_error("Failed to set IP_ADD_MEMBERSHIP"); + return; + } + } + err = this->listen_socket_->bind((struct sockaddr *) &server, sizeof(server)); if (err != 0) { ESP_LOGE(TAG, "Socket unable to bind: errno %d", errno); @@ -565,6 +580,9 @@ void UDPComponent::dump_config() { ESP_LOGCONFIG(TAG, " Ping-pong: %s", YESNO(this->ping_pong_enable_)); for (const auto &address : this->addresses_) ESP_LOGCONFIG(TAG, " Address: %s", address.c_str()); + if (this->listen_address_.has_value()) { + ESP_LOGCONFIG(TAG, " Listen address: %s", this->listen_address_.value().str().c_str()); + } #ifdef USE_SENSOR for (auto sensor : this->sensors_) ESP_LOGCONFIG(TAG, " Sensor: %s", sensor.id); diff --git a/esphome/components/udp/udp_component.h b/esphome/components/udp/udp_component.h index b4e11cf652..fb9b93e255 100644 --- a/esphome/components/udp/udp_component.h +++ b/esphome/components/udp/udp_component.h @@ -1,6 +1,7 @@ #pragma once #include "esphome/core/component.h" +#include "esphome/components/network/ip_address.h" #ifdef USE_SENSOR #include "esphome/components/sensor/sensor.h" #endif @@ -69,6 +70,7 @@ class UDPComponent : public PollingComponent { } #endif void add_address(const char *addr) { this->addresses_.emplace_back(addr); } + void set_listen_address(const char *listen_addr) { this->listen_address_ = network::IPAddress(listen_addr); } void set_port(uint16_t port) { this->port_ = port; } float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } @@ -143,6 +145,7 @@ class UDPComponent : public PollingComponent { std::map> remote_binary_sensors_{}; #endif + optional listen_address_{}; std::map providers_{}; std::vector ping_header_{}; std::vector header_{}; diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 20a0774ccb..27d11e4ded 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -1168,6 +1168,15 @@ def ipv4address(value): return address +def ipv4address_multi_broadcast(value): + address = ipv4address(value) + if not (address.is_multicast or (address == IPv4Address("255.255.255.255"))): + raise Invalid( + f"{value} is not a multicasst address nor local broadcast address" + ) + return address + + def ipaddress(value): try: address = ip_address(value) diff --git a/tests/components/udp/common.yaml b/tests/components/udp/common.yaml index 3bdc19ece5..e533cb965e 100644 --- a/tests/components/udp/common.yaml +++ b/tests/components/udp/common.yaml @@ -7,6 +7,7 @@ udp: encryption: "our key goes here" rolling_code_enable: true ping_pong_enable: true + listen_address: 239.0.60.53 binary_sensors: - binary_sensor_id1 - id: binary_sensor_id1 From 619ce93dece3bc066115ae1ec828b9f88925333a Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Wed, 29 Jan 2025 21:45:29 +1100 Subject: [PATCH 09/17] [display] Properly handle case of auto_clear_enabled: false (#8156) --- esphome/components/display/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/display/__init__.py b/esphome/components/display/__init__.py index 99224df7b3..12c63231e7 100644 --- a/esphome/components/display/__init__.py +++ b/esphome/components/display/__init__.py @@ -101,7 +101,7 @@ async def setup_display_core_(var, config): if CONF_ROTATION in config: cg.add(var.set_rotation(DISPLAY_ROTATIONS[config[CONF_ROTATION]])) - if auto_clear := config.get(CONF_AUTO_CLEAR_ENABLED): + if (auto_clear := config.get(CONF_AUTO_CLEAR_ENABLED)) is not None: # Default to true if pages or lambda is specified. Ideally this would be done during validation, but # the possible schemas are too complex to do this easily. if auto_clear == CONF_UNSPECIFIED: From 67ccd0eb7f864307891d71983338acc99547e808 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 29 Jan 2025 05:51:04 -0500 Subject: [PATCH 10/17] [esp32_rmt] Increase default symbols in led strip and remove IRAM config (#8133) --- esphome/components/esp32_rmt_led_strip/light.py | 12 ++++++------ esphome/components/remote_receiver/__init__.py | 5 +---- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/esphome/components/esp32_rmt_led_strip/light.py b/esphome/components/esp32_rmt_led_strip/light.py index 64104bb6de..e2c9f7e64a 100644 --- a/esphome/components/esp32_rmt_led_strip/light.py +++ b/esphome/components/esp32_rmt_led_strip/light.py @@ -127,12 +127,12 @@ CONFIG_SCHEMA = cv.All( ), OptionalForIDF5( CONF_RMT_SYMBOLS, - esp32_idf=64, - esp32_s2_idf=64, - esp32_s3_idf=48, - esp32_c3_idf=48, - esp32_c6_idf=48, - esp32_h2_idf=48, + esp32_idf=192, + esp32_s2_idf=192, + esp32_s3_idf=192, + esp32_c3_idf=96, + esp32_c6_idf=96, + esp32_h2_idf=96, ): cv.All(only_with_new_rmt_driver, cv.int_range(min=2)), cv.Optional(CONF_MAX_REFRESH_RATE): cv.positive_time_period_microseconds, cv.Optional(CONF_CHIPSET): cv.one_of(*CHIPSETS, upper=True), diff --git a/esphome/components/remote_receiver/__init__.py b/esphome/components/remote_receiver/__init__.py index b01443a974..8c21cb210c 100644 --- a/esphome/components/remote_receiver/__init__.py +++ b/esphome/components/remote_receiver/__init__.py @@ -1,6 +1,6 @@ from esphome import pins import esphome.codegen as cg -from esphome.components import esp32, esp32_rmt, remote_base +from esphome.components import esp32_rmt, remote_base import esphome.config_validation as cv from esphome.const import ( CONF_BUFFER_SIZE, @@ -158,9 +158,6 @@ async def to_code(config): cg.add(var.set_clock_resolution(config[CONF_CLOCK_RESOLUTION])) if CONF_FILTER_SYMBOLS in config: cg.add(var.set_filter_symbols(config[CONF_FILTER_SYMBOLS])) - if CORE.using_esp_idf: - esp32.add_idf_sdkconfig_option("CONFIG_RMT_RECV_FUNC_IN_IRAM", True) - esp32.add_idf_sdkconfig_option("CONFIG_RMT_ISR_IRAM_SAFE", True) else: if (rmt_channel := config.get(CONF_RMT_CHANNEL, None)) is not None: var = cg.new_Pvariable( From ba3e5e8ecb77639b549114217c128e4e3cc58341 Mon Sep 17 00:00:00 2001 From: Olliver Schinagl Date: Wed, 29 Jan 2025 12:27:55 +0100 Subject: [PATCH 11/17] =?UTF-8?q?[climate]=20Accept=20=C2=B0K=20as=20inten?= =?UTF-8?q?ded=20(#8134)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- esphome/components/climate/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/climate/__init__.py b/esphome/components/climate/__init__.py index ec68940726..aa705e7332 100644 --- a/esphome/components/climate/__init__.py +++ b/esphome/components/climate/__init__.py @@ -115,7 +115,7 @@ CONF_MAX_HUMIDITY = "max_humidity" CONF_TARGET_HUMIDITY = "target_humidity" visual_temperature = cv.float_with_unit( - "visual_temperature", "(°C|° C|°|C|° K|° K|K|°F|° F|F)?" + "visual_temperature", "(°C|° C|°|C|°K|° K|K|°F|° F|F)?" ) From f9856135d0fd6c919f732e18f374d3b7367ac209 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 29 Jan 2025 13:40:25 +0100 Subject: [PATCH 12/17] Bump docker/build-push-action from 6.12.0 to 6.13.0 in /.github/actions/build-image (#8136) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/actions/build-image/action.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/actions/build-image/action.yaml b/.github/actions/build-image/action.yaml index b2a3394563..25ae21fbd0 100644 --- a/.github/actions/build-image/action.yaml +++ b/.github/actions/build-image/action.yaml @@ -46,7 +46,7 @@ runs: - name: Build and push to ghcr by digest id: build-ghcr - uses: docker/build-push-action@v6.12.0 + uses: docker/build-push-action@v6.13.0 env: DOCKER_BUILD_SUMMARY: false DOCKER_BUILD_RECORD_UPLOAD: false @@ -72,7 +72,7 @@ runs: - name: Build and push to dockerhub by digest id: build-dockerhub - uses: docker/build-push-action@v6.12.0 + uses: docker/build-push-action@v6.13.0 env: DOCKER_BUILD_SUMMARY: false DOCKER_BUILD_RECORD_UPLOAD: false From 334e952a340e57e84351fcb59ee29745ecb6d47e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 29 Jan 2025 13:40:31 +0100 Subject: [PATCH 13/17] Bump pypa/gh-action-pypi-publish from 1.12.3 to 1.12.4 (#8137) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 962bc66e94..1d6813b1d1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -65,7 +65,7 @@ jobs: pip3 install build python3 -m build - name: Publish - uses: pypa/gh-action-pypi-publish@v1.12.3 + uses: pypa/gh-action-pypi-publish@v1.12.4 deploy-docker: name: Build ESPHome ${{ matrix.platform }} From 7727879f01b4d52417b447446ab4575f301310d4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 29 Jan 2025 14:30:30 +0100 Subject: [PATCH 14/17] Bump actions/setup-python from 5.3.0 to 5.4.0 (#8154) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci-api-proto.yml | 2 +- .github/workflows/ci-docker.yml | 2 +- .github/workflows/ci.yml | 2 +- .github/workflows/release.yml | 4 ++-- .github/workflows/sync-device-classes.yml | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci-api-proto.yml b/.github/workflows/ci-api-proto.yml index a6b2e2b2b3..d06d8a8eec 100644 --- a/.github/workflows/ci-api-proto.yml +++ b/.github/workflows/ci-api-proto.yml @@ -23,7 +23,7 @@ jobs: - name: Checkout uses: actions/checkout@v4.1.7 - name: Set up Python - uses: actions/setup-python@v5.3.0 + uses: actions/setup-python@v5.4.0 with: python-version: "3.11" diff --git a/.github/workflows/ci-docker.yml b/.github/workflows/ci-docker.yml index b994cfaf17..65f847bc66 100644 --- a/.github/workflows/ci-docker.yml +++ b/.github/workflows/ci-docker.yml @@ -42,7 +42,7 @@ jobs: steps: - uses: actions/checkout@v4.1.7 - name: Set up Python - uses: actions/setup-python@v5.3.0 + uses: actions/setup-python@v5.4.0 with: python-version: "3.9" - name: Set up Docker Buildx diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a344b177ae..ab77db5ca5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,7 +42,7 @@ jobs: run: echo key="${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }}" >> $GITHUB_OUTPUT - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v5.3.0 + uses: actions/setup-python@v5.4.0 with: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore Python virtual environment diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1d6813b1d1..ca266c1f2c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -53,7 +53,7 @@ jobs: steps: - uses: actions/checkout@v4.1.7 - name: Set up Python - uses: actions/setup-python@v5.3.0 + uses: actions/setup-python@v5.4.0 with: python-version: "3.x" - name: Set up python environment @@ -85,7 +85,7 @@ jobs: steps: - uses: actions/checkout@v4.1.7 - name: Set up Python - uses: actions/setup-python@v5.3.0 + uses: actions/setup-python@v5.4.0 with: python-version: "3.9" diff --git a/.github/workflows/sync-device-classes.yml b/.github/workflows/sync-device-classes.yml index 9160ab4a1b..9abbb20e86 100644 --- a/.github/workflows/sync-device-classes.yml +++ b/.github/workflows/sync-device-classes.yml @@ -22,7 +22,7 @@ jobs: path: lib/home-assistant - name: Setup Python - uses: actions/setup-python@v5.3.0 + uses: actions/setup-python@v5.4.0 with: python-version: 3.12 From 12d6c1bbcae83cb79ed5f6018c18339a42a4dbf5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 29 Jan 2025 14:31:49 +0100 Subject: [PATCH 15/17] Bump actions/setup-python from 5.3.0 to 5.4.0 in /.github/actions/restore-python (#8153) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/actions/restore-python/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/restore-python/action.yml b/.github/actions/restore-python/action.yml index 6b87cf0170..e95eb6331f 100644 --- a/.github/actions/restore-python/action.yml +++ b/.github/actions/restore-python/action.yml @@ -17,7 +17,7 @@ runs: steps: - name: Set up Python ${{ inputs.python-version }} id: python - uses: actions/setup-python@v5.3.0 + uses: actions/setup-python@v5.4.0 with: python-version: ${{ inputs.python-version }} - name: Restore Python virtual environment From 714e2d3e56be5ecb1a0e1a1ea8a5ec1167f7e6c5 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 29 Jan 2025 08:34:10 -0500 Subject: [PATCH 16/17] [remote_transmitter] Fix issues with 32bit rollover on esp8266 and libretiny (#8056) Co-authored-by: Jonathan Swoboda --- .../remote_transmitter/remote_transmitter_esp8266.cpp | 6 +++--- .../remote_transmitter/remote_transmitter_libretiny.cpp | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/esphome/components/remote_transmitter/remote_transmitter_esp8266.cpp b/esphome/components/remote_transmitter/remote_transmitter_esp8266.cpp index 613f00b7f5..09cc16e975 100644 --- a/esphome/components/remote_transmitter/remote_transmitter_esp8266.cpp +++ b/esphome/components/remote_transmitter/remote_transmitter_esp8266.cpp @@ -37,7 +37,7 @@ void RemoteTransmitterComponent::await_target_time_() { const uint32_t current_time = micros(); if (this->target_time_ == 0) { this->target_time_ = current_time; - } else if (this->target_time_ > current_time) { + } else if ((int32_t) (this->target_time_ - current_time) > 0) { delayMicroseconds(this->target_time_ - current_time); } } @@ -50,13 +50,13 @@ void RemoteTransmitterComponent::mark_(uint32_t on_time, uint32_t off_time, uint if (this->carrier_duty_percent_ < 100 && (on_time > 0 || off_time > 0)) { while (true) { // Modulate with carrier frequency this->target_time_ += on_time; - if (this->target_time_ >= target) + if ((int32_t) (this->target_time_ - target) >= 0) break; this->await_target_time_(); this->pin_->digital_write(false); this->target_time_ += off_time; - if (this->target_time_ >= target) + if ((int32_t) (this->target_time_ - target) >= 0) break; this->await_target_time_(); this->pin_->digital_write(true); diff --git a/esphome/components/remote_transmitter/remote_transmitter_libretiny.cpp b/esphome/components/remote_transmitter/remote_transmitter_libretiny.cpp index ad9265fb14..20d8736c00 100644 --- a/esphome/components/remote_transmitter/remote_transmitter_libretiny.cpp +++ b/esphome/components/remote_transmitter/remote_transmitter_libretiny.cpp @@ -38,7 +38,7 @@ void RemoteTransmitterComponent::await_target_time_() { if (this->target_time_ == 0) { this->target_time_ = current_time; } else { - while (this->target_time_ > micros()) { + while ((int32_t) (this->target_time_ - micros()) > 0) { // busy loop that ensures micros is constantly called } } @@ -52,13 +52,13 @@ void RemoteTransmitterComponent::mark_(uint32_t on_time, uint32_t off_time, uint if (this->carrier_duty_percent_ < 100 && (on_time > 0 || off_time > 0)) { while (true) { // Modulate with carrier frequency this->target_time_ += on_time; - if (this->target_time_ >= target) + if ((int32_t) (this->target_time_ - target) >= 0) break; this->await_target_time_(); this->pin_->digital_write(false); this->target_time_ += off_time; - if (this->target_time_ >= target) + if ((int32_t) (this->target_time_ - target) >= 0) break; this->await_target_time_(); this->pin_->digital_write(true); From 73923976302825a2951bd8683cee3eda6a906b76 Mon Sep 17 00:00:00 2001 From: NicoIIT <69118344+NicoIIT@users.noreply.github.com> Date: Wed, 29 Jan 2025 15:03:42 +0100 Subject: [PATCH 17/17] Use abspath for config path dir (#8044) --- esphome/core/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/core/__init__.py b/esphome/core/__init__.py index f26c3da483..59cea6f09b 100644 --- a/esphome/core/__init__.py +++ b/esphome/core/__init__.py @@ -582,7 +582,7 @@ class EsphomeCore: @property def config_dir(self): - return os.path.dirname(self.config_path) + return os.path.dirname(os.path.abspath(self.config_path)) @property def data_dir(self):