Generic selectors
Exact matches only
Search in title
Search in content
Post Type Selectors
/
/
Criando uma página web para ESP32

Criando uma página web para ESP32





home page do site OpenWeatherMap com seta para o botão de criação de conta.
depois de logado, page principal com seta para o campo
depois de clicar nas keys, painel mostrando a key da API que o user deve copiar.

como install biblioteca ArduinoJson, imagem da lib no campo de busca das LIbs na IDE do arduino



/**
 * @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.");
}


Site rodando no navegador com dados da API OpenWeatherMap em tempo real.

Introdução

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


Obtendo a API Key no OpenWeatherMap

home page do site OpenWeatherMap com seta para o botão de criação de conta.
depois de logado, page principal com seta para o campo
depois de clicar nas keys, painel mostrando a key da API que o user deve copiar.

Conhecendo as Bibliotecas do Projeto

como install biblioteca ArduinoJson, imagem da lib no campo de busca das LIbs na IDE do arduino

Como Acessar o Site em Rede Local (mDNS)

A Lógica Interna: Circuit Breaker e Frontend


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

Demonstração do projeto em funcionamento

Site rodando no navegador com dados da API OpenWeatherMap em tempo real.

Sobre o autor


Abraão da Silva

Estudante de Engenharia da Computação, especializado em curiosidades aparentemente aleatórias e desenvolvimento de software. Se eu não estiver pedalando agora, estou estudando ou tentando aproveitar a energia dos raios.

Neste tutorial, você aprenderá como subir um site (servidor web) no ESP32 acessível de forma local. Para deixar nossa página dinâmica e interessante, utilizaremos como exemplo prático o consumo de dados climáticos em tempo real de uma API externa gratuita (OpenWeatherMap).

Precisa dos componentes para este projeto?

Encontre tudo na Loja Eletrogate com frete grátis para compras acima de R$ 200