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:
- 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).
- Storage: These credentials (SSID and password) are written to NVS under specific keys (e.g.,
"wifi_ssid"
,"wifi_pass"
). - 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 byesp_wifi_connect()
in theWIFI_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:
- Initialize a
retry_delay
variable (e.g., to a base delay like 1000 ms). - Initialize a
retry_count
to 0. - 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 currentretry_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;
).
- Check if
- Check the
- When
IP_EVENT_STA_GOT_IP
occurs (successful connection):- Reset
retry_count
to 0. - Reset
retry_delay
to the initial base delay.
- Reset
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()
andesp_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:
// 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
:
/* --- 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 ensureExample 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.
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.
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:
// 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):
/* --- 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 likeBEACON_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 hitsMAX_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.
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
- Backoff Delay Cap: Verify that the
MAX_RETRY_DELAY_MS
cap in Example 2 works. SetMAX_RETRY_DELAY_MS
to a shorter value (e.g., 5000 ms) andEXAMPLE_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. - 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: Usenvs_erase_key
). - 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 queryesp_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. - 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 inwifi_init_sta
and use it when setting the configuration withesp_wifi_set_config
. This makes the auto-connect more flexible for different network types. (Hint: Usenvs_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
- ESP-IDF Programming Guide – NVS API Reference: https://docs.espressif.com/projects/esp-idf/en/v5.4/esp32/api-reference/storage/nvs_flash.html
- ESP-IDF Programming Guide – WiFi: (Review Station Mode sections and Event descriptions) https://docs.espressif.com/projects/esp-idf/en/v5.4/esp32/api-reference/network/esp_wifi.html
- ESP-IDF Programming Guide – Event Loop Library: https://docs.espressif.com/projects/esp-idf/en/v5.4/esp32/api-reference/system/esp_event.html
- ESP-IDF WiFi Station Example: (Check for patterns related to reconnection) https://github.com/espressif/esp-idf/tree/v5.4/examples/wifi/getting_started/station
