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
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
Estrutura do Projeto
O projeto foi organizado em pastas independentes, de acordo com a função de cada parte do sistema.
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

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.




