Aggiornamenti OTA su ESP32 tramite browser web: firmware, filesystem e autenticazione – 1

Spread the love

Il microcontrollore ESP32 è una scelta popolare per la realizzazione di progetti IoT grazie al suo basso costo, basso consumo energetico e alle sue potenti capacità.

Una delle caratteristiche più importanti di qualsiasi dispositivo IoT è la possibilità di aggiornare il firmware e il filesystem tramite OTA (Over-The-Air) senza la necessità di accesso fisico.

In questo articolo, esploreremo come effettuare un aggiornamento OTA sull’ESP32 utilizzando un browser web, concentrando l’attenzione su tre aspetti chiave: aggiornamento del firmware, aggiornamento del filesystem e autenticazione. Discuteremo delle diverse sfide e delle considerazioni necessarie per ogni passaggio, fornendo esempi pratici e porzioni di codice per guidarti attraverso il processo. Alla fine della lettura, dovresti avere una buona comprensione di come implementare gli aggiornamenti OTA sui tuoi progetti basati su ESP32, rendendoli più flessibili e affidabili.

ESP32 OTA update with Web Browser: firmware, filesystem, and authentication
ESP32 OTA update with Web Browser: firmware, filesystem, and authentication

L’aggiornamento OTA (Over the Air) è il processo di caricamento del firmware su un modulo ESP32 utilizzando una connessione Wi-Fi anziché una porta seriale. Tale funzionalità diventa estremamente utile in caso di accesso fisico limitato o nullo al modulo.

Gli aggiornamenti OTA possono essere effettuati con le seguenti modalità:

  • IDE Arduino
  • Programma di navigazione in rete
  • Server HTTP

Prima di tutto, controlla il tutorial “ESP32: flash del firmware binario compilato (.bin)“.

In questo articolo, spiegheremo OTA tramite Web Browser, con e senza un’autenticazione basica.

Introduzione

Innanzitutto, osserviamo che il componente principale di ESP32 core ha bisogno di python installato e, durante l’installazione, ricordati di aggiungerlo alla PATH (per Windows)

ESP Tools Install Python and add It to path
ESP Tools Install Python and add It to path

Quindi vai a leggere come creare un file binario da questo articolo “ESP32: flash del firmware binario compilato (.bin)“.

Esempio di base con HTTPUpdateServer

Puoi usare HTTPUpdateServer e puoi trovare un esempio di base in File -> Examples -> HTTPUpdateServer -> WebUpdater.

ESP32 HTTPUpdateServer Web OTA update select library examples
ESP32 HTTPUpdateServer Web OTA update select library examples

Devi solo aggiungere il tuo SSID WiFi e la password.

/*
  To upload through terminal you can use: curl -F "image=@firmware.bin" esp32-webupdate.local/update
*/

#include <WiFi.h>
#include <WiFiClient.h>
#include <WebServer.h>
#include <ESPmDNS.h>
#include <HTTPUpdateServer.h>

#ifndef STASSID
#define STASSID "<YOUR-SSID>"
#define STAPSK  "<YOUR-PASSWD>"
#endif

const char* host = "esp32-webupdate";
const char* ssid = STASSID;
const char* password = STAPSK;

WebServer httpServer(80);
HTTPUpdateServer httpUpdater;

void setup(void) {

  Serial.begin(115200);
  Serial.println();
  Serial.println("Booting Sketch...");
  WiFi.mode(WIFI_AP_STA);
  WiFi.begin(ssid, password);

  while (WiFi.waitForConnectResult() != WL_CONNECTED) {
    WiFi.begin(ssid, password);
    Serial.println("WiFi failed, retrying.");
  }

  MDNS.begin(host);
  if (MDNS.begin("esp32")) {
    Serial.println("mDNS responder started");
  }


  httpUpdater.setup(&httpServer);
  httpServer.begin();

  MDNS.addService("http", "tcp", 80);
  Serial.printf("HTTPUpdateServer ready! Open http://%s.local/update in your browser\n", host);
}

void loop(void) {
  httpServer.handleClient();
}

Il nome host funziona grazie al servizio mDNS

  1. Avahi  https://avahi.org/  per Linux
  2. Bonjour  https://www.apple.com/support/bonjour/  per Windows
  3. Mac OSX e iOS: il supporto è già integrato / non è richiesto alcun software aggiuntivo

Sul monitor ArduinoIDE troverai questo output:

Booting Sketch...
mDNS responder started
HTTPUpdateServer ready! Open http://esp32-webupdate.local/update in your browser

E quando scrivi al browser l’URL, appare questa pagina web.

ESP32 HTTPUpdateServer Web OTA update basic web page
ESP32 HTTPUpdateServer Web OTA update basic web page

Ora cambieremo qualcosa nello sketch, come l’indirizzo in questo modo:

    Serial.printf("HTTPUpdateServer ready! Open http://%s.local/update in your browser\n", host);

ed esegui Sketch -> Export compiled binary, quando hai finito clicca su Sketch -> Show sketch folder, qui troviamo un file aggiuntivo chiamato WebUpdate.ino.esp32.bin.

Fare clic su Firmware -> Select file, selezionare questo file e avviare l’aggiornamento del firmware, attendere fino a quando viene visualizzata la pagina Aggiornamento riuscito! Riavvio…, dopo il riavvio, puoi vedere nell’output seriale il messaggio aggiornato:

Booting Sketch...
mDNS responder started
HTTPUpdateServer ready! Open http://esp32-webupdate.local/update in your browser

Se ricevi un messaggio asincrono come questo.

dhcps: send_nak>>udp_sendto result 0

Probabilmente mDNS non funziona per te e per recuperare l’IP modifica la riga in questo modo:

  IPAddress ip = WiFi.localIP();
  Serial.printf("HTTPUpdateServer ready! Open http://%s.local/update in your browser (%u.%u.%u.%u)\n", host, ip & 0xFF, (ip>>8) & 0xFF, (ip>>16) & 0xFF, (ip>>24) & 0xFF);

Ora hai qualcosa del genere:

HTTPUpdateServer ready! Open http://esp32-webupdate.local/update in your browser (192.168.1.119)

Ora spiegheremo come funziona tecnicamente.

HTTPUpdateServer esegue il wrapping del server Web e aggiunge un endpoint che in risposta GET con una semplice pagina

static const char serverIndex[] PROGMEM =
R"(<!DOCTYPE html>
     <html lang='en'>
     <head>
         <meta charset='utf-8'>
         <meta name='viewport' content='width=device-width,initial-scale=1'/>
     </head>
     <body>
     <form method='POST' action='' enctype='multipart/form-data'>
         Firmware:<br>
         <input type='file' accept='.bin,.bin.gz' name='firmware'>
         <input type='submit' value='Update Firmware'>
     </form>
     <form method='POST' action='' enctype='multipart/form-data'>
         FileSystem:<br>
         <input type='file' accept='.bin,.bin.gz,.image' name='filesystem'>
         <input type='submit' value='Update FileSystem'>
     </form>
     </body>
     </html>)";
static const char successResponse[] PROGMEM =
"<META http-equiv=\"refresh\" content=\"15;URL=/\">Update Success! Rebooting...";

Qui l’handle

        // handler for the /update form page
        _server->on(path.c_str(), HTTP_GET, [&]() {
            if (_username != emptyString && _password != emptyString && !_server->authenticate(_username.c_str(), _password.c_str()))
                return _server->requestAuthentication();
            _server->send_P(200, PSTR("text/html"), serverIndex);
            });

in POST, controlla il nome dell’input (firmware o filesystem) per capire se il file è per FLASH o FileSystem, quindi carica il file e aggiorna il servizio per fare il lavoro; ecco il codice in POST.

        // handler for the /update form POST (once file upload finishes)
        _server->on(path.c_str(), HTTP_POST, [&]() {
            if (!_authenticated)
                return _server->requestAuthentication();
            if (Update.hasError()) {
                _server->send(200, F("text/html"), String(F("Update error: ")) + _updaterError);
            }
            else {
                _server->client().setNoDelay(true);
                _server->send_P(200, PSTR("text/html"), successResponse);
                delay(100);
                _server->client().stop();
                ESP.restart();
            }
            }, [&]() {
                // handler for the file upload, get's the sketch bytes, and writes
                // them through the Update object
                HTTPUpload& upload = _server->upload();

                if (upload.status == UPLOAD_FILE_START) {
                    _updaterError.clear();
                    if (_serial_output)
                        Serial.setDebugOutput(true);

                    _authenticated = (_username == emptyString || _password == emptyString || _server->authenticate(_username.c_str(), _password.c_str()));
                    if (!_authenticated) {
                        if (_serial_output)
                            Serial.printf("Unauthenticated Update\n");
                        return;
                    }

                    if (_serial_output)
                        Serial.printf("Update: %s\n", upload.filename.c_str());
                    if (upload.name == "filesystem") {
                        if (!Update.begin(SPIFFS.totalBytes(), U_SPIFFS)) {//start with max available size
                            if (_serial_output) Update.printError(Serial);
                        }
                    }
                    else {
                        uint32_t maxSketchSpace = (ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000;
                        if (!Update.begin(maxSketchSpace, U_FLASH)) {//start with max available size
                            _setUpdaterError();
                        }
                    }
                }
                else if (_authenticated && upload.status == UPLOAD_FILE_WRITE && !_updaterError.length()) {
                    if (_serial_output) Serial.printf(".");
                    if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) {
                        _setUpdaterError();
                    }
                }
                else if (_authenticated && upload.status == UPLOAD_FILE_END && !_updaterError.length()) {
                    if (Update.end(true)) { //true to set the size to the current progress
                        if (_serial_output) Serial.printf("Update Success: %u\nRebooting...\n", upload.totalSize);
                    }
                    else {
                        _setUpdaterError();
                    }
                    if (_serial_output) Serial.setDebugOutput(false);
                }
                else if (_authenticated && upload.status == UPLOAD_FILE_ABORTED) {
                    Update.end();
                    if (_serial_output) Serial.println("Update was aborted");
                }
                delay(0);
            });

Per capire meglio ti consiglio di leggere “Come creare un Server Web con l’esp8266 e l’esp32” e “Come creare un REST server con esp8266 o esp32”.

Puoi vedere che il codice è banale e possiamo manipolarlo per farlo più bello, ma lo faremo dopo.

Esempio di base con codice nativo

Nella libreria ArduinoOTA, puoi ottenere un altro esempio di aggiornamento web con codice nativo.

#include <WiFi.h>
#include <WiFiClient.h>
#include <WebServer.h>
#include <ESPmDNS.h>
#include <Update.h>

const char* host = "esp32";
const char* ssid = "<YOUR-SSID>";
const char* password = "<YOUR-PASSWD>";

WebServer server(80);

/*
 * Login page
 */

const char* loginIndex =
 "<form name='loginForm'>"
    "<table width='20%' bgcolor='A09F9F' align='center'>"
        "<tr>"
            "<td colspan=2>"
                "<center><font size=4><b>ESP32 Login Page</b></font></center>"
                "<br>"
            "</td>"
            "<br>"
            "<br>"
        "</tr>"
        "<tr>"
             "<td>Username:</td>"
             "<td><input type='text' size=25 name='userid'><br></td>"
        "</tr>"
        "<br>"
        "<br>"
        "<tr>"
            "<td>Password:</td>"
            "<td><input type='Password' size=25 name='pwd'><br></td>"
            "<br>"
            "<br>"
        "</tr>"
        "<tr>"
            "<td><input type='submit' onclick='check(this.form)' value='Login'></td>"
        "</tr>"
    "</table>"
"</form>"
"<script>"
    "function check(form)"
    "{"
    "if(form.userid.value=='admin' && form.pwd.value=='admin')"
    "{"
    "window.open('/serverIndex')"
    "}"
    "else"
    "{"
    " alert('Error Password or Username')/*displays error message*/"
    "}"
    "}"
"</script>";

/*
 * Server Index Page
 */

const char* serverIndex =
"<script src='https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js'></script>"
"<form method='POST' action='#' enctype='multipart/form-data' id='upload_form'>"
   "<input type='file' name='update'>"
        "<input type='submit' value='Update'>"
    "</form>"
 "<div id='prg'>progress: 0%</div>"
 "<script>"
  "$('form').submit(function(e){"
  "e.preventDefault();"
  "var form = $('#upload_form')[0];"
  "var data = new FormData(form);"
  " $.ajax({"
  "url: '/update',"
  "type: 'POST',"
  "data: data,"
  "contentType: false,"
  "processData:false,"
  "xhr: function() {"
  "var xhr = new window.XMLHttpRequest();"
  "xhr.upload.addEventListener('progress', function(evt) {"
  "if (evt.lengthComputable) {"
  "var per = evt.loaded / evt.total;"
  "$('#prg').html('progress: ' + Math.round(per*100) + '%');"
  "}"
  "}, false);"
  "return xhr;"
  "},"
  "success:function(d, s) {"
  "console.log('success!')"
 "},"
 "error: function (a, b, c) {"
 "}"
 "});"
 "});"
 "</script>";

/*
 * setup function
 */
void setup(void) {
  Serial.begin(115200);

  // Connect to WiFi network
  WiFi.begin(ssid, password);
  Serial.println("");

  // Wait for connection
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.print("Connected to ");
  Serial.println(ssid);
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());

  /*use mdns for host name resolution*/
  if (!MDNS.begin(host)) { //http://esp32.local
    Serial.println("Error setting up MDNS responder!");
    while (1) {
      delay(1000);
    }
  }
  Serial.println("mDNS responder started");
  /*return index page which is stored in serverIndex */
  server.on("/", HTTP_GET, []() {
    server.sendHeader("Connection", "close");
    server.send(200, "text/html", loginIndex);
  });
  server.on("/serverIndex", HTTP_GET, []() {
    server.sendHeader("Connection", "close");
    server.send(200, "text/html", serverIndex);
  });
  /*handling uploading firmware file */
  server.on("/update", HTTP_POST, []() {
    server.sendHeader("Connection", "close");
    server.send(200, "text/plain", (Update.hasError()) ? "FAIL" : "OK");
    ESP.restart();
  }, []() {
    HTTPUpload& upload = server.upload();
    if (upload.status == UPLOAD_FILE_START) {
      Serial.printf("Update: %s\n", upload.filename.c_str());
      if (!Update.begin(UPDATE_SIZE_UNKNOWN)) { //start with max available size
        Update.printError(Serial);
      }
    } else if (upload.status == UPLOAD_FILE_WRITE) {
      /* flashing firmware to ESP*/
      if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) {
        Update.printError(Serial);
      }
    } else if (upload.status == UPLOAD_FILE_END) {
      if (Update.end(true)) { //true to set the size to the current progress
        Serial.printf("Update Success: %u\nRebooting...\n", upload.totalSize);
      } else {
        Update.printError(Serial);
      }
    }
  });
  server.begin();
}

void loop(void) {
  server.handleClient();
  delay(1);
}

Implementa una login fake (nessuna sicurezza per eseguire il controllo dell’accesso front-end), una semplice pagina Web simile alla pagina di aggiornamento di HTTPUpdateServer e l’endpoint REST in POST con gestione multipart/form-data.

La pagina è fornita per fare l’upload, funziona solo se c’è una connessione internet perché uso jQuery per gestire l’avanzamento e il POST, successivamente pubblico la mia pagina che usa VanillaJS senza una libreria per fare tutto.

La parte rilevante e interessante di questo semplice sketch è la classe Update. Questo è il nucleo dell’aggiornamento OTA e deve avviare la sessione

      if (!Update.begin(UPDATE_SIZE_UNKNOWN)) { //start with max available size

quindi può gestire un flusso di dati generico (ereditare una classe Stream di base)

      if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) {

e alla fine chiudere la sessione e riavviare il dispositivo per effettuare la modifica del firmware.

      if (Update.end(true)) { //true to set the size to the current progress

Ma scoraggio l’uso della classe nativa per eseguire aggiornamenti web meglio un’implementazione della libreria che viene mantenuta quando si verifica un cambiamento, qualcuno aggiorna il codice per te.

Scoraggio l’uso di comandi nativi se esiste una libreria che viene mantenuta per te.

Generare il file system (SPIFFS, LittleFS e FFat)

Una caratteristica interessante è la gestione dei File System come il firmware compilato.

Aggiungi output dettagliato all’IDE di Arduino

Per comprendere meglio tutti i processi, abiliteremo l’output dettagliato sul nostro IDE di Arduino. Puoi trovare queste opzioni su File -> Preferences e controllare i controlli Show verbose output.

Arduino IDE enable show verbose ouput
Arduino IDE enable show verbose ouput

Ora possiamo prendere e riutilizzare il comando della console.

Caricare file system (SPIFFS, LittleFS o FFatFS)

Puoi anche caricare i dati del filesystem con il metodo classico tramite il plugin, per installare il plugin SPIFFS, LittleFS o FFat, fai riferimento al relativo tutorial:

Dopo tale operazione, puoi utilizzare il plug-in come al solito.

Arduino IDE esp32 SPIFFS Sketch Data Upload
Arduino IDE esp32 SPIFFS Sketch Data Upload
esp32 SPIFFS LittleFS FatFS file uploader from Arduino IDE
esp32 SPIFFS LittleFS FatFS file uploader from Arduino IDE

Puoi controllare l’output della console dell’IDE per verificare cosa è successo.

Chip : esp32
Using partition scheme from Arduino IDE.
Start: 0x290000
Size : 0x170000
mkspiffs : C:\Users\renzo\AppData\Local\Arduino15\packages\esp32\tools\mkspiffs\0.2.3\mkspiffs.exe

espota : C:\Users\renzo\AppData\Local\Arduino15\packages\esp32\hardware\esp32\2.0.0\tools\espota.exe

[SPIFFS] data   : D:\Projects\Arduino\sloeber-workspace-OTA\ArduinoOTAesp32_basic_arduino\data
[SPIFFS] offset : 0
[SPIFFS] start  : 2686976
[SPIFFS] size   : 1472
[SPIFFS] page   : 256
[SPIFFS] block  : 4096
->/version.txt
[SPIFFS] upload : C:\Users\renzo\AppData\Local\Temp\arduino_build_258074/ArduinoOTAesp32_basic_arduino.spiffs.bin
[SPIFFS] IP     : 192.168.1.186
Running: C:\Users\renzo\AppData\Local\Arduino15\packages\esp32\hardware\esp32\2.0.0\tools\espota.exe -i 192.168.1.186 -p 3232 -s -f C:\Users\renzo\AppData\Local\Temp\arduino_build_258074/ArduinoOTAesp32_basic_arduino.spiffs.bin

_>Sending invitation to 192.168.1.186 
_>Uploading................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................
_>09:21:38 [ERROR]: Error response from device
SPIFFS Upload failed!

La console dell’IDE restituisce un errore, ma il caricamento funziona. Penso che ora ci sia un piccolo bug quando scrivo questo articolo.

IDE dà un errore ma è un bug, il caricamento funziona correttamente

E sul microcontrollore

[SPIFFS] upload : C:\Users\renzo\AppData\Local\Temp\arduino_build_258074/ArduinoOTAesp32_basic_arduino.spiffs.bin

Aggiungere l’autenticazione di base

L’ESP8266HTTPUpdateServer implementa la Basic Authentication (già spiegata in questa sezione del tutorial WebServer). È elementare, cambieremo questa riga di codice

  httpUpdater.setup(&httpServer, "/update", "mischianti", "passwd");

Nella riga di codice, puoi vedere 3 nuovi parametri,

  • "/update": è il percorso dell’aggiornamento OTA, se si modifica questo percorso l’endpoint e la pagina cambiano;
  • "mischianti": è la login che devi inserire;
  • "passwd": è la password.

Quando provi a inserire l’URL /update, viene visualizzato un popup di accesso

ESP32 HTTPUpdateServer Web OTA update basic authentication
ESP32 HTTPUpdateServer Web OTA update basic authentication

Se inserisci un login o una password errati, ricevi lo stato HTTP standard 401.

Grazie

  1. ESP32: piedinatura, specifiche e configurazione dell’Arduino IDE
  2. ESP32: fileSystem integrato SPIFFS
  3. ESP32: gestire più seriali e logging per il debug
  4. ESP32 risparmio energetico pratico
    1. ESP32 risparmio energetico pratico: gestire WiFi e CPU
    2. ESP32 risparmio energetico pratico: modem e light sleep
    3. ESP32 risparmio energetico pratico: deep sleep e ibernazione
    4. ESP32 risparmio energetico pratico: preservare dati al riavvio, sveglia a tempo e tramite tocco
    5. ESP32 risparmio energetico pratico: sveglia esterna e da ULP
    6. ESP32 risparmio energetico pratico: sveglia da UART e GPIO
  5. ESP32: filesystem integrato LittleFS
  6. ESP32: filesystem integrato FFat (Fat/exFAT)
  7. ESP32-wroom-32
    1. ESP32-wroom-32: flash, piedinatura, specifiche e configurazione dell’Arduino IDE
  8. ESP32-CAM
    1. ESP32-CAM: piedinatura, specifiche e configurazione dell’Arduino IDE
    2. ESP32-CAM: upgrade CamerWebServer con gestione della luce flash
  9. ESP32: ethernet w5500 con chiamate standard (HTTP) e SSL (HTTPS)
  10. ESP32: ethernet enc28j60 con chiamate standard (HTTP) e SSL (HTTPS)
  11. Come usare la scheda SD con l’esp32
  12. esp32 e esp8266: file system FAT su memoria SPI flash esterna
  13. Gestione aggiornamenti firmware e OTA
    1. Gestione del firmware
      1. ESP32: flash del firmware binario compilato (.bin)
      2. ESP32: flash del firmware e filesystem (.bin) con strumenti grafici
    2. Aggiornamento OTA con Arduino IDE
      1. Aggiornamenti OTA su ESP32 con Arduino IDE: filesystem, firmware e password
    3. Aggiornamento OTA con browser web
      1. Aggiornamenti OTA su ESP32 tramite browser web: firmware, filesystem e autenticazione
      2. Aggiornamenti OTA su ESP32 tramite browser web: caricamento in HTTPS (SSL/TLS) con certificato autofirmato
      3. Aggiornamenti OTA su ESP32 tramite browser web: interfaccia web personalizzata
    4. Aggiornamenti automatici OTA da un server HTTP
      1. Aggiornamento automatico Firmware OTA dell’ESP32 dal server
      2. Aggiornamento automatico Firmware OTA dell’ESP32 dal server con controllo della versione
      3. Aggiornamento automatico Firmware OTA dell’ESP32 in HTTPS (SSL/TLS) con certificato autofirmato affidabile
    5. Aggiornamento del firmware non standard
      1. Aggiornamento firmware e filesystem ESP32 dalla scheda SD
      2. Aggiornamento firmware e filesystem ESP32 con client FTP
  14. Integrare LAN8720 con ESP32 per la connettività Ethernet con plain (HTTP) e SSL (HTTPS)
  15. Collegare l’EByte E70 (CC1310) ai dispositivi ESP32 c3/s3 ed un semplice sketch di esempio
  16. ESP32-C3: piedinatura, specifiche e configurazione dell’IDE Arduino
  17. i2c esp32: gestione rete a 5v, 3.3v e interfaccia aggiuntiva
  18. […]

Spread the love

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *