Web Server con esp8266 e esp32: web server generico multiuso – 3
Ora che abbiamo capito come servire a una pagina o un’immagine compressa, sappiamo tutto su come creare un server web generico.
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:
senza cartella e con una lunghezza dei files corretta.
Il risultato:
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
}
});
controlla l’URL richiesto e all’interno ottiene il tipo MIME corretto tramite l’estensione del file.handleFileRead(httpServer.uri())
Puoi aprire gli strumenti di sviluppo dal tuo browser con il pulsante F12
, qui puoi vedere le risorse servite dall’esp8266 nella scheda “Rete”.
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
puoi vedere nell’intestazione della risposta una lunghezza del contenuto di 23K
, ma quando carichi il file gzippato alla stessa richiesta ottieni questo:
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
- Web Server su esp8266 e esp32: servire pagine e gestire LEDs
- Web Server su esp8266 e esp32: servire pagine compresse come byte array e SPIFFS
- Web Server su esp8266 e esp32: web server generico multiuso
- Web Server su esp8266 e esp32: gestione sicurezza ed autenticazione
- Web Server su esp8266 e esp32: aggiunta di un back-end REST protetto
- Web Server su esp8266 e esp32: Interfaccia Web sicura per temperatura ed umidità di un DHT
Codice ed esempio su repository GitHub