Come scrivere un server web con ESP32

Pubblicato su News il 01/03/2024 da sebadima ‐ 9 min di lettura

Come scrivere un server web con ESP32

Cosa è un server web

Un server web (server HTTP) è un software che gestisce le richieste HTTP (Hypertext Transfer Protocol) da client come web browser o app per mobile. In ambito IoT, un server web può essere implementato su un dispositivo ESP32 per:

  • Fornire un’interfaccia web che gestisca un dispositivo,
  • Restituire dati in formato JSON o XML per l’analisi,
  • Ricevere comandi da client remoti.

Perchè usare un server web con ESP32

Un progetto che si limiti a presentare i valori dei sensori sul “Monitor Seriale” di Arduino IDE è una tappa inevitabile per un programmatore IoT, ma si tratta di una applicazione amatoriale e gravata da ovvi limiti. Per realizzare applicazioni professionali abbiamo bisogno di condividere i dati rilevati magari inviandoli a qualche app su Android. Per fare questo salto di qualità dobbiamo imparare delle nuove, semplici tecniche di networking (nulla di complicato) e usare un programma fondamentale nel mondo IoT: Il Server Web.

In questo paragrafo vedremo quali sono le strutture dati e le funzioni per creare un server web minimale. Lo stesso codice verrà quindi “incorporato” nella versione evoluta della nostra Centralina Multi-sensore. Con tale innesto la centralina potrà condividere i dati usando un sito web dinamico con HTML, Javascript e JSON. Il protocollo aggiuntivo JSON diventa necessario perchè vogliamo adottare la tecnologia AJAX.

AJAX (Asynchronous JavaScript and XML) è una tecnica di sviluppo web che permette di aggiornare una pagina web in modo dinamico, senza ricaricare l’intera pagina e senza cliccare sul tasto Aggiorna del browser.
Come funziona Ajax
Il meccanismo software si può dividere per semplicità in tre parti:
- Richiesta: L’utente invia una richiesta al server tramite JavaScript.
- Elaborazione: Il server elabora la richiesta e restituisce una risposta in formato XML, JSON o testo.
- Aggiornamento: Il client JavaScript aggiorna la pagina web in base alla risposta ricevuta.

Una applicazione IoT moderna dovrebbe necessariamente includere AJAX per i grandi benefici che apporta nella esperienza utente (Le pagine web sono più fluide e reattive), ma anche per la riduzione nel traffico da e verso l’ESP32. Seppure i moderni microcontroller siano molto superiori ad Arduino UNO, le loro capacità di elaborazione sono comunque ben lontane dalle classiche CPU per Desktop.

“Se vorrete costruire la nostra centralina con le modifiche che vi presentiamo potrete realizzare a basso costo un efficiente prodotto IoT dalla reale valenza commerciale.”

Come scrivere un server web con ESP32

ESP32 utilizza (per fortuna) la sterminata libreria di Arduino e chi ha familiarità con questa piattaforma non dovrà imparare alcun nuovo concetto di programmazione. Come avviene con Arduino, per risolvere dei compiti complessi come la creazione di un server web, conviene appoggiarsi a del software già esistente. In questo caso potevamo usare, ad esempio la libreria “webServer” inclusa nell’IDE di Arduino e adottata da Espressif per l’ESP32.

Per dei progetti “basici” di IoT puoi tranquillamente usare “webServer”, ma per il nostro server ESP32 con il sistema asincrono AJAX e il rendering dei valori in background, abbiamo preferito utilizzare la più performante libreria ESPAsyncwebServer, asincrona come suggerisce il nome e specifica per l’ESP32.

Il codice

Per iniziare vediamo come caricare le librerie che ci servono. Ci bastano le prime due linee con gli #include header delle librerie utilizzate.

I file header, o file di intestazione, sono file di testo con estensione .h che contengono informazioni utili per la compilazione del codice C++.

Le funzioni dei file header sono molteplici e non si limitano ad agganciare librerie come nel nostro caso, ma servono a vari altri scopi come:
- Dichiarazioni di funzioni: Prototipi di funzioni che definiscono il nome, il tipo di ritorno e i parametri,
- Dichiarazioni di classi: Struttura e membri di classi C++.
- Definizioni di macro: Costanti simboliche utilizzate nel codice.


Per chi inizia con il C++, aggiungere altre istruzioni solo per definire quello che vogliamo fare nel resto del programma può sembrare una complicazione inutile, ma non è così. Inoltre parrebbe più semplice caricare tutto nello stesso file sorgente, magari molto lungo e fare delle chiamate a funzioni. Ma le “best practices” della programmazione strutturata sconsigliano tale approccio:

Ecco dunque gli header del programma:

#include "ESPAsyncwebServer.h"
#include <WiFi.h>

Il primo include “carica” la libreria fondamentale e cioè “ESPAsyncwebServer”, mentre il secondo mette a disposizione del codice tutte le funzioni per il wireless offerte dalla libreria Wifi di Arduino.

Le “variabili” statiche:

constexpr char WIFI_SSID[] = "Cambia-il-nome";
constexpr char WIFI_PASS[] = "Cambia-il-nome";

IPAddress local_IP(192, 168, 1, 200);
IPAddress gateway(192, 168, 1, 1);
IPAddress subnet(255, 255, 0, 0);
IPAddress primaryDNS(8, 8, 8, 8);
IPAddress secondaryDNS(8, 8, 4, 4);

Le prime due righe usano la istruzione “constexpr” per motivi di ottimizzazione del codice. Con constexpr possiamo assegnare il valore di espressioni (anche complesse) nel momento della compilazione, anziché a tempo di esecuzione, migliorando significativamente le prestazioni del codice.

Le cinque righe successive assomigliano a delle normali dichiarazioni di variabili, ma sono variabili di un tipo particolare e cioè “IPAddress” specifico per interfacciarsi con la libreria Wifi. Per fortuna non dobbiamo ridefinire e modificare il loro “tipo”, ci basta seguire il formato ideato dagli sviluppatori e inserire i quattro numeri di un classico indirizzo IP.

Parlare di variabili in queste instruzioni è un poco ingannevole, perchè si tratta di valori che vengono definiti “una tantum” nel momento della creazione: Sono in realtà dei parametri per le funzioni OOP della libreria WiFi, ma per semplificare la spiegazione potete pensarle come cinque variabili con un tipo dati ad hoc.

La struttura dati principale del server web:

AsyncwebServer server(80);

const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE HTML><html>
<head>
  <title>Robotdazero - un semplice sito Statico</title>
</head>
<body>
  <p>Robotdazero - un semplice sito Statico</p>
</body>
</html>)rawliteral";

La prima riga crea la “istanza” dell’oggetto AsyncwebServer assegnando nel contempo il valore “80” alla porta del server. Il valore “80” viene usato per il normale protocollo HTTP mentre il valore “443” viene riservato a quello HTTP(S). Il nome dell’oggetto creato sarà un generico “server” e la cosa non è casuale: Se decidiamo di cambiare tipo di server e libreria collegata non avremo bisogno di modificare tutte le istruzioni nel codice, ma ci basta cambiare la riga:

AsyncwebServer server(80);

in

webServer server(80);

La riga successiva:
const char index_html[] PROGMEM = R"rawliteral(… …)rawliteral"
(si tratta di una singola riga!) crea un oggetto String “index_html” che usa il “modificatore di variabile” PROGMEM per caricare la String nella zona di memoria flash di ESP32.

Si tratta dunque di un trucco specifico per l’ESP32: Nel caso in questione, vista la ridotta lunghezza della stringa, potevamo fare a meno di usarlo, ma ti ricordo che stiamo illustrando il funzionamento di un server HTTP minimale. Nella versione completa che useremo nella Centralina Multi-sensore, l’accorgimento invece diventerà indispensabile per poter compilare il programma. Nella nota seguente cercheremo di chiarire l’altro comando misterioso della riga e cioè il costrutto sintattico “R()”.

Nel C++ di Arduino, la parola chiave R"()" (rawliteral) consente di definire stringhe letterali senza interpretare caratteri di escape come \n o \t. Questo significa che i caratteri di escape vengono trattati come caratteri letterali all’interno della stringa.
La cosa è molto utile quando si tratta di stringhe che includono i caratteri “slash” e “back-slash” onnipresenti nei tag del codice HTML e XML.

La parola rawliteral non ha un valore particolare e viene usata solo per consuetudine: puoi usare qualsiasi altra parola come delimitatore.

La connessione al WI-Fi:

void initWiFi() {
    WiFi.mode(WIFI_MODE_STA);

    if(!WiFi.config(local_IP, gateway, subnet, primaryDNS, secondaryDNS)) {
      Serial.println("Non riesco a configurare la modalità station (STA)");
    }

    WiFi.begin(WIFI_SSID, WIFI_PASS);

    Serial.printf("In connessione a %s .", WIFI_SSID);
    while (WiFi.status() != WL_CONNECTED) { Serial.print("."); delay(200); }
    Serial.println("connesso!");

    IPAddress ip = WiFi.localIP();

    Serial.printf("SSID: %s\n", WIFI_SSID);
    Serial.printf("Canale: %u\n", WiFi.channel());
    Serial.printf("IP: %u.%u.%u.%u\n", ip & 0xff, (ip >> 8) & 0xff, (ip >> 16) & 0xff, ip >> 24);
}

La funzione “initWiFi()” è una nostra funzione utente, priva di parametri in ingresso e non presenta “sottigliezze” particolari: E’ una pura sequenza di istruzioni, destinata ad essere invocata dal “setup()” del programma. Una funzione di questo tipo, cioè senza parametri in ingresso e valore in uscita, potrebbe essere meglio chiamata una “procedura”.

  • La prima istruzione che incontriamo è:
    WiFi.mode(WIFI_MODE_STA);"
    che assegna alla sezione radio dell’ESP32 la modalità “STATION” per collegarsi al Wi-Fi. Esistono altre modalità, ad esempio di tipo misto come “APSTA” di cui dovremo occuparci meglio in seguito.

  • La chiamata di funzione
    "WiFi.config(local_IP, gateway, subnet, primaryDNS, secondaryDNS)"
    serve a configurare l’oggetto Wifi con i parametri definiti in precedenza. L’operatore “.” è tipico dei linguaggi di programmazione OOP. In caso di errore il programma scriverà un messaggio di errore sul monitor seriale.

  • Le istruzioni
    while (WiFi.status() != WL_CONNECTED) { Serial.print(”.”); delay(200); }"
    mettono in loop il programma in attesa che lo stato della connessione sia = “WL_CONNECTED”.
    La istruzione “IPAddress ip = WiFi.localIP();” serve a settare l’indirizzo IP statico che avevamo definito ad inizio programma.

  • Una istruzione interessante è “Serial.printf(“Canale: %u\n”, WiFi.channel());” perchè permette di leggere il valore del canale Wi-Fi su cui opera la connessione: un dato fondamentale per lavorare con il protocollo ESP-NOW.

La funzione setup() e la funzione loop()

void setup() {
  Serial.begin(115200);
  initWiFi();

  server.on("/", HTTP_GET, [](AsyncwebServerRequest *request){
    request->send_P(200, "text/html", index_html);
  });
   
  server.begin();
}
 
void loop() {} 

La funzione loop è vuota perchè stiamo considerando solo la struttura minima di un server HTTP, mentre la funzione “setup()” presenta una importante chiamata di funzione e cioè
“*server.on(”/", HTTP_GET, [](AsyncwebServerRequest request){ request->send_P(200, “text/html”, index_html);"
che mappa in RAM l’oggetto “server” (ricordate il nome molto generico?). L’oggetto request è di tipo AsyncwebServerRequest. Lo puoi considerare una zona di buffer dove sono conservate e manipolate moltissime informazioni quali ad esempio: il metodo HTTP utilizzato (GET, POST, ecc.), l’URL richiesto, i parametri passati nella “query string”, le intestazioni HTTP e moltissime altre informazioni.


logo sezione

In conclusione

I server web (server HTTP) per ESP32 e Arduino offrono una serie di grandi opportunità nello sviluppo di applicazioni IoT:

  • Controllo e monitoraggio remoti: Permettono di controllare e monitorare i dispositivi IoT da qualsiasi luogo con un dispositivo connesso a internet.
  • Interfacce utente web: Consentono di creare interfacce utente web per interagire con i dispositivi IoT.
  • Comunicazione dati: Facilitano la comunicazione di dati tra dispositivi IoT e server remoti.
  • Flessibilità: Offrono una piattaforma flessibile per creare applicazioni IoT personalizzate.

Capire questo versione minimale del Server Web ti sarà di grande aiuto nell’affrontare altri progetti software più sofisticati.

Robotdazero.it - post - R.157.1.4.5

RESTA IN CONTATTO

Novità settimanali su prodotti, offerte speciali, corsi e altro ancora.