Gabriel Cachadiña

Interfaz Web para almacenar variables no volátiles en un ESP32

· Gabriel Cachadiña

Actualmente estoy realizando un proyecto con el chip ESP32-C3 que tiene que conectarse a un punto Wi-Fi mediante su antena integrada y mandar una serie de datos por el protocolo MQTT. Para conseguir esto he guardado el usuario y la contraseña del punto en varias globales que son usadas por la librería esp_wifi.h lo cual es una solución válida si no deseamos que esta información cambie nunca. Sin embargo, para mi proyecto necesito una solución que:

  1. Me permita cambiar estas variables mientras el código está funcionando.
  2. Le permita al usuario hacerlo sin tener que volver a compilar el código.
  3. Se guarde en una memoria no volátil, para que, en caso de un corte de energía, la información permanezca en el controlador

En este post describo mi solución a estos problemas, pasando por alternativas que podrían ser igualmente válidas para el lector en el caso de este lo requiera.

Interfaz de comunicación

Como ya he descrito, en mi caso necesito que las variables de conexión al punto Wi-Fi sean variables, para poder funcionar en cualquier instalación que se desee. En primer lugar debemos pensar en cómo interactuará el usuario con nuestro dispositivo. Las principales interfaces que ofrece el chip son:

Programación

Una vez decidido el canal, debemos determinar la forma en la que los datos se enviarán al dispositivo. Una forma técnica sería enviando una petición HTTP mediante un post con la información que se desee, pero no es una solución fácil de explicar a un usuario no técnico, es por ello que he decidido que el mismo ESP32 tenga una página web donde se puedan configurar las variables. Esta solución tendrá la siguiente forma:

HotSpot

  1. En primer lugar iniciaremos el punto de acceso:
 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, "Wi-Fi AP started. SSID='%s', IP=192.168.4.1", WIFI_SSID_AP);
28}

Páginas

En segundo lugar tendremos el web server, que tendrá a su vez 2 páginas, la página de registro de credenciales y la página encargada de guardarlas.

 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}

Página de credenciales

Página donde se introducirán las credenciales

 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}

Página de almacenado

Página donde se ejecutará la función de almacenado de las variables de la forma que se desee, en este caso en la memoria NVS que se explicará más adelante:

 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}

Memoria permanente

Ahora mismo tenemos una solución que puede obtener los datos que un usuario introduzca a través de una interfaz web, pero, ¿cómo almacenamos estos datos? Usar una variable que pasemos por distintas funciones podría ser una posible solución, pero esta no sería resistente ante reinicios del ESP, lo cual provocaría la pérdida de la información almacenada en memoria.

Por estos motivos es por los que decido almacenar las variables introducidas en la interfaz web dentro de la memoria no volátil, la memoria NVS. Esta memoria se encuentra en la Flash del ESP32 y es resistente a reinicios del sistema. Para esto crearemos 2 funciones, una para leer los datos de la memoria NVS y otro para almacenar nuevos datos.

Lectura de memoria NVS

En primer lugar tendremos una función que cargará de leer la memoria no volátil para ver si se encuentran variables ya guardadas dentro de la sección de wifi_creds. Si no es el caso yo he considerado como una buena opción retornar unas variables por defecto con estos valores.

 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}

Escritura en la memoria NVS

Por otro lado si se desean modificar estas variables se accederá a esos zonas de memoria y se introducirán los nuevos datos.

 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}

Conclusión

Cabe destacar que la solución aquí mencionada cuenta con una serie de vulnerabilidades a tomar en cuenta. En primer lugar se tiene un punto de acceso Wi-Fi constantemente abierto, lo cual puede suponer un riesgo de seguridad, ya que, cualquier usuario que llegue a obtener las credenciales puede interactuar con esta interfaz web y sacar de la misma las credenciales que se están usando actualmente. Por ello recomiendo usar un GPIO que funcione como Jumper para habilitar o no esta opción. Un ejemplo de esta configuración sería:

 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    }

Además de esto el código del ESP32 actualmente no se encuentra encriptado, por lo que cualquier usuario podría leer la memoria del mismo y extraer su funcionamiento/obtener las credenciales que deseara. La memoria del ESP puede ser encriptada para este tipo de casos, pero no se documentará en este post.

Con esto dispones de una solución que permite a un usuario no técnico configurar variables almacenadas en la memoria de un ESP32 y garantizando la permanencia de los datos. A continuación muestro un esquema del sistema que utiliza todas las funciones anteriormente descritas y que, en caso de conectarse al punto Wi-Fi realiza una serie de acciones hasta hacer un DeepSleep (esencialmente un reinicio).

esphotspotnvs

#esp32 #electronics

Reply to this post by email ↪