Gabriel Cachadiña

Web Interface for Storing Non-Volatile Variables on an ESP32

· Gabriel Cachadiña

I am currently working on a project using the ESP32-C3 chip that needs to connect to a Wi-Fi access point using its integrated antenna and send a set of data through the MQTT protocol. To achieve this, I initially stored the access point username and password in several global variables used by the esp_wifi.h library. This is a valid solution if we do not expect this information to ever change.

However, for my project I need a solution that:

  1. Allows these variables to be changed while the code is running.
  2. Lets the user modify them without recompiling the firmware.
  3. Stores them in non-volatile memory so that, in case of a power loss, the information remains in the controller.

In this post I describe my solution to these problems, also reviewing alternatives that could be equally valid depending on the reader’s requirements.

Communication Interface

As mentioned earlier, in my case the Wi-Fi connection parameters must be configurable so the device can operate in any installation environment. The first step is to consider how the user will interact with our device. The main interfaces provided by the chip are:

After considering these alternatives, I decided to use a Hotspot, since—unlike the other solutions—it does not require any additional hardware or software (cables, programs, or external devices). It allows the variables to be modified using only a Wi-Fi capable device such as a smartphone.

Implementation

Once the communication channel has been chosen, we must determine how the data will be sent to the device. A technical approach would be to send an HTTP request with a POST containing the desired information. However, this is not an easy solution to explain to a non-technical user. For this reason, I decided that the ESP32 itself should host a small web page where the variables can be configured.

This solution will work as follows:

HotSpot

First, we start the access point:

 1// -.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-
 2//              Access Point
 3// -.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-
 4void wifi_init_softap(void) {
 5    esp_netif_create_default_wifi_ap();
 6    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
 7    ESP_ERROR_CHECK(esp_wifi_init(&cfg));
 8
 9    wifi_config_t wifi_config = {
10        .ap = {
11            .ssid = WIFI_SSID_AP,
12            .ssid_len = strlen(WIFI_SSID_AP),
13            .channel = 1,
14            .password = WIFI_PASS_AP,
15            .max_connection = MAX_STA_CONN,
16            .authmode = WIFI_AUTH_WPA_WPA2_PSK
17        }
18    };
19    if (strlen(WIFI_PASS_AP) == 0) {
20        wifi_config.ap.authmode = WIFI_AUTH_OPEN;
21    }
22
23    ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_AP));
24    ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP, &wifi_config));
25    ESP_ERROR_CHECK(esp_wifi_start());
26
27    ESP_LOGI(TAG, "WiFi AP started. SSID='%s', IP=192.168.4.1", WIFI_SSID_AP);
28}

Web Pages

Next, we create the web server, which will provide two pages, the credentials configuration page and the page responsible for saving them.

 1// -.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-
 2//              WebServer
 3// -.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-
 4httpd_handle_t start_webserver(void) {
 5    httpd_config_t config = HTTPD_DEFAULT_CONFIG();
 6    httpd_handle_t server = NULL;
 7    if (httpd_start(&server, &config) == ESP_OK) {
 8        ESP_LOGI(TAG, "HTTP server started");
 9
10        httpd_uri_t root = {
11            .uri      = "/",
12            .method   = HTTP_GET,
13            .handler  = root_get_handler,
14            .user_ctx = NULL
15        };
16        httpd_register_uri_handler(server, &root);
17
18        httpd_uri_t submit = {
19            .uri      = "/submit",
20            .method   = HTTP_POST,
21            .handler  = submit_post_handler,
22            .user_ctx = NULL
23        };
24        httpd_register_uri_handler(server, &submit);
25    } else {
26        ESP_LOGE(TAG, "Failed to start HTTP server");
27    }
28    return server;
29}

Credentials Page

This page is where the Wi-Fi credentials will be entered.

 1// -.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-
 2//              HTML Website
 3// -.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-
 4static char html_page[512];
 5
 6static void build_html_page(const char *current_ssid, const char *current_pass) {
 7    snprintf(html_page, sizeof(html_page),
 8        "<!DOCTYPE html>"
 9        "<html>"
10        "<head><title>ESP32 Config</title></head>"
11        "<body>"
12        "<h2>ESP32 Configuration Portal</h2>"
13        "<p>Current Wi-Fi:</p>"
14        "<ul><li>SSID: %s</li><li>Password: %s</li></ul>"
15        "<form method=\"POST\" action=\"/submit\">"
16        "New SSID:<br>"
17        "<input type=\"text\" name=\"ssid\"><br><br>"
18        "New Password:<br>"
19        "<input type=\"password\" name=\"password\"><br><br>"
20        "<input type=\"submit\" value=\"Save\">"
21        "</form>"
22        "</body>"
23        "</html>",
24        current_ssid[0] ? current_ssid : "Not set",
25        current_pass[0] ? current_pass : "Not set");
26}
27
28
29// -.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-
30//              HTTP Handlers
31// -.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-
32static esp_err_t root_get_handler(httpd_req_t *req) {
33    char ssid[32], password[64];
34    load_saved_wifi(ssid, sizeof(ssid), password, sizeof(password));
35
36    // Print to serial
37    ESP_LOGI(TAG, "Current Wi-Fi: SSID='%s', PASSWORD='%s'", ssid, password);
38
39    build_html_page(ssid, password);
40
41    httpd_resp_set_type(req, "text/html");
42    httpd_resp_send(req, html_page, strlen(html_page));
43    return ESP_OK;
44}

Storage Page

This page executes the function responsible for storing the variables in the desired way. In this case, they will be saved in NVS memory, which will be explained later.

 1static esp_err_t submit_post_handler(httpd_req_t *req) {
 2    char content[200];
 3    int ret = httpd_req_recv(req, content, sizeof(content) - 1);  // Get data from the last page
 4    if (ret <= 0) {
 5        httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to receive POST data");
 6        return ESP_FAIL;
 7    }
 8    content[ret] = '\0';
 9
10    // Parse form "ssid=<ssid>&password=<pass>"
11    char ssid[32] = {0};
12    char password[64] = {0};
13    sscanf(content, "ssid=%31[^&]&password=%63s", ssid, password);
14    for (int i = 0; ssid[i]; i++) if (ssid[i] == '+') ssid[i] = ' ';
15    for (int i = 0; password[i]; i++) if (password[i] == '+') password[i] = ' ';
16
17    // Save to NVS and print
18    save_wifi_credentials(ssid, password);
19
20    // Confirmation page
21    const char* response =
22        "<html><body>"
23        "<h3>Credentials Saved!</h3>"
24        "<a href=\"/\">Go Back</a>"
25        "</body></html>";
26    httpd_resp_sendstr(req, response);
27    return ESP_OK;
28}

Permanent Storage

At this point we have a solution that can obtain the data entered by a user through a web interface. But how do we store this data? Using a variable that we pass between different functions could be a possible solution, but it would not survive an ESP restart, which would cause the information stored in memory to be lost.

For this reason, I decided to store the variables entered through the web interface in non-volatile memory, specifically the NVS memory. This memory resides in the ESP32’s flash and persists across system restarts.

To accomplish this, we will create two functions: one to read data from NVS memory and another to store new data.

Reading from NVS Memory

First, we implement a function responsible for reading the non-volatile memory to check whether variables are already stored inside the wifi_creds namespace. If no stored values are found, I considered it a good approach to return default values.

 1static void load_saved_wifi(char *ssid, size_t ssid_len, char *pass, size_t pass_len) {
 2    nvs_handle_t nvs_handle;
 3    esp_err_t err = nvs_open("wifi_creds", NVS_READONLY, &nvs_handle);  // Open non-volatile storage with a given namespace from the default NVS partition
 4    if (err == ESP_OK) { // Namespace Exists
 5        size_t required_size;
 6        required_size = ssid_len;
 7        if (nvs_get_str(nvs_handle, "ssid", ssid, &required_size) != ESP_OK) { // Get string value for given key
 8            strncpy(ssid, WIFI_SSID, ssid_len);
 9        }
10
11        required_size = pass_len;
12        if (nvs_get_str(nvs_handle, "password", pass, &required_size) != ESP_OK) { // Get string value for given key
13            strncpy(pass, WIFI_PASSWORD, pass_len);
14        }
15
16        nvs_close(nvs_handle);
17    } else {
18        // If namespace does not exist
19        // Set Default values as a constant
20        strncpy(ssid, WIFI_SSID, ssid_len);
21        strncpy(pass, WIFI_PASSWORD, pass_len);
22    }
23
24    ESP_LOGI(TAG, "Using -> SSID='%s', PASS='%s'", ssid, pass);
25}

Writing to NVS Memory

On the other hand, when these variables need to be modified, we access the same memory region and store the new values.

 1static void save_wifi_credentials(const char *ssid, const char *pass) {
 2    nvs_handle_t nvs_handle;
 3    if (nvs_open("wifi_creds", NVS_READWRITE, &nvs_handle) == ESP_OK) {
 4        nvs_set_str(nvs_handle, "ssid", ssid);
 5        nvs_set_str(nvs_handle, "password", pass);
 6        nvs_commit(nvs_handle);
 7        nvs_close(nvs_handle);
 8        ESP_LOGI(TAG, "Saved credentials to NVS: SSID='%s', PASS='%s'", ssid, pass);
 9    }
10}

Conclusion

It is important to note that the solution presented here includes several vulnerabilities that should be taken into account. First, there is a Wi-Fi access point that remains constantly open, which may represent a security risk. Any user who obtains the credentials could interact with the web interface and retrieve the credentials currently being used.

For this reason, I recommend using a GPIO configured as a jumper to enable or disable this feature. An example of such a configuration would be:

 1    // -.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-
 2    //              HotSpot
 3    // -.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-
 4    // Initialize NVS
 5    ESP_ERROR_CHECK(nvs_flash_init());                // Initialize the default NVS partition and check for errors
 6
 7    // Jumper that defines if the Hotspot should be active
 8    gpio_config_t io_conf = {
 9      .pin_bit_mask = (1ULL << HOTSPOT_PIN),
10      .mode = GPIO_MODE_INPUT,
11      .pull_up_en = GPIO_PULLUP_ENABLE,
12      .pull_down_en = GPIO_PULLDOWN_DISABLE,
13      .intr_type = GPIO_INTR_DISABLE
14    };
15    gpio_config(&io_conf);
16    int jumper_hotspot = gpio_get_level(HOTSPOT_PIN);
17    if (jumper_hotspot == 0) { //Check if the jumper cable is active
18        wifi_init_softap();
19        start_webserver();
20        while (gpio_get_level(HOTSPOT_PIN) == 0) {
21            vTaskDelay(pdMS_TO_TICKS(100)); // Keep the code here while the user changes the nvs memory
22        }
23        // If jumper removed → reboot cleanly
24        esp_restart();
25    }

In addition to this, the ESP32 firmware is currently not encrypted, meaning that any user could read the device’s memory and extract its functionality or obtain stored credentials. The ESP’s flash memory can be encrypted for these types of scenarios, but that process will not be covered in this post.

With this approach, you now have a solution that allows a non-technical user to configure variables stored in an ESP32’s memory while ensuring that the data remains persistent.

Below is a diagram of the system that uses all the functions described above. When the device successfully connects to the Wi-Fi access point, it performs a sequence of actions until entering Deep Sleep (essentially a controlled restart).

esphotspotnvs

#esp32 #electronics

Reply to this post by email ↪