Chapter 36: Managing Multiple WiFi Networks

Chapter Objectives

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

  • Understand the concept of WiFi network profiles.
  • Store multiple WiFi network credentials securely using the Non-Volatile Storage (NVS) library.
  • Retrieve stored WiFi credentials from NVS.
  • Implement logic to scan for available networks and match them against stored profiles.
  • Develop a strategy to prioritize and attempt connections to known networks.
  • Manage the connection process for multiple potential WiFi networks.

Introduction

In previous chapters, we learned how to connect the ESP32 to a single, predefined WiFi network. However, many real-world IoT devices need to operate in different environments or have fallback options. Imagine a portable sensor device that needs to connect to WiFi at home, in the office, or perhaps a mobile hotspot. Manually reconfiguring the WiFi credentials each time is impractical.

This chapter addresses this challenge by introducing techniques for managing multiple WiFi network profiles. We will leverage the ESP-IDF’s Non-Volatile Storage (NVS) system to persistently store credentials for several networks. We will then develop logic that allows the ESP32 to automatically scan for available networks, check if any match the stored profiles, and attempt to connect based on a defined prioritization scheme. This capability significantly enhances the flexibility and usability of connected ESP32 applications.

Theory

Network Profiles

A WiFi network profile is essentially a collection of information required to connect to a specific wireless network. At a minimum, this includes:

  • SSID (Service Set Identifier): The name of the WiFi network.
  • Password (Pre-Shared Key): The key required for authentication (for secured networks like WPA2/WPA3-Personal).
  • Security Type: The authentication method used by the network (e.g., Open, WEP, WPA/WPA2-Personal, WPA3-Personal, WPA2/WPA3 Enterprise).

For robust multi-network management, we might also want to store additional metadata, such as:

  • Priority: A numerical value indicating the preference for this network.
  • Last Connection Status: Whether the last attempt to connect to this network was successful.
  • Last Connection Timestamp: When the device last connected to this network.
Parameter Description Data Type / Example
SSID (Service Set Identifier) The name of the WiFi network. This is the human-readable name you see when searching for networks. String (up to 32 chars), e.g., "MyHomeWiFi", "OfficeGuest"
Password (Pre-Shared Key) The key required for authentication on secured networks. String (up to 64 chars for WPA2-PSK), e.g., "P@$$wOrd123!"
Security Type The authentication and encryption method used by the network. Enum (wifi_auth_mode_t), e.g., WIFI_AUTH_OPEN, WIFI_AUTH_WPA2_PSK, WIFI_AUTH_WPA3_PSK
Priority (Optional) A numerical value indicating the preference for this network when multiple known networks are available. Lower numbers might indicate higher priority. Integer, e.g., 0 (highest), 1, 2
Last Connection Status (Optional) Indicates if the last attempt to connect to this network was successful. Useful for “last known good” strategies. Boolean or Enum, e.g., true/false, STATUS_SUCCESS
Last Connection Timestamp (Optional) Records when the device last successfully connected to this network. Useful for prioritizing recently used networks. Timestamp (e.g., Unix time, time_t), e.g., 1678886400

Storing Profiles: Non-Volatile Storage (NVS)

Since WiFi credentials need to persist across device reboots and power cycles, we must store them in non-volatile memory. The ESP-IDF provides the Non-Volatile Storage (NVS) library specifically for this purpose. As discussed in Chapter 22, NVS allows you to store data in a key-value format within a dedicated partition in the ESP32’s flash memory.

NVS Concept Purpose in WiFi Profile Management Example Usage
Namespace A logical grouping for all WiFi credential-related keys. This prevents naming conflicts if other parts of the application also use NVS. "wifi_creds" or "user_networks"
Key A unique string identifier for each stored network profile or related metadata within the namespace. Profile 0: "net_0"
Profile 1: "net_1"
Network Count: "net_count"
Value The actual data associated with a key. For WiFi profiles, this is typically a binary blob representing the stored_wifi_config_t structure. A struct stored_wifi_config_t containing SSID and password, stored using nvs_set_blob().
NVS Handle An opaque pointer (nvs_handle_t) obtained by opening a namespace. Used in subsequent NVS operations (read, write, commit). Obtained via nvs_open("wifi_creds", NVS_READWRITE, &my_handle).
Commit The action of writing any changes made (set, erase) from RAM to the underlying flash storage, ensuring persistence. Called via nvs_commit(my_handle) after writing data.

Key NVS Concepts:

  • Namespace: A logical grouping for related keys. Helps avoid key collisions between different software components.
  • Key: A unique string identifier for a piece of data within a namespace.
  • Value: The data associated with a key. Can be various primitive types (integers, strings) or binary blobs (arbitrary blocks of data).

To manage multiple network profiles, a common approach is to:

  1. Define a C structure to hold the essential information for one network profile (e.g., SSID, password).
  2. Store each profile as a binary blob (nvs_set_blob) under a unique key within a dedicated namespace (e.g., namespace “wifi_creds”, keys “net_0”, “net_1”, “net_2”, …).
  3. Optionally, store a separate key (e.g., “net_count”) to keep track of how many profiles are currently saved.
C
// Example structure for storing credentials in NVS
typedef struct {
    uint8_t ssid[33];     // Max SSID length 32 + null terminator
    uint8_t password[65]; // Max password length 64 + null terminator
    // We could add wifi_auth_mode_t auth_mode; if needed
} stored_wifi_config_t;

Prioritization Strategies

When multiple known networks are available, the device needs a strategy to decide which one to connect to. Common strategies include:

  1. Order-Based: Store profiles in a specific order of preference. The device attempts to connect to network 0, then network 1, and so on. The first one found and successfully connected to is used. This is simple to implement.
  2. Signal Strength (RSSI)-Based: Scan for all available networks. Compare the SSIDs of found networks against the stored profiles. Among the matched networks, attempt to connect to the one with the strongest signal (highest RSSI value – remember RSSI is negative, so closer to 0 is stronger). This often provides the most stable connection but requires a scan before every connection attempt sequence.
  3. Last Successfully Connected: Store the profile index or SSID of the network the device was last connected to. Prioritize attempting connection to this network first on the next boot or reconnection attempt. This speeds up reconnection if the device is usually in the same location.
  4. Combination: A mix of the above, e.g., try the last successful network first, then revert to an order-based or RSSI-based approach.
%%{ init: { 'theme': 'base', 'themeVariables': {
  'fontFamily': 'Open Sans, sans-serif',
  'primaryColor': '#DBEAFE',
  'primaryTextColor': '#1E40AF',
  'primaryBorderColor': '#2563EB',
  'lineColor': '#4B5563',
  'textColor': '#1F2937',
  'mainBkg': 'transparent'
}} }%%
graph TD
    Start(["Start: Matched Available Networks List"]) --> StrategyChoice{Choose Strategy};

    StrategyChoice -- "Order-Based" --> OB1["Iterate through Matched Networks<br><b>in Stored Order</b> (e.g., index 0, 1, 2...)"]:::processNode;
    OB1 --> OB2{"Select Next Network<br>in Predefined Order"}:::decisionNode;
    OB2 -- "Network Available" --> OB_Attempt["Attempt Connection"]:::processNode;
    OB_Attempt --> OB_Outcome{Connected?}:::decisionNode;
    OB_Outcome -- "Yes" --> Success(["Connected!"]):::successNode;
    OB_Outcome -- "No" --> OB2;


    StrategyChoice -- "RSSI-Based" --> RB1["Perform Full WiFi Scan<br>(if not already done or outdated)"]:::processNode;
    RB1 --> RB2["Identify Matched Networks<br>from Scan Results & Stored Profiles"]:::processNode;
    RB2 --> RB3["Sort Matched Networks by<br><b>RSSI (Strongest First)</b>"]:::processNode;
    RB3 --> RB4{"Select Network with<br>Highest Current RSSI"}:::decisionNode;
    RB4 -- "Network Available" --> RB_Attempt["Attempt Connection"]:::processNode;
    RB_Attempt --> RB_Outcome{Connected?}:::decisionNode;
    RB_Outcome -- "Yes" --> Success;
    RB_Outcome -- "No" --> RB4;


    classDef startNode fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6;
    classDef successNode fill:#D1FAE5,stroke:#059669,stroke-width:2px,color:#065F46;
    classDef decisionNode fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E;
    classDef processNode fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF;
    classDef checkNode fill:#FEE2E2,stroke:#DC2626,stroke-width:1px,color:#991B1B;

    class Start startNode;
    class StrategyChoice decisionNode;

Here is the comparison table for prioritization strategies:

Strategy Description Pros Cons Typical Use Case
Order-Based Profiles are stored in a predefined order of preference (e.g., index 0 is highest priority). The device attempts connections sequentially. Simple to implement; predictable behavior. Not adaptive to signal strength changes; might repeatedly try a high-priority but unavailable network. Devices with a fixed set of preferred networks where order is static.
Signal Strength (RSSI)-Based Scans for available networks, compares found SSIDs with stored profiles, then connects to the matched network with the strongest signal (highest RSSI). Often results in the most stable connection; adaptive to current RF environment. Requires a WiFi scan before each connection sequence, which consumes time and power; more complex to implement. Mobile devices or devices operating in areas with multiple known APs of varying signal quality.
Last Successfully Connected Prioritizes the network to which the device was last successfully connected. If that fails, falls back to another strategy. Fast reconnection if the device is usually in the same location; user-friendly. May not be optimal if the last network is temporarily down or the device moves; requires storing the last connected network’s identifier. Devices that frequently connect to the same primary network.
Combination Strategy Blends multiple strategies. For example, try last successful first, then RSSI-based among other known networks, or order-based as a final fallback. Can offer the benefits of multiple approaches; highly flexible. Most complex to implement and debug; logic can become intricate. Sophisticated IoT devices requiring robust and adaptable connectivity.

Connection Logic Flow

A typical process for connecting to one of multiple stored networks involves these steps:

  1. Initialize NVS: Ensure the NVS system is ready.
  2. Load Profiles: Read all stored network profiles from NVS into RAM.
  3. Scan for Networks: Perform a WiFi scan to discover currently available networks (Chapter 34).
  4. Match Profiles: Compare the SSIDs from the scan results with the SSIDs in the loaded profiles.
  5. Prioritize: Apply the chosen prioritization strategy (e.g., order-based, RSSI-based) to the list of matched, available networks.
  6. Attempt Connection: Iterate through the prioritized list:
    • Configure the WiFi station with the credentials of the current candidate network (esp_wifi_set_config).
    • Initiate the connection (esp_wifi_connect).
    • Wait for a connection event (WIFI_EVENT_STA_CONNECTED, IP_EVENT_STA_GOT_IP) or a failure event (WIFI_EVENT_STA_DISCONNECTED).
  7. Handle Outcome:
    • Success: The device is connected. Store this as the “last successful” network if using that strategy. Stop the connection attempts.
    • Failure: Log the failure. Move to the next network in the prioritized list and repeat step 6.
  8. No Connection: If all prioritized networks fail, the device might enter a retry loop (e.g., wait a while and restart the scan/connect process) or signal a connection failure state.
%%{ init: { 'theme': 'base', 'themeVariables': {
  'fontFamily': 'Open Sans, sans-serif',
  'primaryColor': '#DBEAFE',      /* Process Nodes BG */
  'primaryTextColor': '#1E40AF', /* Process Nodes Text */
  'primaryBorderColor': '#2563EB',/* Process Nodes Border */
  'lineColor': '#4B5563',        /* Arrow color */
  'textColor': '#1F2937',        /* Default text color for labels */
  'mainBkg': 'transparent'       /* Diagram background */
}} }%%
graph TD
    A[/"Initialize NVS<br>(<code>nvs_flash_init</code>)"/]:::startNode --> B{"Load Stored WiFi Profiles<br>from NVS into RAM"};
    B --> C["Perform WiFi Scan<br>(<code>esp_wifi_scan_start</code>)"];
    C --> D{"Match Scan Results<br>with Loaded Profiles (SSIDs)"};
    D -- "No Matches Found" --> D_NO["Handle No Matches<br>(e.g., Retry Scan, Error State)"]:::checkNode;
    D -- "Matches Found" --> E{"Prioritize Matched &<br>Available Networks<br>(e.g., Order-based, RSSI)"};
    E --> F{"Attempt Connection to<br>Highest Priority Network<br>(<code>esp_wifi_set_config</code>, <code>esp_wifi_connect</code>)"};
    F --> G{"Connection Successful?<br>(Got IP Event)"};
    G -- "Yes" --> H[/"WiFi Connected!<br>Exit Logic"/]:::successNode;
    G -- "No (Fail/Timeout)" --> I{Any More Networks<br>in Prioritized List?};
    I -- "Yes" --> F;
    I -- "No" --> J["All Attempts Failed<br>Handle Failure<br>(e.g., Retry Cycle, Deep Sleep, Error State)"]:::checkNode;

    classDef startNode fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6;
    classDef successNode fill:#D1FAE5,stroke:#059669,stroke-width:2px,color:#065F46;
    classDef decisionNode fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E;
    classDef processNode fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF;
    classDef checkNode fill:#FEE2E2,stroke:#DC2626,stroke-width:1px,color:#991B1B;

    class A,B,C,D,E,F,I,J processNode;
    class G decisionNode;
    class H successNode;
    class D_NO checkNode;

Practical Examples

Let’s implement a system that stores up to 5 network profiles in NVS and uses an order-based prioritization strategy.

1. Project Setup

  • Create a new ESP-IDF project in VS Code or copy an existing basic WiFi station example.
  • Ensure CONFIG_ESP_WIFI_ENABLED=y and CONFIG_NVS_ENABLED=y in sdkconfig.
  • Add necessary component dependencies in CMakeLists.txt (if not already present): nvs_flash, esp_wifi, esp_event, esp_netif, esp_log.
CMake
# CMakeLists.txt (inside main component)
idf_component_register(SRCS "main.c"
                    INCLUDE_DIRS "."
                    REQUIRES nvs_flash esp_wifi esp_event esp_netif esp_log freertos)

2. Storing and Loading Profiles (NVS Operations)

We’ll create helper functions to save and load our stored_wifi_config_t structures.

%%{ init: { 'theme': 'base', 'themeVariables': {
  'fontFamily': 'Open Sans, sans-serif',
  'primaryColor': '#DBEAFE',
  'primaryTextColor': '#1E40AF',
  'primaryBorderColor': '#2563EB',
  'lineColor': '#4B5563',
  'textColor': '#1F2937',
  'mainBkg': 'transparent'
}} }%%
graph LR
    subgraph NVS_Partition ["NVS Flash Partition"]
        direction LR
        subgraph Namespace_WiFiCreds ["Namespace: wifi_creds"]
            direction TB
            Key_Net0["Key: \<i>net_0\</i><br>(stored_wifi_config_t blob for Profile 0)"]
            Key_Net1["Key: \<i>net_1\</i><br>(stored_wifi_config_t blob for Profile 1)"]
            Key_NetX["Key: \<i>net_X\</i><br>(...)"]
            Key_NetCount["Key: \<i>net_count\</i><br>(Optional: integer, total stored profiles)"]
        end
        subgraph Namespace_OtherApp ["Namespace: \"other_app_data\""]
             Key_Other1["Key: \"setting_A\""]
             Key_Other2["Key: \"status_B\""]
        end
    end

    Key_Net0 -.-> Value_Profile0(("SSID: <i>HomeNet</i><br>Pass: <i>password123</i>"))
    Key_Net1 -.-> Value_Profile1(("SSID: <i>OfficeWiFi</i><br>Pass: <i>securePass</i>"))

    classDef startNode fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6
    classDef processNode fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF
    classDef otherNamespace fill:#E0E7FF,stroke:#4338CA,stroke-width:1px,color:#3730A3
    classDef otherData fill:#F3F4F6,stroke:#9CA3AF,stroke-width:1px,color:#4B5563

    %% Apply classes
    class Namespace_WiFiCreds startNode
    class Key_Net0,Key_Net1,Key_NetX,Key_NetCount processNode
    class Namespace_OtherApp otherNamespace
    class Key_Other1,Key_Other2 otherData

    %% Style value nodes
    style Value_Profile0 fill:#F0FFF4,stroke:#2F855A,color:#2F855A,stroke-width:1px
    style Value_Profile1 fill:#F0FFF4,stroke:#2F855A,color:#2F855A,stroke-width:1px
C
// main.c
#include <stdio.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"
#include "esp_system.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_log.h"
#include "nvs_flash.h"
#include "esp_netif.h"

static const char *TAG = "WIFI_MULTI";
static const char *NVS_NAMESPACE = "wifi_creds";

#define MAX_STORED_NETWORKS 5

// Structure for storing credentials
typedef struct {
    uint8_t ssid[33];     // Max SSID length 32 + null terminator
    uint8_t password[65]; // Max password length 64 + null terminator
    // Add wifi_auth_mode_t auth_mode if needed for different security types
} stored_wifi_config_t;

// --- NVS Helper Functions ---

// Function to save a network profile to NVS at a specific index
esp_err_t save_wifi_config_to_nvs(uint8_t index, const stored_wifi_config_t *config) {
    if (index >= MAX_STORED_NETWORKS) {
        ESP_LOGE(TAG, "Index %d out of bounds (max %d)", index, MAX_STORED_NETWORKS - 1);
        return ESP_ERR_INVALID_ARG;
    }

    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));
        return err;
    }

    char key[10]; // "net_X" + null
    snprintf(key, sizeof(key), "net_%d", index);

    err = nvs_set_blob(nvs_handle, key, config, sizeof(stored_wifi_config_t));
    if (err != ESP_OK) {
        ESP_LOGE(TAG, "Error (%s) writing NVS key %s", esp_err_to_name(err), key);
    } else {
        ESP_LOGI(TAG, "Saved config for %s to NVS key %s", config->ssid, key);
        err = nvs_commit(nvs_handle); // Commit changes
        if (err != ESP_OK) {
            ESP_LOGE(TAG, "NVS commit failed!");
        }
    }

    nvs_close(nvs_handle);
    return err;
}

// Function to load a network profile from NVS from a specific index
esp_err_t load_wifi_config_from_nvs(uint8_t index, stored_wifi_config_t *config) {
     if (index >= MAX_STORED_NETWORKS) {
        ESP_LOGE(TAG, "Index %d out of bounds (max %d)", index, MAX_STORED_NETWORKS - 1);
        return ESP_ERR_INVALID_ARG;
    }

    nvs_handle_t nvs_handle;
    esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READONLY, &nvs_handle);
     if (err != ESP_OK) {
        // If namespace doesn't exist yet, it's not an error, just means no creds saved
        if (err == ESP_ERR_NVS_NOT_FOUND) {
             ESP_LOGI(TAG, "NVS namespace '%s' not found. No credentials saved yet.", NVS_NAMESPACE);
             return ESP_ERR_NVS_NOT_FOUND; // Indicate nothing loaded
        }
        ESP_LOGE(TAG, "Error (%s) opening NVS handle!", esp_err_to_name(err));
        return err;
    }

    char key[10];
    snprintf(key, sizeof(key), "net_%d", index);

    size_t required_size = sizeof(stored_wifi_config_t);
    err = nvs_get_blob(nvs_handle, key, config, &required_size);

    if (err == ESP_OK) {
        if (required_size != sizeof(stored_wifi_config_t)) {
             ESP_LOGW(TAG, "NVS blob size mismatch for key %s. Expected %d, got %d.", key, sizeof(stored_wifi_config_t), required_size);
             // Handle potential data corruption or version mismatch if needed
             err = ESP_FAIL; // Treat as error
        } else {
            ESP_LOGI(TAG, "Loaded config for %s from NVS key %s", config->ssid, key);
        }
    } else if (err == ESP_ERR_NVS_NOT_FOUND) {
        ESP_LOGI(TAG, "NVS key %s not found.", key);
        // Not necessarily an error, just means this index is empty
    } else {
        ESP_LOGE(TAG, "Error (%s) reading NVS key %s", esp_err_to_name(err), key);
    }

    nvs_close(nvs_handle);
    return err;
}

// --- Example Usage: Saving some dummy credentials ---
void save_dummy_credentials(void) {
    stored_wifi_config_t net0 = {0};
    strncpy((char*)net0.ssid, "MyHomeNetwork", sizeof(net0.ssid)-1);
    strncpy((char*)net0.password, "HomePassword123", sizeof(net0.password)-1);
    save_wifi_config_to_nvs(0, &net0);

    stored_wifi_config_t net1 = {0};
    strncpy((char*)net1.ssid, "MyOfficeNetwork", sizeof(net1.ssid)-1);
    strncpy((char*)net1.password, "OfficeKeySecure", sizeof(net1.password)-1);
    save_wifi_config_to_nvs(1, &net1);

    // Add more networks up to MAX_STORED_NETWORKS-1 if desired
    ESP_LOGI(TAG, "Dummy credentials saved (run this once or manage via other means).");
}

Tip: You would typically run save_dummy_credentials() once during development or provide a user interface (e.g., via BLE, a web server, or UART commands) to allow users to add/manage networks in a real product. For this example, we’ll call it once in app_main for demonstration, but you should remove or guard this call in production firmware unless it’s part of a first-time setup routine.

3. WiFi Initialization and Event Handling

This part is similar to previous chapters, setting up the event loop and WiFi station mode.

C
// main.c (continued)

// Event group to signal WiFi connection status
static EventGroupHandle_t s_wifi_event_group;
#define WIFI_CONNECTED_BIT BIT0
#define WIFI_FAIL_BIT      BIT1

// WiFi Event Handler
static void wifi_event_handler(void* arg, esp_event_base_t event_base,
                               int32_t event_id, void* event_data)
{
    if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) {
        ESP_LOGI(TAG, "WIFI_EVENT_STA_START: Initiating connection scan...");
        // Start the connection logic (which includes scanning)
        // We will call a dedicated function for this later
    } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
        wifi_event_sta_disconnected_t* event = (wifi_event_sta_disconnected_t*) event_data;
        ESP_LOGW(TAG, "WIFI_EVENT_STA_DISCONNECTED: Reason %d. Setting FAIL bit.", event->reason);
        xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT);
        // Optionally trigger a retry mechanism here or in the main connection logic
    } else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
        ip_event_got_ip_t* event = (ip_event_got_ip_t*) event_data;
        ESP_LOGI(TAG, "IP_EVENT_STA_GOT_IP: Got IP:" IPSTR, IP2STR(&event->ip_info.ip));
        xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT);
    }
}

// Initialize WiFi Station
static void wifi_init_sta(void) {
    s_wifi_event_group = xEventGroupCreate();

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

    ESP_ERROR_CHECK(esp_event_loop_create_default()); // Create default event loop
    esp_netif_create_default_wifi_sta(); // Create default STA network interface

    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); // Default config
    ESP_ERROR_CHECK(esp_wifi_init(&cfg)); // Initialize WiFi driver

    // Register event handlers
    esp_event_handler_instance_t instance_any_id;
    esp_event_handler_instance_t instance_got_ip;
    ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT,
                                                        ESP_EVENT_ANY_ID,
                                                        &wifi_event_handler,
                                                        NULL,
                                                        &instance_any_id));
    ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT,
                                                        IP_EVENT_STA_GOT_IP,
                                                        &wifi_event_handler,
                                                        NULL,
                                                        &instance_got_ip));

    // Set WiFi mode to Station
    ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));

    // Start WiFi driver tasks
    ESP_ERROR_CHECK(esp_wifi_start());

    ESP_LOGI(TAG, "wifi_init_sta finished.");
    // Connection attempts will be triggered by WIFI_EVENT_STA_START event
}

4. Connection Management Logic (Order-Based Prioritization)

This function orchestrates loading profiles, scanning, matching, and attempting connections in order.

C
// main.c (continued)

// Function to attempt connection using a loaded profile
static bool attempt_connection(const stored_wifi_config_t *profile) {
    wifi_config_t wifi_config = { 0 }; // Initialize fully
    strncpy((char*)wifi_config.sta.ssid, (char*)profile->ssid, sizeof(wifi_config.sta.ssid) -1);
    strncpy((char*)wifi_config.sta.password, (char*)profile->password, sizeof(wifi_config.sta.password) -1);
    wifi_config.sta.threshold.authmode = WIFI_AUTH_WPA2_PSK; // Assuming WPA2 for simplicity, load from profile if stored
    wifi_config.sta.scan_method = WIFI_FAST_SCAN; // Or WIFI_ALL_CHANNEL_SCAN
    wifi_config.sta.sort_method = WIFI_CONNECT_AP_BY_SIGNAL; // Let driver pick best BSSID if multiple match SSID
    // wifi_config.sta.pmf_cfg = ... // Configure PMF if needed

    ESP_LOGI(TAG, "Attempting to connect to SSID: %s", profile->ssid);

    // Clear previous connection attempt results
    xEventGroupClearBits(s_wifi_event_group, WIFI_CONNECTED_BIT | WIFI_FAIL_BIT);

    // Set configuration and connect
    ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
    esp_err_t ret = esp_wifi_connect();

    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "esp_wifi_connect failed: %s", esp_err_to_name(ret));
        return false;
    }

    // Wait for connection or failure event
    EventBits_t bits = xEventGroupWaitBits(s_wifi_event_group,
            WIFI_CONNECTED_BIT | WIFI_FAIL_BIT,
            pdFALSE, // Clear bits on exit: No (we check which bit was set)
            pdFALSE, // Wait for EITHER bit: Yes
            pdMS_TO_TICKS(20000)); // Timeout (e.g., 20 seconds)

    if (bits & WIFI_CONNECTED_BIT) {
        ESP_LOGI(TAG, "Successfully connected to SSID: %s", profile->ssid);
        return true;
    } else if (bits & WIFI_FAIL_BIT) {
        ESP_LOGW(TAG, "Failed to connect to SSID: %s (received disconnect event)", profile->ssid);
        // Disconnect explicitly to clean up before trying next network
        esp_wifi_disconnect();
        // Short delay before trying next network to allow driver state to settle
        vTaskDelay(pdMS_TO_TICKS(500));
        return false;
    } else {
        ESP_LOGW(TAG, "Failed to connect to SSID: %s (timeout)", profile->ssid);
        // Disconnect explicitly if timed out during connection attempt
         esp_wifi_disconnect();
         vTaskDelay(pdMS_TO_TICKS(500));
        return false;
    }
}


// Main connection manager task/function
void connect_to_best_network(void) {
    ESP_LOGI(TAG, "Starting connection manager...");

    stored_wifi_config_t loaded_profiles[MAX_STORED_NETWORKS];
    uint8_t num_loaded = 0;

    // 1. Load all potential profiles from NVS
    for (uint8_t i = 0; i < MAX_STORED_NETWORKS; i++) {
        if (load_wifi_config_from_nvs(i, &loaded_profiles[num_loaded]) == ESP_OK) {
            // Check if SSID is not empty (basic validity check)
            if (strlen((char*)loaded_profiles[num_loaded].ssid) > 0) {
                 num_loaded++;
            } else {
                ESP_LOGW(TAG, "Loaded empty profile from index %d, skipping.", i);
            }
        }
    }

    if (num_loaded == 0) {
        ESP_LOGE(TAG, "No valid WiFi profiles found in NVS.");
        // Handle error: maybe enter provisioning mode, sleep, etc.
        return;
    }
    ESP_LOGI(TAG, "Loaded %d profiles from NVS.", num_loaded);


    // 2. Scan for available networks (optional for pure order-based, but good practice)
    ESP_LOGI(TAG, "Starting WiFi scan...");
    wifi_scan_config_t scan_config = {
        .ssid = NULL,
        .bssid = NULL,
        .channel = 0,
        .show_hidden = false, // Set true if needed
        .scan_type = WIFI_SCAN_TYPE_ACTIVE,
        .scan_time.active.min = 100, // ms per channel
        .scan_time.active.max = 150, // ms per channel
    };
    ESP_ERROR_CHECK(esp_wifi_scan_start(&scan_config, true)); // Blocking scan

    uint16_t ap_count = 0;
    esp_wifi_scan_get_ap_num(&ap_count);
    ESP_LOGI(TAG, "Scan finished, found %d access points.", ap_count);

    if (ap_count == 0) {
        ESP_LOGW(TAG, "No networks found in scan. Cannot connect.");
        // Retry scan later?
        return;
    }

    wifi_ap_record_t *ap_list = (wifi_ap_record_t *)malloc(ap_count * sizeof(wifi_ap_record_t));
    if (ap_list == NULL) {
        ESP_LOGE(TAG, "Failed to allocate memory for scan results");
        return;
    }
    ESP_ERROR_CHECK(esp_wifi_scan_get_ap_records(&ap_count, ap_list));


    // 3. Match, Prioritize (Order-Based), and Attempt Connection
    bool connected = false;
    for (uint8_t i = 0; i < num_loaded; i++) { // Iterate through loaded profiles in order
        ESP_LOGI(TAG, "Checking stored profile %d: SSID '%s'", i, loaded_profiles[i].ssid);

        // Check if this profile's SSID was found in the scan results
        bool found_in_scan = false;
        for (uint16_t j = 0; j < ap_count; j++) {
            if (strcmp((char*)loaded_profiles[i].ssid, (char*)ap_list[j].ssid) == 0) {
                ESP_LOGI(TAG, "  -> SSID '%s' found in scan results (RSSI: %d).", ap_list[j].ssid, ap_list[j].rssi);
                found_in_scan = true;
                break; // Found it, no need to check further in scan results for this profile
            }
        }

        if (found_in_scan) {
            // Attempt connection using this profile
            if (attempt_connection(&loaded_profiles[i])) {
                connected = true;
                break; // Success! Stop trying other networks.
            } else {
                ESP_LOGW(TAG, "Connection attempt failed for profile %d ('%s'). Trying next.", i, loaded_profiles[i].ssid);
            }
        } else {
             ESP_LOGI(TAG, "  -> SSID '%s' not found in current scan.", loaded_profiles[i].ssid);
        }
    }

    // Free scan results memory
    free(ap_list);

    // 4. Final Status
    if (!connected) {
        ESP_LOGE(TAG, "Failed to connect to any of the stored and available networks.");
        // Implement retry logic or other failure handling here
    } else {
        ESP_LOGI(TAG, "Connection process complete. Device is connected.");
        // The IP_EVENT_STA_GOT_IP handler already signaled success via the event group bit.
    }
}


5. Main Application (app_main)

Initialize everything and start the connection manager.

C
// main.c (continued)

void app_main(void)
{
    // 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);

    ESP_LOGI(TAG, "ESP_WIFI_MODE_STA");

    // --- IMPORTANT ---
    // Uncomment the following line ONLY if you need to save the dummy credentials
    // for the first time. Comment it out for subsequent runs.
    // In a real application, manage credentials through a user interface.
    // save_dummy_credentials();
    // --- IMPORTANT ---


    // Initialize WiFi
    wifi_init_sta();

    // Now, wait for the WIFI_EVENT_STA_START event, which will trigger
    // the actual connection attempts via the handler.
    // For this example structure, we can directly call our manager function
    // after wifi_start() or trigger it from the event handler.
    // Calling it directly here for simplicity, assuming wifi_start() is synchronous enough
    // for the driver to be ready. A more robust approach might use a task
    // signaled by the WIFI_EVENT_STA_START event.

    // Let's refine: The event handler is simple, let's call connect_to_best_network
    // from app_main after wifi_init_sta() finishes. The WIFI_EVENT_STA_START
    // event isn't strictly needed to *trigger* the connection in this flow,
    // but the handler is still essential for disconnect/connect events.

    // Start the process to connect to the best available network
    connect_to_best_network();

    // Keep main task running (or do other things)
    while(1) {
        vTaskDelay(pdMS_TO_TICKS(10000)); // Idle loop
    }
}

6. Build, Flash, and Observe

  1. Build: Use the VS Code ESP-IDF extension: ESP-IDF: Build your project.
  2. Flash: Connect your ESP32 board and use ESP-IDF: Flash your project.
  3. Monitor: Open the Serial Monitor (ESP-IDF: Monitor your device).

Observation:

  • On the first run (after uncommenting save_dummy_credentials()), you should see logs indicating the dummy credentials being saved to NVS.
  • On subsequent runs (with save_dummy_credentials() commented out), you should see:
    • Logs indicating NVS initialization.
    • Logs showing profiles being loaded from NVS.
    • WiFi scan starting and reporting found APs.
    • Logs checking each stored profile against the scan results.
    • Logs attempting connection to the first stored profile (index 0) if its SSID was found in the scan.
    • If connection to profile 0 succeeds, logs showing “Successfully connected” and “Got IP”.
    • If connection to profile 0 fails (or its SSID wasn’t found), logs showing the failure/skip and then attempting connection to profile 1 (if its SSID was found), and so on.
    • If none of the stored and currently visible networks can be connected to, an error message indicating failure.

Warning: Replace "MyHomeNetwork", "HomePassword123", "MyOfficeNetwork", and "OfficeKeySecure" in save_dummy_credentials() with the actual SSIDs and passwords of networks you want the ESP32 to connect to during testing. Remember to comment out the save_dummy_credentials() call after the first successful run to avoid rewriting the NVS on every boot.

Variant Notes

The core concepts and ESP-IDF APIs used in this chapter (esp_wifi, nvs_flash, esp_event) are generally consistent across the ESP32 variants that support WiFi, including:

  • ESP32
  • ESP32-S2
  • ESP32-S3
  • ESP32-C3
  • ESP32-C6
  • ESP32-C5
  • ESP32-C61

There might be minor differences in specific WiFi features or performance characteristics between these chips, but the fundamental approach to storing credentials in NVS and managing connections remains the same using ESP-IDF v5.x. Always consult the specific datasheet and Technical Reference Manual for your chosen variant if you encounter unexpected behavior.

Common Mistakes & Troubleshooting Tips

Mistake / Issue Symptom(s) Troubleshooting / Solution
NVS Not Initialized/Erased NVS operations (nvs_open, nvs_set_blob, etc.) return errors like ESP_ERR_NVS_NOT_INITIALIZED. Device fails to save or load profiles. Ensure nvs_flash_init() is called in app_main. Handle return errors ESP_ERR_NVS_NO_FREE_PAGES or ESP_ERR_NVS_NEW_VERSION_FOUND by erasing and re-initializing NVS:
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
Incorrect NVS Keys or Namespace Profiles are saved but cannot be loaded, or wrong data is loaded. nvs_get_blob returns ESP_ERR_NVS_NOT_FOUND. Verify that the NVS_NAMESPACE string and key generation logic (e.g., snprintf("net_%d", index)) are identical in both saving and loading functions. Check for typos.
Credentials Not Saved/Overwritten Device always tries to connect to default/dummy credentials, or user-configured profiles are lost on reboot. Ensure credential saving functions (e.g., save_dummy_credentials()) are only called when intended (e.g., first-time setup, specific user command) and not on every boot. Comment out or guard such calls during normal operation.
Blocking Indefinitely on Event Group Device hangs during connection attempt, xEventGroupWaitBits does not return. No “connected” or “failed” logs. Ensure WiFi event handler correctly sets WIFI_FAIL_BIT on WIFI_EVENT_STA_DISCONNECTED. Use a finite timeout for xEventGroupWaitBits (e.g., pdMS_TO_TICKS(20000)) and handle the timeout case (else branch after wait bits) by logging an error and possibly calling esp_wifi_disconnect().
Scan Results Mismatch / SSID Not Found Stored profile for a known nearby network is not attempted because its SSID is not “found” in the scan results. SSID comparison is case-sensitive. Verify stored SSIDs exactly match broadcasted SSIDs. Ensure show_hidden in wifi_scan_config_t is true if connecting to hidden networks. WiFi scans might not always detect all networks; consider retry logic for the scan or connection process.
Incorrect strncpy Usage SSID or password in wifi_config_t is corrupted or not null-terminated, leading to connection failures or unexpected behavior. When using strncpy, ensure the destination buffer is large enough and always null-terminate it manually if the source string length is equal to or greater than the buffer size. E.g., strncpy((char*)config.sta.ssid, src_ssid, sizeof(config.sta.ssid)-1); config.sta.ssid[sizeof(config.sta.ssid)-1] = '\\0';. Or initialize the struct to zeros first: wifi_config_t wifi_config = {0};
Stack Overflow in WiFi Task Device crashes or behaves erratically during WiFi operations, especially scans or complex connection logic. Guru Meditation Errors related to stack. Increase stack size for the task performing WiFi operations if it’s a custom task. The default WiFi internal task stack sizes are usually sufficient, but intensive logging or complex algorithms in event handlers can consume stack. Ensure malloc for scan results is successful and free is called.

Exercises

  1. RSSI Prioritization: Modify the connect_to_best_network function to implement an RSSI-based prioritization strategy.
    • Scan for networks.
    • Load stored profiles.
    • Create a temporary list of matched profiles (found in both scan results and NVS). Store the profile data and the corresponding RSSI from the scan result.
    • Sort this temporary list in descending order of RSSI.
    • Attempt connections based on this sorted list.
  2. Add/Remove Profiles via UART: Implement simple UART commands (using uart_driver_install, uart_read_bytes, printf) to allow a user to:
    • add <index> <ssid> <password>: Save a new profile at the specified index.
    • remove <index>: Clear the profile at the specified index (e.g., by saving an empty structure or using nvs_erase_key).
    • list: Print all currently stored SSIDs and their indices.
  3. Last Successful Connection: Enhance the system to remember the index or SSID of the last successfully connected network (store it in NVS). Modify connect_to_best_network to try connecting to this network first before falling back to the order-based or RSSI-based strategy.
  4. Store Security Type: Modify the stored_wifi_config_t structure to include the wifi_auth_mode_t security type. Update the NVS save/load functions and the attempt_connection function to use the stored security type when setting the wifi_config.sta.threshold.authmode. Add logic to save/load this field via the UART interface from Exercise 2.

Summary

  • Managing multiple WiFi networks allows ESP32 devices to operate flexibly in various environments.
  • Network profiles (SSID, password, security) can be persistently stored using the NVS library.
  • Profiles are typically stored as binary blobs under unique keys within a dedicated NVS namespace.
  • A connection strategy involves loading profiles, scanning for available networks, matching scan results against profiles, prioritizing matched networks, and attempting connections sequentially.
  • Common prioritization methods include order-based, RSSI-based, and last successful connection.
  • Robust implementation requires careful handling of NVS operations, WiFi events (connect, disconnect, got IP), and potential errors or timeouts.
  • The core APIs for WiFi and NVS are consistent across most ESP32 variants supporting WiFi.

Further Reading

Leave a Comment

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

Scroll to Top