Como funciona esse sistema?
Para entender a troca de mensagens entre o computador, o ESP32 e a internet, imagine a dinâmica de um restaurante:
- O Cliente (Navegador): Do seu celular ou computador, você abre o navegador e faz o pedido do site.
- O Garçom (ESP32): O chip recebe o seu pedido e serve o prato principal, que é a página web (HTML e CSS) armazenada na memória dele.
- A Cozinha (API externa): Quando você clica no botão “Atualizar”, a página web faz um pedido assíncrono rápido (AJAX). O ESP32 corre até a cozinha na nuvem (a API da OpenWeatherMap), pega os ingredientes climáticos mais frescos organizados em uma bandeja (JSON) e serve na mesa do cliente de forma instantânea, sem precisar atualizar a tela inteira.
Materiais Necessários
Para construir este projeto, você precisará dos seguintes itens:
- 1x Módulo WiFi NodeMCU ESP8266 v3 Lolin (ou placa de desenvolvimento ESP32 equivalente)
- 1x Cabo Micro-USB Azul
- 1x Chave de acesso (API Key) gratuita criada na plataforma do OpenWeatherMap
⚠️ Segurança de Credenciais O código-fonte do ESP32 conterá a senha da sua rede Wi-Fi e a chave de acesso da API. Lembre-se de nunca compartilhar esse arquivo publicamente (como no GitHub ou redes sociais) sem apagar esses dados primeiro. Mantenha suas credenciais sempre seguras.
Obtendo a API Key no OpenWeatherMap
Antes de programar o ESP32, precisamos obter a credencial de acesso que nos autoriza a consultar a previsão do tempo gratuitamente.
Acesse o site do OpenWeatherMap e clique no botão de criação de conta no canto superior direito:
https://openweathermap.org/

Crie a sua conta de desenvolvedor gratuita. Após realizar o login, navegue até a aba “API Keys”:

Na tela de chaves, você verá a sua chave exclusiva gerada. Copie esse código alfanumérico, pois usaremos ele mais tarde no código do Arduino:

Conhecendo as Bibliotecas do Projeto
Para fazer a mágica acontecer, utilizaremos cinco bibliotecas fundamentais que cuidam da rede, do servidor e da leitura dos dados:
- WiFi.h: Biblioteca nativa que gerencia a conexão física do ESP32 com o seu roteador Wi-Fi local.
- WebServer.h: Cria o servidor de internet no ESP32, habilitando-o a escutar requisições HTTP na porta 80 e responder com arquivos HTML.
- HTTPClient.h: Permite que o ESP32 funcione também como um cliente de rede, capaz de fazer consultas externas a servidores web na nuvem para buscar os dados de clima.
- ESPmDNS.h: Permite registrar um domínio amigável na rede, de forma que dispositivos locais encontrem o ESP32 sem precisar adivinhar o IP.
- ArduinoJson.h: Ferramenta essencial para manipulação e decodificação do formato JSON (formato de troca de dados padrão da web). Você pode consultar a documentação completa no site oficial da ArduinoJson.
Para instalar a biblioteca de processamento JSON, abra o Gerenciador de Bibliotecas na Arduino IDE e faça a instalação conforme a imagem abaixo, as demais já são padrão na IDE do ESP32:

Como Acessar o Site em Rede Local (mDNS)
Normalmente, para acessar um servidor web local, você precisaria abrir o Monitor Serial, ver qual endereço IP (como `192.168.1.15`) o roteador atribuiu ao ESP32 e digitá-lo no navegador.
Para facilitar o uso, este projeto implementa o mDNS (Multicast DNS). A biblioteca registra um domínio amigável na rede. Isso significa que, após ligar o ESP32, basta abrir o navegador de qualquer computador ou celular conectado no mesmo Wi-Fi e digitar na barra de endereços: http://clima.local/
O navegador resolverá o nome automaticamente e abrirá a página de controle!
A Lógica Interna: Circuit Breaker e Frontend
️ O Disjuntor de Segurança (Circuit Breaker)
Fazer requisições constantes a uma API externa em caso de erros pode acarretar no bloqueio da sua chave de acesso. Por isso, implementamos um sistema de proteção:
* O ESP32 monitora as falhas de comunicação com a API.
* Se ocorrerem 5 falhas seguidas (por falta de sinal de internet ou chave de API inválida), o sistema entra em estado suspenso (disjuntor desarmado).
* As atualizações automáticas são interrompidas para proteger o chip, exibindo um erro legível na página.
* O sistema só é rearmado se o usuário clicar ativamente em “Atualizar” no site, resetando o contador de falhas.
️ O Frontend Assíncrono (HTML, CSS e AJAX)
A página web servida pelo ESP32 utiliza:
* HTML: Estrutura os textos, caixas de dados e o botão da tela.
CSS: Aplica um visual elegante com cores gradientes e efeito de vidro fosco (Glassmorphism*).
* JavaScript (AJAX): Ao invés de recarregar o site inteiro a cada consulta (o que faria a tela piscar), uma função JavaScript chamada `fetchWeather()` faz uma requisição silenciosa de fundo no endereço `/api/weather` do ESP32, lê a resposta JSON e atualiza apenas os números de temperatura, vento e umidade instantaneamente na tela.
O Código Completo do ESP32
Substitua `”SUAREDEWIFI”`, `”SENHADAREDEWIFI”` e `”SUAAPI_KEY”` pelas credenciais da sua rede local e a chave obtida no site do OpenWeatherMap:
/**
* @brief Site consumindo API hospedado no ESP em rede local.
* @note Acesse pelo navegador local através do endereço: http://clima.local
* @author Abraão da Silva
*/
// Inclusão das bibliotecas necessárias para Wi-Fi, Servidor Web, Cliente HTTP, Manipulação de JSON e DNS local
#include <WiFi.h>
#include <WebServer.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <ESPmDNS.h>
// --- CONFIGURAÇÕES DE REDE WI-FI ---
// Placeholders para credenciais da rede Wi-Fi local que o leitor deve preencher
const char* ssid = "SUA_REDE_WIFI";
const char* password = "SENHA_DA_REDE_WIFI";
// --- CONFIGURAÇÕES DA API DE CLIMA ---
// Parâmetros para conexão com o serviço climátológico OpenWeatherMap
const String apiKey = "SUA_API_KEY";
const String city = "Sao%20Paulo";
const String cityDisplayName = "São Paulo";
const String countryCode = "BR";
// Endpoint completo configurado para retornar dados em português (pt_br) e graus Celsius (metric)
const String apiEndpoint = "http://api.openweathermap.org/data/2.5/weather?q=" + city + "," + countryCode + "&appid=" + apiKey + "&units=metric&lang=pt_br";
// Inicializa o servidor web escutando requisições na porta HTTP padrão (80)
WebServer server(80);
// Estrutura de dados para armazenar as variáveis de clima processadas
struct WeatherData {
float temperature;
float humidity;
String description;
float windSpeed;
bool isValid;
String errorMessage;
};
// Variável global contendo o estado atual do clima coletado da API
WeatherData currentWeatherData = {0.0, 0.0, "Sem dados", 0.0, false, "Aguardando primeira leitura..."};
// Controle de tempo para consultas à API de clima externo
unsigned long lastApiRequestTime = 0;
const unsigned long apiRequestInterval = 600000; // Intervalo de 10 minutos entre consultas automáticas (600.000 ms)
// Variáveis de controle para a proteção contra excesso de falhas (Circuit Breaker)
int consecutiveApiErrors = 0;
const int maxConsecutiveErrors = 5; // Limite de 5 erros seguidos antes de desarmar o disjuntor
bool apiSuspended = false; // Indica se as requisições automáticas estão suspensas
// Declaração dos protótipos de funções
void connectToWiFi();
void getWeatherDataFromApi();
void handleRoot();
void handleGetWeather();
void handleNotFound();
void setup() {
// Pequeno delay inicial para estabilização do hardware do ESP32
delay(1000);
// Conecta o ESP32 ao roteador Wi-Fi local configurado
connectToWiFi();
// Realiza a primeira consulta de clima imediatamente ao iniciar para popular a tela
getWeatherDataFromApi();
// Associa as rotas de URL do servidor aos seus respectivos manipuladores (handlers)
server.on("/", HTTP_GET, handleRoot); // Rota principal que serve a página do painel (HTML/CSS/JS)
server.on("/api/weather", HTTP_GET, handleGetWeather); // Rota de API local que fornece os dados em formato JSON
server.onNotFound(handleNotFound); // Rota de fallback caso o usuário acesse um endereço inexistente (404)
// Inicializa o servidor web local na porta 80
server.begin();
}
void loop() {
// Trata e atende clientes (navegadores) que realizarem requisições ao servidor do ESP32
server.handleClient();
// Realiza requisições automáticas periódicas (a cada 10 min) caso o Circuit Breaker esteja armado e o tempo tenha expirado
if (!apiSuspended && (millis() - lastApiRequestTime >= apiRequestInterval || lastApiRequestTime == 0)) {
getWeatherDataFromApi();
}
}
// Conecta o chip à rede Wi-Fi configurada
void connectToWiFi() {
// Inicializa a conexão física Wi-Fi
WiFi.begin(ssid, password);
// Tenta conectar por no máximo 15 segundos (30 tentativas de 500ms) para não travar a inicialização infinitamente
int timeoutCounter = 0;
while (WiFi.status() != WL_CONNECTED && timeoutCounter < 30) {
delay(500);
timeoutCounter++;
}
// Se a conexão Wi-Fi foi estabelecida com sucesso, inicializa o domínio mDNS local
if (WiFi.status() == WL_CONNECTED) {
MDNS.begin("clima"); // Permite acessar o site digitando http://clima.local/ no navegador
}
}
// Consome os dados climáticos da API OpenWeatherMap na nuvem
void getWeatherDataFromApi() {
// Atualiza o marcador temporal da última tentativa de consulta para evitar requisições repetidas em loop
lastApiRequestTime = millis();
// Se não houver conexão de rede ativa com o roteador, encerra a consulta e define estado inválido
if (WiFi.status() != WL_CONNECTED) {
currentWeatherData.isValid = false;
currentWeatherData.errorMessage = "Erro de Conexão: ESP32 sem sinal Wi-Fi.";
return;
}
// Se o disjuntor de segurança (Circuit Breaker) estiver desarmado/suspenso, bloqueia novas chamadas automáticas
if (apiSuspended) {
return;
}
// Inicializa a requisição HTTP cliente
HTTPClient http;
http.begin(apiEndpoint);
int httpResponseCode = http.GET(); // Executa o método GET no servidor da OpenWeatherMap
// Se o código de resposta for HTTP 200 (OK), lê o payload e realiza o parseamento do JSON retornado
if (httpResponseCode == HTTP_CODE_OK) {
String payload = http.getString();
StaticJsonDocument<1024> doc;
DeserializationError error = deserializeJson(doc, payload); // Converte o texto JSON em objeto
// Se o JSON foi parseado com sucesso, extrai e armazena os dados desejados
if (!error) {
currentWeatherData.temperature = doc["main"]["temp"];
currentWeatherData.humidity = doc["main"]["humidity"];
currentWeatherData.windSpeed = doc["wind"]["speed"];
const char* desc = doc["weather"][0]["description"];
currentWeatherData.description = String(desc);
currentWeatherData.isValid = true;
currentWeatherData.errorMessage = "";
// Converte a primeira letra da descrição de clima para maiúscula (ex: "nublado" para "Nublado")
if (currentWeatherData.description.length() > 0) {
currentWeatherData.description[0] = toupper(currentWeatherData.description[0]);
}
// Reseta o contador de erros consecutivos se obtivermos sucesso na chamada
consecutiveApiErrors = 0;
} else {
// Caso ocorra falha de parsing no JSON retornado
currentWeatherData.isValid = false;
currentWeatherData.errorMessage = "JSON inválido vindo da API climática.";
consecutiveApiErrors++;
}
} else {
// Caso de falha de conexão HTTP com a API externa (servidor indisponível ou erros 4xx/5xx)
currentWeatherData.isValid = false;
consecutiveApiErrors++;
// Trata códigos de erro comuns de API para exibir de forma legível e amigável na tela do usuário
if (httpResponseCode == 401) {
currentWeatherData.errorMessage = "Erro 401: Chave de API inválida ou não ativada.";
} else if (httpResponseCode == 404) {
currentWeatherData.errorMessage = "Erro 404: Cidade não encontrada.";
} else {
currentWeatherData.errorMessage = "Erro HTTP na API climática. Código: " + String(httpResponseCode);
}
}
// Finaliza a requisição cliente HTTP de forma limpa liberando a memória do socket
http.end();
// Implementação do disjuntor (Circuit Breaker): suspende chamadas após 5 erros seguidos
if (consecutiveApiErrors >= maxConsecutiveErrors) {
apiSuspended = true;
currentWeatherData.errorMessage += " [API Suspensa temporariamente por segurança. Use o botão Atualizar]";
}
}
// Handler da rota principal "/" que monta e envia o HTML, CSS e JavaScript do painel
void handleRoot() {
String html = "<!DOCTYPE html>\n";
html += "<html lang=\"pt-BR\">\n";
html += "<head>\n";
html += " <meta charset=\"UTF-8\">\n";
html += " <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n";
html += " <title>Painel de Clima - ESP32</title>\n";
// Estilização CSS integrada com efeitos de gradiente de fundo e visual de vidro translúcido (Glassmorphism)
html += " <style>\n";
html += " body { font-family: 'Segoe UI', sans-serif; background: linear-gradient(135deg, #1e3c72, #2a5298); color: #fff; margin: 0; display: flex; justify-content: center; align-items: center; min-height: 100vh; }\n";
html += " .card { background: rgba(255, 255, 255, 0.1); backdrop-filter: blur(10px); border-radius: 20px; padding: 30px; box-shadow: 0 8px 32px rgba(0,0,0,0.3); border: 1px solid rgba(255,255,255,0.2); width: 350px; text-align: center; }\n";
html += " .city { font-size: 16px; color: #a0c4ff; margin-bottom: 20px; text-transform: uppercase; letter-spacing: 1px; }\n";
html += " .temp-container { font-size: 64px; font-weight: 700; margin: 20px 0; }\n";
html += " .desc { font-size: 18px; font-style: italic; margin-bottom: 20px; color: #e2eafc; }\n";
html += " .error-box { background: rgba(239, 35, 60, 0.2); border: 1px solid #ef233c; border-radius: 8px; padding: 10px; margin: 15px 0; font-size: 13px; color: #ffb3c1; display: none; }\n";
html += " .details { display: flex; justify-content: space-between; border-top: 1px solid rgba(255,255,255,0.2); padding-top: 20px; }\n";
html += " .btn-update { background: #4cc9f0; border: none; color: #fff; padding: 10px 20px; border-radius: 50px; font-size: 14px; font-weight: 600; cursor: pointer; margin-top: 25px; transition: 0.3s; }\n";
html += " .btn-update:hover { background: #4361ee; }\n";
html += " </style>\n";
html += "</head>\n";
html += "<body>\n";
// Estrutura visível dos elementos da tela: título, caixa de erros oculta por padrão, dados climáticos e botão
html += " <div class=\"card\">\n";
html += " <h1>Tempo Atual</h1>\n";
html += " <div class=\"city\">" + cityDisplayName + "</div>\n";
html += " <div id=\"err\" class=\"error-box\"></div>\n";
html += " <div class=\"temp-container\"><span id=\"temp\">--</span>°C</div>\n";
html += " <div class=\"desc\" id=\"desc\">Aguardando dados...</div>\n";
html += " <div class=\"details\">\n";
html += " <div style=\"flex: 1;\"><div>Umidade</div><div id=\"humidity\" style=\"font-size: 18px; font-weight: 600;\">--%</div></div>\n";
html += " <div style=\"flex: 1; border-left: 1px solid rgba(255,255,255,0.1);\"><div>Vento</div><div id=\"wind\" style=\"font-size: 18px; font-weight: 600;\">-- m/s</div></div>\n";
html += " </div>\n";
html += " <button class=\"btn-update\" onclick=\"fetchWeather()\">Atualizar</button>\n";
html += " </div>\n";
// Script JavaScript embarcado que faz chamadas AJAX dinâmicas no ESP32 para atualizar a tela sem recarregar a página inteira
html += " <script>\n";
html += " function fetchWeather() {\n";
html += " document.getElementById('desc').innerText = 'Atualizando...';\n";
html += " document.getElementById('err').style.display = 'none';\n";
// Realiza requisição AJAX assíncrona usando a Fetch API do navegador no endpoint local /api/weather
html += " fetch('/api/weather?force=true')\n";
html += " .then(res => res.json())\n";
html += " .then(data => {\n";
html += " if (data.isValid) {\n";
// Atualiza os elementos HTML do DOM com os dados climáticos reais retornados no JSON do ESP32
html += " document.getElementById('temp').innerText = data.temperature.toFixed(1);\n";
html += " document.getElementById('humidity').innerText = data.humidity.toFixed(0) + '%';\n";
html += " document.getElementById('wind').innerText = data.windSpeed.toFixed(1) + ' m/s';\n";
html += " document.getElementById('desc').innerText = data.description;\n";
html += " } else {\n";
// Exibe a div de erro com a mensagem amigável de erro fornecida pelo firmware
html += " document.getElementById('desc').innerText = 'Dados Desatualizados';\n";
html += " const errDiv = document.getElementById('err');\n";
html += " errDiv.innerText = data.errorMessage;\n";
html += " errDiv.style.display = 'block';\n";
html += " }\n";
html += " })\n";
html += " .catch(err => {\n";
// Tratamento de falhas caso ocorra erro físico de conexão entre o navegador do usuário e o servidor local do ESP32
html += " document.getElementById('desc').innerText = 'Erro de comunicação';\n";
html += " });\n";
html += " }\n";
html += " window.onload = fetchWeather;\n"; // Executa a primeira chamada AJAX imediatamente ao carregar a página
html += " </script>\n";
html += "</body>\n";
html += "</html>\n";
// Envia a página HTML completa de volta ao navegador do cliente com cabeçalho text/html e código HTTP 200 (OK)
server.send(200, "text/html", html);
}
// Handler da rota secundária "/api/weather" que retorna os dados atuais estruturados em formato JSON
void handleGetWeather() {
// Se o parâmetro 'force=true' for recebido na URL, ignora as travas de tempo e faz uma requisição manual imediata à API externa
if (server.hasArg("force") && server.arg("force") == "true") {
apiSuspended = false;
consecutiveApiErrors = 0;
getWeatherDataFromApi();
}
// Cria, formata e serializa os dados climáticos estruturados para representação em string JSON
StaticJsonDocument<512> doc;
doc["temperature"] = currentWeatherData.temperature;
doc["humidity"] = currentWeatherData.humidity;
doc["windSpeed"] = currentWeatherData.windSpeed;
doc["description"] = currentWeatherData.description;
doc["isValid"] = currentWeatherData.isValid;
doc["errorMessage"] = currentWeatherData.errorMessage;
String jsonResponse;
serializeJson(doc, jsonResponse);
// Habilita cabeçalhos CORS básicos para garantir acesso livre por scripts em domínios locais
server.sendHeader("Access-Control-Allow-Origin", "*");
// Envia a string JSON serializada com o tipo content-type apropriado 'application/json'
server.send(200, "application/json", jsonResponse);
}
// Handler para erros de endereço inexistente (fallback 404)
void handleNotFound() {
// Retorna uma resposta simples com cabeçalho text/plain informando o erro 404 ao navegador
server.send(404, "text/plain", "Erro 404: Rota não encontrada no ESP32.");
}
Conclusão e Desafio
Subir um servidor web local no ESP32 abre as portas para inúmeros projetos de domótica e dashboards interativos. A integração com APIs externas permite que seu hardware reaja ou exiba qualquer informação disponível no mundo digital.
Seu Desafio: Modifique o código para exibir também a sensação térmica (`main.feels_like` no JSON retornado pela API). Tente adaptar o código de extração no Arduino e adicione um novo campo no layout HTML!
Demonstração do Projeto em Funcionamento
Assista ao vídeo abaixo mostrando o painel climático carregando a página web local via AJAX e exibindo os dados meteorológicos dinâmicos diretamente do ESP32:




