ESP32-C6 na Prática: Log de dados com Banco de Dados

Este post faz parte da série ESP32-C6 na Prática

Introdução

Neste artigo, mostraremos como registrar dados para um banco de dados com a placa de desenvolvimento ESP32-C6 DevKitC-1 e a Arduino IDE.

Nosso projeto terá como objetivo enviar dados do sensor de temperatura e umidade DHT11 e registrar cada leitura via HTTP a um servidor local para inserir no banco de dados.

Vamos criar o servidor e uma página web que exibe as informações do banco de dados. Assim você poderá visualizar os dados de qualquer lugar do mundo acessando seu próprio servidor.

Para conhecer mais a placa de desenvolvimento, consulte nosso artigo Introdução à Placa ESP32-C6-DevKitC-1: Ideal para IoT

 Diagrama de aplicação

Figura 1: Diagrama de aplicação

Tecnologias utilizadas  

  • HTML
  • NodeJs
  • PostgreSQL
  • Docker

Materiais Necessários

Para desenvolver nossa aplicação os materiais utilizados foram:

  • Placa ESP23-C6 DevKitC-1
  • Cabo USB-C
  • Resistor 10k Ohms
  • Sensor DHT11
  • Jumpers
  • Arduino IDE

Para configurar o ambiente e instalar o suporte a ESP32-C6 veja o artigo ESP32-C6 na Prática: Seu Primeiro “Hello World”

Circuito

Figura 2: Circuito desenvolvido

Estrutura do Projeto

O projeto foi organizado em pastas independentes, de acordo com a função de cada parte do sistema.

Figura 3: Estrutura de pasta do projeto

Backend

Contém a API responsável por receber os dados do ESP32-C6, salvar no banco de dados e disponibilizar endpoints para o frontend.

Abaixo temos o trecho principal, responsável por iniciar o servidor e configurar as rotas:

import express from "express";
import cors from "cors";

const app = express();
const port = 3000;

app.use(cors());
app.use(express.json());

// Rotas
app.use("/sensor", sensorRouter);

Como as rotas foram modularizadas em outro arquivo, abaixo temos a rota post e get usada pela url /sensor

sensorRouter.post("/", SensorController.post);
sensorRouter.get("/", SensorController.get);

Abaixo temos o código do arquivo init.sql, responsável por criar a tabela do banco de dados

-- criar tabela
CREATE TABLE IF NOT EXISTS sensores (
    id SERIAL PRIMARY KEY,
    device_id VARCHAR(255) NOT NULL,
    sensor VARCHAR(30) NOT NULL,
    location VARCHAR(30) NOT NULL,
    value1 VARCHAR(10),
    value2 VARCHAR(10),
    reading_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

No bloco abaixo temos a classe de funções da rota sensor, como get e post. Podemos observar a query realizada para manipulação com banco de dados.

export class SensorController {
  static async get(req, res) {
    try {
      // pegar página e limite via query params
      const page = parseInt(req.query.page) || 1;
      const limit = parseInt(req.query.limit) || 50;
      const offset = (page - 1) * limit;
      const result = await pool.query("SELECT * FROM sensores ORDER BY reading_time DESC LIMIT $1 OFFSET $2", [limit, offset]);
      res.status(200).json(result.rows);
    } catch (error) {
      console.error(error);
      res.status(500).send("Erro ao processar dados");
    }
  }


  static async post(req, res) {
    try {
      const { device_id, sensor, location, value1, value2 } = req.body;
      console.log(device_id, sensor, location, value1, value2);
      if (!device_id || !sensor || !location || !value1 || !value2) {
        return res.status(400).send("Dados incompletos");
      }
      const result = await pool.query("INSERT INTO sensores (device_id, sensor, location, value1, value2) VALUES ($1, $2, $3, $4, $5) RETURNING *", [
        device_id,
        sensor,
        location,
        value1,
        value2,
      ]);
      res.status(201).json(result.rows[0]);
    } catch (error) {
      console.error(error);
      res.status(500).send("Erro ao processar dados");
    }
  }
}

Por fim, temos o arquivo Dockerfile responsável por definir como será construído o container. No caso do backend, utilizamos o node para servir o servidor na porta 3000

# Usar imagem oficial do Node.js (LTS)
FROM node:20-alpine
# Criar diretório da aplicação
WORKDIR /usr/src/app
# Copiar package.json e package-lock.json
COPY package*.json ./
# Instalar dependências
RUN npm install
# Copiar todo o código da aplicação
COPY . .
# Expõe a porta que o backend vai rodar
EXPOSE 3000
# Comando para iniciar a aplicação
CMD ["npm", "start"]

Embarcado

Inclui o código que roda no ESP32-C6. Abaixo temos o trecho responsável por enviar os dados para API

 if (WiFi.status() == WL_CONNECTED) {
    HTTPClient http; // Cria uma instância do cliente HTTP
    http.begin((serverPath + "/sensor").c_str()); // Inicia a conexão com o servidor na rota
    http.addHeader("Content-Type", "application/json"); // Adiciona o cabeçalho Content-Type
    // Cria o payload JSON
    String payload = String(
                      "{\"device_id\":\"")
                    + deviceID
                    + "\",\"sensor\":\"" + sensorName
                    + "\",\"location\":\"" + sensorLocation
                    + "\",\"value1\":\"" + temperature
                    + "\",\"value2\":\"" + humidity + "\"}";
    int code = http.POST(payload);
    if (code != 201) {
      Serial.printf("Erro ao enviar dados: %s\n", http.errorToString(code).c_str());
    }
    Serial.println("Dados enviados com sucesso");
    String resp = http.getString();
    Serial.println(resp);
    http.end();
  }

Frontend

O frontend é a interface web que exibe os dados de forma amigável.

Abaixo temos o trecho de código HTML responsável pela tabela

<section class="section-content">
        <div class="table-container">
          <h1 class="title">Tabela de Sensores</h1>
          <table id="sensorTable">
            <thead>
              <tr>
                <th>id</th>
                <th>device_id</th>
                <th>sensor</th>
                <th>location</th>
                <th>value1</th>
                <th>value2</th>
                <th>reading_time</th>
              </tr>
            </thead>
            <tbody>
              <!-- dados virão aqui -->
            </tbody>
          </table>
          <div style="text-align: center; margin: 20px">
            <button id="prev">Anterior</button>
            <button id="next">Próximo</button>
          </div>

        </div>
<!-- Gráfico -->
        <div>
          <canvas id="sensorChart"></canvas>
        </div>
</section>


Abaixo temos o  trecho de código javascript responsável por pedir os dados da API

const API_URL = "http://localhost:3000/sensor";
const ctx = document.getElementById("sensorChart").getContext("2d");
// // cria o gráfico vazio
const chart = new Chart(ctx, {
  type: "line",
  data: {
    labels: [],
    datasets: [
      {
        label: "value1",
        data: [],
        borderColor: "blue",
        backgroundColor: "rgba(0, 0, 255, 0.15)",
        tension: 0.25,
        fill: true,
      },
      {
        label: "value2",
        data: [],
        borderColor: "red",
        backgroundColor: "rgba(255, 0, 0, 0.15)",
        tension: 0.25,
        fill: true,
      },
    ],
  },
  options: {
    responsive: true,
    maintainAspectRatio: false,
    scales: {
      x: { ticks: { maxRotation: 0 } },
      y: { beginAtZero: false },
    },
    plugins: {
      legend: { display: true },
      tooltip: { mode: "index", intersect: false },
    },
  },
});


let currentPage = 1;
const limit = 10;
async function loadData() {
  try {
    const response = await fetch(`${API_URL}?page=${currentPage}&limit=${limit}`, {
      method: "GET",
      headers: {
        "Content-Type": "application/json",
      },
    });
    if (!response.ok) throw new Error("Erro ao buscar dados");


    const sensores = await response.json();
    const tbody = document.querySelector("#sensorTable tbody");
    tbody.innerHTML = ""; // limpa antes de preencher


    sensores.forEach((sensor) => {
      const tr = document.createElement("tr");
      tr.innerHTML = `
        <td>${sensor.id}</td>
        <td>${sensor.device_id}</td>
        <td>${sensor.sensor}</td>
        <td>${sensor.location}</td>
        <td>${sensor.value1}</td>
        <td>${sensor.value2}</td>
        <td>${new Date(sensor.reading_time).toLocaleString()}</td>
      `;
      tbody.appendChild(tr);
    });
 // ===== Gráfico =====
    const labels = sensores.map((s) => new Date(s.reading_time).toLocaleTimeString()).reverse();
    const values1 = sensores.map((s) => s.value1).reverse();
    const values2 = sensores.map((s) => s.value2).reverse();
    chart.data.labels = labels;
    chart.data.datasets[0].data = values1;
    chart.data.datasets[1].data = values2;
    chart.update();


  } catch (error) {
    console.error(error);
    alert("Não foi possível carregar os dados");
  }
}


window.onload = loadData;

Por fim, temos o arquivo Dockerfile responsável por definir como será construído o container. No caso do frontend, utilizamos o nginx para servir os arquivos estáticos da aplicação web de forma otimizada na porta 80

FROM nginx:alpine
COPY . /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

Docker compose

Aqui ficam os arquivos de configuração do Docker Compose, que permitem subir rapidamente todo o sistema (backend, banco de dados, frontend e serviços auxiliares) em containers.

  • Com um único comando (docker-compose up), todo o ambiente é inicializado.
  • Isso garante reprodutibilidade e facilita a implantação em servidores locais ou na nuvem.

O código completo pode ser acessado em: ESP32-C6/dataLoggingServidor at main · guilhermefernandesk/ESP32-C6

Resultados

Figura 4: Frontend com dados da API
Figura 5: Docker com containers

Bancada Do Embarcados

Na Bancada do Embarcados “97 – IoT na Prática: Log de Dados com ESP32, Servidor e Banco de Dados“, foi apresentado o projeto. Confira o vídeo a seguir:

Conclusão

Neste artigo, apresentamos a arquitetura do nosso projeto, que integra o ESP32-C6 para coleta de dados de sensores, um backend em container para processamento e armazenamento, um frontend servido por nginx para visualização das informações, e a orquestração de todos esses serviços por meio do docker-compose.

Essa abordagem modular, onde cada serviço roda em seu próprio contêiner, traz benefícios como isolamento, escalabilidade e facilidade de implantação, permitindo que qualquer pessoa reproduza o ambiente com apenas um comando.

Sinta-se à vontade para compartilhar seus resultados com a comunidade, seja por artigos, vídeos ou repositórios abertos. Sua experiência pode inspirar e auxiliar muitos outros desenvolvedores.

Referências

ESP32-C6 na Prática

ESP32-C6 na Prática: Log de dados no Google Sheets ESP32-C6 na Prática: Consumindo a API do OpenWeatherMap com JSON
Licença Creative Commons Esta obra está licenciada com uma Licença Creative Commons Atribuição-CompartilhaIgual 4.0 Internacional.
Comentários:
Notificações
Notificar
0 Comentários
recentes
antigos mais votados
Inline Feedbacks
View all comments
Home » Internet Das Coisas » ESP32-C6 na Prática: Log de dados com Banco de Dados

EM DESTAQUE

WEBINARS

VEJA TAMBÉM

JUNTE-SE HOJE À COMUNIDADE EMBARCADOS

Talvez você goste: