Chapter 30: Handling WiFi Connection Events

Chapter Objectives

By the end of this chapter, you will be able to:

  • Describe the core components of the ESP-IDF event loop library (esp_event).
  • Differentiate between event bases, event IDs, and event data.
  • Register and unregister event handlers dynamically using different API functions.
  • Pass custom context data to event handlers.
  • Implement multiple event handlers for different purposes.
  • Understand the lifecycle of event handlers and the importance of cleanup.
  • Apply best practices for writing efficient and safe event handler functions.

Introduction

In the preceding chapters, we utilized a single event handler function to react to the primary WiFi station connection events (STA_START, STA_CONNECTED, STA_DISCONNECTED, GOT_IP). This function, registered with the default event loop, served as our central point for managing the connection state and initiating actions like esp_wifi_connect() or retries.

While effective for basic scenarios, real-world applications often benefit from a more structured and flexible approach to event handling. You might want separate handlers for different event categories (e.g., one for WiFi link status, another for IP address acquisition), or you might need to dynamically add or remove event listeners based on the application’s current mode of operation.

This chapter delves deeper into the ESP-IDF esp_event library. We’ll explore its architecture, the different ways to register and manage handlers, how to pass custom data, and the best practices required to build robust, event-driven WiFi applications. Mastering event handling is key to creating responsive and well-structured firmware.

Theory

The ESP-IDF Event Loop Library (esp_event)

The esp_event library provides a foundation for decoupled communication between different software components in ESP-IDF. Instead of components calling each other directly, they can post events to an event loop, and other components can register handlers to be notified when specific events occur. This promotes modularity and simplifies system design.

Core Concepts:

  1. Event Loops: An event loop is essentially a queue where events are posted and a mechanism to dispatch these events to registered handlers.
    • Default Event Loop: ESP-IDF creates a system-wide default event loop automatically when you call esp_event_loop_create_default(). This is the loop used by default by system services like WiFi and TCP/IP (LwIP). For most applications, using the default loop is sufficient.
    • Custom Event Loops: You can create custom event loops (esp_event_loop_create()) for specific application modules if you need finer control or isolation, but this is less common for basic WiFi handling.
  2. Event Sources: Components that generate events (e.g., the WiFi driver, the TCP/IP stack, your own application modules).
  3. Events: A notification that something significant has happened. Each event is characterized by:
    • Event Base (esp_event_base_t): A unique identifier (usually a string) grouping related events. Examples: WIFI_EVENT, IP_EVENT.
    • Event ID (int32_t): An integer identifying a specific event within an event base. Examples: WIFI_EVENT_STA_START, IP_EVENT_STA_GOT_IP.
    • Event Data (void*): An optional pointer to data associated with the event, providing more context. The actual data type depends on the specific event base and ID. Example: wifi_event_sta_connected_t* for WIFI_EVENT_STA_CONNECTED.
  4. Event Handlers: Functions that are executed when a specific event (or any event within a base) occurs. They receive the event base, ID, and data pointer as arguments, along with any user-defined context provided during registration.
Concept Description Example(s)
Event Loop A central mechanism that queues events and dispatches them to registered handlers. Manages the flow of event notifications. Default Event Loop (esp_event_loop_create_default()), Custom Event Loops (esp_event_loop_create())
Event Source A software component or module that generates and posts events to an event loop. WiFi Driver, TCP/IP Stack (LwIP), Application Modules, Peripherals
Event Base A unique identifier (esp_event_base_t), typically a string, that groups related events. Defines a category of events. WIFI_EVENT, IP_EVENT, MY_APP_EVENT (custom)
Event ID An integer (int32_t) that specifically identifies an event within a particular event base. WIFI_EVENT_STA_START, IP_EVENT_STA_GOT_IP, ESP_EVENT_ANY_ID (wildcard)
Event Data An optional pointer (void*) to data associated with a specific event, providing context or details about what happened. Its actual type depends on the event base and ID. wifi_event_sta_connected_t* for WIFI_EVENT_STA_CONNECTED, ip_event_got_ip_t* for IP_EVENT_STA_GOT_IP
Event Handler A callback function (esp_event_handler_t) registered with an event loop to be executed when a specific event (or any event in a base) occurs. User-defined functions matching the signature: void handler(void* arg, esp_event_base_t base, int32_t id, void* data);
Handler Argument (Context) A user-defined pointer (void*) passed during handler registration and then provided to the handler function each time it’s called. Used to pass state or context. Pointer to a struct, object instance, queue handle, etc.
Handler Instance An opaque handle (esp_event_handler_instance_t) returned by esp_event_handler_instance_register, allowing unique identification and unregistration of a specific handler registration. Used with esp_event_handler_instance_unregister.
%%{ init: { 'flowchart': { 'curve': 'basis' } } }%%
graph TD
    %% Node Styles
    classDef source fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6; %% Primary/Start
    classDef eventLoop fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF; %% Process
    classDef eventData fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E; %% Decision/Data
    classDef handler fill:#D1FAE5,stroke:#059669,stroke-width:2px,color:#065F46; %% Success/End
    classDef queue fill:#E0F2FE,stroke:#0EA5E9,stroke-width:1px,color:#0C4A6E; %% Lighter blue for queue

    A["<b>Event Source</b><br>(e.g., WiFi Driver, TCP/IP Stack,<br>Application Module)"]:::source
    B(<b>Event Occurs</b><br>Identified by Event Base & Event ID):::eventData
    C["<b>Posts Event to Loop</b><br><code class="code-like">esp_event_post_to()</code> or <code class="code-like">esp_event_post()</code><br>Includes: Base, ID, Event Data (optional)"]:::process
    D["<b>Event Loop Task</b><br>(e.g., Default System Event Loop)"]:::eventLoop
    E(<b>Event Queue</b>):::queue
    F{"<b>Registered Handler(s) Found?</b><br>Matching Event Base and ID<br>(or ESP_EVENT_ANY_ID for base)"}:::decision
    G["<b>User-Defined Event Handler Function</b><br><code class="code-like">void my_handler(arg, base, id, data)</code>"]:::handler
    H["<b>Handler Logic Executes</b><br>- Processes event_data<br>- Uses handler_arg (context)<br>- Updates application state<br>- Signals other tasks"]:::handler
    I["No matching handler /<br>Event handled by default (if any)"]:::process


    A --> B;
    B --> C;
    C --> D;
    D --> E;
    E --> F;
    F -- "Yes" --> G;
    G --> H;
    F -- "No" --> I;

    %% Apply styles
    class A source;
    class C,D,I process;
    class B,F eventData;
    class E queue;
    class G,H handler;

Event Handler Function Signature:

All event handler functions must conform to the following signature:

void event_handler_function(void* handler_arg,
                            esp_event_base_t event_base,
                            int32_t event_id,
                            void* event_data);
  • handler_arg: A void* pointer to custom data that was passed during handler registration. This allows you to associate specific context or state with your handler instance.
  • event_base: The event base of the event being handled (e.g., WIFI_EVENT).
  • event_id: The specific event ID within the base (e.g., WIFI_EVENT_STA_CONNECTED).
  • event_data: A void* pointer to the event-specific data. You must cast this to the correct data structure type based on the event_base and event_id before accessing its members. Check documentation or header files (esp_wifi_types.h, esp_event_base.h, etc.) for the correct types.

Registering Event Handlers

ESP-IDF provides two main functions for registering handlers with an event loop (typically the default loop):

  1. esp_event_handler_register(esp_event_base_t event_base, int32_t event_id, esp_event_handler_t event_handler, void* event_handler_arg):
    • Registers a handler function (event_handler) to be called when a specific event (event_base, event_id) occurs.
    • You can use ESP_EVENT_ANY_ID for event_id to catch all events associated with the specified event_base.
    • event_handler_arg is the custom context pointer passed to your handler.
    • Limitation: You cannot easily unregister a specific instance if you register the same handler function multiple times with this function (e.g., for different event IDs). Unregistering requires matching the exact function pointer.
  2. esp_event_handler_instance_register(esp_event_base_t event_base, int32_t event_id, esp_event_handler_t event_handler, void* event_handler_arg, esp_event_handler_instance_t* instance):
    • Similar to esp_event_handler_register, but it returns an opaque instance handle (esp_event_handler_instance_t).
    • This instance handle allows you to uniquely identify and later unregister this specific registration, even if the same handler function is registered for other events or multiple times.
    • This is generally the preferred method for managing handlers, especially if you might need to unregister them later. We used this function in previous chapters.
Feature esp_event_handler_register() esp_event_handler_instance_register()
Purpose Registers an event handler function for a specific event base and ID. Registers an event handler function and provides an instance handle for that specific registration.
Return Value esp_err_t (error code) esp_err_t (error code). The instance handle is an output parameter (esp_event_handler_instance_t* instance).
Unregistration Uses esp_event_handler_unregister(). Requires matching exact function pointer, base, and ID. Can be problematic if the same function is registered multiple times. Uses esp_event_handler_instance_unregister(). Uses the unique instance handle. Reliable and unambiguous.
Handler Argument (Context) Yes, via void* event_handler_arg. Yes, via void* event_handler_arg.
Wildcard Event ID Supports ESP_EVENT_ANY_ID to catch all events for a base. Supports ESP_EVENT_ANY_ID to catch all events for a base.
Use Case / Preference Simpler for very basic scenarios where unregistration is not complex or not needed. Generally preferred, especially if handlers might be unregistered, or if the same handler function is used for multiple event registrations. Provides better lifecycle management.

Example Scenarios:

  • Single Handler for All WiFi Events:
C
esp_event_handler_instance_t instance_any_wifi;
ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT,
                                                    ESP_EVENT_ANY_ID, // Catch all WiFi events
                                                    &my_wifi_event_handler,
                                                    my_custom_context, // Optional context
                                                    &instance_any_wifi));

  • Specific Handler for IP Acquisition:
C
esp_event_handler_instance_t instance_got_ip;
ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT,
                                                    IP_EVENT_STA_GOT_IP, // Only this specific IP event
                                                    &my_ip_event_handler,
                                                    NULL, // No context needed here
                                                    &instance_got_ip));

  • Separate Handlers (Conceptual):
C
// In wifi_init_sta() or similar setup function
esp_event_handler_instance_t instance_wifi_connect_handler;
esp_event_handler_instance_t instance_wifi_disconnect_handler;
esp_event_handler_instance_t instance_ip_handler;

ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT, WIFI_EVENT_STA_CONNECTED, &handle_wifi_connect, NULL, &instance_wifi_connect_handler));
ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT, WIFI_EVENT_STA_DISCONNECTED, &handle_wifi_disconnect, NULL, &instance_wifi_disconnect_handler));
ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &handle_ip_acquired, NULL, &instance_ip_handler));

Unregistering Event Handlers

It’s crucial to unregister event handlers when they are no longer needed, especially if the component they belong to is being shut down or reconfigured. Failure to unregister can lead to handlers being called with invalid context or attempts to access resources that no longer exist, often resulting in crashes.

  1. esp_event_handler_unregister(esp_event_base_t event_base, int32_t event_id, esp_event_handler_t event_handler):
    • Unregisters a handler previously registered with esp_event_handler_register.
    • Requires matching the exact event_base, event_id, and event_handler function pointer used during registration.
    • Difficult to use if the same handler function was registered multiple times.
  2. esp_event_handler_instance_unregister(esp_event_base_t event_base, int32_t event_id, esp_event_handler_instance_t instance):
    • Unregisters a specific handler instance previously registered with esp_event_handler_instance_register.
    • Uses the instance handle obtained during registration.
    • This is the preferred and safer method for unregistering.
%%{ init: { 'state': { 'defaultRenderer': 'dagre' } } }%%
stateDiagram-v2
    direction TB
    [*] --> NotRegistered

    state NotRegistered {
        s0: Handler function exists but is not active.
    }
    NotRegistered --> Registering : Call <code class="code-like">esp_event_handler_instance_register()</code>

    state Registering {
        s1: System is processing the registration request.
    }
    Registering --> Registered : Registration Successful (ESP_OK)
    Registering --> NotRegistered : Registration Failed (Error)

    state Registered {
        s2: Handler is active in the event loop.<br>Will be called when matching events occur.
        [*] --> ReceivingEvent : Event Posted to Loop
    }
    ReceivingEvent : Event (Base, ID, Data)
    ReceivingEvent --> ExecutingHandler : Handler Invoked
    ExecutingHandler : Handler logic runs
    ExecutingHandler --> Registered : Handler Completes

    Registered --> Unregistering : Call <code class="code-like">esp_event_handler_instance_unregister()</code>
    
    state Unregistering {
        s3: System is processing the unregistration request.
    }
    Unregistering --> NotRegistered : Unregistration Successful (ESP_OK)
    Unregistering --> Registered : Unregistration Failed (Error)


    %% Styling using chapter's palette
    %% Primary/Start Nodes: fill:#EDE9FE,stroke:#5B21B6
    %% Process Nodes: fill:#DBEAFE,stroke:#2563EB
    %% Success/Active Nodes: fill:#D1FAE5,stroke:#059669
    %% Check/Validation (intermediate) Nodes: fill:#FEF3C7,stroke:#D97706

    style NotRegistered fill:#EDE9FE,stroke:#5B21B6,color:#5B21B6
    style Registering fill:#DBEAFE,stroke:#2563EB,color:#1E40AF
    style Registered fill:#D1FAE5,stroke:#059669,color:#065F46
    style ReceivingEvent fill:#FEF3C7,stroke:#D97706,color:#92400E
    style ExecutingHandler fill:#FEF3C7,stroke:#D97706,color:#92400E
    style Unregistering fill:#DBEAFE,stroke:#2563EB,color:#1E40AF

When to Unregister:

  • Before deinitializing a module that registered handlers (e.g., before calling esp_wifi_deinit or esp_event_loop_delete_default).
  • When dynamically changing application behavior where certain event responses are no longer required.
  • During cleanup phases before restarting or reconfiguring a subsystem.

Handler Context (handler_arg)

The handler_arg parameter provided during registration is passed directly to your handler function every time it’s invoked. This is extremely useful for associating state or context with a specific handler instance without relying on global variables.

Example Use Cases:

  • Passing a pointer to a structure containing configuration or state relevant to the handler.
  • Passing a pointer to an object instance in C++ applications.
  • Passing a queue handle or semaphore handle for communication with other tasks.
C
// Example context structure
typedef struct {
    int retry_count;
    QueueHandle_t status_queue;
    // Other relevant data
} wifi_handler_context_t;

// In setup
wifi_handler_context_t* my_context = malloc(sizeof(wifi_handler_context_t));
// Initialize my_context members...
my_context->retry_count = 0;
my_context->status_queue = xQueueCreate(...);

esp_event_handler_instance_t instance_wifi;
ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID,
                                                    &my_wifi_handler,
                                                    my_context, // Pass pointer to context
                                                    &instance_wifi));

// In the handler
void my_wifi_handler(void* handler_arg, esp_event_base_t event_base, int32_t event_id, void* event_data) {
    // Cast the handler_arg back to the correct type
    wifi_handler_context_t* ctx = (wifi_handler_context_t*) handler_arg;

    if (event_id == WIFI_EVENT_STA_DISCONNECTED) {
        ctx->retry_count++;
        ESP_LOGI(TAG, "Disconnect detected, retry count: %d", ctx->retry_count);
        // Use ctx->status_queue to send status update...
    }
    // ... other event handling ...
}

// IMPORTANT: Remember to free the context memory when unregistering the handler
// if it was dynamically allocated.
// esp_event_handler_instance_unregister(WIFI_EVENT, ESP_EVENT_ANY_ID, instance_wifi);
// free(my_context);

Best Practices for Event Handlers

Best Practice Rationale / Benefit Potential Issue if Ignored
Keep Handlers Short & Non-Blocking Prevents stalling the event loop task, ensuring timely processing of other system events. System unresponsiveness, missed events, watchdog timeouts.
Offload Long Tasks Maintains event loop responsiveness. Allows complex operations to run in dedicated tasks with appropriate priority. Same as above; handler effectively blocks the event loop.
Ensure Thread Safety Prevents race conditions and data corruption when handlers access resources shared with other tasks. Intermittent crashes, corrupted data, unpredictable behavior.
Verify Event ID & Cast event_data Carefully Ensures correct interpretation and safe access to event-specific data. Prevents accessing invalid memory. Crashes (Guru Meditation Errors), use of garbage data, incorrect logic.
Manage Handler Lifecycle (Register/Unregister) Prevents resource leaks, avoids handlers being called with invalid context, and ensures clean module deinitialization. Crashes on deinit, memory leaks (if context not freed), handlers firing unexpectedly.
Use Instance-Based Registration/Unregistration esp_event_handler_instance_register and ..._unregister provide unambiguous control over specific handler instances. Difficulty unregistering specific handlers if the same function is used multiple times with esp_event_handler_register.
Check API Return Values Allows detection and handling of errors during registration or unregistration. Silent failures, leading to handlers not running or other unexpected issues.
Free Handler Context Memory If handler_arg points to dynamically allocated memory, it must be freed when the handler is unregistered and no longer needed. Memory leaks.

Practical Examples

Let’s modify our WiFi connection code to demonstrate registering separate handlers and managing their lifecycle.

Prerequisites:

  • ESP-IDF v5.x environment with VS Code.
  • Project based on Chapter 29 (with NVS auto-connect and backoff).

Example 1: Separate Handlers for WiFi and IP Events

We’ll create two distinct handler functions: one for WIFI_EVENTs and one for IP_EVENT_STA_GOT_IP.

1. Define Handler Functions:

C
// --- Globals (Keep necessary ones like TAG, event group, retry state) ---
static const char *TAG = "WIFI_EVENTS";
static EventGroupHandle_t s_wifi_event_group;
#define WIFI_CONNECTED_BIT BIT0
#define WIFI_FAIL_BIT      BIT1

#define EXAMPLE_ESP_MAXIMUM_RETRY  CONFIG_EXAMPLE_MAXIMUM_RETRY
#define BASE_RETRY_DELAY_MS 1000
#define MAX_RETRY_DELAY_MS  60000

static int s_retry_num = 0;
static uint32_t s_retry_delay_ms = BASE_RETRY_DELAY_MS;

// Include NVS definitions if continuing from Chapter 29
#include "nvs_flash.h"
#include "nvs.h"
#define NVS_NAMESPACE "wifi_creds"
#define NVS_KEY_SSID  "ssid"
#define NVS_KEY_PASS  "password"
#define EXAMPLE_DEFAULT_WIFI_SSID      CONFIG_EXAMPLE_WIFI_SSID
#define EXAMPLE_DEFAULT_WIFI_PASSWORD  CONFIG_EXAMPLE_WIFI_PASSWORD

// Helper function from Chapter 28 (keep it)
const char* wifi_reason_to_str(wifi_err_reason_t reason) {
    // ... (implementation from Chapter 28) ...
     switch (reason) {
        case WIFI_REASON_UNSPECIFIED: return "UNSPECIFIED";
        case WIFI_REASON_AUTH_EXPIRE: return "AUTH_EXPIRE";
        // ... other reasons
        case WIFI_REASON_BEACON_TIMEOUT: return "BEACON_TIMEOUT";
        case WIFI_REASON_NO_AP_FOUND: return "NO_AP_FOUND";
        case WIFI_REASON_AUTH_FAIL: return "AUTH_FAIL";
        case WIFI_REASON_ASSOC_FAIL: return "ASSOC_FAIL";
        case WIFI_REASON_HANDSHAKE_TIMEOUT: return "HANDSHAKE_TIMEOUT";
        default: return "UNKNOWN";
    }
}


// --- WiFi Event Handler ---
static void wifi_event_handler(void* arg, esp_event_base_t event_base,
                               int32_t event_id, void* event_data)
{
    ESP_LOGD(TAG, "Received WIFI_EVENT, ID=%ld", event_id);

    switch (event_id) {
        case WIFI_EVENT_STA_START:
            ESP_LOGI(TAG, "[WiFi Handler] WIFI_EVENT_STA_START: Station mode started.");
            ESP_LOGI(TAG, "[WiFi Handler] Initiating connection attempt...");
            esp_wifi_connect(); // Initiate connection
            break;

        case WIFI_EVENT_STA_CONNECTED:
            {
                wifi_event_sta_connected_t* event = (wifi_event_sta_connected_t*) event_data;
                ESP_LOGI(TAG, "[WiFi Handler] WIFI_EVENT_STA_CONNECTED: Associated with AP!");
                ESP_LOGI(TAG, "  SSID: %.*s, BSSID: " MACSTR ", Channel: %d",
                         event->ssid_len, event->ssid, MAC2STR(event->bssid), event->channel);
                s_retry_delay_ms = BASE_RETRY_DELAY_MS; // Reset backoff delay
            }
            break;

        case WIFI_EVENT_STA_DISCONNECTED:
            {
                wifi_event_sta_disconnected_t* event = (wifi_event_sta_disconnected_t*) event_data;
                const char* reason_str = wifi_reason_to_str(event->reason);
                ESP_LOGW(TAG, "[WiFi Handler] WIFI_EVENT_STA_DISCONNECTED: Lost connection. Reason: %d (%s)", event->reason, reason_str);

                // Exponential Backoff Logic (from Chapter 29)
                bool retry = false;
                // ... (Determine if retry is needed based on event->reason) ...
                 switch(event->reason) {
                    case WIFI_REASON_BEACON_TIMEOUT: // Example retryable reason
                    case WIFI_REASON_ASSOC_EXPIRE:
                    case WIFI_REASON_AUTH_EXPIRE:
                    case WIFI_REASON_HANDSHAKE_TIMEOUT:
                    case WIFI_REASON_CONNECTION_FAIL:
                        retry = true;
                        break;
                    case WIFI_REASON_AUTH_FAIL: // Example non-retryable
                    case WIFI_REASON_NO_AP_FOUND:
                        retry = false;
                        ESP_LOGE(TAG, "Non-retryable error.");
                        break;
                    default:
                        retry = true; // Default to retry
                        break;
                 }


                if (retry && s_retry_num < EXAMPLE_ESP_MAXIMUM_RETRY) {
                    ESP_LOGI(TAG, "[WiFi Handler] Retrying connection (%d/%d) after %" PRIu32 " ms delay...",
                             s_retry_num + 1, EXAMPLE_ESP_MAXIMUM_RETRY, s_retry_delay_ms);
                    vTaskDelay(pdMS_TO_TICKS(s_retry_delay_ms));
                    s_retry_delay_ms *= 2;
                    if (s_retry_delay_ms > MAX_RETRY_DELAY_MS) {
                        s_retry_delay_ms = MAX_RETRY_DELAY_MS;
                    }
                    s_retry_num++;
                    esp_wifi_connect();
                } else if (retry) { // Retries exhausted
                    ESP_LOGE(TAG, "[WiFi Handler] Connection failed after %d retries.", EXAMPLE_ESP_MAXIMUM_RETRY);
                    s_retry_num = 0; s_retry_delay_ms = BASE_RETRY_DELAY_MS;
                    if (s_wifi_event_group) xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT);
                } else { // Non-retryable error
                    s_retry_num = 0; s_retry_delay_ms = BASE_RETRY_DELAY_MS;
                    if (s_wifi_event_group) xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT);
                }
            }
            break;

        // Handle other WIFI_EVENTs if needed (e.g., AP mode events)
        // case WIFI_EVENT_AP_START: ...
        // case WIFI_EVENT_AP_STACONNECTED: ...

        default:
            ESP_LOGI(TAG, "[WiFi Handler] Unhandled WIFI_EVENT: %ld", event_id);
            break;
    }
}

// --- IP Event Handler ---
static void ip_event_handler(void* arg, esp_event_base_t event_base,
                             int32_t event_id, void* event_data)
{
    ESP_LOGD(TAG, "Received IP_EVENT, ID=%ld", event_id);

    switch(event_id) {
        case IP_EVENT_STA_GOT_IP:
            {
                ip_event_got_ip_t* event = (ip_event_got_ip_t*) event_data;
                ESP_LOGI(TAG, "[IP Handler] IP_EVENT_STA_GOT_IP: Network Ready!");
                ESP_LOGI(TAG, "  Assigned IP : " IPSTR, IP2STR(&event->ip_info.ip));
                ESP_LOGI(TAG, "  Gateway     : " IPSTR, IP2STR(&event->ip_info.gw));

                // Reset retry counter on successful connection
                ESP_LOGI(TAG, "[IP Handler] Resetting WiFi retry counter.");
                s_retry_num = 0;
                s_retry_delay_ms = BASE_RETRY_DELAY_MS; // Also reset delay here

                if (s_wifi_event_group) {
                    xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT);
                }
            }
            break;

        // Handle other IP_EVENTs if needed (e.g., IPv6, ETH_GOT_IP)
        // case IP_EVENT_GOT_IP6: ...

        default:
            ESP_LOGI(TAG, "[IP Handler] Unhandled IP_EVENT: %ld", event_id);
            break;
    }
}

2. Modify Registration in wifi_init_sta:

C
// --- Global variables for handler instances ---
static esp_event_handler_instance_t instance_wifi_handler;
static esp_event_handler_instance_t instance_ip_handler;


/* --- WiFi Initialization Function --- */
void wifi_init_sta(void)
{
    // 0. Create Event Group
    s_wifi_event_group = xEventGroupCreate();
    ESP_ERROR_CHECK(s_wifi_event_group == NULL ? ESP_FAIL : ESP_OK);

    // 1. Initialize TCP/IP stack
    ESP_ERROR_CHECK(esp_netif_init());

    // 2. Create default event loop
    ESP_ERROR_CHECK(esp_event_loop_create_default());

    // 3. Create default WiFi station network interface
    esp_netif_t *sta_netif = esp_netif_create_default_wifi_sta();
    ESP_ERROR_CHECK(sta_netif == NULL ? ESP_FAIL : ESP_OK);

    // 4. Initialize WiFi with default configuration
    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    ESP_ERROR_CHECK(esp_wifi_init(&cfg));

    // 5. Register SEPARATE event handlers
    ESP_LOGI(TAG, "Registering event handlers...");
    ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT,
                                                        ESP_EVENT_ANY_ID, // Catch all WiFi events
                                                        &wifi_event_handler, // Use the WiFi-specific handler
                                                        NULL, // No context needed for this example
                                                        &instance_wifi_handler)); // Store instance handle

    ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT,
                                                        IP_EVENT_STA_GOT_IP, // Catch only GOT_IP event
                                                        &ip_event_handler, // Use the IP-specific handler
                                                        NULL,
                                                        &instance_ip_handler)); // Store instance handle


    // --- NVS Handling (from Chapter 29) ---
    wifi_config_t wifi_config;
    memset(&wifi_config, 0, sizeof(wifi_config_t));
    bool credentials_found = false;
    // ... (NVS read/write logic as in Chapter 29) ...
     nvs_handle_t nvs_handle;
    esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READWRITE, &nvs_handle);
    if (err == ESP_OK) {
        // Read SSID/Password logic...
        size_t required_size_ssid = sizeof(wifi_config.sta.ssid);
        err = nvs_get_str(nvs_handle, NVS_KEY_SSID, (char*)wifi_config.sta.ssid, &required_size_ssid);
        if(err == ESP_OK && required_size_ssid > 1) {
            size_t required_size_pass = sizeof(wifi_config.sta.password);
            err = nvs_get_str(nvs_handle, NVS_KEY_PASS, (char*)wifi_config.sta.password, &required_size_pass);
            if(err == ESP_OK && required_size_pass > 1) {
                credentials_found = true;
                ESP_LOGI(TAG, "Credentials found in NVS.");
            }
        }
        // Store defaults if not found...
        if (!credentials_found) {
             ESP_LOGW(TAG, "Storing default credentials from Kconfig to NVS.");
             strncpy((char*)wifi_config.sta.ssid, EXAMPLE_DEFAULT_WIFI_SSID, sizeof(wifi_config.sta.ssid) - 1);
             strncpy((char*)wifi_config.sta.password, EXAMPLE_DEFAULT_WIFI_PASSWORD, sizeof(wifi_config.sta.password) - 1);
             nvs_set_str(nvs_handle, NVS_KEY_SSID, (const char*)wifi_config.sta.ssid);
             nvs_set_str(nvs_handle, NVS_KEY_PASS, (const char*)wifi_config.sta.password);
             nvs_commit(nvs_handle);
             credentials_found = true; // Use defaults for this run
        }
        nvs_close(nvs_handle);
    } else {
         ESP_LOGE(TAG, "Error (%s) opening NVS handle!", esp_err_to_name(err));
    }


    if (!credentials_found) {
        ESP_LOGE(TAG, "Failed to load or store WiFi credentials. Cannot connect.");
        xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT);
        return; // Stop initialization
    }

    // 6. Configure WiFi Station
    wifi_config.sta.threshold.authmode = WIFI_AUTH_WPA2_PSK; // Assuming WPA2 PSK

    // 7. Set WiFi Mode
    ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));

    // 8. Set WiFi Configuration
    ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));

    // 9. Start WiFi Driver
    ESP_LOGI(TAG, "[Action] Starting WiFi Driver...");
    ESP_ERROR_CHECK(esp_wifi_start());

    ESP_LOGI(TAG, "wifi_init_sta finished setup. Waiting for connection events...");

    /* --- Wait for connection or failure --- */
    EventBits_t bits = xEventGroupWaitBits(s_wifi_event_group,
            WIFI_CONNECTED_BIT | WIFI_FAIL_BIT,
            pdFALSE, pdFALSE, portMAX_DELAY);

    /* Interpret results */
    if (bits & WIFI_CONNECTED_BIT) {
        ESP_LOGI(TAG, "-------------------------------------------");
        ESP_LOGI(TAG, "WiFi Connected to AP!");
        ESP_LOGI(TAG, "-------------------------------------------");
    } else if (bits & WIFI_FAIL_BIT) {
        ESP_LOGE(TAG, "-------------------------------------------");
        ESP_LOGE(TAG, "WiFi Failed to Connect!");
        ESP_LOGE(TAG, "-------------------------------------------");
    } else {
        ESP_LOGE(TAG, "UNEXPECTED EVENT during wait");
    }

    /* --- Optional: Unregister handlers here if wifi_init_sta might be called again --- */
    /* Or unregister during a dedicated deinitialization phase */
    // ESP_LOGI(TAG, "Unregistering event handlers...");
    // esp_event_handler_instance_unregister(WIFI_EVENT, ESP_EVENT_ANY_ID, instance_wifi_handler);
    // esp_event_handler_instance_unregister(IP_EVENT, IP_EVENT_STA_GOT_IP, instance_ip_handler);
}

// Ensure app_main initializes NVS (as in Chapter 29)
void app_main(void)
{
    ESP_LOGI(TAG, "Starting WiFi Event Handling Example...");

    // Initialize NVS
    esp_err_t ret = nvs_flash_init();
    if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
      ESP_ERROR_CHECK(nvs_flash_erase());
      ret = nvs_flash_init();
    }
    ESP_ERROR_CHECK(ret);

    wifi_init_sta();

    ESP_LOGI(TAG, "Post-connection phase. Application can proceed...");

    // Example: Add a task or loop here for application logic
    while(1) {
        vTaskDelay(pdMS_TO_TICKS(10000)); // Keep main task alive
    }
}

3. Build, Flash, Monitor:

  • Ensure Kconfig options are set (idf.py menuconfig).
  • idf.py build
  • idf.py flash
  • idf.py monitor

Expected Output:

The connection flow should proceed as before, but the log messages will now indicate which handler ([WiFi Handler] or [IP Handler]) processed the event. This demonstrates the separation of concerns.

Plaintext
// ... NVS logs ...
I (XXX) WIFI_EVENTS: Registering event handlers...
I (XXX) WIFI_EVENTS: [Action] Starting WiFi Driver...
I (XXX) WIFI_EVENTS: wifi_init_sta finished setup. Waiting for connection events...
D (XXX) WIFI_EVENTS: Received WIFI_EVENT, ID=2 // WIFI_EVENT_STA_START
I (XXX) WIFI_EVENTS: [WiFi Handler] WIFI_EVENT_STA_START: Station mode started.
I (XXX) WIFI_EVENTS: [WiFi Handler] Initiating connection attempt...
D (XXX) WIFI_EVENTS: Received WIFI_EVENT, ID=4 // WIFI_EVENT_STA_CONNECTED
I (XXX) WIFI_EVENTS: [WiFi Handler] WIFI_EVENT_STA_CONNECTED: Associated with AP!
I (XXX) WIFI_EVENTS:   SSID: YOUR_SSID, BSSID: xx:xx:xx:xx:xx:xx, Channel: X
D (XXX) WIFI_EVENTS: Received IP_EVENT, ID=0 // IP_EVENT_STA_GOT_IP
I (XXX) WIFI_EVENTS: [IP Handler] IP_EVENT_STA_GOT_IP: Network Ready!
I (XXX) WIFI_EVENTS:   Assigned IP : 192.168.x.y
I (XXX) WIFI_EVENTS:   Gateway     : 192.168.x.z
I (XXX) WIFI_EVENTS: [IP Handler] Resetting WiFi retry counter.
I (XXX) WIFI_EVENTS: -------------------------------------------
I (XXX) WIFI_EVENTS: WiFi Connected to AP!
I (XXX) WIFI_EVENTS: -------------------------------------------
I (XXX) WIFI_EVENTS: Post-connection phase. Application can proceed...

If you disconnect (turn off AP), you’ll see the [WiFi Handler] processing the WIFI_EVENT_STA_DISCONNECTED and managing the retries.

Example 2: Dynamic Unregistration (Conceptual)

Imagine a scenario where you want to stop attempting reconnections if the user puts the device into a configuration mode.

C
// Assume 'g_config_mode' is a global boolean flag, managed safely (e.g., atomic or mutex)
// Set to true when entering config mode, false otherwise.

// Modify the WIFI_EVENT_STA_DISCONNECTED case in wifi_event_handler:
case WIFI_EVENT_STA_DISCONNECTED:
    {
        // ... (Log disconnection reason) ...
        if (g_config_mode) {
             ESP_LOGW(TAG, "[WiFi Handler] In config mode, disabling automatic reconnect.");
             // Optionally unregister the handler if no WiFi events are needed in config mode
             // esp_event_handler_instance_unregister(WIFI_EVENT, ESP_EVENT_ANY_ID, instance_wifi_handler);
             // Note: Need to re-register later when exiting config mode.
             if (s_wifi_event_group) xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT); // Signal failure
             break; // Don't proceed with retry logic
        }

        // ... (Normal retry logic with exponential backoff) ...
    }
    break;

// Need logic elsewhere to set g_config_mode and potentially re-register handlers
// void enter_config_mode() {
//    g_config_mode = true;
//    esp_wifi_disconnect(); // Optional: force disconnect
//    // Maybe unregister handlers here if appropriate
// }

// void exit_config_mode() {
//    g_config_mode = false;
//    // Re-register handlers if they were unregistered
//    // ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT, ...));
//    // ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT, ...));
//    esp_wifi_connect(); // Attempt connection again
// }

This conceptual example highlights how you might use esp_event_handler_instance_unregister (and subsequent re-registration) to dynamically control event handling based on application state. Proper state management and careful handling of registration/unregistration are key.

Variant Notes

  • ESP32, ESP32-S2, ESP32-S3, ESP32-C3, ESP32-C6: The esp_event library, handler registration/unregistration functions, event bases (WIFI_EVENT, IP_EVENT), and event IDs discussed are standard across all these WiFi-enabled variants using ESP-IDF v5.x. The principles and practices apply equally.
  • ESP32-H2: While the esp_event library itself is part of ESP-IDF and used for other subsystems (like Bluetooth, Thread), the specific WIFI_EVENT and IP_EVENT related to WiFi station/AP modes are not applicable as the ESP32-H2 lacks 802.11 hardware. It will have its own event bases and IDs for its supported protocols (e.g., ZB_EVENT for Zigbee, BLE_EVENT for Bluetooth LE).

Common Mistakes & Troubleshooting Tips

Mistake / Issue Symptom(s) Solution / Best Practice
Blocking in Handler System sluggishness, missed events, watchdog resets (e.g., “Task watchdog got triggered”). WiFi connection/disconnection delays. Keep handlers short. Offload lengthy operations (long delays, file I/O, complex calculations) to separate tasks signaled by the handler.
Incorrect event_data Casting/Access Crashes (Guru Meditation Errors like LoadProhibited, StoreProhibited), corrupted data, unexpected behavior when certain events occur. Use a switch on event_id. Cast event_data to the correct documented type for that specific event. Check header files (e.g., esp_wifi_types.h). Handle NULL event_data if applicable.
Forgetting to Unregister Handlers Crashes during deinitialization (e.g., after esp_wifi_deinit()) or when event loop tries to call a stale handler. Memory leaks if context isn’t freed. Unregister handlers (preferably using esp_event_handler_instance_unregister) before deinitializing the associated module or event loop. Free dynamically allocated context memory after unregistration.
Race Conditions / Thread Safety Intermittent crashes, corrupted shared data, unpredictable behavior due to concurrent access from handler and other tasks. Protect shared resources accessed by handlers and other tasks using FreeRTOS synchronization primitives (mutexes, semaphores, critical sections).
Using esp_event_handler_register and Losing Track Difficulty unregistering a specific instance if the same handler function is registered multiple times. Unregistering might remove all instances or fail. Strongly prefer esp_event_handler_instance_register and esp_event_handler_instance_unregister for unambiguous handler management.
Ignoring API Return Codes Registration/unregistration might fail silently, leading to handlers not running or other unexpected issues. Always check the esp_err_t return values from event handling API calls and handle errors appropriately (log, halt, error state).

Exercises

  1. Context Passing: Modify Example 1. Define a simple struct to hold a counter. Allocate this struct, initialize the counter to 0, and pass a pointer to it as the handler_arg when registering wifi_event_handler. Inside wifi_event_handler, increment the counter every time any WiFi event is received and log the event ID along with the current counter value from the context. Remember to handle memory allocation/deallocation if using malloc.
  2. Dynamic Handler for Connection: Create a new handler function log_connected_handler that simply logs “Event: WiFi Connected!”. Register this handler only for WIFI_EVENT_STA_CONNECTED inside the WIFI_EVENT_STA_START case of the main wifi_event_handler. In the WIFI_EVENT_STA_DISCONNECTED case, unregister the log_connected_handler using its instance handle. Observe the logs to see the “Event: WiFi Connected!” message appearing only after STA_START and stopping after disconnection.
  3. Separate Task Notification: Remove the vTaskDelay from the WIFI_EVENT_STA_DISCONNECTED handler. Instead, create a separate FreeRTOS task (reconnect_task). When disconnection occurs in the handler, send a notification or message to reconnect_task. reconnect_task should wait for this notification, perform the exponential backoff delay (vTaskDelay), and then call esp_wifi_connect(). This demonstrates offloading blocking operations.
  4. Count Specific Reason Codes: Modify the wifi_event_handler. Add counters (perhaps in a context struct) to track how many times WIFI_REASON_BEACON_TIMEOUT and WIFI_REASON_AUTH_FAIL occur specifically. Log these counts whenever a WIFI_EVENT_STA_DISCONNECTED happens.
  5. Error Handling for Registration: Add explicit checks for the return values of esp_event_handler_instance_register in wifi_init_sta. If registration fails (returns anything other than ESP_OK), log a critical error and perhaps halt the system or enter an error state, as failed event registration likely indicates a fundamental problem.

Summary

  • The esp_event library enables decoupled communication via event loops, bases, IDs, and handlers.
  • The default event loop (esp_event_loop_create_default()) is commonly used for system events like WiFi and IP.
  • Event handlers are functions triggered by specific events, receiving context (handler_arg), event identifiers (event_base, event_id), and event data (event_data).
  • esp_event_handler_instance_register is preferred for registering handlers as it provides an instance handle for reliable unregistration via esp_event_handler_instance_unregister.
  • Handlers must be unregistered when no longer needed to prevent crashes and resource leaks.
  • Handlers should be short, non-blocking, and thread-safe. Offload lengthy operations to separate tasks.
  • Carefully manage event_data casting based on event_id and handle potential NULL values.
  • Passing custom context via handler_arg is a powerful way to associate state with handlers.

Further Reading

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top