Mastering the ESP32 Wi-Fi State Machine

When building robust IoT devices with the ESP32 family—including the ESP32-S3 and the Wi-Fi 6-capable ESP32-C6—relying on blocking connection loops is a recipe for failure. Network environments are inherently unstable; access points reboot, signal interference spikes, and DHCP leases expire. To build production-grade firmware, you must transition from synchronous polling to asynchronous event handling. This is where the Arduino WiFiEvent_t architecture becomes critical.

The Wi-Fi event system acts as a bridge between the underlying FreeRTOS ESP-IDF Wi-Fi driver and the Arduino wrapper. By registering callbacks, your microcontroller can react instantly to network state changes—such as obtaining an IP address or detecting a beacon timeout—without stalling the main loop() execution.

The API Shift: WiFiEvent_t vs arduino_event_id_t

If you are migrating legacy code or reading outdated 2021 tutorials, you will frequently encounter the WiFiEvent_t enum. However, with the stabilization of the ESP32 Arduino Core v3.x (the standard for 2025 and 2026 development), the public API has evolved. The legacy WiFiEvent_t and system_event_id_t structures have been unified under the arduino_event_id_t enum to better align with the ESP-IDF v5.x event loop architecture. While the community still colloquially searches for 'Arduino WiFiEvent_t', modern configuration requires using the updated ARDUINO_EVENT_* macros.

For authoritative details on this transition, refer to the Arduino ESP32 Core Wi-Fi Documentation and the underlying Espressif ESP-IDF Wi-Fi API Guide.

Core Wi-Fi Events Configuration Matrix

Not all events require custom logic. Below is a prioritized matrix of the most critical events you should configure in your sketch to maintain a resilient network connection.

Modern Event Macro (Core v3.x) Legacy WiFiEvent_t Equivalent Trigger Condition Recommended Action
ARDUINO_EVENT_WIFI_STA_START SYSTEM_EVENT_STA_START Wi-Fi station interface initialized. Call WiFi.begin() or initiate SmartConfig/WPS.
ARDUINO_EVENT_WIFI_STA_CONNECTED SYSTEM_EVENT_STA_CONNECTED Layer 2 association successful. Log connection, update local UI/LED. Do NOT assume internet access yet.
ARDUINO_EVENT_WIFI_STA_GOT_IP SYSTEM_EVENT_STA_GOT_IP DHCP lease acquired or static IP assigned. Start MQTT clients, HTTP servers, and NTP sync.
ARDUINO_EVENT_WIFI_STA_DISCONNECTED SYSTEM_EVENT_STA_DISCONNECTED Layer 2 link lost or deauthenticated. Parse disconnect reason, set reconnect flags, tear down sockets.
ARDUINO_EVENT_SC_SCAN_DONE SYSTEM_EVENT_SC_SCAN_DONE SmartConfig network scan completed. Verify scan status before processing SmartConfig payloads.

Step-by-Step Callback Implementation

To configure the event listener, use the WiFi.onEvent() method. You can either register a single generic handler that uses a switch statement, or bind specific functions to individual events. The latter is cleaner and reduces overhead.

#include <WiFi.h>

// Forward declarations
void onWifiGotIP(arduino_event_id_t event, arduino_event_info_t info);
void onWifiDisconnect(arduino_event_id_t event, arduino_event_info_t info);

// Global state flags (volatile for ISR/Task safety)
volatile bool wifi_connected = false;
volatile uint8_t wifi_disconnect_reason = 0;

void setup() {
  Serial.begin(115200);
  
  // Register specific event callbacks
  WiFi.onEvent(onWifiGotIP, ARDUINO_EVENT_WIFI_STA_GOT_IP);
  WiFi.onEvent(onWifiDisconnect, ARDUINO_EVENT_WIFI_STA_DISCONNECTED);

  // Disable auto-reconnect to handle it manually via state machine
  WiFi.setAutoReconnect(false);
  
  WiFi.begin("YourSSID", "YourPassword");
}

void loop() {
  // Handle reconnect logic in the main loop, NOT in the callback
  if (!wifi_connected && wifi_disconnect_reason != 0) {
    handleReconnectLogic();
  }
  // ... rest of application logic ...
}

void onWifiGotIP(arduino_event_id_t event, arduino_event_info_t info) {
  wifi_connected = true;
  wifi_disconnect_reason = 0;
  Serial.printf("Connected! IP: %s\n", WiFi.localIP().toString().c_str());
}

void onWifiDisconnect(arduino_event_id_t event, arduino_event_info_t info) {
  wifi_connected = false;
  wifi_disconnect_reason = info.wifi_sta_disconnected.reason;
  Serial.printf("Disconnected. Reason: %d\n", wifi_disconnect_reason);
}

Decoding Disconnect Reasons (wifi_err_reason_t)

The most common failure mode in ESP32 IoT deployments is the infinite reconnect loop caused by ignoring why the device disconnected. The arduino_event_info_t struct contains a wifi_sta_disconnected object, which holds the exact 802.11 or internal error code. According to the ESP-IDF Network API Reference, these are the critical codes you must handle:

  • Reason 2 (WIFI_REASON_AUTH_EXPIRE): Authentication expired. Usually transient; a simple retry works.
  • Reason 4 (WIFI_REASON_ASSOC_LEAVE): The AP actively deauthenticated the ESP32. Often caused by MAC filtering or AP client limits.
  • Reason 15 (WIFI_REASON_4WAY_HANDSHAKE_TIMEOUT): The WPA2 4-way handshake failed. Action: Do NOT blindly retry. This almost always means the password in your firmware is incorrect or the AP's security protocol (e.g., WPA3 vs WPA2) is mismatched.
  • Reason 201 (WIFI_REASON_NO_AP_FOUND): The SSID is out of range or the router is offline. Action: Implement exponential backoff (e.g., wait 5s, then 15s, then 60s) to prevent spamming the RF spectrum and draining battery on LiPo-powered nodes.

RTOS Execution Context: Avoiding the Sys_Evt Trap

Expert Warning: Wi-Fi event callbacks execute in the context of the ESP-IDF sys_evt (system event) task, not your main Arduino loop(). This task has a severely limited stack size (typically 2KB to 4KB) and handles critical RTOS background operations.

A catastrophic mistake made by intermediate developers is calling heavy functions directly inside the WiFiEvent_t callback. Calling WiFi.reconnect(), initiating an HTTPS request, or writing to an SD card inside the callback will cause a stack overflow, triggering a Task Watchdog Timer (TWDT) panic and rebooting the ESP32.

The Solution: The Flag-and-Poll Pattern.
As demonstrated in the code block above, your callback should only do three things:

  1. Update a volatile boolean or integer flag.
  2. Extract primitive data from the arduino_event_info_t struct (like the disconnect reason code).
  3. Return immediately.

Your main loop() then polls these flags and executes the heavy lifting (reconnection, DNS resolution, MQTT re-initialization) in the safety of the main task's 8KB+ stack.

Frequently Asked Questions

Can I use IRAM_ATTR on Wi-Fi event callbacks?

No. IRAM_ATTR is strictly for hardware Interrupt Service Routines (ISRs) like GPIO pin changes or hardware timers. Wi-Fi events are software messages passed via FreeRTOS queues to the event task. Adding IRAM_ATTR to a WiFi.onEvent callback will not improve performance and may cause compilation faults if the callback references non-IRAM-safe flash strings or functions.

How do I capture the IP address directly from the event struct?

In Core v3.x, the arduino_event_info_t parameter passed to the GOT_IP event contains the raw IP data. You can extract it without calling WiFi.localIP() like this:
IPAddress ip(info.got_ip.ip_info.ip.addr);
This is slightly faster and prevents race conditions if the network interface state changes between the event trigger and your function execution.

Does Wi-Fi 6 (802.11ax) on the ESP32-C6 change the event structure?

The fundamental arduino_event_id_t macros remain identical for backward compatibility. However, the ESP32-C6 introduces new Target Wake Time (TWT) and OFDMA-specific internal states. While these are handled at the ESP-IDF driver layer, the Arduino abstraction layer maps the core connection and disconnection events to the same standard macros used on the classic ESP32 and ESP32-S3.