ESPHome 2026.6.2
Loading...
Searching...
No Matches
improv_serial_component.cpp
Go to the documentation of this file.
2#ifdef USE_WIFI
5#include "esphome/core/hal.h"
6#include "esphome/core/log.h"
8
10
12
13static const char *const TAG = "improv_serial";
14
17#ifdef USE_ESP32
19#elif defined(USE_ARDUINO)
21#endif
22
23 if (wifi::global_wifi_component->has_sta()) {
24 this->state_ = improv::STATE_PROVISIONED;
25 } else if (!wifi::global_wifi_component->is_disabled()) {
26 // Respect Wi-Fi's disabled state; forcing a scan while disabled throws
27 // the wifi component into an invalid state from which it cannot recover.
29 }
30}
31
33 if (this->last_read_byte_ && (millis() - this->last_read_byte_ > IMPROV_SERIAL_TIMEOUT)) {
34 this->last_read_byte_ = 0;
35 this->rx_buffer_.clear();
36 ESP_LOGV(TAG, "Timeout");
37 }
38
39 auto byte = this->read_byte_();
40 while (byte.has_value()) {
41 if (this->parse_improv_serial_byte_(byte.value())) {
42 this->last_read_byte_ = millis();
43 } else {
44 this->last_read_byte_ = 0;
45 this->rx_buffer_.clear();
46 }
47 byte = this->read_byte_();
48 }
49
50 if (this->state_ == improv::STATE_PROVISIONING) {
51 if (wifi::global_wifi_component->is_connected()) {
53 this->connecting_sta_.get_password());
54 this->connecting_sta_ = {};
55 this->cancel_timeout("wifi-connect-timeout");
56 this->set_state_(improv::STATE_PROVISIONED);
57
58 std::vector<uint8_t> url = this->build_rpc_settings_response_(improv::WIFI_SETTINGS);
59 this->send_response_(url);
60 }
61 }
62}
63
64void ImprovSerialComponent::dump_config() { ESP_LOGCONFIG(TAG, "Improv Serial:"); }
65
67 optional<uint8_t> byte;
68 uint8_t data = 0;
69#ifdef USE_ESP32
70 switch (logger::global_logger->get_uart()) {
73#if defined(USE_ESP32_VARIANT_ESP32)
75#endif
76 if (this->uart_num_ >= 0) {
77 size_t available;
78 uart_get_buffered_data_len(this->uart_num_, &available);
79 if (available) {
80 uart_read_bytes(this->uart_num_, &data, 1, 0);
81 byte = data;
82 }
83 }
84 break;
85#if defined(USE_LOGGER_USB_CDC) && defined(CONFIG_ESP_CONSOLE_USB_CDC)
87 if (esp_usb_console_available_for_read()) {
88 esp_usb_console_read_buf((char *) &data, 1);
89 byte = data;
90 }
91 break;
92#endif // USE_LOGGER_USB_CDC
93#ifdef USE_LOGGER_USB_SERIAL_JTAG
95 if (usb_serial_jtag_read_bytes((char *) &data, 1, 0)) {
96 byte = data;
97 }
98 break;
99 }
100#endif // USE_LOGGER_USB_SERIAL_JTAG
101 default:
102 break;
103 }
104#elif defined(USE_ARDUINO)
105 if (this->hw_serial_->available()) {
106 this->hw_serial_->readBytes(&data, 1);
107 byte = data;
108 }
109#endif
110 return byte;
111}
112
113void ImprovSerialComponent::write_data_(const uint8_t *data, const size_t size) {
114 // First, set length field
115 this->tx_header_[TX_LENGTH_IDX] = this->tx_header_[TX_TYPE_IDX] == TYPE_RPC_RESPONSE ? size : 1;
116
117 const bool there_is_data = data != nullptr && size > 0;
118 // If there_is_data, checksum must not include our optional data byte
119 const uint8_t header_checksum_len = there_is_data ? TX_BUFFER_SIZE - 3 : TX_BUFFER_SIZE - 2;
120 // Only transmit the full buffer length if there is no data (only state/error byte is provided in this case)
121 const uint8_t header_tx_len = there_is_data ? TX_BUFFER_SIZE - 3 : TX_BUFFER_SIZE;
122 // Calculate checksum for message
123 uint8_t checksum = 0;
124 for (uint8_t i = 0; i < header_checksum_len; i++) {
125 checksum += this->tx_header_[i];
126 }
127 if (there_is_data) {
128 // Include data in checksum
129 for (size_t i = 0; i < size; i++) {
130 checksum += data[i];
131 }
132 }
133 this->tx_header_[TX_CHECKSUM_IDX] = checksum;
134
135#ifdef USE_ESP32
136 switch (logger::global_logger->get_uart()) {
139#if defined(USE_ESP32_VARIANT_ESP32)
141#endif
142 uart_write_bytes(this->uart_num_, this->tx_header_, header_tx_len);
143 if (there_is_data) {
144 uart_write_bytes(this->uart_num_, data, size);
145 uart_write_bytes(this->uart_num_, &this->tx_header_[TX_CHECKSUM_IDX], 2); // Footer: checksum and newline
146 }
147 break;
148#if defined(USE_LOGGER_USB_CDC) && defined(CONFIG_ESP_CONSOLE_USB_CDC)
150 esp_usb_console_write_buf((const char *) this->tx_header_, header_tx_len);
151 if (there_is_data) {
152 esp_usb_console_write_buf((const char *) data, size);
153 esp_usb_console_write_buf((const char *) &this->tx_header_[TX_CHECKSUM_IDX],
154 2); // Footer: checksum and newline
155 }
156 break;
157#endif
158#ifdef USE_LOGGER_USB_SERIAL_JTAG
160 usb_serial_jtag_write_bytes((const char *) this->tx_header_, header_tx_len, 20 / portTICK_PERIOD_MS);
161 if (there_is_data) {
162 usb_serial_jtag_write_bytes((const char *) data, size, 20 / portTICK_PERIOD_MS);
163 usb_serial_jtag_write_bytes((const char *) &this->tx_header_[TX_CHECKSUM_IDX], 2,
164 20 / portTICK_PERIOD_MS); // Footer: checksum and newline
165 }
166 break;
167#endif
168 default:
169 break;
170 }
171#elif defined(USE_ARDUINO)
172 this->hw_serial_->write(this->tx_header_, header_tx_len);
173 if (there_is_data) {
174 this->hw_serial_->write(data, size);
175 this->hw_serial_->write(&this->tx_header_[TX_CHECKSUM_IDX], 2); // Footer: checksum and newline
176 }
177#endif
178}
179
180std::vector<uint8_t> ImprovSerialComponent::build_rpc_settings_response_(improv::Command command) {
181 std::vector<std::string> urls;
182#ifdef USE_IMPROV_SERIAL_NEXT_URL
183 {
184 char url_buffer[384];
185 size_t len = this->get_formatted_next_url_(url_buffer, sizeof(url_buffer));
186 if (len > 0) {
187 urls.emplace_back(url_buffer, len);
188 }
189 }
190#endif
191#ifdef USE_WEBSERVER
192 for (auto &ip : wifi::global_wifi_component->wifi_sta_ip_addresses()) {
193 if (ip.is_ip4()) {
194 char ip_buf[network::IP_ADDRESS_BUFFER_SIZE];
195 ip.str_to(ip_buf);
196 // "http://" (7) + IP (40) + ":" (1) + port (5) + null (1) = 54
197 char webserver_url[7 + network::IP_ADDRESS_BUFFER_SIZE + 1 + 5 + 1];
198 snprintf(webserver_url, sizeof(webserver_url), "http://%s:%u", ip_buf, USE_WEBSERVER_PORT);
199 urls.emplace_back(webserver_url);
200 break;
201 }
202 }
203#endif
204 std::vector<uint8_t> data = improv::build_rpc_response(command, urls, false);
205 return data;
206}
207
209#ifdef ESPHOME_PROJECT_NAME
210 std::vector<std::string> infos = {ESPHOME_PROJECT_NAME, ESPHOME_PROJECT_VERSION, ESPHOME_VARIANT, App.get_name()};
211#else
212 std::vector<std::string> infos = {"ESPHome", ESPHOME_VERSION, ESPHOME_VARIANT, App.get_name()};
213#endif
214 std::vector<uint8_t> data = improv::build_rpc_response(improv::GET_DEVICE_INFO, infos, false);
215 return data;
216};
217
219 size_t at = this->rx_buffer_.size();
220 this->rx_buffer_.push_back(byte);
221 ESP_LOGV(TAG, "Byte: 0x%02X", byte);
222 const uint8_t *raw = &this->rx_buffer_[0];
223
224 return improv::parse_improv_serial_byte(
225 at, byte, raw, [this](improv::ImprovCommand command) -> bool { return this->parse_improv_payload_(command); },
226 [this](improv::Error error) -> void {
227 ESP_LOGW(TAG, "Error decoding payload");
228 this->set_error_(error);
229 });
230}
231
232bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command) {
233 switch (command.command) {
234 case improv::WIFI_SETTINGS: {
235 if (wifi::global_wifi_component->is_disabled()) {
236 // Wi-Fi is disabled, so we can't provision. Respond immediately
237 // instead of letting the client wait out its provisioning timeout.
238 ESP_LOGW(TAG, "Wi-Fi is disabled; cannot provision");
239 this->set_error_(improv::ERROR_UNABLE_TO_CONNECT);
240 return true;
241 }
242 wifi::WiFiAP sta{};
243 sta.set_ssid(command.ssid.c_str());
244 sta.set_password(command.password.c_str());
245 this->connecting_sta_ = sta;
246
249 this->set_state_(improv::STATE_PROVISIONING);
250 ESP_LOGD(TAG, "Received settings: SSID=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(),
251 command.password.c_str());
252
253 this->set_timeout("wifi-connect-timeout", 30000, [this]() { this->on_wifi_connect_timeout_(); });
254 return true;
255 }
256 case improv::GET_CURRENT_STATE:
257 if (wifi::global_wifi_component->is_disabled()) {
258 // Wi-Fi is disabled; report the Improv "stopped" state so a client can tell
259 // the user that provisioning is unavailable. Reported transiently without
260 // disturbing our internal provisioning state machine, so a later `wifi.enable`
261 // still reports the correct state.
262 this->send_current_state_(improv::STATE_STOPPED);
263 return true;
264 }
265 this->set_state_(this->state_);
266 if (this->state_ == improv::STATE_PROVISIONED) {
267 std::vector<uint8_t> url = this->build_rpc_settings_response_(improv::GET_CURRENT_STATE);
268 this->send_response_(url);
269 }
270 return true;
271 case improv::GET_DEVICE_INFO: {
272 std::vector<uint8_t> info = this->build_version_info_();
273 this->send_response_(info);
274 return true;
275 }
276 case improv::GET_WIFI_NETWORKS: {
277 std::vector<std::string> networks;
278 const auto &results = wifi::global_wifi_component->get_scan_result();
279 for (auto &scan : results) {
280 if (scan.get_is_hidden())
281 continue;
282 const char *ssid_cstr = scan.get_ssid().c_str();
283 // Check if we've already sent this SSID
284 bool duplicate = false;
285 for (const auto &seen : networks) {
286 if (strcmp(seen.c_str(), ssid_cstr) == 0) {
287 duplicate = true;
288 break;
289 }
290 }
291 if (duplicate)
292 continue;
293 // Only allocate std::string after confirming it's not a duplicate
294 std::string ssid(ssid_cstr);
295 // Send each ssid separately to avoid overflowing the buffer
296 char rssi_buf[5]; // int8_t: -128 to 127, max 4 chars + null
297 *int8_to_str(rssi_buf, scan.get_rssi()) = '\0';
298 std::vector<uint8_t> data =
299 improv::build_rpc_response(improv::GET_WIFI_NETWORKS, {ssid, rssi_buf, YESNO(scan.get_with_auth())}, false);
300 this->send_response_(data);
301 networks.push_back(std::move(ssid));
302 }
303 // Send empty response to signify the end of the list.
304 std::vector<uint8_t> data =
305 improv::build_rpc_response(improv::GET_WIFI_NETWORKS, std::vector<std::string>{}, false);
306 this->send_response_(data);
307 return true;
308 }
309 default: {
310 ESP_LOGW(TAG, "Unknown payload");
311 this->set_error_(improv::ERROR_UNKNOWN_RPC);
312 return false;
313 }
314 }
315}
316
318 this->state_ = state;
319 this->send_current_state_(state);
320}
321
323 this->tx_header_[TX_TYPE_IDX] = TYPE_CURRENT_STATE;
324 this->tx_header_[TX_DATA_IDX] = state;
325 this->write_data_();
326}
327
328void ImprovSerialComponent::set_error_(improv::Error error) {
329 this->tx_header_[TX_TYPE_IDX] = TYPE_ERROR_STATE;
330 this->tx_header_[TX_DATA_IDX] = error;
331 this->write_data_();
332}
333
334void ImprovSerialComponent::send_response_(std::vector<uint8_t> &response) {
335 this->tx_header_[TX_TYPE_IDX] = TYPE_RPC_RESPONSE;
336 this->write_data_(response.data(), response.size());
337}
338
340 this->set_error_(improv::ERROR_UNABLE_TO_CONNECT);
341 this->set_state_(improv::STATE_AUTHORIZED);
342 ESP_LOGW(TAG, "Timed out while connecting to Wi-Fi network");
344}
345
346ImprovSerialComponent *global_improv_serial_component = // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
347 nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
348
349} // namespace esphome::improv_serial
350
351#endif
uint8_t checksum
Definition bl0906.h:3
uint8_t raw[35]
Definition bl0939.h:0
const StringRef & get_name() const
Get the name of this Application set by pre_setup().
ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.0") void set_timeout(const std voi set_timeout)(const char *name, uint32_t timeout, std::function< void()> &&f)
Set a timeout function with a unique name.
Definition component.h:493
ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.0") bool cancel_timeout(const std boo cancel_timeout)(const char *name)
Cancel a timeout function.
Definition component.h:515
size_t get_formatted_next_url_(char *buffer, size_t buffer_size)
Format next_url_ into buffer, replacing placeholders. Returns length written.
std::vector< uint8_t > build_rpc_settings_response_(improv::Command command)
void write_data_(const uint8_t *data=nullptr, size_t size=0)
void send_response_(std::vector< uint8_t > &response)
bool parse_improv_payload_(improv::ImprovCommand &command)
Stream * get_hw_serial() const
Definition logger.h:154
uart_port_t get_uart_num() const
Definition logger.h:157
StringRef 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)
const wifi_scan_vector_t< WiFiScanResult > & get_scan_result() const
bool state
Definition fan.h:2
ImprovSerialComponent * global_improv_serial_component
@ UART_SELECTION_UART2
Definition logger.h:113
@ UART_SELECTION_USB_SERIAL_JTAG
Definition logger.h:119
@ UART_SELECTION_USB_CDC
Definition logger.h:116
@ UART_SELECTION_UART0
Definition logger.h:107
@ UART_SELECTION_UART1
Definition logger.h:111
Logger * global_logger
Definition logger.cpp:279
WiFiComponent * global_wifi_component
const void size_t len
Definition hal.h:64
uint16_t size
Definition helpers.cpp:25
char * int8_to_str(char *buf, int8_t val)
Write int8 value to buffer without modulo operations.
Definition helpers.h:1259
uint32_t IRAM_ATTR HOT millis()
Definition hal.cpp:28
Application App
Global storage of Application pointer - only one Application can exist.