ESPHome 2026.5.0-dev
Loading...
Searching...
No Matches
wifi_component_libretiny.cpp
Go to the documentation of this file.
1#include "wifi_component.h"
2
3#ifdef USE_WIFI
4#ifdef USE_LIBRETINY
5
6#include <cinttypes>
7#include <utility>
8#include <algorithm>
9#include "lwip/ip_addr.h"
10#include "lwip/err.h"
11#include "lwip/dns.h"
12
13#include <FreeRTOS.h>
14#include <queue.h>
15
16#ifdef USE_BK72XX
17extern "C" {
18#include <wlan_ui_pub.h>
19}
20#endif
21
22#ifdef USE_RTL87XX
23extern "C" {
24#include <wifi_conf.h>
25#include <wifi_structures.h>
26}
27#endif
28
30#include "esphome/core/hal.h"
32#include "esphome/core/log.h"
33#include "esphome/core/util.h"
34
35namespace esphome::wifi {
36
37static const char *const TAG = "wifi_lt";
38
39// Thread-safe event handling for LibreTiny WiFi
40//
41// LibreTiny's WiFi.onEvent() callback runs in the WiFi driver's thread context,
42// not the main ESPHome loop. Without synchronization, modifying shared state
43// (like connection status flags) from the callback causes race conditions:
44// - The main loop may never see state changes (values cached in registers)
45// - State changes may be visible in inconsistent order
46// - LibreTiny targets (BK7231, RTL8720) lack atomic instructions (no LDREX/STREX)
47//
48// Solution: Queue events in the callback and process them in the main loop.
49// This is the same approach used by ESP32 IDF's wifi_process_event_().
50// All state modifications happen in the main loop context, eliminating races.
51
52static constexpr size_t EVENT_QUEUE_SIZE = 16; // Max pending WiFi events before overflow
53static QueueHandle_t s_event_queue = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
54static volatile uint32_t s_event_queue_overflow_count =
55 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
56
57// Event structure for queued WiFi events - contains a copy of event data
58// to avoid lifetime issues with the original event data from the callback
59struct LTWiFiEvent {
60 arduino_event_id_t event_id;
61 union {
62 struct {
63 uint8_t ssid[33];
64 uint8_t ssid_len;
65 uint8_t bssid[6];
66 uint8_t channel;
67 uint8_t authmode;
68 } sta_connected;
69 struct {
70 uint8_t ssid[33];
71 uint8_t ssid_len;
72 uint8_t bssid[6];
73 uint8_t reason;
74 } sta_disconnected;
75 struct {
76 uint8_t old_mode;
77 uint8_t new_mode;
78 } sta_authmode_change;
79 struct {
80 uint32_t status;
81 uint8_t number;
82 uint8_t scan_id;
83 } scan_done;
84 struct {
85 uint8_t mac[6];
86 int rssi;
87 } ap_probe_req;
88 } data;
89};
90
91// Connection state machine - only modified from main loop after queue processing
92enum class LTWiFiSTAState : uint8_t {
93 IDLE, // Not connecting
94 CONNECTING, // Connection in progress
95 CONNECTED, // Successfully connected with IP
96 ERROR_NOT_FOUND, // AP not found (probe failed)
97 ERROR_FAILED, // Connection failed (auth, timeout, etc.)
98};
99
100// Count of ignored disconnect events during connection - too many indicates real failure
101static uint8_t s_ignored_disconnect_count = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
102// Threshold for ignored disconnect events before treating as connection failure
103// LibreTiny sends spurious "Association Leave" events, but more than this many
104// indicates the connection is failing repeatedly. Value of 3 balances fast failure
105// detection with tolerance for occasional spurious events on successful connections.
106static constexpr uint8_t IGNORED_DISCONNECT_THRESHOLD = 3;
107
108bool WiFiComponent::wifi_mode_(optional<bool> sta, optional<bool> ap) {
109 uint8_t current_mode = WiFi.getMode();
110 bool current_sta = current_mode & 0b01;
111 bool current_ap = current_mode & 0b10;
112 bool enable_sta = sta.value_or(current_sta);
113 bool enable_ap = ap.value_or(current_ap);
114 if (current_sta == enable_sta && current_ap == enable_ap)
115 return true;
116
117 if (enable_sta && !current_sta) {
118 ESP_LOGV(TAG, "Enabling STA");
119 } else if (!enable_sta && current_sta) {
120 ESP_LOGV(TAG, "Disabling STA");
121 }
122 if (enable_ap && !current_ap) {
123 ESP_LOGV(TAG, "Enabling AP");
124 } else if (!enable_ap && current_ap) {
125 ESP_LOGV(TAG, "Disabling AP");
126 }
127
128 uint8_t mode = 0;
129 if (enable_sta)
130 mode |= 0b01;
131 if (enable_ap)
132 mode |= 0b10;
133 bool ret = WiFi.mode(static_cast<wifi_mode_t>(mode));
134
135 if (!ret) {
136 ESP_LOGW(TAG, "Setting mode failed");
137 return false;
138 }
139
140 this->ap_started_ = enable_ap;
141
142 return ret;
143}
144bool WiFiComponent::wifi_apply_output_power_(float output_power) {
145 int8_t val = static_cast<int8_t>(output_power * 4);
146 return WiFi.setTxPower(val);
147}
149 if (!this->wifi_mode_(true, {}))
150 return false;
151
152 WiFi.setAutoReconnect(false);
153 delay(10);
154 return true;
155}
157 bool success = WiFi.setSleep(this->power_save_ != WIFI_POWER_SAVE_NONE);
158#ifdef USE_WIFI_POWER_SAVE_LISTENERS
159 if (success) {
160 for (auto *listener : this->power_save_listeners_) {
161 listener->on_wifi_power_save(this->power_save_);
162 }
163 }
164#endif
165 return success;
166}
167bool WiFiComponent::wifi_sta_ip_config_(const optional<ManualIP> &manual_ip) {
168 // enable STA
169 if (!this->wifi_mode_(true, {}))
170 return false;
171
172 if (!manual_ip.has_value()) {
173 return true;
174 }
175
176 WiFi.config(manual_ip->static_ip, manual_ip->gateway, manual_ip->subnet, manual_ip->dns1, manual_ip->dns2);
177
178 return true;
179}
180
182 if (!this->has_sta())
183 return {};
184 network::IPAddresses addresses;
185 addresses[0] = WiFi.localIP();
186#if USE_NETWORK_IPV6
187 int i = 1;
188 auto v6_addresses = WiFi.allLocalIPv6();
189 for (auto address : v6_addresses) {
190 addresses[i++] = network::IPAddress(address.toString().c_str());
191 }
192#endif /* USE_NETWORK_IPV6 */
193 return addresses;
194}
195
197 // setting is done in SYSTEM_EVENT_STA_START callback too
198 WiFi.setHostname(App.get_name().c_str());
199 return true;
200}
201bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
202 // enable STA
203 if (!this->wifi_mode_(true, {}))
204 return false;
205
206 String ssid = WiFi.SSID();
207 if (ssid && strcmp(ssid.c_str(), ap.ssid_.c_str()) != 0) {
208 WiFi.disconnect();
209 }
210
211#ifdef USE_WIFI_MANUAL_IP
212 if (!this->wifi_sta_ip_config_(ap.get_manual_ip())) {
213 return false;
214 }
215#else
216 if (!this->wifi_sta_ip_config_({})) {
217 return false;
218 }
219#endif
220
221 this->wifi_apply_hostname_();
222
223 // Reset state machine and disconnect counter before connecting
224 this->sta_state_ = static_cast<uint8_t>(LTWiFiSTAState::CONNECTING);
225 s_ignored_disconnect_count = 0;
226
227 WiFiStatus status = WiFi.begin(ap.ssid_.c_str(), ap.password_.empty() ? NULL : ap.password_.c_str(),
228 ap.get_channel(), // 0 = auto
229 ap.has_bssid() ? ap.get_bssid().data() : NULL);
230 if (status != WL_CONNECTED) {
231 ESP_LOGW(TAG, "esp_wifi_connect failed: %d", status);
232 return false;
233 }
234
235 return true;
236}
237const char *get_auth_mode_str(uint8_t mode) {
238 switch (mode) {
239 case WIFI_AUTH_OPEN:
240 return "OPEN";
241 case WIFI_AUTH_WEP:
242 return "WEP";
243 case WIFI_AUTH_WPA_PSK:
244 return "WPA PSK";
245 case WIFI_AUTH_WPA2_PSK:
246 return "WPA2 PSK";
247 case WIFI_AUTH_WPA_WPA2_PSK:
248 return "WPA/WPA2 PSK";
249 default:
250 return "UNKNOWN";
251 }
252}
253
254const char *get_op_mode_str(uint8_t mode) {
255 switch (mode) {
256 case WIFI_OFF:
257 return "OFF";
258 case WIFI_STA:
259 return "STA";
260 case WIFI_AP:
261 return "AP";
262 case WIFI_AP_STA:
263 return "AP+STA";
264 default:
265 return "UNKNOWN";
266 }
267}
268const char *get_disconnect_reason_str(uint8_t reason) {
269 switch (reason) {
270 case WIFI_REASON_AUTH_EXPIRE:
271 return "Auth Expired";
272 case WIFI_REASON_AUTH_LEAVE:
273 return "Auth Leave";
274 case WIFI_REASON_ASSOC_EXPIRE:
275 return "Association Expired";
276 case WIFI_REASON_ASSOC_TOOMANY:
277 return "Too Many Associations";
278 case WIFI_REASON_NOT_AUTHED:
279 return "Not Authenticated";
280 case WIFI_REASON_NOT_ASSOCED:
281 return "Not Associated";
282 case WIFI_REASON_ASSOC_LEAVE:
283 return "Association Leave";
284 case WIFI_REASON_ASSOC_NOT_AUTHED:
285 return "Association not Authenticated";
286 case WIFI_REASON_DISASSOC_PWRCAP_BAD:
287 return "Disassociate Power Cap Bad";
288 case WIFI_REASON_DISASSOC_SUPCHAN_BAD:
289 return "Disassociate Supported Channel Bad";
290 case WIFI_REASON_IE_INVALID:
291 return "IE Invalid";
292 case WIFI_REASON_MIC_FAILURE:
293 return "Mic Failure";
294 case WIFI_REASON_4WAY_HANDSHAKE_TIMEOUT:
295 return "4-Way Handshake Timeout";
296 case WIFI_REASON_GROUP_KEY_UPDATE_TIMEOUT:
297 return "Group Key Update Timeout";
298 case WIFI_REASON_IE_IN_4WAY_DIFFERS:
299 return "IE In 4-Way Handshake Differs";
300 case WIFI_REASON_GROUP_CIPHER_INVALID:
301 return "Group Cipher Invalid";
302 case WIFI_REASON_PAIRWISE_CIPHER_INVALID:
303 return "Pairwise Cipher Invalid";
304 case WIFI_REASON_AKMP_INVALID:
305 return "AKMP Invalid";
306 case WIFI_REASON_UNSUPP_RSN_IE_VERSION:
307 return "Unsupported RSN IE version";
308 case WIFI_REASON_INVALID_RSN_IE_CAP:
309 return "Invalid RSN IE Cap";
310 case WIFI_REASON_802_1X_AUTH_FAILED:
311 return "802.1x Authentication Failed";
312 case WIFI_REASON_CIPHER_SUITE_REJECTED:
313 return "Cipher Suite Rejected";
314 case WIFI_REASON_BEACON_TIMEOUT:
315 return "Beacon Timeout";
316 case WIFI_REASON_NO_AP_FOUND:
317 return "AP Not Found";
318 case WIFI_REASON_AUTH_FAIL:
319 return "Authentication Failed";
320 case WIFI_REASON_ASSOC_FAIL:
321 return "Association Failed";
322 case WIFI_REASON_HANDSHAKE_TIMEOUT:
323 return "Handshake Failed";
324 case WIFI_REASON_CONNECTION_FAIL:
325 return "Connection Failed";
326 case WIFI_REASON_UNSPECIFIED:
327 default:
328 return "Unspecified";
329 }
330}
331
332#define ESPHOME_EVENT_ID_WIFI_READY ARDUINO_EVENT_WIFI_READY
333#define ESPHOME_EVENT_ID_WIFI_SCAN_DONE ARDUINO_EVENT_WIFI_SCAN_DONE
334#define ESPHOME_EVENT_ID_WIFI_STA_START ARDUINO_EVENT_WIFI_STA_START
335#define ESPHOME_EVENT_ID_WIFI_STA_STOP ARDUINO_EVENT_WIFI_STA_STOP
336#define ESPHOME_EVENT_ID_WIFI_STA_CONNECTED ARDUINO_EVENT_WIFI_STA_CONNECTED
337#define ESPHOME_EVENT_ID_WIFI_STA_DISCONNECTED ARDUINO_EVENT_WIFI_STA_DISCONNECTED
338#define ESPHOME_EVENT_ID_WIFI_STA_AUTHMODE_CHANGE ARDUINO_EVENT_WIFI_STA_AUTHMODE_CHANGE
339#define ESPHOME_EVENT_ID_WIFI_STA_GOT_IP ARDUINO_EVENT_WIFI_STA_GOT_IP
340#define ESPHOME_EVENT_ID_WIFI_STA_GOT_IP6 ARDUINO_EVENT_WIFI_STA_GOT_IP6
341#define ESPHOME_EVENT_ID_WIFI_STA_LOST_IP ARDUINO_EVENT_WIFI_STA_LOST_IP
342#define ESPHOME_EVENT_ID_WIFI_AP_START ARDUINO_EVENT_WIFI_AP_START
343#define ESPHOME_EVENT_ID_WIFI_AP_STOP ARDUINO_EVENT_WIFI_AP_STOP
344#define ESPHOME_EVENT_ID_WIFI_AP_STACONNECTED ARDUINO_EVENT_WIFI_AP_STACONNECTED
345#define ESPHOME_EVENT_ID_WIFI_AP_STADISCONNECTED ARDUINO_EVENT_WIFI_AP_STADISCONNECTED
346#define ESPHOME_EVENT_ID_WIFI_AP_STAIPASSIGNED ARDUINO_EVENT_WIFI_AP_STAIPASSIGNED
347#define ESPHOME_EVENT_ID_WIFI_AP_PROBEREQRECVED ARDUINO_EVENT_WIFI_AP_PROBEREQRECVED
348#define ESPHOME_EVENT_ID_WIFI_AP_GOT_IP6 ARDUINO_EVENT_WIFI_AP_GOT_IP6
349using esphome_wifi_event_id_t = arduino_event_id_t;
350using esphome_wifi_event_info_t = arduino_event_info_t;
351
352// Event callback - runs in WiFi driver thread context
353// Only queues events for processing in main loop, no logging or state changes here
355 if (s_event_queue == nullptr) {
356 return;
357 }
358
359 // Allocate on heap and fill directly to avoid extra memcpy
360 auto *to_send = new LTWiFiEvent{}; // NOLINT(cppcoreguidelines-owning-memory)
361 to_send->event_id = event;
362
363 // Copy event-specific data
364 switch (event) {
365 case ESPHOME_EVENT_ID_WIFI_STA_CONNECTED: {
366 auto &it = info.wifi_sta_connected;
367 to_send->data.sta_connected.ssid_len = it.ssid_len;
368 memcpy(to_send->data.sta_connected.ssid, it.ssid,
369 std::min(static_cast<size_t>(it.ssid_len), sizeof(to_send->data.sta_connected.ssid) - 1));
370 memcpy(to_send->data.sta_connected.bssid, it.bssid, 6);
371 to_send->data.sta_connected.channel = it.channel;
372 to_send->data.sta_connected.authmode = it.authmode;
373 break;
374 }
375 case ESPHOME_EVENT_ID_WIFI_STA_DISCONNECTED: {
376 auto &it = info.wifi_sta_disconnected;
377 to_send->data.sta_disconnected.ssid_len = it.ssid_len;
378 memcpy(to_send->data.sta_disconnected.ssid, it.ssid,
379 std::min(static_cast<size_t>(it.ssid_len), sizeof(to_send->data.sta_disconnected.ssid) - 1));
380 memcpy(to_send->data.sta_disconnected.bssid, it.bssid, 6);
381 to_send->data.sta_disconnected.reason = it.reason;
382 break;
383 }
384 case ESPHOME_EVENT_ID_WIFI_STA_AUTHMODE_CHANGE: {
385 auto &it = info.wifi_sta_authmode_change;
386 to_send->data.sta_authmode_change.old_mode = it.old_mode;
387 to_send->data.sta_authmode_change.new_mode = it.new_mode;
388 break;
389 }
390 case ESPHOME_EVENT_ID_WIFI_SCAN_DONE: {
391 auto &it = info.wifi_scan_done;
392 to_send->data.scan_done.status = it.status;
393 to_send->data.scan_done.number = it.number;
394 to_send->data.scan_done.scan_id = it.scan_id;
395 break;
396 }
397 case ESPHOME_EVENT_ID_WIFI_AP_PROBEREQRECVED: {
398 auto &it = info.wifi_ap_probereqrecved;
399 memcpy(to_send->data.ap_probe_req.mac, it.mac, 6);
400 to_send->data.ap_probe_req.rssi = it.rssi;
401 break;
402 }
403 case ESPHOME_EVENT_ID_WIFI_AP_STACONNECTED: {
404 auto &it = info.wifi_sta_connected;
405 memcpy(to_send->data.sta_connected.bssid, it.bssid, 6);
406 break;
407 }
408 case ESPHOME_EVENT_ID_WIFI_AP_STADISCONNECTED: {
409 auto &it = info.wifi_sta_disconnected;
410 memcpy(to_send->data.sta_disconnected.bssid, it.bssid, 6);
411 break;
412 }
413 case ESPHOME_EVENT_ID_WIFI_READY:
414 case ESPHOME_EVENT_ID_WIFI_STA_START:
415 case ESPHOME_EVENT_ID_WIFI_STA_STOP:
416 case ESPHOME_EVENT_ID_WIFI_STA_GOT_IP:
417 case ESPHOME_EVENT_ID_WIFI_STA_GOT_IP6:
418 case ESPHOME_EVENT_ID_WIFI_STA_LOST_IP:
419 case ESPHOME_EVENT_ID_WIFI_AP_START:
420 case ESPHOME_EVENT_ID_WIFI_AP_STOP:
421 case ESPHOME_EVENT_ID_WIFI_AP_STAIPASSIGNED:
422 // No additional data needed
423 break;
424 default:
425 // Unknown event, don't queue
426 delete to_send; // NOLINT(cppcoreguidelines-owning-memory)
427 return;
428 }
429
430 // Queue event (don't block if queue is full)
431 if (xQueueSend(s_event_queue, &to_send, 0) != pdPASS) {
432 delete to_send; // NOLINT(cppcoreguidelines-owning-memory)
433 s_event_queue_overflow_count++;
434 }
435}
436
437// Process a single event from the queue - runs in main loop context.
438// Listener notifications must be deferred until after the state machine transitions
439// (in check_connecting_finished) so that conditions like wifi.connected return
440// correct values in automations.
441void WiFiComponent::wifi_process_event_(LTWiFiEvent *event) {
442 switch (event->event_id) {
443 case ESPHOME_EVENT_ID_WIFI_READY: {
444 ESP_LOGV(TAG, "Ready");
445 break;
446 }
447 case ESPHOME_EVENT_ID_WIFI_SCAN_DONE: {
448 auto &it = event->data.scan_done;
449 ESP_LOGV(TAG, "Scan done: status=%" PRIu32 " number=%u scan_id=%u", it.status, it.number, it.scan_id);
451 break;
452 }
453 case ESPHOME_EVENT_ID_WIFI_STA_START: {
454 ESP_LOGV(TAG, "STA start");
455 WiFi.setHostname(App.get_name().c_str());
456 break;
457 }
458 case ESPHOME_EVENT_ID_WIFI_STA_STOP: {
459 ESP_LOGV(TAG, "STA stop");
460 this->sta_state_ = static_cast<uint8_t>(LTWiFiSTAState::IDLE);
461 break;
462 }
463 case ESPHOME_EVENT_ID_WIFI_STA_CONNECTED: {
464 auto &it = event->data.sta_connected;
465 char bssid_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
466 format_mac_addr_upper(it.bssid, bssid_buf);
467 ESP_LOGV(TAG, "Connected ssid='%.*s' bssid=" LOG_SECRET("%s") " channel=%u, authmode=%s", it.ssid_len,
468 (const char *) it.ssid, bssid_buf, it.channel, get_auth_mode_str(it.authmode));
469 // Note: We don't set CONNECTED state here yet - wait for GOT_IP
470 // This matches ESP32 IDF behavior where s_sta_connected is set but
471 // wifi_sta_connect_status_() also checks got_ipv4_address_
472#ifdef USE_WIFI_CONNECT_STATE_LISTENERS
473 // Defer listener notification until state machine reaches STA_CONNECTED
474 // This ensures wifi.connected condition returns true in listener automations
475 this->pending_.connect_state = true;
476#endif
477 // For static IP configurations, GOT_IP event may not fire, so set connected state here
478#ifdef USE_WIFI_MANUAL_IP
479 if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_manual_ip().has_value()) {
480 this->sta_state_ = static_cast<uint8_t>(LTWiFiSTAState::CONNECTED);
481#ifdef USE_WIFI_IP_STATE_LISTENERS
483#endif
484 }
485#endif
486 break;
487 }
488 case ESPHOME_EVENT_ID_WIFI_STA_DISCONNECTED: {
489 auto &it = event->data.sta_disconnected;
490
491 // LibreTiny can send spurious disconnect events with empty ssid/bssid during connection.
492 // These are typically "Association Leave" events that don't indicate actual failures:
493 // [W][wifi_lt]: Disconnected ssid='' bssid=00:00:00:00:00:00 reason='Association Leave'
494 // [W][wifi_lt]: Disconnected ssid='' bssid=00:00:00:00:00:00 reason='Association Leave'
495 // [V][wifi_lt]: Connected ssid='WIFI' bssid=... channel=3, authmode=WPA2 PSK
496 // Without this check, the spurious events would transition state to ERROR_FAILED,
497 // causing wifi_sta_connect_status_() to return an error. The main loop would then
498 // call retry_connect(), aborting a connection that may succeed moments later.
499 // Only ignore benign reasons - real failures like NO_AP_FOUND should still be processed.
500 // However, if we get too many of these events (IGNORED_DISCONNECT_THRESHOLD), treat it
501 // as a real connection failure to avoid waiting the full timeout for a failing connection.
502 if (it.ssid_len == 0 && this->sta_state_ == static_cast<uint8_t>(LTWiFiSTAState::CONNECTING) &&
503 it.reason != WIFI_REASON_NO_AP_FOUND) {
504 s_ignored_disconnect_count++;
505 if (s_ignored_disconnect_count >= IGNORED_DISCONNECT_THRESHOLD) {
506 ESP_LOGW(TAG, "Too many disconnect events (%u) while connecting, treating as failure (reason=%s)",
507 s_ignored_disconnect_count, get_disconnect_reason_str(it.reason));
508 this->sta_state_ = static_cast<uint8_t>(LTWiFiSTAState::ERROR_FAILED);
509 WiFi.disconnect();
510 this->error_from_callback_ = true;
511 // Don't break - fall through to notify listeners
512 } else {
513 ESP_LOGV(TAG, "Ignoring disconnect event with empty ssid while connecting (reason=%s, count=%u)",
514 get_disconnect_reason_str(it.reason), s_ignored_disconnect_count);
515 break;
516 }
517 }
518
519 if (it.reason == WIFI_REASON_NO_AP_FOUND) {
520 ESP_LOGW(TAG, "Disconnected ssid='%.*s' reason='Probe Request Unsuccessful'", it.ssid_len,
521 (const char *) it.ssid);
522 this->sta_state_ = static_cast<uint8_t>(LTWiFiSTAState::ERROR_NOT_FOUND);
523 } else {
524 char bssid_s[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
525 format_mac_addr_upper(it.bssid, bssid_s);
526 ESP_LOGW(TAG, "Disconnected ssid='%.*s' bssid=" LOG_SECRET("%s") " reason='%s'", it.ssid_len,
527 (const char *) it.ssid, bssid_s, get_disconnect_reason_str(it.reason));
528 this->sta_state_ = static_cast<uint8_t>(LTWiFiSTAState::ERROR_FAILED);
529 }
530
531 uint8_t reason = it.reason;
532 if (reason == WIFI_REASON_AUTH_EXPIRE || reason == WIFI_REASON_BEACON_TIMEOUT ||
533 reason == WIFI_REASON_NO_AP_FOUND || reason == WIFI_REASON_ASSOC_FAIL ||
534 reason == WIFI_REASON_HANDSHAKE_TIMEOUT) {
535 WiFi.disconnect();
536 this->error_from_callback_ = true;
537 }
538
539#ifdef USE_WIFI_CONNECT_STATE_LISTENERS
541#endif
542 break;
543 }
544 case ESPHOME_EVENT_ID_WIFI_STA_AUTHMODE_CHANGE: {
545 auto &it = event->data.sta_authmode_change;
546 ESP_LOGV(TAG, "Authmode Change old=%s new=%s", get_auth_mode_str(it.old_mode), get_auth_mode_str(it.new_mode));
547 // Mitigate CVE-2020-12638
548 // https://lbsfilm.at/blog/wpa2-authenticationmode-downgrade-in-espressif-microprocessors
549 if (it.old_mode != WIFI_AUTH_OPEN && it.new_mode == WIFI_AUTH_OPEN) {
550 ESP_LOGW(TAG, "Potential Authmode downgrade detected, disconnecting");
551 WiFi.disconnect();
552 this->error_from_callback_ = true;
553 this->sta_state_ = static_cast<uint8_t>(LTWiFiSTAState::ERROR_FAILED);
554 }
555 break;
556 }
557 case ESPHOME_EVENT_ID_WIFI_STA_GOT_IP: {
558 char ip_buf[network::IP_ADDRESS_BUFFER_SIZE], gw_buf[network::IP_ADDRESS_BUFFER_SIZE];
559 ESP_LOGV(TAG, "static_ip=%s gateway=%s", network::IPAddress(WiFi.localIP()).str_to(ip_buf),
560 network::IPAddress(WiFi.gatewayIP()).str_to(gw_buf));
561 this->sta_state_ = static_cast<uint8_t>(LTWiFiSTAState::CONNECTED);
562#ifdef USE_WIFI_IP_STATE_LISTENERS
564#endif
565 break;
566 }
567 case ESPHOME_EVENT_ID_WIFI_STA_GOT_IP6: {
568 ESP_LOGV(TAG, "Got IPv6");
569#ifdef USE_WIFI_IP_STATE_LISTENERS
571#endif
572 break;
573 }
574 case ESPHOME_EVENT_ID_WIFI_STA_LOST_IP: {
575 ESP_LOGV(TAG, "Lost IP");
576 // Don't change state to IDLE - let the disconnect event handle that
577 break;
578 }
579 case ESPHOME_EVENT_ID_WIFI_AP_START: {
580 ESP_LOGV(TAG, "AP start");
581 break;
582 }
583 case ESPHOME_EVENT_ID_WIFI_AP_STOP: {
584 ESP_LOGV(TAG, "AP stop");
585 break;
586 }
587 case ESPHOME_EVENT_ID_WIFI_AP_STACONNECTED: {
588#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
589 auto &it = event->data.sta_connected;
590 char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
591 format_mac_addr_upper(it.bssid, mac_buf);
592 ESP_LOGV(TAG, "AP client connected MAC=%s", mac_buf);
593#endif
594 break;
595 }
596 case ESPHOME_EVENT_ID_WIFI_AP_STADISCONNECTED: {
597#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
598 auto &it = event->data.sta_disconnected;
599 char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
600 format_mac_addr_upper(it.bssid, mac_buf);
601 ESP_LOGV(TAG, "AP client disconnected MAC=%s", mac_buf);
602#endif
603 break;
604 }
605 case ESPHOME_EVENT_ID_WIFI_AP_STAIPASSIGNED: {
606 ESP_LOGV(TAG, "AP client assigned IP");
607 break;
608 }
609 case ESPHOME_EVENT_ID_WIFI_AP_PROBEREQRECVED: {
610#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
611 auto &it = event->data.ap_probe_req;
612 char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
613 format_mac_addr_upper(it.mac, mac_buf);
614 ESP_LOGVV(TAG, "AP receive Probe Request MAC=%s RSSI=%d", mac_buf, it.rssi);
615#endif
616 break;
617 }
618 default:
619 break;
620 }
621}
623 // Create event queue for thread-safe event handling
624 // Events are pushed from WiFi callback thread and processed in main loop
625 s_event_queue = xQueueCreate(EVENT_QUEUE_SIZE, sizeof(LTWiFiEvent *));
626 if (s_event_queue == nullptr) {
627 ESP_LOGE(TAG, "Failed to create event queue");
628 return;
629 }
630
631 WiFi.onEvent(
632 [this](arduino_event_id_t event, arduino_event_info_t info) { this->wifi_event_callback_(event, info); });
633 // Make sure WiFi is in clean state before anything starts
634 this->wifi_mode_(false, false);
635}
637 // Use state machine instead of querying WiFi.status() directly
638 // State is updated in main loop from queued events, ensuring thread safety
639 switch (static_cast<LTWiFiSTAState>(this->sta_state_)) {
649 default:
651 }
652}
653bool WiFiComponent::wifi_scan_start_(bool passive) {
654 // enable STA
655 if (!this->wifi_mode_(true, {}))
656 return false;
657
658 // Reset scan_done_ before starting new scan to prevent stale flag from previous scan
659 // (e.g., roaming scan completed just before unexpected disconnect)
660 this->scan_done_ = false;
661
662 // need to use WiFi because of WiFiScanClass allocations :(
663 int16_t err = WiFi.scanNetworks(true, true, passive, 200);
664 if (err != WIFI_SCAN_RUNNING) {
665 ESP_LOGV(TAG, "WiFi.scanNetworks failed: %d", err);
666 return false;
667 }
668
669 return true;
670}
672 this->scan_result_.clear();
673 this->scan_done_ = true;
674
675 int16_t num = WiFi.scanComplete();
676 if (num < 0)
677 return;
678
679 bool needs_full = this->needs_full_scan_results_();
680
681 // Access scan results directly via WiFi.scan struct to avoid Arduino String allocations
682 // WiFi.scan is public in LibreTiny for WiFiEvents & WiFiScan static handlers
683 auto *scan = WiFi.scan;
684
685 // First pass: count matching networks
686 size_t count = 0;
687 for (int i = 0; i < num; i++) {
688 const char *ssid_cstr = scan->ap[i].ssid;
689 if (needs_full || this->matches_configured_network_(ssid_cstr, scan->ap[i].bssid.addr)) {
690 count++;
691 }
692 }
693
694 this->scan_result_.init(count); // Exact allocation
695
696 // Second pass: store matching networks
697 for (int i = 0; i < num; i++) {
698 const char *ssid_cstr = scan->ap[i].ssid;
699 if (needs_full || this->matches_configured_network_(ssid_cstr, scan->ap[i].bssid.addr)) {
700 auto &ap = scan->ap[i];
701 this->scan_result_.emplace_back(bssid_t{ap.bssid.addr[0], ap.bssid.addr[1], ap.bssid.addr[2], ap.bssid.addr[3],
702 ap.bssid.addr[4], ap.bssid.addr[5]},
703 ssid_cstr, strlen(ssid_cstr), ap.channel, ap.rssi, ap.auth != WIFI_AUTH_OPEN,
704 ssid_cstr[0] == '\0');
705 } else {
706 auto &ap = scan->ap[i];
707 this->log_discarded_scan_result_(ssid_cstr, ap.bssid.addr, ap.rssi, ap.channel);
708 }
709 }
710 ESP_LOGV(TAG, "Scan complete: %d found, %zu stored%s", num, this->scan_result_.size(),
711 needs_full ? "" : " (filtered)");
712 WiFi.scanDelete();
713#ifdef USE_WIFI_SCAN_RESULTS_LISTENERS
715#endif
716}
717
718#ifdef USE_WIFI_AP
719bool WiFiComponent::wifi_ap_ip_config_(const optional<ManualIP> &manual_ip) {
720 // enable AP
721 if (!this->wifi_mode_({}, true))
722 return false;
723
724 if (manual_ip.has_value()) {
725 return WiFi.softAPConfig(manual_ip->static_ip, manual_ip->gateway, manual_ip->subnet);
726 } else {
727 return WiFi.softAPConfig(IPAddress(192, 168, 4, 1), IPAddress(192, 168, 4, 1), IPAddress(255, 255, 255, 0));
728 }
729}
730
731bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
732 // enable AP
733 if (!this->wifi_mode_({}, true))
734 return false;
735
736#ifdef USE_WIFI_MANUAL_IP
737 if (!this->wifi_ap_ip_config_(ap.get_manual_ip())) {
738 ESP_LOGV(TAG, "wifi_ap_ip_config_ failed");
739 return false;
740 }
741#else
742 if (!this->wifi_ap_ip_config_({})) {
743 ESP_LOGV(TAG, "wifi_ap_ip_config_ failed");
744 return false;
745 }
746#endif
747
748 yield();
749
750 return WiFi.softAP(ap.ssid_.c_str(), ap.password_.empty() ? NULL : ap.password_.c_str(),
751 ap.has_channel() ? ap.get_channel() : 1, ap.get_hidden());
752}
753
754network::IPAddress WiFiComponent::wifi_soft_ap_ip() { return {WiFi.softAPIP()}; }
755#endif // USE_WIFI_AP
756
758 // Reset state first so disconnect events aren't ignored
759 // and wifi_sta_connect_status_() returns IDLE instead of CONNECTING
760 this->sta_state_ = static_cast<uint8_t>(LTWiFiSTAState::IDLE);
761 return WiFi.disconnect();
762}
763
765 bssid_t bssid{};
766 uint8_t *raw_bssid = WiFi.BSSID();
767 if (raw_bssid != nullptr) {
768 for (size_t i = 0; i < bssid.size(); i++)
769 bssid[i] = raw_bssid[i];
770 }
771 return bssid;
772}
773std::string WiFiComponent::wifi_ssid() { return WiFi.SSID().c_str(); }
774const char *WiFiComponent::wifi_ssid_to(std::span<char, SSID_BUFFER_SIZE> buffer) {
775#ifdef USE_BK72XX
776 LinkStatusTypeDef link_status{};
777 bk_wlan_get_link_status(&link_status);
778 size_t len = strnlen(reinterpret_cast<const char *>(link_status.ssid), SSID_BUFFER_SIZE - 1);
779 memcpy(buffer.data(), link_status.ssid, len);
780#elif defined(USE_RTL87XX)
781 rtw_wifi_setting_t setting{};
782 wifi_get_setting("wlan0", &setting);
783 size_t len = strnlen(reinterpret_cast<const char *>(setting.ssid), SSID_BUFFER_SIZE - 1);
784 memcpy(buffer.data(), setting.ssid, len);
785#else
786 // LN882X: wifi_get_sta_conn_info() provides direct pointer access
787 String ssid = WiFi.SSID();
788 size_t len = std::min(static_cast<size_t>(ssid.length()), SSID_BUFFER_SIZE - 1);
789 memcpy(buffer.data(), ssid.c_str(), len);
790#endif
791 buffer[len] = '\0';
792 return buffer.data();
793}
794int8_t WiFiComponent::wifi_rssi() { return WiFi.status() == WL_CONNECTED ? WiFi.RSSI() : WIFI_RSSI_DISCONNECTED; }
795int32_t WiFiComponent::get_wifi_channel() { return WiFi.channel(); }
796network::IPAddress WiFiComponent::wifi_subnet_mask_() { return {WiFi.subnetMask()}; }
797network::IPAddress WiFiComponent::wifi_gateway_ip_() { return {WiFi.gatewayIP()}; }
798network::IPAddress WiFiComponent::wifi_dns_ip_(int num) { return {WiFi.dnsIP(num)}; }
800 // Process all pending events from the queue
801 if (s_event_queue == nullptr) {
802 return;
803 }
804
805 // Check for dropped events due to queue overflow
806 if (s_event_queue_overflow_count > 0) {
807 ESP_LOGW(TAG, "Event queue overflow, %" PRIu32 " events dropped", s_event_queue_overflow_count);
808 s_event_queue_overflow_count = 0;
809 }
810
811 while (true) {
812 LTWiFiEvent *event;
813 if (xQueueReceive(s_event_queue, &event, 0) != pdTRUE) {
814 // No more events
815 break;
816 }
817
818 wifi_process_event_(event);
819 delete event; // NOLINT(cppcoreguidelines-owning-memory)
820 }
821}
822
823} // namespace esphome::wifi
824#endif // USE_LIBRETINY
825#endif
BedjetMode mode
BedJet operating mode.
uint8_t address
Definition bl0906.h:4
uint8_t status
Definition bl0942.h:8
const StringRef & get_name() const
Get the name of this Application set by pre_setup().
constexpr const char * c_str() const
Definition string_ref.h:73
const optional< ManualIP > & get_manual_ip() const
void notify_scan_results_listeners_()
Notify scan results listeners with current scan results.
const WiFiAP * get_selected_sta_() const
WiFiSTAConnectStatus wifi_sta_connect_status_() const
wifi_scan_vector_t< WiFiScanResult > scan_result_
struct esphome::wifi::WiFiComponent::@190 pending_
void notify_ip_state_listeners_()
Notify IP state listeners with current addresses.
bool wifi_sta_ip_config_(const optional< ManualIP > &manual_ip)
void wifi_process_event_(IDFWiFiEvent *data)
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)
ESPDEPRECATED("Use wifi_ssid_to() instead. Removed in 2026.9.0", "2026.3.0") std const char * wifi_ssid_to(std::span< char, SSID_BUFFER_SIZE > buffer)
Write SSID to buffer without heap allocation.
void wifi_event_callback_(arduino_event_id_t event, arduino_event_info_t info)
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()
mopeka_std_values val[3]
std::array< IPAddress, 5 > IPAddresses
Definition ip_address.h:187
const char *const TAG
Definition spi.cpp:7
std::array< uint8_t, 6 > bssid_t
const LogString * get_auth_mode_str(uint8_t mode)
arduino_event_info_t esphome_wifi_event_info_t
const LogString * get_disconnect_reason_str(uint8_t reason)
arduino_event_id_t esphome_wifi_event_id_t
const LogString * get_op_mode_str(uint8_t mode)
std::string size_t len
Definition helpers.h:1045
void HOT yield()
Definition core.cpp:25
void HOT delay(uint32_t ms)
Definition core.cpp:28
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:1435
static void uint32_t