diff --git a/esphome/components/ble_client/__init__.py b/esphome/components/ble_client/__init__.py index f907d1b6b2..95c186d36a 100644 --- a/esphome/components/ble_client/__init__.py +++ b/esphome/components/ble_client/__init__.py @@ -334,21 +334,23 @@ async def register_ble_node(var, config): cg.add(parent.register_ble_node(var)) -async def register_gatt_node(var, config): - """Register a node on the platform-neutral interface (both engines).""" - parent = await cg.get_variable(config[CONF_BLE_CLIENT_ID]) - # Sizes the client's neutral-node storage on every platform. +def _request_gatt_node_build() -> None: + """Node storage plus, on esp32, the bridge and shared-materializer + defines one neutral node needs compiled in.""" _request_node_slot() if CORE.is_esp32: - # The bridge, the neutral table structs, and the shared materializer. # Deliberately not ble_device_base.request_gatt_client(): that would # claim a phantom backend slot on combined proxy builds. cg.add_define("USE_BLE_CLIENT_GATT_NODES") cg.add_define("USE_BLE_GATT_CLIENT") cg.add_define("USE_BLE_GATT_SERVICE_TABLE") - cg.add(parent.register_gatt_node(var)) - else: - cg.add(parent.register_ble_node(var)) + + +async def register_gatt_node(var, config): + """Register a node on the platform-neutral interface (both engines).""" + parent = await cg.get_variable(config[CONF_BLE_CLIENT_ID]) + _request_gatt_node_build() + cg.add(parent.register_gatt_node(var)) BLE_WRITE_ACTION_SCHEMA = cv.Schema( @@ -427,9 +429,8 @@ async def ble_connect_to_code(config, action_id, template_arg, args): ) async def ble_write_to_code(config, action_id, template_arg, args): parent = await cg.get_variable(config[CONF_ID]) - if not CORE.is_esp32: - # The neutral action registers itself as a node in its constructor. - _request_node_slot() + # The action registers itself as a neutral node in its constructor. + _request_gatt_node_build() var = cg.new_Pvariable(action_id, template_arg, parent) value = config[CONF_VALUE] diff --git a/esphome/components/ble_client/automation.h b/esphome/components/ble_client/automation.h index 94eeb83b3e..4a68a5e3a6 100644 --- a/esphome/components/ble_client/automation.h +++ b/esphome/components/ble_client/automation.h @@ -10,9 +10,6 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -// Maximum bytes to log in hex format for BLE writes (many logging buffers are 256 chars) -static constexpr size_t BLE_WRITE_MAX_LOG_BYTES = 64; - namespace esphome::ble_client { // placeholder class for static TAG . @@ -93,144 +90,6 @@ class BLEClientNumericComparisonRequestTrigger final : public Trigger, } }; -// implement the ble_client.ble_write action. -template class BLEClientWriteAction final : public Action, public BLEClientNode { - public: - BLEClientWriteAction(BLEClient *ble_client) { - ble_client->register_ble_node(this); - ble_client_ = ble_client; - } - - void set_service_uuid16(uint16_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); } - void set_service_uuid32(uint32_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); } - void set_service_uuid128(uint8_t *uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_raw(uuid); } - - void set_char_uuid16(uint16_t uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); } - void set_char_uuid32(uint32_t uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); } - void set_char_uuid128(uint8_t *uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_raw(uuid); } - - void set_value_template(std::vector (*func)(Ts...)) { - this->value_.func = func; - this->len_ = -1; // Sentinel value indicates template mode - } - - // Store pointer to static data in flash (no RAM copy) - void set_value_simple(const uint8_t *data, size_t len) { - this->value_.data = data; - this->len_ = len; // Length >= 0 indicates static mode - } - - void play(const Ts &...x) override {} - - void play_complex(const Ts &...x) override { - this->num_running_++; - this->var_ = std::make_tuple(x...); - - bool result; - if (this->len_ >= 0) { - // Static mode: write directly from flash pointer - result = this->write(this->value_.data, this->len_); - } else { - // Template mode: call function and write the vector - std::vector value = this->value_.func(x...); - result = this->write(value); - } - - // on write failure, continue the automation chain rather than stopping so that e.g. disconnect can work. - if (!result) - this->play_next_(x...); - } - - /** - * Note about logging: the esph_log_X macros are used here because the CI checks complain about use of the ESP LOG - * macros in header files (Can't even write it in a comment!) - * Not sure why, because they seem to work just fine. - * The problem is that the implementation of a templated class can't be placed in a .cpp file when using C++ less than - * 17, so the methods have to be here. The esph_log_X macros are equivalent in function, but don't trigger the CI - * errors. - */ - // initiate the write. Return true if all went well, will be followed by a WRITE_CHAR event. - bool write(const uint8_t *data, size_t len) { - if (this->node_state != espbt::ClientState::ESTABLISHED) { - esph_log_w(Automation::TAG, "Cannot write to BLE characteristic - not connected"); - return false; - } -#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE - char hex_buf[format_hex_pretty_size(BLE_WRITE_MAX_LOG_BYTES)]; - esph_log_vv(Automation::TAG, "Will write %d bytes: %s", len, format_hex_pretty_to(hex_buf, data, len)); -#endif - esp_err_t err = - esp_ble_gattc_write_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), this->char_handle_, len, - const_cast(data), this->write_type_, ESP_GATT_AUTH_REQ_NONE); - if (err != ESP_OK) { - esph_log_e(Automation::TAG, "Error writing to characteristic: %s!", esp_err_to_name(err)); - return false; - } - return true; - } - - bool write(const std::vector &value) { return this->write(value.data(), value.size()); } - - void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, - esp_ble_gattc_cb_param_t *param) override { - switch (event) { - case ESP_GATTC_WRITE_CHAR_EVT: - // upstream code checked the MAC address, verify the characteristic. - if (param->write.handle == this->char_handle_) - this->parent()->run_later([this]() { this->play_next_tuple_(this->var_); }); - break; - case ESP_GATTC_DISCONNECT_EVT: - if (this->num_running_ != 0) - this->stop_complex(); - break; - case ESP_GATTC_SEARCH_CMPL_EVT: { - auto *chr = this->parent()->get_characteristic(this->service_uuid_, this->char_uuid_); - if (chr == nullptr) { - char char_buf[esp32_ble::UUID_STR_LEN]; - char service_buf[esp32_ble::UUID_STR_LEN]; - esph_log_w("ble_write_action", "Characteristic %s was not found in service %s", - this->char_uuid_.to_str(char_buf), this->service_uuid_.to_str(service_buf)); - break; - } - this->char_handle_ = chr->handle; - this->char_props_ = chr->properties; - if (this->char_props_ & ESP_GATT_CHAR_PROP_BIT_WRITE) { - this->write_type_ = ESP_GATT_WRITE_TYPE_RSP; - esph_log_d(Automation::TAG, "Write type: ESP_GATT_WRITE_TYPE_RSP"); - } else if (this->char_props_ & ESP_GATT_CHAR_PROP_BIT_WRITE_NR) { - this->write_type_ = ESP_GATT_WRITE_TYPE_NO_RSP; - esph_log_d(Automation::TAG, "Write type: ESP_GATT_WRITE_TYPE_NO_RSP"); - } else { - char char_buf[esp32_ble::UUID_STR_LEN]; - esph_log_e(Automation::TAG, "Characteristic %s does not allow writing", this->char_uuid_.to_str(char_buf)); - break; - } - this->node_state = espbt::ClientState::ESTABLISHED; - char char_buf[esp32_ble::UUID_STR_LEN]; - esph_log_d(Automation::TAG, "Found characteristic %s on device %s", this->char_uuid_.to_str(char_buf), - ble_client_->address_str()); - break; - } - default: - break; - } - } - - private: - BLEClient *ble_client_; - ssize_t len_{-1}; // -1 = template mode, >=0 = static mode with length - union Value { - std::vector (*func)(Ts...); // Function pointer (stateless lambdas) - const uint8_t *data; // Pointer to static data in flash - } value_; - espbt::ESPBTUUID service_uuid_; - espbt::ESPBTUUID char_uuid_; - std::tuple var_{}; - uint16_t char_handle_{}; - esp_gatt_char_prop_t char_props_{}; - esp_gatt_write_type_t write_type_{}; -}; - template class BLEClientPasskeyReplyAction final : public Action { public: BLEClientPasskeyReplyAction(BLEClient *ble_client) { parent_ = ble_client; } diff --git a/esphome/components/ble_client/automation_gatt.h b/esphome/components/ble_client/automation_gatt.h index c59d3c245e..d4666c59f0 100644 --- a/esphome/components/ble_client/automation_gatt.h +++ b/esphome/components/ble_client/automation_gatt.h @@ -19,9 +19,6 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -// Maximum bytes to log in hex format for BLE writes (many logging buffers are 256 chars) -static constexpr size_t BLE_WRITE_MAX_LOG_BYTES = 64; - namespace esphome::ble_client { // placeholder class for static TAG (shared with automation.cpp). @@ -46,135 +43,6 @@ class BLEClientDisconnectTrigger final : public Trigger<> { } }; -template class BLEClientWriteAction final : public Action, public BLEClientNode { - public: - BLEClientWriteAction(BLEClient *ble_client) { - ble_client->register_ble_node(this); - ble_client_ = ble_client; - } - - void set_service_uuid16(uint16_t uuid) { this->service_uuid_ = ble_device_base::ESPBTUUID::from_uint16(uuid); } - void set_service_uuid32(uint32_t uuid) { this->service_uuid_ = ble_device_base::ESPBTUUID::from_uint32(uuid); } - void set_service_uuid128(uint8_t *uuid) { this->service_uuid_ = ble_device_base::ESPBTUUID::from_raw(uuid); } - - void set_char_uuid16(uint16_t uuid) { this->char_uuid_ = ble_device_base::ESPBTUUID::from_uint16(uuid); } - void set_char_uuid32(uint32_t uuid) { this->char_uuid_ = ble_device_base::ESPBTUUID::from_uint32(uuid); } - void set_char_uuid128(uint8_t *uuid) { this->char_uuid_ = ble_device_base::ESPBTUUID::from_raw(uuid); } - - void set_value_template(std::vector (*func)(Ts...)) { - this->value_.func = func; - this->len_ = -1; // Sentinel value indicates template mode - } - - // Store pointer to static data in flash (no RAM copy) - void set_value_simple(const uint8_t *data, size_t len) { - this->value_.data = data; - this->len_ = len; // Length >= 0 indicates static mode - } - - void play(const Ts &...x) override {} - - void play_complex(const Ts &...x) override { - this->num_running_++; - this->var_ = std::make_tuple(x...); - - bool result; - if (this->len_ >= 0) { - result = this->write(this->value_.data, this->len_); - } else { - std::vector value = this->value_.func(x...); - result = this->write(value.data(), value.size()); - } - - // on write failure, continue the automation chain rather than stopping so - // that e.g. disconnect can work. - if (!result) - this->play_next_(x...); - } - - // Initiate the write; the completion arrives in on_write_result. The - // response-less path can complete synchronously inside the call, so the - // handle is armed before the backend is touched. - bool write(const uint8_t *data, size_t len) { - if (!this->ble_client_->connected()) { - esph_log_w(Automation::TAG, "Cannot write to BLE characteristic - not connected"); - return false; - } - if (!this->resolved_) { - esph_log_w(Automation::TAG, "Cannot write to BLE characteristic - characteristic was not resolved"); - return false; - } -#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE - char hex_buf[format_hex_pretty_size(BLE_WRITE_MAX_LOG_BYTES)]; - esph_log_vv(Automation::TAG, "Will write %d bytes: %s", len, format_hex_pretty_to(hex_buf, data, len)); -#endif - int err = this->ble_client_->write_characteristic(this->char_handle_, data, len, this->write_response_); - if (err != 0) { - esph_log_e(Automation::TAG, "Error writing to characteristic: %d!", err); - return false; - } - return true; - } - - void on_connected(const ble_device_base::GattServiceTable &table) override { - const auto *service = ble_device_base::find_service(table, this->service_uuid_); - const auto *chr = - service == nullptr ? nullptr : ble_device_base::find_characteristic(table, *service, this->char_uuid_); - if (chr == nullptr) { - char char_buf[ble_device_base::UUID_STR_LEN]; - char service_buf[ble_device_base::UUID_STR_LEN]; - esph_log_w("ble_write_action", "Characteristic %s was not found in service %s", this->char_uuid_.to_str(char_buf), - this->service_uuid_.to_str(service_buf)); - return; - } - if (chr->properties & ble_device_base::GATT_CHAR_PROP_WRITE) { - this->write_response_ = true; - } else if (chr->properties & ble_device_base::GATT_CHAR_PROP_WRITE_NO_RSP) { - this->write_response_ = false; - } else { - char char_buf[ble_device_base::UUID_STR_LEN]; - esph_log_e(Automation::TAG, "Characteristic %s does not allow writing", this->char_uuid_.to_str(char_buf)); - return; - } - this->char_handle_ = chr->value_handle; - this->resolved_ = true; - char char_buf[ble_device_base::UUID_STR_LEN]; - esph_log_d(Automation::TAG, "Found characteristic %s on device %s", this->char_uuid_.to_str(char_buf), - this->ble_client_->address_str()); - } - - void on_disconnected() override { - this->resolved_ = false; - this->char_handle_ = 0; - if (this->num_running_ != 0) - this->stop_complex(); - } - - void on_write_result(uint16_t handle, int error) override { - if (this->resolved_ && handle == this->char_handle_ && this->num_running_ != 0) { - if (error != 0) { - // Continue the chain (legacy parity) but leave a breadcrumb. - esph_log_w(Automation::TAG, "Write completed with status %d", error); - } - this->ble_client_->run_later([this]() { this->play_next_tuple_(this->var_); }); - } - } - - private: - BLEClient *ble_client_; - ssize_t len_{-1}; // -1 = template mode, >=0 = static mode with length - union Value { - std::vector (*func)(Ts...); // Function pointer (stateless lambdas) - const uint8_t *data; // Pointer to static data in flash - } value_; - ble_device_base::ESPBTUUID service_uuid_; - ble_device_base::ESPBTUUID char_uuid_; - std::tuple var_{}; - uint16_t char_handle_{}; - bool write_response_{false}; - bool resolved_{false}; -}; - template class BLEClientConnectAction final : public Action { public: BLEClientConnectAction(BLEClient *ble_client) { diff --git a/esphome/components/ble_client/ble_client_gatt.h b/esphome/components/ble_client/ble_client_gatt.h index 0560d85ba8..166519d42f 100644 --- a/esphome/components/ble_client/ble_client_gatt.h +++ b/esphome/components/ble_client/ble_client_gatt.h @@ -47,6 +47,9 @@ class BLEClient : public Component, const char *address_str() const { return this->address_str_; } void register_ble_node(BLEClientNode *node); + // Every node on this engine is a neutral node; one registration spelling + // shared with the esp32 engine's bridge. + void register_gatt_node(BLEClientNode *node) { this->register_ble_node(node); } bool connected() const { return this->state_ == State::CONNECTED; } bool idle() const { return this->state_ == State::IDLE; } diff --git a/esphome/components/ble_client/ble_write_action.h b/esphome/components/ble_client/ble_write_action.h new file mode 100644 index 0000000000..89fa745ab5 --- /dev/null +++ b/esphome/components/ble_client/ble_write_action.h @@ -0,0 +1,158 @@ +// The ble_client.ble_write action: a node on the platform-neutral interface, +// so one implementation serves both engines (the esp32 bridge and the +// neutral engine). + +#pragma once + +#include "esphome/core/defines.h" + +#if defined(USE_BLE_CLIENT_GATT_NODES) || (defined(USE_BLE_GATT_CLIENT) && !defined(USE_ESP32)) + +#include +#include + +// One of the two resolves per build; both define class Automation's TAG box. +#include "automation.h" +#include "automation_gatt.h" +#include "ble_client_node.h" +#include "esphome/core/automation.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome::ble_client { + +// Maximum bytes to log in hex format for BLE writes (many logging buffers are 256 chars) +static constexpr size_t BLE_WRITE_MAX_LOG_BYTES = 64; + +template class BLEClientWriteAction final : public Action, public BLEClientNode { + public: + BLEClientWriteAction(BLEClient *ble_client) { + ble_client->register_gatt_node(this); + ble_client_ = ble_client; + } + + void set_service_uuid16(uint16_t uuid) { this->service_uuid_ = ble_device_base::ESPBTUUID::from_uint16(uuid); } + void set_service_uuid32(uint32_t uuid) { this->service_uuid_ = ble_device_base::ESPBTUUID::from_uint32(uuid); } + void set_service_uuid128(uint8_t *uuid) { this->service_uuid_ = ble_device_base::ESPBTUUID::from_raw(uuid); } + + void set_char_uuid16(uint16_t uuid) { this->char_uuid_ = ble_device_base::ESPBTUUID::from_uint16(uuid); } + void set_char_uuid32(uint32_t uuid) { this->char_uuid_ = ble_device_base::ESPBTUUID::from_uint32(uuid); } + void set_char_uuid128(uint8_t *uuid) { this->char_uuid_ = ble_device_base::ESPBTUUID::from_raw(uuid); } + + void set_value_template(std::vector (*func)(Ts...)) { + this->value_.func = func; + this->len_ = -1; // Sentinel value indicates template mode + } + + // Store pointer to static data in flash (no RAM copy) + void set_value_simple(const uint8_t *data, size_t len) { + this->value_.data = data; + this->len_ = len; // Length >= 0 indicates static mode + } + + void play(const Ts &...x) override {} + + void play_complex(const Ts &...x) override { + this->num_running_++; + this->var_ = std::make_tuple(x...); + + bool result; + if (this->len_ >= 0) { + result = this->write(this->value_.data, this->len_); + } else { + std::vector value = this->value_.func(x...); + result = this->write(value.data(), value.size()); + } + + // on write failure, continue the automation chain rather than stopping so + // that e.g. disconnect can work. + if (!result) + this->play_next_(x...); + } + + // Initiate the write; the completion arrives in on_write_result. The + // response-less path can complete synchronously inside the call, so the + // handle is armed before the backend is touched. + bool write(const uint8_t *data, size_t len) { + if (!this->ble_client_->connected()) { + esph_log_w(Automation::TAG, "Cannot write to BLE characteristic - not connected"); + return false; + } + if (!this->resolved_) { + esph_log_w(Automation::TAG, "Cannot write to BLE characteristic - characteristic was not resolved"); + return false; + } +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE + char hex_buf[format_hex_pretty_size(BLE_WRITE_MAX_LOG_BYTES)]; + esph_log_vv(Automation::TAG, "Will write %d bytes: %s", len, format_hex_pretty_to(hex_buf, data, len)); +#endif + int err = this->ble_client_->write_characteristic(this->char_handle_, data, len, this->write_response_); + if (err != 0) { + esph_log_e(Automation::TAG, "Error writing to characteristic: %d!", err); + return false; + } + return true; + } + + void on_connected(const ble_device_base::GattServiceTable &table) override { + const auto *service = ble_device_base::find_service(table, this->service_uuid_); + const auto *chr = + service == nullptr ? nullptr : ble_device_base::find_characteristic(table, *service, this->char_uuid_); + if (chr == nullptr) { + char char_buf[ble_device_base::UUID_STR_LEN]; + char service_buf[ble_device_base::UUID_STR_LEN]; + esph_log_w("ble_write_action", "Characteristic %s was not found in service %s", this->char_uuid_.to_str(char_buf), + this->service_uuid_.to_str(service_buf)); + return; + } + if (chr->properties & ble_device_base::GATT_CHAR_PROP_WRITE) { + this->write_response_ = true; + } else if (chr->properties & ble_device_base::GATT_CHAR_PROP_WRITE_NO_RSP) { + this->write_response_ = false; + } else { + char char_buf[ble_device_base::UUID_STR_LEN]; + esph_log_e(Automation::TAG, "Characteristic %s does not allow writing", this->char_uuid_.to_str(char_buf)); + return; + } + this->char_handle_ = chr->value_handle; + this->resolved_ = true; + char char_buf[ble_device_base::UUID_STR_LEN]; + esph_log_d(Automation::TAG, "Found characteristic %s on device %s", this->char_uuid_.to_str(char_buf), + this->ble_client_->address_str()); + } + + void on_disconnected() override { + this->resolved_ = false; + this->char_handle_ = 0; + if (this->num_running_ != 0) + this->stop_complex(); + } + + void on_write_result(uint16_t handle, int error) override { + if (this->resolved_ && handle == this->char_handle_ && this->num_running_ != 0) { + if (error != 0) { + // Continue the chain (legacy parity) but leave a breadcrumb. + esph_log_w(Automation::TAG, "Write completed with status %d", error); + } + this->ble_client_->run_later([this]() { this->play_next_tuple_(this->var_); }); + } + } + + private: + BLEClient *ble_client_; + ssize_t len_{-1}; // -1 = template mode, >=0 = static mode with length + union Value { + std::vector (*func)(Ts...); // Function pointer (stateless lambdas) + const uint8_t *data; // Pointer to static data in flash + } value_; + ble_device_base::ESPBTUUID service_uuid_; + ble_device_base::ESPBTUUID char_uuid_; + std::tuple var_{}; + uint16_t char_handle_{}; + bool write_response_{false}; + bool resolved_{false}; +}; + +} // namespace esphome::ble_client + +#endif // USE_BLE_CLIENT_GATT_NODES || (USE_BLE_GATT_CLIENT && !USE_ESP32)