Chapter 29: WiFi Auto-connect and Reconnection Strategies

Chapter Objectives

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

  • Understand the need for automatic connection attempts on device startup.
  • Utilize Non-Volatile Storage (NVS) to persistently store WiFi credentials.
  • Implement logic to attempt connection automatically using stored credentials.
  • Design and implement robust reconnection strategies after disconnection.
  • Apply the exponential backoff algorithm to avoid overwhelming the network or AP during retries.
  • Manage connection state more effectively within your application.
  • Appreciate the importance of persistence and resilience in connected devices.

Introduction

In the previous chapters, we learned how to manually initiate a WiFi connection and handle the basic events associated with the connection lifecycle, including disconnection. However, real-world IoT devices often need to operate autonomously. They should ideally reconnect automatically after a power cycle, a temporary network outage, or other disruptions without requiring manual intervention. Simply calling esp_wifi_connect() once during initialization isn’t sufficient for a reliable product.

This chapter focuses on building resilience into our WiFi connectivity. We will explore how to store WiFi credentials persistently so the device remembers the network across reboots. We’ll then implement logic to automatically use these credentials to connect upon startup. Furthermore, we’ll refine the reconnection logic introduced in Chapter 28, moving beyond simple retries to implement more sophisticated strategies like exponential backoff, ensuring our device behaves responsibly when facing connection difficulties.

Theory

The Need for Persistence: Storing Credentials

For a device to connect automatically after booting up, it must remember the credentials (SSID and password) of the target WiFi network. Storing these in the firmware’s code (like we did initially) works for development, but it’s inflexible for deployment. If the network password changes, you’d need to reflash the device.

The standard solution in ESP-IDF is to use the Non-Volatile Storage (NVS) library. As introduced in Volume 1, Chapter 22, NVS allows you to store key-value pairs in a dedicated partition of the ESP32’s flash memory, ensuring the data persists across device reboots and power cycles.

For auto-connect functionality, the typical workflow is:

  1. Provisioning (First Boot / Configuration): The device somehow obtains the WiFi credentials (e.g., via a mobile app using Bluetooth/SmartConfig, a serial console command, or a configuration webpage – topics for later chapters).
  2. Storage: These credentials (SSID and password) are written to NVS under specific keys (e.g., "wifi_ssid", "wifi_pass").
  3. Subsequent Boots:
    • The application initializes the NVS library.
    • It attempts to read the stored credentials from NVS using the known keys.
    • If credentials are found, it configures the WiFi station with these credentials.
    • It initiates the connection process (esp_wifi_start(), followed by esp_wifi_connect() in the WIFI_EVENT_STA_START handler).

NVS Keys for WiFi Credentials:

NVS Item Typical Key Name Data Type in NVS Purpose Example Value
Namespace "wifi_creds" N/A (Container) Groups related WiFi credentials together to avoid key collisions with other parts of the application. N/A
WiFi SSID "ssid" String (char*) Stores the Service Set Identifier (network name) of the target WiFi network. "MyHomeNetwork"
WiFi Password "password" String (char*) Stores the password (Pre-Shared Key) for the target WiFi network. "MySecureP@ss!"
Authentication Mode (Optional) "authmode" Unsigned 8-bit Integer (uint8_t) Stores the wifi_auth_mode_t enum value (e.g., WIFI_AUTH_WPA2_PSK). Useful if supporting multiple auth types. 4 (for WIFI_AUTH_WPA2_PSK)
Channel (Optional) "channel" Unsigned 8-bit Integer (uint8_t) Stores a specific WiFi channel to connect to. Usually not needed as APs are found by SSID scan. 6

Auto-Connect Logic Flow

On startup, after basic system initialization (including NVS), the application should check if valid credentials exist in NVS.

%%{ init: { 'flowchart': { 'curve': 'basis' } } }%%
graph TD

    A[<b>Application Start</b>]:::primary
    B["Initialize NVS<br><code class="code-like">nvs_flash_init()</code>"]:::storage
    C{NVS Init OK?}:::decision
    D["Open NVS Namespace<br><code class="code-like">nvs_open(<b>wifi_creds</b>, ...)</code>"]:::storage
    E{Namespace Open OK?}:::decision
    F["Read SSID from NVS<br><code class="code-like">nvs_get_str(<b>ssid</b>, ...)</code>"]:::storage
    G{SSID Found & Valid?}:::decision
    H["Read Password from NVS<br><code class="code-like">nvs_get_str(<b>password</b>, ...)</code>"]:::storage
    I{Password Found & Valid?}:::decision
    J["Store Credentials in RAM<br>(e.g., in <code class="code-like">wifi_config_t</code>)"]:::process
    K["Initialize WiFi Stack<br><code class="code-like">esp_netif_init()</code>, <code class="code-like">event_loop_create_default()</code>,<br><code class="code-like">esp_netif_create_default_wifi_sta()</code>,<br><code class="code-like">esp_wifi_init()</code>"]:::process
    L["Configure WiFi with Stored Credentials<br><code class="code-like">esp_wifi_set_config()</code>"]:::process
    M["Start WiFi Driver<br><code class="code-like">esp_wifi_start()</code>"]:::process
    N[Event: <code class="code-like">WIFI_EVENT_STA_START</code> Received]:::check
    O["Handler calls <code class="code-like">esp_wifi_connect()</code>"]:::process
    P["Connection Process Continues...<br>(Auth, Assoc, DHCP)"]:::success
    Q["Handle Missing/Invalid Credentials<br>(e.g., Enter Provisioning Mode, Use Defaults, Log Error)"]:::fail
    R[Handle NVS Error]:::fail

    A --> B;
    B --> C;
    C -- |Yes| --> D;
    C -- |No| --> R;
    D --> E;
    E -- Yes --> F;
    E -- No --> R;
    F --> G;
    G -- Yes --> H;
    G -- No --> Q;
    H --> I;
    I -- Yes --> J;
    I -- No --> Q;
    J --> K;
    K --> L;
    L --> M;
    M --> N;
    N --> O;
    O --> P;

    %% Node Styles
    classDef primary fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6;
    classDef process fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF;
    classDef decision fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E;
    classDef check fill:#FEE2E2,stroke:#DC2626,stroke-width:1px,color:#991B1B;
    classDef storage fill:#E0F2FE,stroke:#0EA5E9,stroke-width:1px,color:#0C4A6E;
    classDef success fill:#D1FAE5,stroke:#059669,stroke-width:2px,color:#065F46;
    classDef fail fill:#FECACA,stroke:#B91C1C,stroke-width:2px,color:#7F1D1D;

    %% Apply styles
    class A primary;
    class B,D,F,H storage;
    class C,E,G,I decision;
    class J,K,L,M,O process;
    class N check;
    class P success;
    class Q,R fail;

Robust Reconnection Strategies

Chapter 28 introduced basic reconnection logic within the WIFI_EVENT_STA_DISCONNECTED handler, typically involving a fixed number of retries with a potential fixed delay. While better than nothing, this can be improved.

Problems with Simple Retry:

  • Network Flooding: If the AP is temporarily overloaded or the reason for disconnection is persistent (wrong password), retrying immediately or very frequently can add unnecessary traffic and load.
  • Inefficiency: Constantly attempting to connect when the network is genuinely unavailable (e.g., AP powered off) wastes power.

Exponential Backoff:

A more robust and network-friendly approach is exponential backoff. The core idea is to increase the delay between retry attempts exponentially after each failure.

  • Attempt 1: Fails -> Wait D seconds.
  • Attempt 2: Fails -> Wait 2 * D seconds.
  • Attempt 3: Fails -> Wait 4 * D seconds.
  • Attempt 4: Fails -> Wait 8 * D seconds.
  • …and so on, often up to a maximum delay (D_max).
%%{ init: { 'flowchart': { 'curve': 'basis' } } }%%
graph TD
    %% Node Styles
    classDef primary fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6;
    classDef process fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF;
    classDef decision fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E;
    classDef check fill:#FEE2E2,stroke:#DC2626,stroke-width:1px,color:#991B1B;
    classDef success fill:#D1FAE5,stroke:#059669,stroke-width:2px,color:#065F46;
    classDef fail fill:#FECACA,stroke:#B91C1C,stroke-width:2px,color:#7F1D1D;
    classDef io fill:#E0F2FE,stroke:#0EA5E9,stroke-width:1px,color:#0C4A6E; %% For event triggers

    A[Event: <code class="code-like">WIFI_EVENT_STA_DISCONNECTED</code>]:::io
    B{Retryable Reason Code?}:::decision
    C{Retry Count < Max Retries?}:::decision
    D["Log: Retrying connection attempt...<br>Current Delay: <code class="code-like">s_retry_delay_ms</code>"]:::process
    E["Wait for <code class="code-like">s_retry_delay_ms</code><br>(e.g., <code class="code-like">vTaskDelay()</code>)"]:::process
    F["Call <code class="code-like">esp_wifi_connect()</code>"]:::process
    G["Increment Retry Count<br><code class="code-like">s_retry_num++</code>"]:::process
    H["Update Delay for Next Time:<br><code class="code-like">s_retry_delay_ms *= 2</code>"]:::process
    I{"<code class="code-like">s_retry_delay_ms</code> > <code class="code-like">MAX_RETRY_DELAY_MS</code>?"}:::decision
    J["Cap Delay:<br><code class="code-like">s_retry_delay_ms = MAX_RETRY_DELAY_MS</code>"]:::process
    K["Event: <code class="code-like">IP_EVENT_STA_GOT_IP</code> (Connected)"]:::success
    L["Reset <code class="code-like">s_retry_num = 0</code><br>Reset <code class="code-like">s_retry_delay_ms = BASE_DELAY</code>"]:::process
    M["Signal Permanent Failure<br>(e.g., Set <code class="code-like">WIFI_FAIL_BIT</code>, Log Error)"]:::fail
    N["Handle Non-Retryable Disconnect<br>(Log Error, Signal Failure)"]:::fail
    O[Connection Process Continues...<br>Waiting for Connected/Disconnected Event]:::process

    A --> B;
    B -- "Yes" --> C;
    B -- "No" --> N;
    C -- "Yes" --> D;
    C -- "No" --> M;
    D --> E;
    E --> F;
    F --> O; 
    O --> A; 
    O --> K; 
    F -- "After esp_wifi_connect(), update state for next potential failure" --> G; 
    G --> H;
    H --> I;
    I -- "Yes" --> J;
    I -- "No" --> ZEndRetryLogic["End of this disconnect's retry logic prep"];
    J --> ZEndRetryLogic;


    K --> L;
    L --> ZEndSuccess["End of successful connection logic"];

    %% Styling
    class A io;
    class B,C,I decision;
    class D,E,F,G,H,J,L,O process;
    class K success;
    class M,N fail;
    class ZEndRetryLogic process;
    class ZEndSuccess process;

Algorithm:

  1. Initialize a retry_delay variable (e.g., to a base delay like 1000 ms).
  2. Initialize a retry_count to 0.
  3. When WIFI_EVENT_STA_DISCONNECTED occurs:
    • Check the reason code. Decide if retrying is appropriate (as discussed in Chapter 28).
    • If retrying:
      • Check if retry_count exceeds a maximum limit (MAX_RETRIES). If so, signal permanent failure.
      • Log the retry attempt and the current delay.
      • Schedule the next connection attempt using vTaskDelay with the current retry_delay.
      • Call esp_wifi_connect().
      • Increment retry_count.
      • Double retry_delay for the next potential failure (e.g., retry_delay *= 2).
      • Optionally, cap retry_delay at a maximum value (e.g., if (retry_delay > MAX_DELAY) retry_delay = MAX_DELAY;).
  4. When IP_EVENT_STA_GOT_IP occurs (successful connection):
    • Reset retry_count to 0.
    • Reset retry_delay to the initial base delay.

Benefits:

  • Reduces network load during persistent failures.
  • Gives the network/AP time to recover if the issue is temporary.
  • Saves power by reducing connection attempts during prolonged outages.
Feature Simple Retry (Fixed Delay) Exponential Backoff
Delay Mechanism Fixed delay between retries (e.g., always 5 seconds). Delay increases with each failed attempt (e.g., 1s, 2s, 4s, 8s…). Often capped at a maximum.
Network Load Higher during persistent AP/network issues, as retries are frequent. Can contribute to network congestion. Lower during persistent issues, as retry frequency decreases. More “polite” to the network.
Power Efficiency Lower if connection attempts are frequent during prolonged outages (more radio activity). Higher during prolonged outages due to fewer, spaced-out connection attempts.
Recovery Time (for temporary glitches) Predictable if the fixed delay is short. May be slower if fixed delay is long. Quick for initial short-lived glitches. Adapts by waiting longer if the issue persists.
Implementation Complexity Simpler to implement. Requires a retry counter and a fixed delay. Slightly more complex. Requires managing a variable delay, a retry counter, a base delay, and potentially a max delay.
Adaptability to Outage Duration Poor. Treats short and long outages the same way. Good. Quickly retries for short issues, backs off for longer ones.
Risk of “Thundering Herd” Higher if many devices reconnect simultaneously after an outage with the same fixed delay. Lower, as varied delays (especially with jitter) spread out connection attempts.

Managing Connection State

It’s often useful for the rest of your application to know the current WiFi connection state (Disconnected, Connecting, Connected-No-IP, Connected-Got-IP). While the event handler reacts to changes, other tasks might need to query the state.

  • Event Flags/Groups: Use FreeRTOS event groups (as shown before) or flags updated by the event handler to signal major state changes (e.g., WIFI_CONNECTED_BIT, WIFI_CONNECTING_BIT, WIFI_DISCONNECTED_BIT).
  • State Variable: Maintain a global or module-specific variable (e.g., an enum) that the event handler updates. Ensure thread safety if accessed by multiple tasks (use mutexes or critical sections).
  • API Checks: Use functions like esp_wifi_get_mode() and esp_netif_get_ip_info() to query the driver and network interface status directly, although this is less efficient than reacting to events.
%%{ init: { 'state': { 'defaultRenderer': 'dagre' } } }%%
stateDiagram-v2
    direction TB

    [*] --> Idle

    state Idle {
        s0: Application not yet attempting to connect
    }
    Idle --> InitializingNVS : Start Application / Boot
    
    state InitializingNVS {
        s1: Initializing NVS, preparing to read credentials
    }
    InitializingNVS --> ReadingCreds : NVS Init OK
    InitializingNVS --> ErrorState : NVS Init Fail

    state ReadingCreds {
        s2: Attempting to read SSID/Pass from NVS
    }
    ReadingCreds --> NoCredentials : SSID or Pass not found / invalid
    ReadingCreds --> HaveCredentials : Credentials Found

    state NoCredentials {
        s3: No valid credentials in NVS
        [*] --> AwaitingProvisioning : Enter Provisioning Mode
        [*] --> UsingDefaults : Use Hardcoded Defaults (if any)
    }
    AwaitingProvisioning : (User action, e.g. SmartConfig)
    UsingDefaults --> HaveCredentials : Defaults Loaded

    state HaveCredentials {
        s4: Credentials loaded into RAM (wifi_config_t)
    }
    HaveCredentials --> InitializingWiFi : Proceed to WiFi Init

    state InitializingWiFi {
        s5: esp_netif_init, esp_event_loop_create, esp_wifi_init, esp_wifi_set_config
    }
    InitializingWiFi --> StartingWiFi : WiFi Stack Init OK
    InitializingWiFi --> ErrorState : WiFi Init Fail

    state StartingWiFi {
        s6: esp_wifi_start() called
    }
    StartingWiFi --> STA_Driver_Started : WIFI_EVENT_STA_START

    state STA_Driver_Started {
        s7: WiFi STA driver is active
    }
    STA_Driver_Started --> ConnectingToAP : esp_wifi_connect() called by handler

    state ConnectingToAP {
        s8: Actively trying to Auth/Assoc with AP
    }
    ConnectingToAP --> ConnectedMACLayer : WIFI_EVENT_STA_CONNECTED
    ConnectingToAP --> Disconnected_Retrying : Connection Attempt Failed (Retryable)
    ConnectingToAP --> Disconnected_NoRetry : Connection Attempt Failed (Non-Retryable)

    state ConnectedMACLayer {
        s9: Associated with AP (MAC Layer), no IP yet
        s10: Waiting for DHCP
    }
    ConnectedMACLayer --> ObtainingIP : DHCP process initiated
    ConnectedMACLayer --> Disconnected_Retrying : Disconnected (e.g. beacon timeout before IP)

    state ObtainingIP {
        s11: DHCP client running
    }
    ObtainingIP --> NetworkReady : IP_EVENT_STA_GOT_IP
    ObtainingIP --> Disconnected_Retrying : Disconnected during DHCP

    state NetworkReady {
        s12: Fully connected with IP address
        s13: Application can use network
    }
    NetworkReady --> Disconnected_Retrying : Connection Lost (Retryable)
    NetworkReady --> Disconnected_NoRetry : Connection Lost (Non-Retryable, e.g. explicit disconnect)


    state Disconnected_Retrying {
        s14: Disconnected, will attempt reconnection with backoff
    }
    Disconnected_Retrying --> ConnectingToAP : Retry attempt (esp_wifi_connect)
    Disconnected_Retrying --> Disconnected_NoRetry : Max Retries Reached

    state Disconnected_NoRetry {
        s15: Disconnected, no further automatic retries
        s16: May require user intervention or different logic
    }
    Disconnected_NoRetry --> AwaitingProvisioning : If creds issue
    Disconnected_NoRetry --> Idle : Or other app-specific state

    state ErrorState {
        s17: A critical error occurred (NVS, WiFi init)
    }

    %% Styling (using provided color scheme)
    %% Primary/Start Nodes: fill:#EDE9FE,stroke:#5B21B6
    %% End/Success Nodes: fill:#D1FAE5,stroke:#059669
    %% Decision Nodes: fill:#FEF3C7,stroke:#D97706 (Not directly applicable here, but states are like decisions)
    %% Process Nodes: fill:#DBEAFE,stroke:#2563EB
    %% Check/Validation Nodes: fill:#FEE2E2,stroke:#DC2626

    style Idle fill:#EDE9FE,stroke:#5B21B6,color:#5B21B6
    style InitializingNVS, ReadingCreds, InitializingWiFi, StartingWiFi, ConnectingToAP, ObtainingIP fill:#DBEAFE,stroke:#2563EB,color:#1E40AF
    style NoCredentials, AwaitingProvisioning, UsingDefaults fill:#FEF3C7,stroke:#D97706,color:#92400E
    style HaveCredentials, STA_Driver_Started, ConnectedMACLayer fill:#DBEAFE,stroke:#2563EB,color:#1E40AF
    style NetworkReady fill:#D1FAE5,stroke:#059669,color:#065F46
    style Disconnected_Retrying fill:#FEE2E2,stroke:#DC2626,color:#991B1B
    style Disconnected_NoRetry, ErrorState fill:#FECACA,stroke:#B91C1C,color:#7F1D1D

Practical Examples

Let’s enhance our previous WiFi station example to include NVS-based auto-connect and exponential backoff reconnection.

Prerequisites:

  • Your ESP-IDF v5.x environment with VS Code is set up.
  • You have the project structure from Chapter 28.
  • NVS partition is enabled in your sdkconfig (usually default).

Example 1: Auto-Connect using NVS

We’ll modify wifi_init_sta to read credentials from NVS before configuring and starting WiFi. We’ll also add a simple way (for this example) to store credentials if they aren’t found initially – in a real application, this would be replaced by a proper provisioning mechanism.

1. Add NVS Includes and Helper Keys:

C
// Add near other includes in main/your_main_file.c
#include "nvs_flash.h"
#include "nvs.h"

// Define keys for NVS storage
#define NVS_NAMESPACE "wifi_creds"
#define NVS_KEY_SSID  "ssid"
#define NVS_KEY_PASS  "password"

// Define default credentials (used only if NVS is empty for the first time)
// In a real app, get these via provisioning, not hardcoding defaults!
#define EXAMPLE_DEFAULT_WIFI_SSID      CONFIG_EXAMPLE_WIFI_SSID
#define EXAMPLE_DEFAULT_WIFI_PASSWORD  CONFIG_EXAMPLE_WIFI_PASSWORD

2. Modify wifi_init_sta:

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

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

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

    // 3. Create default WiFi station network interface (as before)
    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 (as before)
    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    ESP_ERROR_CHECK(esp_wifi_init(&cfg));

    // 5. Register event handlers (as before)
    esp_event_handler_instance_t instance_any_wifi_id;
    esp_event_handler_instance_t instance_got_ip;
    ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT,
                                                        ESP_EVENT_ANY_ID,
                                                        &event_handler, // Use the enhanced handler from Example 2 later
                                                        NULL,
                                                        &instance_any_wifi_id));
    ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT,
                                                        IP_EVENT_STA_GOT_IP,
                                                        &event_handler,
                                                        NULL,
                                                        &instance_got_ip));

    // --- NEW: NVS Handling ---
    wifi_config_t wifi_config;
    memset(&wifi_config, 0, sizeof(wifi_config_t));
    bool credentials_found = false;

    nvs_handle_t nvs_handle;
    esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READWRITE, &nvs_handle);
    if (err != ESP_OK) {
        ESP_LOGE(TAG, "Error (%s) opening NVS handle!", esp_err_to_name(err));
    } else {
        ESP_LOGI(TAG, "NVS handle opened successfully.");
        // Read SSID
        size_t required_size = sizeof(wifi_config.sta.ssid);
        err = nvs_get_str(nvs_handle, NVS_KEY_SSID, (char*)wifi_config.sta.ssid, &required_size);
        if (err == ESP_OK && required_size > 1) { // Check size > 1 to ensure not empty string
             ESP_LOGI(TAG, "Found SSID in NVS: %s", wifi_config.sta.ssid);
             // Read Password
             required_size = sizeof(wifi_config.sta.password);
             err = nvs_get_str(nvs_handle, NVS_KEY_PASS, (char*)wifi_config.sta.password, &required_size);
             if (err == ESP_OK && required_size > 1) {
                 ESP_LOGI(TAG, "Found Password in NVS.");
                 credentials_found = true;
             } else if (err == ESP_ERR_NVS_NOT_FOUND) {
                 ESP_LOGW(TAG, "Password not found in NVS.");
             } else {
                 ESP_LOGE(TAG, "Error (%s) reading password from NVS!", esp_err_to_name(err));
             }
        } else if (err == ESP_ERR_NVS_NOT_FOUND) {
            ESP_LOGW(TAG, "SSID not found in NVS.");
        } else {
            ESP_LOGE(TAG, "Error (%s) reading SSID from NVS!", esp_err_to_name(err));
        }

        // If credentials not found, store defaults (for example purposes)
        if (!credentials_found) {
            ESP_LOGW(TAG, "No valid credentials found in NVS. Storing defaults from Kconfig.");
            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);

            err = nvs_set_str(nvs_handle, NVS_KEY_SSID, (const char*)wifi_config.sta.ssid);
            if (err != ESP_OK) ESP_LOGE(TAG, "Failed to write SSID to NVS!");
            err = nvs_set_str(nvs_handle, NVS_KEY_PASS, (const char*)wifi_config.sta.password);
            if (err != ESP_OK) ESP_LOGE(TAG, "Failed to write Password to NVS!");

            err = nvs_commit(nvs_handle);
             if (err != ESP_OK) ESP_LOGE(TAG, "NVS commit failed!");
             else {
                 ESP_LOGI(TAG, "Default credentials stored in NVS.");
                 credentials_found = true; // Now we consider them found for the current run
             }
        }
        nvs_close(nvs_handle);
    }
    // --- End NVS Handling ---

    if (!credentials_found) {
        ESP_LOGE(TAG, "Failed to load or store WiFi credentials. Cannot connect.");
        // Optional: Signal failure immediately or enter a provisioning mode
        xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT);
        // Do not proceed to start WiFi if no credentials
        return;
    }

    // 6. Configure WiFi Station (using credentials from NVS or defaults)
    wifi_config.sta.threshold.authmode = WIFI_AUTH_WPA2_PSK; // Or make this configurable/stored too
    // wifi_config.sta.pmf_cfg.capable = true; // Optional PMF config

    // 7. Set WiFi Mode to Station
    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 with stored credentials...");
    ESP_ERROR_CHECK(esp_wifi_start());

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

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

    /* Interpret results (as before) */
    // ... (rest of the function remains the same) ...
}

// Ensure app_main initializes NVS
void app_main(void)
{
    ESP_LOGI(TAG, "Starting WiFi Auto-Connect Example...");

    // Initialize NVS (MUST be done before wifi_init_sta)
    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()); // Erase if NVS is corrupt or version changed
      ret = nvs_flash_init();
    }
    ESP_ERROR_CHECK(ret);

    // Ensure Kconfig options exist (SSID, Password, Max Retry)
    // ...

    // Initialize WiFi (which now includes NVS logic)
    wifi_init_sta();

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

    while(1) {
        // Main application loop
        vTaskDelay(pdMS_TO_TICKS(60000));
    }
}

3. Build, Flash, Monitor:

  • Run idf.py menuconfig and ensure Example Connection Configuration has your correct WiFi SSID and Password set. These will be used as defaults if NVS is empty.
  • idf.py build
  • idf.py flash
  • idf.py monitor

Expected Output (First Run):

You should see logs indicating that NVS was checked, credentials weren’t found (likely), defaults were stored, and then the connection proceeded using those defaults.

Plaintext
I (XXX) WIFI_FLOW: Starting WiFi Auto-Connect Example...
I (XXX) main: Initializing NVS
I (XXX) WIFI_FLOW: NVS handle opened successfully.
W (XXX) WIFI_FLOW: SSID not found in NVS.
W (XXX) WIFI_FLOW: No valid credentials found in NVS. Storing defaults from Kconfig.
I (XXX) WIFI_FLOW: Default credentials stored in NVS.
I (XXX) WIFI_FLOW: [Action] Starting WiFi Driver with stored credentials...
I (XXX) WIFI_FLOW: wifi_init_sta finished setup. Waiting for connection events...
I (XXX) WIFI_FLOW: Received Event: Base=WIFI_EVENT, ID=2
I (XXX) WIFI_FLOW: [State] WIFI_EVENT_STA_START: Station mode started.
I (XXX) WIFI_FLOW: [Action] Initiating connection attempt...
I (XXX) WIFI_FLOW: Received Event: Base=WIFI_EVENT, ID=4
I (XXX) WIFI_FLOW: [State] WIFI_EVENT_STA_CONNECTED: Associated with AP!
... (Connected logs as before) ...
I (XXX) WIFI_FLOW: Received Event: Base=IP_EVENT, ID=0
I (XXX) WIFI_FLOW: [State] IP_EVENT_STA_GOT_IP: Network Ready!
... (IP logs as before) ...
I (XXX) WIFI_FLOW: -------------------------------------------
I (XXX) WIFI_FLOW: WiFi Connected to AP!
I (XXX) WIFI_FLOW: -------------------------------------------
I (XXX) WIFI_FLOW: Post-connection phase. Application can proceed...

Expected Output (Second Run / After Reset):

Now, if you reset the ESP32 (press the RST button), it should find the credentials in NVS and connect directly without storing defaults.

Plaintext
I (XXX) WIFI_FLOW: Starting WiFi Auto-Connect Example...
I (XXX) main: Initializing NVS
I (XXX) WIFI_FLOW: NVS handle opened successfully.
I (XXX) WIFI_FLOW: Found SSID in NVS: YOUR_SSID
I (XXX) WIFI_FLOW: Found Password in NVS.
I (XXX) WIFI_FLOW: [Action] Starting WiFi Driver with stored credentials...
I (XXX) WIFI_FLOW: wifi_init_sta finished setup. Waiting for connection events...
// ... rest of connection flow ...

Example 2: Exponential Backoff Reconnection

Now, let’s modify the event_handler to implement exponential backoff.

1. Add Backoff Configuration and State:

C
// Add near other globals
#define EXAMPLE_ESP_MAXIMUM_RETRY  CONFIG_EXAMPLE_MAXIMUM_RETRY // From Kconfig
#define BASE_RETRY_DELAY_MS 1000    // Initial delay: 1 second
#define MAX_RETRY_DELAY_MS  60000   // Maximum delay: 60 seconds

static int s_retry_num = 0;
static uint32_t s_retry_delay_ms = BASE_RETRY_DELAY_MS; // Current delay

2. Modify event_handler (specifically WIFI_EVENT_STA_DISCONNECTED case):

C
/* --- Event Handler --- */
static void event_handler(void* arg, esp_event_base_t event_base,
                                int32_t event_id, void* event_data)
{
    ESP_LOGD(TAG, "Received Event: Base=%s, ID=%ld", event_base, event_id); // Use Debug level for less noise

    // --- WIFI_EVENT handling ---
    if (event_base == WIFI_EVENT) {
        switch (event_id) {
            case WIFI_EVENT_STA_START:
                ESP_LOGI(TAG, "[State] WIFI_EVENT_STA_START: Station mode started.");
                ESP_LOGI(TAG, "[Action] 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, "[State] WIFI_EVENT_STA_CONNECTED: Associated with AP!");
                    ESP_LOGI(TAG, "  SSID: %.*s, BSSID: " MACSTR ", Channel: %d, AuthMode: %d",
                             event->ssid_len, event->ssid, MAC2STR(event->bssid), event->channel, event->authmode);
                    ESP_LOGI(TAG, "[Next Step] Waiting for IP address via DHCP...");
                    // Reset retry delay on successful MAC connection as well (optional, good practice)
                    s_retry_delay_ms = BASE_RETRY_DELAY_MS;
                }
                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); // Use helper from Ch 28
                    ESP_LOGW(TAG, "[State] WIFI_EVENT_STA_DISCONNECTED: Lost connection.");
                    ESP_LOGW(TAG, "  Reason Code: %d (%s)", event->reason, reason_str);
                    // Log SSID/BSSID only if available (might be NULL if never connected)
                    if (event->ssid_len > 0) {
                         ESP_LOGW(TAG, "  SSID: %.*s, BSSID: " MACSTR, event->ssid_len, event->ssid, MAC2STR(event->bssid));
                    }

                    // Decide whether to retry based on reason code (same logic as Ch 28)
                    bool retry = false;
                    switch(event->reason) {
                        // Retryable reasons
                        case WIFI_REASON_BEACON_TIMEOUT:
                        case WIFI_REASON_ASSOC_EXPIRE:
                        case WIFI_REASON_AUTH_EXPIRE:
                        case WIFI_REASON_HANDSHAKE_TIMEOUT: // Can be password, but also temporary
                        case WIFI_REASON_CONNECTION_FAIL:
                        case WIFI_REASON_AP_TSF_RESET:
                        case WIFI_REASON_ROAMING:
                        case WIFI_REASON_ASSOC_TOOMANY: // AP full, retry later
                            retry = true;
                            break;
                        // Non-retryable reasons (configuration/security issues)
                        case WIFI_REASON_AUTH_FAIL:
                        case WIFI_REASON_ASSOC_FAIL:
                        case WIFI_REASON_NO_AP_FOUND:
                        case WIFI_REASON_MIC_FAILURE:
                        case WIFI_REASON_4WAY_HANDSHAKE_TIMEOUT: // More likely password issue
                        // ... other non-retryable reasons ...
                            ESP_LOGE(TAG, "Connection failed due to non-temporary issue (%s). Not retrying automatically.", reason_str);
                            retry = false;
                            break;
                        default:
                           ESP_LOGW(TAG, "Unhandled disconnect reason: %d. Retrying by default.", event->reason);
                           retry = true; // Default to retry for unknown reasons
                           break;
                    }

                    if (retry && s_retry_num < EXAMPLE_ESP_MAXIMUM_RETRY) {
                        ESP_LOGI(TAG, "[Action] Retrying connection (%d/%d) after %" PRIu32 " ms delay...",
                                 s_retry_num + 1, EXAMPLE_ESP_MAXIMUM_RETRY, s_retry_delay_ms);

                        // Use vTaskDelay for the backoff period
                        // IMPORTANT: This blocks the event handler task!
                        // For very long delays or complex logic, consider signaling a separate task.
                        // For typical backoff delays (up to ~60s), this is often acceptable.
                        vTaskDelay(pdMS_TO_TICKS(s_retry_delay_ms));

                        // Increase delay for the *next* potential failure (Exponential Backoff)
                        s_retry_delay_ms *= 2;
                        if (s_retry_delay_ms > MAX_RETRY_DELAY_MS) {
                            s_retry_delay_ms = MAX_RETRY_DELAY_MS; // Cap the delay
                        }

                        s_retry_num++;
                        esp_err_t connect_err = esp_wifi_connect();
                        if (connect_err != ESP_OK) {
                             ESP_LOGE(TAG, "esp_wifi_connect failed: %s", esp_err_to_name(connect_err));
                             // Handle connect initiation failure (maybe signal fail bit)
                              if (s_wifi_event_group) {
                                xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT);
                              }
                        } else {
                            ESP_LOGI(TAG, "Connection attempt initiated.");
                        }

                    } else if (retry) { // Retries exhausted
                         ESP_LOGE(TAG, "Connection failed after %d retries.", EXAMPLE_ESP_MAXIMUM_RETRY);
                         s_retry_num = 0; // Reset counter for potential future manual connect
                         s_retry_delay_ms = BASE_RETRY_DELAY_MS; // Reset delay
                         if (s_wifi_event_group) {
                            xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT);
                         }
                    } else { // Non-retryable error
                         s_retry_num = 0; // Reset counter
                         s_retry_delay_ms = BASE_RETRY_DELAY_MS; // Reset delay
                         if (s_wifi_event_group) {
                            xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT);
                         }
                    }
                }
                break;

            default:
                ESP_LOGD(TAG, "Unhandled WIFI_EVENT: %ld", event_id);
                break;
        }
    }
    // --- IP_EVENT handling ---
    else if (event_base == IP_EVENT) {
        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, "[State] IP_EVENT_STA_GOT_IP: Network Ready!");
                    ESP_LOGI(TAG, "  Assigned IP : " IPSTR ", Gateway: " IPSTR ", Netmask: " IPSTR,
                             IP2STR(&event->ip_info.ip), IP2STR(&event->ip_info.gw), IP2STR(&event->ip_info.netmask));

                    // Reset retry counter and delay on successful connection
                    ESP_LOGI(TAG, "Resetting WiFi retry counter and delay.");
                    s_retry_num = 0;
                    s_retry_delay_ms = BASE_RETRY_DELAY_MS;

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

            default:
                ESP_LOGD(TAG, "Unhandled IP_EVENT: %ld", event_id);
                break;
        }
    }
    // --- Other event bases ---
    else {
        ESP_LOGW(TAG, "Received event from unknown base: %s", event_base);
    }
}

3. Build, Flash, Monitor:

  • idf.py build
  • idf.py flash
  • idf.py monitor

Test Disconnection:

  • Connect successfully first.
  • Turn off your WiFi Access Point.
  • Observe the logs. You should see WIFI_EVENT_STA_DISCONNECTED with a reason like BEACON_TIMEOUT.
  • The device should then log retry attempts with increasing delays:
    • Retry 1 after 1000 ms
    • Retry 2 after 2000 ms
    • Retry 3 after 4000 ms
    • Retry 4 after 8000 ms
    • … up to EXAMPLE_ESP_MAXIMUM_RETRY attempts or until the delay hits MAX_RETRY_DELAY_MS.
  • If you turn the AP back on during the retries, it should eventually reconnect, log IP_EVENT_STA_GOT_IP, and reset the retry counter/delay.
  • If it exhausts retries, it should log the failure and set the WIFI_FAIL_BIT.

ESP32 Auto-Connect & Reconnection Simulator

Simulates NVS credential handling and exponential backoff on disconnect.

Status: Idle

Simulation log will appear here…

Variant Notes

  • ESP32, ESP32-S2, ESP32-S3, ESP32-C3, ESP32-C6: The NVS library, WiFi event system, and the concepts of auto-connect and exponential backoff described in this chapter are fully applicable and work consistently across these WiFi-enabled variants using ESP-IDF v5.x. The core APIs (nvs_*, esp_wifi_*, esp_event_*) are the same.
  • ESP32-H2: This chapter is not applicable to the ESP32-H2 as it does not possess 802.11 WiFi hardware capabilities. It focuses on 802.15.4 (Thread/Zigbee) and Bluetooth LE.

Common Mistakes & Troubleshooting Tips

Mistake / Issue Symptom(s) Solution / Best Practice
Forgetting NVS Initialization nvs_open() returns ESP_ERR_NVS_NOT_INITIALIZED. Device fails to read/write credentials, always uses defaults or fails to connect. Ensure nvs_flash_init() is called successfully (with error checking and potential erase) in app_main() before any NVS access.
NVS Key/Namespace Mismatch Credentials seem to save but are not found on read (ESP_ERR_NVS_NOT_FOUND). Device might revert to defaults or fail. Use #define constants for NVS keys and namespaces. Double-check spelling and case-sensitivity.
Forgetting nvs_commit() Credentials written with nvs_set_str() are not persisted across reboots if nvs_commit() isn’t called before nvs_close(). Always call nvs_commit(nvs_handle) after writing data to NVS if immediate persistence is required.
Incorrect Backoff Reset After a successful reconnection (post-failures), the next disconnect might immediately use a long backoff delay or an incorrect retry count. Always reset retry counter and backoff delay to initial values in the IP_EVENT_STA_GOT_IP handler.
Blocking Event Handler Excessively Using very long vTaskDelay() in the event handler for backoff, or performing other lengthy blocking operations. System unresponsiveness, delayed event processing, watchdog timeouts. For very long backoffs or complex logic, signal a separate task. For typical WiFi backoffs (seconds to a minute), vTaskDelay() in the handler is often acceptable.
Not Handling NVS Read/Write Errors Ignoring return codes from NVS functions. Application might use uninitialized or garbage data for credentials. Check all esp_err_t return values from NVS operations. Handle errors gracefully (log, use defaults, enter provisioning).
Insufficient Buffer Size for NVS Read Providing too small a buffer to nvs_get_str(). SSID/password might be truncated or cause buffer overflows if not handled carefully with required_size. Ensure buffer sizes match NVS schema (e.g., sizeof(wifi_config.sta.ssid)). Check required_size after nvs_get_str() if dynamic allocation is used, or ensure fixed buffers are large enough.
No Provisioning for Empty NVS If NVS is empty and no default/fallback credentials are provided, the device cannot connect on first boot or after NVS erase. Implement a provisioning mechanism (e.g., SmartConfig, BLE, Web UI) or at least provide compile-time defaults that can be written to NVS if it’s empty.

Exercises

  1. Backoff Delay Cap: Verify that the MAX_RETRY_DELAY_MS cap in Example 2 works. Set MAX_RETRY_DELAY_MS to a shorter value (e.g., 5000 ms) and EXAMPLE_ESP_MAXIMUM_RETRY to a higher value (e.g., 10). Observe during a disconnection test that the delay between retries stops increasing once it hits the cap.
  2. Clear NVS Credentials: Add a mechanism to clear the stored WiFi credentials from NVS. For example, check the state of a specific GPIO pin connected to a button during startup. If the button is held down, erase the "ssid" and "password" keys from the "wifi_creds" namespace in NVS. This provides a way to force the device back into a “needs provisioning” state. (Hint: Use nvs_erase_key).
  3. Connection Monitoring Task: Create a separate FreeRTOS task (wifi_monitor_task). This task should periodically (e.g., every 30 seconds) check the WiFi connection status. It could use the event group bits (s_wifi_event_group) or query esp_netif_get_ip_info for the station interface. Log the current status (e.g., “Connected with IP”, “Connecting”, “Disconnected”). This demonstrates decoupling monitoring from the main application flow.
  4. Store Auth Mode: Modify the NVS storage logic to also save the WiFi authentication mode (wifi_config.sta.threshold.authmode). Read it back along with the SSID and password in wifi_init_sta and use it when setting the configuration with esp_wifi_set_config. This makes the auto-connect more flexible for different network types. (Hint: Use nvs_set_u8 / nvs_get_u8 for the enum value).

Summary

  • Robust IoT devices require automatic WiFi connection on startup and resilient reconnection strategies.
  • Non-Volatile Storage (NVS) is essential for persistently storing WiFi credentials (SSID, password) across reboots.
  • Auto-connect logic involves reading credentials from NVS during initialization and attempting connection if found.
  • Simple retry loops on disconnection can be inefficient and flood the network.
  • Exponential backoff is a superior reconnection strategy that increases the delay between retries, saving power and reducing network load during outages.
  • The backoff delay should be capped, and the retry mechanism should be reset upon successful connection (IP_EVENT_STA_GOT_IP).
  • Careful handling of NVS operations (initialization, error checking, commits) and event handler design (avoiding excessive blocking) is crucial.

Further Reading

Leave a Comment

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

Scroll to Top