Web server asincrono ESP32 e WebSocket: controllo in tempo reale
Web server asincrono ESP32 con WebSocket: comanda un relè e guarda i sensori in tempo reale senza ricaricare. Librerie, PROGMEM o LittleFS, esp32.local, errori.
Un web server asincrono ESP32 (ESP32 async web server), realizzato con la libreria ESPAsyncWebServer, trasforma il tuo ESP32 in un web server veloce che può rispondere a più smartphone e computer contemporaneamente. Con il supporto a WebSocket incluso nella libreria, browser e scheda mantengono una linea sempre aperta: premi il pulsante sulla pagina e il relè scatta subito, e quando la temperatura cambia il numero sulla pagina si aggiorna da solo, senza ricaricare. Ogni altro dispositivo che ha aperto lo stesso pannello vede la tua modifica nello stesso secondo. In breve, questa coppia è il modo più pratico per costruire il tuo pannello domotico all'interno della rete di casa, senza alcun servizio cloud.
In questa guida vedrai prima in cosa un server asincrono si differenzia dalla classica libreria WebServer e perché WebSocket è più veloce delle normali richieste HTTP. Poi installerai le librerie giuste, avvierai il server più semplice possibile e costruirai un pannello di controllo in tempo reale con un relè e un sensore DHT22. Successivamente imparerai a tenere l'HTML in PROGMEM o in LittleFS, a usare esp32.local al posto dell'indirizzo IP, a gestire più client, a proteggere il pannello con una semplice password e a risolvere gli errori di compilazione e di connessione più comuni. Il codice è scritto per funzionare con le versioni 2.x e 3.x del core ESP32 nell'Arduino IDE.

Che cos'è il web server asincrono dell'ESP32 e come funziona?
La libreria WebServer.h fornita con il core ESP32 è sincrona: le richieste in arrivo vengono gestite, una alla volta e in ordine, solo quando dentro loop() viene chiamato server.handleClient(). Se loop() contiene un delay() di 2 secondi, anche la pagina aspetta 2 secondi; se due persone si collegano insieme, una aspetta che l'altra finisca. ESPAsyncWebServer, invece, gira in un task FreeRTOS separato (async_tcp) creato dalla libreria AsyncTCP. Quando arriva una richiesta, le intestazioni vengono lette in background, la funzione che hai scritto (il callback) viene chiamata quando la richiesta è pronta e anche la risposta parte in background. Nel frattempo loop() continua il suo lavoro. Così più connessioni possono restare aperte contemporaneamente e diventano possibili le connessioni sempre aperte come WebSocket e Server-Sent Events (SSE). Se ti incuriosiscono i due core dell'ESP32 e i task, dai un'occhiata all'articolo su FreeRTOS e l'ESP32.
| Caratteristica | WebServer.h (sincrono) | ESPAsyncWebServer (asincrono) |
|---|---|---|
| Installazione | Inclusa nel core ESP32 | 2 librerie dal Gestore librerie |
| Gestione delle richieste | Una alla volta con handleClient() in loop() | Nel task async_tcp in background, più richieste insieme |
| delay() dentro loop() | Ritarda la risposta della pagina | Non influisce sul server |
| WebSocket | No; serve una libreria a parte (ad es. WebSockets di Markus Sattler) | Integrato (AsyncWebSocket) |
| Server-Sent Events | No | Integrato (AsyncEventSource) |
| File statici | Con serveStatic | serveStatic, supporto .gz automatico e controllo della cache |
| Attenzione a | La più facile da imparare | Niente delay() né operazioni lunghe nei callback |
Che cos'è WebSocket e in cosa differisce da una richiesta HTTP?
In una normale richiesta HTTP il browser apre una connessione, dice "dammi questa pagina", riceve la risposta e la conversazione finisce. Il server non può dire nulla al browser di sua iniziativa; per vedere un nuovo valore, il browser deve chiedere di nuovo. WebSocket (RFC 6455) è invece una stretta di mano che inizia come HTTP: il browser aggiunge alla richiesta l'intestazione Upgrade: websocket, l'ESP32 accetta con la risposta 101 Switching Protocols e la stessa connessione TCP resta aperta. Da quel momento entrambe le parti possono inviare messaggi quando vogliono (full-duplex). Ogni messaggio aggiunge solo da 2 a 14 byte di overhead, mentre ogni richiesta HTTP si porta dietro centinaia di byte di intestazioni. Per questo i messaggi WebSocket arrivano in pochi millisecondi ed è il metodo più efficiente per i pannelli che inviano diversi aggiornamenti al secondo. Ecco come si confrontano i quattro modi per tenere una pagina aggiornata con i valori di un sensore:
| Metodo | Come funziona | Latenza e carico | Quando usarlo |
|---|---|---|---|
| Ricarica della pagina (meta refresh) | Il browser riscarica l'intera pagina ogni pochi secondi | Alti; la pagina sfarfalla ogni volta | Meglio evitarlo |
| Interrogazione con fetch (polling) | JavaScript chiede un piccolo JSON ogni 1–2 secondi | Medi; ogni richiesta porta nuove intestazioni | Un singolo valore che cambia di rado |
| Server-Sent Events (SSE) | Il server invia eventi in una sola direzione su una connessione aperta | Bassi; solo dal server al browser | Pannelli di sola visualizzazione |
| WebSocket | Messaggi nelle due direzioni su una connessione aperta | I più bassi; frame di pochi byte | Pulsanti, slider e dati in tempo reale insieme |
Librerie necessarie: installare ESPAsyncWebServer e AsyncTCP
- Assicurati che nell'Arduino IDE sia installato il pacchetto di schede ESP32 (Espressif Systems); i passaggi si trovano in Che cos'è l'ESP32?
- Apri il Gestore librerie (Ctrl+Maiusc+I) e cerca ESPAsyncWebServer. Installa la libreria ESP Async WebServer il cui autore è ESP32Async.
- Questa libreria non dichiara la sua dipendenza al Gestore librerie, quindi l'IDE non ti propone altro: installa separatamente la libreria Async TCP, pubblicata anch'essa da ESP32Async. Su un ESP8266 ti serve invece la libreria ESP Async TCP di ESP32Async (ESPAsyncTCP.h).
- Per l'esempio del pannello, installa la DHT sensor library di Adafruit e la dipendenza Adafruit Unified Sensor che richiede. Se è la prima volta che installi una libreria, consulta la guida su come aggiungere librerie ad Arduino.
- Se nella cartella Documenti/Arduino/libraries ci sono cartelle ESPAsyncWebServer-master, AsyncTCP-master o ESPAsyncWebSrv aggiunte in passato da un file ZIP, eliminale. Due copie della stessa libreria portano il compilatore a scegliere quella sbagliata.
Primo passo: il server più semplice con AsyncWebServer
Prima di passare al pannello, vediamo la logica della libreria con un piccolo esempio. Il codice qui sotto collega l'ESP32 alla rete di casa, mostra un saluto nella pagina principale e accende e spegne il LED integrato tramite gli indirizzi /led?durum=1 e /led?durum=0. Caricalo, apri il Monitor seriale a 115200 baud e scrivi l'indirizzo che compare nel browser di uno smartphone collegato alla stessa Wi-Fi. Se la scheda non si collega alla rete, usa il codice di diagnosi della guida l'ESP32 non si connette al Wi-Fi.
1#include <WiFi.h>
2#include <AsyncTCP.h>
3#include <ESPAsyncWebServer.h>
4
5const char* ssid = "AgAdi"; // Wi-Fi adın / your Wi-Fi name
6const char* password = "Sifre123"; // Wi-Fi şifren / your Wi-Fi password
7const int LED_PIN = 2; // çoğu DevKit'te dahili LED / onboard LED on most DevKits
8
9AsyncWebServer server(80); // 80 = standart HTTP portu / standard HTTP port
10bool ledAcik = false;
11
12void setup() {
13 Serial.begin(115200);
14 pinMode(LED_PIN, OUTPUT);
15
16 WiFi.mode(WIFI_STA);
17 WiFi.begin(ssid, password);
18 while (WiFi.status() != WL_CONNECTED) {
19 delay(500);
20 Serial.print(".");
21 }
22 Serial.println();
23 Serial.print("Adres / address: http://");
24 Serial.println(WiFi.localIP());
25
26 // Ana sayfa / home page
27 server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
28 request->send(200, "text/plain", "Merhaba, ben ESP32 / Hello from ESP32");
29 });
30
31 // /led?durum=1 yakar, /led?durum=0 söndürür / turns the LED on or off
32 server.on("/led", HTTP_GET, [](AsyncWebServerRequest *request) {
33 if (request->hasParam("durum")) {
34 ledAcik = request->getParam("durum")->value() == "1";
35 digitalWrite(LED_PIN, ledAcik ? HIGH : LOW);
36 }
37 request->send(200, "text/plain", ledAcik ? "LED: ON" : "LED: OFF");
38 });
39
40 // Tanımsız adresler / unknown addresses
41 server.onNotFound([](AsyncWebServerRequest *request) {
42 request->send(404, "text/plain", "Sayfa yok / Not found");
43 });
44
45 server.begin();
46}
47
48void loop() {
49 // Sunucu arka planda çalışır, loop() boş kalabilir / the server runs in the background
50}| Istruzione | A cosa serve |
|---|---|
| AsyncWebServer server(80) | Crea il server in ascolto sulla porta HTTP 80 |
| server.on("/led", HTTP_GET, ...) | Registra la funzione (callback) da eseguire per un indirizzo e un tipo di richiesta |
| request->hasParam("durum") | Controlla se l'indirizzo contiene il parametro ?durum=... |
| request->getParam("durum")->value() | Restituisce il valore del parametro come String |
| request->send(codice, tipo, contenuto) | Invia la risposta: 200 riuscita, 404 non trovato; tipo text/plain, text/html o application/json |
| server.onNotFound(...) | Viene eseguita per ogni indirizzo non definito |
| server.begin() | Avvia il server; va chiamata dopo tutte le righe server.on() |
Materiali e collegamenti per il pannello di controllo
Nell'esempio del pannello accenderai e spegnerai una lampada o una ventola con un modulo relè e misurerai temperatura e umidità con un DHT22. Ti servono un ESP32 DevKit, un modulo relè a 5 V a un canale, un modulo sensore DHT22 (o DHT11), una breadboard e cavetti jumper. Per il relè, preferisci i moduli con fotoaccoppiatore. Il DHT22 misura da −40 a 80 °C con una precisione di ±0,5 °C e si può leggere al massimo una volta ogni 2 secondi; il DHT11 costa meno, ma offre ±2 °C nell'intervallo 0–50 °C. Per i dettagli sul sensore guarda l'articolo sul sensore di temperatura e umidità DHT11, e per la piedinatura della tua scheda lo strumento Pinout.
| Pin del modulo | ESP32 DevKit | ESP8266 NodeMCU | Nota |
|---|---|---|---|
| VCC del relè | VIN (5 V) | VIN o VU (5 V) | La bobina del relè vuole 5 V; alimentata da USB |
| GND del relè | GND | GND | Massa comune |
| IN del relè | GPIO 26 | D1 (GPIO 5) | La maggior parte dei moduli scatta con un segnale LOW |
| VCC (+) del DHT22 | 3V3 | 3V3 | Alimentato a 3,3 V, anche la linea dati è a 3,3 V |
| DATA (OUT) del DHT22 | GPIO 4 | D2 (GPIO 4) | Sensore nudo: resistenza da 10 kΩ tra DATA e VCC |
| GND (−) del DHT22 | GND | GND | Massa comune |
Pannello di controllo in tempo reale: relè, DHT22 e WebSocket
Passiamo ora al progetto vero e proprio. Il codice qui sotto è un unico file: HTML, CSS e JavaScript sono al suo interno come array PROGMEM. All'apertura della pagina il browser stabilisce una connessione WebSocket verso /ws. Ogni 2 secondi l'ESP32 legge il DHT22 e invia a tutti i browser temperatura, umidità, stato del relè e numero di dispositivi collegati in un unico messaggio JSON. Quando premi il pulsante, il browser invia il messaggio toggle, l'ESP32 inverte il relè e comunica di nuovo il nuovo stato a tutti. Cambia nome della rete e password, carica il codice e poi apri nel browser http://esp32.local o l'indirizzo IP mostrato nel Monitor seriale.
1#include <WiFi.h>
2#include <AsyncTCP.h>
3#include <ESPAsyncWebServer.h>
4#include <ESPmDNS.h>
5#include <DHT.h>
6
7const char* ssid = "AgAdi"; // Wi-Fi adın / your Wi-Fi name
8const char* password = "Sifre123"; // Wi-Fi şifren / your Wi-Fi password
9
10const int ROLE_PIN = 26; // röle modülü IN / relay module IN
11const bool ROLE_AKTIF_LOW = true; // çoğu modül LOW ile çeker / most modules switch on LOW
12const int DHT_PIN = 4; // DHT22 veri pini / DHT22 data pin
13
14DHT dht(DHT_PIN, DHT22); // DHT11 için DHT11 yaz / write DHT11 for a DHT11
15AsyncWebServer server(80);
16AsyncWebSocket ws("/ws"); // WebSocket adresi / WebSocket path
17
18bool roleAcik = false;
19float sicaklik = NAN;
20float nem = NAN;
21volatile bool gonder = false; // loop() yayın yapsın mı? / should loop() broadcast?
22unsigned long sonOlcum = 0;
23unsigned long sonTemizlik = 0;
24
25// Tarayıcıya giden sayfa / the page sent to the browser
26const char index_html[] PROGMEM = R"rawliteral(
27<!DOCTYPE html>
28<html lang="tr">
29<head>
30<meta charset="utf-8">
31<meta name="viewport" content="width=device-width, initial-scale=1">
32<title>ESP32 Panel</title>
33<style>
34body{font-family:sans-serif;background:#0f172a;color:#e2e8f0;text-align:center;margin:0;padding:16px}
35.kart{background:#1e293b;border-radius:12px;padding:16px;margin:12px auto;max-width:320px}
36.deger{font-size:2.4em;font-weight:bold}
37button{font-size:1.2em;padding:14px 32px;border:0;border-radius:8px;color:#fff;width:100%}
38.acik{background:#16a34a}.kapali{background:#475569}
39</style>
40</head>
41<body>
42<h2>ESP32 Panel</h2>
43<p id="baglanti">Bağlanıyor / Connecting...</p>
44<div class="kart">Sıcaklık / Temperature<div class="deger"><span id="sicaklik">--</span> °C</div></div>
45<div class="kart">Nem / Humidity<div class="deger"><span id="nem">--</span> %</div></div>
46<div class="kart">Röle / Relay<p><button id="dugme" class="kapali" onclick="degistir()">KAPALI / OFF</button></p></div>
47<p>Bağlı cihaz / Clients: <span id="istemci">0</span></p>
48<script>
49let ws;
50const $ = (id) => document.getElementById(id);
51function baglan() {
52 ws = new WebSocket('ws://' + location.host + '/ws');
53 ws.onopen = () => { $('baglanti').textContent = 'Bağlı / Connected'; };
54 ws.onclose = () => {
55 $('baglanti').textContent = 'Koptu, yeniden deneniyor / Reconnecting...';
56 setTimeout(baglan, 2000);
57 };
58 ws.onmessage = (e) => {
59 const d = JSON.parse(e.data);
60 $('sicaklik').textContent = d.sicaklik === null ? '--' : d.sicaklik.toFixed(1);
61 $('nem').textContent = d.nem === null ? '--' : d.nem.toFixed(0);
62 $('istemci').textContent = d.istemci;
63 $('dugme').textContent = d.role ? 'AÇIK / ON' : 'KAPALI / OFF';
64 $('dugme').className = d.role ? 'acik' : 'kapali';
65 };
66}
67function degistir() {
68 if (ws.readyState === WebSocket.OPEN) ws.send('toggle');
69}
70baglan();
71</script>
72</body>
73</html>
74)rawliteral";
75
76void roleAyarla(bool acik) {
77 roleAcik = acik;
78 digitalWrite(ROLE_PIN, ROLE_AKTIF_LOW ? !acik : acik);
79}
80
81// Durumu JSON metnine çevir / build the state as JSON text
82String durumJson() {
83 String s = "{\"role\":";
84 s += roleAcik ? "true" : "false";
85 s += ",\"sicaklik\":";
86 s += isnan(sicaklik) ? String("null") : String(sicaklik, 1);
87 s += ",\"nem\":";
88 s += isnan(nem) ? String("null") : String(nem, 0);
89 s += ",\"istemci\":";
90 s += (unsigned long)ws.count();
91 s += "}";
92 return s;
93}
94
95// WebSocket olayları (async_tcp görevinde çalışır) / WebSocket events (run in the async_tcp task)
96void wsOlay(AsyncWebSocket *sunucu, AsyncWebSocketClient *client, AwsEventType type,
97 void *arg, uint8_t *data, size_t len) {
98 switch (type) {
99 case WS_EVT_CONNECT:
100 Serial.printf("Istemci #%u baglandi / connected: %s\n",
101 (unsigned)client->id(), client->remoteIP().toString().c_str());
102 gonder = true; // yeni gelen güncel durumu alsın / newcomer gets the state
103 break;
104 case WS_EVT_DISCONNECT:
105 Serial.printf("Istemci #%u ayrildi / disconnected\n", (unsigned)client->id());
106 gonder = true;
107 break;
108 case WS_EVT_DATA: {
109 AwsFrameInfo *info = (AwsFrameInfo *)arg;
110 // Tek parça, tam bir metin mesajı mı? / a complete single-frame text message?
111 if (info->final && info->index == 0 && info->len == len && info->opcode == WS_TEXT) {
112 if (len == 6 && memcmp(data, "toggle", 6) == 0) {
113 roleAyarla(!roleAcik);
114 gonder = true; // tüm ekranlar eşitlensin / sync every screen
115 }
116 }
117 break;
118 }
119 default: // PONG, ERROR vb. / PONG, ERROR etc.
120 break;
121 }
122}
123
124void setup() {
125 Serial.begin(115200);
126 pinMode(ROLE_PIN, OUTPUT);
127 roleAyarla(false); // açılışta röle kapalı / relay off at start
128 dht.begin();
129
130 WiFi.mode(WIFI_STA);
131 WiFi.begin(ssid, password);
132 Serial.print("Baglaniyor / connecting");
133 while (WiFi.status() != WL_CONNECTED) {
134 delay(500);
135 Serial.print(".");
136 }
137 WiFi.setSleep(false); // daha hızlı tepki / lower latency
138 Serial.println();
139 Serial.print("http://");
140 Serial.println(WiFi.localIP());
141
142 if (MDNS.begin("esp32")) { // http://esp32.local
143 MDNS.addService("http", "tcp", 80);
144 Serial.println("http://esp32.local");
145 }
146
147 ws.onEvent(wsOlay);
148 server.addHandler(&ws);
149
150 server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
151 request->send(200, "text/html", index_html);
152 });
153 server.onNotFound([](AsyncWebServerRequest *request) {
154 request->send(404, "text/plain", "Sayfa yok / Not found");
155 });
156 server.begin();
157}
158
159void loop() {
160 if (millis() - sonOlcum >= 2000) { // DHT22 en fazla 2 sn'de bir / DHT22: once per 2 s
161 sonOlcum = millis();
162 float t = dht.readTemperature();
163 float h = dht.readHumidity();
164 if (!isnan(t) && !isnan(h)) { // hatalı okumayı atla / skip failed reads
165 sicaklik = t;
166 nem = h;
167 }
168 gonder = true;
169 }
170
171 if (gonder) {
172 gonder = false;
173 ws.textAll(durumJson()); // tüm tarayıcılara yolla / send to every browser
174 }
175
176 if (millis() - sonTemizlik >= 1000) {
177 sonTemizlik = millis();
178 ws.cleanupClients(); // kopuk istemcileri temizle / drop stale clients
179 }
180}Come funziona il codice?
- AsyncWebSocket ws("/ws") crea l'endpoint WebSocket e server.addHandler(&ws) lo collega al server. L'indirizzo usato nel JavaScript deve corrispondere esattamente a questo percorso.
- La funzione wsOlay() viene chiamata a ogni connessione, disconnessione e messaggio in arrivo. WS_EVT_CONNECT indica un nuovo client, WS_EVT_DISCONNECT un client che se ne va e WS_EVT_DATA un messaggio in arrivo.
- AwsFrameInfo contiene le informazioni sul frame ricevuto. Il controllo di final, index == 0 e len conferma che il messaggio è arrivato completo in un solo frame, e WS_TEXT che si tratta di testo. I comandi brevi stanno sempre in un solo frame.
- I dati in arrivo vengono confrontati insieme alla loro lunghezza con memcmp(); così non dipendi dalla presenza o meno di un carattere nullo alla fine dei dati.
- Poiché i callback girano nel task async_tcp, si limitano a invertire il relè e ad alzare il flag gonder. L'invio vero e proprio avviene in loop() con ws.textAll(); in questo modo tutti i messaggi partono da un solo punto, in ordine.
- ws.cleanupClients() viene chiamata una volta al secondo: libera la memoria delle connessioni cadute e chiude la connessione più vecchia quando il numero di client supera il limite.
- Lato JavaScript, l'evento onclose si ricollega dopo 2 secondi. Anche se l'ESP32 si riavvia o la Wi-Fi cade per un attimo, la pagina si riprende da sola. Poiché l'indirizzo è costruito con location.host, la pagina funziona sia con l'IP sia con esp32.local.
- WiFi.setSleep(false) disattiva il risparmio energetico della Wi-Fi. La sospensione del modem predefinita può aggiungere ritardi fino a qualche centinaio di millisecondi; disattivarla su un pannello alimentato da rete rende i pulsanti istantanei.
Scegliere JSON come formato dei messaggi ti semplifica la vita: il browser trasforma il testo ricevuto in un oggetto con una sola riga grazie a JSON.parse(). Un messaggio di esempio è questo: {"role":true,"sicaklik":23.4,"nem":48,"istemci":2}. Quando il sensore non si riesce a leggere, il valore viene inviato come null e la pagina mostra "--"; se un errore di lettura partisse come NaN, il JSON non valido bloccherebbe del tutto l'aggiornamento della pagina. Dato che ogni messaggio occupa circa 60 byte, nemmeno diversi aggiornamenti al secondo appesantiscono la rete. Per messaggi più complessi puoi passare alla libreria ArduinoJson, ma per un pannello di queste dimensioni unire delle String è più che sufficiente. Puoi anche provare lo stesso codice senza modifiche collegando un LED o un buzzer al posto del relè.
Dove tenere l'HTML: PROGMEM o LittleFS?
Nel codice del pannello la pagina si trova in una stringa raw scritta nella forma R"rawliteral( ... )rawliteral". Questa sintassi ti permette di usare virgolette e a capo senza caratteri di escape. Sugli Arduino basati su AVR, PROGMEM serve per tenere il testo nella flash invece che nella RAM; sull'ESP32 gli array costanti vengono comunque letti dalla flash, quindi PROGMEM si scrive solo per compatibilità e non fa danni. Quando la pagina cresce, modificare l'HTML dentro il codice diventa scomodo; a quel punto è più ordinato spostare i file nel file system LittleFS della flash.
| Criterio | PROGMEM (nel codice) | LittleFS (file system) |
|---|---|---|
| Preparazione | Nessun passaggio in più, un solo file | Servono la cartella data e un plugin di caricamento |
| Modifica | HTML dentro un array C++ | File index.html, style.css e app.js separati |
| Aggiornamento | Ricarichi lo sketch a ogni modifica | Ricarichi solo il file system |
| Dimensioni | Ideale per pagine di pochi KB | Circa 1,5 MB di spazio con la partizione predefinita |
| Immagini e font | Poco pratico | Si possono servire icone, immagini e file .gz |
| Quando? | Primi test e pannelli piccoli | Interfacce multipagina o in crescita |
Servire file HTML, CSS e JavaScript con LittleFS
- Nella cartella dello sketch (quella che contiene il file .ino) crea una cartella chiamata data e salva come data/index.html l'HTML che nel codice del pannello si trova tra R"rawliteral( e )rawliteral". Se vuoi, puoi dividere CSS e JavaScript in file separati.
- L'Arduino IDE 2 non supporta il vecchio menu ESP32 Sketch Data Upload. Scarica invece il file .vsix del plugin arduino-littlefs-upload e mettilo in C:\Users\(tuo nome utente)\.arduinoIDE\plugins su Windows o in ~/.arduinoIDE/plugins su macOS e Linux (crea la cartella se non esiste), poi riavvia l'IDE. Il plugin richiede l'IDE 2.2.1 o successivo.
- Chiudi il Monitor seriale se è aperto, poi apri la tavolozza dei comandi con Ctrl+Maiusc+P (Cmd+Maiusc+P su macOS) e scegli Upload LittleFS to Pico/ESP8266/ESP32. I file vengono scritti nella partizione del file system della flash.
- Controlla che in Strumenti > Partition Scheme sia selezionato uno schema con una partizione per il file system (ad esempio Default 4MB with spiffs). LittleFS usa questa partizione, chiamata spiffs.
- Carica il codice qui sotto come al solito. Finché l'opzione Erase All Flash del menu Strumenti è disattivata, caricare lo sketch non cancella il file system; quando modifichi i file basta ripetere il passaggio 3.
1#include <WiFi.h>
2#include <AsyncTCP.h>
3#include <ESPAsyncWebServer.h>
4#include <LittleFS.h>
5
6const char* ssid = "AgAdi"; // Wi-Fi adın / your Wi-Fi name
7const char* password = "Sifre123"; // Wi-Fi şifren / your Wi-Fi password
8
9AsyncWebServer server(80);
10
11void setup() {
12 Serial.begin(115200);
13
14 if (!LittleFS.begin(true)) { // true: bağlanamazsa biçimlendir / format if mount fails
15 Serial.println("LittleFS hatasi / LittleFS error");
16 return;
17 }
18
19 WiFi.mode(WIFI_STA);
20 WiFi.begin(ssid, password);
21 while (WiFi.status() != WL_CONNECTED) {
22 delay(500);
23 }
24 Serial.println(WiFi.localIP());
25
26 // data/ klasörünün içeriğini sun / serve the contents of the data/ folder
27 server.serveStatic("/", LittleFS, "/")
28 .setDefaultFile("index.html") // varsayılan index.htm'dir / default is index.htm
29 .setCacheControl("max-age=600"); // tarayıcı 10 dk önbelleğe alsın / cache for 10 min
30
31 server.onNotFound([](AsyncWebServerRequest *request) {
32 request->send(404, "text/plain", "Sayfa yok / Not found");
33 });
34 server.begin();
35}
36
37void loop() {
38}mDNS: esp32.local al posto dell'indirizzo IP
Il router può assegnare all'ESP32 un indirizzo IP diverso a ogni avvio, e numeri come 192.168.1.47 sono difficili da ricordare. Il mDNS (multicast DNS) permette a un dispositivo di annunciare il proprio nome sulla rete locale. Grazie alla riga MDNS.begin("esp32") del codice del pannello, i dispositivi sulla stessa rete possono aprire la pagina all'indirizzo http://esp32.local, mentre MDNS.addService("http", "tcp", 80) segnala alle app che esplorano la rete che qui c'è un web server. Non aggiungere ".local" al nome e usa solo lettere minuscole, cifre e trattini. Se usi più ESP32, dai a ciascuno un nome diverso (cucina, salotto e così via), perché due dispositivi con lo stesso nome si disturbano a vicenda. Il mDNS non è supportato allo stesso modo ovunque:
| Sistema | Supporto .local | Se non funziona |
|---|---|---|
| iPhone, iPad, macOS | Integrato (Bonjour) | Controlla di essere sulla stessa rete Wi-Fi |
| Windows 10 e 11 | Di solito funziona nelle versioni aggiornate | Installa il servizio Bonjour o usa l'IP |
| Linux | Funziona se avahi-daemon è installato | Installa i pacchetti avahi-daemon e nss-mdns |
| Android | Dipende dalla versione e dal browser | Usa l'indirizzo IP o imposta una prenotazione DHCP sul router |
Più client: smartphone, tablet e computer insieme
La cosa più apprezzata di WebSocket è che chiunque apra il pannello vede lo stesso stato. Quando accendi il relè dallo smartphone, l'ESP32 invia il nuovo stato a tutte le connessioni con textAll() e la scheda del browser sul computer diventa verde nello stesso istante. Ogni scheda del browser è un client separato: se apri tre schede nello stesso browser, il contatore mostra 3. Per impostazione predefinita la libreria consente 8 client WebSocket contemporanei sull'ESP32 e 4 sull'ESP8266 (DEFAULT_MAX_WS_CLIENTS). Questo limite non viene applicato da solo; quando si chiama cleanupClients(), la connessione più vecchia oltre il limite viene chiusa. Tieni inoltre presente quanto segue:
- Quando lo smartphone va in standby: uno smartphone con lo schermo spento potrebbe non chiudere bene la connessione. Queste connessioni rimaste a metà vengono ripulite con il tempo grazie a cleanupClients() e all'invio regolare di dati.
- Frequenza di invio: per ogni client possono restare in coda al massimo 32 messaggi (8 sull'ESP8266). Se chiami textAll() a ogni giro di loop(), la coda si riempie e i nuovi messaggi vengono scartati senza avviso; se imposti Strumenti > Core Debug Level su Warn o su un livello più dettagliato, nel Monitor seriale compare l'avviso Too many messages queued. Invia un valore solo quando cambia, oppure poche volte al secondo.
- Client appena collegato: invia subito lo stato attuale a ogni pagina che si collega; altrimenti l'utente guarda un pannello vuoto fino alla prima misura. Nel codice del pannello se ne occupa il flag alzato in WS_EVT_CONNECT.
- Risposta a un solo client: per rispondere solo al browser che ha chiesto e non a tutti, usa client->text("..."); il numero del client te lo dà client->id().
- Memoria: ogni connessione aperta occupa un socket TCP e della memoria buffer. I 520 KB di RAM dell'ESP32 bastano e avanzano per alcuni client, ma stampare ogni tanto la memoria libera con ESP.getFreeHeap() ti aiuta a scoprire per tempo eventuali perdite.
Semplice protezione con password
Chiunque sulla rete di casa conosca l'indirizzo del pannello può accendere e spegnere il relè. Con le funzioni request->authenticate() e request->requestAuthentication() della libreria puoi proteggere la pagina con nome utente e password; il browser apre la sua finestra di accesso e ricorda i dati per tutta la sessione. Il metodo predefinito è l'autenticazione Digest, quindi la password non viaggia in chiaro sulla rete. Proteggere solo la pagina però non basta: l'indirizzo WebSocket è una porta a parte e qualcuno potrebbe collegarsi direttamente a ws://ip/ws. L'esempio qui sotto chiude anche questa porta: a ogni avvio l'ESP32 genera una chiave casuale, la consegna solo al browser che ha effettuato l'accesso tramite /anahtar e con setFilter() rifiuta le connessioni WebSocket che non portano la chiave giusta.
1#include <WiFi.h>
2#include <AsyncTCP.h>
3#include <ESPAsyncWebServer.h>
4
5const char* ssid = "AgAdi"; // Wi-Fi adın / your Wi-Fi name
6const char* password = "Sifre123"; // Wi-Fi şifren / your Wi-Fi password
7const char* KULLANICI = "admin"; // panel kullanıcı adı / panel username
8const char* PAROLA = "Gizli-2026"; // panel parolası / panel password
9const int LED_PIN = 2;
10
11AsyncWebServer server(80);
12AsyncWebSocket ws("/ws");
13String wsAnahtar; // her açılışta yeni anahtar / a new key on every boot
14bool ledAcik = false;
15
16const char index_html[] PROGMEM = R"rawliteral(
17<!DOCTYPE html>
18<html><head><meta charset="utf-8">
19<meta name="viewport" content="width=device-width, initial-scale=1"></head>
20<body style="font-family:sans-serif;text-align:center">
21<h2>Korumali panel / Protected panel</h2>
22<button style="font-size:1.4em;padding:12px 32px" onclick="ws && ws.send('toggle')">LED</button>
23<p id="durum">...</p>
24<script>
25let ws;
26fetch('/anahtar').then(r => r.text()).then(k => {
27 ws = new WebSocket('ws://' + location.host + '/ws?k=' + k);
28 ws.onmessage = (e) => { document.getElementById('durum').textContent = e.data; };
29});
30</script>
31</body></html>
32)rawliteral";
33
34bool girisVar(AsyncWebServerRequest *request) {
35 return request->authenticate(KULLANICI, PAROLA);
36}
37
38void setup() {
39 Serial.begin(115200);
40 pinMode(LED_PIN, OUTPUT);
41
42 WiFi.mode(WIFI_STA);
43 WiFi.begin(ssid, password);
44 while (WiFi.status() != WL_CONNECTED) {
45 delay(500);
46 }
47 Serial.println(WiFi.localIP());
48
49 // Wi-Fi açıkken random() donanım üretecini kullanır / random() uses the hardware RNG
50 wsAnahtar = String(random(0x7FFFFFFF), HEX) + String(random(0x7FFFFFFF), HEX);
51
52 // Sayfa: tarayıcı kullanıcı adı ve parola sorar / page: the browser asks for login
53 server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
54 if (!girisVar(request)) return request->requestAuthentication();
55 request->send(200, "text/html", index_html);
56 });
57
58 // Anahtar yalnız giriş yapana verilir / the key is given only after login
59 server.on("/anahtar", HTTP_GET, [](AsyncWebServerRequest *request) {
60 if (!girisVar(request)) return request->requestAuthentication();
61 request->send(200, "text/plain", wsAnahtar);
62 });
63
64 // Doğru anahtarı getirmeyen bağlantıyı reddet / reject sockets without the right key
65 ws.setFilter([](AsyncWebServerRequest *request) {
66 return request->hasParam("k") && request->getParam("k")->value() == wsAnahtar;
67 });
68
69 ws.onEvent([](AsyncWebSocket *sunucu, AsyncWebSocketClient *client, AwsEventType type,
70 void *arg, uint8_t *data, size_t len) {
71 if (type == WS_EVT_DATA && len == 6 && memcmp(data, "toggle", 6) == 0) {
72 ledAcik = !ledAcik;
73 digitalWrite(LED_PIN, ledAcik ? HIGH : LOW);
74 sunucu->textAll(ledAcik ? "LED: ON" : "LED: OFF");
75 }
76 });
77
78 server.addHandler(&ws);
79 server.begin();
80}
81
82void loop() {
83 ws.cleanupClients(); // saniyede bir yeterli / once per second is enough
84 delay(1000);
85}Note per ESP8266 e Raspberry Pi Pico W
La stessa libreria funziona anche sull'ESP8266 (NodeMCU, Wemos D1 mini). Ecco cosa devi cambiare: installa la libreria ESP Async TCP di ESP32Async al posto di Async TCP; includi ESP8266WiFi.h al posto di WiFi.h, ESPAsyncTCP.h al posto di AsyncTCP.h ed ESP8266mDNS.h al posto di ESPmDNS.h, e aggiungi la riga MDNS.update() dentro loop(). Sostituisci WiFi.setSleep(false) con WiFi.setSleepMode(WIFI_NONE_SLEEP) e adatta i pin secondo la colonna NodeMCU della tabella dei collegamenti. L'ESP8266 ha solo circa 80 KB di RAM utilizzabile, quindi tieni la pagina leggera e ricorda che il limite di client è 4. Se usi un Raspberry Pi Pico W, trovi come creare un web server con MicroPython nella guida Raspberry Pi Pico W e Wi-Fi; le versioni recenti di ESPAsyncWebServer supportano anche i chip RP2040 e RP2350 lato Arduino.
Idee di progetto con il web server ESP32
- Presa smart multicanale: con una scheda a 4 relè crea pulsanti separati per una lampada, una ventola e una presa; distingui i comandi con testi brevi come "r1" e "r2". Per scegliere il relè consulta l'articolo sul modulo relè.
- Pannello di irrigazione: mostra in tempo reale il valore del sensore di umidità del terreno, regola la soglia di irrigazione con uno slider sulla pagina e avvia la pompa a mano quando vuoi.
- Stazione meteo: usa un BME280 al posto del DHT22 per aggiungere la pressione e mostra le ultime 60 misure in un piccolo grafico a linee sulla pagina.
- Lampada con selettore di colore: invia via WebSocket il valore di un selettore di colore sulla pagina e colora all'istante una striscia WS2812B NeoPixel.
- Servo con slider: invia il valore dello slider a ogni movimento e fai ruotare un servomotore; ideale per orientare una telecamera o per un chiavistello.
- Registro con data e ora: aggiungi a ogni misura l'ora ottenuta via NTP e aggiorna il codice in modalità wireless via OTA invece di passare ogni volta dalla USB.
Errori comuni e soluzioni
| Sintomo | Causa | Soluzione |
|---|---|---|
| 'mbedtls_md5_starts_ret' was not declared in this scope | Vecchia libreria me-no-dev con il core ESP32 3.x | Installa ESP Async WebServer e Async TCP di ESP32Async ed elimina le vecchie cartelle |
| Multiple libraries were found for "AsyncTCP.h" | Ci sono due copie della stessa libreria | Elimina le vecchie copie -master nella cartella libraries |
| Avviso deprecated per send_P | send_P sta per essere rimossa nella nuova versione | Usa request->send(200, "text/html", index_html) |
| La pagina non si apre affatto | Smartphone sulla rete ospiti o in rete mobile, oppure l'IP è cambiato | Collegati alla stessa Wi-Fi e usa l'IP attuale del Monitor seriale |
| La pagina si apre ma i valori restano su -- | WebSocket non si collega: percorso diverso o addHandler dimenticato | Apri la console con F12 e allinea ws("/ws") all'indirizzo nel JavaScript |
| Task watchdog got triggered con async_tcp | delay(), ciclo lungo o richiesta HTTP dentro un callback | Tieni il callback breve e sposta il lavoro in loop() con un flag |
| Too many messages queued | textAll() viene chiamata troppo spesso | Invia solo quando cambia qualcosa o poche volte al secondo |
| La pagina LittleFS restituisce 404 | Cartella data non caricata o setDefaultFile mancante | Carica i file con il plugin e aggiungi setDefaultFile("index.html") |
| does not exist, no permits for creation nel Monitor seriale | Il file richiesto non è in LittleFS | Controlla il nome del file e le maiuscole e minuscole; le righe con .gz sono innocue |
| Il relè non si spegne mai | Il modulo a 5 V non riconosce 3,3 V come HIGH | Usa un modulo compatibile con 3,3 V, JD-VCC o un transistor |
| L'ESP32 si riavvia quando scatta il relè | La corrente della bobina fa crollare l'alimentazione (brownout) | Usa un alimentatore a 5 V affidabile e un condensatore da 470 µF |
| esp32.local non si apre | Dispositivo senza supporto mDNS o nome scritto con .local | Scrivi MDNS.begin("esp32") e, se serve, usa l'indirizzo IP |
| Il pulsante risponde in ritardo | La sospensione del modem Wi-Fi è attiva | Chiama WiFi.setSleep(false) dopo la connessione |
La coppia web server asincrono ESP32 e WebSocket è la strada più breve per un pannello di controllo veloce, che funziona senza servizi cloud né app da scrivere per lo smartphone e che più dispositivi possono condividere. Comincia con il pannello PROGMEM in un solo file, passa a LittleFS quando l'interfaccia cresce e dai a ogni nuovo dispositivo della tua rete il suo nome mDNS. Se hai appena conosciuto l'ESP32, trovi le basi in Che cos'è l'ESP32? Nell'app BlueGrays puoi anche seguire passo dopo passo, con schemi elettrici e codice pronto, progetti ESP32 come un web server asincrono, un contatore WebSocket, l'accesso tramite mDNS, un orologio NTP e l'aggiornamento OTA.
Domande frequenti
Qual è la differenza tra ESPAsyncWebServer e WebServer sull'ESP32?
La libreria WebServer gestisce le richieste una alla volta tramite la chiamata a handleClient() in loop(); se loop() rallenta, rallenta anche la pagina. ESPAsyncWebServer gestisce le richieste in un task separato in background, regge più connessioni insieme e integra già WebSocket e SSE. Per i pannelli in tempo reale la versione asincrona è la scelta migliore.
Quanti client possono collegarsi contemporaneamente a un web server ESP32?
Per impostazione predefinita ESPAsyncWebServer consente 8 client WebSocket sull'ESP32 e chiude quelli in più quando si chiama cleanupClients(); sull'ESP8266 il limite è 4. Poiché le normali richieste di pagina durano poco, più dispositivi possono aprire la pagina uno dopo l'altro. In pratica un pannello domestico funziona senza problemi con 3–5 dispositivi contemporanei.
Posso accedere al mio web server ESP32 da internet?
Tecnicamente è possibile con un port forwarding sul router, ma non è consigliato: la libreria non offre HTTPS sull'ESP32 e il dispositivo resta esposto agli attacchi da internet. Per il controllo da remoto, un bot Telegram, un sistema basato su MQTT o la funzione VPN del router sono molto più sicuri.
Perché esp32.local non funziona?
Il motivo più frequente è che lo smartphone o il computer non riesce a risolvere i nomi mDNS; succede soprattutto su alcuni telefoni Android e sulle vecchie versioni di Windows. Anche scrivere MDNS.begin("esp32.local") nel codice è un errore, perché il nome va indicato senza .local. Se il nome continua a non essere risolto, usa l'indirizzo IP del Monitor seriale o riserva un IP fisso all'ESP32 sul router.
Un web server ESP32 funziona senza router?
Sì. Con WiFi.softAP("ESP32-Panel", "12345678") l'ESP32 crea una propria rete Wi-Fi; collega lo smartphone a questa rete e apri 192.168.4.1 nel browser. In questa modalità access point non c'è internet, ma pannello e WebSocket funzionano allo stesso modo. La password della rete deve avere almeno 8 caratteri.
Quale libreria ESPAsyncWebServer devo installare?
Nel Gestore librerie dell'Arduino IDE installa ESP Async WebServer e Async TCP il cui autore è ESP32Async. La versione originale dell'account me-no-dev non viene più aggiornata e non si compila con il core ESP32 3.x. Elimina anche dalla cartella libraries le vecchie copie aggiunte da un file ZIP.
Progetti da realizzare dopo questo articolo
Vedi tutti i progetti
Server web asincrono ESP32 (con indicazione dell'IP sul display LCD)
In questo progetto trasformiamo l'ESP32 in un server web asincrono che potrete controllare dal vostro telefono o computer collegato alla stessa rete Wi-Fi. Grazie alla libreria ESPAsyncWebServer, il server è in grado di elaborare più richieste contemporaneamente (a differenza del classico WebServer.h, che si blocca su una singola richiesta).

Contatore WebSocket ESP32 (2 pulsanti + LCD + Web in tempo reale)
In questo progetto due pulsanti collegati all'ESP32 (GPIO 5 incrementa, GPIO 4 decrementa) modificano un contatore. Il valore si aggiorna all'istante su un display LCD I2C 16x2 e, grazie al supporto WebSocket di ESPAsyncWebServer, in tutti i browser collegati senza ricaricare la pagina. Al posto del polling HTTP si usa un'unica connessione persistente e l'ESP32 invia il nuovo valore a tutti con ws.textAll().

ESP32 mDNS: accesso al dispositivo tramite bluegrays-esp.local (con schermo LCD)
In questo progetto, aggiungendo il servizio mDNS (Multicast DNS) all'ESP32, consentiamo l'accesso tramite un nome host leggibile, come bluegrays-esp.local, anziché tramite l'indirizzo IP. Anche se l'IP assegnato dal DHCP dovesse cambiare, potrete sempre raggiungere il dispositivo con lo stesso nome. Eseguendo anche un piccolo server web, inviamo comandi di controllo dei LED dal browser all’indirizzo http://bluegrays-esp.local/. Sul display LCD I2C vengono visualizzati il nome host, l’IP e lo

Connessione Wi-Fi STA con ESP32 (con display LCD)
In questo progetto colleghiamo l'ESP32 alla rete Wi-Fi domestica in modalità Station (STA) in modo sicuro e tollerante agli errori. Separiamo SSID e password in un file secrets.h per impedire fughe su Git, impostiamo un meccanismo di riconnessione automatica e visualizziamo il livello del segnale RSSI e l'indirizzo IP su un display LCD I2C. Il LED interno funge da indicatore di stato.

ESP32 DHT22 Temperatura e umidità (con display LCD)
Si tratta di un progetto che misura la temperatura e l'umidità dell'ambiente tramite il sensore DHT22 e le visualizza in tempo reale su un display LCD I2C 16x2. I dati vengono letti dal sensore utilizzando il protocollo one-wire tramite la libreria Adafruit DHT. Questo progetto costituisce la base per stazioni meteorologiche IoT, sistemi di automazione domestica intelligente o sistemi di monitoraggio ambientale nelle serre. La comunicazione con il DHT22 avviene tramite il pin GPIO 4 dell'ESP32.

Illuminazione Intelligente ESP32 (PIR + LDR + Relè + OLED)
Trasforma il tuo corridoio, bagno o dispensa in illuminazione intelligente con ESP32! Utilizzando un sensore di movimento PIR, un sensore di luce LDR e un modulo relè, costruiamo lo scenario classico "la luce si accende quando viene rilevato movimento al buio" — se l'ambiente è già luminoso, il movimento NON attiva la luce, risparmiando energia durante il giorno. La luce rimane accesa per 30 secondi; se il movimento continua, il timer si azzera e la luce rimane accesa più a lungo. Il display OLE

Aggiornamento OTA dell'ESP32 — Installazione wireless del firmware (con display LCD)
In questo progetto imparerai a installare il nuovo firmware sull'ESP32 tramite Wi-Fi, senza bisogno di un cavo USB. Grazie alla libreria ArduinoOTA, l'ESP32 diventa visibile sulla rete e viene automaticamente elencato come bluegrays-ota nella scheda "Network port" dell'IDE di Arduino. Il primo caricamento avviene tramite USB, mentre tutti gli aggiornamenti successivi saranno wireless — una caratteristica fondamentale per i dispositivi sul campo. Sul display LCD I2C vengono visualizzati, sia

ESP32: ora reale da Internet tramite NTP
L'ESP32 si connette al Wi-Fi e recupera l'ora reale con precisione al millisecondo dai server NTP (Network Time Protocol). Grazie a configTzTime(), il fuso orario della Turchia (TRT, UTC+3) viene applicato automaticamente e il passaggio all'ora legale è gestito tramite la stringa POSIX TZ. L'ora viene mostrata sul Serial Monitor e su un display LCD I2C opzionale. configTzTime() avvia il servizio SNTP; vengono eseguite risincronizzazioni periodiche finché è disponibile l'accesso alla rete, e
Altri articoli
App BlueGrays
Costruisci questo circuito passo passo sul telefono
Schema, codice ed elenco componenti in un’unica schermata. Oltre 200 progetti Arduino, ESP32 e Pico da realizzare.
Inquadra con il telefono
Punta la fotocamera sul codice: si apre lo store del tuo telefono.
oppure apri lo store

