diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index 6b6b19e079..db16e12ae2 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -73,6 +73,28 @@ void ESP32BLE::advertising_set_manufacturer_data(const std::vector &dat this->advertising_start(); } +void ESP32BLE::advertising_set_service_data_and_name(std::span data, bool include_name) { + // This method atomically updates both service data and device name inclusion in BLE advertising. + // When include_name is true, the device name is included in the advertising packet making it + // visible to passive BLE scanners. When false, the name is only visible in scan response + // (requires active scanning). This atomic operation ensures we only restart advertising once + // when changing both properties, avoiding the brief gap that would occur with separate calls. + + this->advertising_init_(); + bool needs_restart = false; + + this->advertising_->set_service_data(data); + + if (this->advertising_->get_include_name() != include_name) { + this->advertising_->set_include_name(include_name); + needs_restart = true; + } + + if (needs_restart || !data.empty()) { + this->advertising_start(); + } +} + void ESP32BLE::advertising_register_raw_advertisement_callback(std::function &&callback) { this->advertising_init_(); this->advertising_->register_raw_advertisement_callback(std::move(callback)); diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h index 368ac644cf..1aa3bc86ef 100644 --- a/esphome/components/esp32_ble/ble.h +++ b/esphome/components/esp32_ble/ble.h @@ -9,6 +9,7 @@ #endif #include +#include #include "esphome/core/automation.h" #include "esphome/core/component.h" @@ -118,6 +119,7 @@ class ESP32BLE : public Component { void advertising_set_service_data(const std::vector &data); void advertising_set_manufacturer_data(const std::vector &data); void advertising_set_appearance(uint16_t appearance) { this->appearance_ = appearance; } + void advertising_set_service_data_and_name(std::span data, bool include_name); void advertising_add_service_uuid(ESPBTUUID uuid); void advertising_remove_service_uuid(ESPBTUUID uuid); void advertising_register_raw_advertisement_callback(std::function &&callback); diff --git a/esphome/components/esp32_ble/ble_advertising.cpp b/esphome/components/esp32_ble/ble_advertising.cpp index d8b9b1cc36..df70768c23 100644 --- a/esphome/components/esp32_ble/ble_advertising.cpp +++ b/esphome/components/esp32_ble/ble_advertising.cpp @@ -43,7 +43,7 @@ void BLEAdvertising::remove_service_uuid(ESPBTUUID uuid) { this->advertising_uuids_.end()); } -void BLEAdvertising::set_service_data(const std::vector &data) { +void BLEAdvertising::set_service_data(std::span data) { delete[] this->advertising_data_.p_service_data; this->advertising_data_.p_service_data = nullptr; this->advertising_data_.service_data_len = data.size(); @@ -54,6 +54,10 @@ void BLEAdvertising::set_service_data(const std::vector &data) { } } +void BLEAdvertising::set_service_data(const std::vector &data) { + this->set_service_data(std::span(data)); +} + void BLEAdvertising::set_manufacturer_data(const std::vector &data) { delete[] this->advertising_data_.p_manufacturer_data; this->advertising_data_.p_manufacturer_data = nullptr; @@ -84,7 +88,7 @@ esp_err_t BLEAdvertising::services_advertisement_() { esp_err_t err; this->advertising_data_.set_scan_rsp = false; - this->advertising_data_.include_name = !this->scan_response_; + this->advertising_data_.include_name = this->include_name_in_adv_ || !this->scan_response_; this->advertising_data_.include_txpower = !this->scan_response_; err = esp_ble_gap_config_adv_data(&this->advertising_data_); if (err != ESP_OK) { diff --git a/esphome/components/esp32_ble/ble_advertising.h b/esphome/components/esp32_ble/ble_advertising.h index e373554ea9..83db8fcd31 100644 --- a/esphome/components/esp32_ble/ble_advertising.h +++ b/esphome/components/esp32_ble/ble_advertising.h @@ -4,6 +4,7 @@ #include #include +#include #include #ifdef USE_ESP32 @@ -36,6 +37,9 @@ class BLEAdvertising { void set_manufacturer_data(const std::vector &data); void set_appearance(uint16_t appearance) { this->advertising_data_.appearance = appearance; } void set_service_data(const std::vector &data); + void set_service_data(std::span data); + void set_include_name(bool include_name) { this->include_name_in_adv_ = include_name; } + bool get_include_name() const { return this->include_name_in_adv_; } void register_raw_advertisement_callback(std::function &&callback); void start(); @@ -45,6 +49,7 @@ class BLEAdvertising { esp_err_t services_advertisement_(); bool scan_response_; + bool include_name_in_adv_{false}; esp_ble_adv_data_t advertising_data_; esp_ble_adv_data_t scan_response_data_; esp_ble_adv_params_t advertising_params_; diff --git a/esphome/components/esp32_improv/esp32_improv_component.cpp b/esphome/components/esp32_improv/esp32_improv_component.cpp index c5a0b89f99..1c3ea538f3 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.cpp +++ b/esphome/components/esp32_improv/esp32_improv_component.cpp @@ -17,6 +17,8 @@ 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"; static constexpr uint16_t STOP_ADVERTISING_DELAY = 10000; // Delay (ms) before stopping service to allow BLE clients to read the final state +static constexpr uint16_t NAME_ADVERTISING_INTERVAL = 60000; // Advertise name every 60 seconds +static constexpr uint16_t NAME_ADVERTISING_DURATION = 1000; // Advertise name for 1 second ESP32ImprovComponent::ESP32ImprovComponent() { global_improv_component = this; } @@ -99,6 +101,11 @@ void ESP32ImprovComponent::loop() { this->process_incoming_data_(); uint32_t now = App.get_loop_component_start_time(); + // Check if we need to update advertising type + if (this->state_ != improv::STATE_STOPPED && this->state_ != improv::STATE_PROVISIONED) { + this->update_advertising_type_(); + } + switch (this->state_) { case improv::STATE_STOPPED: this->set_status_indicator_state_(false); @@ -107,9 +114,22 @@ void ESP32ImprovComponent::loop() { if (this->service_->is_created()) { this->service_->start(); } else if (this->service_->is_running()) { + // Start by advertising the device name first BEFORE setting any state + ESP_LOGV(TAG, "Starting with device name advertising"); + this->advertising_device_name_ = true; + this->last_name_adv_time_ = App.get_loop_component_start_time(); + esp32_ble::global_ble->advertising_set_service_data_and_name(std::span{}, true); esp32_ble::global_ble->advertising_start(); - this->set_state_(improv::STATE_AWAITING_AUTHORIZATION); + // Set initial state based on whether we have an authorizer + // authorizer_ member only exists when USE_BINARY_SENSOR is defined +#ifdef USE_BINARY_SENSOR + this->set_state_( + this->authorizer_ == nullptr ? improv::STATE_AUTHORIZED : improv::STATE_AWAITING_AUTHORIZATION, false); +#else + // No binary_sensor support = no authorizer possible, start as authorized + this->set_state_(improv::STATE_AUTHORIZED, false); +#endif this->set_error_(improv::ERROR_NONE); ESP_LOGD(TAG, "Service started!"); } @@ -226,12 +246,15 @@ bool ESP32ImprovComponent::check_identify_() { return identify; } -void ESP32ImprovComponent::set_state_(improv::State state) { -#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG - if (this->state_ != state) { - ESP_LOGD(TAG, "State transition: %s (0x%02X) -> %s (0x%02X)", this->state_to_string_(this->state_), this->state_, - this->state_to_string_(state), state); +void ESP32ImprovComponent::set_state_(improv::State state, bool update_advertising) { + // Skip if state hasn't changed + if (this->state_ == state) { + return; } + +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG + ESP_LOGD(TAG, "State transition: %s (0x%02X) -> %s (0x%02X)", this->state_to_string_(this->state_), this->state_, + this->state_to_string_(state), state); #endif this->state_ = state; if (this->status_ != nullptr && (this->status_->get_value().empty() || this->status_->get_value()[0] != state)) { @@ -243,25 +266,13 @@ void ESP32ImprovComponent::set_state_(improv::State state) { // STATE_STOPPED (0x00) is internal only and not part of the Improv spec. // Advertising 0x00 causes undefined behavior in some clients and makes them // repeatedly connect trying to determine the actual state. - if (state != improv::STATE_STOPPED) { - std::vector service_data(8, 0); - service_data[0] = 0x77; // PR - service_data[1] = 0x46; // IM - service_data[2] = static_cast(state); - - uint8_t capabilities = 0x00; -#ifdef USE_OUTPUT - if (this->status_indicator_ != nullptr) - capabilities |= improv::CAPABILITY_IDENTIFY; -#endif - - service_data[3] = capabilities; - service_data[4] = 0x00; // Reserved - service_data[5] = 0x00; // Reserved - service_data[6] = 0x00; // Reserved - service_data[7] = 0x00; // Reserved - - esp32_ble::global_ble->advertising_set_service_data(service_data); + if (state != improv::STATE_STOPPED && update_advertising) { + // State change always overrides name advertising and resets the timer + this->advertising_device_name_ = false; + // Reset the timer so we wait another 60 seconds before advertising name + this->last_name_adv_time_ = App.get_loop_component_start_time(); + // Advertise the new state via service data + this->advertise_service_data_(); } #ifdef USE_ESP32_IMPROV_STATE_CALLBACK this->state_callback_.call(this->state_, this->error_state_); @@ -388,6 +399,50 @@ void ESP32ImprovComponent::on_wifi_connect_timeout_() { wifi::global_wifi_component->clear_sta(); } +void ESP32ImprovComponent::advertise_service_data_() { + uint8_t service_data[8] = {}; + service_data[0] = 0x77; // PR + service_data[1] = 0x46; // IM + service_data[2] = static_cast(this->state_); + + uint8_t capabilities = 0x00; +#ifdef USE_OUTPUT + if (this->status_indicator_ != nullptr) + capabilities |= improv::CAPABILITY_IDENTIFY; +#endif + + service_data[3] = capabilities; + // service_data[4-7] are already 0 (Reserved) + + // Atomically set service data and disable name in advertising + esp32_ble::global_ble->advertising_set_service_data_and_name(std::span(service_data), false); +} + +void ESP32ImprovComponent::update_advertising_type_() { + uint32_t now = App.get_loop_component_start_time(); + + // If we're advertising the device name and it's been more than NAME_ADVERTISING_DURATION, switch back to service data + if (this->advertising_device_name_) { + if (now - this->last_name_adv_time_ >= NAME_ADVERTISING_DURATION) { + ESP_LOGV(TAG, "Switching back to service data advertising"); + this->advertising_device_name_ = false; + // Restore service data advertising + this->advertise_service_data_(); + } + return; + } + + // Check if it's time to advertise the device name (every NAME_ADVERTISING_INTERVAL) + if (now - this->last_name_adv_time_ >= NAME_ADVERTISING_INTERVAL) { + ESP_LOGV(TAG, "Switching to device name advertising"); + this->advertising_device_name_ = true; + this->last_name_adv_time_ = now; + + // Atomically clear service data and enable name in advertising data + esp32_ble::global_ble->advertising_set_service_data_and_name(std::span{}, true); + } +} + 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 686da08111..ea51f64d4b 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.h +++ b/esphome/components/esp32_improv/esp32_improv_component.h @@ -100,14 +100,18 @@ class ESP32ImprovComponent : public Component { #endif bool status_indicator_state_{false}; + uint32_t last_name_adv_time_{0}; + bool advertising_device_name_{false}; void set_status_indicator_state_(bool state); + void update_advertising_type_(); - void set_state_(improv::State state); + void set_state_(improv::State state, bool update_advertising = true); void set_error_(improv::Error error); void send_response_(std::vector &response); void process_incoming_data_(); void on_wifi_connect_timeout_(); bool check_identify_(); + void advertise_service_data_(); #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG const char *state_to_string_(improv::State state); #endif