Chapter 45: WiFi Provisioning Methods for ESP32
Chapter Objectives
By the end of this chapter, you will be able to:
- Define WiFi provisioning and understand its importance for IoT devices.
- Compare and contrast various provisioning methods: hardcoding, UART, WPS, SmartConfig, AP Mode (Captive Portal), and BLE.
- Understand the advantages and disadvantages of each provisioning technique.
- Implement AP mode provisioning on an ESP32, including serving a web page to capture credentials.
- Discuss security considerations for different provisioning methods.
- Manage WiFi credentials using Non-Volatile Storage (NVS).
- Select the appropriate provisioning method based on application requirements and user experience goals.
Introduction
One of the first and most critical steps in deploying an IoT device is connecting it to a WiFi network. This process, known as “provisioning,” involves configuring the device with the necessary network credentials (SSID and password). For devices with screens and keyboards, this is straightforward. However, many ESP32-based IoT products are “headless” – they lack traditional input/output interfaces. This makes provisioning a significant design challenge: how do you securely and easily get WiFi credentials onto the device?
In previous chapters, we’ve touched upon specific methods like WPS (Chapter 40) and SmartConfig (Chapter 41). This chapter takes a broader look at the landscape of WiFi provisioning techniques available for the ESP32. We will revisit some familiar methods in the context of a complete provisioning strategy and introduce new ones, such as AP mode provisioning (often using a captive portal). Understanding these diverse approaches will enable you to choose the most suitable method for your project, balancing ease of use, security, and implementation complexity.
Theory
What is WiFi Provisioning?
WiFi provisioning is the process of configuring an uninitialized device with the information it needs to connect to a specific wireless network. This typically includes the network’s Service Set Identifier (SSID) and its password (Pre-Shared Key or other credentials for enterprise networks). The goal is to make this process as seamless and secure as possible for the end-user.
Categorizing Provisioning Methods
Provisioning methods can be broadly categorized based on how the user interacts with the device and how data is transferred:
- Out-of-Band Methods: Credentials are provided through a channel other than the target WiFi network itself (e.g., BLE, UART, pre-programming).
- In-Band Methods (using WiFi): The device uses its WiFi radio in a special mode to receive credentials (e.g., SmartConfig, AP mode).
- No User Interaction (Pre-provisioning): Credentials are set during manufacturing or development.
Let’s explore common methods in more detail.
1. Hardcoding Credentials
- How it works: SSID and password are directly embedded into the firmware code.
- Pros: Simplest to implement for developers during early testing.
- Cons: Highly insecure and impractical for production. If credentials change, firmware must be updated and reflashed. Distributing firmware with embedded credentials is a major security risk.
- Use Case: Strictly for developer-only prototypes or very specific, controlled environments where credentials never change and firmware is not publicly distributed. Not recommended for general use.
2. UART/Serial Console Provisioning
- How it works: The ESP32 prompts for SSID and password over its serial (UART) interface. A user connects via a terminal program to enter the details.
- Pros: Simple to implement, good for development and debugging. No complex app or network setup needed initially.
- Cons: Requires physical access to the device’s serial port and a computer. Not user-friendly for consumers.
- Use Case: Developer setups, testing, or devices in accessible technical environments.
3. WiFi Protected Setup (WPS) – Recap
- How it works: (As detailed in Chapter 40) User presses a button on the router and often on the device (PBC mode), or uses a PIN. The device and router then negotiate credentials.
- Pros: User-friendly (especially PBC), standardized.
- Cons: Requires physical access to the router (for PBC). PIN mode has known vulnerabilities. Limited adoption on some routers due to security concerns.
- Use Case: Consumer devices where ease of use is paramount and the router supports WPS.
4. SmartConfig (Esptouch) – Recap
- How it works: (As detailed in Chapter 41) A smartphone app broadcasts encoded SSID/password packets. The ESP32, in promiscuous mode, sniffs and decodes these packets.
- Pros: User-friendly (via app), no physical access to router needed beyond knowing credentials.
- Cons: Requires a dedicated smartphone app. Can be affected by network congestion or specific router configurations (e.g., AP isolation). Credentials broadcast (obfuscated, not strongly encrypted).
- Use Case: Headless consumer IoT devices.
5. Access Point (AP) Mode Provisioning (SoftAP or Captive Portal)
This is a very common and versatile method.
- How it works:
- The ESP32 starts up in AP mode, creating its own temporary WiFi network (e.g., “ESP32-Config”).
- The user connects their smartphone or laptop to this temporary ESP32 network.
- Once connected, the user’s device might automatically open a web browser to a specific page (captive portal behavior), or the user manually navigates to a known IP address for the ESP32 (e.g., 192.168.4.1).
- The ESP32 runs a simple HTTP web server, serving a web page that prompts the user to select a target WiFi network (from a scanned list, or by manual entry) and enter its password.
- The user submits the credentials via the web form.
- The ESP32 receives the credentials, saves them (e.g., to NVS).
- The ESP32 then disables AP mode, switches to Station (STA) mode, and attempts to connect to the user-specified WiFi network using the provided credentials.
- Captive Portal: To make the web page appear “automatically,” the ESP32 often runs a DNS server that redirects all DNS queries to its own IP address. When the connected device tries to resolve any domain name (e.g.,
google.com
), it’s directed to the ESP32’s web server. Most operating systems detect this and trigger the captive portal browser. - Pros: No special app required on the user’s phone (uses a standard web browser). Platform-agnostic. Can provide a richer UI for configuration. Generally more secure during transmission than SmartConfig if HTTPS is used on the SoftAP (though often HTTP is used for simplicity on the local config network).
- Cons: More complex to implement on the ESP32 (requires AP mode, HTTP server, DNS server for captive portal, HTML/JS for the web page). User experience involves switching WiFi networks, which can be slightly confusing for some.
- Use Case: Widely used for many IoT devices, offering a good balance of usability and implementation flexibility.
%%{ init: { 'theme': 'base', 'themeVariables': { 'fontFamily': 'Open Sans', 'primaryColor': '#EDE9FE', 'primaryBorderColor': '#5B21B6', 'primaryTextColor': '#5B21B6', 'secondaryColor': '#DBEAFE', 'secondaryBorderColor': '#2563EB', 'secondaryTextColor': '#1E40AF', 'tertiaryColor': '#FEF3C7', 'tertiaryBorderColor': '#D97706', 'tertiaryTextColor': '#92400E', 'successColor': '#D1FAE5', 'successBorderColor': '#059669', 'successTextColor': '#065F46', 'actorBorder': '#5B21B6', 'actorTextColor': '#1F2937', 'lineColor': '#5B21B6' } } }%% sequenceDiagram actor User participant UserDevice as User's Device (Phone/Laptop) participant ESP32 as ESP32 Device ESP32->>ESP32: Starts in AP Mode (e.g., SSID "ESP32-Config") Note over ESP32: style ESP32 fill:#EDE9FE,stroke:#5B21B6 ESP32->>ESP32: Starts HTTP & DNS Server (for captive portal) Note over ESP32: style ESP32 fill:#EDE9FE,stroke:#5B21B6 User->>UserDevice: Scans WiFi, Selects "ESP32-Config" Note over UserDevice: style UserDevice fill:#FEF3C7,stroke:#D97706 UserDevice->>ESP32: Connects to "ESP32-Config" Network Note over UserDevice,ESP32: style UserDevice->ESP32 fill:#DBEAFE,stroke:#2563EB alt Captive Portal or Manual Navigation UserDevice->>ESP32: HTTP GET / (or DNS redirect to ESP32 IP) Note over UserDevice,ESP32: style UserDevice->ESP32 fill:#DBEAFE,stroke:#2563EB end ESP32-->>UserDevice: Serves Provisioning Web Page (HTML Form) Note over ESP32,UserDevice: style ESP32-->>UserDevice fill:#DBEAFE,stroke:#2563EB User->>UserDevice: Enters Target WiFi SSID & Password into Form UserDevice->>UserDevice: Submits Form Note over UserDevice: style UserDevice fill:#FEF3C7,stroke:#D97706 UserDevice->>ESP32: HTTP POST /connect (with credentials) Note over UserDevice,ESP32: style UserDevice->ESP32 fill:#DBEAFE,stroke:#2563EB activate ESP32 ESP32->>ESP32: Receives Credentials ESP32->>ESP32: Saves Credentials (e.g., to NVS) Note over ESP32: style ESP32 fill:#DBEAFE,stroke:#2563EB ESP32-->>UserDevice: HTTP Response (e.g., "Success, attempting to connect...") deactivate ESP32 ESP32->>ESP32: Disables AP Mode ESP32->>ESP32: Enables STA Mode Note over ESP32: style ESP32 fill:#EDE9FE,stroke:#5B21B6 ESP32->>UserDevice: (SoftAP "ESP32-Config" goes down) activate UserDevice UserDevice-->>User: Disconnected from "ESP32-Config" User->>UserDevice: Reconnects Phone to Normal WiFi Note over UserDevice: style UserDevice fill:#FEF3C7,stroke:#D97706 ESP32->>ESP32: Attempts to Connect to Target WiFi Network (using saved credentials) Note over ESP32: style ESP32 fill:#DBEAFE,stroke:#2563EB alt Connection Successful ESP32->>ESP32: Connected to Target WiFi! Note over ESP32: style ESP32 fill:#D1FAE5,stroke:#059669 else Connection Failed ESP32->>ESP32: Failed to Connect to Target WiFi Note over ESP32: style ESP32 fill:#FEE2E2,stroke:#DC2626 end deactivate UserDevice
6. Bluetooth Low Energy (BLE) Provisioning
- How it works:
- The ESP32 advertises itself as a BLE peripheral with a specific GATT (Generic Attribute Profile) service for WiFi provisioning.
- A companion smartphone app (with BLE capabilities) scans for and connects to the ESP32.
- The app securely writes the WiFi SSID and password to specific GATT characteristics on the ESP32.
- The ESP32 receives the credentials via BLE, saves them, and then attempts to connect to the WiFi network.
- Pros: Secure transmission of credentials (BLE has its own encryption and security mechanisms). Does not require the ESP32 to initially expose a WiFi network. Can work even if the target WiFi network is down or out of range during provisioning. Lower power than WiFi AP mode for the provisioning phase.
- Cons: Requires a custom smartphone app with BLE capabilities. More complex to implement on both the ESP32 (BLE stack, GATT services) and the companion app.
- Use Case: Headless devices where security is paramount, or where WiFi AP mode is undesirable. Common in wearables and some smart home devices.
7. Provisioning via NVS/Manufacturing Data
- How it works: Credentials or unique device identifiers (that can be used to fetch credentials from a server) are programmed into the ESP32’s Non-Volatile Storage (NVS) or eFuses during the manufacturing process.
- Pros: No user interaction needed for initial setup if full credentials are pre-programmed. Can be very secure if managed properly.
- Cons: Inflexible if credentials change. Pre-programming full credentials for many unique networks is not scalable for consumer products. More suitable for devices deployed in known, fixed environments or using device-specific identities to connect to a backend that provides network details.
- Use Case: Industrial IoT, devices sold for specific pre-configured networks, or as part of a more complex “zero-touch” provisioning system involving a cloud backend.
ESP-IDF Unified Provisioning (protocomm
)
ESP-IDF includes a component called “Unified Provisioning” (protocomm
) which provides a framework for implementing various provisioning schemes over different transports (Wi-Fi SoftAP, BLE). It handles the secure session establishment and data exchange, allowing developers to focus on the UI/UX aspects. Libraries like wifi_provisioning
build upon protocomm
. This is a more advanced topic but good to be aware of for production-grade solutions.
Comparison of Methods
Feature | Hardcoding | UART | WPS | SmartConfig | AP Mode (SoftAP) | BLE Provisioning | Pre-Provisioned |
---|---|---|---|---|---|---|---|
User Ease | N/A (Dev) | Low | High | Med-High | Medium | Medium | Highest (None) |
App Required | No | No (Terminal) | No | Yes | No (Browser) | Yes | No |
Security (Transmission/Storage) | Very Low | Medium | Med-Low | Medium | Med-High (if HTTPS) | High | High (if managed well) |
Complexity (ESP32 Firmware) | Very Low | Low | Medium | Medium | High | High | Low (runtime) / High (mfg process) |
Physical Access / Proximity | N/A (Dev) | Yes (UART) | Yes (Router for PBC) | No | No (WiFi Proximity) | Proximity (BLE Range) | N/A (Mfg) |
Practical Examples
Example 1: AP Mode (SoftAP) WiFi Provisioning
This example demonstrates setting up the ESP32 in AP mode to serve a web page for WiFi credential input.
1. Project Setup:
- Create a new ESP-IDF project.
- In
menuconfig
, ensure:Component config
->Wi-Fi
->WiFi Station Enable
andWiFi AP Enable
are checked.Component config
->LWIP
->Enable DNS server
is checked (for captive portal functionality).Component config
->ESP HTTP Server
->Enable HTTP Server
(if not already enabled by default).
2. HTML for Provisioning Page:
Create a simple HTML page. For this example, we’ll embed it as a string in the C code. In a real application, you might use SPIFFS or embed it as binary data.
<!DOCTYPE html>
<html>
<head>
<title>ESP32 WiFi Provisioning</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body { font-family: Arial, sans-serif; margin: 20px; background-color: #f4f4f4; color: #333; }
.container { background-color: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 0 10px rgba(0,0,0,0.1); }
h2 { text-align: center; color: #007bff; }
label { display: block; margin-bottom: 8px; font-weight: bold; }
input[type="text"], input[type="password"] {
width: calc(100% - 22px); padding: 10px; margin-bottom: 20px; border: 1px solid #ddd; border-radius: 4px;
}
input[type="submit"] {
background-color: #007bff; color: white; padding: 10px 15px; border: none; border-radius: 4px; cursor: pointer; width: 100%; font-size: 16px;
}
input[type="submit"]:hover { background-color: #0056b3; }
.status { margin-top: 20px; padding: 10px; border-radius: 4px; }
.success { background-color: #d4edda; color: #155724; border: 1px solid #c3e6cb;}
.error { background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb;}
</style>
</head>
<body>
<div class="container">
<h2>Configure WiFi Network</h2>
<form action="/connect" method="post">
<label for="ssid">SSID (Network Name):</label>
<input type="text" id="ssid" name="ssid" required><br>
<label for="password">Password:</label>
<input type="password" id="password" name="password" required><br>
<input type="submit" value="Connect">
</form>
</div>
</body>
</html>
3. C Code Implementation:
// 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"
#include "esp_http_server.h"
#include "lwip/dns.h" // For DNS server captive portal
static const char *TAG = "AP_PROVISIONING";
// Event group to signal provisioning completion and WiFi connection
static EventGroupHandle_t s_wifi_event_group;
const int PROVISIONING_DONE_BIT = BIT0;
const int WIFI_CONNECTED_BIT = BIT1;
const int WIFI_FAIL_BIT = BIT2;
// --- Configuration for SoftAP ---
#define EXAMPLE_ESP_WIFI_AP_SSID "ESP32-Provision"
#define EXAMPLE_ESP_WIFI_AP_PASS "esp32prov" // Optional password for the config AP
#define EXAMPLE_ESP_WIFI_AP_CHANNEL 1
#define EXAMPLE_ESP_WIFI_AP_MAX_CONN 1
// Store received credentials temporarily
static char s_sta_ssid[32] = {0};
static char s_sta_password[64] = {0};
// Embedded HTML for the provisioning page
const char provisioning_html_start[] = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<title>ESP32 WiFi Provisioning</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body { font-family: Arial, sans-serif; margin: 20px; background-color: #f4f4f4; color: #333; }
.container { background-color: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 0 10px rgba(0,0,0,0.1); max-width: 400px; margin: auto; }
h2 { text-align: center; color: #007bff; }
label { display: block; margin-bottom: 8px; font-weight: bold; }
input[type="text"], input[type="password"] {
width: calc(100% - 22px); padding: 10px; margin-bottom: 20px; border: 1px solid #ddd; border-radius: 4px;
}
input[type="submit"] {
background-color: #007bff; color: white; padding: 10px 15px; border: none; border-radius: 4px; cursor: pointer; width: 100%; font-size: 16px;
}
input[type="submit"]:hover { background-color: #0056b3; }
</style>
</head>
<body>
<div class="container">
<h2>Configure WiFi Network</h2>
<form action="/connect" method="post">
<label for="ssid">SSID (Network Name):</label>
<input type="text" id="ssid" name="ssid" required><br>
<label for="password">Password:</label>
<input type="password" id="password" name="password"><br>
<p style="font-size:0.8em; color: #555;">Leave password blank for open networks.</p>
<input type="submit" value="Save & Connect">
</form>
</div>
</body>
</html>
)rawliteral";
// HTTP GET Handler for the root page
static esp_err_t root_get_handler(httpd_req_t *req)
{
ESP_LOGI(TAG, "Serving provisioning page");
httpd_resp_set_type(req, "text/html");
httpd_resp_send(req, provisioning_html_start, HTTPD_RESP_USE_STRLEN);
return ESP_OK;
}
// HTTP POST Handler for /connect (receiving credentials)
static esp_err_t connect_post_handler(httpd_req_t *req)
{
char buf[256]; // Increased buffer size
int ret, remaining = req->content_len;
ESP_LOGI(TAG, "Received /connect POST request, content_len: %d", remaining);
if (remaining >= sizeof(buf)) {
ESP_LOGE(TAG, "Content too long");
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Content too long");
return ESP_FAIL;
}
int received = 0;
while (remaining > 0) {
ret = httpd_req_recv(req, buf + received, MIN(remaining, sizeof(buf) - received -1));
if (ret <= 0) {
if (ret == HTTPD_SOCK_ERR_TIMEOUT) {
httpd_resp_send_408(req);
}
ESP_LOGE(TAG, "Error receiving POST data or timeout");
return ESP_FAIL;
}
received += ret;
remaining -= ret;
}
buf[received] = '\0'; // Null-terminate the received data
ESP_LOGI(TAG, "POST data: %s", buf);
// Parse SSID and password from the form data (buf)
// Example: "ssid=MyNetwork&password=MyPassword"
char ssid_param[32] = {0};
char pass_param[64] = {0};
if (httpd_query_key_value(buf, "ssid", ssid_param, sizeof(ssid_param)) == ESP_OK) {
ESP_LOGI(TAG, "Parsed SSID: %s", ssid_param);
strncpy(s_sta_ssid, ssid_param, sizeof(s_sta_ssid) - 1);
} else {
ESP_LOGE(TAG, "SSID not found in POST data");
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "SSID missing");
return ESP_FAIL;
}
if (httpd_query_key_value(buf, "password", pass_param, sizeof(pass_param)) == ESP_OK) {
ESP_LOGI(TAG, "Parsed Password: %s", pass_param);
strncpy(s_sta_password, pass_param, sizeof(s_sta_password) - 1);
} else {
ESP_LOGI(TAG, "Password not found or empty, assuming open network for this SSID.");
s_sta_password[0] = '\0'; // Explicitly clear password for open networks
}
// Respond to the client
const char* resp_str = "Credentials received. ESP32 will attempt to connect. You can now close this page and connect this device to your main WiFi network.";
httpd_resp_send(req, resp_str, HTTPD_RESP_USE_STRLEN);
// Signal that provisioning is done
xEventGroupSetBits(s_wifi_event_group, PROVISIONING_DONE_BIT);
return ESP_OK;
}
static const httpd_uri_t root_uri = {
.uri = "/",
.method = HTTP_GET,
.handler = root_get_handler,
.user_ctx = NULL
};
static const httpd_uri_t connect_uri = {
.uri = "/connect",
.method = HTTP_POST,
.handler = connect_post_handler,
.user_ctx = NULL
};
// Function to start the HTTP server
static httpd_handle_t start_webserver(void)
{
httpd_handle_t server = NULL;
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
config.max_uri_handlers = 8; // Increase if more handlers are needed
config.lru_purge_enable = true; // Good for captive portals
ESP_LOGI(TAG, "Starting HTTP server on port: '%d'", config.server_port);
if (httpd_start(&server, &config) == ESP_OK) {
ESP_LOGI(TAG, "Registering URI handlers");
httpd_register_uri_handler(server, &root_uri);
httpd_register_uri_handler(server, &connect_uri);
return server;
}
ESP_LOGE(TAG, "Error starting server!");
return NULL;
}
static void stop_webserver(httpd_handle_t server)
{
if (server) {
httpd_stop(server);
}
}
// WiFi Event Handler
static void wifi_event_handler(void* arg, esp_event_base_t event_base,
int32_t event_id, void* event_data)
{
httpd_handle_t* server = (httpd_handle_t*) arg;
if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_AP_STACONNECTED) {
wifi_event_ap_staconnected_t* event = (wifi_event_ap_staconnected_t*) event_data;
ESP_LOGI(TAG, "Client "MACSTR" joined SoftAP, AID=%d", MAC2STR(event->mac), event->aid);
} else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_AP_STADISCONNECTED) {
wifi_event_ap_stadisconnected_t* event = (wifi_event_ap_stadisconnected_t*) event_data;
ESP_LOGI(TAG, "Client "MACSTR" left SoftAP, AID=%d", MAC2STR(event->mac), event->aid);
} else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) {
ESP_LOGI(TAG, "STA mode started. Connecting to target AP...");
esp_wifi_connect();
} 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, "STA Disconnected, reason: %d. Retrying...", event->reason);
xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT);
xEventGroupClearBits(s_wifi_event_group, WIFI_CONNECTED_BIT);
// Simple retry, could be more sophisticated
esp_wifi_connect();
} 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, "STA Got IP: " IPSTR, IP2STR(&event->ip_info.ip));
xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT);
xEventGroupClearBits(s_wifi_event_group, WIFI_FAIL_BIT);
}
}
// Initialize WiFi in AP mode for provisioning
static void wifi_init_softap_provisioning(httpd_handle_t* server_handle_ptr)
{
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
esp_netif_t* ap_netif = esp_netif_create_default_wifi_ap();
assert(ap_netif);
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
// Register event handlers for AP and STA events
ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &wifi_event_handler, server_handle_ptr));
ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &wifi_event_handler, NULL));
wifi_config_t wifi_ap_config = {
.ap = {
.ssid = EXAMPLE_ESP_WIFI_AP_SSID,
.ssid_len = strlen(EXAMPLE_ESP_WIFI_AP_SSID),
.channel = EXAMPLE_ESP_WIFI_AP_CHANNEL,
.password = EXAMPLE_ESP_WIFI_AP_PASS,
.max_connection = EXAMPLE_ESP_WIFI_AP_MAX_CONN,
.authmode = WIFI_AUTH_WPA2_PSK, // Secure the config AP
.pmf_cfg = {
.required = false, // PMF not typically used/needed for config AP
},
},
};
if (strlen(EXAMPLE_ESP_WIFI_AP_PASS) == 0) {
wifi_ap_config.ap.authmode = WIFI_AUTH_OPEN;
}
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_AP));
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP, &wifi_ap_config));
ESP_ERROR_CHECK(esp_wifi_start());
ESP_LOGI(TAG, "SoftAP WiFi initialized. SSID:%s Password:%s Channel:%d",
EXAMPLE_ESP_WIFI_AP_SSID, EXAMPLE_ESP_WIFI_AP_PASS, EXAMPLE_ESP_WIFI_AP_CHANNEL);
// Start DNS server for captive portal
// All DNS requests will point to the ESP32's IP address
ip_addr_t dnsserver;
dnsserver.type = IPADDR_TYPE_V4;
esp_netif_ip_info_t ip_info;
esp_netif_get_ip_info(ap_netif, &ip_info);
dnsserver.u_addr.ip4.addr = ip_info.ip.addr; // DNS server is the AP itself
dns_setserver(0, &dnsserver); // Set primary DNS server
// Start HTTP server
*server_handle_ptr = start_webserver();
}
// Switch to STA mode and connect to the target AP
static void switch_to_sta_mode(void)
{
ESP_LOGI(TAG, "Switching to STA mode to connect to SSID: %s", s_sta_ssid);
// It's good practice to unregister AP-specific event handlers if any were unique,
// or ensure the common handler behaves correctly for STA events.
// For simplicity, this example uses a shared handler.
esp_netif_create_default_wifi_sta(); // Create STA netif if not already done (or reconfigure)
wifi_config_t wifi_sta_config = {
.sta = {
// .ssid and .password will be filled from s_sta_ssid and s_sta_password
.threshold.authmode = WIFI_AUTH_WPA2_PSK, // Default, can be refined
.pmf_cfg = {
.capable = true,
.required = false // Adjust as per target network
},
},
};
strncpy((char*)wifi_sta_config.sta.ssid, s_sta_ssid, sizeof(wifi_sta_config.sta.ssid));
strncpy((char*)wifi_sta_config.sta.password, s_sta_password, sizeof(wifi_sta_config.sta.password));
if (strlen(s_sta_password) == 0) {
wifi_sta_config.sta.threshold.authmode = WIFI_AUTH_OPEN;
} else {
// Attempt WPA2/WPA3 mixed mode for wider compatibility if password is present
// Could also try to infer WPA3 if AP beacons indicate it
wifi_sta_config.sta.threshold.authmode = WIFI_AUTH_WPA2_WPA3_PSK;
wifi_sta_config.sta.pmf_cfg.required = true; // Good practice for WPA3
}
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_sta_config));
// esp_wifi_start() was already called. esp_wifi_connect() will be called by WIFI_EVENT_STA_START
// or we can call it directly here if WIFI_EVENT_STA_START was for AP mode.
// For clarity, let's ensure it's called.
ESP_LOGI(TAG, "Attempting to connect to target AP: %s", s_sta_ssid);
esp_wifi_connect(); // Explicitly try to connect
}
void app_main(void)
{
s_wifi_event_group = xEventGroupCreate();
static httpd_handle_t server = NULL;
// 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, "Starting AP Mode Provisioning Example");
wifi_init_softap_provisioning(&server);
ESP_LOGI(TAG, "Waiting for provisioning data via HTTP server...");
EventBits_t bits = xEventGroupWaitBits(s_wifi_event_group, PROVISIONING_DONE_BIT, pdFALSE, pdFALSE, portMAX_DELAY);
if (bits & PROVISIONING_DONE_BIT) {
ESP_LOGI(TAG, "Provisioning data received. SSID: %s", s_sta_ssid);
// Stop DNS server for captive portal
dns_setserver(0, NULL); // Clear DNS server
stop_webserver(server); // Stop HTTP server
server = NULL;
// Deinit AP mode more cleanly before switching
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_NULL)); // Turn off WiFi temporarily
esp_netif_t* ap_netif = esp_netif_get_handle_from_ifkey("WIFI_AP_DEF");
if (ap_netif) {
esp_netif_destroy(ap_netif);
}
// ESP_ERROR_CHECK(esp_wifi_deinit()); // Could deinit and reinit for full clean switch
// ESP_ERROR_CHECK(esp_wifi_init(&cfg_default_wifi_init)); // Re-init for STA
switch_to_sta_mode();
ESP_LOGI(TAG, "Waiting for connection to target AP...");
EventBits_t sta_bits = xEventGroupWaitBits(s_wifi_event_group,
WIFI_CONNECTED_BIT | WIFI_FAIL_BIT,
pdFALSE,
pdFALSE,
pdMS_TO_TICKS(60000)); // Timeout after 60s
if (sta_bits & WIFI_CONNECTED_BIT) {
ESP_LOGI(TAG, "Successfully connected to target AP: %s", s_sta_ssid);
// TODO: Save credentials to NVS here
} else {
ESP_LOGE(TAG, "Failed to connect to target AP: %s. Restarting provisioning AP mode (example).", s_sta_ssid);
// In a real app, might restart AP mode or enter error state
// For simplicity, this example would end here or require a restart.
// To restart provisioning:
// wifi_init_softap_provisioning(&server); // Re-init AP and server
// xEventGroupClearBits(s_wifi_event_group, PROVISIONING_DONE_BIT | WIFI_CONNECTED_BIT | WIFI_FAIL_BIT);
// Then loop back to wait for PROVISIONING_DONE_BIT
}
}
// Keep main task running
while(1) {
vTaskDelay(pdMS_TO_TICKS(10000));
}
}
4. Build, Flash, and Observe:
- Build and flash the project.
- On your phone/laptop, scan for WiFi networks. You should see “ESP32-Provision”.
- Connect to it (password: “esp32prov” if you set one).
- Your device should automatically open a browser to the provisioning page, or you can manually navigate to
192.168.4.1
(default AP IP for ESP32). - Enter your target WiFi SSID and password and submit.
- Observe the ESP32 serial monitor. It will log received credentials, stop the AP, and attempt to connect to your target network.
Note: This is a simplified example. A production system would need more robust HTML/CSS/JS, error handling, security (HTTPS for the config AP if sensitive data beyond WiFi creds is handled), and NVS storage for credentials.
Variant Notes
The provisioning methods discussed are generally applicable to all ESP32 variants that support the required underlying technology:
ESP32 Variant | Hardcode / UART | WPS | SmartConfig | AP Mode (SoftAP) | BLE Provisioning |
---|---|---|---|---|---|
ESP32 (Original) | ✔ Yes | ✔ Yes | ✔ Yes | ✔ Yes | ✔ Yes (Has BLE) |
ESP32-S2 | ✔ Yes | ✔ Yes | ✔ Yes | ✔ Yes | ✘ No (No BLE) |
ESP32-S3 | ✔ Yes | ✔ Yes | ✔ Yes | ✔ Yes | ✔ Yes (Has BLE) |
ESP32-C3 | ✔ Yes | ✔ Yes | ✔ Yes | ✔ Yes | ✔ Yes (Has BLE) |
ESP32-C6 | ✔ Yes | ✔ Yes | ✔ Yes | ✔ Yes | ✔ Yes (Has BLE) |
ESP32-H2 | UART Only* | ✘ No | ✘ No | ✘ No | Yes (BLE only)** |
* ESP32-H2 supports UART for general communication; hardcoding is always possible but not a “provisioning method” in the network sense.
** ESP32-H2 uses BLE for its primary connectivity (Thread/Zigbee commissioning often involves BLE); it does not provision WiFi.
The ESP-IDF Unified Provisioning framework aims to provide a consistent API layer over BLE and SoftAP transports, simplifying cross-variant development for these two methods.
Common Mistakes & Troubleshooting Tips
Mistake / Issue (Provisioning Method) | Symptom(s) | Troubleshooting / Solution |
---|---|---|
AP Mode: Captive Portal Not Working | User connects to ESP32’s SoftAP, but browser doesn’t auto-redirect to provisioning page. Manual IP entry works. |
Fix: Ensure DNS server is enabled (CONFIG_LWIP_DNS_SERVER in menuconfig) and correctly started on ESP32, redirecting all queries to ESP32’s AP IP. Test by manually navigating to ESP32’s IP (e.g., 192.168.4.1). Some OS captive portal detection is aggressive; ensure basic HTTP 200 for common check URLs.
|
AP Mode: Web Form Data Parsing Issues | ESP32 doesn’t receive or incorrectly interprets SSID/password from web form. Connection to target AP fails with auth errors. |
Fix: Verify HTTP POST request handling. Ensure buffer for form data (httpd_req_recv ) is large enough. Use httpd_query_key_value correctly to parse URL-encoded data. Log raw received buffer for debugging. Handle empty passwords for open networks.
|
Credentials Not Saved Persistently (All Methods) | Device successfully provisions and connects, but loses credentials after reboot and needs re-provisioning. | Fix: Implement NVS (Non-Volatile Storage) write operation after successfully receiving credentials and connecting. On boot, attempt to read credentials from NVS before starting any provisioning process. |
BLE Provisioning: GATT Complexity / App Issues | Companion app cannot connect to ESP32 via BLE, or fails to write/read characteristics. Credentials not transferred. | Fix: Verify GATT service/characteristic UUIDs and properties (read, write, notify, indicate) match between ESP32 and app. Test BLE communication with a generic BLE scanner tool first. Debug both ESP32 (GATT server logic) and mobile app (GATT client logic) sides carefully. Check BLE permissions on the phone. |
Switching Modes (AP to STA) Not Clean | After AP mode provisioning, ESP32 fails to connect in STA mode, or behaves erratically. IP conflicts or resource issues. |
Fix: Stop the HTTP server and DNS server. Call esp_wifi_set_mode(WIFI_MODE_NULL) or esp_wifi_stop() . Optionally esp_wifi_deinit() and re-initialize WiFi stack for STA for a very clean switch. Manage esp_netif_t instances properly (destroy AP netif, create STA netif).
|
SmartConfig: Fails in Certain Network Environments | ESP32 doesn’t receive credentials despite app sending them. Common on congested 2.4GHz, or networks with AP Isolation or multicast/broadcast filtering. | Fix: Ensure phone is on 2.4GHz band. Try disabling AP Isolation on router. Test in a simpler network environment. Ensure mobile data is off on the phone. Try both multicast/broadcast modes in Esptouch app if available. |
WPS: PBC Mode Timeout or PIN Issues | WPS process times out before completion. PIN method fails authentication. | Fix: For PBC, ensure buttons are pressed within the active window (usually 2 mins). Check router WPS logs. For PIN, ensure correct PIN is used and router supports device-initiated PIN. Some routers have WPS disabled by default due to security concerns. |
Exercises
- Enhance AP Mode Web Page:
- Modify the HTML and ESP32 C code for the AP mode provisioning example to include:
- A WiFi scan button that, when clicked (using JavaScript on the client-side), makes an AJAX request to the ESP32.
- An ESP32 HTTP endpoint (e.g.,
/scanwifi
) that performs a WiFi scan and returns the list of found SSIDs as JSON. - JavaScript on the web page to populate a dropdown list with the scanned SSIDs, allowing the user to select instead of typing.
- Modify the HTML and ESP32 C code for the AP mode provisioning example to include:
- NVS for AP Mode Provisioning:
- Extend the AP Mode provisioning example. After successfully receiving credentials and connecting to the target AP, save the SSID and password to NVS.
- On boot, check if credentials exist in NVS. If yes, attempt to connect using them directly. If not, or if connection fails, then start the AP provisioning mode.
- BLE Provisioning Outline:
- Design the GATT service and characteristics for BLE-based WiFi provisioning. Specify UUIDs, properties (read, write, notify), and the data format for sending SSID and password.
- Write pseudo-code or a basic C structure for the ESP32 side to handle BLE write requests for these characteristics. (Full BLE implementation is extensive, focus on the design).
- Provisioning State Machine:
- Design a conceptual state machine for an ESP32 device that attempts provisioning in a sequence:
- Check NVS for saved credentials.
- If NVS fails, try WPS PBC for 60 seconds.
- If WPS fails, try SmartConfig for 120 seconds.
- If SmartConfig fails, fall back to AP Mode provisioning.
- Outline the event handling and logic needed to transition between these states.
- Design a conceptual state machine for an ESP32 device that attempts provisioning in a sequence:
Summary
- WiFi provisioning is crucial for configuring headless IoT devices with network credentials.
- Methods range from simple (hardcoding, UART) to user-friendly (WPS, SmartConfig) to more complex and robust (AP Mode, BLE).
- AP Mode (SoftAP) Provisioning is popular: ESP32 creates a temporary network, serves a web page for credential input, then switches to STA mode.
- BLE Provisioning offers secure, low-power credential transfer using a companion app.
- Persistent storage of credentials (e.g., in NVS) is vital for devices to reconnect after reboot.
- ESP-IDF provides tools for various provisioning methods, including HTTP server, DNS, and the Unified Provisioning (
protocomm
) framework for advanced scenarios. - Choosing the right method depends on user experience, security needs, device capabilities, and development complexity.
Further Reading
- ESP-IDF HTTP Server documentation: https://docs.espressif.com/projects/esp-idf/en/v5.4/esp32/api-reference/protocols/esp_http_server.html
- ESP-IDF WiFi Provisioning (
wifi_provisioning
manager): https://docs.espressif.com/projects/esp-idf/en/v5.4/esp32/api-reference/provisioning/wifi_provisioning.html (This builds onprotocomm
). - ESP-IDF Non-Volatile Storage (NVS) documentation: https://docs.espressif.com/projects/esp-idf/en/v5.4/esp32/api-reference/storage/nvs_flash.html
- ESP-IDF Bluetooth Low Energy (BLE) GATT Server Example:
$IDF_PATH/examples/bluetooth/bluedroid/ble/gatt_server
(for understanding BLE basics if considering BLE provisioning).
