ESPHome 2026.6.0-dev
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 {
27 }
28}
29
31 if (this->last_read_byte_ && (millis() - this->last_read_byte_ > IMPROV_SERIAL_TIMEOUT)) {
32 this->last_read_byte_ = 0;
33 this->rx_buffer_.clear();
34 ESP_LOGV(TAG, "Timeout");
35 }
36
37 auto byte = this->read_byte_();
38 while (byte.has_value()) {
39 if (this->parse_improv_serial_byte_(byte.value())) {
40 this->last_read_byte_ = millis();
41 } else {
42 this->last_read_byte_ = 0;
43 this->rx_buffer_.clear();
44 }
45 byte = this->read_byte_();
46 }
47
48 if (this->state_ == improv::STATE_PROVISIONING) {
49 if (wifi::global_wifi_component->is_connected()) {
51 this->connecting_sta_.get_password());
52 this->connecting_sta_ = {};
53 this->cancel_timeout("wifi-connect-timeout");
54 this->set_state_(improv::STATE_PROVISIONED);
55
56 std::vector<uint8_t> url = this->build_rpc_settings_response_(improv::WIFI_SETTINGS);
57 this->send_response_(url);
58 }
59 }
60}
61
62void ImprovSerialComponent::dump_config() { ESP_LOGCONFIG(TAG, "Improv Serial:"); }
63
65 optional<uint8_t> byte;
66 uint8_t data = 0;
67#ifdef USE_ESP32
68 switch (logger::global_logger->get_uart()) {
71#if defined(USE_ESP32_VARIANT_ESP32)
73#endif
74 if (this->uart_num_ >= 0) {
75 size_t available;
76 uart_get_buffered_data_len(this->uart_num_, &available);
77 if (available) {
78 uart_read_bytes(this->uart_num_, &data, 1, 0);
79 byte = data;
80 }
81 }
82 break;
83#if defined(USE_LOGGER_USB_CDC) && defined(CONFIG_ESP_CONSOLE_USB_CDC)
85 if (esp_usb_console_available_for_read()) {
86 esp_usb_console_read_buf((char *) &data, 1);
87 byte = data;
88 }
89 break;
90#endif // USE_LOGGER_USB_CDC
91#ifdef USE_LOGGER_USB_SERIAL_JTAG
93 if (usb_serial_jtag_read_bytes((char *) &data, 1, 0)) {
94 byte = data;
95 }
96 break;
97 }
98#endif // USE_LOGGER_USB_SERIAL_JTAG
99 default:
100 break;
101 }
102#elif defined(USE_ARDUINO)
103 if (this->hw_serial_->available()) {
104 this->hw_serial_->readBytes(&data, 1);
105 byte = data;
106 }
107#endif
108 return byte;
109}
110
111void ImprovSerialComponent::write_data_(const uint8_t *data, const size_t size) {
112 // First, set length field
113 this->tx_header_[TX_LENGTH_IDX] = this->tx_header_[TX_TYPE_IDX] == TYPE_RPC_RESPONSE ? size : 1;
114
115 const bool there_is_data = data != nullptr && size > 0;
116 // If there_is_data, checksum must not include our optional data byte
117 const uint8_t header_checksum_len = there_is_data ? TX_BUFFER_SIZE - 3 : TX_BUFFER_SIZE - 2;
118 // Only transmit the full buffer length if there is no data (only state/error byte is provided in this case)
119 const uint8_t header_tx_len = there_is_data ? TX_BUFFER_SIZE - 3 : TX_BUFFER_SIZE;
120 // Calculate checksum for message
121 uint8_t checksum = 0;
122 for (uint8_t i = 0; i < header_checksum_len; i++) {
123 checksum += this->tx_header_[i];
124 }
125 if (there_is_data) {
126 // Include data in checksum
127 for (size_t i = 0; i < size; i++) {
128 checksum += data[i];
129 }
130 }
131 this->tx_header_[TX_CHECKSUM_IDX] = checksum;
132
133#ifdef USE_ESP32
134 switch (logger::global_logger->get_uart()) {
137#if defined(USE_ESP32_VARIANT_ESP32)
139#endif
140 uart_write_bytes(this->uart_num_, this->tx_header_, header_tx_len);
141 if (there_is_data) {
142 uart_write_bytes(this->uart_num_, data, size);
143 uart_write_bytes(this->uart_num_, &this->tx_header_[TX_CHECKSUM_IDX], 2); // Footer: checksum and newline
144 }
145 break;
146#if defined(USE_LOGGER_USB_CDC) && defined(CONFIG_ESP_CONSOLE_USB_CDC)
148 esp_usb_console_write_buf((const char *) this->tx_header_, header_tx_len);
149 if (there_is_data) {
150 esp_usb_console_write_buf((const char *) data, size);
151 esp_usb_console_write_buf((const char *) &this->tx_header_[TX_CHECKSUM_IDX],
152 2); // Footer: checksum and newline
153 }
154 break;
155#endif
156#ifdef USE_LOGGER_USB_SERIAL_JTAG
158 usb_serial_jtag_write_bytes((const char *) this->tx_header_, header_tx_len, 20 / portTICK_PERIOD_MS);
159 if (there_is_data) {
160 usb_serial_jtag_write_bytes((const char *) data, size, 20 / portTICK_PERIOD_MS);
161 usb_serial_jtag_write_bytes((const char *) &this->tx_header_[TX_CHECKSUM_IDX], 2,
162 20 / portTICK_PERIOD_MS); // Footer: checksum and newline
163 }
164 break;
165#endif
166 default:
167 break;
168 }
169#elif defined(USE_ARDUINO)
170 this->hw_serial_->write(this->tx_header_, header_tx_len);
171 if (there_is_data) {
172 this->hw_serial_->write(data, size);
173 this->hw_serial_->write(&this->tx_header_[TX_CHECKSUM_IDX], 2); // Footer: checksum and newline
174 }
175#endif
176}
177
178std::vector<uint8_t> ImprovSerialComponent::build_rpc_settings_response_(improv::Command command) {
179 std::vector<std::string> urls;
180#ifdef USE_IMPROV_SERIAL_NEXT_URL
181 {
182 char url_buffer[384];
183 size_t len = this->get_formatted_next_url_(url_buffer, sizeof(url_buffer));
184 if (len > 0) {
185 urls.emplace_back(url_buffer, len);
186 }
187 }
188#endif
189#ifdef USE_WEBSERVER
190 for (auto &ip : wifi::global_wifi_component->wifi_sta_ip_addresses()) {
191 if (ip.is_ip4()) {
192 char ip_buf[network::IP_ADDRESS_BUFFER_SIZE];
193 ip.str_to(ip_buf);
194 // "http://" (7) + IP (40) + ":" (1) + port (5) + null (1) = 54
195 char webserver_url[7 + network::IP_ADDRESS_BUFFER_SIZE + 1 + 5 + 1];
196 snprintf(webserver_url, sizeof(webserver_url), "http://%s:%u", ip_buf, USE_WEBSERVER_PORT);
197 urls.emplace_back(webserver_url);
198 break;
199 }
200 }
201#endif
202 std::vector<uint8_t> data = improv::build_rpc_response(command, urls, false);
203 return data;
204}
205
207#ifdef ESPHOME_PROJECT_NAME
208 std::vector<std::string> infos = {ESPHOME_PROJECT_NAME, ESPHOME_PROJECT_VERSION, ESPHOME_VARIANT, App.get_name()};
209#else
210 std::vector<std::string> infos = {"ESPHome", ESPHOME_VERSION, ESPHOME_VARIANT, App.get_name()};
211#endif
212 std::vector<uint8_t> data = improv::build_rpc_response(improv::GET_DEVICE_INFO, infos, false);
213 return data;
214};
215
217 size_t at = this->rx_buffer_.size();
218 this->rx_buffer_.push_back(byte);
219 ESP_LOGV(TAG, "Byte: 0x%02X", byte);
220 const uint8_t *raw = &this->rx_buffer_[0];
221
222 return improv::parse_improv_serial_byte(
223 at, byte, raw, [this](improv::ImprovCommand command) -> bool { return this->parse_improv_payload_(command); },
224 [this](improv::Error error) -> void {
225 ESP_LOGW(TAG, "Error decoding payload");
226 this->set_error_(error);
227 });
228}
229
230bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command) {
231 switch (command.command) {
232 case improv::WIFI_SETTINGS: {
233 wifi::WiFiAP sta{};
234 sta.set_ssid(command.ssid.c_str());
235 sta.set_password(command.password.c_str());
236 this->connecting_sta_ = sta;
237
240 this->set_state_(improv::STATE_PROVISIONING);
241 ESP_LOGD(TAG, "Received settings: SSID=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(),
242 command.password.c_str());
243
244 this->set_timeout("wifi-connect-timeout", 30000, [this]() { this->on_wifi_connect_timeout_(); });
245 return true;
246 }
247 case improv::GET_CURRENT_STATE:
248 this->set_state_(this->state_);
249 if (this->state_ == improv::STATE_PROVISIONED) {
250 std::vector<uint8_t> url = this->build_rpc_settings_response_(improv::GET_CURRENT_STATE);
251 this->send_response_(url);
252 }
253 return true;
254 case improv::GET_DEVICE_INFO: {
255 std::vector<uint8_t> info = this->build_version_info_();
256 this->send_response_(info);
257 return true;
258 }
259 case improv::GET_WIFI_NETWORKS: {
260 std::vector<std::string> networks;
261 const auto &results = wifi::global_wifi_component->get_scan_result();
262 for (auto &scan : results) {
263 if (scan.get_is_hidden())
264 continue;
265 const char *ssid_cstr = scan.get_ssid().c_str();
266 // Check if we've already sent this SSID
267 bool duplicate = false;
268 for (const auto &seen : networks) {
269 if (strcmp(seen.c_str(), ssid_cstr) == 0) {
270 duplicate = true;
271 break;
272 }
273 }
274 if (duplicate)
275 continue;
276 // Only allocate std::string after confirming it's not a duplicate
277 std::string ssid(ssid_cstr);
278 // Send each ssid separately to avoid overflowing the buffer
279 char rssi_buf[5]; // int8_t: -128 to 127, max 4 chars + null
280 *int8_to_str(rssi_buf, scan.get_rssi()) = '\0';
281 std::vector<uint8_t> data =
282 improv::build_rpc_response(improv::GET_WIFI_NETWORKS, {ssid, rssi_buf, YESNO(scan.get_with_auth())}, false);
283 this->send_response_(data);
284 networks.push_back(std::move(ssid));
285 }
286 // Send empty response to signify the end of the list.
287 std::vector<uint8_t> data =
288 improv::build_rpc_response(improv::GET_WIFI_NETWORKS, std::vector<std::string>{}, false);
289 this->send_response_(data);
290 return true;
291 }
292 default: {
293 ESP_LOGW(TAG, "Unknown payload");
294 this->set_error_(improv::ERROR_UNKNOWN_RPC);
295 return false;
296 }
297 }
298}
299
301 this->state_ = state;
302 this->tx_header_[TX_TYPE_IDX] = TYPE_CURRENT_STATE;
303 this->tx_header_[TX_DATA_IDX] = state;
304 this->write_data_();
305}
306
307void ImprovSerialComponent::set_error_(improv::Error error) {
308 this->tx_header_[TX_TYPE_IDX] = TYPE_ERROR_STATE;
309 this->tx_header_[TX_DATA_IDX] = error;
310 this->write_data_();
311}
312
313void ImprovSerialComponent::send_response_(std::vector<uint8_t> &response) {
314 this->tx_header_[TX_TYPE_IDX] = TYPE_RPC_RESPONSE;
315 this->write_data_(response.data(), response.size());
316}
317
319 this->set_error_(improv::ERROR_UNABLE_TO_CONNECT);
320 this->set_state_(improv::STATE_AUTHORIZED);
321 ESP_LOGW(TAG, "Timed out while connecting to Wi-Fi network");
323}
324
325ImprovSerialComponent *global_improv_serial_component = // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
326 nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
327
328} // namespace esphome::improv_serial
329
330#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:275
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.