ESPHome 2026.2.3
Loading...
Searching...
No Matches
wifi_component_esp8266.cpp
Go to the documentation of this file.
1#include "wifi_component.h"
3
4#ifdef USE_WIFI
5#ifdef USE_ESP8266
6
7#include <user_interface.h>
8
9#include <utility>
10#include <algorithm>
11#ifdef USE_WIFI_WPA2_EAP
12#include <wpa2_enterprise.h>
13#endif
14
15extern "C" {
16#include "lwip/err.h"
17#include "lwip/dns.h"
18#include "lwip/dhcp.h"
19#include "lwip/init.h" // LWIP_VERSION_
20#include "lwip/apps/sntp.h"
21#include "lwip/netif.h" // struct netif
22#include <AddrList.h>
23#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(3, 0, 0)
24#include "LwipDhcpServer.h"
25#if USE_ARDUINO_VERSION_CODE < VERSION_CODE(3, 1, 0)
26#include <ESP8266WiFi.h>
27#include "ESP8266WiFiAP.h"
28#define wifi_softap_set_dhcps_lease(lease) dhcpSoftAP.set_dhcps_lease(lease)
29#define wifi_softap_set_dhcps_lease_time(time) dhcpSoftAP.set_dhcps_lease_time(time)
30#define wifi_softap_set_dhcps_offer_option(offer, mode) dhcpSoftAP.set_dhcps_offer_option(offer, mode)
31#endif
32#endif
33}
34
36#include "esphome/core/hal.h"
38#include "esphome/core/log.h"
40#include "esphome/core/util.h"
41
42namespace esphome::wifi {
43
44static const char *const TAG = "wifi_esp8266";
45
46static bool s_sta_connected = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
47static bool s_sta_got_ip = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
48static bool s_sta_connect_not_found = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
49static bool s_sta_connect_error = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
50static bool s_sta_connecting = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
51
53 uint8_t current_mode = wifi_get_opmode();
54 bool current_sta = current_mode & 0b01;
55 bool current_ap = current_mode & 0b10;
56 bool target_sta = sta.value_or(current_sta);
57 bool target_ap = ap.value_or(current_ap);
58 if (current_sta == target_sta && current_ap == target_ap)
59 return true;
60
61 if (target_sta && !current_sta) {
62 ESP_LOGV(TAG, "Enabling STA");
63 } else if (!target_sta && current_sta) {
64 ESP_LOGV(TAG, "Disabling STA");
65 // Stop DHCP client when disabling STA
66 // See https://github.com/esp8266/Arduino/pull/5703
67 wifi_station_dhcpc_stop();
68 }
69 if (target_ap && !current_ap) {
70 ESP_LOGV(TAG, "Enabling AP");
71 } else if (!target_ap && current_ap) {
72 ESP_LOGV(TAG, "Disabling AP");
73 }
74
75 ETS_UART_INTR_DISABLE();
76 uint8_t mode = 0;
77 if (target_sta)
78 mode |= 0b01;
79 if (target_ap)
80 mode |= 0b10;
81 bool ret = wifi_set_opmode_current(mode);
82 ETS_UART_INTR_ENABLE();
83
84 if (!ret) {
85 ESP_LOGW(TAG, "Set mode failed");
86 return false;
87 }
88
89 this->ap_started_ = target_ap;
90
91 return ret;
92}
94 sleep_type_t power_save;
95 switch (this->power_save_) {
97 power_save = LIGHT_SLEEP_T;
98 break;
100 power_save = MODEM_SLEEP_T;
101 break;
103 default:
104 power_save = NONE_SLEEP_T;
105 break;
106 }
107 wifi_fpm_auto_sleep_set_in_null_mode(1);
108 bool success = wifi_set_sleep_type(power_save);
109#ifdef USE_WIFI_POWER_SAVE_LISTENERS
110 if (success) {
111 for (auto *listener : this->power_save_listeners_) {
112 listener->on_wifi_power_save(this->power_save_);
113 }
114 }
115#endif
116 return success;
117}
118
119#if LWIP_VERSION_MAJOR != 1
120/*
121 lwip v2 needs to be notified of IP changes, see also
122 https://github.com/d-a-v/Arduino/blob/0e7d21e17144cfc5f53c016191daca8723e89ee8/libraries/ESP8266WiFi/src/ESP8266WiFiSTA.cpp#L251
123 */
124#undef netif_set_addr // need to call lwIP-v1.4 netif_set_addr()
125extern "C" {
126struct netif *eagle_lwip_getif(int netif_index);
127void netif_set_addr(struct netif *netif, const ip4_addr_t *ip, const ip4_addr_t *netmask, const ip4_addr_t *gw);
128};
129#endif
130
132 // enable STA
133 if (!this->wifi_mode_(true, {}))
134 return false;
135
136 enum dhcp_status dhcp_status = wifi_station_dhcpc_status();
137 if (!manual_ip.has_value()) {
138 // lwIP starts the SNTP client if it gets an SNTP server from DHCP. We don't need the time, and more importantly,
139 // the built-in SNTP client has a memory leak in certain situations. Disable this feature.
140 // https://github.com/esphome/issues/issues/2299
141 sntp_servermode_dhcp(false);
142
143 // Use DHCP client
144 if (dhcp_status != DHCP_STARTED) {
145 bool ret = wifi_station_dhcpc_start();
146 if (!ret) {
147 ESP_LOGV(TAG, "Starting DHCP client failed");
148 }
149 return ret;
150 }
151 return true;
152 }
153
154 bool ret = true;
155
156#if LWIP_VERSION_MAJOR != 1
157 // get current->previous IP address
158 // (check below)
159 ip_info previp{};
160 wifi_get_ip_info(STATION_IF, &previp);
161#endif
162
163 struct ip_info info {};
164 info.ip = manual_ip->static_ip;
165 info.gw = manual_ip->gateway;
166 info.netmask = manual_ip->subnet;
167
168 if (dhcp_status == DHCP_STARTED) {
169 bool dhcp_stop_ret = wifi_station_dhcpc_stop();
170 if (!dhcp_stop_ret) {
171 ESP_LOGV(TAG, "Stopping DHCP client failed");
172 ret = false;
173 }
174 }
175 bool wifi_set_info_ret = wifi_set_ip_info(STATION_IF, &info);
176 if (!wifi_set_info_ret) {
177 ESP_LOGV(TAG, "Set manual IP info failed");
178 ret = false;
179 }
180
181 ip_addr_t dns;
182 if (manual_ip->dns1.is_set()) {
183 dns = manual_ip->dns1;
184 dns_setserver(0, &dns);
185 }
186 if (manual_ip->dns2.is_set()) {
187 dns = manual_ip->dns2;
188 dns_setserver(1, &dns);
189 }
190
191#if LWIP_VERSION_MAJOR != 1
192 // trigger address change by calling lwIP-v1.4 api
193 // only when ip is already set by other mean (generally dhcp)
194 if (previp.ip.addr != 0 && previp.ip.addr != info.ip.addr) {
195 netif_set_addr(eagle_lwip_getif(STATION_IF), reinterpret_cast<const ip4_addr_t *>(&info.ip),
196 reinterpret_cast<const ip4_addr_t *>(&info.netmask), reinterpret_cast<const ip4_addr_t *>(&info.gw));
197 }
198#endif
199 return ret;
200}
201
203 if (!this->has_sta())
204 return {};
205 network::IPAddresses addresses;
206 uint8_t index = 0;
207 for (auto &addr : addrList) {
208 addresses[index++] = addr.ipFromNetifNum();
209 }
210 return addresses;
211}
213 const std::string &hostname = App.get_name();
214 bool ret = wifi_station_set_hostname(const_cast<char *>(hostname.c_str()));
215 if (!ret) {
216 ESP_LOGV(TAG, "Set hostname failed");
217 }
218
219 // Update hostname on all lwIP interfaces so DHCP packets include it.
220 // lwIP includes the hostname in DHCP DISCOVER/REQUEST automatically
221 // via LWIP_NETIF_HOSTNAME — no dhcp_renew() needed. The hostname is
222 // fixed at compile time and never changes at runtime.
223 for (netif *intf = netif_list; intf; intf = intf->next) {
224#if LWIP_VERSION_MAJOR == 1
225 intf->hostname = (char *) wifi_station_get_hostname();
226#else
227 intf->hostname = wifi_station_get_hostname();
228#endif
229 }
230
231 return ret;
232}
233
235 // enable STA
236 if (!this->wifi_mode_(true, {}))
237 return false;
238
239 this->wifi_disconnect_();
240
241 struct station_config conf {};
242 memset(&conf, 0, sizeof(conf));
243 if (ap.ssid_.size() > sizeof(conf.ssid)) {
244 ESP_LOGE(TAG, "SSID too long");
245 return false;
246 }
247 if (ap.password_.size() > sizeof(conf.password)) {
248 ESP_LOGE(TAG, "Password too long");
249 return false;
250 }
251 memcpy(reinterpret_cast<char *>(conf.ssid), ap.ssid_.c_str(), ap.ssid_.size());
252 memcpy(reinterpret_cast<char *>(conf.password), ap.password_.c_str(), ap.password_.size());
253
254 if (ap.has_bssid()) {
255 conf.bssid_set = 1;
256 memcpy(conf.bssid, ap.get_bssid().data(), 6);
257 } else {
258 conf.bssid_set = 0;
259 }
260
261#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 4, 0)
262 if (ap.password_.empty()) {
263 conf.threshold.authmode = AUTH_OPEN;
264 } else {
265 // Set threshold based on configured minimum auth mode
266 // Note: ESP8266 doesn't support WPA3
267 switch (this->min_auth_mode_) {
269 conf.threshold.authmode = AUTH_WPA_PSK;
270 break;
272 case WIFI_MIN_AUTH_MODE_WPA3: // Fall back to WPA2 for ESP8266
273 conf.threshold.authmode = AUTH_WPA2_PSK;
274 break;
275 }
276 }
277 conf.threshold.rssi = -127;
278#endif
279
280 ETS_UART_INTR_DISABLE();
281 bool ret = wifi_station_set_config_current(&conf);
282 ETS_UART_INTR_ENABLE();
283
284 if (!ret) {
285 ESP_LOGV(TAG, "Set Station config failed");
286 return false;
287 }
288
289#ifdef USE_WIFI_MANUAL_IP
290 if (!this->wifi_sta_ip_config_(ap.get_manual_ip())) {
291 return false;
292 }
293#else
294 if (!this->wifi_sta_ip_config_({})) {
295 return false;
296 }
297#endif
298
299 // setup enterprise authentication if required
300#ifdef USE_WIFI_WPA2_EAP
301 if (ap.get_eap().has_value()) {
302 // note: all certificates and keys have to be null terminated. Lengths are appended by +1 to include \0.
303 EAPAuth eap = ap.get_eap().value();
304 ret = wifi_station_set_enterprise_identity((uint8_t *) eap.identity.c_str(), eap.identity.length());
305 if (ret) {
306 ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_identity failed: %d", ret);
307 }
308 int ca_cert_len = strlen(eap.ca_cert);
309 int client_cert_len = strlen(eap.client_cert);
310 int client_key_len = strlen(eap.client_key);
311 if (ca_cert_len) {
312 ret = wifi_station_set_enterprise_ca_cert((uint8_t *) eap.ca_cert, ca_cert_len + 1);
313 if (ret) {
314 ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_ca_cert failed: %d", ret);
315 }
316 }
317 // workout what type of EAP this is
318 // validation is not required as the config tool has already validated it
319 if (client_cert_len && client_key_len) {
320 // if we have certs, this must be EAP-TLS
321 ret = wifi_station_set_enterprise_cert_key((uint8_t *) eap.client_cert, client_cert_len + 1,
322 (uint8_t *) eap.client_key, client_key_len + 1,
323 (uint8_t *) eap.password.c_str(), eap.password.length());
324 if (ret) {
325 ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_cert_key failed: %d", ret);
326 }
327 } else {
328 // in the absence of certs, assume this is username/password based
329 ret = wifi_station_set_enterprise_username((uint8_t *) eap.username.c_str(), eap.username.length());
330 if (ret) {
331 ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_username failed: %d", ret);
332 }
333 ret = wifi_station_set_enterprise_password((uint8_t *) eap.password.c_str(), eap.password.length());
334 if (ret) {
335 ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_password failed: %d", ret);
336 }
337 }
338 ret = wifi_station_set_wpa2_enterprise_auth(true);
339 if (ret) {
340 ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_enable failed: %d", ret);
341 }
342 }
343#endif // USE_WIFI_WPA2_EAP
344
345 this->wifi_apply_hostname_();
346
347 // Reset flags, do this _before_ wifi_station_connect as the callback method
348 // may be called from wifi_station_connect
349 s_sta_connecting = true;
350 s_sta_connected = false;
351 s_sta_got_ip = false;
352 s_sta_connect_error = false;
353 s_sta_connect_not_found = false;
354
355 ETS_UART_INTR_DISABLE();
356 ret = wifi_station_connect();
357 ETS_UART_INTR_ENABLE();
358 if (!ret) {
359 ESP_LOGV(TAG, "wifi_station_connect failed");
360 return false;
361 }
362
363#if USE_NETWORK_IPV6
364 bool connected = false;
365 while (!connected) {
366 uint8_t ipv6_addr_count = 0;
367 for (auto addr : addrList) {
368 char ip_buf[network::IP_ADDRESS_BUFFER_SIZE];
369 ESP_LOGV(TAG, "Address %s", network::IPAddress(addr.ipFromNetifNum()).str_to(ip_buf));
370 if (addr.isV6()) {
371 ipv6_addr_count++;
372 }
373 }
374 delay(500); // NOLINT
375 connected = (ipv6_addr_count >= USE_NETWORK_MIN_IPV6_ADDR_COUNT);
376 }
377#endif /* USE_NETWORK_IPV6 */
378
379 if (ap.has_channel()) {
380 ret = wifi_set_channel(ap.get_channel());
381 if (!ret) {
382 ESP_LOGV(TAG, "wifi_set_channel failed");
383 return false;
384 }
385 }
386
387 return true;
388}
389
390class WiFiMockClass : public ESP8266WiFiGenericClass {
391 public:
392 static void _event_callback(void *event) { ESP8266WiFiGenericClass::_eventCallback(event); } // NOLINT
393};
394
395// Auth mode strings indexed by AUTH_* constants (0-4), with UNKNOWN at last index
396// Static asserts verify the SDK constants are contiguous as expected
397static_assert(AUTH_OPEN == 0 && AUTH_WEP == 1 && AUTH_WPA_PSK == 2 && AUTH_WPA2_PSK == 3 && AUTH_WPA_WPA2_PSK == 4,
398 "AUTH_* constants are not contiguous");
399PROGMEM_STRING_TABLE(AuthModeStrings, "OPEN", "WEP", "WPA PSK", "WPA2 PSK", "WPA/WPA2 PSK", "UNKNOWN");
400
401const LogString *get_auth_mode_str(uint8_t mode) {
402 return AuthModeStrings::get_log_str(mode, AuthModeStrings::LAST_INDEX);
403}
404
405// WiFi op mode strings indexed by WIFI_* constants (0-3), with UNKNOWN at last index
406static_assert(WIFI_OFF == 0 && WIFI_STA == 1 && WIFI_AP == 2 && WIFI_AP_STA == 3,
407 "WIFI_* op mode constants are not contiguous");
408PROGMEM_STRING_TABLE(OpModeStrings, "OFF", "STA", "AP", "AP+STA", "UNKNOWN");
409
410const LogString *get_op_mode_str(uint8_t mode) { return OpModeStrings::get_log_str(mode, OpModeStrings::LAST_INDEX); }
411
412// Use if-chain instead of switch to avoid jump tables in RODATA (wastes RAM on ESP8266).
413// A single switch would generate a sparse lookup table with ~175 default entries, wasting 700 bytes of RAM.
414// Even split switches still generate smaller jump tables in RODATA.
415const LogString *get_disconnect_reason_str(uint8_t reason) {
416 if (reason == REASON_AUTH_EXPIRE)
417 return LOG_STR("Auth Expired");
418 if (reason == REASON_AUTH_LEAVE)
419 return LOG_STR("Auth Leave");
420 if (reason == REASON_ASSOC_EXPIRE)
421 return LOG_STR("Association Expired");
422 if (reason == REASON_ASSOC_TOOMANY)
423 return LOG_STR("Too Many Associations");
424 if (reason == REASON_NOT_AUTHED)
425 return LOG_STR("Not Authenticated");
426 if (reason == REASON_NOT_ASSOCED)
427 return LOG_STR("Not Associated");
428 if (reason == REASON_ASSOC_LEAVE)
429 return LOG_STR("Association Leave");
430 if (reason == REASON_ASSOC_NOT_AUTHED)
431 return LOG_STR("Association not Authenticated");
432 if (reason == REASON_DISASSOC_PWRCAP_BAD)
433 return LOG_STR("Disassociate Power Cap Bad");
434 if (reason == REASON_DISASSOC_SUPCHAN_BAD)
435 return LOG_STR("Disassociate Supported Channel Bad");
436 if (reason == REASON_IE_INVALID)
437 return LOG_STR("IE Invalid");
438 if (reason == REASON_MIC_FAILURE)
439 return LOG_STR("Mic Failure");
440 if (reason == REASON_4WAY_HANDSHAKE_TIMEOUT)
441 return LOG_STR("4-Way Handshake Timeout");
442 if (reason == REASON_GROUP_KEY_UPDATE_TIMEOUT)
443 return LOG_STR("Group Key Update Timeout");
444 if (reason == REASON_IE_IN_4WAY_DIFFERS)
445 return LOG_STR("IE In 4-Way Handshake Differs");
446 if (reason == REASON_GROUP_CIPHER_INVALID)
447 return LOG_STR("Group Cipher Invalid");
448 if (reason == REASON_PAIRWISE_CIPHER_INVALID)
449 return LOG_STR("Pairwise Cipher Invalid");
450 if (reason == REASON_AKMP_INVALID)
451 return LOG_STR("AKMP Invalid");
452 if (reason == REASON_UNSUPP_RSN_IE_VERSION)
453 return LOG_STR("Unsupported RSN IE version");
454 if (reason == REASON_INVALID_RSN_IE_CAP)
455 return LOG_STR("Invalid RSN IE Cap");
456 if (reason == REASON_802_1X_AUTH_FAILED)
457 return LOG_STR("802.1x Authentication Failed");
458 if (reason == REASON_CIPHER_SUITE_REJECTED)
459 return LOG_STR("Cipher Suite Rejected");
460 if (reason == REASON_BEACON_TIMEOUT)
461 return LOG_STR("Beacon Timeout");
462 if (reason == REASON_NO_AP_FOUND)
463 return LOG_STR("AP Not Found");
464 if (reason == REASON_AUTH_FAIL)
465 return LOG_STR("Authentication Failed");
466 if (reason == REASON_ASSOC_FAIL)
467 return LOG_STR("Association Failed");
468 if (reason == REASON_HANDSHAKE_TIMEOUT)
469 return LOG_STR("Handshake Failed");
470 return LOG_STR("Unspecified");
471}
472
473// TODO: This callback runs in ESP8266 system context with limited stack (~2KB).
474// All listener notifications should be deferred to wifi_loop_() via pending_ flags
475// to avoid stack overflow. Currently only connect_state is deferred; disconnect,
476// IP, and scan listeners still run in this context and should be migrated.
477void WiFiComponent::wifi_event_callback(System_Event_t *event) {
478 switch (event->event) {
479 case EVENT_STAMODE_CONNECTED: {
480 auto it = event->event_info.connected;
481#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
482 char bssid_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
483 format_mac_addr_upper(it.bssid, bssid_buf);
484 ESP_LOGV(TAG, "Connected ssid='%.*s' bssid=%s channel=%u", it.ssid_len, (const char *) it.ssid, bssid_buf,
485 it.channel);
486#endif
487 s_sta_connected = true;
488#ifdef USE_WIFI_CONNECT_STATE_LISTENERS
489 // Defer listener notification until state machine reaches STA_CONNECTED
490 // This ensures wifi.connected condition returns true in listener automations
492#endif
493 break;
494 }
495 case EVENT_STAMODE_DISCONNECTED: {
496 auto it = event->event_info.disconnected;
497 if (it.reason == REASON_NO_AP_FOUND) {
498 ESP_LOGW(TAG, "Disconnected ssid='%.*s' reason='Probe Request Unsuccessful'", it.ssid_len,
499 (const char *) it.ssid);
500 s_sta_connect_not_found = true;
501 } else {
502 char bssid_s[18];
503 format_mac_addr_upper(it.bssid, bssid_s);
504 ESP_LOGW(TAG, "Disconnected ssid='%.*s' bssid=" LOG_SECRET("%s") " reason='%s'", it.ssid_len,
505 (const char *) it.ssid, bssid_s, LOG_STR_ARG(get_disconnect_reason_str(it.reason)));
506 s_sta_connect_error = true;
507 }
508 s_sta_connected = false;
509 s_sta_connecting = false;
511#ifdef USE_WIFI_CONNECT_STATE_LISTENERS
513#endif
514 break;
515 }
516 case EVENT_STAMODE_AUTHMODE_CHANGE: {
517 auto it = event->event_info.auth_change;
518 ESP_LOGV(TAG, "Changed Authmode old=%s new=%s", LOG_STR_ARG(get_auth_mode_str(it.old_mode)),
519 LOG_STR_ARG(get_auth_mode_str(it.new_mode)));
520 // Mitigate CVE-2020-12638
521 // https://lbsfilm.at/blog/wpa2-authenticationmode-downgrade-in-espressif-microprocessors
522 if (it.old_mode != AUTH_OPEN && it.new_mode == AUTH_OPEN) {
523 ESP_LOGW(TAG, "Potential Authmode downgrade detected, disconnecting");
524 wifi_station_disconnect();
526 }
527 break;
528 }
529 case EVENT_STAMODE_GOT_IP: {
530 auto it = event->event_info.got_ip;
531 char ip_buf[network::IP_ADDRESS_BUFFER_SIZE], gw_buf[network::IP_ADDRESS_BUFFER_SIZE],
532 mask_buf[network::IP_ADDRESS_BUFFER_SIZE];
533 ESP_LOGV(TAG, "static_ip=%s gateway=%s netmask=%s", network::IPAddress(&it.ip).str_to(ip_buf),
534 network::IPAddress(&it.gw).str_to(gw_buf), network::IPAddress(&it.mask).str_to(mask_buf));
535 s_sta_got_ip = true;
536#ifdef USE_WIFI_IP_STATE_LISTENERS
537 // Defer listener callbacks to main loop - system context has limited stack
539#endif
540 break;
541 }
542 case EVENT_STAMODE_DHCP_TIMEOUT: {
543 ESP_LOGW(TAG, "DHCP request timeout");
544 break;
545 }
546 case EVENT_SOFTAPMODE_STACONNECTED: {
547#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
548 auto it = event->event_info.sta_connected;
549 char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
550 format_mac_addr_upper(it.mac, mac_buf);
551 ESP_LOGV(TAG, "AP client connected MAC=%s aid=%u", mac_buf, it.aid);
552#endif
553 break;
554 }
555 case EVENT_SOFTAPMODE_STADISCONNECTED: {
556#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
557 auto it = event->event_info.sta_disconnected;
558 char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
559 format_mac_addr_upper(it.mac, mac_buf);
560 ESP_LOGV(TAG, "AP client disconnected MAC=%s aid=%u", mac_buf, it.aid);
561#endif
562 break;
563 }
564 case EVENT_SOFTAPMODE_PROBEREQRECVED: {
565#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
566 auto it = event->event_info.ap_probereqrecved;
567 char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
568 format_mac_addr_upper(it.mac, mac_buf);
569 ESP_LOGVV(TAG, "AP receive Probe Request MAC=%s RSSI=%d", mac_buf, it.rssi);
570#endif
571 break;
572 }
573#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 4, 0)
574 case EVENT_OPMODE_CHANGED: {
575 auto it = event->event_info.opmode_changed;
576 ESP_LOGV(TAG, "Changed Mode old=%s new=%s", LOG_STR_ARG(get_op_mode_str(it.old_opmode)),
577 LOG_STR_ARG(get_op_mode_str(it.new_opmode)));
578 break;
579 }
580 case EVENT_SOFTAPMODE_DISTRIBUTE_STA_IP: {
581#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
582 auto it = event->event_info.distribute_sta_ip;
583 char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
584 char ip_buf[network::IP_ADDRESS_BUFFER_SIZE];
585 format_mac_addr_upper(it.mac, mac_buf);
586 ESP_LOGV(TAG, "AP Distribute Station IP MAC=%s IP=%s aid=%u", mac_buf, network::IPAddress(&it.ip).str_to(ip_buf),
587 it.aid);
588#endif
589 break;
590 }
591#endif
592 default:
593 break;
594 }
595
596 WiFiMockClass::_event_callback(event);
597}
598
600 uint8_t val = static_cast<uint8_t>(output_power * 4);
601 system_phy_set_max_tpw(val);
602 return true;
603}
605 if (!this->wifi_mode_(true, {}))
606 return false;
607
608 bool ret1, ret2;
609 ETS_UART_INTR_DISABLE();
610 ret1 = wifi_station_set_auto_connect(0);
611 ret2 = wifi_station_set_reconnect_policy(false);
612 ETS_UART_INTR_ENABLE();
613
614 if (!ret1 || !ret2) {
615 ESP_LOGV(TAG, "Disabling Auto-Connect failed");
616 }
617
618 delay(10);
619 return true;
620}
621
623 wifi_set_event_handler_cb(&WiFiComponent::wifi_event_callback);
624
625 // Make sure WiFi is in clean state before anything starts
626 this->wifi_mode_(false, false);
627}
628
630 station_status_t status = wifi_station_get_connect_status();
631 if (status == STATION_GOT_IP)
633 if (status == STATION_NO_AP_FOUND)
635 if (status == STATION_CONNECT_FAIL || status == STATION_WRONG_PASSWORD)
637 if (status == STATION_CONNECTING)
640}
642 static bool first_scan = false;
643
644 // enable STA
645 if (!this->wifi_mode_(true, {}))
646 return false;
647
648 // Reset scan_done_ before starting new scan to prevent stale flag from previous scan
649 // (e.g., roaming scan completed just before unexpected disconnect)
650 this->scan_done_ = false;
651
652 struct scan_config config {};
653 memset(&config, 0, sizeof(config));
654 config.ssid = nullptr;
655 config.bssid = nullptr;
656 config.channel = 0;
657 config.show_hidden = 1;
658#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 4, 0)
659 config.scan_type = passive ? WIFI_SCAN_TYPE_PASSIVE : WIFI_SCAN_TYPE_ACTIVE;
660 if (first_scan) {
661 if (passive) {
662 config.scan_time.passive = 200;
663 } else {
664 config.scan_time.active.min = 100;
665 config.scan_time.active.max = 200;
666 }
667 } else {
668 if (passive) {
669 config.scan_time.passive = 500;
670 } else {
671 config.scan_time.active.min = 400;
672 config.scan_time.active.max = 500;
673 }
674 }
675#endif
676 first_scan = false;
677 bool ret = wifi_station_scan(&config, &WiFiComponent::s_wifi_scan_done_callback);
678 if (!ret) {
679 ESP_LOGV(TAG, "wifi_station_scan failed");
680 return false;
681 }
682
683 return ret;
684}
686 bool ret = true;
687 // Only call disconnect if interface is up
688 if (wifi_get_opmode() & WIFI_STA)
689 ret = wifi_station_disconnect();
690 station_config conf{};
691 memset(&conf, 0, sizeof(conf));
692 ETS_UART_INTR_DISABLE();
693 wifi_station_set_config_current(&conf);
694 ETS_UART_INTR_ENABLE();
695 return ret;
696}
700
701void WiFiComponent::wifi_scan_done_callback_(void *arg, STATUS status) {
702 this->scan_result_.clear();
703
704 if (status != OK) {
705 ESP_LOGV(TAG, "Scan failed: %d", status);
706 // Don't call retry_connect() here - this callback runs in SDK system context
707 // where yield() cannot be called. Instead, just set scan_done_ and let
708 // check_scanning_finished() handle the empty scan_result_ from loop context.
709 this->scan_done_ = true;
710 return;
711 }
712
713 auto *head = reinterpret_cast<bss_info *>(arg);
714 bool needs_full = this->needs_full_scan_results_();
715
716 // First pass: count matching networks (linked list is non-destructive)
717 size_t total = 0;
718 size_t count = 0;
719 for (bss_info *it = head; it != nullptr; it = STAILQ_NEXT(it, next)) {
720 total++;
721 const char *ssid_cstr = reinterpret_cast<const char *>(it->ssid);
722 if (needs_full || this->matches_configured_network_(ssid_cstr, it->bssid)) {
723 count++;
724 }
725 }
726
727 this->scan_result_.init(count); // Exact allocation
728
729 // Second pass: store matching networks
730 for (bss_info *it = head; it != nullptr; it = STAILQ_NEXT(it, next)) {
731 const char *ssid_cstr = reinterpret_cast<const char *>(it->ssid);
732 if (needs_full || this->matches_configured_network_(ssid_cstr, it->bssid)) {
733 this->scan_result_.emplace_back(
734 bssid_t{it->bssid[0], it->bssid[1], it->bssid[2], it->bssid[3], it->bssid[4], it->bssid[5]}, ssid_cstr,
735 it->ssid_len, it->channel, it->rssi, it->authmode != AUTH_OPEN, it->is_hidden != 0);
736 } else {
737 this->log_discarded_scan_result_(ssid_cstr, it->bssid, it->rssi, it->channel);
738 }
739 }
740 ESP_LOGV(TAG, "Scan complete: %zu found, %zu stored%s", total, this->scan_result_.size(),
741 needs_full ? "" : " (filtered)");
742 this->scan_done_ = true;
743#ifdef USE_WIFI_SCAN_RESULTS_LISTENERS
744 this->pending_.scan_complete = true; // Defer listener callbacks to main loop
745#endif
746}
747
748#ifdef USE_WIFI_AP
750 // enable AP
751 if (!this->wifi_mode_({}, true))
752 return false;
753
754 struct ip_info info {};
755 if (manual_ip.has_value()) {
756 info.ip = manual_ip->static_ip;
757 info.gw = manual_ip->gateway;
758 info.netmask = manual_ip->subnet;
759 } else {
760 info.ip = network::IPAddress(192, 168, 4, 1);
761 info.gw = network::IPAddress(192, 168, 4, 1);
762 info.netmask = network::IPAddress(255, 255, 255, 0);
763 }
764
765 if (wifi_softap_dhcps_status() == DHCP_STARTED) {
766 if (!wifi_softap_dhcps_stop()) {
767 ESP_LOGW(TAG, "Stopping DHCP server failed");
768 }
769 }
770
771 if (!wifi_set_ip_info(SOFTAP_IF, &info)) {
772 ESP_LOGE(TAG, "Set SoftAP info failed");
773 return false;
774 }
775
776#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(3, 0, 0) && USE_ARDUINO_VERSION_CODE < VERSION_CODE(3, 1, 0)
777 dhcpSoftAP.begin(&info);
778#endif
779
780 struct dhcps_lease lease {};
781 lease.enable = true;
782 network::IPAddress start_address = network::IPAddress(&info.ip);
783 start_address += 99;
784 lease.start_ip = start_address;
785#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
786 char ip_buf[network::IP_ADDRESS_BUFFER_SIZE];
787#endif
788 ESP_LOGV(TAG, "DHCP server IP lease start: %s", start_address.str_to(ip_buf));
789 start_address += 10;
790 lease.end_ip = start_address;
791 ESP_LOGV(TAG, "DHCP server IP lease end: %s", start_address.str_to(ip_buf));
792 if (!wifi_softap_set_dhcps_lease(&lease)) {
793 ESP_LOGE(TAG, "Set SoftAP DHCP lease failed");
794 return false;
795 }
796
797 // lease time 1440 minutes (=24 hours)
798 if (!wifi_softap_set_dhcps_lease_time(1440)) {
799 ESP_LOGE(TAG, "Set SoftAP DHCP lease time failed");
800 return false;
801 }
802
803#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(3, 1, 0)
804 ESP8266WiFiClass::softAPDhcpServer().setRouter(true); // send ROUTER option with netif's gateway IP
805#else
806 uint8_t mode = 1;
807 // bit0, 1 enables router information from ESP8266 SoftAP DHCP server.
808 if (!wifi_softap_set_dhcps_offer_option(OFFER_ROUTER, &mode)) {
809 ESP_LOGE(TAG, "wifi_softap_set_dhcps_offer_option failed");
810 return false;
811 }
812#endif
813
814 if (!wifi_softap_dhcps_start()) {
815 ESP_LOGE(TAG, "Starting SoftAP DHCPS failed");
816 return false;
817 }
818
819 return true;
820}
821
823 // enable AP
824 if (!this->wifi_mode_({}, true))
825 return false;
826
827 struct softap_config conf {};
828 if (ap.ssid_.size() > sizeof(conf.ssid)) {
829 ESP_LOGE(TAG, "AP SSID too long");
830 return false;
831 }
832 memcpy(reinterpret_cast<char *>(conf.ssid), ap.ssid_.c_str(), ap.ssid_.size());
833 conf.ssid_len = static_cast<uint8>(ap.ssid_.size());
834 conf.channel = ap.has_channel() ? ap.get_channel() : 1;
835 conf.ssid_hidden = ap.get_hidden();
836 conf.max_connection = 5;
837 conf.beacon_interval = 100;
838
839 if (ap.password_.empty()) {
840 conf.authmode = AUTH_OPEN;
841 *conf.password = 0;
842 } else {
843 conf.authmode = AUTH_WPA2_PSK;
844 if (ap.password_.size() > sizeof(conf.password)) {
845 ESP_LOGE(TAG, "AP password too long");
846 return false;
847 }
848 memcpy(reinterpret_cast<char *>(conf.password), ap.password_.c_str(), ap.password_.size());
849 }
850
851 ETS_UART_INTR_DISABLE();
852 bool ret = wifi_softap_set_config_current(&conf);
853 ETS_UART_INTR_ENABLE();
854
855 if (!ret) {
856 ESP_LOGV(TAG, "wifi_softap_set_config_current failed");
857 return false;
858 }
859
860#ifdef USE_WIFI_MANUAL_IP
861 if (!this->wifi_ap_ip_config_(ap.get_manual_ip())) {
862 ESP_LOGV(TAG, "wifi_ap_ip_config_ failed");
863 return false;
864 }
865#else
866 if (!this->wifi_ap_ip_config_({})) {
867 ESP_LOGV(TAG, "wifi_ap_ip_config_ failed");
868 return false;
869 }
870#endif
871
872 return true;
873}
874
876 struct ip_info ip {};
877 wifi_get_ip_info(SOFTAP_IF, &ip);
878 return network::IPAddress(&ip.ip);
879}
880#endif // USE_WIFI_AP
881
883 bssid_t bssid{};
884 struct station_config conf {};
885 if (wifi_station_get_config(&conf)) {
886 std::copy_n(conf.bssid, bssid.size(), bssid.begin());
887 }
888 return bssid;
889}
891 struct station_config conf {};
892 if (!wifi_station_get_config(&conf)) {
893 return "";
894 }
895 // conf.ssid is uint8[32], not null-terminated if full
896 auto *ssid_s = reinterpret_cast<const char *>(conf.ssid);
897 size_t len = strnlen(ssid_s, sizeof(conf.ssid));
898 return {ssid_s, len};
899}
900const char *WiFiComponent::wifi_ssid_to(std::span<char, SSID_BUFFER_SIZE> buffer) {
901 struct station_config conf {};
902 if (!wifi_station_get_config(&conf)) {
903 buffer[0] = '\0';
904 return buffer.data();
905 }
906 // conf.ssid is uint8[32], not null-terminated if full
907 size_t len = strnlen(reinterpret_cast<const char *>(conf.ssid), sizeof(conf.ssid));
908 memcpy(buffer.data(), conf.ssid, len);
909 buffer[len] = '\0';
910 return buffer.data();
911}
913 if (wifi_station_get_connect_status() != STATION_GOT_IP)
914 return WIFI_RSSI_DISCONNECTED;
915 sint8 rssi = wifi_station_get_rssi();
916 // Values >= 31 are error codes per NONOS SDK API, not valid RSSI readings
917 return rssi >= 31 ? WIFI_RSSI_DISCONNECTED : rssi;
918}
919int32_t WiFiComponent::get_wifi_channel() { return wifi_get_channel(); }
921 struct ip_info ip {};
922 wifi_get_ip_info(STATION_IF, &ip);
923 return network::IPAddress(&ip.netmask);
924}
926 struct ip_info ip {};
927 wifi_get_ip_info(STATION_IF, &ip);
928 return network::IPAddress(&ip.gw);
929}
932
934 // Process callbacks deferred from ESP8266 SDK system context (~2KB stack)
935 // to main loop context (full stack). Connect state listeners are handled
936 // by notify_connect_state_listeners_() in the shared state machine code.
937
938#ifdef USE_WIFI_CONNECT_STATE_LISTENERS
939 if (this->pending_.disconnect) {
940 this->pending_.disconnect = false;
942 }
943#endif
944
945#ifdef USE_WIFI_IP_STATE_LISTENERS
946 if (this->pending_.got_ip) {
947 this->pending_.got_ip = false;
949 }
950#endif
951
952#ifdef USE_WIFI_SCAN_RESULTS_LISTENERS
953 if (this->pending_.scan_complete) {
954 this->pending_.scan_complete = false;
956 }
957#endif
958}
959
960} // namespace esphome::wifi
961#endif
962#endif
BedjetMode mode
BedJet operating mode.
uint8_t status
Definition bl0942.h:8
const std::string & get_name() const
Get the name of this Application set by pre_setup().
bool has_value() const
Definition optional.h:92
value_type value_or(U const &v) const
Definition optional.h:98
const char * c_str() const
uint8_t get_channel() const
const optional< EAPAuth > & get_eap() const
const optional< ManualIP > & get_manual_ip() const
const bssid_t & get_bssid() const
void wifi_scan_done_callback_(void *arg, STATUS status)
void notify_scan_results_listeners_()
Notify scan results listeners with current scan results.
WiFiSTAConnectStatus wifi_sta_connect_status_() const
struct esphome::wifi::WiFiComponent::@175 pending_
wifi_scan_vector_t< WiFiScanResult > scan_result_
void notify_ip_state_listeners_()
Notify IP state listeners with current addresses.
bool wifi_sta_ip_config_(const optional< ManualIP > &manual_ip)
static void wifi_event_callback(System_Event_t *event)
void notify_disconnect_state_listeners_()
Notify connect state listeners of disconnection.
void log_discarded_scan_result_(const char *ssid, const uint8_t *bssid, int8_t rssi, uint8_t channel)
Log a discarded scan result at VERBOSE level (skipped during roaming scans to avoid log overflow)
const char * wifi_ssid_to(std::span< char, SSID_BUFFER_SIZE > buffer)
Write SSID to buffer without heap allocation.
static void s_wifi_scan_done_callback(void *arg, STATUS status)
network::IPAddress wifi_dns_ip_(int num)
bool matches_configured_network_(const char *ssid, const uint8_t *bssid) const
Check if network matches any configured network (for scan result filtering) Matches by SSID when conf...
bool wifi_ap_ip_config_(const optional< ManualIP > &manual_ip)
bool needs_full_scan_results_() const
Check if full scan results are needed (captive portal active, improv, listeners)
StaticVector< WiFiPowerSaveListener *, ESPHOME_WIFI_POWER_SAVE_LISTENERS > power_save_listeners_
bool wifi_apply_output_power_(float output_power)
bool wifi_mode_(optional< bool > sta, optional< bool > ap)
network::IPAddresses wifi_sta_ip_addresses()
in_addr ip_addr_t
Definition ip_address.h:22
in_addr ip4_addr_t
Definition ip_address.h:23
mopeka_std_values val[4]
std::array< IPAddress, 5 > IPAddresses
Definition ip_address.h:188
const char *const TAG
Definition spi.cpp:7
std::array< uint8_t, 6 > bssid_t
const LogString * get_auth_mode_str(uint8_t mode)
const LogString * get_disconnect_reason_str(uint8_t reason)
struct netif * eagle_lwip_getif(int netif_index)
void netif_set_addr(struct netif *netif, const ip4_addr_t *ip, const ip4_addr_t *netmask, const ip4_addr_t *gw)
WiFiComponent * global_wifi_component
PROGMEM_STRING_TABLE(AuthModeStrings, "OPEN", "WEP", "WPA PSK", "WPA2 PSK", "WPA/WPA2 PSK", "UNKNOWN")
const LogString * get_op_mode_str(uint8_t mode)
std::string size_t len
Definition helpers.h:692
void IRAM_ATTR HOT delay(uint32_t ms)
Definition core.cpp:26
Application App
Global storage of Application pointer - only one Application can exist.
char * format_mac_addr_upper(const uint8_t *mac, char *output)
Format MAC address as XX:XX:XX:XX:XX:XX (uppercase, colon separators)
Definition helpers.h:1045