Web Server con esp8266 e esp32: web server generico multiuso – 3

Spread the love

Ora che abbiamo capito come servire a una pagina o un’immagine compressa, sappiamo tutto su come creare un server web generico.

WebServer Esp8266 ESP32 distribution entire site
WebServer Esp8266 ESP32 distribution entire site

Vado a creare un repository git con l’esempio completo, aggiungo lo sketch e vado a spiegare le singole righe di codice.

Per l’esp32 è necessario modificare solo queste righe

#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <FS.h>
[...]
ESP8266WebServer server(80);

in

#include <WiFi.h>
#include <WebServer.h>
#include <SPIFFS.h>
[...]
WebServer server(80);

Ma fai attenzione ora sul WiFiClient di ESP32 c’è un grosso bug, puoi controllare qui , quindi se provi ad avviare questo server avrai qualche problema in presenza di molte richieste simultanee. Perciò ho scritto un porting di questo sketch usando la libreria ESPAsyncWebServer che usa l’AsyncTCP  dove non si presenta questo problema. Ma ora spiego la soluzione attuale per l’esp8266 e alla fine dell’articolo la soluzione equivalente per ESP32.

Risolto sulla versione 1.0.5 dell’esp32 core.

Qui lo sketch funzionante per l’esp8266 e per ora non funziona con esp32.

/*
 *  WeMos D1 mini (esp8266)
 *  Simple web server that read from SPIFFS and
 *  stream on browser various type of file
 *  and manage gzip format also
 *
 *  by Mischianti Renzo <https://mischianti.org>
 *
 *  https://mischianti.org/category/tutorial/how-to-create-a-web-server-with-esp8266-and-esp32/
 *
 */
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <FS.h>

const char* ssid = "<your-ssid>";
const char* password = "<your-passwd>";

ESP8266WebServer httpServer(80);


bool loadFromSPIFFS(String path, String dataType) {
  Serial.print("Requested page -> ");
  Serial.println(path);
  if (SPIFFS.exists(path)){
	  File dataFile = SPIFFS.open(path, "r");
	  if (!dataFile) {
		  handleNotFound();
		  return false;
	  }

	  if (httpServer.streamFile(dataFile, dataType) != dataFile.size()) {
	    Serial.println("Sent less data than expected!");
	  }else{
		  Serial.println("Page served!");
	  }

	  dataFile.close();
  }else{
	  handleNotFound();
	  return false;
  }
  return true;
}

void serverRouting();

void setup(void) {
  Serial.begin(115200);
  WiFi.mode(WIFI_STA);
  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());

  Serial.print(F("Inizializing FS..."));
  if (SPIFFS.begin()){
	Serial.println(F("done."));
  }else{
	Serial.println(F("fail."));
  }

  Serial.println("Set routing for http server!");
  serverRouting();
  httpServer.begin();
  Serial.println("HTTP server started");
}

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

String getContentType(String filename){
  if(filename.endsWith(F(".htm"))) 			return F("text/html");
  else if(filename.endsWith(F(".html"))) 	return F("text/html");
  else if(filename.endsWith(F(".css"))) 	return F("text/css");
  else if(filename.endsWith(F(".js"))) 		return F("application/javascript");
  else if(filename.endsWith(F(".json"))) 	return F("application/json");
  else if(filename.endsWith(F(".png"))) 	return F("image/png");
  else if(filename.endsWith(F(".gif"))) 	return F("image/gif");
  else if(filename.endsWith(F(".jpg"))) 	return F("image/jpeg");
  else if(filename.endsWith(F(".jpeg"))) 	return F("image/jpeg");
  else if(filename.endsWith(F(".ico"))) 	return F("image/x-icon");
  else if(filename.endsWith(F(".xml"))) 	return F("text/xml");
  else if(filename.endsWith(F(".pdf"))) 	return F("application/x-pdf");
  else if(filename.endsWith(F(".zip"))) 	return F("application/x-zip");
  else if(filename.endsWith(F(".gz"))) 		return F("application/x-gzip");
  return F("text/plain");
}

bool handleFileRead(String path){
  Serial.print(F("handleFileRead: "));
  Serial.println(path);

  if(path.endsWith("/")) path += F("index.html");           // If a folder is requested, send the index file
  String contentType = getContentType(path);             	// Get the MIME type
  String pathWithGz = path + F(".gz");
  if(SPIFFS.exists(pathWithGz) || SPIFFS.exists(path)){  	// If the file exists, either as a compressed archive, or normal
    if(SPIFFS.exists(pathWithGz))                          	// If there's a compressed version available
      path += F(".gz");                                     // Use the compressed version
    fs::File file = SPIFFS.open(path, "r");                 // Open the file
    size_t sent = httpServer.streamFile(file, contentType); // Send it to the client
    file.close();                                          	// Close the file again
    Serial.println(String(F("\tSent file: ")) + path + String(F(" of size ")) + sent);
    return true;
  }
  Serial.println(String(F("\tFile Not Found: ")) + path);
  return false;                                          	// If the file doesn't exist, return false
}

void handleNotFound() {
  String message = "File Not Found\n\n";
  message += "URI: ";
  message += httpServer.uri();
  message += "\nMethod: ";
  message += (httpServer.method() == HTTP_GET) ? "GET" : "POST";
  message += "\nArguments: ";
  message += httpServer.args();
  message += "\n";

  for (uint8_t i = 0; i < httpServer.args(); i++) {
    message += " " + httpServer.argName(i) + ": " + httpServer.arg(i) + "\n";
  }

  httpServer.send(404, "text/plain", message);
}

void serverRouting() {
	  httpServer.onNotFound([]() {                              // If the client requests any URI
		  Serial.println(F("On not found"));
	    if (!handleFileRead(httpServer.uri())){                  // send it if it exists
	    	handleNotFound(); // otherwise, respond with a 404 (Not Found) error
	    }
	  });

	  Serial.println(F("Set cache!"));
	  // Serve a file with no cache so every tile It's downloaded
	  httpServer.serveStatic("/configuration.json", SPIFFS, "/configuration.json","no-cache, no-store, must-revalidate");
	  // Server all other page with long cache so browser chaching they
	  httpServer.serveStatic("/", SPIFFS, "/","max-age=31536000");


}

Per prima cosa devi ricordare, come scrivo qui “WeMos D1 mini (esp8266), integrated SPIFFS Filesystem” o qui per esp32 “ESP32: integrated SPIFFS FileSystem”, che il filesystem SPIFFS ha qualche limitazione: non sono ammesse cartelle, nell’articolo c’è anche la procedura per caricare i dati con l’Arduino IDE.

Visto quanto detto andrai a creare un sito in una struttura piatta come questa:

Flat structure for WebServer site on esp8266 in SPIFFS
Flat structure for WebServer site on esp8266 in SPIFFS

senza cartella e con una lunghezza dei files corretta.

Il risultato:

Site template served from esp8266 WebServer
Site template served from esp8266 WebServer

Tutta la gestione dei files è in questo codice

	  httpServer.onNotFound([]() {                              // If the client requests any URI
		  Serial.println(F("On not found"));
	    if (!handleFileRead(httpServer.uri())){                  // send it if it exists
	    	handleNotFound(); // otherwise, respond with a 404 (Not Found) error
	    }
	  });

handleFileRead(httpServer.uri()) controlla l’URL richiesto e all’interno ottiene il tipo MIME corretto tramite l’estensione del file.

Puoi aprire gli strumenti di sviluppo dal tuo browser con il pulsante F12, qui puoi vedere le risorse servite dall’esp8266 nella scheda “Rete”.

Network of site template served from esp8266 WebServer
Network of site template served from esp8266 WebServer

Anche se non scrivi index.html va automaticamente a quella pagina, il codice che se ne occupa è

  if(path.endsWith("/")) path += F("index.html");           // If a folder is requested, send the index file

Ora come puoi vedere l’esp8266 gestisce molti tipi di file, puoi controllare quale tipo di file può gestire in questa funzione:

String getContentType(String filename){
  if(filename.endsWith(F(".htm"))) 			return F("text/html");
  else if(filename.endsWith(F(".html"))) 	return F("text/html");
  else if(filename.endsWith(F(".css"))) 	return F("text/css");
  else if(filename.endsWith(F(".js"))) 		return F("application/javascript");
  else if(filename.endsWith(F(".json"))) 	return F("application/json");
  else if(filename.endsWith(F(".png"))) 	return F("image/png");
  else if(filename.endsWith(F(".gif"))) 	return F("image/gif");
  else if(filename.endsWith(F(".jpg"))) 	return F("image/jpeg");
  else if(filename.endsWith(F(".jpeg"))) 	return F("image/jpeg");
  else if(filename.endsWith(F(".ico"))) 	return F("image/x-icon");
  else if(filename.endsWith(F(".xml"))) 	return F("text/xml");
  else if(filename.endsWith(F(".pdf"))) 	return F("application/x-pdf");
  else if(filename.endsWith(F(".zip"))) 	return F("application/x-zip");
  else if(filename.endsWith(F(".gz"))) 		return F("application/x-gzip");
  return F("text/plain");
}

Memorizza il sito nella cache

Una caratteristica importante offerta dal browser è la possibilità di memorizzare nella cache locale i files, questo codice da le specifiche al browser:

// Serve a file with no cache so every tile It's downloaded
	  httpServer.serveStatic("/configuration.json", SPIFFS, "/configuration.json","no-cache, no-store, must-revalidate");
	  // Server all other page with long cache so browser chaching they
	  httpServer.serveStatic("/", SPIFFS, "/","max-age=31536000");

nel codice trovi 2 tipi di gestione, il file configuration.json non è mai nella cache, quindi viene scaricato dal dispositivo ogni volta, tutti gli altri file sotto / hanno timeout infinito.

Puoi verificarlo premendo il tasto F5 con il tab Network di Chrome aperto, la prima volta per tutti i file hai la dimensione sulla Size colonna, la seconda volta hai una descrizione (disk cache).

Fai attenzione: se vuoi ricaricare tutto il contenuto perché hai fatto una modifica su un file devi rimuovere la cache, se l’hai gli strumenti per sviluppatore aperti puoi tenere premuto il pulsante di aggiornamento del browser (Chrome) e selezionare la voce di menu che specifica di rimuovere la cache.

Gestire i contenuti gzip

Adesso possiamo andare a testare una funzionalità aggiuntiva, la gestione del file in formato gzip:

  String pathWithGz = path + F(".gz");
  if(SPIFFS.exists(pathWithGz) || SPIFFS.exists(path)){  	// If the file exists, either as a compressed archive, or normal
    if(SPIFFS.exists(pathWithGz))                          	// If there's a compressed version available
      path += F(".gz");                                     // Use the compressed version

dopo il controllo del tipo mime il codice controlla se esiste una versione gzippata del file.

Quindi ora andremo a lanciare il gzip su tutti i file:

gzip *

per Windows puoi usare Cygwin o scaricare l’ implementazione di gzip per windows.

Il risultato è semplice

app.js			->		app.js.gz
bootstrap.min.css	->		bootstrap.min.css.gz
bootstrap.min.js	->		bootstrap.min.js.gz
bridge-theme.css	->		bridge-theme.css.gz
CREDITS.txt		->		CREDITS.txt.gz
custom.js		->		custom.js.gz
dark-blue-theme.css	->		dark-blue-theme.css.gz
dark-red-theme.css	->		dark-red-theme.css.gz
default-theme.css	->		default-theme.css.gz
favicon.ico		->		favicon.ico.gz
font-awesome.min.css	->		font-awesome.min.css.gz
green-theme.css		->		green-theme.css.gz
imagelightbox.min.css	->		imagelightbox.min.css.gz
img-1.jpeg		->		img-1.jpeg.gz
img-2.jpeg		->		img-2.jpeg.gz
img-3.jpeg		->		img-3.jpeg.gz
img-4.jpeg		->		img-4.jpeg.gz
img-5.jpeg		->		img-5.jpeg.gz
img-6.jpeg		->		img-6.jpeg.gz
img-7.jpg		->		img-7.jpg.gz
img-8.jpeg		->		img-8.jpeg.gz
img-9.jpeg		->		img-9.jpeg.gz
index.html		->		index.html.gz
jquery.appear.js	->		jquery.appear.js.gz
jquery.filterizr.min.js	->		jquery.filterizr.min.js.gz
jquery.lineProgressbar.js->	jquery.lineProgressbar.js.gz
jquery.mag-pop.min.js	->		jquery.mag-pop.min.js.gz
LICENSE.txt		->		LICENSE.txt.gz
lite-blue-theme.css	->		lite-blue-theme.css.gz
logo.png		->		logo.png.gz
magnific-popup.css	->		magnific-popup.css.gz
mailer.php		->		mailer.php.gz
markups-kevin.rar	->		markups-kevin.rar.gz
orange-theme.css	->		orange-theme.css.gz
pink-theme.css		->		pink-theme.css.gz
profile.jpg		->		profile.jpg.gz
purple-theme.css	->		purple-theme.css.gz
red-theme.css		->		red-theme.css.gz
slick.css		->		slick.css.gz
slick.min.js		->		slick.min.js.gz
style.css		->		style.css.gz
testimonials-bg.jpeg	->		testimonials-bg.jpeg.gz
typed.min.js		->		typed.min.js.gz

Ora se sostituisci su SPIFFS il file non compresso con quello compresso con gzip e ricarichi tutto (ricordati di rimuovere la cache), puoi vedere che non cambia nulla visivamente.

Ma se confronti il dettaglio della richiesta / (che corrisponde alla pagina index.html) prima della compressione

Network detail index.html normal
Network detail index.html normal

puoi vedere nell’intestazione della risposta una lunghezza del contenuto di 23K, ma quando carichi il file gzippato alla stessa richiesta ottieni questo:

Network detail index.html gzip
Network detail index.html gzip

ora la lunghezza del contenuto è 0.4Kb e c’è una nuova codifica del Content-Encoding cioè gzip.

Quindi ora noti la differenza, ma è tutto trasparente per te perché il browser può gestire questa situazione e fare tutto il lavoro per te.

ESPAsyncWebServer per ESP32

Ok, ora andremo a capire la soluzione alternativa con l’ESPAsyncWebServer e l’AsyncTCP per ESP32. Prima scarica la libreria ESPAsyncWebServer e AsyncTCP .

Ora ecco il porting dello sketch descritto sopra.

/*
 *  ESP32
 *  Simple web server with ESPAsyncWebServer and AsyncTCP that 
 *  read from SPIFFS and
 *  stream on browser various type of file
 *  and manage gzip format also
 *
 *  by Mischianti Renzo <https://mischianti.org>
 *
 *  https://mischianti.org/category/tutorial/how-to-create-a-web-server-with-esp8266-and-esp32/
 *
 */
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <SPIFFS.h>

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

AsyncWebServer  httpServer(80);

void handleNotFound(AsyncWebServerRequest *request);

void serverRouting();

void setup(void) {
  Serial.begin(115200);
  Serial.setDebugOutput(true);

  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  WiFi.setSleep(false);
  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());

  Serial.print(F("Inizializing FS..."));
  if (SPIFFS.begin()){
	Serial.println(F("done."));
  }else{
	Serial.println(F("fail."));
  }

  Serial.println("Set routing for http server!");
  serverRouting();
  httpServer.begin();
  Serial.println("HTTP server started");
}

void loop(void) {

}

String getContentType(String filename){
  if(filename.endsWith(F(".htm"))) 			return F("text/html");
  else if(filename.endsWith(F(".html"))) 	return F("text/html");
  else if(filename.endsWith(F(".css"))) 	return F("text/css");
  else if(filename.endsWith(F(".js"))) 		return F("application/javascript");
  else if(filename.endsWith(F(".json"))) 	return F("application/json");
  else if(filename.endsWith(F(".png"))) 	return F("image/png");
  else if(filename.endsWith(F(".gif"))) 	return F("image/gif");
  else if(filename.endsWith(F(".jpg"))) 	return F("image/jpeg");
  else if(filename.endsWith(F(".jpeg"))) 	return F("image/jpeg");
  else if(filename.endsWith(F(".ico"))) 	return F("image/x-icon");
  else if(filename.endsWith(F(".xml"))) 	return F("text/xml");
  else if(filename.endsWith(F(".pdf"))) 	return F("application/x-pdf");
  else if(filename.endsWith(F(".zip"))) 	return F("application/x-zip");
  else if(filename.endsWith(F(".gz"))) 		return F("application/x-gzip");
  return F("text/plain");
}

bool handleFileRead(AsyncWebServerRequest *request, String path){
  Serial.print(F("handleFileRead: "));
  Serial.println(path);

  if(path.endsWith("/")) path += F("index.html");           // If a folder is requested, send the index file
  String contentType = getContentType(path);             	// Get the MIME type
  String pathWithGz = path + F(".gz");
  if(SPIFFS.exists(pathWithGz) || SPIFFS.exists(path)){  	// If the file exists, either as a compressed archive, or normal
	bool gzipped = false;

    if(SPIFFS.exists(pathWithGz)) {                        	// If there's a compressed version available
      path += F(".gz");                                     // Use the compressed version
      gzipped = true;
    }
    AsyncWebServerResponse *response = request->beginResponse(SPIFFS, path, contentType);
    if (gzipped){
    	response->addHeader("Content-Encoding", "gzip");
    }
    Serial.print("Real file path: ");
    Serial.println(path);

    request->send(response);

    return true;
  }
  Serial.println(String(F("\tFile Not Found: ")) + path);
  return false;                                          	// If the file doesn't exist, return false
}

void handleNotFound(AsyncWebServerRequest *request) {
  String message = "File Not Found\n\n";
  message += "URI: ";
  message += request->url();
  message += "\nMethod: ";
  message += (request->method() == HTTP_GET) ? "GET" : "POST";
  message += "\nArguments: ";
  message += request->args();
  message += "\n";

  for (uint8_t i = 0; i < request->args(); i++) {
    message += " " + request->argName(i) + ": " + request->arg(i) + "\n";
  }

  request->send(200, "text/plain", message);
}

void serverRouting() {
	  httpServer.onNotFound([](AsyncWebServerRequest *request) {                              // If the client requests any URI
		  Serial.println(F("On not found"));
	    if (!handleFileRead(request, request->url())){                  // send it if it exists
	    	handleNotFound(request); // otherwise, respond with a 404 (Not Found) error
	    }
	  });

	  Serial.println(F("Set cache!"));
	  // Serve a file with no cache so every tile It's downloaded
	  httpServer.serveStatic("/configuration.json", SPIFFS, "/configuration.json","no-cache, no-store, must-revalidate");
	  // Server all other page with long cache so browser chaching they
	  httpServer.serveStatic("/", SPIFFS, "/","max-age=31536000");
}

Come puoi vedere è molto simile, ma noterai che non ci sono istruzioni sul ciclo loop, questo perché la libreria è asincrona.

Nella funzione serverRouting()ora dobbiamo passare il parametro request.

void serverRouting() {
	  httpServer.onNotFound([](AsyncWebServerRequest *request) {                              // If the client requests any URI
		  Serial.println(F("On not found"));
	    if (!handleFileRead(request, request->url())){                  // send it if it exists
	    	handleNotFound(request); // otherwise, respond with a 404 (Not Found) error
	    }
	  });

	  Serial.println(F("Set cache!"));
	  // Serve a file with no cache so every tile It's downloaded
	  httpServer.serveStatic("/configuration.json", SPIFFS, "/configuration.json","no-cache, no-store, must-revalidate");
	  // Server all other page with long cache so browser chaching they
	  httpServer.serveStatic("/", SPIFFS, "/","max-age=31536000");
}

e poi propagarlo.

La gestione della cache è invariata.

Nell’handleFileRead puoi verificare che lo stream del file sia abbastanza diverso e devi gestire il formato gzip tramite codice (nello streamFile il controllo dell’estensione gz era implicito).

    AsyncWebServerResponse *response = request->beginResponse(SPIFFS, path, contentType);
    if (gzipped){
    	response->addHeader("Content-Encoding", "gzip");
    }
    Serial.print("Real file path: ");
    Serial.println(path);

    request->send(response);

Non vengono apportate altre modifiche significative, ma se il bug viene risolto consiglio di utilizzare la libreria standard con la soluzione precedente.

Grazie

  1. Web Server su esp8266 e esp32: servire pagine e gestire LEDs
  2. Web Server su esp8266 e esp32: servire pagine compresse come byte array e SPIFFS
  3. Web Server su esp8266 e esp32: web server generico multiuso
  4. Web Server su esp8266 e esp32: gestione sicurezza ed autenticazione
  5. Web Server su esp8266 e esp32: aggiunta di un back-end REST protetto
  6. Web Server su esp8266 e esp32: Interfaccia Web sicura per temperatura ed umidità di un DHT

Codice ed esempio su repository GitHub


Spread the love

Lascia un commento

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