Série: Automação de Cafeteira - Nível Avançado
Este é o Nível 3 (Profissional) da nossa série completa de automação de cafeteira.
No primeiro artigo da série, vimos como automatizar uma moka italiana usando produtos prontos. No segundo, construímos um sistema ESP32 básico. Agora vamos elevar o nível: construir do zero um sistema completo de automação para qualquer cafeteira usando ESP32, com controle total, interface web profissional e recursos que superam cafeteiras comerciais premium.
O que vamos construir
Sistema completo ESP32 + sensores + relés + interface web
Resultado: Cafeteira 100% smart com recursos profissionais e custo reduzido
💡 Evolução da Série
Nível 1
Timer + Tomada Smart
R$ 110-350
Nível 2
ESP32 + Sensor Básico
R$ 160-290
Nível 3 - VOCÊ ESTÁ AQUI
Sistema Profissional
R$ 300-550
Por que construir do zero com ESP32?
Produtos prontos têm limitações. Com ESP32, você tem:
- Controle total: Programação personalizada, sensores múltiplos, lógica avançada
- Interface profissional: Dashboard web responsivo, gráficos em tempo real
- Custo-benefício superior: Funcionalidades que custam milhares por uma fração
- Recursos únicos: Múltiplos perfis, detecção automática, machine learning básico
Material necessário
Eletrônicos principais:
- ESP32 DevKit V1 (38 pinos) - R$ 35-55
- Módulo relé 5V 2 canais - R$ 18-35
- Sensor de fluxo de água YF-S201 - R$ 25-45
- Sensor de temperatura DS18B20 - R$ 12-25
- Display OLED 128x64 I2C - R$ 25-40
- Buzzer ativo 5V + LEDs RGB - R$ 12-20
Componentes auxiliares:
- Fonte chaveada 5V 3A - R$ 25-45
- Protoboard + jumpers + resistores - R$ 20-35
- Caixa vedada + conectores - R$ 25-50
Custo total: R$ 300-550 (incluindo cafeteira simples)
⚠️ Importante: As marcas mencionadas são citadas apenas para fins educativos.
Esquema de ligações ESP32
ESP32 DevKit V1 Componente ================= =================== GPIO4 (SDA) → Display OLED SDA GPIO5 (SCL) → Display OLED SCL GPIO12 → Relé 1 (Bomba Água) GPIO13 → Relé 2 (Aquecimento) GPIO14 → Sensor Fluxo Água GPIO15 → Buzzer GPIO16 → LED RGB Status GPIO17 → Sensor Temperatura GPIO22 → Sensor Corrente 5V/3.3V → Alimentação sensores GND → Terra comum (CRÍTICO!)
Proteções elétricas essenciais:
- Diodo 1N4007 antiparasita nos relés
- Capacitor 470µF na alimentação
- Fusível 10A na linha principal
- Aterramento obrigatório
Código principal ESP32
Estrutura modular do firmware:
Ver código completo
#include <WiFi.h> #include <ESPAsyncWebServer.h> #include <ArduinoJson.h> #include <OneWire.h> #include <DallasTemperature.h> #include <Wire.h> #include <Adafruit_SSD1306.h> // =============== CONFIGURAÇÕES =============== #define RELAY_PUMP_PIN 12 #define RELAY_HEATER_PIN 13 #define WATER_FLOW_PIN 14 #define BUZZER_PIN 15 #define TEMP_SENSOR_PIN 17 #define CURRENT_SENSOR_PIN 22 #define MIN_WATER_LEVEL 200 // ml #define MAX_BREW_TEMP 96.0 // °C #define OVERHEAT_TEMP 105.0 // °C emergência // =============== ESTRUTURAS DE DADOS =============== struct CoffeeProfile { String name; float targetTemp; int brewTime; // segundos float waterAmount; // ml }; struct SensorData { float temperature; float waterLevel; float powerConsumption; bool heaterStatus; bool pumpStatus; unsigned long lastUpdate; }; struct SystemStatus { String currentState; bool brewingActive; unsigned long brewStartTime; float progressPercent; String lastError; }; // =============== OBJETOS GLOBAIS =============== Adafruit_SSD1306 display(128, 64, &Wire, -1); OneWire oneWire(TEMP_SENSOR_PIN); DallasTemperature tempSensor(&oneWire); AsyncWebServer server(80); SensorData sensors; SystemStatus systemStatus; CoffeeProfile profiles[4]; int currentProfileIndex = 0; volatile unsigned long flowPulseCount = 0; // =============== SETUP PRINCIPAL =============== void setup() { Serial.begin(115200); initializePins(); initializeDisplay(); initializeSensors(); initializeProfiles(); initializeWiFi(); initializeWebServer(); attachInterrupt(digitalPinToInterrupt(WATER_FLOW_PIN), flowPulseCounter, RISING); systemStatus.currentState = "Pronto"; Serial.println("Sistema inicializado!" </>); } // =============== LOOP PRINCIPAL =============== void loop() { readAllSensors(); if (systemStatus.brewingActive) { processCoffeeBrewing(); } checkSafetyConditions(); updateDisplay(); delay(1000); } // =============== INICIALIZAÇÃO =============== void initializePins() { pinMode(RELAY_PUMP_PIN, OUTPUT); pinMode(RELAY_HEATER_PIN, OUTPUT); pinMode(BUZZER_PIN, OUTPUT); digitalWrite(RELAY_PUMP_PIN, LOW); digitalWrite(RELAY_HEATER_PIN, LOW); } void initializeProfiles() { profiles[0] = {"Espresso", 93.0, 25, 30}; profiles[1] = {"Americano", 90.0, 45, 120}; profiles[2] = {"Filtrado", 88.0, 240, 200}; profiles[3] = {"Custom", 90.0, 60, 100}; } // =============== LEITURA DE SENSORES =============== void readAllSensors() { tempSensor.requestTemperatures(); sensors.temperature = tempSensor.getTempCByIndex(0); // Fluxo de água (pulsos por segundo) static unsigned long lastFlowCalc = 0; if (millis() - lastFlowCalc >= 1000) { sensors.waterLevel += (flowPulseCount / 7.5) * 1000 / 60; // ml/min -> ml/s flowPulseCount = 0; lastFlowCalc = millis(); } // Consumo energético int currentRaw = analogRead(CURRENT_SENSOR_PIN); float current = (currentRaw * 3.3 / 4095.0 - 2.5) / 0.066; sensors.powerConsumption = abs(current) * 220.0; sensors.heaterStatus = digitalRead(RELAY_HEATER_PIN); sensors.pumpStatus = digitalRead(RELAY_PUMP_PIN); sensors.lastUpdate = millis(); } // =============== LÓGICA DE PREPARO =============== void startCoffeeBrewing(int profileIndex) { if (systemStatus.brewingActive) return; if (sensors.temperature > OVERHEAT_TEMP) { systemStatus.lastError = "Temperatura muito alta"; return; } currentProfileIndex = profileIndex; systemStatus.brewingActive = true; systemStatus.currentState = "Preparando " + profiles[profileIndex].name; systemStatus.brewStartTime = millis(); systemStatus.progressPercent = 0.0; Serial.println("Iniciando: " + profiles[profileIndex].name); sendNotification("Café iniciado", profiles[profileIndex].name); } void processCoffeeBrewing() { CoffeeProfile &profile = profiles[currentProfileIndex]; unsigned long elapsedTime = (millis() - systemStatus.brewStartTime) / 1000; systemStatus.progressPercent = (float(elapsedTime) / float(profile.brewTime)) * 100.0; if (systemStatus.progressPercent > 100.0) systemStatus.progressPercent = 100.0; // Controle de temperatura if (sensors.temperature < profile.targetTemp - 2.0) { digitalWrite(RELAY_HEATER_PIN, HIGH); } else if (sensors.temperature > profile.targetTemp + 1.0) { digitalWrite(RELAY_HEATER_PIN, LOW); } // Controle da bomba if (elapsedTime >= 5 && sensors.temperature >= profile.targetTemp - 3.0) { digitalWrite(RELAY_PUMP_PIN, HIGH); } // Verificar conclusão if (elapsedTime >= profile.brewTime) { finishCoffeeBrewing(); } } void finishCoffeeBrewing() { systemStatus.brewingActive = false; digitalWrite(RELAY_PUMP_PIN, LOW); digitalWrite(RELAY_HEATER_PIN, LOW); systemStatus.currentState = "Pronto"; systemStatus.progressPercent = 100.0; sendNotification("Café pronto!", profiles[currentProfileIndex].name + " concluído"); Serial.println("Café concluído!"); } // =============== SEGURANÇA =============== void checkSafetyConditions() { if (sensors.temperature > OVERHEAT_TEMP) { emergencyStop("Superaquecimento"); } if (sensors.powerConsumption > 2000) { emergencyStop("Consumo excessivo"); } } void emergencyStop(String reason) { systemStatus.brewingActive = false; digitalWrite(RELAY_PUMP_PIN, LOW); digitalWrite(RELAY_HEATER_PIN, LOW); systemStatus.currentState = "ERRO"; systemStatus.lastError = reason; sendNotification("EMERGÊNCIA", reason); Serial.println("EMERGÊNCIA: " + reason); } // =============== INTERRUPÇÕES =============== void IRAM_ATTR flowPulseCounter() { flowPulseCount++; }
Interface web responsiva
Dashboard HTML5 otimizado:
Ver código completo
const char* getWebInterface() { return R"rawliteral( <!DOCTYPE html> <html lang="pt-BR"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>SmartCoffee ESP32</title> <style> body { font-family: -apple-system, BlinkMacSystemFont, sans-serif; background: linear-gradient(135deg, #6B4423, #8B4513); color: white; margin: 0; padding: 20px; min-height: 100vh; } .container { max-width: 1200px; margin: 0 auto; } .header { text-align: center; background: rgba(0,0,0,0.3); padding: 20px; border-radius: 15px; margin-bottom: 20px; } .status-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; margin-bottom: 20px; } .status-card { background: rgba(255,255,255,0.1); padding: 20px; border-radius: 12px; text-align: center; } .status-value { font-size: 1.8em; font-weight: bold; } .status-label { font-size: 0.9em; opacity: 0.8; } .controls { display: grid; grid-template-columns: 1fr 2fr; gap: 20px; } .profiles-panel, .brewing-panel { background: rgba(0,0,0,0.4); padding: 20px; border-radius: 15px; } .profile-card { background: rgba(255,255,255,0.1); margin: 10px 0; padding: 15px; border-radius: 10px; cursor: pointer; transition: all 0.3s; } .profile-card:hover { background: rgba(255,255,255,0.2); } .profile-card.active { background: rgba(255,215,0,0.2); } .progress-container { background: rgba(0,0,0,0.3); border-radius: 25px; padding: 4px; margin: 20px 0; } .progress-bar { background: linear-gradient(90deg, #4CAF50, #8BC34A); height: 30px; border-radius: 20px; transition: width 0.5s; display: flex; align-items: center; justify-content: center; color: white; font-weight: bold; width: 0%; } .btn { padding: 15px 30px; border: none; border-radius: 10px; font-size: 1.1em; font-weight: bold; cursor: pointer; transition: all 0.3s; margin: 5px; } .btn-primary { background: linear-gradient(45deg, #4CAF50, #45a049); color: white; } .btn-danger { background: linear-gradient(45deg, #f44336, #d32f2f); color: white; } @media (max-width: 768px) { .controls { grid-template-columns: 1fr; } .status-grid { grid-template-columns: repeat(2, 1fr); } } </style> </head> <body> <div class="container"> <div class="header"> <h1>☕ SmartCoffee ESP32</h1> <p>Sistema de Automação Avançada</p> </div> <div class="status-grid"> <div class="status-card"> <div class="status-value" id="temperature">--°C</div> <div class="status-label">Temperatura</div> </div> <div class="status-card"> <div class="status-value" id="waterLevel">-- ml</div> <div class="status-label">Nível Água</div> </div> <div class="status-card"> <div class="status-value" id="power">-- W</div> <div class="status-label">Consumo</div> </div> <div class="status-card"> <div class="status-value" id="systemState">Carregando</div> <div class="status-label">Status</div> </div> </div> <div class="controls"> <div class="profiles-panel"> <h3>📋 Perfis de Café</h3> <div id="profilesList"></div> </div> <div class="brewing-panel"> <h3>🍵 Controle de Preparo</h3> <div class="progress-container"> <div class="progress-bar" id="progressBar">0%</div> </div> <div style="text-align: center; margin: 15px 0;"> <strong id="currentProfile">Selecione um perfil</strong> </div> <div style="text-align: center;"> <button class="btn btn-primary" id="startBtn" onclick="startBrewing()">▶️ Iniciar</button> <button class="btn btn-danger" id="stopBtn" onclick="stopBrewing()">⏹️ Parar</button> </div> </div> </div> </div> <script> let selectedProfile = 0; const profiles = [ {name: "Espresso", temp: 93, time: 25, water: 30}, {name: "Americano", temp: 90, time: 45, water: 120}, {name: "Filtrado", temp: 88, time: 240, water: 200}, {name: "Custom", temp: 90, time: 60, water: 100} ]; function loadProfiles() { const container = document.getElementById('profilesList'); container.innerHTML = ''; profiles.forEach((profile, index) => { const card = document.createElement('div'); card.className = `profile-card ${index === selectedProfile ? 'active' : ''}`; card.onclick = () => selectProfile(index); card.innerHTML = ` <strong>${profile.name}</strong><br> ${profile.temp}°C • ${profile.time}s • ${profile.water}ml `; container.appendChild(card); }); } function selectProfile(index) { selectedProfile = index; loadProfiles(); document.getElementById('currentProfile').textContent = profiles[index].name; } function updateDisplay() { fetch('/api/status') .then(response => response.json()) .then(data => { document.getElementById('temperature').textContent = data.temperature.toFixed(1) + '°C'; document.getElementById('waterLevel').textContent = data.waterLevel.toFixed(0) + ' ml'; document.getElementById('power').textContent = data.powerConsumption.toFixed(0) + ' W'; document.getElementById('systemState').textContent = data.currentState; if (data.brewingActive) { document.getElementById('progressBar').style.width = data.progressPercent + '%'; document.getElementById('progressBar').textContent = data.progressPercent.toFixed(1) + '%'; document.getElementById('startBtn').disabled = true; document.getElementById('stopBtn').disabled = false; } else { document.getElementById('progressBar').style.width = '0%'; document.getElementById('progressBar').textContent = '0%'; document.getElementById('startBtn').disabled = false; document.getElementById('stopBtn').disabled = true; } }) .catch(error => console.error('Erro:', error)); } function startBrewing() { fetch('/api/start', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({profile: selectedProfile}) }); } function stopBrewing() { fetch('/api/stop', {method: 'POST'}); } window.addEventListener('load', () => { loadProfiles(); updateDisplay(); setInterval(updateDisplay, 2000); }); </script> </body> </html> )rawliteral"; }
Servidor web e notificações
APIs essenciais:
Ver código completo
void initializeWebServer() { server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){ request->send_P(200, "text/html", getWebInterface()); }); server.on("/api/status", HTTP_GET, [](AsyncWebServerRequest *request){ DynamicJsonDocument doc(1024); doc["temperature"] = sensors.temperature; doc["waterLevel"] = sensors.waterLevel; doc["powerConsumption"] = sensors.powerConsumption; doc["currentState"] = systemStatus.currentState; doc["brewingActive"] = systemStatus.brewingActive; doc["progressPercent"] = systemStatus.progressPercent; String response; serializeJson(doc, response); request->send(200, "application/json", response); }); server.on("/api/start", HTTP_POST, [](AsyncWebServerRequest *request){ int profileIndex = 0; if (request->hasParam("profile", true)) { profileIndex = request->getParam("profile", true)->value().toInt(); } startCoffeeBrewing(profileIndex); request->send(200, "application/json", "{\"success\":true}"); }); server.on("/api/stop", HTTP_POST, [](AsyncWebServerRequest *request){ emergencyStop("Parada manual"); request->send(200, "application/json", "{\"success\":true}"); }); server.begin(); } void sendNotification(String title, String message) { Serial.println("[NOTIF] " + title + ": " + message); // Telegram (configurar token) if (WiFi.status() == WL_CONNECTED) { // Implementar envio Telegram } }
Instalação e configuração
Montagem física:
- Caixa eletrônica vedada com ESP32 e relés
- Sensor temperatura fixado na base da cafeteira
- Sensor fluxo na mangueira de água
- Relés de potência entre tomada e cafeteira
- Display OLED na frente da caixa
- Aterramento obrigatório e proteções elétricas
Configuração inicial:
Ver código completo
void initializeWiFi() { WiFi.begin("SUA_REDE", "SUA_SENHA"); while (WiFi.status() != WL_CONNECTED) { delay(1000); Serial.print("."); } Serial.println("WiFi conectado: " + WiFi.localIP().toString()); }
Análise de custos vs. comercial
Aspecto | DIY ESP32 | Cafeteira Premium |
---|---|---|
Preço | R$ 300-550 | R$ 2000-6000 |
Customização | 100% | 20% |
Sensores | 4+ sensores | 2 básicos |
Interface | Web profissional | App limitado |
Automação | Ilimitada | Básica |
Manutenção | DIY/Barata | Assistência cara |
- ✓ Economia: 80-85% vs. sistemas comerciais equivalentes
- ✓ Payback: 2-4 meses para usuário regular
Solução de problemas
Diagnósticos comuns:
Ver código completo
void runDiagnostics() { Serial.println("=== DIAGNÓSTICO ==="); Serial.println("WiFi: " + String(WiFi.status() == WL_CONNECTED ? "OK" : "FALHA")); Serial.println("Temperatura: " + String(sensors.temperature)); Serial.println("Relés: " + String(digitalRead(RELAY_PUMP_PIN)) + "/" + String(digitalRead(RELAY_HEATER_PIN))); Serial.println("=================="); }
Problemas frequentes:
Temperatura -127°C: Sensor desconectado
Sistema não liga: Verificar alimentação e proteções
Interface não carrega: Checar WiFi e IP
Relés não acionam: Verificar ligações e fusíveis
Conclusão
Com investimento de R$ 300-550, você constrói um sistema que supera cafeteiras de R$ 3000+:
Recursos implementados:
- Controle total de temperatura e fluxo
- Interface web profissional responsiva
- 4 perfis programáveis + customização
- Sistema de proteção e emergência
- Notificações automáticas
- Monitoramento energético
Vantagens competitivas:
- 80-85% economia vs. cafeteiras premium
- Customização ilimitada
- Upgrade contínuo conforme evolução
- Aprendizado técnico valioso
- Qualidade superior a sistemas comerciais
Próximos passos:
- Implemente gradualmente cada módulo
- Teste extensivamente antes de usar
- Documente suas modificações
- Monitore métricas para otimização
🎉 Parabéns! Série Completa Concluída
Nível 1: Básico
Timer + Tomada Smart
Nível 2: ESP32 Básico
Sensor + Interface Web
Nível 3: Profissional
Sistema Completo ✓
Agora você domina toda a progressão: do básico ao profissional!
Agora você tem o conhecimento completo para construir sua cafeteira inteligente profissional!