Sistema de Monitoramento IoT com ESP32
Neste projeto, vamos construir um sistema completo de monitoramento ambiental usando ESP32, que coleta dados de temperatura e umidade e os disponibiliza através de uma interface web em tempo real. O sistema também registra histórico dos dados e permite configuração remota.
Visão Geral do Projeto
O que você vai aprender:
- Conexão de sensores ao ESP32
- Programação de servidor web embarcado
- Criação de interface web responsiva
- Armazenamento de dados em SPIFFS
- Conectividade WiFi e configuração
Funcionalidades:
- ✅ Monitoramento em tempo real
- ✅ Interface web responsiva
- ✅ Histórico de dados (24h)
- ✅ Alertas configuráveis
- ✅ Configuração via web
- ✅ Modo AP para configuração inicial
Lista de Materiais
Componentes Eletrônicos
| Item | Quantidade | Preço Aprox. | |------|------------|--------------| | ESP32 DevKit V1 | 1 | R$ 30,00 | | Sensor DHT22 | 1 | R$ 15,00 | | Resistor 10kΩ | 1 | R$ 0,10 | | Protoboard 400 pontos | 1 | R$ 8,00 | | Jumpers macho-macho | 10 | R$ 5,00 | | Total | | R$ 58,10 |
Ferramentas Necessárias
- Computador com Arduino IDE
- Cabo USB-C ou Micro-USB (dependendo do ESP32)
- Navegador web
Esquema de Ligação
ESP32 DevKit V1 DHT22
┌─────────────────┐ ┌──────┐
│ 3V3├────┤ VCC │
│ │ │ │
│ D4 ├────┤ DATA │
│ │ │ │
│ GND ├────┤ GND │
└─────────────────┘ └──────┘
│
┌┴┐
│ │ 10kΩ (pull-up)
│ │
└┬┘
│
3V3
Pinout Detalhado:
- ESP32 3V3 → DHT22 VCC
- ESP32 D4 → DHT22 DATA
- ESP32 GND → DHT22 GND
- Resistor 10kΩ entre DATA e VCC (pull-up)
Código do Projeto
1. Bibliotecas Necessárias
Primeiro, instale as bibliotecas no Arduino IDE:
# Via Library Manager:
- DHT sensor library by Adafruit
- Adafruit Unified Sensor
- ArduinoJson
- ESPAsyncWebServer
2. Código Principal
#include <WiFi.h>
#include <ESPAsyncWebServer.h>
#include <SPIFFS.h>
#include <DHT.h>
#include <ArduinoJson.h>
// Configurações do DHT22
#define DHT_PIN 4
#define DHT_TYPE DHT22
DHT dht(DHT_PIN, DHT_TYPE);
// Servidor web na porta 80
AsyncWebServer server(80);
// Variáveis globais
struct SensorData {
float temperature;
float humidity;
unsigned long timestamp;
};
SensorData currentData;
SensorData dataHistory[144]; // 24h com leituras a cada 10min
int historyIndex = 0;
// Configurações WiFi
String ssid = "";
String password = "";
bool wifiConfigured = false;
// Configurações de alerta
float tempMin = 18.0;
float tempMax = 28.0;
float humMin = 40.0;
float humMax = 70.0;
void setup() {
Serial.begin(115200);
Serial.println("Iniciando Sistema de Monitoramento IoT...");
// Inicializar DHT22
dht.begin();
// Inicializar SPIFFS
if (!SPIFFS.begin(true)) {
Serial.println("Erro ao inicializar SPIFFS!");
return;
}
// Carregar configurações
loadConfig();
// Configurar WiFi
setupWiFi();
// Configurar servidor web
setupWebServer();
// Primeira leitura
readSensor();
Serial.println("Sistema inicializado com sucesso!");
}
void loop() {
static unsigned long lastReading = 0;
static unsigned long lastSave = 0;
// Ler sensor a cada 2 segundos
if (millis() - lastReading > 2000) {
readSensor();
lastReading = millis();
}
// Salvar no histórico a cada 10 minutos
if (millis() - lastSave > 600000) {
saveToHistory();
lastSave = millis();
}
delay(100);
}
void readSensor() {
float temp = dht.readTemperature();
float hum = dht.readHumidity();
if (!isnan(temp) && !isnan(hum)) {
currentData.temperature = temp;
currentData.humidity = hum;
currentData.timestamp = millis();
// Verificar alertas
checkAlerts();
Serial.printf("Temp: %.1f°C, Umidade: %.1f%%\n", temp, hum);
} else {
Serial.println("Erro na leitura do sensor!");
}
}
void checkAlerts() {
bool alert = false;
String message = "ALERTA: ";
if (currentData.temperature < tempMin) {
message += "Temperatura baixa (" + String(currentData.temperature, 1) + "°C) ";
alert = true;
}
if (currentData.temperature > tempMax) {
message += "Temperatura alta (" + String(currentData.temperature, 1) + "°C) ";
alert = true;
}
if (currentData.humidity < humMin) {
message += "Umidade baixa (" + String(currentData.humidity, 1) + "%) ";
alert = true;
}
if (currentData.humidity > humMax) {
message += "Umidade alta (" + String(currentData.humidity, 1) + "%) ";
alert = true;
}
if (alert) {
Serial.println(message);
// Aqui você pode adicionar notificações (email, push, etc.)
}
}
void saveToHistory() {
dataHistory[historyIndex] = currentData;
historyIndex = (historyIndex + 1) % 144;
// Salvar no SPIFFS
saveHistoryToFile();
}
void setupWiFi() {
if (ssid.length() > 0) {
WiFi.mode(WIFI_STA);
WiFi.begin(ssid.c_str(), password.c_str());
Serial.print("Conectando ao WiFi");
int attempts = 0;
while (WiFi.status() != WL_CONNECTED && attempts < 30) {
delay(1000);
Serial.print(".");
attempts++;
}
if (WiFi.status() == WL_CONNECTED) {
wifiConfigured = true;
Serial.println("\nWiFi conectado!");
Serial.print("IP: ");
Serial.println(WiFi.localIP());
} else {
Serial.println("\nFalha na conexão WiFi!");
startAPMode();
}
} else {
startAPMode();
}
}
void startAPMode() {
WiFi.mode(WIFI_AP);
WiFi.softAP("IoT-Monitor-Config", "12345678");
Serial.println("Modo AP iniciado!");
Serial.print("IP: ");
Serial.println(WiFi.softAPIP());
}
void setupWebServer() {
// Servir arquivos estáticos
server.serveStatic("/", SPIFFS, "/").setDefaultFile("index.html");
// API para dados atuais
server.on("/api/current", HTTP_GET, [](AsyncWebServerRequest *request){
DynamicJsonDocument doc(200);
doc["temperature"] = currentData.temperature;
doc["humidity"] = currentData.humidity;
doc["timestamp"] = currentData.timestamp;
String response;
serializeJson(doc, response);
request->send(200, "application/json", response);
});
// API para histórico
server.on("/api/history", HTTP_GET, [](AsyncWebServerRequest *request){
DynamicJsonDocument doc(8192);
JsonArray array = doc.createNestedArray("data");
for (int i = 0; i < 144; i++) {
int index = (historyIndex + i) % 144;
if (dataHistory[index].timestamp > 0) {
JsonObject entry = array.createNestedObject();
entry["temperature"] = dataHistory[index].temperature;
entry["humidity"] = dataHistory[index].humidity;
entry["timestamp"] = dataHistory[index].timestamp;
}
}
String response;
serializeJson(doc, response);
request->send(200, "application/json", response);
});
// API para configuração WiFi
server.on("/api/wifi", HTTP_POST, [](AsyncWebServerRequest *request){
if (request->hasParam("ssid", true) && request->hasParam("password", true)) {
ssid = request->getParam("ssid", true)->value();
password = request->getParam("password", true)->value();
saveConfig();
request->send(200, "text/plain", "Configuração salva! Reiniciando...");
delay(1000);
ESP.restart();
} else {
request->send(400, "text/plain", "Parâmetros inválidos");
}
});
// API para configuração de alertas
server.on("/api/alerts", HTTP_POST, [](AsyncWebServerRequest *request){
if (request->hasParam("tempMin", true)) {
tempMin = request->getParam("tempMin", true)->value().toFloat();
}
if (request->hasParam("tempMax", true)) {
tempMax = request->getParam("tempMax", true)->value().toFloat();
}
if (request->hasParam("humMin", true)) {
humMin = request->getParam("humMin", true)->value().toFloat();
}
if (request->hasParam("humMax", true)) {
humMax = request->getParam("humMax", true)->value().toFloat();
}
saveConfig();
request->send(200, "text/plain", "Alertas configurados!");
});
server.begin();
Serial.println("Servidor web iniciado!");
}
void loadConfig() {
if (SPIFFS.exists("/config.json")) {
File file = SPIFFS.open("/config.json", "r");
DynamicJsonDocument doc(1024);
deserializeJson(doc, file);
file.close();
ssid = doc["ssid"].as<String>();
password = doc["password"].as<String>();
tempMin = doc["tempMin"] | 18.0;
tempMax = doc["tempMax"] | 28.0;
humMin = doc["humMin"] | 40.0;
humMax = doc["humMax"] | 70.0;
}
}
void saveConfig() {
DynamicJsonDocument doc(1024);
doc["ssid"] = ssid;
doc["password"] = password;
doc["tempMin"] = tempMin;
doc["tempMax"] = tempMax;
doc["humMin"] = humMin;
doc["humMax"] = humMax;
File file = SPIFFS.open("/config.json", "w");
serializeJson(doc, file);
file.close();
}
void saveHistoryToFile() {
// Implementar salvamento do histórico se necessário
// Para este exemplo, mantemos apenas em RAM
}
3. Interface Web (HTML/CSS/JS)
Crie o arquivo data/index.html
no projeto:
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Monitor IoT - ESP32</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
h1 {
text-align: center;
color: white;
margin-bottom: 30px;
font-size: 2.5em;
text-shadow: 0 2px 4px rgba(0,0,0,0.3);
}
.cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.card {
background: white;
border-radius: 15px;
padding: 30px;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
text-align: center;
transition: transform 0.3s ease;
}
.card:hover {
transform: translateY(-5px);
}
.card h2 {
color: #333;
margin-bottom: 15px;
font-size: 1.5em;
}
.value {
font-size: 3em;
font-weight: bold;
margin: 10px 0;
}
.temp { color: #ff6b6b; }
.hum { color: #4ecdc4; }
.unit {
font-size: 0.5em;
color: #666;
}
.status {
padding: 10px;
border-radius: 8px;
margin-top: 15px;
font-weight: bold;
}
.status.ok { background: #d4edda; color: #155724; }
.status.alert { background: #f8d7da; color: #721c24; }
.chart-container {
background: white;
border-radius: 15px;
padding: 30px;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
margin-bottom: 30px;
}
.config {
background: white;
border-radius: 15px;
padding: 30px;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
color: #333;
}
input {
width: 100%;
padding: 12px;
border: 2px solid #ddd;
border-radius: 8px;
font-size: 16px;
}
button {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 12px 30px;
border-radius: 8px;
font-size: 16px;
cursor: pointer;
transition: opacity 0.3s ease;
}
button:hover {
opacity: 0.9;
}
.last-update {
text-align: center;
color: white;
margin-top: 20px;
font-size: 0.9em;
}
@media (max-width: 768px) {
.cards {
grid-template-columns: 1fr;
}
h1 {
font-size: 2em;
}
.value {
font-size: 2.5em;
}
}
</style>
</head>
<body>
<div class="container">
<h1>🌡️ Monitor IoT - ESP32</h1>
<div class="cards">
<div class="card">
<h2>🌡️ Temperatura</h2>
<div class="value temp" id="temperature">--<span class="unit">°C</span></div>
<div class="status ok" id="temp-status">Normal</div>
</div>
<div class="card">
<h2>💧 Umidade</h2>
<div class="value hum" id="humidity">--<span class="unit">%</span></div>
<div class="status ok" id="hum-status">Normal</div>
</div>
</div>
<div class="chart-container">
<h2>📊 Histórico (24h)</h2>
<canvas id="chart" width="800" height="400"></canvas>
</div>
<div class="config">
<h2>⚙️ Configurações</h2>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 20px;">
<div>
<h3>🌡️ Limites de Temperatura</h3>
<div class="form-group">
<label>Mínima (°C):</label>
<input type="number" id="tempMin" value="18" step="0.1">
</div>
<div class="form-group">
<label>Máxima (°C):</label>
<input type="number" id="tempMax" value="28" step="0.1">
</div>
</div>
<div>
<h3>💧 Limites de Umidade</h3>
<div class="form-group">
<label>Mínima (%):</label>
<input type="number" id="humMin" value="40" step="0.1">
</div>
<div class="form-group">
<label>Máxima (%):</label>
<input type="number" id="humMax" value="70" step="0.1">
</div>
</div>
</div>
<button onclick="saveAlerts()">💾 Salvar Configurações</button>
</div>
<div class="last-update" id="lastUpdate">
Última atualização: --
</div>
</div>
<script>
let chart = null;
// Atualizar dados a cada 5 segundos
setInterval(updateData, 5000);
updateData();
// Atualizar histórico a cada minuto
setInterval(updateChart, 60000);
updateChart();
async function updateData() {
try {
const response = await fetch('/api/current');
const data = await response.json();
document.getElementById('temperature').innerHTML =
`${data.temperature.toFixed(1)}<span class="unit">°C</span>`;
document.getElementById('humidity').innerHTML =
`${data.humidity.toFixed(1)}<span class="unit">%</span>`;
// Verificar status
updateStatus(data);
// Atualizar timestamp
const now = new Date();
document.getElementById('lastUpdate').textContent =
`Última atualização: ${now.toLocaleTimeString()}`;
} catch (error) {
console.error('Erro ao buscar dados:', error);
}
}
function updateStatus(data) {
const tempMin = parseFloat(document.getElementById('tempMin').value);
const tempMax = parseFloat(document.getElementById('tempMax').value);
const humMin = parseFloat(document.getElementById('humMin').value);
const humMax = parseFloat(document.getElementById('humMax').value);
// Status temperatura
const tempStatus = document.getElementById('temp-status');
if (data.temperature < tempMin || data.temperature > tempMax) {
tempStatus.className = 'status alert';
tempStatus.textContent = 'ALERTA!';
} else {
tempStatus.className = 'status ok';
tempStatus.textContent = 'Normal';
}
// Status umidade
const humStatus = document.getElementById('hum-status');
if (data.humidity < humMin || data.humidity > humMax) {
humStatus.className = 'status alert';
humStatus.textContent = 'ALERTA!';
} else {
humStatus.className = 'status ok';
humStatus.textContent = 'Normal';
}
}
async function updateChart() {
try {
const response = await fetch('/api/history');
const data = await response.json();
const canvas = document.getElementById('chart');
const ctx = canvas.getContext('2d');
// Limpar canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
if (data.data.length === 0) return;
// Configurações do gráfico
const padding = 50;
const width = canvas.width - 2 * padding;
const height = canvas.height - 2 * padding;
// Encontrar min/max
const temps = data.data.map(d => d.temperature);
const hums = data.data.map(d => d.humidity);
const tempMin = Math.min(...temps) - 2;
const tempMax = Math.max(...temps) + 2;
const humMin = Math.min(...hums) - 5;
const humMax = Math.max(...hums) + 5;
// Desenhar eixos
ctx.strokeStyle = '#ddd';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(padding, padding);
ctx.lineTo(padding, height + padding);
ctx.lineTo(width + padding, height + padding);
ctx.stroke();
// Desenhar temperatura
ctx.strokeStyle = '#ff6b6b';
ctx.lineWidth = 2;
ctx.beginPath();
data.data.forEach((point, index) => {
const x = padding + (index / (data.data.length - 1)) * width;
const y = padding + (1 - (point.temperature - tempMin) / (tempMax - tempMin)) * height;
if (index === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
});
ctx.stroke();
// Desenhar umidade
ctx.strokeStyle = '#4ecdc4';
ctx.lineWidth = 2;
ctx.beginPath();
data.data.forEach((point, index) => {
const x = padding + (index / (data.data.length - 1)) * width;
const y = padding + (1 - (point.humidity - humMin) / (humMax - humMin)) * height;
if (index === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
});
ctx.stroke();
// Legenda
ctx.fillStyle = '#ff6b6b';
ctx.fillRect(padding, 10, 20, 10);
ctx.fillStyle = '#333';
ctx.font = '12px Arial';
ctx.fillText('Temperatura', padding + 25, 20);
ctx.fillStyle = '#4ecdc4';
ctx.fillRect(padding + 120, 10, 20, 10);
ctx.fillStyle = '#333';
ctx.fillText('Umidade', padding + 145, 20);
} catch (error) {
console.error('Erro ao buscar histórico:', error);
}
}
async function saveAlerts() {
const formData = new FormData();
formData.append('tempMin', document.getElementById('tempMin').value);
formData.append('tempMax', document.getElementById('tempMax').value);
formData.append('humMin', document.getElementById('humMin').value);
formData.append('humMax', document.getElementById('humMax').value);
try {
const response = await fetch('/api/alerts', {
method: 'POST',
body: formData
});
if (response.ok) {
alert('Configurações salvas com sucesso!');
} else {
alert('Erro ao salvar configurações!');
}
} catch (error) {
console.error('Erro ao salvar:', error);
alert('Erro ao salvar configurações!');
}
}
</script>
</body>
</html>
Upload de Arquivos SPIFFS
Para que a interface web funcione, você precisa fazer upload dos arquivos para o SPIFFS:
- Instale o plugin ESP32 Sketch Data Upload
- Crie a pasta
data
no diretório do projeto - Coloque o arquivo
index.html
na pasta data - Use Tools > ESP32 Sketch Data Upload
Configuração e Uso
1. Primeira Configuração
- Carregue o código no ESP32
- Abra o Serial Monitor (115200 baud)
- Conecte-se ao WiFi "IoT-Monitor-Config" (senha: 12345678)
- Acesse http://192.168.4.1
- Configure sua rede WiFi nas configurações
2. Operação Normal
Após configurar o WiFi:
- O ESP32 se conectará automaticamente à sua rede
- Acesse o IP mostrado no Serial Monitor
- Monitor os dados em tempo real
- Configure alertas conforme necessário
Funcionalidades Avançadas
1. Histórico de Dados
O sistema mantém 24 horas de histórico com pontos a cada 10 minutos:
// Estrutura para armazenar dados históricos
struct SensorData {
float temperature;
float humidity;
unsigned long timestamp;
};
SensorData dataHistory[144]; // 24h * 6 pontos/hora
2. Sistema de Alertas
Alertas configuráveis para valores fora da faixa:
void checkAlerts() {
if (currentData.temperature < tempMin) {
// Enviar alerta de temperatura baixa
}
// ... outros alertas
}
3. Interface Responsiva
A interface se adapta automaticamente a diferentes tamanhos de tela usando CSS Grid e Media Queries.
Melhorias Possíveis
1. Banco de Dados Externo
- Integração com InfluxDB
- Grafana para visualização avançada
- Retenção de dados de longo prazo
2. Notificações
- E-mail via SMTP
- Push notifications
- Integração com Telegram
3. Sensores Adicionais
- Pressão atmosférica (BMP280)
- Qualidade do ar (MQ-135)
- Luminosidade (BH1750)
4. Atuadores
- Ventilação automática
- Aquecimento/resfriamento
- Umidificação
Troubleshooting
Problemas Comuns
1. Sensor não lê valores:
- Verifique as conexões
- Confirme se o resistor pull-up está correto
- Teste com outro sensor DHT22
2. WiFi não conecta:
- Verifique SSID e senha
- Use o modo AP para reconfigurar
- Verifique se a rede é 2.4GHz
3. Interface web não carrega:
- Confirme se os arquivos foram uploadados para SPIFFS
- Verifique se o servidor web está funcionando
- Use F12 no navegador para ver erros
Códigos de Debug
// Adicione debug no código principal
void setup() {
Serial.begin(115200);
Serial.setDebugOutput(true); // Habilita debug WiFi
// ... resto do código
}
Expansões do Projeto
1. Versão com Display
Adicione um display OLED para mostrar dados localmente:
#include <Adafruit_SSD1306.h>
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);
void updateDisplay() {
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(WHITE);
display.setCursor(0,0);
display.printf("Temp: %.1f C\n", currentData.temperature);
display.printf("Umid: %.1f %%\n", currentData.humidity);
display.display();
}
2. Integração MQTT
Para integração com sistemas IoT maiores:
#include <PubSubClient.h>
WiFiClient espClient;
PubSubClient mqtt(espClient);
void publishData() {
String payload = "{\"temp\":" + String(currentData.temperature) +
",\"hum\":" + String(currentData.humidity) + "}";
mqtt.publish("home/sensors/living_room", payload.c_str());
}
Conclusão
Este projeto demonstra como criar um sistema IoT completo usando ESP32, desde a coleta de dados até a visualização web. O sistema é escalável e pode ser facilmente expandido com novos sensores e funcionalidades.
Próximos Passos:
- Teste o projeto básico e familiarize-se com o código
- Experimente diferentes sensores (pressão, luz, etc.)
- Implemente notificações via e-mail ou telegram
- Integre com plataformas IoT como Blynk ou ThingSpeak
- Adicione controle de atuadores para automação completa
Código completo disponível no GitHub: rafaelabrantest2/iot-monitoring-esp32
Próximo projeto: "Central de Automação Residencial com ESP32 e Relés"