IoT

Crie sua API para Persistência de Dados do ESP32

Eletrogate 30 de outubro de 2024

Introdução

Atualmente, em eletrônica e projetos de Internet das Coisas (IoT), é comum gerar grandes quantidades de dados, como leituras de sensores e outros parâmetros. No entanto, muitos desses projetos enfrentam um desafio: não têm um método eficiente para armazenar esses dados de forma permanente. Isso torna difícil analisar as informações de maneira detalhada no futuro ou criar painéis de controle que ajudem a entender o comportamento dos dados.

Normalmente, os dados são armazenados temporariamente na memória do dispositivo, como o ESP32. Quando o sistema é desligado ou encerrado, esses dados são perdidos, embora seja possível visualizar as informações em tempo real enquanto o sistema está ativo.

Agora, imagine um cenário em que, em vez de apenas coletar dados enquanto o projeto está funcionando, você pudesse armazená-los em um local separado e acessível a qualquer momento. Isso abriria possibilidades para análises mais profundas e a criação de interfaces para visualizar e explorar esses dados. Com a prática de persistência de dados, podemos garantir que as informações sejam guardadas de forma que possam ser recuperadas e usadas mais tarde, mesmo após o término da execução do sistema.

Neste projeto, vamos explorar essa ideia. Iremos construir um circuito simples com o ESP32, que será responsável por enviar dados para armazenamento e análise futura. Além disso, desenvolveremos uma interface para visualizar esses dados em tempo real, acessar informações coletadas anteriormente e aplicar filtros para analisar detalhes como os maiores e menores valores. Também criaremos uma API para atuar como intermediária entre o circuito e a interface, facilitando a comunicação e o processamento dos dados.

Essa abordagem permitirá uma melhor compreensão dos dados e possibilitará o desenvolvimento de soluções baseadas em análises detalhadas.


Para Onde Vão os Dados Que Criamos? (Persistência De Dados)

Quando você se cadastra em um site, compra um produto online ou faz uma postagem nas redes sociais, para onde vão esses dados?

Todos os sistemas modernos mantêm um histórico das suas atividades. Por exemplo, no Facebook, você pode acessar postagens e fotos antigas de 2015. Na Amazon, você pode consultar seu histórico de compras. Tudo isso é possível graças ao conceito de persistência de dados.

Persistência de dados não é uma ferramenta específica, mas sim um conceito que é utilizado em diversas tecnologias e linguagens de programação. Em termos simples, persistência de dados significa guardar informações de forma que elas possam ser recuperadas e usadas mais tarde, mesmo depois que o programa ou aplicativo for fechado.

Quando você realiza uma ação em um sistema, como salvar uma foto ou fazer uma compra, esses dados são armazenados em algo chamado banco de dados. Esse armazenamento permite que o sistema lembre das suas atividades passadas e acesse essas informações quando necessário.

Portanto, a persistência de dados é essencial para manter registros e fornecer uma experiência contínua e personalizada em sites e aplicativos. É o que torna possível, por exemplo, encontrar suas fotos antigas ou consultar o histórico de compras ao fazer login em um serviço online.


Introdução ao Banco de dados (SQL NoSQL)

Durante muito tempo, com a evolução da tecnologia, sempre houve a necessidade de criar sistemas que permitissem o armazenamento de dados de maneira organizada. Nesse percurso, houve muitos avanços importantes que nos permitiram ter sistemas complexos capazes de armazenar milhões de dados. Hoje, após muita evolução, chegamos aos bancos de dados relacionais, que organizam os dados em tabelas e utilizam a Linguagem de Consulta Estruturada (SQL). O SQL é uma linguagem padrão para criação e manipulação de bancos de dados relacionais, sendo eficiente para trabalhar com grandes quantidades de dados complexos e inter-relacionados. Além disso, existem bancos de dados NoSQL, que lidam com dados não relacionais e são indicados para dados menos estruturados, menos complexos e em grandes volumes.

Banco de Dados Relacional

Um banco de dados relacional organiza os dados em várias tabelas. Cada tabela é composta por linhas e colunas, onde cada coluna representa um atributo (ou campo) e cada linha representa um registro. Os Sistemas de Gerenciamento de Banco de Dados Relacional (RDBMS) são softwares que gerenciam esses bancos de dados e permitem a criação, manipulação e consulta dos dados de forma eficiente.

Na imagem acima, temos uma ilustração de como seria um banco de dados relacional em um RDBMS. Nela, podemos visualizar dados relacionados ao endereço de um usuário, onde as linhas representam os registros e as colunas indicam quais atributos estão sendo armazenados.

SQL

SQL (Structured Query Language) é a linguagem padrão utilizada para trabalhar com bancos de dados relacionais. Com SQL, é possível criar e manipular tabelas, inserir, atualizar e excluir dados, entre outras operações. SQL permite uma interação eficaz com o banco de dados, facilitando a criação e gestão de estruturas de dados complexas.

Alguns comandos comuns em SQL incluem:

  • Criar banco de dados: CREATE DATABASE nome_do_banco;
  • Criar tabela: CREATE TABLE nome_da_tabela (coluna1 tipo, coluna2 tipo, ...);
  • Adicionar dados na tabela: INSERT INTO nome_da_tabela (coluna1, coluna2, ...) VALUES (valor1, valor2, ...);
  • Buscar dados na tabela: SELECT coluna1, coluna2 FROM nome_da_tabela WHERE condição;
  • Editar dados na tabela: UPDATE nome_da_tabela SET coluna1 = valor1 WHERE condição;
  • Deletar dados da tabela: DELETE FROM nome_da_tabela WHERE condição;
  • Deletar tabela: DROP TABLE nome_da_tabela;
  • Deletar banco de dados: DROP DATABASE nome_do_banco;

NoSQL

Os bancos de dados NoSQL (Not Only SQL) são uma categoria de sistemas de gerenciamento de banco de dados que não utilizam o modelo relacional. Dessa forma, é possível ter diversas formas de armazenamento, sendo as mais comuns os bancos de dados orientados a documentos, chave-valor, colunas e grafos. Cada formato possui seu banco mais recomendado para ser utilizado, com suas próprias limitações e regras. Os bancos de dados NoSQL são projetados para lidar com dados não estruturados e escalar horizontalmente de forma eficiente.

 


Introdução ao MongoDB

O MongoDB é um sistema de banco de dados orientado a documentos e de código aberto. Ele armazena dados em um formato semelhante ao JSON, conhecido como BSON (Binary JSON), e é classificado como um banco de dados NoSQL. Isso ocorre porque ele não utiliza o modelo relacional tradicional de tabelas, permitindo maior flexibilidade no armazenamento e na manipulação de dados.

O MongoDB oferece dois tipos de serviços: local e em nuvem (cloud). Ambos têm a mesma funcionalidade, mas o processo de configuração e utilização é diferente. Para acessar os dados de qualquer local e a qualquer hora, utilizaremos a opção em nuvem.

Ele será usado para armazenar os dados emitidos pelo nosso sistema no ESP32. Entretanto, para utilizar o MongoDB, é necessário seguir alguns passos para permitir a comunicação com ele através de outros sistemas. O primeiro passo é acessar o link indicado e criar uma conta ou, caso já tenha uma conta, fazer login.

link: https://www.mongodb.com/products/platform/atlas-database

Preparando o ambiente

Após criar sua conta ou fazer login, teremos acesso ao sistema, onde realizaremos algumas configurações.

Primeiro, vamos criar um novo projeto. Para isso, é necessário ir até a parte superior esquerda e clicar no ícone de uma pasta.

Será aberto um modal onde estarão todos os seus projetos já criados. Logo abaixo, haverá o botão “Novo Projeto” onde você deve clicar.

Você será redirecionado para uma área onde adicionaremos as informações do projeto. No nosso caso, será necessário apenas inserir o nome de sua escolha no campo indicado na imagem abaixo. Em seguida, clique no botão “Next” e depois em “Create Project”.

Criando o banco de dados

Assim, ao finalizar, seremos redirecionados para a área do projeto que criamos. Para garantir que estamos na pasta correta, verifique a indicação no canto superior esquerdo.

Estando tudo certo, vamos criar nosso cluster (banco de dados NoSQL) clicando no botão “Create”.

IMPORTANTE: Esta etapa é de extrema importância para evitar cobranças indevidas. É recomendado ter conhecimento prévio do sistema caso queira seguir um procedimento diferente.

Na imagem abaixo, temos o ambiente onde definiremos os parâmetros do nosso banco de dados. A primeira opção é o tamanho limite de armazenamento do banco de dados. Para o nosso projeto, a opção “Free” é mais do que suficiente, pois oferece um limite de 512 MB. Caso selecione outra opção que ofereça mais espaço ou que não indique ser gratuita, haverá uma cobrança. Portanto, recomendamos seguir os passos abaixo cuidadosamente.

Após selecionar o tamanho do banco, escolha um nome para ele, que pode ser totalmente à sua escolha. Em seguida, selecione o sistema de nuvem da AWS e a região de São Paulo. Para finalizar, clique no botão “Create Deployment”.

Assim que o processo anterior for finalizado, será aberto um modal onde deve ser informado o usuário e a senha do nosso banco de dados. No exemplo abaixo, foram utilizados “root” como nome de usuário e “root” como senha apenas para fins ilustrativos. Você deve escolher suas próprias credenciais de acordo com sua preferência. Para completar essa etapa, é necessário clicar no botão “Create Database User” e, após a confirmação, você pode fechar o modal clicando no botão “Close”.

IMPORTANTE: O nome de usuário e a senha são de grande importância para o desenvolvimento deste projeto. Portanto, recomendamos que você anote essas informações em um local seguro.

Acessando o Banco de Dados

Após realizar todos os passos até aqui, o banco de dados estará criado e pronto para acesso. Para isso, permaneça dentro do projeto e acesse a opção “Database” no menu lateral à esquerda.

Será aberta uma nova área onde você encontrará algumas informações sobre o banco de dados, como o modelo e o espaço consumido. Para visualizar o ambiente onde os dados estão armazenados, é necessário clicar no botão “Browse Collections”.

Na imagem abaixo, você pode ver o local onde os dados ficarão disponíveis após a criação arquivo e a inserção de dados. No momento, não há dados, pois ainda não adicionamos nenhuma informação. No entanto, essa etapa será abordada mais adiante, quando inserirmos os dados do ESP32. Você poderá visualizar esses dados neste ambiente conforme for necessário.

Pegando a String de Conexão

A string de conexão permite estabelecer a conexão com o banco de dados através de outros sistemas, possibilitando a manipulação dos dados armazenados nele. No nosso caso, utilizaremos essa string dentro da API.

Para obter a string de conexão, siga os passos abaixo. Ainda na tela inicial do Database, clique no botão “Connect”. Em seguida, será aberto um modal onde você deve selecionar a primeira opção chamada “Drivers”.

 

Em seguida, selecionaremos algumas opções específicas para o nosso projeto. Caso você esteja realizando o projeto em um cenário diferente, adapte esta parte ao seu contexto.

Primeiro, escolha o sistema com o qual iremos integrar a string de conexão. Para isso, selecione “Node.js” e a versão “2.2.12”.

Logo abaixo, você encontrará duas informações. A mais importante no momento é a string de conexão, que permitirá o acesso ao banco de dados que criamos anteriormente. Essa string deve ser copiada e armazenada em um local seguro. A outra informação é o comando para realizar o download da biblioteca no sistema, que não utilizaremos por agora, mas retornaremos a falar sobre ele em breve.


Desenvolvendo a interface

Para visualizar melhor os dados e analisá-los de uma forma mais concreta, vamos criar uma interface web onde poderemos visualizar os dados coletados pelo ESP32 em tempo real, além de acessar o histórico de dados após o desligamento do sistema ou para analisar dados antigos.

Para o desenvolvimento dessa interface, usaremos HTML e CSS para construir e estilizar os elementos da página, além de JavaScript para adicionar mecanismos que possibilitem a busca de dados em tempo real.

Para começar, é importante entender um pouco sobre HTML e CSS. Recomendamos ler nosso post “Introdução ao HTML e CSS” no blog através deste [link]. Também será necessário utilizar a ferramenta IDE Visual Studio Code (VSCode). Caso ainda não esteja instalada, recomendamos seguir as instruções no post deste [link], que também faz parte do nosso blog.

Primeiros Passos

  1. Criação de Arquivos e Pasta:
    • Crie uma pasta em um local de sua preferência.
    • Dentro dessa pasta, crie dois arquivos: index.html e script.js.
    • Abra o VSCode e selecione a pasta criada anteriormente.
    • Após a realização das etapas anteriores, o resultado deve ser algo semelhante à imagem abaixo.

  1. Estrutura dos Arquivos:
    • O index.html será responsável por construir a parte visual da nossa página, servindo como interface para interação com os dados emitidos pelo ESP32.
    • O script.js será responsável por realizar as requisições de dados na API. Ele solicitará os dados à API, que os buscará no banco de dados e os disponibilizará visualmente para nós. As requisições no JavaScript são realizadas através do método fetch. Após obter os dados, o JavaScript fará uma inserção dinâmica no HTML para construir a tabela com os dados adquiridos.

Detalhamento do código (Arquivo index.html)

<!DOCTYPE html>
<html lang="pt-BR">

<head>
    <meta charset="UTF-8">
    <!-- Define a codificação de caracteres como UTF-8 -->
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <!-- Configura a largura da página para ser igual à largura da tela do dispositivo -->
    <title>Dashboard - ESP32</title>
    <!-- Define o título da página -->
    <style>
        body { font-family: Arial, sans-serif; /* Define a fonte padrão da página */ margin: 0; /* Remove as margens padrão do corpo da página */ display: flex; /* Define que os elementos dentro do corpo serão organizados em um layout flexível */ background-color: #f5f5f5; /* Define a cor de fundo do corpo da página */ overflow: hidden; /* Oculta qualquer conteúdo que exceda os limites do corpo */ } .sidebar { width: 250px; /* Define a largura da barra lateral */ background-color: #333; /* Define a cor de fundo da barra lateral */ color: white; /* Define a cor do texto na barra lateral */ height: 100vh; /* Define a altura da barra lateral para ocupar toda a altura da janela de visualização */ display: flex; /* Organiza os elementos da barra lateral em um layout flexível */ flex-direction: column; /* Define que os elementos da barra lateral serão organizados em coluna */ align-items: flex-start; /* Alinha os elementos ao início do contêiner da barra lateral */ border-radius: 0 45px 45px 0; /* Define bordas arredondadas na barra lateral */ transition: width 0.3s ease; /* Adiciona uma transição suave ao alterar a largura da barra lateral */ overflow: hidden; /* Oculta qualquer conteúdo que exceda os limites da barra lateral */ padding-top: 20px; /* Adiciona preenchimento superior dentro da barra lateral */ } .sidebar.collapsed { width: 60px; /* Define a largura da barra lateral quando ela está colapsada */ } .sidebar h2 { font-size: 1.5em; /* Define o tamanho da fonte para o título dentro da barra lateral */ margin: 0 0 20px 20px; /* Adiciona margem ao redor do título */ } .sidebar a, .sidebar button { padding: 10px 15px; /* Adiciona preenchimento dentro dos links e botões */ margin: 5px 20px; /* Adiciona margem ao redor dos links e botões */ text-decoration: none; /* Remove a decoração de texto (como sublinhado) dos links */ color: white; /* Define a cor do texto dos links e botões */ background: none; /* Remove o fundo padrão dos links e botões */ border: none; /* Remove a borda padrão dos links e botões */ cursor: pointer; /* Muda o cursor para indicar que o elemento é clicável */ text-align: left; /* Alinha o texto à esquerda dentro dos links e botões */ width: 100%; /* Define a largura dos links e botões para ocupar 100% do contêiner */ font-size: 1em; /* Define o tamanho da fonte dos links e botões */ transition: background-color 0.3s; /* Adiciona uma transição suave para a mudança de cor de fundo */ border-radius: 10px 0px 0px 10px; /* Define bordas arredondadas para links e botões */ } .sidebar a:hover, .sidebar button:hover { background-color: #5B9BD5; /* Define a cor de fundo ao passar o mouse sobre links e botões */ box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); /* Adiciona uma sombra ao passar o mouse sobre links e botões */ } .content { flex-grow: 1; /* Permite que a área de conteúdo cresça para ocupar o espaço disponível */ padding: 30px; /* Adiciona preenchimento ao redor da área de conteúdo */ background-color: #f5f5f5; /* Define a cor de fundo da área de conteúdo */ } .filter-section { display: none; /* Oculta a seção de filtro por padrão */ margin-bottom: 20px; /* Adiciona margem inferior à seção de filtro */ background-color: #ffffff; /* Define a cor de fundo da seção de filtro */ padding: 10px; /* Adiciona preenchimento dentro da seção de filtro */ border-radius: 10px; /* Define bordas arredondadas para a seção de filtro */ box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); /* Adiciona uma sombra à seção de filtro */ } table { width: 100%; /* Define a largura da tabela para ocupar 100% do contêiner */ border-collapse: collapse; /* Remove o espaçamento entre as células da tabela */ background-color: #fff; /* Define a cor de fundo da tabela */ border-radius: 10px; /* Define bordas arredondadas para a tabela */ overflow: hidden; /* Oculta qualquer conteúdo que exceda os limites da tabela */ box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); /* Adiciona uma sombra à tabela */ } th,td { border: 1px solid #ddd; margin: 50PX; padding: 10px; text-align: center; width: 150px; /* Alinhar texto ao centro */ } th { background-color: #5B9BD5; /* Define a cor de fundo para as células de cabeçalho da tabela */ color: white; /* Define a cor do texto nas células de cabeçalho da tabela */ } .areaTabela { max-height: 550px; /* Define a altura máxima para a área da tabela com rolagem */ overflow-y: auto; /* Adiciona rolagem vertical para a área da tabela */ } h1 { font-size: 2em; /* Define o tamanho da fonte para os títulos principais */ margin-bottom: 20px; /* Adiciona margem inferior aos títulos principais */ color: #333; /* Define a cor do texto para os títulos principais */ } @media (max-width: 768px) { .sidebar { width: 100%; /* Define a largura da barra lateral para ocupar toda a largura da tela em dispositivos menores */ height: auto; /* Define a altura da barra lateral como automática em dispositivos menores */ position: static; /* Define a posição da barra lateral como estática em dispositivos menores */ border-radius: 0; /* Remove bordas arredondadas em dispositivos menores */ } .sidebar.collapsed { width: 100%; /* Define a largura da barra lateral como 100% quando colapsada em dispositivos menores */ } }
    </style>
</head>

<body>
    <div class="sidebar" id="sidebar">
        <!-- Define a barra lateral e atribui um ID -->
        <h2>Eletrogate</h2>
        <!-- Título da barra lateral -->
        <button onclick="showData('atual')">Dados Atual</button>
        <!-- Botão para mostrar dados atuais -->
        <button onclick="showData('dashboard')">Dashboard</button>
        <!-- Botão para mostrar o dashboard -->
    </div>
    <div class="content">
        <!-- Define a área de conteúdo principal -->
        <div id="dashboard-content">
            <!-- Contêiner para o conteúdo do dashboard -->
            <h1 id="tituloPag">Dashboard</h1>
            <!-- Título da página -->
            <div class="filter-section">
                <!-- Seção para filtros de dados -->
                <select id="filterSelect">
                    <!-- Dropdown para selecionar tipo de filtro -->
                    <option value="maiorTemp">Maiores valores emitidos - Temperatura</option>
                    <!-- Opção para filtrar por maiores valores -->
                    <option value="menorTemp">Menores valores emitidos - Temperatura</option>
                    <!-- Opção para filtrar por menores valores -->
                    <option value="maioUmi">Maiores valores emitidos - Umidade</option>
                    <!-- Opção para filtrar por maiores valores -->
                    <option value="menorUmi">Menores valores emitidos - Umidade</option>
                    <!-- Opção para filtrar por menores valores -->
                </select>
                <button onclick="applyFilters()">Filtrar</button>
                <!-- Botão para aplicar filtros -->
                <br>
                <input type="date" id="filterDate">
                <!-- Campo para filtrar por data -->
                <button onclick="applyFiltersDate()">Filtrar Data</button>
                <!-- Botão para aplicar filtro por data -->
                <br>
                <input type="time" id="filterTime">
                <!-- Campo para filtrar por hora -->
                <button onclick="applyFiltersTime()">Filtrar</button>
                <!-- Botão para aplicar filtro por hora -->
                <br>
                <button onclick="updateTable(true)">Atualizar tabela</button>
                <!-- Botão para atualizar a tabela de dados -->
            </div>
        </div>
        <div id="dataTableContainer">
            <!-- Contêiner para a tabela de dados -->
            <table>
                <thead>
                    <tr>
                        <th>Data</th>
                        <!-- Cabeçalho da coluna para data -->
                        <th>Hora</th>
                        <!-- Cabeçalho da coluna para hora -->
                        <th>Temperatura</th>
                        <!-- Cabeçalho da coluna para valor -->
                        <th>Umidade</th>
                        <!-- Cabeçalho da coluna para valor -->
                    </tr>
                </thead>
            </table>
            <div class="areaTabela">
                <!-- Área com rolagem para a tabela de dados -->
                <table id="dataTable">
                    <!-- Tabela de dados -->
                    <tbody> </tbody>
                </table>
            </div>
        </div>
    </div>
    <script src="script.js"></script>
    <!-- Importa o arquivo JavaScript para interação -->
</body>

</html>

Escrevendo o código (Arquivo script.js)

let dataGlobal = [];

let currentView = 'dashboard';
let data = []

Primeiro, criamos variáveis responsáveis por armazenar os dados em tempo de execução. dataGlobal mantém todos os dados recebidos, currentView armazena a visão atual da interface (como ‘dashboard’ ou ‘atual’), e data é usada para armazenar os dados filtrados ou ordenados que serão exibidos na tabela.

function showData(type) {
    const tituloPagina = document.getElementById("tituloPag");
    tituloPagina.textContent = type === "atual" ? "Medições Emitida pelo ESP32" : "Dashboard - Histórico";

    currentView = type;

    switch (type) {
        case 'dashboard':
            addNewItem();
            data = dataGlobal;
            filterSection.style.display = 'block';
            break;
        case 'atual':
            data = dataGlobal
            .sort((a, b) => b.id - a.id);
            filterSection.style.display = 'none';
            break;
        default:
            break;
    }

    generateTable(data);
}

Na função showData, definimos o título da página com base no tipo de visão selecionada. A variável currentView é atualizada com o tipo de visão atual, e o conteúdo da tabela é ajustado conforme a visão escolhida. No caso do tipo ‘dashboard’, a função addNewItem é chamada para atualizar os dados, e a seção de filtros é exibida. Já no tipo ‘atual’, os dados são ordenados em ordem decrescente com base no id, e a seção de filtros é ocultada.

function addNewItem() {
    fetch('http://localhost:3000/infoEsp/all', {
        method: 'GET',
        headers: {
            'Content-Type': 'application/json'
        }
    })
    .then(response => response.json())
    .then(data => {
        dataGlobal = data;
        switch (currentView) {
            case 'atual':
                filteredData = data
                .sort((a, b) => b.id - a.id);
                generateTable(filteredData);
                break;
            default:
                break;
        }
    })
    .catch(error => {
        console.error('Erro:', error);
    });

A função addNewItem faz uma requisição GET para buscar todos os dados armazenados no servidor e atualiza a variável dataGlobal com os novos dados recebidos. Se a visão atual for ‘atual’, os dados são ordenados e a tabela é gerada novamente com os dados filtrados. Caso ocorra algum erro durante a requisição, ele é capturado e exibido no console.

function generateTable(data) {
    const tableBody = document.querySelector('#dataTable tbody');
    tableBody.innerHTML = '';

    data.forEach(item => {
        const row = document.createElement('tr');

        // Formatação da data e da hora
        const itemDate = new Date(item.data);
        const date = itemDate.toLocaleDateString();
        const time = itemDate.toLocaleTimeString();

        const dateCell = document.createElement('td');
        dateCell.textContent = date;
        row.appendChild(dateCell);

        const timeCell = document.createElement('td');
        timeCell.textContent = time;
        row.appendChild(timeCell);

        const dataCellTemp = document.createElement('td');
        dataCellTemp.textContent = `${item.temperatura}°C`;
        row.appendChild(dataCellTemp);

        const dataCellUmi = document.createElement('td');
        dataCellUmi.textContent = `${item.umidade}%`;
        row.appendChild(dataCellUmi);

        tableBody.appendChild(row);
    });
}

A função generateTable recebe os dados como parâmetro e gera dinamicamente as linhas da tabela HTML. Cada linha é criada com base nas informações de data, hora, temperatura e umidade de cada item, que são formatadas e inseridas nas células da tabela. A tabela é atualizada para exibir os dados mais recentes.

function applyFilters() {
    const filterSelect = document.getElementById('filterSelect').value;
    let filteredData = [...dataGlobal];

    if (filterSelect === 'maiorTemp') {
        filteredData.sort((a, b) => b.temperatura - a.temperatura);
    } else if (filterSelect === 'menorTemp') {
        filteredData.sort((a, b) => a.temperatura - b.temperatura);
    } else if (filterSelect === 'maioUmi') {
        filteredData.sort((a, b) => b.umidade - a.umidade);
    } else if (filterSelect === 'menorUmi') {
        filteredData.sort((a, b) => a.umidade - b.umidade);
    }

    generateTable(filteredData);
}

A função applyFilters é responsável por aplicar filtros específicos aos dados da tabela, com base na opção selecionada pelo usuário. Os dados podem ser ordenados por maior ou menor temperatura, ou por maior ou menor umidade, e em seguida, a tabela é atualizada com os dados filtrados.

function applyFiltersDate() {
    const filterDate = new Date(document.getElementById('filterDate').value);
    if (isNaN(filterDate.getTime())) {
        alert('Por favor, selecione uma data válida.');
        return;
    }

    const filteredData = dataGlobal.filter(item => {
        const itemDate = new Date(item.data.split(" ")[0]);
        return itemDate.toDateString() === filterDate.toDateString();
    });

    generateTable(filteredData);
}

A função applyFiltersDate filtra os dados com base na data selecionada pelo usuário. Se a data não for válida, um alerta é exibido. Os dados são filtrados para incluir apenas aqueles que correspondem à data selecionada, e a tabela é atualizada com esses dados.

function applyFiltersTime() {
    const filterTime = document.getElementById('filterTime').value;

    if (!filterTime) {
        alert('Por favor, selecione uma hora válida.');
        return;
    }

    const [filterHours, filterMinutes] = filterTime.split(':').map(Number);

    const filteredData = dataGlobal.filter(item => {
        const itemDate = new Date(item.data);
        return itemDate.getHours() === filterHours && itemDate.getMinutes() === filterMinutes;
    });

    generateTable(filteredData);
}

A função applyFiltersTime permite que os dados sejam filtrados com base na hora selecionada. Se uma hora inválida for selecionada, o usuário é alertado. Os dados são filtrados para incluir apenas os registros que correspondem à hora selecionada, e a tabela é atualizada com os dados filtrados.

function updateTable(varify) {
    if (varify == true) {
        generateTable(dataGlobal);
    }
}

A função updateTable é usada para atualizar a tabela com os dados globais se a condição varify for verdadeira. Isso garante que a tabela exiba os dados mais recentes ou filtrados, conforme necessário.

setInterval(addNewItem, 1000);

window.onload = () => {
    showData('dashboard');
};

O setInterval é utilizado para chamar a função addNewItem a cada segundo, garantindo que a tabela seja atualizada automaticamente com os dados mais recentes do servidor. No evento window.onload, a função showData é chamada com o parâmetro ‘dashboard’ para configurar a visão inicial da interface como o dashboard.

Resultado

// Arrays de dados
let dataGlobal = [];   // Array para armazenar dados globais que serão recuperados do servidor

// Variável para armazenar o tipo de visualização atual
let currentView = 'dashboard'; // Define a visualização inicial como 'dashboard'
let data = []; // Array para armazenar dados filtrados para visualização

// Função para adicionar um novo item, recuperando dados do servidor
function addNewItem() {
    fetch('http://localhost:3000/infoEsp/all', {
        method: 'GET', // Método HTTP para recuperar dados (GET neste caso)
        headers: {
            'Content-Type': 'application/json' // Define o tipo de conteúdo como JSON
        }
    })
    .then(response => response.json()) // Converte a resposta da requisição para JSON
    .then(data => {
        dataGlobal = data; // Atualiza o array global com os dados recebidos
        switch (currentView) {
            case 'atual':
                filteredData = data
                .sort((a, b) => b.id - a.id); // Ordena os dados por ID, do maior para o menor
                generateTable(filteredData); // Gera a tabela com os dados filtrados
                break;
            default:
                //console.log("Nenhuma visualização correspondente encontrada."); // Comentado para depuração
                break;
        }
    })
    .catch(error => {
        console.error('Erro:', error); // Exibe qualquer erro que ocorra durante a requisição
    });
}

// Função para mostrar dados com base no tipo de visualização
function showData(type) {
    const filterSection = document.querySelector('.filter-section'); // Seleciona a seção de filtros

    const tituloPagina = document.getElementById("tituloPag"); // Seleciona o elemento do título da página
    tituloPagina.textContent = type === "atual" ? "Medições Emitida pelo ESP32" : "Dashboard - Histórico"; // Atualiza o título da página com base no tipo

    currentView = type; // Atualiza a visualização atual

    switch (type) {
        case 'dashboard':
            addNewItem(); // Adiciona novos itens para a visualização do dashboard
            data = dataGlobal; // Define os dados para a visualização do dashboard
            filterSection.style.display = 'block'; // Exibe a seção de filtros
            break;
        case 'atual':
            data = dataGlobal
            .sort((a, b) => b.id - a.id); // Ordena os dados por ID, do maior para o menor
            filterSection.style.display = 'none'; // Oculta a seção de filtros
            break;
        default:
            break;
    }

    generateTable(data); // Gera a tabela com os dados para a visualização atual
}

// Define um intervalo para atualizar os dados a cada 60 segundos
setInterval(addNewItem, 60000);

// Função para gerar a tabela com dados fornecidos
function generateTable(data) {
    const tableBody = document.querySelector('#dataTable tbody'); // Seleciona o corpo da tabela
    tableBody.innerHTML = ''; // Limpa o corpo da tabela antes de adicionar novos dados
    
    data.forEach(item => { // Itera sobre cada item de dados
        const row = document.createElement('tr'); // Cria uma nova linha para a tabela

        // Formatação da data e da hora
        const itemDate = new Date(item.data); // Converte a string de data em um objeto Date
        const date = itemDate.toLocaleDateString(); // Obtém a data em formato local
        const time = itemDate.toLocaleTimeString(); // Obtém a hora em formato local

        const dateCell = document.createElement('td'); // Cria uma célula para a data
        dateCell.textContent = date; // Define o conteúdo da célula como a data
        row.appendChild(dateCell); // Adiciona a célula à linha

        const timeCell = document.createElement('td'); // Cria uma célula para a hora
        timeCell.textContent = time; // Define o conteúdo da célula como a hora
        row.appendChild(timeCell); // Adiciona a célula à linha

        const dataCellTemp = document.createElement('td'); // Cria uma célula para a temperatura
        dataCellTemp.textContent = `${item.temperatura}C`; // Define o conteúdo da célula como a temperatura com unidade
        row.appendChild(dataCellTemp); // Adiciona a célula à linha

        const dataCellUmi = document.createElement('td'); // Cria uma célula para a umidade
        dataCellUmi.textContent = `${item.umidade}%`; // Define o conteúdo da célula como a umidade com unidade
        row.appendChild(dataCellUmi); // Adiciona a célula à linha

        tableBody.appendChild(row); // Adiciona a linha ao corpo da tabela
    });
}

// Função para aplicar filtros gerais baseados em seleção
function applyFilters() {
    const filterSelect = document.getElementById('filterSelect').value; // Obtém o valor selecionado no filtro
    let filteredData = [...dataGlobal]; // Cria uma cópia dos dados globais

    if (filterSelect === 'maiorTemp') {
        // Ordena de forma decrescente pela temperatura (maior para menor)
        filteredData.sort((a, b) => b.temperatura - a.temperatura);
    } else if (filterSelect === 'menorTemp') {
        // Ordena de forma crescente pela temperatura (menor para maior)
        filteredData.sort((a, b) => a.temperatura - b.temperatura);
    } else if (filterSelect === 'maiorUmi') {
        // Ordena de forma decrescente pela umidade (maior para menor)
        filteredData.sort((a, b) => b.umidade - a.umidade);
    } else if (filterSelect === 'menorUmi') {
        // Ordena de forma crescente pela umidade (menor para maior)
        filteredData.sort((a, b) => a.umidade - b.umidade);
    }

    generateTable(filteredData); // Gera a tabela com os dados filtrados
}

// Função para aplicar filtro de data
function applyFiltersDate() {
    const filterDate = new Date(document.getElementById('filterDate').value); // Obtém a data do input e converte para objeto Date
    if (isNaN(filterDate.getTime())) {
        alert('Por favor, selecione uma data válida.'); // Alerta se a data não for válida
        return;
    }

    const filteredData = dataGlobal.filter(item => {
        const itemDate = new Date(item.data.split(" ")[0]); // Converte a string de data do item para objeto Date, apenas a parte da data
        return itemDate.toDateString() === filterDate.toDateString(); // Compara apenas a parte da data
    });

    generateTable(filteredData); // Gera a tabela com os dados filtrados por data
}

// Função para aplicar filtro de hora
function applyFiltersTime() {
    const filterTime = document.getElementById('filterTime').value; // Obtém a hora do input

    if (!filterTime) {
        alert('Por favor, selecione uma hora válida.'); // Alerta se a hora não for válida
        return;
    }

    const [filterHours, filterMinutes] = filterTime.split(':').map(Number); // Divide a hora em horas e minutos e converte para número

    const filteredData = dataGlobal.filter(item => {
        const itemDate = new Date(item.data); // Converte a string de data do item para objeto Date
        return itemDate.getHours() === filterHours && itemDate.getMinutes() === filterMinutes; // Compara hora e minuto
    });

    generateTable(filteredData); // Gera a tabela com os dados filtrados por hora
}

// Função para atualizar a tabela, se a condição for verdadeira
function updateTable(varify){
    if(varify == true){
        generateTable(dataGlobal); // Gera a tabela com os dados globais
    }
}

// Inicializa a visualização com 'Dashboard' quando a página é carregada
window.onload = () => {
    showData('dashboard'); // Inicializa com a visualização 'Dashboard'
};

Construindo a API e os endpoints:

Nesta etapa, vamos desenvolver a parte mais importante, pois é ela que permitirá a comunicação entre os sistemas de forma simples e eficaz. Portanto, a API que vamos desenvolver será simples, mas poderá ser expandida para incluir muitas outras rotas conforme necessário para o projeto. Entretanto, no nosso projeto ela terá duas rotas principais: a primeira será utilizada pelo ESP32 para enviar os dados coletados e realizar a persistência desses dados no banco de dados, enquanto a segunda será responsável por buscar os dados salvos no banco e enviá-los para a nossa interface.

Para melhor compreensão, recomendamos a leitura prévia do post em nosso blog sobre o desenvolvimento de APIs, que pode ser acessado através deste [link]

Abaixo, temos duas imagens que ilustram o comportamento do tráfego de dados (as imagens são meramente ilustrativas e não contêm termos técnicos).

Endpoints

  • Salvar Dados no banco
    • Método HTTP: Post
Request Body
{
    temp: ""
    umi : ""
    date: ""
}
Response
{
    Status: 200
}
  • Buscar todos dados
    • Método HTTP: GET

Response
    [{
        temp: ""
        umi : ""
        date: ""
     }]
  • Buscar mais recente dados
    • Método HTTP: GET

Response
{
    temp: ""
    umi : ""
    date: ""
}

Primeiros passos

É necessário ter previamente instalado e configurado o Node.js e o VSCode. Caso precise de ajuda nessa etapa, você pode seguir o tutorial neste [link]. Após isso, devemos seguir os passos a seguir:

Primeiro, devemos criar uma pasta em um local de sua escolha e abrir essa pasta dentro do VSCode.

Após os passos acima, com o VSCode aberto, devemos verificar se realmente estamos dentro da pasta correta. No canto superior esquerdo, temos o nome da pasta em que estamos, que deve ser o mesmo nome da pasta criada anteriormente. Com a confirmação da pasta, devemos abrir o terminal. Para isso, podemos ir no menu superior horizontal e selecionar Terminal → Novo Terminal. Assim, será aberta uma nova área na parte inferior da tela, onde podemos também confirmar o caminho de onde estamos.

Agora devemos inserir os comandos na linha dentro do terminal. Eles criarão os arquivos necessários para que possamos começar a escrever o código e assim construir a API. Lembrando que os comandos devem ser executados um por vez e não todos juntos.

Comandos

npm init -y
npm install -D typescript @types/node @types/express ts-node-dev @types/mongoose
npm install express mongoose date-fns cors
npx tsc --init
mkdir src

Importante: O Mongoose é uma biblioteca que serve como uma ferramenta de Object Data Modeling (ODM) para MongoDB e Node.js. O Mongoose é específico para o MongoDB, que é um banco de dados NoSQL. Com o Mongoose, é possível definir esquemas que mapeiam documentos MongoDB para objetos JavaScript, o que facilita a manipulação e a validação dos dados. Ele permite que você crie modelos (ou entidades) onde você define as características desses modelos, como tipos de dados, valores padrão, validações, entre outros. Dessa forma, o Mongoose ajuda o desenvolvedor a entender melhor como realizar consultas e manipular dados no banco de dados, tornando o processo mais estruturado e seguro.

Resultado

Logo em seguida, devemos executar mais três passos muito importantes. O primeiro é criar três arquivos dentro da pasta src. Podemos criar os arquivos clicando com o botão direito na pasta e selecionando “New File”, e então colocando os seguintes nomes: index.ts, entidade.ts, db.ts, counter.ts. Ao final, devemos ter algo semelhante ao que segue:

Após realizar o passo anterior, devemos acessar o arquivo tsconfig.json e substituir o código existente pelo código abaixo:

{
"compilerOptions": {
"target": "ES6",
"module": "commonjs",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
    }
}

Agora, para finalizar a etapa de configuração, devemos acessar o arquivo package.json e, na área de scripts, substituir o código existente pelo código abaixo:

"scripts": {
    "start": "node dist/index.js",
    "build": "tsc",
    "dev": "npm run build && npm start"
  },

Código-fonte

Agora que finalizamos todas as configurações básicas do nosso projeto, podemos dar início à escrita do código principal, que será nossa API.

Detalhamento do Codigo

Arquivo db.ts

const mongoose = require("mongoose");
// Importa o módulo mongoose, que é uma biblioteca de modelagem de objetos do MongoDB para Node.js.

const connectionString = 'YOUR_MONGODB_ATLAS_CONNECTION_STRING';
// Define a string de conexão para o banco de dados MongoDB Atlas. 
// Substitua 'YOUR_MONGODB_ATLAS_CONNECTION_STRING' pela sua string de conexão real.

const connectDB = async () => {
  // Define uma função assíncrona chamada connectDB, que será usada para conectar ao banco de dados MongoDB.
  
  try {
    await mongoose.connect(connectionString);
    // Tenta conectar ao banco de dados usando a string de conexão fornecida.
    // O método mongoose.connect() retorna uma Promise, por isso usamos await para aguardar a conclusão da conexão.

    console.log('MongoDB connected');
    // Se a conexão for bem-sucedida, imprime 'MongoDB connected' no console.
    
  } catch (err) {
    console.error(err);
    // Se ocorrer um erro durante a conexão, o erro é capturado e impresso no console.
    
    process.exit(1);
    // Encerra o processo Node.js com um código de erro 1, indicando que ocorreu um erro fatal.
  }
};

export default connectDB;
// Exporta a função connectDB como o módulo padrão, para que possa ser importada e utilizada em outras partes da aplicação.

Na primeira linha, importamos a biblioteca que baixamos, a qual permitirá a comunicação com o banco de dados. Utilizando o método connect, passaremos nossa string de conexão, que foi obtida ao seguir as instruções para usar o MongoDB Atlas.

Portanto, o código acima tentará se conectar ao banco de dados utilizando a string de conexão do MongoDB Atlas. Se a conexão for bem-sucedida, será exibida a mensagem “MongoDB connected” no terminal; caso contrário, o erro será exibido.

Codigo sobre o arquivo counter.ts

counter.ts

No arquivo counter.ts, importamos mongoose, Document e Schema do pacote mongoose para definir e manipular a estrutura dos dados no MongoDB. Criamos uma interface TypeScript chamada ICounter que descreve os documentos da coleção Counter, incluindo um campo name do tipo string e um campo seq do tipo number.

Definimos o esquema CounterSchema para a coleção Counter, com o campo name obrigatório e único, e o campo seq com valor padrão 0. Usamos esse esquema para criar o modelo Counter com o método mongoose.model(), permitindo a manipulação dos documentos da coleção.

Por fim, exportamos o modelo Counter, tornando-o disponível para uso em outras partes da aplicação.

Código do arquivo Entidade.ts

entidade.ts

No arquivo Entidade.ts, importamos mongoose, Document e Schema do pacote mongoose, e também o modelo Counter de um arquivo local para gerar IDs sequenciais. Definimos a interface TypeScript IInfoEsp para documentos na coleção InfoEsp, que inclui campos para id, temperatura, umidade, e data.

Criamos o esquema InfoEspSchema, que define a estrutura dos documentos, incluindo campos obrigatórios para temperatura e umidade, e um campo data que é preenchido automaticamente. Adicionamos um middleware pré-save que incrementa o campo id usando o modelo Counter para garantir IDs únicos e sequenciais.

Finalmente, criamos o modelo InfoEsp baseado no InfoEspSchema e o exportamos como o módulo padrão, tornando-o disponível para uso em outras partes da aplicação.

Código do arquivo index.ts

import express, { Application, Request, Response } from 'express';
// Importa o express, que é um framework para construir APIs em Node.js, 
// e os tipos Application, Request e Response do express para tipagem no TypeScript.

import connectDB from './db';
// Importa a função connectDB do arquivo db, que será usada para conectar ao banco de dados MongoDB.

import InfoEsp from './entidade';
// Importa o modelo InfoEsp do arquivo entidade, que será usado para manipular dados da coleção InfoEsp.

import { format } from 'date-fns';
// Importa a função format do pacote date-fns, usada para formatar datas.

const cors = require('cors');
// Importa o pacote cors, que permite configurar políticas de CORS (Cross-Origin Resource Sharing).

const app: Application = express();
// Cria uma instância do aplicativo express.

app.use(cors());
// Configura o aplicativo para usar o middleware CORS, permitindo requisições de outras origens.

connectDB();
// Conecta ao banco de dados MongoDB chamando a função connectDB.

app.use(express.json());
// Configura o aplicativo para usar o middleware express.json(), 
// permitindo que ele entenda requisições com corpo em formato JSON.

let formattedDateFns;
// Declara uma variável para armazenar a data formatada.

type InfoEspResumo = {
  id: string;
  umidade: number;
  temperatura: number;
  data: Date;
};
// Define um tipo TypeScript chamado InfoEspResumo para tipar as respostas enviadas nas rotas. 
// Ele especifica que cada objeto deve ter os campos id, umidade, temperatura e data.

app.post('/infoEsp', async (req: Request, res: Response) => {
  // Define uma rota POST em '/infoEsp'. 
  // Essa rota é usada para cadastrar novas informações de temperatura e umidade.

  const { temperatura, umidade } = req.body;
  // Desestrutura os valores de temperatura e umidade do corpo da requisição.

  formattedDateFns = format(new Date(), 'yyyy-MM-dd HH:mm:ss');
  // Formata a data atual no formato 'yyyy-MM-dd HH:mm:ss' e armazena na variável formattedDateFns.

  try {
    const newInfoEsp = new InfoEsp({
      temperatura,
      umidade,
      data: formattedDateFns
    });
    // Cria uma nova instância do modelo InfoEsp com os dados recebidos e a data formatada.

    const infoEsp = await newInfoEsp.save();
    // Salva o novo documento no banco de dados.

    const result: InfoEspResumo = {
      id: infoEsp.id,
      umidade: infoEsp.umidade,
      temperatura: infoEsp.temperatura,
      data: infoEsp.data
    };
    // Cria um objeto result com os dados retornados pelo banco, tipado como InfoEspResumo.

    res.status(200).json(result);
    // Retorna o objeto result como resposta JSON com status 200 (OK).
  } catch (err) {
    console.error(err);
    // Em caso de erro, o erro é impresso no console.

    res.status(500).send('Erro no servidor');
    // Retorna um erro 500 (Erro Interno do Servidor) como resposta.
  }
});

app.get('/infoEsp/all', async (req: Request, res: Response) => {
  // Define uma rota GET em '/infoEsp/all'. 
  // Essa rota é usada para buscar todas as informações de temperatura e umidade.

  try {
    const infos = await InfoEsp.find().sort({ data: 1 });
    // Busca todos os documentos na coleção InfoEsp e os ordena por data de forma crescente.

    const result: InfoEspResumo[] = infos.map(info => ({
      id: info.id,
      umidade: info.umidade,
      temperatura: info.temperatura,
      data: info.data
    }));
    // Mapeia os documentos encontrados para um array de objetos InfoEspResumo.

    res.json(result);
    // Retorna o array result como resposta JSON.
  } catch (err) {
    console.error(err);
    // Em caso de erro, o erro é impresso no console.

    res.status(500).send('Erro no servidor');
    // Retorna um erro 500 (Erro Interno do Servidor) como resposta.
  }
});

app.get('/infoEsp', async (req: Request, res: Response) => {
  // Define uma rota GET em '/infoEsp'. 
  // Essa rota é usada para buscar a informação mais recente de temperatura e umidade.

  try {
    const infoEsp = await InfoEsp.findOne().sort({ id: -1 });
    // Busca o documento mais recente na coleção InfoEsp, baseado no maior id.

    if (!infoEsp) {
      return res.status(404).send('Nenhum dado encontrado');
      // Se nenhum documento for encontrado, retorna um erro 404 (Não Encontrado).
    }

    const result: InfoEspResumo = {
      id: infoEsp.id,
      umidade: infoEsp.umidade,
      temperatura: infoEsp.temperatura,
      data: infoEsp.data
    };
    // Cria um objeto result com os dados retornados pelo banco, tipado como InfoEspResumo.

    res.json(result);
    // Retorna o objeto result como resposta JSON.
  } catch (err) {
    console.error(err);
    // Em caso de erro, o erro é impresso no console.

    res.status(500).send('Erro no servidor');
    // Retorna um erro 500 (Erro Interno do Servidor) como resposta.
  }
});

const PORT = process.env.PORT || 3000;
// Define a porta na qual o servidor irá escutar, utilizando a variável de ambiente PORT, 
// ou 3000 caso a variável não esteja definida.

app.listen(PORT, () => {
  console.log(`Servidor rodando na porta ${PORT}`);
  // Inicia o servidor e exibe uma mensagem no console indicando em qual porta ele está rodando.
});


Criando o script no esp32 e Montagem circuito

Agora que já concluímos a preparação de quase todos os sistemas necessários, vamos trabalhar na parte de hardware, onde será feita toda a coleta de dados e envio para os sistemas desenvolvidos anteriormente.

Montando Circuito

  1. Lista de componentes
    • ESP32
    • Protobord
    • Sensor DHT11
    • 2 LED
    • 2 Resistor 220ohm
    • Jumper
  2. Diagrama do circuito

  1. Montar o circuito do ESP32 com o sensor de temperatura
    • VCC do DHT113.3V do ESP32
    • GND do DHT11GND do ESP32
    • DATA do DHT11Pino D4 do ESP32
    • LED de SucessoPino D2 do ESP32
    • LED de ErroPino D15 do ESP32

 

  1. Bibliotecas Utilizadas
  • WiFi.h
  • WebServer.h
  • HTTPClient.h
  • DHT.h
  • Preferences.h

Código Fonte

IMPORTANTE: Ao adicionar o endpoint responsável por cadastrar os dados no ESP32, é necessário substituir localhost pelo IP do computador que está executando a API. Para descobrir o IP, siga estes passos:

  1. Abra o Prompt de Comando (cmd).
  2. Digite ipconfig e pressione Enter.
  3. Localize a seção “Adaptador Ethernet” e encontre o endereço IPv4 listado.
#include <WiFi.h> // Inclui a biblioteca WiFi.h para conectar o ESP32 a uma rede Wi-Fi.
#include <WebServer.h> // Inclui a biblioteca WebServer.h para criar um servidor web.
#include <HTTPClient.h> // Inclui a biblioteca HTTPClient.h para fazer requisições HTTP.
#include <DHT.h> // Inclui a biblioteca DHT.h para interagir com sensores DHT.
#include <Preferences.h> // Inclui a biblioteca Preferences.h para armazenar e recuperar configurações na memória EEPROM do ESP32.

#define DHTPIN 4 // Define o pino ao qual o sensor DHT está conectado.
#define DHTTYPE DHT11 // Define o tipo do sensor DHT (DHT11 neste caso).
#define LED_SUCESSO 2 // Define o pino do LED que indica sucesso.
#define LED_ERRO 15 // Define o pino do LED que indica erro.

DHT dht(DHTPIN, DHTTYPE); // Cria uma instância do objeto DHT com o pino e tipo especificados.
Preferences preferences; // Cria uma instância do objeto Preferences para armazenar configurações.
WebServer server(80); // Cria uma instância do objeto WebServer na porta 80.

const char* ap_ssid = "ESP32_Config"; // Define o SSID da rede Wi-Fi criada pelo ESP32 para configuração.
const char* ap_password = "123456789"; // Define a senha da rede Wi-Fi criada pelo ESP32 para configuração.

unsigned long previousMillis = 0; // Variável para armazenar o tempo da última operação.
const long interval = 60000;  // Intervalo de 1 minuto (60000 milissegundos).

// Função para enviar dados para a rota HTTP
void sendData() {
  float temperature = dht.readTemperature(); // Lê a temperatura do sensor DHT.
  float humidity = dht.readHumidity(); // Lê a umidade do sensor DHT.

  if (isnan(temperature) || isnan(humidity)) { // Verifica se a leitura falhou.
    Serial.println("Failed to read from DHT sensor!"); // Imprime uma mensagem de erro no console serial.
    digitalWrite(LED_ERRO, HIGH);  // Liga o LED de erro.
    return; // Encerra a função se a leitura falhar.
  }

  Serial.print("Umidade: ");
  Serial.print(humidity); // Imprime a umidade no console serial.
  Serial.print(" %\t");
  Serial.print("Temperatura: ");
  Serial.print(temperature); // Imprime a temperatura no console serial.
  Serial.println(" °C");
  
  if (WiFi.status() == WL_CONNECTED) { // Verifica se o ESP32 está conectado ao Wi-Fi.
    HTTPClient http; // Cria uma instância do cliente HTTP.
    http.begin("http://IP do computador:3000/infoEsp"); // Define o URL do servidor para enviar os dados.
    http.addHeader("Content-Type", "application/json"); // Define o tipo de conteúdo como JSON.

    String postData = "{\"temperatura\":\"" + String(temperature) + "\",\"umidade\":\"" + String(humidity) + "\"}"; // Cria a string JSON com os dados da temperatura e umidade.

    Serial.println(postData); // Imprime a string JSON no console serial.

    int httpResponseCode = http.POST(postData); // Envia uma requisição POST com os dados para o servidor e obtém o código de resposta HTTP.

    if (httpResponseCode > 0) { // Verifica se a requisição foi bem-sucedida.
      String response = http.getString(); // Obtém a resposta do servidor.
      Serial.println(httpResponseCode); // Imprime o código de resposta HTTP no console serial.
      Serial.println(response); // Imprime a resposta do servidor no console serial.
      if (httpResponseCode == 200) { // Verifica se o código de resposta é 200 (OK).
        Serial.println("Data sent successfully!"); // Imprime uma mensagem de sucesso no console serial.
        digitalWrite(LED_SUCESSO, HIGH); // Liga o LED de sucesso.
        digitalWrite(LED_ERRO, LOW);  // Desliga o LED de erro.
      } else {
        digitalWrite(LED_ERRO, HIGH);  // Liga o LED de erro se a resposta não for 200.
        digitalWrite(LED_SUCESSO, LOW); // Desliga o LED de sucesso.
      }
    } else {
      Serial.print("Error on sending POST: "); // Imprime uma mensagem de erro no console serial.
      Serial.println(httpResponseCode); // Imprime o código de erro no console serial.
      digitalWrite(LED_ERRO, HIGH);  // Liga o LED de erro se falhar ao enviar a requisição.
      digitalWrite(LED_SUCESSO, LOW); // Desliga o LED de sucesso.
    }

    http.end(); // Finaliza a conexão HTTP.
  } else {
    Serial.println("WiFi not connected!"); // Imprime uma mensagem de erro no console serial se não estiver conectado ao Wi-Fi.
    digitalWrite(LED_ERRO, HIGH);  // Liga o LED de erro.
    digitalWrite(LED_SUCESSO, LOW); // Desliga o LED de sucesso.
  }
}

void setup() {
  Serial.begin(9600); // Inicializa a comunicação serial a 9600 bps.
  pinMode(LED_SUCESSO, OUTPUT); // Define o pino do LED de sucesso como saída.
  pinMode(LED_ERRO, OUTPUT); // Define o pino do LED de erro como saída.
  digitalWrite(LED_SUCESSO, LOW); // Desliga o LED de sucesso.
  digitalWrite(LED_ERRO, LOW); // Desliga o LED de erro.

  dht.begin(); // Inicializa o sensor DHT.

  WiFi.softAP(ap_ssid, ap_password); // Configura o ESP32 como um ponto de acesso com o SSID e senha definidos.

  server.on("/", HTTP_GET, []() { // Define o comportamento do servidor quando a raiz ("/") for acessada via GET.
    server.send(200, "text/html", "<form action=\"/save\" method=\"POST\">"
                                  "<label for=\"ssid\">SSID:</label><br>"
                                  "<input type=\"text\" id=\"ssid\" name=\"ssid\"><br>"
                                  "<label for=\"password\">Password:</label><br>"
                                  "<input type=\"password\" id=\"password\" name=\"password\"><br><br>"
                                  "<input type=\"submit\" value=\"Submit\">"
                                  "</form>");
  });

  server.on("/save", HTTP_POST, []() { // Define o comportamento do servidor quando a rota "/save" for acessada via POST.
    String ssid = server.arg("ssid"); // Obtém o SSID enviado pelo formulário.
    String password = server.arg("password"); // Obtém a senha enviada pelo formulário.

    preferences.begin("wifi-config", false); // Inicializa o armazenamento de preferências com a chave "wifi-config" em modo gravação.
    preferences.putString("ssid", ssid); // Salva o SSID na memória.
    preferences.putString("password", password); // Salva a senha na memória.
    preferences.end(); // Finaliza o armazenamento de preferências.

    server.send(200, "text/html", "Credentials saved. Rebooting..."); // Envia uma resposta ao cliente indicando que as credenciais foram salvas e o ESP32 será reiniciado.
    delay(2000); // Aguarda 2 segundos.
    ESP.restart(); // Reinicia o ESP32.
  });

  server.begin(); // Inicia o servidor web.
  Serial.println("Access Point started."); // Imprime uma mensagem indicando que o ponto de acesso foi iniciado.

  preferences.begin("wifi-config", true); // Inicializa o armazenamento de preferências com a chave "wifi-config" em modo leitura.
  String savedSSID = preferences.getString("ssid", ""); // Obtém o SSID salvo na memória.
  String savedPassword = preferences.getString("password", ""); // Obtém a senha salva na memória.
  preferences.end(); // Finaliza o armazenamento de preferências.

  if (savedSSID.length() > 0 && savedPassword.length() > 0) { // Verifica se o SSID e a senha foram salvos.
    WiFi.begin(savedSSID.c_str(), savedPassword.c_str()); // Conecta-se à rede Wi-Fi com o SSID e a senha salvos.
    Serial.print("Connecting to ");
    Serial.print(savedSSID); // Imprime uma mensagem indicando que está tentando conectar-se à rede.
    Serial.print("...");

    int attempts = 0; // Variável para contar as tentativas de conexão.
    while (WiFi.status() != WL_CONNECTED && attempts < 20) { // Tenta conectar-se à rede Wi-Fi até 20 vezes.
      delay(500); // Aguarda 500 milissegundos entre as tentativas.
      Serial.print("."); // Imprime um ponto no console serial para indicar progresso.
      attempts++; // Incrementa o contador de tentativas.
    }

    if (WiFi.status() == WL_CONNECTED) { // Verifica se a conexão foi bem-sucedida.
      Serial.println("Connected!"); // Imprime uma mensagem indicando que a conexão foi estabelecida.
      digitalWrite(LED_SUCESSO, HIGH);  // Liga o LED de sucesso.
    } else {
      Serial.println("Failed to connect."); // Imprime uma mensagem indicando que a conexão falhou.
      digitalWrite(LED_ERRO, HIGH);     // Liga o LED de erro.
    }
  }
}

void loop() {
  server.handleClient(); // Trata as requisições do cliente.

  unsigned long currentMillis = millis(); // Obtém o tempo atual.
  if (WiFi.status() == WL_CONNECTED && currentMillis - previousMillis >= interval) { // Verifica se está conectado ao Wi-Fi e se o intervalo de tempo foi alcançado.
    previousMillis = currentMillis; // Atualiza o tempo da última operação.
    sendData(); // Chama a função para enviar dados.
  }
}

Resumo

No código acima, temos três funções principais, sendo duas padrão para scripts do ESP32 e uma personalizada, sendData, que realiza a leitura dos dados do sensor e os envia para uma API.

Para configurar a rede que será utilizada para realizar as requisições, é necessário primeiramente conectar-se à rede “ESP32_Config” com a senha “123456789”. Isso dará acesso a uma página web, cujo endereço é o IP do ESP32, onde devemos inserir o nome e a senha da rede Wi-Fi desejada e clicar no botão de envio. Se a conexão com a rede for bem-sucedida, o LED azul acenderá, indicando que o processo de coleta de dados e requisições será iniciado. Em caso de falha na conexão ou nas requisições, um LED vermelho acenderá para indicar um erro.

O código inclui a biblioteca WiFi.h para conectar o ESP32 a uma rede Wi-Fi, WebServer.h para criar um servidor web, HTTPClient.h para fazer requisições HTTP, DHT.h para interagir com sensores DHT e Preferences.h para armazenar e recuperar configurações na memória EEPROM do ESP32.

O pino ao qual o sensor DHT está conectado é o 4 e o tipo de sensor é DHT11. Os LEDs para indicar sucesso e erro estão conectados aos pinos 2 e 15, respectivamente. A função sendData lê a temperatura e a umidade do sensor DHT e verifica se as leituras são válidas. Se as leituras falharem, um erro é indicado pelo LED vermelho. Caso contrário, os dados são enviados via uma requisição POST para o servidor. Se a requisição for bem-sucedida, o LED azul acende; caso contrário, o LED vermelho acende.

No setup, a comunicação serial é iniciada e os LEDs são configurados. O ESP32 é configurado como um ponto de acesso com o SSID e senha definidos, e um servidor web é inicializado. O servidor web fornece um formulário HTML para inserir o SSID e a senha da rede Wi-Fi. Após o envio do formulário, as credenciais são salvas na memória EEPROM e o ESP32 é reiniciado. Após a reinicialização, o ESP32 tenta se conectar à rede Wi-Fi usando as credenciais salvas. Se a conexão for bem-sucedida, o LED azul acende; se falhar, o LED vermelho acende.

No loop, o servidor trata as requisições do cliente e verifica se o ESP32 está conectado ao Wi-Fi e se o intervalo de tempo (1 minuto) foi alcançado. Se essas condições forem atendidas, a função sendData é chamada para enviar os dados.

O código é projetado para garantir que a configuração e o envio dos dados ocorram de forma robusta, utilizando LEDs para feedback visual do status do sistema.

Executado Projeto


Considerações finais

Nesta publicação, abordamos um projeto que contém conceitos fundamentais e básicos para desenvolver projetos muito maiores. Portanto, este foi o pontapé inicial para iniciar seus projetos de eletrônica integrados com o desenvolvimento de software. Dessa forma, quero agradecer e espero que tenha contribuído para o seu desenvolvimento.

Referencia:

Cotação de Moedas com HTTPClient e ESP32

APIs: Introdução e Desenvolvimento com TypeScript e Express

https://www.dio.me/articles/modelagem-de-banco-de-dados-principais-conceitos-que-todo-desenvolvedor-precisa-saber

https://logap.com.br/blog/banco-de-dados-nosql-para-iniciantes/ https://www.youtube.com/watch?v=BA3QM6Sy1S8

blog-eletrogate-webserver-esp-bmp280-capa


Sobre o Autor


João Vitor
@v_ribeiro_v

Acredito ser uma pessoa muito curiosa, sempre tentando encontrar soluções e perspectivas diferentes para os problemas cotidianos. Portanto, busco meios de me aprimorar. Atualmente, cursando Engenharia da Computação e atuando profissionalmente como desenvolvedor fullstack, estou sempre em busca de novos desafios e oportunidades de aprendizado, prezando pela maestria na minha vocação e pela inovação.


Eletrogate

30 de outubro de 2024

A Eletrogate é uma loja virtual de componentes eletrônicos do Brasil e possui diversos produtos relacionados à Arduino, Automação, Robótica e Eletrônica em geral.

Tenha a Metodologia Eletrogate dentro da sua Escola! Conheça nosso Programa de Robótica nas Escolas!

Eletrogate Robô

Assine nossa newsletter e
receba  10% OFF  na sua
primeira compra!