Serveur web ESP32 asynchrone et WebSocket : pilotage en direct
Serveur web asynchrone ESP32 et WebSocket : pilote un relais, vois tes capteurs en direct sans recharger. Installation, PROGMEM, LittleFS, esp32.local, erreurs.
Un serveur web asynchrone ESP32 (ESP32 async web server), basé sur la bibliothèque ESPAsyncWebServer, transforme ton ESP32 en un serveur web rapide capable de répondre à plusieurs téléphones et ordinateurs en même temps. Grâce à la prise en charge de WebSocket intégrée à la bibliothèque, le navigateur et la carte gardent une ligne ouverte en permanence : tu appuies sur le bouton de la page et le relais colle aussitôt, et quand la température change, le chiffre affiché se met à jour tout seul, sans recharger la page. Tous les autres appareils qui ont ouvert le même tableau de bord voient ton changement dans la même seconde. En bref, ce duo est la façon la plus pratique de créer ton propre panneau domotique au sein de ton réseau local, sans aucun service cloud.
Dans ce guide, tu verras d'abord ce qui distingue un serveur asynchrone de la bibliothèque WebServer classique et pourquoi WebSocket est plus rapide que de simples requêtes HTTP. Ensuite, tu installeras les bonnes bibliothèques, tu lanceras le serveur le plus simple possible, puis tu construiras un tableau de bord en temps réel avec un relais et un capteur DHT22. Tu apprendras ensuite à stocker le HTML dans PROGMEM ou dans LittleFS, à utiliser esp32.local au lieu de l'adresse IP, à gérer plusieurs clients, à protéger le panneau par un mot de passe simple et à corriger les erreurs de compilation et de connexion les plus courantes. Le code est écrit pour fonctionner avec les versions 2.x et 3.x du cœur ESP32 dans l'Arduino IDE.

Qu'est-ce que le serveur web asynchrone de l'ESP32 et comment fonctionne-t-il ?
La bibliothèque WebServer.h fournie avec le cœur ESP32 est synchrone : les requêtes entrantes ne sont traitées, une par une et dans l'ordre, que lorsque server.handleClient() est appelé dans loop(). Si loop() contient un delay() de 2 secondes, la page attend 2 secondes elle aussi ; si deux personnes se connectent en même temps, l'une attend que l'autre ait fini. ESPAsyncWebServer, lui, tourne dans une tâche FreeRTOS séparée (async_tcp) créée par la bibliothèque AsyncTCP. Quand une requête arrive, ses en-têtes sont lus en arrière-plan, la fonction que tu as écrite (le callback) est appelée dès que la requête est prête, et la réponse part elle aussi en arrière-plan. Pendant ce temps, loop() continue son travail. Plusieurs connexions peuvent ainsi rester ouvertes à la fois, et les connexions permanentes comme WebSocket et les Server-Sent Events (SSE) deviennent possibles. Si les deux cœurs de l'ESP32 et les tâches t'intéressent, jette un œil à l'article sur FreeRTOS et l'ESP32.
| Caractéristique | WebServer.h (synchrone) | ESPAsyncWebServer (asynchrone) |
|---|---|---|
| Installation | Fournie avec le cœur ESP32 | 2 bibliothèques via le gestionnaire de bibliothèques |
| Traitement des requêtes | Une par une avec handleClient() dans loop() | Dans la tâche async_tcp en arrière-plan, plusieurs à la fois |
| delay() dans loop() | Retarde la réponse de la page | N'affecte pas le serveur |
| WebSocket | Non ; bibliothèque à part nécessaire (par ex. WebSockets de Markus Sattler) | Intégré (AsyncWebSocket) |
| Server-Sent Events | Non | Intégré (AsyncEventSource) |
| Fichiers statiques | Avec serveStatic | serveStatic, prise en charge automatique du .gz et gestion du cache |
| Point de vigilance | La plus simple à apprendre | Pas de delay() ni de traitement long dans les callbacks |
Qu'est-ce que WebSocket et en quoi diffère-t-il d'une requête HTTP ?
Lors d'une requête HTTP normale, le navigateur ouvre une connexion, dit "donne-moi cette page", reçoit la réponse et la conversation s'arrête. Le serveur ne peut rien dire au navigateur de lui-même ; pour voir une nouvelle valeur, le navigateur doit redemander. WebSocket (RFC 6455) est une poignée de main qui commence en HTTP : le navigateur ajoute l'en-tête Upgrade: websocket à sa requête, l'ESP32 accepte avec la réponse 101 Switching Protocols et la même connexion TCP reste ouverte. Dès lors, les deux côtés peuvent envoyer un message quand ils le veulent (full-duplex). Chaque message n'ajoute que 2 à 14 octets, alors que chaque requête HTTP transporte des centaines d'octets d'en-têtes. C'est pourquoi les messages WebSocket arrivent en quelques millisecondes et que c'est la méthode la plus efficace pour les tableaux de bord qui envoient plusieurs mises à jour par seconde. Voici comment se comparent les quatre façons de tenir une page à jour avec les valeurs d'un capteur :
| Méthode | Fonctionnement | Latence et charge | Quand l'utiliser ? |
|---|---|---|---|
| Rechargement de page (meta refresh) | Le navigateur retélécharge toute la page toutes les quelques secondes | Élevées ; la page clignote à chaque fois | À éviter |
| Interrogation avec fetch (polling) | JavaScript demande un petit JSON toutes les 1 à 2 secondes | Moyennes ; chaque requête porte de nouveaux en-têtes | Une seule valeur qui change rarement |
| Server-Sent Events (SSE) | Le serveur pousse des événements dans un seul sens sur une connexion ouverte | Faibles ; du serveur vers le navigateur seulement | Tableaux de bord de simple affichage |
| WebSocket | Messages dans les deux sens sur une connexion ouverte | Les plus faibles ; trames de quelques octets | Boutons, curseurs et données en direct réunis |
Bibliothèques nécessaires : installer ESPAsyncWebServer et AsyncTCP
- Vérifie que le paquet de cartes ESP32 (Espressif Systems) est installé dans l'Arduino IDE ; les étapes se trouvent dans Qu'est-ce que l'ESP32 ?
- Ouvre le gestionnaire de bibliothèques (Ctrl+Maj+I) et cherche ESPAsyncWebServer. Installe la bibliothèque ESP Async WebServer dont l'auteur est ESP32Async.
- Cette bibliothèque ne déclare pas sa dépendance au gestionnaire de bibliothèques, l'IDE ne te proposera donc rien d'autre : installe séparément la bibliothèque Async TCP, publiée elle aussi par ESP32Async. Sur un ESP8266, il te faut à la place la bibliothèque ESP Async TCP d'ESP32Async (ESPAsyncTCP.h).
- Pour l'exemple du tableau de bord, installe la DHT sensor library d'Adafruit et la dépendance Adafruit Unified Sensor qu'elle demande. Si c'est la première fois que tu installes une bibliothèque, consulte le guide pour ajouter une bibliothèque Arduino.
- Si le dossier Documents/Arduino/libraries contient des dossiers ESPAsyncWebServer-master, AsyncTCP-master ou ESPAsyncWebSrv ajoutés autrefois depuis un ZIP, supprime-les. Deux copies de la même bibliothèque poussent le compilateur à choisir la mauvaise.
Première étape : le serveur le plus simple avec AsyncWebServer
Avant de passer au tableau de bord, voyons la logique de la bibliothèque avec un petit exemple. Le code ci-dessous connecte l'ESP32 à ton réseau domestique, affiche un message d'accueil sur la page principale et allume ou éteint la LED intégrée via les adresses /led?durum=1 et /led?durum=0. Téléverse-le, ouvre le moniteur série à 115200 bauds et tape l'adresse affichée dans le navigateur d'un téléphone connecté au même Wi-Fi. Si la carte ne rejoint pas le réseau, utilise le code de diagnostic du guide l'ESP32 ne se connecte pas au 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}| Instruction | Rôle |
|---|---|
| AsyncWebServer server(80) | Crée le serveur qui écoute sur le port HTTP 80 |
| server.on("/led", HTTP_GET, ...) | Enregistre la fonction (callback) à exécuter pour une adresse et un type de requête |
| request->hasParam("durum") | Vérifie si l'adresse contient un paramètre ?durum=... |
| request->getParam("durum")->value() | Renvoie la valeur du paramètre sous forme de String |
| request->send(code, type, contenu) | Envoie la réponse : 200 OK, 404 introuvable ; type text/plain, text/html ou application/json |
| server.onNotFound(...) | S'exécute pour toute adresse non définie |
| server.begin() | Démarre le serveur ; à appeler après toutes les lignes server.on() |
Matériel et câblage du tableau de bord
Dans l'exemple du tableau de bord, tu allumeras et éteindras une lampe ou un ventilateur avec un module relais et tu mesureras la température et l'humidité avec un DHT22. Il te faut une ESP32 DevKit, un module relais 5 V à un canal, un module capteur DHT22 (ou DHT11), une breadboard et des câbles Dupont. Pour le relais, préfère les modules à optocoupleur. Le DHT22 mesure de −40 à 80 °C avec une précision de ±0,5 °C et ne peut être lu qu'une fois toutes les 2 secondes au maximum ; le DHT11 est moins cher, mais n'offre que ±2 °C entre 0 et 50 °C. Pour les détails du capteur, vois l'article sur le capteur de température et d'humidité DHT11, et pour le brochage de ta carte, l'outil Pinout.
| Broche du module | ESP32 DevKit | ESP8266 NodeMCU | Remarque |
|---|---|---|---|
| VCC du relais | VIN (5 V) | VIN ou VU (5 V) | La bobine du relais demande 5 V ; alimentée par l'USB |
| GND du relais | GND | GND | Masse commune |
| IN du relais | GPIO 26 | D1 (GPIO 5) | La plupart des modules collent sur un niveau LOW |
| VCC (+) du DHT22 | 3V3 | 3V3 | Alimenté en 3,3 V, la ligne de données est aussi en 3,3 V |
| DATA (OUT) du DHT22 | GPIO 4 | D2 (GPIO 4) | Capteur nu : résistance de 10 kΩ entre DATA et VCC |
| GND (−) du DHT22 | GND | GND | Masse commune |
Tableau de bord en temps réel : relais, DHT22 et WebSocket
Passons maintenant au projet principal. Le code ci-dessous tient en un seul fichier : le HTML, le CSS et le JavaScript y sont intégrés sous forme de tableau PROGMEM. À l'ouverture de la page, le navigateur établit une connexion WebSocket vers /ws. Toutes les 2 secondes, l'ESP32 lit le DHT22 et envoie à tous les navigateurs la température, l'humidité, l'état du relais et le nombre d'appareils connectés dans un seul message JSON. Quand tu appuies sur le bouton, le navigateur envoie le message toggle, l'ESP32 inverse le relais et annonce de nouveau l'état à tout le monde. Modifie le nom du réseau et le mot de passe, téléverse, puis ouvre http://esp32.local ou l'adresse IP du moniteur série dans ton navigateur.
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}Comment fonctionne le code ?
- AsyncWebSocket ws("/ws") crée le point d'accès WebSocket et server.addHandler(&ws) le rattache au serveur. L'adresse utilisée dans le JavaScript doit correspondre exactement à ce chemin.
- La fonction wsOlay() est appelée à chaque connexion, déconnexion et message entrant. WS_EVT_CONNECT signale un nouveau client, WS_EVT_DISCONNECT un client qui part et WS_EVT_DATA un message reçu.
- AwsFrameInfo contient les informations de la trame reçue. Le contrôle de final, index == 0 et len confirme que le message est arrivé complet en une seule trame, et WS_TEXT qu'il s'agit de texte. Les commandes courtes tiennent toujours dans une seule trame.
- Les données reçues sont comparées avec leur longueur grâce à memcmp() ; tu ne dépends donc pas de la présence d'un caractère nul à la fin des données.
- Comme les callbacks tournent dans la tâche async_tcp, ils se contentent d'inverser le relais et de lever le drapeau gonder. L'envoi proprement dit se fait dans loop() avec ws.textAll() ; ainsi, tous les messages partent d'un seul endroit, dans l'ordre.
- ws.cleanupClients() est appelé une fois par seconde : il libère la mémoire des connexions coupées et ferme la plus ancienne connexion quand le nombre de clients dépasse la limite.
- Côté JavaScript, l'événement onclose relance la connexion au bout de 2 secondes. Même si l'ESP32 redémarre ou si le Wi-Fi coupe un instant, la page se rétablit toute seule. Comme l'adresse est construite à partir de location.host, la page fonctionne aussi bien avec l'IP qu'avec esp32.local.
- WiFi.setSleep(false) désactive l'économie d'énergie du Wi-Fi. La mise en veille du modem par défaut peut ajouter jusqu'à plusieurs centaines de millisecondes de retard ; la couper sur un tableau de bord alimenté sur secteur rend les boutons instantanés.
Choisir JSON comme format de message te simplifie la vie : le navigateur transforme le texte reçu en objet en une seule ligne avec JSON.parse(). Un message type ressemble à ceci : {"role":true,"sicaklik":23.4,"nem":48,"istemci":2}. Quand le capteur ne peut pas être lu, la valeur part sous la forme null et la page affiche "--" ; si une erreur de lecture partait sous forme de NaN, le JSON invalide empêcherait toute mise à jour de la page. Comme chaque message pèse environ 60 octets, même plusieurs mises à jour par seconde ne chargent pas le réseau. Pour des messages plus complexes, tu peux passer à la bibliothèque ArduinoJson, mais pour un tableau de bord de cette taille, concaténer des String suffit largement. Tu peux aussi essayer le même code sans rien changer en branchant une LED ou un buzzer à la place du relais.
Où stocker le HTML : PROGMEM ou LittleFS ?
Dans le code du tableau de bord, la page se trouve dans une chaîne brute écrite sous la forme R"rawliteral( ... )rawliteral". Cette syntaxe te permet d'utiliser guillemets et retours à la ligne sans caractères d'échappement. Sur les Arduino à base d'AVR, PROGMEM est nécessaire pour garder le texte en flash plutôt qu'en RAM ; sur l'ESP32, les tableaux constants sont de toute façon lus depuis la flash, donc PROGMEM n'est écrit que par compatibilité et ne gêne pas. Quand la page grossit, modifier du HTML au milieu du code devient pénible ; à ce stade, il est plus propre de déplacer les fichiers dans le système de fichiers LittleFS de la flash.
| Critère | PROGMEM (dans le code) | LittleFS (système de fichiers) |
|---|---|---|
| Mise en place | Aucune étape en plus, un seul fichier | Dossier data et plugin de téléversement nécessaires |
| Édition | HTML dans un tableau C++ | Fichiers index.html, style.css et app.js séparés |
| Mise à jour | Tu téléverses le croquis à chaque modification | Tu téléverses seulement le système de fichiers |
| Taille | Idéal pour des pages de quelques Ko | Environ 1,5 Mo d'espace avec le partitionnement par défaut |
| Images et polices | Peu pratique | Icônes, images et fichiers .gz possibles |
| Quand ? | Premiers essais et petits tableaux de bord | Interfaces multipages ou qui grandissent |
Servir des fichiers HTML, CSS et JavaScript avec LittleFS
- Dans le dossier du croquis (celui qui contient le fichier .ino), crée un dossier nommé data et enregistre dans data/index.html le HTML placé entre R"rawliteral( et )rawliteral" dans le code du tableau de bord. Si tu veux, tu peux séparer le CSS et le JavaScript dans leurs propres fichiers.
- L'Arduino IDE 2 ne prend pas en charge l'ancien menu ESP32 Sketch Data Upload. À la place, télécharge le fichier .vsix du plugin arduino-littlefs-upload et place-le dans C:\Users\(ton nom d'utilisateur)\.arduinoIDE\plugins sous Windows ou dans ~/.arduinoIDE/plugins sous macOS et Linux (crée le dossier s'il n'existe pas), puis redémarre l'IDE. Le plugin demande l'IDE 2.2.1 ou plus récent.
- Ferme le moniteur série s'il est ouvert, puis ouvre la palette de commandes avec Ctrl+Maj+P (Cmd+Maj+P sur macOS) et choisis Upload LittleFS to Pico/ESP8266/ESP32. Les fichiers sont écrits dans la partition du système de fichiers en flash.
- Vérifie que le menu Outils > Partition Scheme indique un schéma avec une partition de système de fichiers (par exemple Default 4MB with spiffs). LittleFS utilise cette partition, nommée spiffs.
- Téléverse le code ci-dessous comme d'habitude. Tant que l'option Erase All Flash du menu Outils est désactivée, téléverser le croquis n'efface pas le système de fichiers ; quand tu modifies les fichiers, il suffit de refaire l'étape 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 au lieu de l'adresse IP
Ta box peut attribuer une adresse IP différente à l'ESP32 à chaque démarrage, et des nombres comme 192.168.1.47 sont difficiles à retenir. Le mDNS (multicast DNS) permet à un appareil d'annoncer son propre nom sur le réseau local. Grâce à la ligne MDNS.begin("esp32") du code du tableau de bord, les appareils du même réseau peuvent ouvrir la page à l'adresse http://esp32.local, et MDNS.addService("http", "tcp", 80) signale aux applis d'exploration du réseau qu'un serveur web se trouve ici. N'ajoute pas ".local" au nom et n'utilise que des minuscules, des chiffres et des tirets. Si tu utilises plusieurs ESP32, donne à chacun un nom différent (cuisine, salon, etc.), car deux appareils portant le même nom se gênent. Le mDNS n'est pas pris en charge de la même façon partout :
| Système | Prise en charge de .local | Si ça ne marche pas |
|---|---|---|
| iPhone, iPad, macOS | Intégrée (Bonjour) | Vérifie que tu es sur le même réseau Wi-Fi |
| Windows 10 et 11 | Fonctionne en général sur les versions à jour | Installe le service Bonjour ou utilise l'IP |
| Linux | Fonctionne si avahi-daemon est installé | Installe les paquets avahi-daemon et nss-mdns |
| Android | Dépend de la version et du navigateur | Utilise l'adresse IP ou crée un bail DHCP fixe sur la box |
Plusieurs clients : téléphone, tablette et ordinateur en même temps
Ce qu'on apprécie le plus avec WebSocket, c'est que tous ceux qui ouvrent le tableau de bord voient le même état. Quand tu actives le relais depuis ton téléphone, l'ESP32 envoie le nouvel état à toutes les connexions avec textAll(), et l'onglet de ton ordinateur passe au vert au même moment. Chaque onglet est un client distinct : ouvre trois onglets dans le même navigateur et le compteur affiche 3. Par défaut, la bibliothèque autorise 8 clients WebSocket simultanés sur l'ESP32 et 4 sur l'ESP8266 (DEFAULT_MAX_WS_CLIENTS). Cette limite ne s'applique pas toute seule ; c'est l'appel à cleanupClients() qui ferme la plus ancienne connexion au-delà de la limite. Garde aussi ceci en tête :
- Quand le téléphone se met en veille : un téléphone dont l'écran s'éteint peut ne pas fermer proprement la connexion. Ces connexions à moitié ouvertes sont nettoyées avec le temps grâce à cleanupClients() et à l'envoi régulier de données.
- Rythme d'envoi : jusqu'à 32 messages peuvent attendre dans la file de chaque client (8 sur l'ESP8266). Si tu appelles textAll() à chaque tour de loop(), la file se remplit et les nouveaux messages sont jetés sans prévenir ; si tu règles Outils > Core Debug Level sur Warn ou un niveau plus détaillé, l'avertissement Too many messages queued apparaît dans le moniteur série. N'envoie une valeur que lorsqu'elle change, ou quelques fois par seconde.
- Client fraîchement connecté : envoie tout de suite l'état actuel à chaque page qui se connecte ; sinon, l'utilisateur fixe un tableau de bord vide jusqu'à la première mesure. Dans le code, c'est le drapeau levé dans WS_EVT_CONNECT qui s'en charge.
- Répondre à un seul client : pour ne répondre qu'au navigateur qui a demandé et non à tout le monde, utilise client->text("...") ; client->id() te donne le numéro du client.
- Mémoire : chaque connexion ouverte occupe un socket TCP et de la mémoire tampon. Les 520 Ko de RAM de l'ESP32 suffisent largement pour quelques clients, mais afficher de temps en temps la mémoire libre avec ESP.getFreeHeap() t'aide à repérer tôt les fuites.
Protection simple par mot de passe
N'importe qui sur ton réseau domestique connaissant l'adresse du tableau de bord peut actionner le relais. Avec les fonctions request->authenticate() et request->requestAuthentication() de la bibliothèque, tu peux protéger la page par un identifiant et un mot de passe ; le navigateur ouvre sa propre fenêtre de connexion et retient les informations pour la session. La méthode par défaut est l'authentification Digest : le mot de passe ne circule donc pas en clair sur le réseau. Mais protéger seulement la page ne suffit pas : l'adresse WebSocket est une porte distincte, et quelqu'un pourrait se connecter directement à ws://ip/ws. L'exemple ci-dessous ferme aussi cette porte : à chaque démarrage, l'ESP32 génère une clé aléatoire, ne la donne qu'au navigateur connecté via /anahtar et rejette avec setFilter() les connexions WebSocket qui n'apportent pas la bonne clé.
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}Remarques pour l'ESP8266 et le Raspberry Pi Pico W
La même bibliothèque fonctionne aussi sur l'ESP8266 (NodeMCU, Wemos D1 mini). Voici ce qu'il faut changer : installe la bibliothèque ESP Async TCP d'ESP32Async au lieu d'Async TCP ; inclus ESP8266WiFi.h au lieu de WiFi.h, ESPAsyncTCP.h au lieu d'AsyncTCP.h et ESP8266mDNS.h au lieu d'ESPmDNS.h, et ajoute la ligne MDNS.update() dans loop(). Remplace WiFi.setSleep(false) par WiFi.setSleepMode(WIFI_NONE_SLEEP) et adapte les broches d'après la colonne NodeMCU du tableau de câblage. L'ESP8266 n'a qu'environ 80 Ko de RAM utilisable : garde la page légère et souviens-toi que la limite est de 4 clients. Si tu utilises un Raspberry Pi Pico W, tu trouveras comment créer un serveur web en MicroPython dans le guide Raspberry Pi Pico W et Wi-Fi ; les versions récentes d'ESPAsyncWebServer prennent aussi en charge les puces RP2040 et RP2350 côté Arduino.
Idées de projets avec le serveur web ESP32
- Prise connectée multicanal : avec une carte 4 relais, crée des boutons séparés pour une lampe, un ventilateur et une prise ; distingue les commandes par de courts textes comme "r1" et "r2". Pour choisir le relais, consulte l'article sur le module relais.
- Tableau de bord d'arrosage : affiche en direct la valeur du capteur d'humidité du sol, règle le seuil d'arrosage avec un curseur sur la page et lance la pompe à la main quand tu veux.
- Station météo : utilise un BME280 à la place du DHT22 pour ajouter la pression, et affiche les 60 dernières mesures sous forme de petite courbe sur la page.
- Lampe à sélecteur de couleur : envoie par WebSocket la valeur d'un sélecteur de couleur de la page et colore instantanément un ruban WS2812B NeoPixel.
- Servo au curseur : envoie la valeur du curseur à chaque mouvement et fais tourner un servomoteur ; idéal pour orienter une caméra ou actionner un loquet.
- Journal horodaté : ajoute à chaque mesure l'heure obtenue par NTP et mets ton code à jour sans fil par OTA plutôt que par USB à chaque fois.
Erreurs fréquentes et solutions
| Symptôme | Cause | Solution |
|---|---|---|
| 'mbedtls_md5_starts_ret' was not declared in this scope | Ancienne bibliothèque me-no-dev avec le cœur ESP32 3.x | Installe ESP Async WebServer et Async TCP d'ESP32Async, supprime les anciens dossiers |
| Multiple libraries were found for "AsyncTCP.h" | Deux copies de la même bibliothèque | Supprime les anciennes copies -master du dossier libraries |
| Avertissement deprecated sur send_P | send_P est en cours de retrait dans la nouvelle version | Utilise request->send(200, "text/html", index_html) |
| La page ne s'ouvre pas du tout | Téléphone sur le réseau invité ou en 4G, ou adresse IP changée | Rejoins le même Wi-Fi et utilise l'IP actuelle du moniteur série |
| La page s'ouvre mais les valeurs restent à -- | WebSocket ne se connecte pas : chemin différent ou addHandler oublié | Ouvre la console avec F12 et aligne ws("/ws") sur l'adresse du JavaScript |
| Task watchdog got triggered avec async_tcp | delay(), longue boucle ou requête HTTP dans un callback | Garde le callback court et déplace le travail dans loop() avec un drapeau |
| Too many messages queued | textAll() est appelé trop souvent | N'envoie qu'en cas de changement ou quelques fois par seconde |
| La page LittleFS renvoie 404 | Dossier data non téléversé ou setDefaultFile manquant | Téléverse les fichiers avec le plugin et ajoute setDefaultFile("index.html") |
| does not exist, no permits for creation dans le moniteur série | Le fichier demandé n'est pas dans LittleFS | Vérifie le nom du fichier, majuscules et minuscules comprises ; les lignes avec .gz sont sans danger |
| Le relais ne se coupe jamais | Le module 5 V ne reconnaît pas 3,3 V comme HIGH | Utilise un module compatible 3,3 V, JD-VCC ou un transistor |
| L'ESP32 redémarre quand le relais colle | Le courant de la bobine fait chuter l'alimentation (brownout) | Utilise un bon bloc 5 V et un condensateur de 470 µF |
| esp32.local ne s'ouvre pas | Appareil sans prise en charge mDNS ou nom écrit avec .local | Écris MDNS.begin("esp32") et utilise l'adresse IP si besoin |
| Le bouton réagit avec du retard | La veille du modem Wi-Fi est active | Appelle WiFi.setSleep(false) après la connexion |
Le duo serveur web asynchrone ESP32 et WebSocket est le chemin le plus court vers un tableau de bord rapide, qui fonctionne sans service cloud ni appli mobile à écrire, et que plusieurs appareils peuvent partager. Commence par le tableau de bord PROGMEM en un seul fichier, passe à LittleFS quand l'interface grandit, et donne à chaque nouvel appareil de ton réseau son propre nom mDNS. Si tu découvres l'ESP32, tu trouveras les bases dans Qu'est-ce que l'ESP32 ? Dans l'application BlueGrays, tu peux aussi suivre pas à pas, avec schémas de câblage et code prêt à l'emploi, des projets ESP32 comme un serveur web asynchrone, un compteur WebSocket, l'accès par mDNS, une horloge NTP et la mise à jour OTA.
Questions fréquentes
Quelle est la différence entre ESPAsyncWebServer et WebServer sur l'ESP32 ?
La bibliothèque WebServer traite les requêtes une par une via l'appel à handleClient() dans loop() ; si loop() ralentit, la page ralentit aussi. ESPAsyncWebServer traite les requêtes dans une tâche séparée en arrière-plan, supporte plusieurs connexions à la fois et intègre WebSocket et SSE. Pour les tableaux de bord en temps réel, la version asynchrone est la plus adaptée.
Combien de clients peuvent se connecter en même temps à un serveur web ESP32 ?
Par défaut, ESPAsyncWebServer autorise 8 clients WebSocket sur l'ESP32 et ferme les clients en trop lors de l'appel à cleanupClients() ; sur l'ESP8266, la limite est de 4. Comme les requêtes de page classiques sont brèves, davantage d'appareils peuvent ouvrir la page les uns après les autres. En pratique, un tableau de bord domestique tourne sans problème avec 3 à 5 appareils simultanés.
Puis-je accéder à mon serveur web ESP32 depuis internet ?
C'est techniquement possible avec une redirection de port sur ta box, mais ce n'est pas conseillé : la bibliothèque ne propose pas de HTTPS sur l'ESP32 et l'appareil reste exposé aux attaques venues d'internet. Pour piloter à distance, un bot Telegram, un système basé sur MQTT ou la fonction VPN de ta box sont bien plus sûrs.
Pourquoi esp32.local ne fonctionne pas ?
La cause la plus fréquente est que le téléphone ou l'ordinateur ne sait pas résoudre les noms mDNS ; c'est surtout le cas sur certains téléphones Android et sur les anciennes versions de Windows. Écrire MDNS.begin("esp32.local") dans le code est aussi une erreur, car le nom doit être donné sans .local. Si le nom ne se résout toujours pas, utilise l'adresse IP du moniteur série ou réserve une IP fixe pour l'ESP32 sur ta box.
Un serveur web ESP32 fonctionne-t-il sans box ni routeur ?
Oui. Avec WiFi.softAP("ESP32-Panel", "12345678"), l'ESP32 crée son propre réseau Wi-Fi ; connecte ton téléphone à ce réseau et ouvre 192.168.4.1 dans le navigateur. Dans ce mode point d'accès, il n'y a pas d'internet, mais le tableau de bord et WebSocket fonctionnent de la même façon. Le mot de passe du réseau doit compter au moins 8 caractères.
Quelle bibliothèque ESPAsyncWebServer faut-il installer ?
Dans le gestionnaire de bibliothèques de l'Arduino IDE, installe ESP Async WebServer et Async TCP dont l'auteur est ESP32Async. La version d'origine du compte me-no-dev n'est plus maintenue et ne compile pas avec le cœur ESP32 3.x. Supprime aussi de ton dossier libraries les anciennes copies ajoutées depuis un ZIP.
Projets à réaliser après cet article
Voir tous les projets
Serveur Web asynchrone ESP32 (avec affichage de l'adresse IP sur l'écran LCD)
Dans ce projet, nous transformons l'ESP32 en un serveur web asynchrone que vous pouvez contrôler depuis votre téléphone ou votre ordinateur connecté au même réseau Wi-Fi. Grâce à la bibliothèque ESPAsyncWebServer, le serveur peut traiter plusieurs requêtes simultanément (contrairement au classique WebServer.h qui se bloque sur une seule requête). Depuis votre navigateur, vous pourrez vous connecter à l'adresse IP de l'ESP32, puis allumer et éteindre la LED co

Compteur ESP32 WebSocket (2 boutons + écran LCD + affichage Web en temps réel)
Dans ce projet, deux boutons reliés à l'ESP32 (GPIO 5 incrémente, GPIO 4 décrémente) modifient un compteur. La valeur se met à jour instantanément sur un écran LCD I2C 16x2 et, grâce au support WebSocket d'ESPAsyncWebServer, dans tous les navigateurs connectés sans recharger la page. Au lieu du polling HTTP, une seule connexion persistante est utilisée et l'ESP32 envoie la nouvelle valeur à tous avec ws.textAll().

ESP32 mDNS : accès à l'appareil via bluegrays-esp.local (avec écran LCD)
Dans ce projet, en ajoutant un service mDNS (Multicast DNS) à l'ESP32, nous permettons d'accéder à l'appareil via un nom d'hôte lisible, tel que bluegrays-esp.local, plutôt qu'à l'aide d'une adresse IP. Même si l'adresse IP attribuée par le DHCP venait à changer, vous pourrez toujours accéder à l'appareil sous le même nom. En exécutant également un petit serveur web sur l'appareil, nous envoyons des commandes de contrôle des LED depuis un navigateur via l'adresse http://bluegrays-esp.local/.

Connexion Wi-Fi STA ESP32 (avec écran LCD)
Dans ce projet, nous connectons l'ESP32 au réseau Wi-Fi domestique en mode Station (STA) de manière sécurisée et tolérante aux pannes. Nous isolons SSID/mot de passe dans un fichier secrets.h pour empêcher leur fuite sur Git, mettons en place une reconnexion automatique, et affichons le niveau RSSI et l'adresse IP sur un écran LCD I2C. La LED intégrée sert d'indicateur d'état.

ESP32 DHT22 Température et humidité (avec écran LCD)
Il s'agit d'un projet permettant de mesurer la température ambiante et le taux d'humidité à l'aide d'un capteur DHT22 et d'afficher ces données en temps réel sur un écran LCD I2C 16x2. Les données du capteur sont lues à l'aide du protocole one-wire grâce à la bibliothèque Adafruit DHT. Ce projet constitue la base des stations météo IoT, de la domotique ou des systèmes de surveillance de l'environnement dans les serres.

Éclairage Intelligent ESP32 (PIR + LDR + Relais + OLED)
Transformez votre couloir, votre salle de bain ou votre garde-manger en éclairage intelligent avec l'ESP32 ! En utilisant un capteur de mouvement PIR, un capteur de lumière LDR et un module relais, nous construisons le scénario classique "la lumière s'allume lorsqu'un mouvement est détecté dans l'obscurité" — si l'environnement est déjà lumineux, le mouvement N'ACTIVE PAS la lumière, économisant ainsi de l'énergie pendant la journée. La lumière reste allumée pendant 30 secondes ; si le mouvement

Mise à jour OTA de l'ESP32 — Installation sans fil du firmware (avec écran LCD)
Dans ce projet, vous allez apprendre à installer un nouveau firmware sur l'ESP32 via Wi-Fi, sans câble USB. Grâce à la bibliothèque ArduinoOTA, l'ESP32 devient visible sur le réseau et apparaît automatiquement sous le nom bluegrays-ota dans l'onglet « Network port » de l'IDE Arduino. La première installation s'effectue via USB, puis toutes les mises à jour se font sans fil — une fonctionnalité essentielle pour les appareils sur le terrain. L'écran LCD I2C affiche l'état du Wi-Fi, l'adresse I

ESP32 : heure réelle depuis Internet via NTP
L'ESP32 se connecte au Wi-Fi et récupère l'heure réelle avec une précision de la milliseconde depuis des serveurs NTP (Network Time Protocol). Grâce à configTzTime(), le fuseau horaire de la Turquie (TRT, UTC+3) est appliqué automatiquement et le passage DST est géré via la chaîne POSIX TZ. L'heure est affichée sur le moniteur série et sur un écran LCD I2C en option. configTzTime() démarre le service SNTP ; des resynchronisations périodiques ont lieu tant que l'accès au réseau est disponible
Autres articles
Application BlueGrays
Réalise ce montage pas à pas sur ton téléphone
Schéma, code et liste des composants sur un seul écran. Plus de 200 projets Arduino, ESP32 et Pico à réaliser.
Scanne avec ton téléphone
Pointe l’appareil photo vers le code : la boutique d’applications de ton téléphone s’ouvre.
ou ouvre la boutique

