

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.
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.
Qualquer modelo de ESP32 pode ser utilizado no projeto. Sugerimos alguns modelos a seguir:
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
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-MeteoOs 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¤t=temperature_2m,relative_humidity_2m,wind_speed_10m&timezone=autoOs 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-APIPara 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/BRLA 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 — CoinGeckoAs 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=trueA 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 WebA 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.
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 EquipamentoA 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 OTADepois 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:

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.
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 UtilizadasO 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.
//-------------------------------------------------------------------------------------------------
// 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¤t=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);
}
#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";
#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";
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.
|
|
Encontre tudo na Loja Eletrogate com frete grátis para compras acima de R$ 200