Generic selectors
Exact matches only
Search in title
Search in content
Post Type Selectors
/
/
Painel IoT – Clima, Moedas e Criptomoedas

Painel IoT – Clima, Moedas e Criptomoedas

Sumário

Baixe agora a apostila Arduino Iniciante e comece seus projetos

Compartilhe seu conhecimento e receba em dinheiro ou produtos.

Introdução

A Internet das Coisas (IoT) permite desenvolver aplicações que vão além da aquisição de dados provenientes de sensores conectados diretamente a um microcontrolador. Dispositivos como o ESP32 também podem utilizar sua conectividade Wi-Fi para acessar informações disponíveis na Internet, processá-las e apresentá-las ao usuário por meio de interfaces Web.

Neste projeto será desenvolvido um Painel IoT de Clima, Moedas e Criptomoedas, utilizando o ESP32 como elemento central da aplicação. O dispositivo conecta-se à Internet, consulta diferentes serviços Web e disponibiliza as informações obtidas em um painel acessível diretamente pelo navegador de computadores, tablets ou smartphones.

A interface foi organizada em três grupos principais de informações. O primeiro apresenta dados climáticos, como temperatura, umidade relativa do ar e velocidade do vento, associados à localização configurada no equipamento. O segundo apresenta cotações de moedas internacionais em relação ao Real brasileiro, enquanto o terceiro permite acompanhar preços de criptomoedas e suas respectivas variações percentuais nas últimas 24 horas.

Além da consulta e apresentação dessas informações, o projeto explora diferentes recursos importantes no desenvolvimento de aplicações conectadas com o ESP32, como configuração da rede Wi-Fi, armazenamento de parâmetros, sincronização de data e hora pela Internet, identificação do dispositivo na rede local e atualização remota do firmware.

Toda a interface do painel é fornecida pelo próprio ESP32 por meio de um servidor Web. Dessa forma, não é necessária a instalação de um aplicativo específico para sua utilização: estando o dispositivo conectado à rede, o usuário poderá acessar o painel utilizando um navegador Web.

Ao longo deste projeto serão apresentados a arquitetura da aplicação, as fontes de dados utilizadas, a configuração do ESP32, a organização do código-fonte e o funcionamento dos três painéis, permitindo compreender como diferentes informações disponíveis na Internet podem ser integradas em uma única aplicação IoT.

Motivação

Grande parte dos projetos desenvolvidos com o ESP32 utiliza sua conectividade Wi-Fi para permitir o acesso remoto a sensores, atuadores ou outros dispositivos conectados diretamente às suas portas. Entretanto, essa mesma conectividade também permite utilizar o microcontrolador como cliente de serviços disponíveis na Internet, ampliando significativamente as possibilidades de aplicações IoT.

A proposta deste projeto surgiu justamente da ideia de explorar essa característica, reunindo em um único painel informações provenientes de diferentes fontes. Foram escolhidos três grupos de dados de consulta frequente: condições climáticas, cotações de moedas internacionais e preços de criptomoedas.

Embora sejam informações de naturezas distintas, todas podem ser obtidas por meio de serviços disponíveis na Internet e tratadas pelo ESP32 antes de serem apresentadas ao usuário. Essa combinação torna o projeto particularmente interessante do ponto de vista didático, pois permite demonstrar como um dispositivo IoT pode realizar requisições a diferentes serviços Web, interpretar os dados recebidos e disponibilizar os resultados de forma organizada em uma interface própria.

Outro aspecto considerado foi a facilidade de utilização. Em vez de depender da instalação de um aplicativo específico, o painel utiliza uma interface Web responsiva, que pode ser acessada por computadores, tablets ou smartphones conectados à mesma rede. O próprio ESP32 atua como servidor Web, concentrando a comunicação com os serviços externos e o fornecimento da interface utilizada para a visualização das informações.

O projeto também permite explorar recursos que podem ser reaproveitados em diversas outras aplicações baseadas no ESP32, como configuração da conexão Wi-Fi, armazenamento persistente de parâmetros, sincronização de data e hora, identificação do equipamento na rede local e atualização remota do firmware.

Dessa forma, além de resultar em um painel útil para consulta de informações, o projeto serve como exemplo de integração entre ESP32, serviços Web e interfaces de usuário, formando uma base que pode ser adaptada posteriormente para outros tipos de dados, APIs e aplicações conectadas.

Materiais Necessários

Qualquer modelo de ESP32 pode ser utilizado no projeto. Sugerimos alguns modelos a seguir:

  1. Módulo WiFi ESP32 DevKitC S1 ou
  2. Módulo WiFi ESP32 DevKitC S1 - Modelo 2 ou
  3. Módulo WiFi ESP32 Bluetooth 30 pinos ou
  4. ESP32-C3 Mini - Tipo-C

Visão Geral do Projeto

O Painel IoT de Clima, Moedas e Criptomoedas tem como elemento central um módulo ESP32 conectado a uma rede Wi-Fi com acesso à Internet. A partir dessa conexão, o microcontrolador realiza consultas periódicas a diferentes serviços Web para obter as informações apresentadas nos três painéis da aplicação.

Os dados climáticos são obtidos a partir da localização configurada no equipamento. Com base nessas coordenadas, o ESP32 consulta o serviço responsável pelas informações meteorológicas e obtém os dados necessários para compor o painel de Clima. De maneira semelhante, serviços específicos são consultados para obter as cotações das moedas internacionais e os valores das criptomoedas acompanhadas pela aplicação.

As informações recebidas desses serviços são processadas pelo ESP32 e disponibilizadas por meio de um servidor Web executado no próprio microcontrolador. Quando o usuário acessa o endereço do equipamento utilizando um navegador, o ESP32 fornece a página que compõe a interface principal da aplicação.

A página utiliza requisições periódicas ao servidor Web do ESP32 para atualizar as informações apresentadas na tela. Para organizar essa comunicação, o firmware disponibiliza endpoints específicos para cada grupo de informações: dados climáticos, moedas e criptomoedas. Dessa forma, a interface pode solicitar somente os dados necessários para atualizar cada painel, sem que seja preciso recarregar toda a página.

Além da função principal de consulta e apresentação dos dados, o firmware incorpora recursos auxiliares importantes para a operação do equipamento. Entre eles estão a configuração da rede Wi-Fi, o armazenamento dos parâmetros de funcionamento, a sincronização de data e hora pela Internet, a identificação do dispositivo na rede local por meio de mDNS e a possibilidade de atualização do firmware através da rede.

Essa arquitetura separa conceitualmente a aplicação em três partes: os serviços externos responsáveis pelo fornecimento das informações, o ESP32 responsável pela comunicação, processamento e disponibilização dos dados e o navegador Web responsável pela apresentação e interação com o usuário. Essa divisão facilita a compreensão do funcionamento do projeto e permite que novas fontes de dados ou novos painéis sejam adicionados futuramente.

Figura 1 - Visão Geral do Projeto

Fontes de Dados e Indicadores

As informações apresentadas pelo painel não são geradas localmente pelo ESP32. O microcontrolador utiliza sua conexão com a Internet para realizar requisições HTTP a diferentes serviços Web, recebendo como resposta documentos no formato JSON (JavaScript Object Notation).

O formato JSON é bastante utilizado na comunicação entre aplicações e serviços Web por representar informações através de uma estrutura de pares formados por nomes e valores. No projeto, as respostas recebidas são interpretadas pelo ESP32 com o auxílio da biblioteca ArduinoJson, permitindo selecionar apenas os campos necessários para cada painel.

A aplicação utiliza três fontes principais de informações: Open-Meteo para os dados climáticos, ExchangeRate-API para as cotações das moedas e CoinGecko para os preços e variações das criptomoedas. A seguir serão apresentadas as requisições realizadas pelo ESP32 e os principais elementos das respostas fornecidas por cada serviço.

Dados Climáticos — Open-Meteo

Os dados apresentados no painel de Clima são obtidos através da API Open-Meteo. A requisição é construída dinamicamente pelo firmware utilizando os valores de latitude e longitude armazenados na configuração do equipamento.

Considerando uma latitude e uma longitude representadas abaixo por LATITUDE e LONGITUDE, a requisição realizada pelo ESP32 possui o seguinte formato:

https://api.open-meteo.com/v1/forecast?latitude=LATITUDE&longitude=LONGITUDE&current=temperature_2m,relative_humidity_2m,wind_speed_10m&timezone=auto

Os parâmetros informam ao serviço a posição geográfica desejada e solicitam, através do parâmetro current, a temperatura a 2 metros, a umidade relativa do ar a 2 metros e a velocidade do vento a 10 metros. O parâmetro timezone=auto solicita que o serviço determine automaticamente o fuso horário correspondente às coordenadas fornecidas.

A resposta fornecida pelo Open-Meteo contém diversas informações. De forma simplificada, considerando apenas os elementos utilizados pela aplicação, sua estrutura pode ser representada pelo seguinte JSON:

{ "latitude": ..., "longitude": ..., "elevation": ..., "current": { "time": "...", "interval": 900, "temperature_2m": ..., "relative_humidity_2m": ..., "wind_speed_10m": ... } }

Após receber a resposta, o firmware interpreta o objeto current e armazena os valores de temperature_2m, relative_humidity_2m e wind_speed_10m. Também são aproveitados a elevação informada pelo serviço, o horário correspondente à leitura e o intervalo de atualização.

Essas informações são posteriormente disponibilizadas pelo servidor Web interno através do endpoint /api/weather, juntamente com as coordenadas configuradas e o horário atual do ESP32. Dessa maneira, a página Web não precisa acessar diretamente o Open-Meteo: ela solicita os dados ao próprio ESP32.

Cotações de Moedas — ExchangeRate-API

Para obter as cotações das moedas, o firmware utiliza como referência o Real brasileiro (BRL) e realiza a seguinte requisição:

https://open.er-api.com/v6/latest/BRL

A resposta contém, entre outras informações, um objeto denominado rates, no qual são fornecidas as taxas das diferentes moedas em relação à moeda-base. Uma representação simplificada da parte utilizada pelo projeto é mostrada a seguir:

{ "base_code": "BRL", "rates": { "USD": ..., "EUR": ..., "GBP": ..., "JPY": ..., "CNY": ..., "RUB": ..., "ARS": ... } }

Como a moeda-base da consulta é o BRL, as taxas retornadas representam quantas unidades de cada moeda correspondem a um Real. Entretanto, no painel desejamos apresentar a relação inversa, ou seja, quantos Reais correspondem a uma unidade da moeda estrangeira. Por esse motivo, o firmware calcula o inverso de cada taxa recebida.

Assim, se a taxa USD retornada pelo serviço for representada por rates["USD"], o valor apresentado pelo projeto é calculado através da relação 1 / rates["USD"]. O mesmo procedimento é aplicado ao Euro, Libra esterlina, Iene japonês, Yuan chinês, Rublo russo e Peso argentino.

Os valores já convertidos para o formato utilizado pelo painel são disponibilizados pelo endpoint /api/forex, evitando que o navegador precise conhecer a estrutura original da resposta fornecida pelo serviço externo.

Criptomoedas — CoinGecko

As informações referentes às criptomoedas são obtidas através da API CoinGecko. O firmware solicita simultaneamente os nove ativos utilizados pelo painel, seus preços em dólares americanos e suas respectivas variações percentuais nas últimas 24 horas.

A requisição utilizada no projeto é:

https://api.coingecko.com/api/v3/simple/price?ids=bitcoin,ethereum,binancecoin,solana,ripple,cardano,tron,dogecoin,shiba-inu&vs_currencies=usd&include_24hr_change=true

A resposta é organizada por identificador da criptomoeda. De maneira simplificada, a estrutura recebida para cada ativo apresenta o seguinte formato:

{ "bitcoin": { "usd": ..., "usd_24h_change": ... }, "ethereum": { "usd": ..., "usd_24h_change": ... } }

A mesma estrutura é retornada para BNB, Solana, XRP, Cardano, TRON, Dogecoin e Shiba Inu. O campo usd contém o preço atual em dólares, enquanto usd_24h_change contém a variação percentual correspondente às últimas 24 horas.

Neste ponto existe uma integração interessante entre duas das fontes de dados utilizadas pelo projeto. Como o CoinGecko é consultado em dólares, o firmware utiliza a cotação USD/BRL anteriormente obtida pelo serviço de moedas para converter cada preço para Reais. Por esse motivo, a atualização das criptomoedas depende da existência de uma cotação USD/BRL válida.

Para cada criptomoeda, o ESP32 monta então uma nova estrutura contendo o nome do ativo, o preço convertido para BRL e a variação percentual em 24 horas. Essa estrutura é armazenada internamente e posteriormente fornecida ao navegador através do endpoint /api/crypto.

Observação sobre o acesso à API: neste projeto foi utilizada a API pública da CoinGecko sem a necessidade de uma chave de acesso (API Key). Essa opção simplifica a reprodução do projeto, pois permite que o leitor compile e execute o firmware sem precisar criar previamente uma conta ou cadastrar uma chave individual.

Entretanto, o acesso público sem chave está sujeito a limites de utilização definidos pela CoinGecko e pode sofrer restrições de acordo com as condições de uso do serviço. Dessa forma, eventualmente uma requisição poderá ser temporariamente recusada mesmo que não exista qualquer problema com a conexão Wi-Fi ou com o firmware do ESP32.

Para aplicações que necessitem de condições de acesso mais previsíveis, a CoinGecko também disponibiliza gratuitamente o Demo Plan. Para utilizá-lo é necessário criar uma conta, gerar uma Demo API Key e incluí-la nas requisições realizadas ao serviço. No momento da elaboração deste projeto, o plano Demo gratuito disponibilizava até 10.000 créditos de chamadas por mês e possuía também um limite para a quantidade de requisições realizadas por minuto. Como esses limites e condições podem ser alterados pelo provedor, recomenda-se consultar a documentação oficial da CoinGecko antes da utilização.

A criação da conta Demo e a geração da chave podem ser realizadas a partir da página oficial da API da CoinGecko:

CoinGecko API — Planos e criação da Demo API Key

Caso seja utilizada uma Demo API Key, a chave deve ser tratada como uma credencial pessoal e não deve ser publicada juntamente com o código-fonte. Para uma evolução deste projeto, a chave poderia, por exemplo, ser acrescentada aos parâmetros de configuração do equipamento e armazenada no ESP32, evitando que permaneça gravada diretamente no código publicado.

Das APIs Externas para a Interface Web

A interface Web não realiza diretamente as consultas ao Open-Meteo, ExchangeRate-API ou CoinGecko. Essa responsabilidade pertence ao firmware do ESP32. O navegador comunica-se somente com o servidor Web executado no próprio dispositivo através dos endpoints /api/weather, /api/forex e /api/crypto.

Essa organização cria uma separação entre as fontes externas de dados e a interface apresentada ao usuário. O ESP32 atua como intermediário: consulta os serviços externos, interpreta suas respostas, realiza os cálculos necessários e fornece ao navegador estruturas JSON já adequadas à apresentação nos respectivos painéis.

No navegador, um código JavaScript consulta periodicamente esses três endpoints e atualiza os elementos da página sem que seja necessário recarregá-la completamente. Na versão desenvolvida neste projeto, essa atualização da interface ocorre a cada 60 segundos.

Conhecendo a Aplicação

A interação com o Painel IoT é realizada inteiramente através de um navegador Web. Depois que o ESP32 está conectado à rede Wi-Fi, computadores, tablets ou smartphones podem acessar a interface fornecida pelo servidor Web executado no próprio microcontrolador.

A interface principal pode ser acessada através do endereço http://forecast.local ou diretamente pelo endereço IP atribuído ao ESP32, utilizando o formato http://<ip>. O nome forecast utilizado no primeiro endereço corresponde ao nome mDNS definido na configuração do equipamento. Dessa forma, caso esse parâmetro seja alterado, o endereço de acesso também será modificado para http://<nome-mDNS>.local. O acesso pelo endereço IP permanece disponível como alternativa, utilizando o endereço recebido pelo ESP32 na rede Wi-Fi.

A tela principal foi organizada em três abas — Clima, Moedas e Cripto — permitindo reunir diferentes tipos de informações em uma única aplicação sem sobrecarregar a visualização. Além da consulta aos dados, o equipamento dispõe de uma interface de configuração e de atualização remota do firmware.

Painel de Clima

Figura 2 - Painel de Clima

A aba Clima apresenta as informações meteorológicas correspondentes às coordenadas configuradas no equipamento. Os dados são organizados em cartões individuais, facilitando a leitura tanto em computadores quanto em dispositivos móveis.

São apresentados a temperatura atual, a umidade relativa do ar e a velocidade do vento. O painel também informa a latitude e a longitude utilizadas na consulta, além da elevação correspondente à localização.

Outras informações auxiliam no acompanhamento da atualização dos dados: o horário associado à informação fornecida pelo Open-Meteo, o intervalo informado pelo serviço e o horário atual do próprio ESP32.

A presença dessas informações permite não apenas consultar as condições meteorológicas, mas também verificar facilmente a localização utilizada e a referência temporal dos dados apresentados.

Painel de Moedas

Figura 3 - Painel de Moedas

A segunda aba apresenta as cotações de moedas internacionais em Reais. A disposição em cartões segue o mesmo padrão visual utilizado no painel climático, mantendo uma interface simples e uniforme.

Nesta versão da aplicação são apresentadas as cotações do Dólar americano (USD), Euro (EUR), Libra esterlina (GBP), Iene japonês (JPY), Yuan chinês (CNY), Rublo russo (RUB) e Peso argentino (ARS).

Os valores exibidos correspondem ao valor, em Reais, de uma unidade de cada moeda estrangeira. Conforme apresentado anteriormente, essas informações são obtidas pelo ESP32 através do serviço de cotações e processadas antes de serem disponibilizadas para a interface Web.

Painel de Criptomoedas

Figura 4 - Painel de Criptomoedas

A aba Cripto utiliza uma tabela para apresentar os ativos acompanhados pela aplicação. Para cada criptomoeda são mostrados seu nome, o preço convertido para Reais e a variação percentual registrada nas últimas 24 horas.

A aplicação acompanha Bitcoin (BTC), Ethereum (ETH), BNB, Solana (SOL), XRP, Cardano (ADA), TRON (TRX), Dogecoin (DOGE) e Shiba Inu (SHIB), permitindo visualizar em uma única tela ativos com diferentes faixas de preço.

Para facilitar a interpretação da variação diária, a interface utiliza uma indicação visual: valores positivos são apresentados em verde e valores negativos em vermelho. Os preços também recebem tratamento de formatação, permitindo apresentar maior quantidade de casas decimais para criptomoedas cujo valor unitário é muito pequeno.

Configuração do Equipamento

A configuração do equipamento é realizada através de uma interface Web, evitando a necessidade de alterar e recompilar o firmware sempre que determinados parâmetros de funcionamento precisarem ser modificados.

A interface de configuração é baseada no WiFiManager e permite definir a rede Wi-Fi utilizada pelo ESP32, além dos parâmetros específicos necessários ao funcionamento da aplicação. As configurações informadas pelo usuário são armazenadas na memória do dispositivo e recuperadas posteriormente durante sua inicialização.

Na primeira execução o portal entrará automaticamente para a definição dos parâmetros. Depois de feita a primeira parametrização, o painel do WifiManager pode ser acessado pressionando-se o botão de BOOT do ESP32. Desta forma o portal ficará ativo no modo AP.

Uma vez no modo AP, o usuário pode procurar o SSID ESP32-Setup (configuração inicial), conforme a figura a seguir, utilizando a senha inicial "12345678":

Figura 5 - Seleção do Modo AP do WifiManeger para Configuração

Na primeira vez, após a seleção do SSID, uma tela e confirmação aparecerá:

Figura 6 - Confirmação de Entrada no WifiManager

Após a confirmação, o modo AP ficará conectado conforme figura a seguir:

Figura 7 - Conectado no Modo AP

Através do Navegador, a tela principal do WifiManager pode ser acessada através do endereço http://192.168.4.1

O portal de configuração também recebeu uma personalização visual em tema escuro, mantendo uma aparência mais próxima da utilizada no painel principal da aplicação. Caso o leitor se interesse em saber como se faz a personalização do portal do WifiManager, o POST a seguir demonstra os procedimentos:

https://blog.eletrogate.com/biblioteca-wifimanager-recursos-avancados/

Figura 8 - Tela do WifiManager para Configuração

Pressionando-se o botão Configure Wifi, a tela de parâmetros será mostrada para a preenchimento dos dados:

Figura  9 - Tela de Parâmetros

A página de configuração concentra os principais parâmetros necessários para adaptar o equipamento ao ambiente em que será instalado. Os campos SSIDWifi e Password correspondem, respectivamente, ao nome e à senha da rede Wi-Fi utilizada pelo ESP32. O parâmetro AliasName, inicialmente definido como forecast, estabelece o nome mDNS do dispositivo e permite, por exemplo, acessar a aplicação através do endereço http://forecast.local. Já SSIDAP e PassAP definem o nome e a senha da rede criada pelo próprio ESP32 quando é necessário utilizar o portal de configuração, sendo utilizados inicialmente os valores ESP32-Setup e 12345678.

Os parâmetros UserOTA e PassOTA definem as credenciais utilizadas para acesso ao recurso de atualização remota do firmware, tendo admin como valor inicial para ambos. Para a sincronização do relógio do ESP32 através da Internet são utilizados os campos NTP e TimeZone. Na configuração inicial do projeto são utilizados o servidor pool.ntp.br e o fuso horário -3, correspondente ao horário de Brasília.

Os campos Latitude e Longitude determinam a localização utilizada nas consultas ao serviço de informações meteorológicas e devem ser preenchidos pelo usuário com as coordenadas geográficas do local desejado. Essas coordenadas não são determinadas automaticamente pelo equipamento e podem ser obtidas, por exemplo, através do Google Maps: ao selecionar um ponto no mapa, suas coordenadas de latitude e longitude podem ser consultadas e posteriormente informadas nesses campos. Outros serviços de mapas ou ferramentas de consulta de coordenadas geográficas também podem ser utilizados.

Finalmente, o parâmetro Rebootar Após Atualização controla o comportamento do equipamento após uma atualização OTA. Quando configurado com o valor 1, o ESP32 é reiniciado após a conclusão da atualização, permitindo que a nova versão do firmware entre em execução.

Depois do preenchimento, o botão SAVE deve ser pressionado e, se a credencial da Rede Wifi estiver correta, o programa entrará no modo de operação normal e poderá ser acessado com o Navegador.

Atualização do Firmware via OTA

Depois que o equipamento está instalado e conectado à rede, novas versões do firmware podem ser transferidas para o ESP32 sem que seja necessário conectá-lo novamente ao computador através da porta USB. Para isso, o projeto incorpora o recurso de atualização OTA (Over-the-Air).

Através da interface fornecida pelo AsyncElegantOTA, o arquivo contendo uma nova versão do firmware pode ser selecionado no computador e enviado ao ESP32 utilizando a própria conexão de rede. para acessar a página da atualização, utilize o endereço a seguir e informe a credencial/senha para autenticação:

http://forecast.local/update

Figura 10 - Autenticação para Atualização do Firmware

Figura 11 - Tela da Atualização AsyncOTA

Figura 12 - Seleção da Imagem a ser carregada no Firmware

Para gerar a imagem a ser atualizada, o Arduino IDE pode ser utilizado conforme figura a seguir:

Figura 13 - Gerando a Imagem no Arduino IDE

Durante o processo, a própria página permite acompanhar o envio da nova versão. Esse recurso facilita consideravelmente a manutenção do projeto, principalmente quando o equipamento já se encontra instalado em seu local definitivo.

Sugerimos um REBOOT manual logo após a atualização.

Código-Fonte

Para facilitar a organização e a manutenção do projeto, o código-fonte foi dividido em três arquivos. O arquivo principal, sketch_esp32_clima_moedas_cryptos_post.ino, contém o firmware executado pelo ESP32, incluindo a inicialização do equipamento, configuração da rede, acesso aos serviços externos, tratamento das informações recebidas e implementação do servidor Web.

A página principal da aplicação foi separada no arquivo html_main.h. Nele estão armazenados o código HTML responsável pela estrutura da interface, os estilos utilizados na apresentação dos três painéis e o código JavaScript responsável pela comunicação com os endpoints disponibilizados pelo ESP32 e pela atualização das informações exibidas no navegador.

O terceiro arquivo, wm_css.h, contém as definições CSS utilizadas para personalizar a aparência das páginas fornecidas pelo WiFiManager. Essa separação evita concentrar grandes blocos de HTML e CSS dentro do arquivo principal e torna o projeto mais organizado e simples de consultar.

Os três arquivos devem permanecer na mesma pasta do projeto e são apresentados como abas independentes no Arduino IDE. Para criar um novo arquivo, pode-se utilizar o menu disponível ao lado das abas do editor, selecionar a opção correspondente à criação de uma nova aba e informar o nome completo do arquivo, incluindo sua extensão. A figura a seguir ilustra esse procedimento.

Figura 14 - Criação das abas html_main.h e wm_css.h no Arduino IDE

Ao final, o projeto deverá apresentar no Arduino IDE as três abas correspondentes aos arquivos utilizados pela aplicação, conforme a estrutura mostrada a seguir:

sketch_esp32_clima_moedas_cryptos_post/ ├── sketch_esp32_clima_moedas_cryptos_post.ino ├── html_main.h └── wm_css.h Bibliotecas Utilizadas

O firmware utiliza algumas bibliotecas fornecidas pelo próprio suporte do ESP32 para Arduino e outras bibliotecas adicionais responsáveis por funções específicas da aplicação. Antes da compilação do projeto, é importante verificar se as dependências externas utilizadas pelo código estão instaladas no ambiente de desenvolvimento.

Entre os recursos utilizados estão as bibliotecas responsáveis pela conexão Wi-Fi, comunicação através de requisições HTTP, armazenamento de arquivos no sistema LittleFS, sincronização de data e hora e identificação do equipamento na rede local através de mDNS. Esses recursos formam a infraestrutura básica utilizada pelo ESP32 para acessar a Internet, armazenar suas configurações e disponibilizar a aplicação na rede local.

O projeto utiliza também a biblioteca WiFiManager, responsável pelo portal de configuração da rede Wi-Fi e dos parâmetros adicionais da aplicação. Sua utilização permite configurar o equipamento através do navegador, sem que as credenciais da rede precisem permanecer fixas no código-fonte.

Para interpretar as respostas no formato JSON recebidas dos serviços Open-Meteo, ExchangeRate-API e CoinGecko é utilizada a biblioteca ArduinoJson. Ela também auxilia na geração das estruturas JSON fornecidas pelos endpoints internos da aplicação para a página Web.

O servidor Web utilizado pela aplicação é baseado na arquitetura assíncrona do ESP32. Sobre essa infraestrutura também é disponibilizado o recurso AsyncElegantOTA, responsável pela atualização do firmware através da rede, dispensando a conexão física do equipamento ao computador sempre que uma nova versão precisar ser instalada.

De forma resumida, as principais bibliotecas e recursos utilizados pelo projeto são:

WiFi — conexão do ESP32 à rede sem fio;
HTTPClient — realização das requisições HTTP aos serviços externos;
LittleFS — armazenamento persistente dos parâmetros de configuração;
ESPmDNS — disponibilização do acesso através do nome `forecast.local`;
WiFiManager — portal de configuração da rede e dos parâmetros da aplicação;
ArduinoJson — leitura, tratamento e geração das estruturas JSON;
ESPAsyncWebServer — implementação do servidor Web assíncrono;
AsyncElegantOTA — atualização remota do firmware pela rede.

As bibliotecas que fazem parte do próprio suporte do ESP32 são disponibilizadas juntamente com o pacote da placa instalado no Arduino IDE. As bibliotecas adicionais podem ser instaladas através do Gerenciador de Bibliotecas do Arduino IDE ou conforme as instruções disponibilizadas pelos respectivos projetos.

Nas subseções seguintes são apresentados integralmente os três arquivos que compõem o código-fonte do projeto.

Firmware Principal — sketch_esp32_clima_moedas_cryptos_post.ino

//-------------------------------------------------------------------------------------------------
// Função   : Este programa tem como objetivo implementar a atualização de três paineis num
//            html a partir das coordenadas Latitude/Longitude com os seguintes dados:
//            1) Clima : Temperatura, Umidade, Vento, Elevação,
//            2) Moedas : USD, EUR, GBP, JPY, CNY, RUB e ARS
//            3) Cripto : Bitcoin, Ethereum, BNB, Solana, XRP, Cardano, TRON, Dogecoin e Shiba Inu
//
// Autor    : Dailton de Oliveira Menezes
//
// Versão   : 1.0 Abr/2026
//
// Observações:
//
//    1) A aplicação foi desenvolvida para a família ESP32. As definições a seguir devem ser 
//       ajustadas de acordo com o modelo do ESP32:
//       BOARD               : ESP32 Dev Module, ESP32C3 Dev Module ou outras placas
//       #define LED_BUILTIN : deve ser ajustado para o pino do LED interno
//       #define LED_ON      : deve ser ajustado para HIGH ou LOW 
//       #define LED_OFF     : deve ser ajustado para HIGH ou LOW
//    2) A aplicação utiliza o WiFiManager para definir as credenciais da Rede na primeira vez
//       ou quando há mudança de SSID ou senha da Rede. Para saber se a aplicação está no modo de
//       configuração, basta observar o LED interno. Se apagado, está no modo de configuração. 
//       Se aceso, está no modo de operação normal e o Modo de Configuração pode ser acionado 
//       pressionando o botão de BOOT do ESP32. Uma vez no modo de configuração, o usuário 
//       deve se conectar com o SSIDAP/SENHAAP e, em seguida, acessar a URL http://192.168.4.1 
//       A seguir, descrevemos os parâmetros que devem ser definidos:
//       . SSID/Senha -> credenciais da Rede Wifi
//       . AliasName  -> para ser incluído no mDNS e permitir o acesso na forma: http://<alias>.local
//       . ssidAp     -> SSID no Modo de Configuração
//       . passAp     -> Senha no Modo de Configuração
//       . ntpServer  -> Endereço do Servidor NTP para sincronismo do relógio interno
//       . timezone   -> fuso hórário em relação ao GMT no caso do Brasil -3
//       . latitude   -> valor da latitude onde está o equipamento  (para dados do Clima)
//       . longitude  -> valor da longitude onde está o equipamento (para dados do Clima)
//       . userAdmin  -> nome do usuário para a atualização do FirmWare via ElegantOTA
//       . passAdmin  -> senha do usuário para a atualização do FirmWare via ElegantOTA
//       . autoReboot -> para defunir se deve rebootar ou não após a atualização
//    3) A aplicação utiliza a biblioteca AsyncElegantOTA para permitir a atualização do código
//       via WEB usando a url: http://<alias>.local/update
//       A atualização exige autenticação usando as definições de usrAdmin/passAdmin no Modo
//       de Configuração. Para atualizar é necessário informar o nome do arquivo contendo a imagem
//       gerada pelo compilador através do Menu Sketch | Export Compiled Binary.
//-------------------------------------------------------------------------------------------------

#include <WiFi.h>                   // Biblioteca para a rede wifi
#include <ESP32Ping.h>              // Biblioteca Ping
#include <ESPmDNS.h>                // Biblioteca para adiocionar aliases no DNS da Rede Local
#include <HTTPClient.h>             // Biblioteca para manipulação de Request Http
#include <WiFiManager.h>            // Biblioteca WiFiManager para Credenciais da rede e parâmetros
#include <ESPAsyncWebServer.h>      // Biblioteca para Servidor Web Assíncrono
#include <ArduinoJson.h>            // Biblioteca para manipulação de estrutiras JSON 
#include <AsyncElegantOTA.h>        // Biblioteca para atualização via Web
#include <LittleFS.h>               // Biblioteca que implementa o filesystem
#include <time.h>                   // Biblioteca para Manipulações de Relógio

//--------------------------
// Definição de Constantes
//--------------------------

#define LED_ON               LOW    // Para ligar o LED
#define LED_OFF              HIGH   // Para desligar o LED
#define TIMEOUT_PORTAL       300    // Tempo máximo que o Portal ficará ativo em seg
#define TIMEOUT_CONEXAO      60     // Tempo máximo de conexão com o Roteador em seg
#define INTERVALO_NTP        30000  // Intervalo de tentativa de atualização NTP Server (ms)

//-----------------------------
// Pino do Botão BOOT do ESP32
//-----------------------------

#define BOOT_PIN             9      // Pino de Boot do ESP32C3 OLED 

//----------------------------
// Pino do Led Interno 
//----------------------------

#define LED_BUILTIN          8      // Pino do Led Interno do ESP32

//--------------------------
// OBJETOS GLOBAIS
//--------------------------

AsyncWebServer server(80);          // Servidor Web
IPAddress ip (1, 1, 1, 1);          // The remote ip to ping, DNS do Google
WiFiManager wm;                     // Servidor WiFiManager
WiFiManagerParameter *aliasName;    // Parâmetros aliasName para WiFiManager
WiFiManagerParameter *ssidAp;       // Parâmetros ssidAp para WiFiManager
WiFiManagerParameter *passAp;       // Parâmetros passAp para WiFiManager
WiFiManagerParameter *userAdmin;    // Parâmetros userAdmin para WiFiManager
WiFiManagerParameter *passAdmin;    // Parâmetros passAdmin para WiFiManager 
WiFiManagerParameter *ntpServer;    // Parâmetros ntpServer para WiFiManager
WiFiManagerParameter *timezone;     // Parâmetros timezone para WiFiManager
WiFiManagerParameter *latitude;     // Parâmetros latitude para WiFiManager
WiFiManagerParameter *longitude;    // Parâmetros longitude para WiFiManager 
WiFiManagerParameter *autoRebootOTA;// Parâmetros autoRebootOTA para WiFiManager
bool portalFoiUsado = false;        // Flag se passou pelo Portal WiFiManager
bool wifiSalvo = false;             // Flag se salvou credenciais
bool paramsSalvos = false;          // Flag se salvou demais parâmetros

struct AppConfig 
{
  String aliasName;                 // aliasName no registro para persistêncua
  String ssidAp;                    // ssidAp no registro para persistêncua
  String passAp;                    // passAp no registro para persistêncua
  String userAdmin;                 // userAdmin no registro para persistêncua                
  String passAdmin;                 // passAdmin no registro para persistêncua 
  String ntpServer;                 // ntpServer no registro para persistêncua
  int timezone;                     // timezone no registro para persistêncua
  float latitude;                   // latitude no registro para persistêncua
  float longitude;                  // longitude no registro para persistêncua 
  bool autoRebootOTA;               // autoRebootOTA no registro para persistêncua
} config;

// ----------------------
// Variáveis globais
// ----------------------

float g_temp      = 0.0;            // Para receber a temperatura 
float g_humidity  = 0.0;            // Para receber a umidade 
float g_wind      = 0.0;            // Para receber a velocidade do vento 
float g_elevation = 0.0;            // Para receber a altitude 

float g_usd_brl   = 0.0;            // Para receber o valor do Dólar 
float g_eur_brl   = 0.0;            // Para receber o valor do Euro 
float g_gbp_brl   = 0.0;            // Para receber o valor da Libra

float g_jpy_brl   = 0.0;            // Para receber o valor da moeda Japonesa
float g_cny_brl   = 0.0;            // Para receber o valor da moeda Chinesa 
float g_rub_brl   = 0.0;            // Para receber o valor da moeda Russa 
float g_ars_brl   = 0.0;            // Para receber o valor da peso Argentino 

char g_weather_time[32];            // Para receber data/hora da atualização
int g_weather_interval;             // Para receber o intervalo de atualização

volatile bool g_bootWasPressed = false;  // Se pressionou o botão BOOT

// JSON bruto das cripto em BRL (para servir direto)
String g_cryptoJsonBRL;             // Para montar o JSON das moedas

unsigned long lastUpdate = 0;       // Última varredura de atualização
//const unsigned long UPDATE_INTERVAL = 5UL * 60UL * 1000UL; // 5 minutos
bool sincronizado = false;          // Se sincronizado ao NTP Server
unsigned long lastNTP = 0;          // última tentatita de sincronismo com NTP Server 
unsigned long nextWeatherUpdateEpoch = 0; // Próxima verificação para site meteo.com
bool firstWeatherSync = true;


// Interface Web principal (armazenada em PROGMEM)
#include "html_main.h"

// Tema personalizado do portal WiFiManager (PROGMEM)
#include "wm_css.h"

//--------------------------
// Prototipação de Rotinas
//--------------------------

bool getLocationFromMLS();
bool fetchWeather();
bool fetchForex();
bool fetchCrypto();
void IRAM_ATTR onBootPressed() ;
void preparaWifiManager();
void processaWifiManager();
bool initFS();
void loadConfigMock();
bool loadConfigFromFile();
bool saveConfigToFile();
bool setDNSNAME(String nome);
void initServer();
String getTimeStamp();
bool getNTPtime(int sec);
bool refreshNTPServer();
String getIsoTimestampNow();
void applyTimezone(int tzOffset);
time_t parseIso8601ToEpoch(const String& iso);
String getIsoTimestampFromEpoch(time_t epoch);

// ----------------------
// Setup
// ----------------------

void setup() 
{
  Serial.begin(115200);
  delay(1000);

  // Hello na Console

  Serial.println("\n--- Inicializando Sistema ---");
  Serial.printf("Chip model: %s, Rev %d\n", ESP.getChipModel(), ESP.getChipRevision());
  Serial.printf("Cores: %d\n", ESP.getChipCores());    

  // Inicializa o LED_BUILTIN do ESP32

  pinMode(LED_BUILTIN, OUTPUT);

  // Inicializa o BOTÂO BOOT interno do ESP32

  pinMode(BOOT_PIN, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(BOOT_PIN), onBootPressed, FALLING);

  // Demais inicializações

  initFS();               // Inicialização do sistema de arquivo
  loadConfigMock();       // define defaults iniciais
  loadConfigFromFile();   // tenta sobrescrever a partir do arquivo

  // Processa o WifiManager (Menu, campos, timeout, callback) e chama Portal se necessário

  processaWifiManager();

  Serial.print("Conectado em: ");
  Serial.println(WiFi.SSID());

  Serial.print("IP: ");
  Serial.println(WiFi.localIP());

  // Seta o DNSNAME

  bool ok = setDNSNAME(config.aliasName.c_str());
  Serial.printf("[Alias] %s %s no mDNS.\n",config.aliasName.c_str(),
                 ok ? "adicionado com sucesso" : "falha ao adicionar");

  // Tenta sincronizar como NTP Server

  sincronizado = refreshNTPServer();

  // 1) Localização
  // if (!getLocationFromMLS()) {
  //   Serial.println("Não foi possível obter localização.");
  // }

  // 2) Forex

  fetchForex();

  // 3) Clima

  fetchWeather();

  // 4) Crypto

  fetchCrypto();

  // Inicializa o Servidor Web

  initServer();

}

// ----------------------
// Loop
// ----------------------

void loop() 
{
  // Obtém o momento atual

  unsigned long now = millis();

 // Verifica se pressionou o botão de entrar no modo de configuração

  if (g_bootWasPressed) 
  {
    Serial.println("Boot pressionado...");
    wm.resetSettings();
     delay(100);
    ESP.restart();
  }  

  // Verifica se deve atualizar os indicadores no HTML

  time_t nowEpoch = time(nullptr);

  if (!firstWeatherSync && nowEpoch >= nextWeatherUpdateEpoch)
  {
      Serial.println("[Loop] Atualizando dados sincronizados com Open‑Meteo...");

      fetchWeather();
      fetchForex();
      fetchCrypto();

      // recalcula o próximo ciclo baseado no novo meteoTime
      time_t meteoEpoch = parseIso8601ToEpoch(String(g_weather_time));
      nextWeatherUpdateEpoch = meteoEpoch + g_weather_interval + 60;

      Serial.printf("[Loop] Próxima atualização às %s\n",
                    getIsoTimestampFromEpoch(nextWeatherUpdateEpoch).c_str());
  }

   // Verifica se não está sincronizado com NTP Server e no 
   // intervalo para tentar sincronizar novamente
  
  if (!sincronizado && (now - lastNTP >= INTERVALO_NTP) && WiFi.status() == WL_CONNECTED) 
  {
    lastNTP = now;
    sincronizado = refreshNTPServer(); 
  }  

  // Verifica se há Atualizações

  AsyncElegantOTA.loop();

}

// ----------------------
// Função: Geolocalização via Mozilla Location Service
// ----------------------

bool getLocationFromMLS() 
{
  Serial.println("[MLS] Escaneando redes WiFi...");
  int n = WiFi.scanNetworks();
  if (n <= 0) {
    Serial.println("[MLS] Nenhuma rede encontrada.");
    return false;
  }

  StaticJsonDocument<2048> doc;
  JsonArray wifiArray = doc.createNestedArray("wifiAccessPoints");

  for (int i = 0; i < n && i < 10; i++) { // limita a 10 redes
    JsonObject ap = wifiArray.createNestedObject();
    ap["macAddress"] = WiFi.BSSIDstr(i);
    ap["signalStrength"] = WiFi.RSSI(i);
  }

  String payload;
  serializeJson(doc, payload);

  WiFiClientSecure *client = new WiFiClientSecure;
  client->setInsecure();  // Ignora certificado TLS (necessário no C3)

  HTTPClient http;
  http.begin(*client, "https://location.services.mozilla.com/v1/geolocate?key=geoclue");
  http.addHeader("Content-Type", "application/json");
  http.addHeader("Connection", "close");
  http.setTimeout(5000);

  Serial.println("[MLS] Enviando requisição...");
  int httpCode = http.POST(payload);
  if (httpCode != 200) {
    Serial.printf("[MLS] Erro HTTP: %d\n", httpCode);
    http.end();
    return false;
  }

  String resp = http.getString();
  http.end();

  StaticJsonDocument<512> respDoc;
  DeserializationError err = deserializeJson(respDoc, resp);
  if (err) {
    Serial.println("[MLS] Erro parse JSON.");
    return false;
  }

  config.latitude  = respDoc["location"]["lat"] | 0.0;
  config.longitude = respDoc["location"]["lng"] | 0.0;

  Serial.printf("[MLS] Lat: %.6f, Lon: %.6f\n", config.latitude, config.longitude);
  return true;
}

// ----------------------
// Função: Open-Meteo
// ----------------------

bool fetchWeather() 
{
  if (config.latitude == 0.0 && config.longitude == 0.0) {
    Serial.println("[Weather] Localização inválida.");
    return false;
  }

  char url[256];
  snprintf(url, sizeof(url),
           "https://api.open-meteo.com/v1/forecast?latitude=%.6f&longitude=%.6f&current=temperature_2m,relative_humidity_2m,wind_speed_10m&timezone=auto",
           config.latitude, config.longitude);

  HTTPClient http;
  http.begin(url);
  int httpCode = http.GET();
  if (httpCode != 200) {
    Serial.printf("[Weather] HTTP erro: %d\n", httpCode);
    http.end();
    return false;
  }

  String resp = http.getString();
  http.end();

  StaticJsonDocument<1024> doc;
  if (deserializeJson(doc, resp)) {
    Serial.println("[Weather] Erro parse JSON.");
    return false;
  }

  JsonObject current = doc["current"];
  g_temp     = current["temperature_2m"] | 0.0;
  g_humidity = current["relative_humidity_2m"] | 0.0;
  g_wind     = current["wind_speed_10m"] | 0.0;
  g_elevation = doc["elevation"] | 0.0;

  strncpy(g_weather_time, doc["current"]["time"] | "", sizeof(g_weather_time));
  g_weather_interval = doc["current"]["interval"] | 900; // 900s = 15 min

  if (firstWeatherSync) 
  {
      time_t meteoEpoch = parseIso8601ToEpoch(String(g_weather_time));

      // Próximo bloco = meteoEpoch + interval + 60s de margem
      nextWeatherUpdateEpoch = meteoEpoch + g_weather_interval + 60;

      Serial.printf("[Weather] Próxima atualização às %s\n",
                    getIsoTimestampFromEpoch(nextWeatherUpdateEpoch).c_str());

      firstWeatherSync = false;
  }

  Serial.printf("[Weather] T=%.1f, H=%.1f, W=%.1f\n", g_temp, g_humidity, g_wind);
  return true;
}

// ----------------------
// Função: Forex (Exchangerate.host)
// ----------------------

bool fetchForex() 
{
  WiFiClientSecure *client = new WiFiClientSecure;
  client->setInsecure();

  HTTPClient http;
  http.begin(*client, "https://open.er-api.com/v6/latest/BRL");

  int httpCode = http.GET();
  if (httpCode != 200) {
    Serial.printf("[Forex] HTTP erro: %d\n", httpCode);
    http.end();
    return false;
  }

  String resp = http.getString();
  http.end();

  StaticJsonDocument<4096> doc;
  if (deserializeJson(doc, resp)) {
    Serial.println("[Forex] Erro parse JSON.");
    return false;
  }

  JsonObject rates = doc["rates"];

  float usd = rates["USD"] | 0.0;
  float eur = rates["EUR"] | 0.0;
  float gbp = rates["GBP"] | 0.0;

  float jpy = rates["JPY"] | 0.0;
  float cny = rates["CNY"] | 0.0;
  float rub = rates["RUB"] | 0.0;
  float ars = rates["ARS"] | 0.0;

  if (usd == 0.0) return false;

  // Conversão correta (1 moeda = X BRL)
  g_usd_brl = 1.0 / usd;
  g_eur_brl = 1.0 / eur;
  g_gbp_brl = 1.0 / gbp;

  g_jpy_brl = 1.0 / jpy;
  g_cny_brl = 1.0 / cny;
  g_rub_brl = 1.0 / rub;
  g_ars_brl = 1.0 / ars;

  Serial.printf("[Forex] 1 USD = %.4f BRL\n", g_usd_brl);
  Serial.printf("[Forex] 1 EUR = %.4f BRL\n", g_eur_brl);
  Serial.printf("[Forex] 1 GBP = %.4f BRL\n", g_gbp_brl);

  Serial.printf("[Forex] 1 JPY = %.4f BRL\n", g_jpy_brl);
  Serial.printf("[Forex] 1 CNY = %.4f BRL\n", g_cny_brl);
  Serial.printf("[Forex] 1 RUB = %.4f BRL\n", g_rub_brl);
  Serial.printf("[Forex] 1 ARS = %.4f BRL\n", g_ars_brl);

  return true;
}

// ----------------------
// Função: Crypto (CoinGecko) + conversão para BRL
// ----------------------

bool fetchCrypto() 
{
  if (g_usd_brl == 0.0) {
    Serial.println("[Crypto] USD/BRL inválido, chame fetchForex() antes.");
    return false;
  }

  HTTPClient http;
  http.begin("https://api.coingecko.com/api/v3/simple/price?ids=bitcoin,ethereum,binancecoin,solana,ripple,cardano,tron,dogecoin,shiba-inu&vs_currencies=usd&include_24hr_change=true");
  int httpCode = http.GET();
  if (httpCode != 200) {
    Serial.printf("[Crypto] HTTP erro: %d\n", httpCode);
    http.end();
    return false;
  }

  String resp = http.getString();
  http.end();

  StaticJsonDocument<4096> doc;
  if (deserializeJson(doc, resp)) {
    Serial.println("[Crypto] Erro parse JSON.");
    return false;
  }

  // Montar JSON já em BRL
  StaticJsonDocument<4096> out;
  struct {
    const char* id;
    const char* name;
  } coins[] = {
    {"bitcoin", "Bitcoin"},
    {"ethereum", "Ethereum"},
    {"binancecoin", "BNB"},
    {"solana", "Solana"},
    {"ripple", "XRP"},
    {"cardano", "Cardano"},
    {"tron", "TRON"},
    {"dogecoin", "Dogecoin"},
    {"shiba-inu", "Shiba Inu"}
  };

  for (auto &c : coins) {
    JsonObject src = doc[c.id];
    if (src.isNull()) continue;
    float usd = src["usd"] | 0.0;
    float chg = src["usd_24h_change"] | 0.0;

    JsonObject dst = out.createNestedObject(c.id);
    dst["name"]       = c.name;
    dst["price_brl"]  = usd * g_usd_brl;
    dst["change_24h"] = chg;
  }

  g_cryptoJsonBRL.clear();
  serializeJson(out, g_cryptoJsonBRL);
  Serial.println("[Crypto] Atualizado.");
  return true;
}

//--------------------------------------------
// Definição da Rotina de Interrupção do Botão
//--------------------------------------------

void IRAM_ATTR onBootPressed() 
{
  g_bootWasPressed = true;
}

//--------------------------------------
// Definição do Callback do WifiManager
//--------------------------------------

void configModeCallback(WiFiManager *wm) 
{
  Serial.println("Entrou no modo de configuração!");
  digitalWrite(LED_BUILTIN, LED_OFF); // apaga o LED informativo para o modo de configuração
  portalFoiUsado = true;
}

//--------------------------------------
// Prepara o ambiente do WifiManager
//--------------------------------------

void preparaWifiManager()
{
  // Callbacks e timeouts
  wm.setAPCallback(configModeCallback);
  wm.setConfigPortalTimeout(TIMEOUT_PORTAL);
  wm.setConnectTimeout(TIMEOUT_CONEXAO);

  // Menu customizado

  std::vector<const char *> menu = {"wifi", "sep", "exit"};
  wm.setMenu(menu);

  // Título da página
 
  wm.setTitle("<center>⭐ Configuração ⭐</center>");

  // Aplica o Tema Dark

  wm.setCustomHeadElement(wm_css_dark);

  // ------------------------------------
  // Campos de Parâmetros no Wifimanager
  // -----------------------------------

   aliasName = new WiFiManagerParameter("aliasName", "Informe o AliasName para o mDNS", config.aliasName.c_str(), 30);
   ssidAp    = new WiFiManagerParameter("ssidAp", "Informe o SSID do Modo AP", config.ssidAp.c_str(), 30);   
   passAp    = new WiFiManagerParameter("passAp", "Informe a Senha do Modo AP", config.passAp.c_str(), 30); 
   userAdmin = new WiFiManagerParameter("userAdmin", "Informe o Usuário para Atualização", config.userAdmin.c_str(), 30);        
   passAdmin = new WiFiManagerParameter("passAdmin", "Informe a Senha do Usuário de Atualização", config.passAdmin.c_str(), 30);
   ntpServer = new WiFiManagerParameter("ntpServer", "Informe o Servidor NTP para Sincronismo", config.ntpServer.c_str(), 30);
   timezone  = new WiFiManagerParameter("timezone", "Informe o TimeZone", String(config.timezone).c_str(), 4);
   latitude  = new WiFiManagerParameter("latitude", "Informe o Latitude", String(config.latitude,5).c_str(), 15);
   longitude = new WiFiManagerParameter("longitude", "Informe o Longitude", String(config.longitude,5).c_str(), 15);
   autoRebootOTA = new WiFiManagerParameter("autoRebootOTA", "Rebootar Após Atualização (0 ou 1)", String(config.autoRebootOTA).c_str(), 1);

  // Adiciona os Parâmetros

  wm.addParameter(aliasName);
  wm.addParameter(ssidAp);
  wm.addParameter(passAp);
  wm.addParameter(userAdmin);
  wm.addParameter(passAdmin);
  wm.addParameter(ntpServer);
  wm.addParameter(timezone);
  wm.addParameter(latitude);
  wm.addParameter(longitude);
  wm.addParameter(autoRebootOTA);
  
  // Sincronismo de parâmetros

  wm.setSaveParamsCallback([]() {
    Serial.println("Parâmetros salvos.");
    paramsSalvos = true;
  });

  // Sincronismo de WiFi

  wm.setSaveConfigCallback([]() {
    Serial.println("Config WiFi salva.");
    wifiSalvo = true;
  });
}

//--------------------------------------
// Processa o WifiManager
//--------------------------------------

void processaWifiManager() 
{
    //WiFi.mode(WIFI_STA);
    preparaWifiManager();
    Serial.println("Iniciando WiFiManager...");
    bool conectado = wm.autoConnect(config.ssidAp.c_str(), config.passAp.c_str());
    if (!conectado) 
    {
        Serial.println("Falha ao conectar. Reiniciando...");
        delay(2000);
        ESP.restart();
    }

    // Se o portal foi usado, precisamos salvar os parâmetros e reiniciar

    if (portalFoiUsado)
    {
        Serial.println("Portal foi usado. Salvando parâmetros...");

        // Recupera os Campos Strings

        config.aliasName   = String(aliasName->getValue());
        config.ssidAp      = String(ssidAp->getValue());
        config.passAp      = String(passAp->getValue());
        config.userAdmin   = String(userAdmin->getValue());
        config.passAdmin   = String(passAdmin->getValue());
        config.ntpServer   = String(ntpServer->getValue());

        // Recupera Campos numéricos
        
        config.latitude    = atof(latitude->getValue());
        config.longitude   = atof(longitude->getValue());
        config.timezone    = atoi(timezone->getValue());

        // Recupera Campo Bool

        config.autoRebootOTA = atoi(autoRebootOTA->getValue()) == 1 ? true : false;

        // Salva no FileSystem 

        saveConfigToFile();

        Serial.println("Parâmetros atualizados e salvos com sucesso!");
        Serial.println("Reiniciando para liberar porta 80...");
        delay(1000);
        ESP.restart();
    }
    // Se chegou aqui, o portal NÃO foi usado → porta 80 está livre
    digitalWrite(LED_BUILTIN, LED_ON); // acende o LED informativo para o modo normal 
    Serial.println("WiFiManager finalizado. Iniciando AsyncWebServer...");
}

//--------------------------------------
// Inicialização do sistema de arquivos
//--------------------------------------

bool initFS() 
{
    if (!LittleFS.begin(true)) {
        Serial.println("ERRO: LittleFS não inicializou.");
        return false;
    }
    Serial.println("LittleFS OK.");
    return true;
}

//-------------------------------------------
// Carregar configuração default
//-------------------------------------------

void loadConfigMock() 
{
  config.aliasName = "forecast";
  config.ssidAp    = "ESP32-Setup";
  config.passAp    = "12345678";
  config.userAdmin = "admin";
  config.passAdmin = "admin";    
  config.ntpServer = "pool.ntp.br";
  config.timezone  = -3;
  config.latitude  = -20.05399;
  config.longitude = -44.35925;  
  config.autoRebootOTA = true;  
}

//---------------------------------------------------
// Carrega configuração de /config.json (se existir)
//---------------------------------------------------

bool loadConfigFromFile() 
{
    if (!LittleFS.exists("/config.json")) {
        Serial.println("[Config] /config.json não existe, usando mock/default");
        return false;
    }

    File f = LittleFS.open("/config.json", "r");
    if (!f) {
        Serial.println("[Config] ERRO ao abrir /config.json para leitura");
        return false;
    }

    DynamicJsonDocument doc(2048);
    DeserializationError err = deserializeJson(doc, f);
    f.close();

    if (err) {
        Serial.print("[Config] ERRO ao parsear JSON: ");
        Serial.println(err.c_str());
        return false;
    }

    // 🔎 DEBUG PRETTY

    Serial.println("[Config] JSON lido do arquivo (pretty):");
    serializeJsonPretty(doc, Serial);
    Serial.println();

    // Campos string

    if (doc.containsKey("aliasName"))   config.aliasName   = (const char*) doc["aliasName"];
    if (doc.containsKey("ssidAp"))      config.ssidAp      = (const char*) doc["ssidAp"];
    if (doc.containsKey("passAp"))      config.passAp      = (const char*) doc["passAp"];
    if (doc.containsKey("userAdmin"))   config.userAdmin   = (const char*) doc["userAdmin"];
    if (doc.containsKey("passAdmin"))   config.passAdmin   = (const char*) doc["passAdmin"];
    if (doc.containsKey("ntpServer"))   config.ntpServer   = (const char*) doc["ntpServer"];

    // Campos numéricos
    
    if (doc.containsKey("latitude"))    config.latitude    = doc["latitude"];
    if (doc.containsKey("longitude"))   config.longitude   = doc["longitude"];
    if (doc.containsKey("timezone"))    config.timezone    = doc["timezone"];

    // Bool

    if (doc.containsKey("autoRebootOTA")) config.autoRebootOTA = doc["autoRebootOTA"];

    Serial.println("[Config] Configuração carregada de /config.json");
    return true;
}

//--------------------------------------------
// Salva configuração no arquivo /config.json
//--------------------------------------------

bool saveConfigToFile() 
{
    DynamicJsonDocument doc(2048);

    doc["aliasName"]     = config.aliasName;
    doc["ssidAp"]        = config.ssidAp;
    doc["passAp"]        = config.passAp;
    doc["userAdmin"]     = config.userAdmin;
    doc["passAdmin"]     = config.passAdmin;
    doc["latitude"]      = config.latitude;
    doc["longitude"]     = config.longitude;
    doc["ntpServer"]     = config.ntpServer;    
    doc["timezone"]      = config.timezone;
    doc["autoRebootOTA"] = config.autoRebootOTA;

    // 🔎 DEBUG PRETTY
    Serial.println("[Config] Conteúdo salvo (pretty):");
    serializeJsonPretty(doc, Serial);
    Serial.println();

    File f = LittleFS.open("/config.json", "w");
    if (!f) {
        Serial.println("[Config] ERRO ao abrir /config.json para escrita");
        return false;
    }

    serializeJson(doc, f);
    f.close();

    Serial.println("[Config] Configuração salva em /config.json");
    return true;
}

//---------------------------------
// Define o HostName como DNS NAME
//---------------------------------

bool setDNSNAME(String nome)
{
   WiFi.setHostname(nome.c_str());
   bool ok = MDNS.begin(nome.c_str());
   if (ok) 
   {
      MDNS.addService("http", "tcp", 80);
      MDNS.setInstanceName(nome.c_str()); // Adicionar o nome da instância      
   }
   return ok;
}

//---------------------------------
// Inicializa o Servidor Web
//---------------------------------

void initServer()
{
  // ----------------------
  // Rotas HTTP
  // ----------------------
  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send_P(200, "text/html", MAIN_page);
  });

  server.on("/api/weather", HTTP_GET, [](AsyncWebServerRequest *request){
    StaticJsonDocument<256> doc;
    doc["temperature"] = g_temp;
    doc["humidity"]    = g_humidity;
    doc["wind"]        = g_wind;
    doc["lat"]         = config.latitude;
    doc["lon"]         = config.longitude;
    doc["elevation"] = g_elevation;

    doc["time"] = g_weather_time;
    doc["interval"] = g_weather_interval;

    doc["esp_time"] = getIsoTimestampNow();     

    String json;
    serializeJson(doc, json);
    request->send(200, "application/json", json);
  });

  server.on("/api/forex", HTTP_GET, [](AsyncWebServerRequest *request){
    StaticJsonDocument<256> doc;
    doc["usd_brl"] = g_usd_brl;
    doc["eur_brl"] = g_eur_brl;
    doc["gbp_brl"] = g_gbp_brl;

    doc["jpy_brl"] = g_jpy_brl;
    doc["cny_brl"] = g_cny_brl;
    doc["rub_brl"] = g_rub_brl;
    doc["ars_brl"] = g_ars_brl;

    String json;
    serializeJson(doc, json);
    request->send(200, "application/json", json);
  });

  server.on("/api/crypto", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send(200, "application/json", g_cryptoJsonBRL);
  });

  // Ativa a Elegant OTA para atualização via WEB

  AsyncElegantOTA.begin(&server,config.userAdmin.c_str(),config.passAdmin.c_str());    

  // Ativa o Servidor Principal da Aplicação

  server.begin();
  Serial.println("Servidor HTTP iniciado.");
}

//-----------------------------------------
// Devolve o localtime dd/mm/aaaa hh:mm:ss
//-----------------------------------------

String getTimeStamp()
{
  time_t now;
  time(&now);
  char timestamp[30];
  strftime(timestamp, 30, "%d/%m/%Y %T", localtime(&now));
  return String(timestamp);
}

//----------------------------------------------------------
// Sincroniza o horário do ESP32 com  NTP server brasileiro
//----------------------------------------------------------

bool getNTPtime(int sec) 
{

  {
    uint32_t start = millis();

    tm timeinfo;
    time_t now;
    int cont=0;

    do 
    {
      time(&now);
      localtime_r(&now, &timeinfo);
      if (++cont % 80 == 0) Serial.println();
      else Serial.print(".");
      delay(10);
    } while (((millis() - start) <= (1000 * sec)) && (timeinfo.tm_year < (2016 - 1900)));
    if (timeinfo.tm_year <= (2016 - 1900)) return false;  // the NTP call was not successful
    Serial.print("\nnow ");
    Serial.println(now);
    Serial.print("Time "); 
    Serial.println(getTimeStamp());
  }

  return true;
}

// ======================================================
// Função de fazer o sincronismo do relógio interno com o
// servidor NTP definido nos parâmetros do WifiManager
// ======================================================

//----------------------------------------------------------
// Função de fazer o sincronismo do relógio interno com o
// servidor NTP definido nos parâmetros do WifiManager
//----------------------------------------------------------

bool refreshNTPServer()
{
   // Sincroniza o horário interno com o Servidor NTP nacional
   Serial.print("Tentando sincronismo com o servidor NTP ");
   Serial.print(config.ntpServer.c_str());
   Serial.print(" com TimeZone ");
   Serial.println(config.timezone);   
   
   // Verifica se está navegando pela internet pois às vezes fica conectado no AP porém sem internet
   if (!Ping.ping(ip,4))
   {
       Serial.println("Sem internet no momento...");
   }
   else 
   {
      Serial.print("Internet ativa com média de ");
      Serial.print(Ping.averageTime());
      Serial.println(" ms");
      configTime(0, 0, config.ntpServer.c_str());
      applyTimezone(config.timezone);
    
      if (getNTPtime(10))
      { // wait up to 10sec to sync
        Serial.println("NTP Server sincronizado");
        return true;
      } 
      else 
      {
         Serial.println("Timer interno não foi sincronizado");
         //ESP.restart();
      }
   }
   return false;
}

//-----------------------------------
// Aplica o Timezone como int na API
//-----------------------------------

void applyTimezone(int tzOffset)
{
    // tzOffset = -3 → "GMT3" (UTC-3)
    int off = abs(tzOffset);

    char tz[16];
    snprintf(tz, sizeof(tz), "GMT%d", off);

    Serial.printf("Aplicando timezone POSIX: %s\n", tz);

    setenv("TZ", tz, 1);
    tzset();
}

//---------------------------------------------------------
// Função de pegar o timestamp completo do relógio interno
//---------------------------------------------------------

String getIsoTimestampNow()
{
    time_t now;
    time(&now);

    struct tm info;
    localtime_r(&now, &info);  // já respeita o timezone configurado

    char buf[24];
    // Formato: YYYY-MM-DDTHH:MM
    strftime(buf, sizeof(buf), "%Y-%m-%dT%H:%M", &info);

    return String(buf);
}

//--------------------------------------
// Transforma a hora string para time_t
//--------------------------------------

time_t parseIso8601ToEpoch(const String& iso)
{
    struct tm t = {0};
    // Formato: YYYY-MM-DDTHH:MM
    sscanf(iso.c_str(), "%d-%d-%dT%d:%d",
           &t.tm_year, &t.tm_mon, &t.tm_mday,
           &t.tm_hour, &t.tm_min);

    t.tm_year -= 1900;
    t.tm_mon  -= 1;

    return mktime(&t); // já respeita timezone configurado
}

//-----------------------------------------
// Transforma a hora de time_t para string
//-----------------------------------------

String getIsoTimestampFromEpoch(time_t epoch)
{
    struct tm info;
    localtime_r(&epoch, &info);

    char buf[24];
    strftime(buf, sizeof(buf), "%Y-%m-%dT%H:%M", &info);
    return String(buf);
}

Interface Web — html_main.h

#pragma once

// Página principal do Painel IoT: Clima, Moedas e Criptomoedas
const char MAIN_page[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ESP32 Dashboard</title>
<style>
  body {
    font-family: Arial, sans-serif;
    background: #0f172a;
    color: #e5e7eb;
    margin: 0;
    padding: 0;
  }
  header {
    text-align: center;
    background: linear-gradient(90deg, #1d4ed8, #7c3aed);
    padding: 16px 24px;
    color: white;
    box-shadow: 0 2px 8px rgba(0,0,0,0.4);
  }
  header h1 {
    margin: 0;
    font-size: 20px;
  }
  header small {
    opacity: 0.8;
  }
  .container {
    padding: 16px;
  }
  .tabs {
    display: flex;
    border-bottom: 1px solid #1f2937;
    margin-bottom: 16px;
  }
  .tablink {
    flex: 1;
    padding: 10px;
    cursor: pointer;
    background: #111827;
    border: none;
    color: #9ca3af;
    font-weight: bold;
    transition: background 0.2s, color 0.2s;
  }
  .tablink.active {
    background: #1f2937;
    color: #f9fafb;
    border-bottom: 2px solid #3b82f6;
  }
  .tabcontent {
    display: none;
  }
  .card {
    background: #111827;
    border-radius: 8px;
    padding: 16px;
    margin-bottom: 16px;
    box-shadow: 0 2px 8px rgba(0,0,0,0.4);
  }
  .card h2 {
    margin-top: 0;
    font-size: 18px;
    margin-bottom: 8px;
  }
  /* Layout padrão: vertical (celular) */
  .grid {
    display: grid;
    grid-template-columns: 1fr;
    gap: 12px;
  }

  /* Layout horizontal quando a tela for maior que 600px */
  @media (min-width: 600px) {
    .grid {
      grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
    }
  }
  .metric {
    background: #020617;
    border-radius: 6px;
    padding: 10px;
    text-align: center;
  }
  .metric span.label {
    display: block;
    font-size: 12px;
    color: #9ca3af;
  }
  .metric span.value {
    display: block;
    font-size: 18px;
    font-weight: bold;
    margin-top: 4px;
  }
  table {
    width: 100%;
    border-collapse: collapse;
    font-size: 14px;
  }
  th, td {
    padding: 8px;
    border-bottom: 1px solid #1f2937;
    text-align: left;
  }
  th {
    background: #020617;
  }
  tr:nth-child(even) {
    background: #020617;
  }
  .change-pos {
    color: #22c55e;
  }
  .change-neg {
    color: #ef4444;
  }
  footer {
    text-align: center;
    font-size: 11px;
    color: #6b7280;
    padding: 8px 0 16px;
  }
</style>
</head>
<body>
<header>
  <h1>ESP32 Dashboard</h1>
  <small>Clima • Moedas • Criptomoedas</small>
</header>
<div class="container">
  <div class="tabs">
    <button class="tablink active" onclick="openTab('clima', this)">Clima</button>
    <button class="tablink" onclick="openTab('moedas', this)">Moedas</button>
    <button class="tablink" onclick="openTab('cripto', this)">Cripto</button>
  </div>

  <div id="clima" class="tabcontent" style="display:block;">
    <div class="card">
      <h2>Clima atual</h2>
      <div class="grid">
        <div class="metric">
          <span class="label">Temperatura</span>
          <span class="value" id="temp">-- °C</span>
        </div>
        <div class="metric">
          <span class="label">Umidade</span>
          <span class="value" id="humidity">-- %</span>
        </div>
        <div class="metric">
          <span class="label">Vento</span>
          <span class="value" id="wind">-- km/h</span>
        </div>
        <div class="metric">
          <span class="label">Latitude</span>
          <span class="value" id="lat">--</span>
        </div>
        <div class="metric">
          <span class="label">Longitude</span>
          <span class="value" id="lon">--</span>
        </div>
        <div class="metric">
          <span class="label">Elevação</span>
          <span class="value" id="elevation">-- m</span>
        </div>
        <div class="metric">
          <span class="label">Atualização Open‑Meteo</span>
          <span id="meteo_time" class="value">--</span>
        </div>
        <div class="metric">
          <span class="label">Intervalo</span>
          <span id="meteo_interval" class="value">--</span>
        </div>
        <div class="metric">
          <span class="label">Hora do ESP32</span>
          <span id="esp_time" class="value">--</span>
        </div>
      </div>
    </div>
  </div>

  <div id="moedas" class="tabcontent">
    <div class="card">
      <h2>Cotações (BRL)</h2>
      <div class="grid">

        <div class="metric">
          <span class="label">USD → BRL</span>
          <span class="value" id="usd_brl">--</span>
        </div>

        <div class="metric">
          <span class="label">EUR → BRL</span>
          <span class="value" id="eur_brl">--</span>
        </div>

        <div class="metric">
          <span class="label">GBP → BRL</span>
          <span class="value" id="gbp_brl">--</span>
        </div>

        <div class="metric">
          <span class="label">JPY → BRL</span>
          <span class="value" id="jpy_brl">--</span>
        </div>

        <div class="metric">
          <span class="label">CNY → BRL</span>
          <span class="value" id="cny_brl">--</span>
        </div>

        <div class="metric">
          <span class="label">RUB → BRL</span>
          <span class="value" id="rub_brl">--</span>
        </div>

        <div class="metric">
          <span class="label">ARS → BRL</span>
          <span class="value" id="ars_brl">--</span>
        </div>

      </div>
    </div>
  </div>

  <div id="cripto" class="tabcontent">
    <div class="card">
      <h2>Criptomoedas (BRL)</h2>
      <table>
        <thead>
          <tr>
            <th>Moeda</th>
            <th>Preço (BRL)</th>
            <th>Variação 24h (%)</th>
          </tr>
        </thead>
        <tbody id="crypto_table">
        </tbody>
      </table>
    </div>
  </div>
</div>

<footer>
  ESP32 • Open-Meteo • Exchangerate.host • CoinGecko • Mozilla Location Service
</footer>

<script>
function openTab(tabName, btn) {
  document.querySelectorAll('.tabcontent').forEach(el => el.style.display = 'none');
  document.getElementById(tabName).style.display = 'block';
  document.querySelectorAll('.tablink').forEach(b => b.classList.remove('active'));
  btn.classList.add('active');
}

function formatCrypto(value) {
  if (value < 0.01) {
    return value.toFixed(5);  // moedas muito pequenas
  }
  return value.toFixed(2);    // padrão
}

async function loadWeather() {
  try {
    const res = await fetch('/api/weather');
    const data = await res.json();
    document.getElementById('temp').textContent = data.temperature.toFixed(1) + ' °C';
    document.getElementById('humidity').textContent = data.humidity.toFixed(0) + ' %';
    document.getElementById('wind').textContent = data.wind.toFixed(1) + ' km/h';
    document.getElementById('lat').textContent = data.lat.toFixed(5);
    document.getElementById('lon').textContent = data.lon.toFixed(5);
    document.getElementById('elevation').textContent = data.elevation.toFixed(0) + ' m';
    document.getElementById("meteo_time").innerText = data.time;
    document.getElementById("meteo_interval").innerText = (data.interval / 60) + " min";
    document.getElementById("esp_time").innerText = data.esp_time;    
  } catch(e) {
    console.log('Erro weather', e);
  }
}

async function loadForex() {
  try {
    const res = await fetch('/api/forex');
    const data = await res.json();

    document.getElementById('usd_brl').textContent = data.usd_brl.toFixed(4);
    document.getElementById('eur_brl').textContent = data.eur_brl.toFixed(4);
    document.getElementById('gbp_brl').textContent = data.gbp_brl.toFixed(4);

    document.getElementById('jpy_brl').textContent = data.jpy_brl.toFixed(4);
    document.getElementById('cny_brl').textContent = data.cny_brl.toFixed(4);
    document.getElementById('rub_brl').textContent = data.rub_brl.toFixed(4);
    document.getElementById('ars_brl').textContent = data.ars_brl.toFixed(4);

  } catch(e) {
    console.log('Erro forex', e);
  }
}

async function loadCrypto() {
  try {
    const res = await fetch('/api/crypto');
    const data = await res.json();
    const tbody = document.getElementById('crypto_table');
    tbody.innerHTML = '';
    Object.keys(data).forEach(key => {
      const c = data[key];
      const tr = document.createElement('tr');
      const tdName = document.createElement('td');
      const tdPrice = document.createElement('td');
      const tdChange = document.createElement('td');

      tdName.textContent = c.name;
      tdPrice.textContent = formatCrypto(c.price_brl);

      const change = c.change_24h;
      tdChange.textContent = change.toFixed(2);
      tdChange.className = change >= 0 ? 'change-pos' : 'change-neg';

      tr.appendChild(tdName);
      tr.appendChild(tdPrice);
      tr.appendChild(tdChange);
      tbody.appendChild(tr);
    });
  } catch(e) {
    console.log('Erro crypto', e);
  }
}

function refreshAll() {
  loadWeather();
  loadForex();
  loadCrypto();
}

refreshAll();
setInterval(refreshAll, 60000);
</script>
</body>
</html>
)rawliteral";

Personalização do WiFiManager — wm_css.h

#pragma once

//-------------------------------------------
// Definição do Tema Dark para o WiFiManager
//-------------------------------------------

const char wm_css_dark[] PROGMEM = R"rawliteral(
<style>
/* ======== TEMA DARK PARA WIFIMANAGER ======== */

/* Fundo geral */
body {
    background-color: #121212 !important;
    color: #e0e0e0 !important;
    font-family: Arial, sans-serif;
}

/* Títulos */
h1, h2, h3 {
    color: #ffffff !important;
}

/* Inputs */
input[type="text"],
input[type="password"],
input[type="number"],
select {
    background-color: #1e1e1e !important;
    color: #ffffff !important;
    border: 1px solid #444 !important;
    padding: 8px !important;
    border-radius: 4px !important;
}

/* Botões */
button, input[type="submit"] {
    background-color: #333 !important;
    color: #fff !important;
    border: 1px solid #555 !important;
    padding: 10px 16px !important;
    border-radius: 4px !important;
    cursor: pointer;
}

button:hover, input[type="submit"]:hover {
    background-color: #444 !important;
}

/* Links */
a {
    color: #80bfff !important;
}

/* Separadores */
hr {
    border: 1px solid #333 !important;
}

/* ======== CORREÇÃO DA LISTA DE SSIDs ======== */

#networks,
.wifi,
.wifi li,
.ssid {
    display: block !important;
    visibility: visible !important;
    background-color: #1b1b1b !important;
    color: #ffffff !important;
    border: 1px solid #333 !important;
    padding: 10px !important;
    margin-bottom: 8px !important;
    border-radius: 4px !important;
}

/* Hover nos SSIDs */
.wifi li:hover {
    background-color: #2a2a2a !important;
}

/* Nome da rede */
.ssid span {
    color: #fff !important;
    font-weight: bold !important;
}

/* Força exibição da tabela de redes */
table {
    color: #fff !important;
    background-color: #1b1b1b !important;
    border: 1px solid #333 !important;
}

table td, table th {
    border: 1px solid #333 !important;
    padding: 6px !important;
}

/* Corrige o botão de scan */
#scan {
    background-color: #333 !important;
    color: #fff !important;
    border: 1px solid #555 !important;
}

/* Corrige o spinner de scan */
#scanning {
    color: #fff !important;
}

/* Painéis */
div {
    background-color: transparent !important;
}
/* ======== OCULTA CAMPOS INVISÍVEIS DO WIFIMANAGER ======== */
input[type="hidden"],
.wm_hidden,
#hidden,
input[name="modo"],
input[name="log"] {
    display: none !important;
    visibility: hidden !important;
    height: 0 !important;
    margin: 0 !important;
    padding: 0 !important;
    border: none !important;
}
</style>
)rawliteral";  

 

Conclusão

O desenvolvimento do Painel IoT de Clima, Moedas e Criptomoedas buscou demonstrar como a conectividade do ESP32 pode ser utilizada, não apenas para transmitir informações provenientes de sensores locais, mas também para acessar, processar e apresentar dados disponibilizados por diferentes serviços na Internet.

Ao longo do projeto, informações de naturezas bastante distintas foram reunidas em uma única aplicação. Os dados meteorológicos são obtidos através do Open-Meteo, as cotações de moedas são consultadas através da ExchangeRate-API e os valores das criptomoedas são fornecidos pelo CoinGecko. O ESP32 atua como elemento central dessa arquitetura, realizando as consultas, processando as respostas recebidas em formato JSON e disponibilizando as informações através de sua própria interface Web.

Além da função principal do painel, foram incorporados recursos importantes para tornar sua utilização mais prática. A configuração da rede e dos parâmetros da aplicação pode ser realizada através do navegador, as informações são preservadas na memória do dispositivo, o relógio é sincronizado através de um servidor NTP e o acesso ao equipamento pode ser realizado utilizando seu nome mDNS. A possibilidade de atualização do firmware através de OTA também facilita futuras alterações, principalmente depois que o equipamento estiver instalado em seu local de utilização.

A separação do código-fonte em arquivos específicos para o firmware principal, a interface Web e a personalização visual do WiFiManager contribui para manter o projeto organizado e facilita futuras modificações. Da mesma forma, a utilização de endpoints independentes para clima, moedas e criptomoedas cria uma estrutura que pode ser ampliada sem a necessidade de reformular toda a aplicação.

Uma das possibilidades de expansão é incorporar novos serviços Web e novos indicadores ao painel. Outras moedas, criptomoedas, índices econômicos ou diferentes informações meteorológicas poderiam ser acrescentados seguindo a mesma arquitetura utilizada nesta versão. A interface também pode receber novas abas, transformando o projeto gradualmente em um painel mais amplo de informações obtidas através da Internet.

Outra evolução possível é acrescentar dispositivos físicos ao ESP32. Um módulo GPS, por exemplo, poderia fornecer diretamente as coordenadas geográficas utilizadas nas consultas meteorológicas, dispensando o preenchimento manual da latitude e longitude. Sensores ambientais, como o BMP280, poderiam complementar as informações provenientes da Internet com medições realizadas no próprio local de instalação.

Essa combinação permitiria ainda comparar informações obtidas de serviços externos com dados medidos localmente. A temperatura fornecida por um serviço meteorológico, por exemplo, poderia ser apresentada juntamente com a temperatura registrada por um sensor conectado ao ESP32, criando novas possibilidades de experimentação e análise.

Dessa forma, o projeto apresentado não precisa ser visto apenas como um painel para consulta de clima, moedas e criptomoedas, mas também como uma base para o desenvolvimento de outras aplicações IoT nas quais o ESP32 atua como intermediário entre serviços disponíveis na Internet, dispositivos locais e uma interface Web acessível ao usuário.

Sobre o Autor


Alberto de Almeida Menezes
[email protected]

Bacharel em Engenharia de Áudio e Produção Musical pela Berklee College of Music.

Dailton de Oliveira Menezes
[email protected]

Bacharel em Ciência da Computação pela Universidade Federal de Minas Gerais.

Precisa dos componentes para este projeto?

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