Servidor web assíncrono no ESP32 com WebSocket: painel de controle em tempo real
Servidor web assíncrono no ESP32 com WebSocket: acione um relé e acompanhe dados de sensores ao vivo sem recarregar a página. Instalação, PROGMEM ou LittleFS, esp32.local, senha e soluções.
Um servidor web assíncrono no ESP32, feito com a biblioteca ESPAsyncWebServer, transforma seu ESP32 em um servidor web rápido, capaz de responder a vários celulares e computadores ao mesmo tempo. Com o suporte a WebSocket que já vem na biblioteca, o navegador e a placa mantêm uma linha sempre aberta: você aperta o botão na página e o relé aciona na hora; quando a temperatura muda, o número na página se atualiza sozinho, sem recarregar. Todos os outros dispositivos com o mesmo painel aberto veem sua alteração no mesmo segundo. Resumindo, essa dupla é o jeito mais prático de montar seu próprio painel de casa inteligente dentro da sua rede doméstica, sem nenhum serviço de nuvem.
Neste guia você vai ver primeiro como um servidor assíncrono se diferencia da biblioteca WebServer clássica e por que o WebSocket é mais rápido que requisições HTTP comuns. Depois vai instalar as bibliotecas certas, rodar o servidor mais simples possível e montar um painel de controle em tempo real com um relé e um sensor DHT22. Em seguida vai aprender a guardar o HTML em PROGMEM ou no LittleFS, usar esp32.local no lugar do endereço IP, lidar com vários clientes, colocar uma senha simples no painel e resolver os erros de compilação e de conexão mais comuns. O código foi escrito para funcionar tanto com as versões 2.x quanto 3.x do core do ESP32 no Arduino IDE.

O que é o servidor web assíncrono do ESP32 e como ele funciona?
A biblioteca WebServer.h que vem com o core do ESP32 é síncrona: as requisições que chegam só são tratadas, uma de cada vez, quando server.handleClient() é chamado dentro do loop(). Se o loop() tiver um delay() de 2 segundos, a página também espera 2 segundos; se duas pessoas se conectarem ao mesmo tempo, uma espera a outra terminar. Já a ESPAsyncWebServer roda em uma tarefa separada do FreeRTOS (async_tcp), criada pela biblioteca AsyncTCP. Quando uma requisição chega, os cabeçalhos são lidos em segundo plano, a função que você escreveu (o callback) é chamada assim que a requisição está pronta, e a resposta também é enviada em segundo plano. Enquanto isso, o loop() segue com o próprio trabalho. Isso permite manter várias conexões abertas ao mesmo tempo e torna possíveis conexões sempre abertas, como WebSocket e Server-Sent Events (SSE). Se você tem curiosidade sobre os dois núcleos e as tarefas do ESP32, dê uma olhada no artigo ESP32 FreeRTOS.
| Característica | WebServer.h (síncrona) | ESPAsyncWebServer (assíncrona) |
|---|---|---|
| Instalação | Já vem com o core do ESP32 | 2 bibliotecas pelo Gerenciador de Bibliotecas |
| Tratamento das requisições | Uma por vez, via handleClient() no loop() | Na tarefa async_tcp em segundo plano, várias ao mesmo tempo |
| delay() dentro do loop() | Atrasa a resposta da página | Não afeta o servidor |
| WebSocket | Não tem; precisa de outra biblioteca (ex.: a biblioteca WebSockets de Markus Sattler) | Integrado (AsyncWebSocket) |
| Server-Sent Events | Não tem | Integrado (AsyncEventSource) |
| Servir arquivos estáticos | Com serveStatic | serveStatic, suporte automático a .gz e controle de cache |
| Atenção a | A mais fácil de aprender | Nada de delay() ou trabalho demorado dentro dos callbacks |
O que é WebSocket e qual a diferença para uma requisição HTTP?
Em uma requisição HTTP normal, o navegador abre uma conexão, diz “me dê esta página”, recebe a resposta e a conversa termina. O servidor não consegue avisar o navegador de nada por conta própria; para ver um valor novo, o navegador precisa perguntar de novo. O WebSocket (RFC 6455) é um handshake que começa como HTTP: o navegador adiciona o cabeçalho Upgrade: websocket à requisição, o ESP32 aceita com a resposta 101 Switching Protocols e a mesma conexão TCP continua aberta. A partir daí, os dois lados podem enviar mensagens quando quiserem (full-duplex). Cada mensagem carrega só de 2 a 14 bytes de overhead, enquanto cada requisição HTTP carrega centenas de bytes de cabeçalho. Por isso as mensagens WebSocket chegam em milissegundos, e esse é o método mais eficiente para painéis que enviam várias atualizações por segundo. Veja como se comparam as quatro formas de manter uma página atualizada com os valores dos sensores:
| Método | Como funciona | Latência e overhead | Quando usar |
|---|---|---|---|
| Recarregar a página (meta refresh) | O navegador baixa a página inteira de novo a cada poucos segundos | Alto; a página pisca toda vez | Evite |
| Polling com fetch | O JavaScript pede um JSON pequeno a cada 1–2 segundos | Médio; cada requisição leva cabeçalhos novos | Um único valor que muda pouco |
| Server-Sent Events (SSE) | O servidor envia eventos em um só sentido por uma conexão aberta | Baixo; só do servidor para o navegador | Painéis só de monitoramento |
| WebSocket | Troca de mensagens nos dois sentidos por uma conexão aberta | O mais baixo; quadros de poucos bytes | Botões, sliders e dados ao vivo juntos |
Bibliotecas necessárias: instalando ESPAsyncWebServer e AsyncTCP
- Confira se o pacote de placas do ESP32 (Espressif Systems) está instalado no Arduino IDE; os passos de instalação estão em O que é o ESP32?
- Abra o Gerenciador de Bibliotecas (Ctrl+Shift+I) e busque ESPAsyncWebServer. Instale a biblioteca ESP Async WebServer cujo autor é ESP32Async.
- Essa biblioteca não declara a dependência ao Gerenciador de Bibliotecas, então o IDE não vai oferecer bibliotecas extras: instale separadamente a biblioteca Async TCP, também publicada pela ESP32Async. Em um ESP8266, você precisa no lugar dela da biblioteca ESP Async TCP da ESP32Async (ESPAsyncTCP.h).
- Para o exemplo do painel, instale a DHT sensor library da Adafruit e a dependência Adafruit Unified Sensor que ela pede. Se for a primeira vez que você instala uma biblioteca, veja o guia como instalar bibliotecas no Arduino.
- Se a sua pasta Documentos/Arduino/libraries tiver pastas ESPAsyncWebServer-master, AsyncTCP-master ou ESPAsyncWebSrv adicionadas por ZIP no passado, apague-as. Duas cópias da mesma biblioteca fazem o compilador escolher a errada.
Primeiro passo: o servidor mais simples com AsyncWebServer
Antes de partir para o painel, vamos ver como a biblioteca funciona com um exemplo pequeno. O código abaixo conecta o ESP32 à sua rede doméstica, mostra uma saudação na página inicial e liga e desliga o LED integrado pelos endereços /led?durum=1 e /led?durum=0. Carregue o código, abra o Monitor Serial em 115200 baud e digite o endereço que aparece no navegador de um celular conectado ao mesmo Wi-Fi. Se a placa não entrar na rede, use o código de diagnóstico do guia ESP32 não conecta ao 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}| Comando | O que faz |
|---|---|
| AsyncWebServer server(80) | Cria o servidor que escuta a porta HTTP 80 |
| server.on("/led", HTTP_GET, ...) | Registra a função (callback) que roda para um endereço e um tipo de requisição |
| request->hasParam("durum") | Verifica se o endereço tem um parâmetro ?durum=... |
| request->getParam("durum")->value() (retorna o valor do parâmetro como String) | Retorna o valor do parâmetro como String |
| request->send(código, tipo, conteúdo) | Envia a resposta: 200 OK, 404 não encontrado; tipo text/plain, text/html ou application/json |
| server.onNotFound(...) | Roda para todo endereço que não foi definido |
| server.begin() | Inicia o servidor; chame depois de todas as linhas server.on() |
Componentes e ligações do painel de controle
No exemplo do painel, você vai ligar e desligar uma lâmpada ou uma ventoinha com um módulo relé e medir temperatura e umidade com um DHT22. Você vai precisar de um ESP32 DevKit, um módulo relé de 5 V de um canal, um módulo sensor DHT22 (ou DHT11), uma protoboard e jumpers. Na hora de escolher o relé, prefira módulos com optoacoplador. O DHT22 mede de −40 a 80 °C com precisão de ±0,5 °C e pode ser lido no máximo uma vez a cada 2 segundos; o DHT11 é mais barato, mas tem precisão de ±2 °C na faixa de 0–50 °C. Para os detalhes do sensor, veja o artigo sensor de temperatura e umidade DHT11 e, para o mapa de pinos da sua placa, a ferramenta Pinagem.
| Pino do módulo | ESP32 DevKit | ESP8266 NodeMCU | Observação |
|---|---|---|---|
| Relé VCC | VIN (5 V) | VIN ou VU (5 V) | A bobina do relé precisa de 5 V; é alimentada pela USB |
| Relé GND | GND | GND | GND comum |
| Relé IN | GPIO 26 | D1 (GPIO 5) | A maioria dos módulos aciona com sinal LOW |
| DHT22 VCC (+) | 3V3 | 3V3 | Alimentado com 3,3 V, a linha de dados também fica em 3,3 V |
| DHT22 DATA (OUT) | GPIO 4 | D2 (GPIO 4) | Sensor avulso: resistor de 10 kΩ entre DATA e VCC |
| DHT22 GND (−) | GND | GND | GND comum |
Painel de controle em tempo real: relé, DHT22 e WebSocket
Agora o projeto principal. O código abaixo é um arquivo só: o HTML, o CSS e o JavaScript ficam dentro dele como um array PROGMEM. Quando a página abre, o navegador abre uma conexão WebSocket com /ws. A cada 2 segundos o ESP32 lê o DHT22 e envia a temperatura, a umidade, o estado do relé e o número de dispositivos conectados para todos os navegadores em uma única mensagem JSON. Quando você aperta o botão, o navegador envia a mensagem toggle, o ESP32 inverte o relé e de novo avisa todo mundo do novo estado. Troque o nome da rede e a senha, carregue o código e abra no navegador http://esp32.local ou o endereço IP mostrado no Monitor Serial.
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}Como o código funciona?
- AsyncWebSocket ws("/ws") cria o endpoint WebSocket, e server.addHandler(&ws) o conecta ao servidor. O endereço no JavaScript tem que bater exatamente com esse caminho.
- A função wsOlay() é chamada a cada conexão, desconexão e mensagem recebida. WS_EVT_CONNECT significa um cliente novo, WS_EVT_DISCONNECT um cliente saindo e WS_EVT_DATA uma mensagem chegando.
- AwsFrameInfo traz as informações do quadro recebido. Verificar final, index == 0 e len confirma que a mensagem chegou completa em um único quadro, e WS_TEXT que ela é texto. Comandos curtos sempre cabem em um quadro.
- Os dados recebidos são comparados junto com o comprimento usando memcmp(), assim você não depende de existir ou não um caractere nulo no fim dos dados.
- Como os callbacks rodam na tarefa async_tcp, eles só invertem o relé e levantam a flag gonder. O envio de fato acontece no loop() com ws.textAll(), então todas as mensagens saem de um lugar só, em ordem.
- ws.cleanupClients() é chamado uma vez por segundo: ele libera a memória das conexões perdidas e fecha a conexão mais antiga quando o número de clientes passa do limite.
- No lado do JavaScript, o evento onclose reconecta depois de 2 segundos. Mesmo que o ESP32 reinicie ou o Wi-Fi caia por um instante, a página se recupera sozinha. Como o endereço é montado a partir de location.host, a página funciona tanto com o IP quanto com esp32.local.
- WiFi.setSleep(false) desliga a economia de energia do Wi-Fi. O modem sleep padrão pode acrescentar atrasos de até algumas centenas de milissegundos às respostas; desligá-lo em um painel alimentado pela tomada faz os botões responderem na hora.
Escolher JSON como formato das mensagens facilita a vida: o navegador transforma o texto recebido em um objeto em uma linha com JSON.parse(). Uma mensagem de exemplo fica assim: {"role":true,"sicaklik":23.4,"nem":48,"istemci":2}. Quando o sensor não pode ser lido, o valor é enviado como null e a página mostra “--”; se uma leitura com falha saísse como NaN, o JSON inválido impediria a página de se atualizar. Como cada mensagem tem só uns 60 bytes, nem várias atualizações por segundo sobrecarregam a rede. Para mensagens mais complexas você pode passar para a biblioteca ArduinoJson, mas para um painel deste tamanho, juntar Strings é mais que suficiente. Você também pode testar o mesmo código sem mudar nada, com um LED ou um buzzer no lugar do relé.
Onde guardar o HTML: PROGMEM ou LittleFS?
No código do painel, a página fica em uma string literal bruta escrita como R"rawliteral( ... )rawliteral". Essa sintaxe permite usar aspas e quebras de linha sem caracteres de escape. Nos Arduinos baseados em AVR, o PROGMEM é necessário para manter o texto na flash em vez da RAM; no ESP32, arrays constantes já são lidos da flash, então o PROGMEM é escrito só por compatibilidade e não faz mal. À medida que a página cresce, editar HTML dentro do código fica chato; nesse ponto, é mais organizado mover os arquivos para o sistema de arquivos LittleFS na flash.
| Critério | PROGMEM (dentro do código) | LittleFS (sistema de arquivos) |
|---|---|---|
| Instalação | Sem passo extra, um arquivo só | Precisa de uma pasta data e de um plugin de upload |
| Edição | HTML dentro de um array C++ | Arquivos separados index.html, style.css e app.js |
| Atualização | Você carrega o sketch de novo a cada mudança | Você só carrega de novo o sistema de arquivos |
| Tamanho | Ideal para páginas de poucos KB | Cerca de 1,5 MB de espaço com o particionamento padrão |
| Imagens e fontes | Pouco prático | Dá para servir ícones, imagens e arquivos .gz |
| Quando? | Primeiros testes e painéis pequenos | Interfaces com várias páginas ou que vão crescer |
Servindo arquivos HTML, CSS e JavaScript com LittleFS
- Dentro da pasta do sketch (a pasta que contém o arquivo .ino), crie uma pasta chamada data e salve o HTML que fica entre R"rawliteral( e )rawliteral" no código do painel como data/index.html. Se quiser, pode separar o CSS e o JavaScript em arquivos próprios.
- O Arduino IDE 2 não suporta o antigo menu ESP32 Sketch Data Upload. Em vez disso, baixe o arquivo .vsix do plugin arduino-littlefs-upload e coloque-o em C:\Users\(seu nome de usuário)\.arduinoIDE\plugins no Windows ou em ~/.arduinoIDE/plugins no macOS e no Linux (crie a pasta se ela não existir) e reinicie o IDE. O plugin exige o IDE 2.2.1 ou mais recente.
- Feche o Monitor Serial se estiver aberto, abra a paleta de comandos com Ctrl+Shift+P (Cmd+Shift+P no macOS) e escolha Upload LittleFS to Pico/ESP8266/ESP32. Os arquivos são gravados na partição do sistema de arquivos na flash.
- Confira se em Ferramentas > Partition Scheme está selecionado um esquema com partição de sistema de arquivos (por exemplo, Default 4MB with spiffs). O LittleFS usa essa partição, que se chama spiffs.
- Carregue o código abaixo como de costume. Enquanto a opção Erase All Flash do menu Ferramentas estiver desativada, carregar um sketch não apaga o sistema de arquivos; quando você mudar os arquivos, basta repetir o passo 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 no lugar do endereço IP
O seu roteador pode dar ao ESP32 um endereço IP diferente a cada vez que ele liga, e números como 192.168.1.47 são difíceis de lembrar. O mDNS (multicast DNS) permite que um dispositivo anuncie o próprio nome na rede local. Graças à linha MDNS.begin("esp32") no código do painel, os dispositivos da mesma rede podem abrir a página em http://esp32.local, e MDNS.addService("http", "tcp", 80) avisa aos apps que exploram a rede que há um servidor web ali. Não coloque “.local” no nome e use só letras minúsculas, números e hifens. Se você usa vários ESP32, dê um nome diferente a cada um (cozinha, sala-de-estar e assim por diante), porque dois dispositivos com o mesmo nome atrapalham um ao outro. O mDNS não tem o mesmo suporte em todos os sistemas:
| Sistema | Suporte a .local | Se não funcionar |
|---|---|---|
| iPhone, iPad, macOS | Integrado (Bonjour) | Confira se você está na mesma rede Wi-Fi |
| Windows 10 e 11 | Costuma funcionar nas versões atualizadas | Instale o serviço Bonjour ou use o IP |
| Linux | Funciona se o avahi-daemon estiver instalado | Instale os pacotes avahi-daemon e nss-mdns |
| Android | Depende da versão e do navegador | Use o endereço IP ou faça uma reserva de DHCP no roteador |
Vários clientes: celular, tablet e computador ao mesmo tempo
O que todo mundo mais gosta no WebSocket é que todos que abrem o painel veem o mesmo estado. Quando você liga o relé pelo celular, o ESP32 envia o novo estado para todas as conexões com textAll(), e a aba no computador fica verde no mesmo instante. Cada aba é um cliente separado: abra três abas no mesmo navegador e o contador mostra 3. Por padrão, a biblioteca permite 8 clientes WebSocket simultâneos no ESP32 e 4 no ESP8266 (DEFAULT_MAX_WS_CLIENTS). Esse limite não é aplicado automaticamente; quando cleanupClients() é chamado, a conexão mais antiga acima do limite é fechada. Veja o que mais vale ter em mente:
- Quando o celular entra em repouso: um celular com a tela apagada pode não fechar a conexão direito. Essas conexões meio abertas são limpas com o tempo graças ao cleanupClients() e ao envio regular de dados.
- Ritmo de envio: até 32 mensagens podem esperar na fila de cada cliente (8 no ESP8266). Se você chamar textAll() a cada ciclo do loop(), a fila enche e as mensagens novas são descartadas sem aviso; coloque Ferramentas > Core Debug Level em Warn ou em um nível mais detalhado e você verá o aviso Too many messages queued no Monitor Serial. Envie um valor só quando ele mudar, ou algumas vezes por segundo.
- Cliente recém-conectado: envie o estado atual para toda página que se conectar, logo de cara; senão o usuário fica olhando um painel vazio até a primeira leitura. No código do painel, quem faz isso é a flag levantada em WS_EVT_CONNECT.
- Responder a um único cliente: para responder só ao navegador que perguntou, e não a todos, use client->text("..."); client->id() dá o número do cliente.
- Memória: cada conexão aberta ocupa um socket TCP e memória de buffer. Os 520 KB de RAM do ESP32 sobram para alguns clientes, mas imprimir de vez em quando a memória livre com ESP.getFreeHeap() ajuda a pegar vazamentos cedo.
Proteção simples com senha
Qualquer pessoa na sua rede doméstica que saiba o endereço do painel pode acionar o relé. Com as funções request->authenticate() e request->requestAuthentication() da biblioteca, você pode colocar usuário e senha na página; o navegador abre a própria janela de login e lembra os dados durante a sessão. O método padrão é a autenticação Digest, ou seja, a senha não trafega pela rede em texto puro. Mas proteger só a página não basta: o endereço do WebSocket é outra porta, e alguém poderia se conectar direto a ws://ip/ws. O exemplo abaixo fecha essa porta também: a cada inicialização, o ESP32 gera uma chave aleatória, entrega essa chave só para um navegador já logado em /anahtar e usa setFilter() para recusar conexões WebSocket que não tragam a chave certa.
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}Observações para ESP8266 e Raspberry Pi Pico W
A mesma biblioteca também roda no ESP8266 (NodeMCU, Wemos D1 mini). Veja o que mudar: instale a biblioteca ESP Async TCP da ESP32Async no lugar da Async TCP; inclua ESP8266WiFi.h no lugar de WiFi.h, ESPAsyncTCP.h no lugar de AsyncTCP.h e ESP8266mDNS.h no lugar de ESPmDNS.h, e adicione a linha MDNS.update() dentro do loop(). Troque WiFi.setSleep(false) por WiFi.setSleepMode(WIFI_NONE_SLEEP) e defina os pinos conforme a coluna NodeMCU da tabela de ligações. O ESP8266 tem só cerca de 80 KB de RAM utilizável, então mantenha a página pequena e lembre que o limite de clientes é 4. Se você usa um Raspberry Pi Pico W, encontra como montar um servidor web com MicroPython no guia Raspberry Pi Pico W Wi-Fi; as versões mais novas da ESPAsyncWebServer também suportam os chips RP2040 e RP2350 no lado do Arduino.
Ideias de projetos com servidor web no ESP32
- Tomada inteligente de vários canais: use uma placa de relés de 4 canais para criar botões separados para uma lâmpada, uma ventoinha e uma tomada; diferencie os comandos com textos curtos como “r1” e “r2”. Para escolher o relé, veja o artigo módulo relé.
- Painel de irrigação de plantas: mostre ao vivo o valor do sensor de umidade do solo, ajuste o limite de irrigação com um slider na página e acione a bomba manualmente quando quiser.
- Estação meteorológica: use um BME280 no lugar do DHT22 para incluir a pressão atmosférica e mostre as últimas 60 leituras em um pequeno gráfico de linha na página.
- Luminária com seletor de cor: envie pelo WebSocket o valor de um seletor de cor da página e pinte na hora uma fita WS2812B NeoPixel.
- Servo no slider: envie o valor do slider a cada movimento e gire um servo motor; ideal para apontar uma câmera ou para uma trava de porta.
- Registro com data e hora: acrescente a cada leitura o horário obtido via NTP e atualize seu código sem fio via OTA em vez de usar a USB toda vez.
Erros comuns e como resolver
| Sintoma | Causa | Solução |
|---|---|---|
| 'mbedtls_md5_starts_ret' was not declared in this scope (não declarado neste escopo) | Biblioteca antiga da me-no-dev com o core 3.x do ESP32 | Instale ESP Async WebServer e Async TCP da ESP32Async e apague as pastas antigas |
| Multiple libraries were found for "AsyncTCP.h" (várias bibliotecas encontradas) | Duas cópias da mesma biblioteca | Apague as cópias antigas -master na pasta libraries |
| Aviso de deprecated para send_P | send_P está sendo removida na versão nova | Use request->send(200, "text/html", index_html) no lugar de send_P |
| A página não abre de jeito nenhum | Celular na rede de convidados ou nos dados móveis, ou o endereço IP mudou | Conecte-se ao mesmo Wi-Fi e use o IP atual do Monitor Serial |
| A página abre, mas os valores ficam em -- | O WebSocket não conectou: caminho diferente ou addHandler esquecido | Abra o console com F12 e faça ws("/ws") bater com o endereço no JavaScript |
| Task watchdog got triggered com async_tcp | delay(), laço longo ou requisição HTTP dentro de um callback | Mantenha o callback curto e passe o trabalho para o loop() com uma flag |
| Too many messages queued (mensagens demais na fila) | textAll() está sendo chamado com frequência demais | Envie só quando mudar, ou algumas vezes por segundo |
| A página no LittleFS retorna 404 | Pasta data não enviada ou falta setDefaultFile | Envie os arquivos com o plugin e adicione setDefaultFile("index.html") |
| does not exist, no permits for creation no Monitor Serial | O arquivo pedido não está no LittleFS | Confira o nome do arquivo e as maiúsculas e minúsculas; linhas que mencionam .gz são inofensivas |
| O relé nunca desliga | O módulo de 5 V não reconhece um HIGH de 3,3 V | Use um módulo compatível com 3,3 V, JD-VCC ou um transistor |
| O ESP32 reinicia quando o relé aciona | A corrente da bobina derruba a alimentação (brownout) | Use uma fonte de 5 V de qualidade e um capacitor de 470 µF |
| esp32.local não abre | O dispositivo não tem suporte a mDNS ou o nome foi escrito com .local | Escreva MDNS.begin("esp32") e use o endereço IP se for preciso |
| O botão demora a responder | O modem sleep do Wi-Fi está ativado | Chame WiFi.setSleep(false) depois de conectar |
A combinação de servidor web assíncrono no ESP32 com WebSocket é o caminho mais curto para um painel de controle rápido, que funciona sem serviço de nuvem e sem precisar escrever um app de celular, e que vários dispositivos podem compartilhar. Comece com o painel PROGMEM de um arquivo só, passe para o LittleFS quando a interface crescer e dê a cada novo dispositivo da sua rede o próprio nome mDNS. Se você está começando com o ESP32, encontra o básico em O que é o ESP32? No app BlueGrays você também pode explorar passo a passo projetos com ESP32, como servidor web assíncrono, contador com WebSocket, acesso por mDNS, relógio NTP e atualização OTA, com esquemas do circuito e código pronto para usar.
Perguntas frequentes
Qual a diferença entre ESPAsyncWebServer e WebServer no ESP32?
A biblioteca WebServer trata as requisições uma por vez pela chamada handleClient() no loop(); se o loop() fica lento, a página também fica. A ESPAsyncWebServer trata as requisições em uma tarefa separada em segundo plano, aguenta várias conexões ao mesmo tempo e já vem com suporte a WebSocket e SSE. Para painéis em tempo real, a versão assíncrona é a mais indicada.
Quantos clientes podem se conectar ao mesmo tempo a um servidor web no ESP32?
Por padrão, a ESPAsyncWebServer permite 8 clientes WebSocket no ESP32 e fecha os excedentes quando cleanupClients() é chamado; no ESP8266 o limite é 4. Como as requisições normais de página são curtas, mais dispositivos podem abrir a página um depois do outro. Na prática, 3–5 dispositivos simultâneos funcionam sem problemas em um painel doméstico.
Posso acessar meu servidor web do ESP32 pela internet?
Tecnicamente é possível com redirecionamento de portas no roteador, mas não é recomendado: a biblioteca não oferece HTTPS no ESP32 e o dispositivo fica exposto a ataques vindos da internet. Para controle remoto, um bot do Telegram, um sistema baseado em MQTT ou o recurso de VPN do seu roteador são bem mais seguros.
Por que o esp32.local não funciona?
O motivo mais comum é o celular ou o computador não conseguir resolver nomes mDNS; isso acontece principalmente em alguns celulares Android e em versões antigas do Windows. Escrever MDNS.begin("esp32.local") no código também é um erro, porque o nome deve ser dado sem .local. Se o nome continuar sem resolver, use o endereço IP do Monitor Serial ou reserve um IP fixo para o ESP32 no roteador.
Um servidor web no ESP32 funciona sem roteador?
Sim. Com WiFi.softAP("ESP32-Panel", "12345678") o ESP32 cria a própria rede Wi-Fi; conecte o celular a ela e abra 192.168.4.1 no navegador. Nesse modo de ponto de acesso não há internet, mas o painel e o WebSocket funcionam do mesmo jeito. A senha da rede precisa ter pelo menos 8 caracteres.
Qual biblioteca ESPAsyncWebServer devo instalar?
No Gerenciador de Bibliotecas do Arduino, instale as bibliotecas ESP Async WebServer e Async TCP cujo autor é ESP32Async. A versão original na conta me-no-dev não é mais mantida e não compila com o core 3.x do ESP32. Apague também da pasta libraries as cópias antigas adicionadas por ZIP.
Projetos para montar depois desta leitura
Ver todos os projetos
Servidor web assíncrono com ESP32 (IP no LCD)
Neste projeto, transformamos o ESP32 em um servidor web assíncrono que você pode controlar pelo celular ou computador na mesma rede Wi-Fi. Com a biblioteca ESPAsyncWebServer, o servidor processa várias requisições ao mesmo tempo (ao contrário do WebServer.h tradicional, que bloqueia a execução ao processar uma requisição). Você poderá abrir o endereço IP do ESP32 no navegador e clicar nos botões ON/OFF (as rotas /on e /off) para ligar e desligar o LED conectado ao GPIO 5.

Contador WebSocket com ESP32 (2 botões + LCD + web em tempo real)
Neste projeto, dois botões conectados ao ESP32 (GPIO 5 aumenta, GPIO 4 diminui) alteram um contador. O valor é atualizado instantaneamente no LCD I2C 16x2 e, com o suporte a WebSocket do ESPAsyncWebServer, em todos os navegadores conectados, sem recarregar a página. Em vez de consultas periódicas por HTTP, usamos uma única conexão persistente, e o ESP32 envia o novo valor a todos com ws.textAll().

ESP32 mDNS: acesso via bluegrays-esp.local (com display LCD)
Neste projeto, adicionamos o serviço mDNS (Multicast DNS) ao ESP32 para acessar o dispositivo por um nome de host fácil de ler, como bluegrays-esp.local, em vez de um endereço IP. Mesmo que o IP atribuído pelo DHCP mude, você pode acessar o dispositivo pelo mesmo nome. Também executamos um pequeno servidor web no dispositivo para enviar comandos de controle do LED pelo navegador no endereço http://bluegrays-esp.local/.

Conexão Wi-Fi STA com ESP32 e display LCD
Neste projeto, conectamos o ESP32 à rede Wi-Fi de casa no modo estação (STA), de forma segura e tolerante a falhas. Guardamos o SSID e a senha em um arquivo secrets.h separado para evitar que sejam expostos no Git, configuramos um mecanismo de reconexão automática e exibimos o nível de sinal RSSI e o endereço IP em um display LCD I2C. O LED integrado funciona como indicador de status. É um gerenciador de conexão pronto para uso em produção.

Temperatura e umidade com ESP32 e DHT22 (com display LCD)
Este projeto mede a temperatura e a umidade do ambiente com um sensor DHT22 e exibe os dados em tempo real em um LCD I2C 16x2. Os dados são lidos do sensor pelo protocolo de um fio (one-wire) com a biblioteca Adafruit DHT. Este projeto serve de base para estações meteorológicas IoT, automação residencial ou sistemas de monitoramento ambiental em estufas. A comunicação com o DHT22 é feita pelo pino GPIO 4 do ESP32.

Iluminação inteligente com ESP32 (PIR + LDR + relé + OLED)
Transforme a iluminação do corredor, banheiro ou despensa em iluminação inteligente com ESP32! Com um sensor de movimento PIR, um sensor de luz LDR e um módulo relé, montamos o cenário clássico de "acender a luz ao detectar movimento no escuro" — se o ambiente já estiver claro, o movimento NÃO aciona a luz, economizando energia durante o dia. A luz fica acesa por 30 segundos; se o movimento continuar, o temporizador reinicia e a luz permanece acesa por mais tempo.

Atualização OTA no ESP32 — firmware sem fio (com display LCD)
Neste projeto, você aprende a carregar um novo firmware no ESP32 por Wi-Fi, sem cabo USB. Com a biblioteca ArduinoOTA, o ESP32 fica visível na rede e aparece automaticamente como bluegrays-ota na aba "Porta de rede" do Arduino IDE. A primeira carga é feita por USB, e todas as atualizações seguintes são sem fio — um recurso essencial para dispositivos em campo. O LCD I2C mostra o estado do Wi-Fi, o IP, o nome do host, a porcentagem de envio por OTA e as mensagens de erro em turco e inglês.

Hora exata pela internet com ESP32 e NTP
O ESP32 se conecta ao Wi-Fi e obtém a hora exata com precisão de milissegundos de servidores NTP (Network Time Protocol). Com a função configTzTime(), o fuso horário configurado (TRT, UTC+3 neste exemplo) é aplicado automaticamente, e as mudanças nos países que adotam horário de verão são gerenciadas automaticamente por uma string POSIX TZ. A hora é exibida tanto no Monitor Serial quanto em um LCD I2C opcional.
Mais artigos
App BlueGrays
Monte este circuito passo a passo pelo celular
Esquema do circuito, código e lista de componentes em uma só tela. Mais de 200 projetos com Arduino, ESP32 e Pico prontos para montar.
Escaneie com seu celular
Aponte a câmera para o código para abrir a loja de aplicativos do seu celular.
ou abrir a loja

