ESPHome 2025.10.0-dev
Loading...
Searching...
No Matches
esp32_improv_component.cpp
Go to the documentation of this file.
2
6#include "esphome/core/log.h"
8
9#ifdef USE_ESP32
10
11namespace esphome {
12namespace esp32_improv {
13
14using namespace bytebuffer;
15
16static const char *const TAG = "esp32_improv.component";
17static const char *const ESPHOME_MY_LINK = "https://my.home-assistant.io/redirect/config_flow_start?domain=esphome";
18static constexpr uint16_t STOP_ADVERTISING_DELAY =
19 10000; // Delay (ms) before stopping service to allow BLE clients to read the final state
20static constexpr uint16_t NAME_ADVERTISING_INTERVAL = 60000; // Advertise name every 60 seconds
21static constexpr uint16_t NAME_ADVERTISING_DURATION = 1000; // Advertise name for 1 second
22
23// Improv service data constants
24static constexpr uint8_t IMPROV_SERVICE_DATA_SIZE = 8;
25static constexpr uint8_t IMPROV_PROTOCOL_ID_1 = 0x77; // 'P' << 1 | 'R' >> 7
26static constexpr uint8_t IMPROV_PROTOCOL_ID_2 = 0x46; // 'I' << 1 | 'M' >> 7
27
29
31#ifdef USE_BINARY_SENSOR
32 if (this->authorizer_ != nullptr) {
33 this->authorizer_->add_on_state_callback([this](bool state) {
34 if (state) {
35 this->authorized_start_ = millis();
36 this->identify_start_ = 0;
37 }
38 });
39 }
40#endif
41 global_ble_server->on(BLEServerEvt::EmptyEvt::ON_DISCONNECT,
42 [this](uint16_t conn_id) { this->set_error_(improv::ERROR_NONE); });
43
44 // Start with loop disabled - will be enabled by start() when needed
45 this->disable_loop();
46}
47
51 BLEDescriptor *status_descriptor = new BLE2902();
52 this->status_->add_descriptor(status_descriptor);
53
56 BLEDescriptor *error_descriptor = new BLE2902();
57 this->error_->add_descriptor(error_descriptor);
58
59 this->rpc_ = this->service_->create_characteristic(improv::RPC_COMMAND_UUID, BLECharacteristic::PROPERTY_WRITE);
60 this->rpc_->EventEmitter<BLECharacteristicEvt::VectorEvt, std::vector<uint8_t>, uint16_t>::on(
61 BLECharacteristicEvt::VectorEvt::ON_WRITE, [this](const std::vector<uint8_t> &data, uint16_t id) {
62 if (!data.empty()) {
63 this->incoming_data_.insert(this->incoming_data_.end(), data.begin(), data.end());
64 }
65 });
66 BLEDescriptor *rpc_descriptor = new BLE2902();
67 this->rpc_->add_descriptor(rpc_descriptor);
68
71 BLEDescriptor *rpc_response_descriptor = new BLE2902();
72 this->rpc_response_->add_descriptor(rpc_response_descriptor);
73
74 this->capabilities_ =
75 this->service_->create_characteristic(improv::CAPABILITIES_UUID, BLECharacteristic::PROPERTY_READ);
76 BLEDescriptor *capabilities_descriptor = new BLE2902();
77 this->capabilities_->add_descriptor(capabilities_descriptor);
78 uint8_t capabilities = 0x00;
79#ifdef USE_OUTPUT
80 if (this->status_indicator_ != nullptr)
81 capabilities |= improv::CAPABILITY_IDENTIFY;
82#endif
83 this->capabilities_->set_value(ByteBuffer::wrap(capabilities));
84 this->setup_complete_ = true;
85}
86
89 if (this->state_ != improv::STATE_STOPPED) {
90 this->state_ = improv::STATE_STOPPED;
91#ifdef USE_ESP32_IMPROV_STATE_CALLBACK
92 this->state_callback_.call(this->state_, this->error_state_);
93#endif
94 }
95 this->incoming_data_.clear();
96 return;
97 }
98 if (this->service_ == nullptr) {
99 // Setup the service
100 ESP_LOGD(TAG, "Creating Improv service");
101 this->service_ = global_ble_server->create_service(ESPBTUUID::from_raw(improv::SERVICE_UUID), true);
102 this->setup_characteristics();
103 }
104
105 if (!this->incoming_data_.empty())
107 uint32_t now = App.get_loop_component_start_time();
108
109 // Check if we need to update advertising type
110 if (this->state_ != improv::STATE_STOPPED && this->state_ != improv::STATE_PROVISIONED) {
112 }
113
114 switch (this->state_) {
115 case improv::STATE_STOPPED:
116 this->set_status_indicator_state_(false);
117
118 if (this->should_start_ && this->setup_complete_) {
119 if (this->service_->is_created()) {
120 this->service_->start();
121 } else if (this->service_->is_running()) {
122 // Start by advertising the device name first BEFORE setting any state
123 ESP_LOGV(TAG, "Starting with device name advertising");
124 this->advertising_device_name_ = true;
126 esp32_ble::global_ble->advertising_set_service_data_and_name(std::span<const uint8_t>{}, true);
128
129 // Set initial state based on whether we have an authorizer
130 this->set_state_(this->get_initial_state_(), false);
131 this->set_error_(improv::ERROR_NONE);
132 ESP_LOGD(TAG, "Service started!");
133 }
134 }
135 break;
136 case improv::STATE_AWAITING_AUTHORIZATION: {
137#ifdef USE_BINARY_SENSOR
138 if (this->authorizer_ == nullptr ||
139 (this->authorized_start_ != 0 && ((now - this->authorized_start_) < this->authorized_duration_))) {
140 this->set_state_(improv::STATE_AUTHORIZED);
141 } else {
142 if (!this->check_identify_())
143 this->set_status_indicator_state_(true);
144 }
145#else
146 this->set_state_(improv::STATE_AUTHORIZED);
147#endif
148 break;
149 }
150 case improv::STATE_AUTHORIZED: {
151#ifdef USE_BINARY_SENSOR
152 if (this->authorizer_ != nullptr && now - this->authorized_start_ > this->authorized_duration_) {
153 ESP_LOGD(TAG, "Authorization timeout");
154 this->set_state_(improv::STATE_AWAITING_AUTHORIZATION);
155 return;
156 }
157#endif
158 if (!this->check_identify_()) {
159 this->set_status_indicator_state_((now % 1000) < 500);
160 }
161 break;
162 }
163 case improv::STATE_PROVISIONING: {
164 this->set_status_indicator_state_((now % 200) < 100);
165 if (wifi::global_wifi_component->is_connected()) {
167 this->connecting_sta_.get_password());
168 this->connecting_sta_ = {};
169 this->cancel_timeout("wifi-connect-timeout");
170 this->set_state_(improv::STATE_PROVISIONED);
171
172 std::vector<std::string> urls = {ESPHOME_MY_LINK};
173#ifdef USE_WEBSERVER
174 for (auto &ip : wifi::global_wifi_component->wifi_sta_ip_addresses()) {
175 if (ip.is_ip4()) {
176 std::string webserver_url = "http://" + ip.str() + ":" + to_string(USE_WEBSERVER_PORT);
177 urls.push_back(webserver_url);
178 break;
179 }
180 }
181#endif
182 std::vector<uint8_t> data = improv::build_rpc_response(improv::WIFI_SETTINGS, urls);
183 this->send_response_(data);
184 this->stop();
185 }
186 break;
187 }
188 case improv::STATE_PROVISIONED: {
189 this->incoming_data_.clear();
190 this->set_status_indicator_state_(false);
191 // Provisioning complete, no further loop execution needed
192 this->disable_loop();
193 break;
194 }
195 }
196}
197
199#ifdef USE_OUTPUT
200 if (this->status_indicator_ == nullptr)
201 return;
202 if (this->status_indicator_state_ == state)
203 return;
205 if (state) {
206 this->status_indicator_->turn_on();
207 } else {
209 }
210#endif
211}
212
213#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG
215 switch (state) {
216 case improv::STATE_STOPPED:
217 return "STOPPED";
218 case improv::STATE_AWAITING_AUTHORIZATION:
219 return "AWAITING_AUTHORIZATION";
220 case improv::STATE_AUTHORIZED:
221 return "AUTHORIZED";
222 case improv::STATE_PROVISIONING:
223 return "PROVISIONING";
224 case improv::STATE_PROVISIONED:
225 return "PROVISIONED";
226 default:
227 return "UNKNOWN";
228 }
229}
230#endif
231
233 uint32_t now = millis();
234
235 bool identify = this->identify_start_ != 0 && now - this->identify_start_ <= this->identify_duration_;
236
237 if (identify) {
238 uint32_t time = now % 1000;
239 this->set_status_indicator_state_(time < 600 && time % 200 < 100);
240 }
241 return identify;
242}
243
244void ESP32ImprovComponent::set_state_(improv::State state, bool update_advertising) {
245 // Skip if state hasn't changed
246 if (this->state_ == state) {
247 return;
248 }
249
250#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG
251 ESP_LOGD(TAG, "State transition: %s (0x%02X) -> %s (0x%02X)", this->state_to_string_(this->state_), this->state_,
252 this->state_to_string_(state), state);
253#endif
254 this->state_ = state;
255 if (this->status_ != nullptr && (this->status_->get_value().empty() || this->status_->get_value()[0] != state)) {
256 this->status_->set_value(ByteBuffer::wrap(static_cast<uint8_t>(state)));
257 if (state != improv::STATE_STOPPED)
258 this->status_->notify();
259 }
260 // Only advertise valid Improv states (0x01-0x04).
261 // STATE_STOPPED (0x00) is internal only and not part of the Improv spec.
262 // Advertising 0x00 causes undefined behavior in some clients and makes them
263 // repeatedly connect trying to determine the actual state.
264 if (state != improv::STATE_STOPPED && update_advertising) {
265 // State change always overrides name advertising and resets the timer
266 this->advertising_device_name_ = false;
267 // Reset the timer so we wait another 60 seconds before advertising name
269 // Advertise the new state via service data
271 }
272#ifdef USE_ESP32_IMPROV_STATE_CALLBACK
273 this->state_callback_.call(this->state_, this->error_state_);
274#endif
275}
276
277void ESP32ImprovComponent::set_error_(improv::Error error) {
278 if (error != improv::ERROR_NONE) {
279 ESP_LOGE(TAG, "Error: %d", error);
280 }
281 // The error_ characteristic is initialized in setup_characteristics() which is called
282 // from the loop, while the BLE disconnect callback is registered in setup().
283 // error_ can be nullptr if:
284 // 1. A client connects/disconnects before setup_characteristics() is called
285 // 2. The device is already provisioned so the service never starts (should_start_ is false)
286 if (this->error_ != nullptr && (this->error_->get_value().empty() || this->error_->get_value()[0] != error)) {
287 this->error_->set_value(ByteBuffer::wrap(static_cast<uint8_t>(error)));
288 if (this->state_ != improv::STATE_STOPPED)
289 this->error_->notify();
290 }
291}
292
293void ESP32ImprovComponent::send_response_(std::vector<uint8_t> &response) {
294 this->rpc_response_->set_value(ByteBuffer::wrap(response));
295 if (this->state_ != improv::STATE_STOPPED)
296 this->rpc_response_->notify();
297}
298
300 if (this->should_start_ || this->state_ != improv::STATE_STOPPED)
301 return;
302
303 ESP_LOGD(TAG, "Setting Improv to start");
304 this->should_start_ = true;
305 this->enable_loop();
306}
307
309 this->should_start_ = false;
310 // Wait before stopping the service to ensure all BLE clients see the state change.
311 // This prevents clients from repeatedly reconnecting and wasting resources by allowing
312 // them to observe that the device is provisioned before the service disappears.
313 this->set_timeout("end-service", STOP_ADVERTISING_DELAY, [this] {
314 if (this->state_ == improv::STATE_STOPPED || this->service_ == nullptr)
315 return;
316 this->service_->stop();
317 this->set_state_(improv::STATE_STOPPED);
318 });
319}
320
322
324 ESP_LOGCONFIG(TAG, "ESP32 Improv:");
325#ifdef USE_BINARY_SENSOR
326 LOG_BINARY_SENSOR(" ", "Authorizer", this->authorizer_);
327#endif
328#ifdef USE_OUTPUT
329 ESP_LOGCONFIG(TAG, " Status Indicator: '%s'", YESNO(this->status_indicator_ != nullptr));
330#endif
331}
332
334 uint8_t length = this->incoming_data_[1];
335
336 ESP_LOGV(TAG, "Processing bytes - %s", format_hex_pretty(this->incoming_data_).c_str());
337 if (this->incoming_data_.size() - 3 == length) {
338 this->set_error_(improv::ERROR_NONE);
339 improv::ImprovCommand command = improv::parse_improv_data(this->incoming_data_);
340 switch (command.command) {
341 case improv::BAD_CHECKSUM:
342 ESP_LOGW(TAG, "Error decoding Improv payload");
343 this->set_error_(improv::ERROR_INVALID_RPC);
344 this->incoming_data_.clear();
345 break;
346 case improv::WIFI_SETTINGS: {
347 if (this->state_ != improv::STATE_AUTHORIZED) {
348 ESP_LOGW(TAG, "Settings received, but not authorized");
349 this->set_error_(improv::ERROR_NOT_AUTHORIZED);
350 this->incoming_data_.clear();
351 return;
352 }
353 wifi::WiFiAP sta{};
354 sta.set_ssid(command.ssid);
355 sta.set_password(command.password);
356 this->connecting_sta_ = sta;
357
360 this->set_state_(improv::STATE_PROVISIONING);
361 ESP_LOGD(TAG, "Received Improv Wi-Fi settings ssid=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(),
362 command.password.c_str());
363
364 auto f = std::bind(&ESP32ImprovComponent::on_wifi_connect_timeout_, this);
365 this->set_timeout("wifi-connect-timeout", 30000, f);
366 this->incoming_data_.clear();
367 break;
368 }
369 case improv::IDENTIFY:
370 this->incoming_data_.clear();
371 this->identify_start_ = millis();
372 break;
373 default:
374 ESP_LOGW(TAG, "Unknown Improv payload");
375 this->set_error_(improv::ERROR_UNKNOWN_RPC);
376 this->incoming_data_.clear();
377 }
378 } else if (this->incoming_data_.size() - 2 > length) {
379 ESP_LOGV(TAG, "Too much data received or data malformed; resetting buffer");
380 this->incoming_data_.clear();
381 } else {
382 ESP_LOGV(TAG, "Waiting for split data packets");
383 }
384}
385
387 this->set_error_(improv::ERROR_UNABLE_TO_CONNECT);
388 this->set_state_(improv::STATE_AUTHORIZED);
389#ifdef USE_BINARY_SENSOR
390 if (this->authorizer_ != nullptr)
391 this->authorized_start_ = millis();
392#endif
393 ESP_LOGW(TAG, "Timed out while connecting to Wi-Fi network");
395}
396
398 uint8_t service_data[IMPROV_SERVICE_DATA_SIZE] = {};
399 service_data[0] = IMPROV_PROTOCOL_ID_1; // PR
400 service_data[1] = IMPROV_PROTOCOL_ID_2; // IM
401 service_data[2] = static_cast<uint8_t>(this->state_);
402
403 uint8_t capabilities = 0x00;
404#ifdef USE_OUTPUT
405 if (this->status_indicator_ != nullptr)
406 capabilities |= improv::CAPABILITY_IDENTIFY;
407#endif
408
409 service_data[3] = capabilities;
410 // service_data[4-7] are already 0 (Reserved)
411
412 // Atomically set service data and disable name in advertising
413 esp32_ble::global_ble->advertising_set_service_data_and_name(std::span<const uint8_t>(service_data), false);
414}
415
417 uint32_t now = App.get_loop_component_start_time();
418
419 // If we're advertising the device name and it's been more than NAME_ADVERTISING_DURATION, switch back to service data
420 if (this->advertising_device_name_) {
421 if (now - this->last_name_adv_time_ >= NAME_ADVERTISING_DURATION) {
422 ESP_LOGV(TAG, "Switching back to service data advertising");
423 this->advertising_device_name_ = false;
424 // Restore service data advertising
426 }
427 return;
428 }
429
430 // Check if it's time to advertise the device name (every NAME_ADVERTISING_INTERVAL)
431 if (now - this->last_name_adv_time_ >= NAME_ADVERTISING_INTERVAL) {
432 ESP_LOGV(TAG, "Switching to device name advertising");
433 this->advertising_device_name_ = true;
434 this->last_name_adv_time_ = now;
435
436 // Atomically clear service data and enable name in advertising data
437 esp32_ble::global_ble->advertising_set_service_data_and_name(std::span<const uint8_t>{}, true);
438 }
439}
440
442#ifdef USE_BINARY_SENSOR
443 // If we have an authorizer, start in awaiting authorization state
444 return this->authorizer_ == nullptr ? improv::STATE_AUTHORIZED : improv::STATE_AWAITING_AUTHORIZATION;
445#else
446 // No binary_sensor support = no authorizer possible, start as authorized
447 return improv::STATE_AUTHORIZED;
448#endif
449}
450
451ESP32ImprovComponent *global_improv_component = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
452
453} // namespace esp32_improv
454} // namespace esphome
455
456#endif
uint32_t IRAM_ATTR HOT get_loop_component_start_time() const
Get the cached time in milliseconds from when the current component started its loop execution.
bool cancel_timeout(const std::string &name)
Cancel a timeout function.
void enable_loop()
Enable this component's loop.
void disable_loop()
Disable this component's loop.
void set_timeout(const std::string &name, uint32_t timeout, std::function< void()> &&f)
Set a timeout function with a unique name.
void add_on_state_callback(std::function< void(T)> &&callback)
static ByteBuffer wrap(T value, Endian endianness=LITTLE)
Definition bytebuffer.h:156
void advertising_set_service_data_and_name(std::span< const uint8_t > data, bool include_name)
Definition ble.cpp:76
static ESPBTUUID from_raw(const uint8_t *data)
Definition ble_uuid.cpp:29
void add_descriptor(BLEDescriptor *descriptor)
BLEService * create_service(ESPBTUUID uuid, bool advertise=false, uint16_t num_handles=15)
BLECharacteristic * create_characteristic(const std::string &uuid, esp_gatt_char_prop_t properties)
void send_response_(std::vector< uint8_t > &response)
CallbackManager< void(improv::State, improv::Error)> state_callback_
void set_state_(improv::State state, bool update_advertising=true)
const char * state_to_string_(improv::State state)
EventEmitterListenerID on(EvtType event, std::function< void(Args...)> listener)
virtual void turn_off()
Disable this binary output.
virtual void turn_on()
Enable this binary output.
const std::string & get_ssid() const
void set_ssid(const std::string &ssid)
void set_sta(const WiFiAP &ap)
void save_wifi_sta(const std::string &ssid, const std::string &password)
void start_connecting(const WiFiAP &ap, bool two)
bool state
Definition fan.h:0
ESP32BLE * global_ble
Definition ble.cpp:586
ESP32ImprovComponent * global_improv_component
const float AFTER_BLUETOOTH
Definition component.cpp:52
WiFiComponent * global_wifi_component
Providing packet encoding functions for exchanging data with a remote host.
Definition a01nyub.cpp:7
std::string format_hex_pretty(const uint8_t *data, size_t length, char separator, bool show_length)
Format a byte array in pretty-printed, human-readable hex format.
Definition helpers.cpp:292
uint32_t IRAM_ATTR HOT millis()
Definition core.cpp:28
Application App
Global storage of Application pointer - only one Application can exist.
uint16_t length
Definition tt21100.cpp:0