ESPHome 2025.9.3
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
20
22
24#ifdef USE_BINARY_SENSOR
25 if (this->authorizer_ != nullptr) {
26 this->authorizer_->add_on_state_callback([this](bool state) {
27 if (state) {
28 this->authorized_start_ = millis();
29 this->identify_start_ = 0;
30 }
31 });
32 }
33#endif
34 global_ble_server->on(BLEServerEvt::EmptyEvt::ON_DISCONNECT,
35 [this](uint16_t conn_id) { this->set_error_(improv::ERROR_NONE); });
36
37 // Start with loop disabled - will be enabled by start() when needed
38 this->disable_loop();
39}
40
44 BLEDescriptor *status_descriptor = new BLE2902();
45 this->status_->add_descriptor(status_descriptor);
46
49 BLEDescriptor *error_descriptor = new BLE2902();
50 this->error_->add_descriptor(error_descriptor);
51
52 this->rpc_ = this->service_->create_characteristic(improv::RPC_COMMAND_UUID, BLECharacteristic::PROPERTY_WRITE);
53 this->rpc_->EventEmitter<BLECharacteristicEvt::VectorEvt, std::vector<uint8_t>, uint16_t>::on(
54 BLECharacteristicEvt::VectorEvt::ON_WRITE, [this](const std::vector<uint8_t> &data, uint16_t id) {
55 if (!data.empty()) {
56 this->incoming_data_.insert(this->incoming_data_.end(), data.begin(), data.end());
57 }
58 });
59 BLEDescriptor *rpc_descriptor = new BLE2902();
60 this->rpc_->add_descriptor(rpc_descriptor);
61
64 BLEDescriptor *rpc_response_descriptor = new BLE2902();
65 this->rpc_response_->add_descriptor(rpc_response_descriptor);
66
67 this->capabilities_ =
68 this->service_->create_characteristic(improv::CAPABILITIES_UUID, BLECharacteristic::PROPERTY_READ);
69 BLEDescriptor *capabilities_descriptor = new BLE2902();
70 this->capabilities_->add_descriptor(capabilities_descriptor);
71 uint8_t capabilities = 0x00;
72#ifdef USE_OUTPUT
73 if (this->status_indicator_ != nullptr)
74 capabilities |= improv::CAPABILITY_IDENTIFY;
75#endif
76 this->capabilities_->set_value(ByteBuffer::wrap(capabilities));
77 this->setup_complete_ = true;
78}
79
82 if (this->state_ != improv::STATE_STOPPED) {
83 this->state_ = improv::STATE_STOPPED;
84#ifdef USE_ESP32_IMPROV_STATE_CALLBACK
85 this->state_callback_.call(this->state_, this->error_state_);
86#endif
87 }
88 this->incoming_data_.clear();
89 return;
90 }
91 if (this->service_ == nullptr) {
92 // Setup the service
93 ESP_LOGD(TAG, "Creating Improv service");
94 this->service_ = global_ble_server->create_service(ESPBTUUID::from_raw(improv::SERVICE_UUID), true);
96 }
97
98 if (!this->incoming_data_.empty())
100 uint32_t now = App.get_loop_component_start_time();
101
102 switch (this->state_) {
103 case improv::STATE_STOPPED:
104 this->set_status_indicator_state_(false);
105
106 if (this->should_start_ && this->setup_complete_) {
107 if (this->service_->is_created()) {
108 this->service_->start();
109 } else if (this->service_->is_running()) {
111
112 this->set_state_(improv::STATE_AWAITING_AUTHORIZATION);
113 this->set_error_(improv::ERROR_NONE);
114 ESP_LOGD(TAG, "Service started!");
115 }
116 }
117 break;
118 case improv::STATE_AWAITING_AUTHORIZATION: {
119#ifdef USE_BINARY_SENSOR
120 if (this->authorizer_ == nullptr ||
121 (this->authorized_start_ != 0 && ((now - this->authorized_start_) < this->authorized_duration_))) {
122 this->set_state_(improv::STATE_AUTHORIZED);
123 } else
124#else
125 { this->set_state_(improv::STATE_AUTHORIZED); }
126#endif
127 {
128 if (!this->check_identify_())
129 this->set_status_indicator_state_(true);
130 }
131 break;
132 }
133 case improv::STATE_AUTHORIZED: {
134#ifdef USE_BINARY_SENSOR
135 if (this->authorizer_ != nullptr) {
136 if (now - this->authorized_start_ > this->authorized_duration_) {
137 ESP_LOGD(TAG, "Authorization timeout");
138 this->set_state_(improv::STATE_AWAITING_AUTHORIZATION);
139 return;
140 }
141 }
142#endif
143 if (!this->check_identify_()) {
144 this->set_status_indicator_state_((now % 1000) < 500);
145 }
146 break;
147 }
148 case improv::STATE_PROVISIONING: {
149 this->set_status_indicator_state_((now % 200) < 100);
150 if (wifi::global_wifi_component->is_connected()) {
152 this->connecting_sta_.get_password());
153 this->connecting_sta_ = {};
154 this->cancel_timeout("wifi-connect-timeout");
155 this->set_state_(improv::STATE_PROVISIONED);
156
157 std::vector<std::string> urls = {ESPHOME_MY_LINK};
158#ifdef USE_WEBSERVER
159 for (auto &ip : wifi::global_wifi_component->wifi_sta_ip_addresses()) {
160 if (ip.is_ip4()) {
161 std::string webserver_url = "http://" + ip.str() + ":" + to_string(USE_WEBSERVER_PORT);
162 urls.push_back(webserver_url);
163 break;
164 }
165 }
166#endif
167 std::vector<uint8_t> data = improv::build_rpc_response(improv::WIFI_SETTINGS, urls);
168 this->send_response_(data);
169 this->stop();
170 }
171 break;
172 }
173 case improv::STATE_PROVISIONED: {
174 this->incoming_data_.clear();
175 this->set_status_indicator_state_(false);
176 // Provisioning complete, no further loop execution needed
177 this->disable_loop();
178 break;
179 }
180 }
181}
182
184#ifdef USE_OUTPUT
185 if (this->status_indicator_ == nullptr)
186 return;
187 if (this->status_indicator_state_ == state)
188 return;
190 if (state) {
191 this->status_indicator_->turn_on();
192 } else {
194 }
195#endif
196}
197
198#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG
200 switch (state) {
201 case improv::STATE_STOPPED:
202 return "STOPPED";
203 case improv::STATE_AWAITING_AUTHORIZATION:
204 return "AWAITING_AUTHORIZATION";
205 case improv::STATE_AUTHORIZED:
206 return "AUTHORIZED";
207 case improv::STATE_PROVISIONING:
208 return "PROVISIONING";
209 case improv::STATE_PROVISIONED:
210 return "PROVISIONED";
211 default:
212 return "UNKNOWN";
213 }
214}
215#endif
216
218 uint32_t now = millis();
219
220 bool identify = this->identify_start_ != 0 && now - this->identify_start_ <= this->identify_duration_;
221
222 if (identify) {
223 uint32_t time = now % 1000;
224 this->set_status_indicator_state_(time < 600 && time % 200 < 100);
225 }
226 return identify;
227}
228
230#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG
231 if (this->state_ != state) {
232 ESP_LOGD(TAG, "State transition: %s (0x%02X) -> %s (0x%02X)", this->state_to_string_(this->state_), this->state_,
233 this->state_to_string_(state), state);
234 }
235#endif
236 this->state_ = state;
237 if (this->status_ != nullptr && (this->status_->get_value().empty() || this->status_->get_value()[0] != state)) {
238 this->status_->set_value(ByteBuffer::wrap(static_cast<uint8_t>(state)));
239 if (state != improv::STATE_STOPPED)
240 this->status_->notify();
241 }
242 // Only advertise valid Improv states (0x01-0x04).
243 // STATE_STOPPED (0x00) is internal only and not part of the Improv spec.
244 // Advertising 0x00 causes undefined behavior in some clients and makes them
245 // repeatedly connect trying to determine the actual state.
246 if (state != improv::STATE_STOPPED) {
247 std::vector<uint8_t> service_data(8, 0);
248 service_data[0] = 0x77; // PR
249 service_data[1] = 0x46; // IM
250 service_data[2] = static_cast<uint8_t>(state);
251
252 uint8_t capabilities = 0x00;
253#ifdef USE_OUTPUT
254 if (this->status_indicator_ != nullptr)
255 capabilities |= improv::CAPABILITY_IDENTIFY;
256#endif
257
258 service_data[3] = capabilities;
259 service_data[4] = 0x00; // Reserved
260 service_data[5] = 0x00; // Reserved
261 service_data[6] = 0x00; // Reserved
262 service_data[7] = 0x00; // Reserved
263
265 }
266#ifdef USE_ESP32_IMPROV_STATE_CALLBACK
267 this->state_callback_.call(this->state_, this->error_state_);
268#endif
269}
270
271void ESP32ImprovComponent::set_error_(improv::Error error) {
272 if (error != improv::ERROR_NONE) {
273 ESP_LOGE(TAG, "Error: %d", error);
274 }
275 // The error_ characteristic is initialized in setup_characteristics() which is called
276 // from the loop, while the BLE disconnect callback is registered in setup().
277 // error_ can be nullptr if:
278 // 1. A client connects/disconnects before setup_characteristics() is called
279 // 2. The device is already provisioned so the service never starts (should_start_ is false)
280 if (this->error_ != nullptr && (this->error_->get_value().empty() || this->error_->get_value()[0] != error)) {
281 this->error_->set_value(ByteBuffer::wrap(static_cast<uint8_t>(error)));
282 if (this->state_ != improv::STATE_STOPPED)
283 this->error_->notify();
284 }
285}
286
287void ESP32ImprovComponent::send_response_(std::vector<uint8_t> &response) {
288 this->rpc_response_->set_value(ByteBuffer::wrap(response));
289 if (this->state_ != improv::STATE_STOPPED)
290 this->rpc_response_->notify();
291}
292
294 if (this->should_start_ || this->state_ != improv::STATE_STOPPED)
295 return;
296
297 ESP_LOGD(TAG, "Setting Improv to start");
298 this->should_start_ = true;
299 this->enable_loop();
300}
301
303 this->should_start_ = false;
304 // Wait before stopping the service to ensure all BLE clients see the state change.
305 // This prevents clients from repeatedly reconnecting and wasting resources by allowing
306 // them to observe that the device is provisioned before the service disappears.
307 this->set_timeout("end-service", STOP_ADVERTISING_DELAY, [this] {
308 if (this->state_ == improv::STATE_STOPPED || this->service_ == nullptr)
309 return;
310 this->service_->stop();
311 this->set_state_(improv::STATE_STOPPED);
312 });
313}
314
316
318 ESP_LOGCONFIG(TAG, "ESP32 Improv:");
319#ifdef USE_BINARY_SENSOR
320 LOG_BINARY_SENSOR(" ", "Authorizer", this->authorizer_);
321#endif
322#ifdef USE_OUTPUT
323 ESP_LOGCONFIG(TAG, " Status Indicator: '%s'", YESNO(this->status_indicator_ != nullptr));
324#endif
325}
326
328 uint8_t length = this->incoming_data_[1];
329
330 ESP_LOGV(TAG, "Processing bytes - %s", format_hex_pretty(this->incoming_data_).c_str());
331 if (this->incoming_data_.size() - 3 == length) {
332 this->set_error_(improv::ERROR_NONE);
333 improv::ImprovCommand command = improv::parse_improv_data(this->incoming_data_);
334 switch (command.command) {
335 case improv::BAD_CHECKSUM:
336 ESP_LOGW(TAG, "Error decoding Improv payload");
337 this->set_error_(improv::ERROR_INVALID_RPC);
338 this->incoming_data_.clear();
339 break;
340 case improv::WIFI_SETTINGS: {
341 if (this->state_ != improv::STATE_AUTHORIZED) {
342 ESP_LOGW(TAG, "Settings received, but not authorized");
343 this->set_error_(improv::ERROR_NOT_AUTHORIZED);
344 this->incoming_data_.clear();
345 return;
346 }
347 wifi::WiFiAP sta{};
348 sta.set_ssid(command.ssid);
349 sta.set_password(command.password);
350 this->connecting_sta_ = sta;
351
354 this->set_state_(improv::STATE_PROVISIONING);
355 ESP_LOGD(TAG, "Received Improv Wi-Fi settings ssid=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(),
356 command.password.c_str());
357
358 auto f = std::bind(&ESP32ImprovComponent::on_wifi_connect_timeout_, this);
359 this->set_timeout("wifi-connect-timeout", 30000, f);
360 this->incoming_data_.clear();
361 break;
362 }
363 case improv::IDENTIFY:
364 this->incoming_data_.clear();
365 this->identify_start_ = millis();
366 break;
367 default:
368 ESP_LOGW(TAG, "Unknown Improv payload");
369 this->set_error_(improv::ERROR_UNKNOWN_RPC);
370 this->incoming_data_.clear();
371 }
372 } else if (this->incoming_data_.size() - 2 > length) {
373 ESP_LOGV(TAG, "Too much data received or data malformed; resetting buffer");
374 this->incoming_data_.clear();
375 } else {
376 ESP_LOGV(TAG, "Waiting for split data packets");
377 }
378}
379
381 this->set_error_(improv::ERROR_UNABLE_TO_CONNECT);
382 this->set_state_(improv::STATE_AUTHORIZED);
383#ifdef USE_BINARY_SENSOR
384 if (this->authorizer_ != nullptr)
385 this->authorized_start_ = millis();
386#endif
387 ESP_LOGW(TAG, "Timed out while connecting to Wi-Fi network");
389}
390
391ESP32ImprovComponent *global_improv_component = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
392
393} // namespace esp32_improv
394} // namespace esphome
395
396#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(const std::vector< uint8_t > &data)
Definition ble.cpp:64
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_
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:544
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