📚 Pré-requisito: Parte 1
Este artigo é a continuação da série. Se você ainda não fez a Parte 1, comece por lá para montar o hardware básico, calibrar os sensores e entender o funcionamento fundamental do sistema.
← Ir para a Parte 1: Montagem do HardwareNa Parte 1, criamos um sistema robusto e funcional que monitora pH, umidade e temperatura localmente. Agora vamos elevar o projeto a outro nível: conectividade total, interface web profissional e recursos IoT avançados.
Recapitulando a Parte 1: Sistema local com Arduino Uno, sensores calibrados, lógica de proteção e display LCD
Na Parte 2 (este artigo): ESP8266, WiFi, interface web, APIs, notificações e múltiplas zonas
Por que evoluir para IoT?
O sistema da Parte 1 já funciona perfeitamente para uso local, mas a conectividade adiciona funcionalidades profissionais:
- Monitoramento remoto: Acompanhe suas plantas de qualquer lugar
- Histórico completo: Gráficos e dados para otimização contínua
- Alertas inteligentes: Notificações automáticas no celular
- Interface profissional: Dashboard web responsivo
- Múltiplas zonas: Controle jardins inteiros
- Integração externa: APIs de clima e automação residencial
Resultado: Sistema profissional que compete com soluções comerciais de R$ 3000+
Material adicional necessário
Componentes para conectividade:
- ESP8266 NodeMCU v3 - R$ 25-45
- Módulo SD Card (opcional) - R$ 15-25
- Resistores pull-up 4.7kΩ (2x) - R$ 2-5
- Capacitores 100µF (2x) - R$ 3-8
Para múltiplas zonas (opcional):
- Módulo relé 8 canais - R$ 35-60
- Sensores umidade capacitivos adicionais (3x) - R$ 35-60
- Válvulas solenoides 12V (4x) - R$ 120-200
- Fonte 12V 5A - R$ 40-80
IMPORTANTE - Sensores capacitivos:
Mesmo em sistemas avançados, sempre utilize sensores de umidade do tipo capacitivo em vez dos resistivos tradicionais. Os capacitivos não sofrem corrosão, têm vida útil muito maior (anos vs. meses) e mantêm precisão em ambientes constantemente úmidos. O investimento adicional (R$ 12-20 por sensor) compensa rapidamente.
Custo adicional Parte 2: R$ 70-190 (básico) ou R$ 260-470 (sistema completo)
⚠️ Importante: As marcas mencionadas neste tutorial são citadas apenas para fins educativos e informativos. Não possuímos qualquer parceria, vínculo comercial ou patrocínio com essas empresas.
Arquitetura do sistema IoT
O sistema evoluído funciona em múltiplas camadas:
- Camada de sensores: Hardware da Parte 1 + ESP8266
- Camada de comunicação: WiFi local e internet
- Camada de aplicação: Servidor web embarcado
- Camada de dados: Armazenamento local e nuvem
- Camada de interface: Dashboard web responsivo
Migração do Arduino para ESP8266
1. Preparando o ambiente ESP8266
Instalar suporte ESP8266 no Arduino IDE:
- Arquivo > Preferências
- URLs adicionais: https://arduino.esp8266.com/stable/package_esp8266com_index.json
- Ferramentas > Placa > Gerenciador de Placas
- Instalar "esp8266 by ESP8266 Community"
Bibliotecas adicionais necessárias:
// Gerenciar Bibliotecas > Instalar: ESP8266WiFi // WiFi para ESP8266 ESPAsyncWebServer // Servidor web assíncrono ArduinoJson // Manipulação JSON NTPClient // Sincronização de horário WiFiManager // Configuração WiFi fácil
2. Conexões atualizadas ESP8266
ESP8266 NodeMCU Componente A0 → Sensor umidade solo (Analog) D0 (GPIO16) → Sensor pH (via divisor de tensão) D1 (GPIO5) → Sensor temperatura DS18B20 D2 (GPIO4) → SDA LCD I2C D3 (GPIO0) → SCL LCD I2C D4 (GPIO2) → Relé bomba principal D5-D8 → Relés zonas adicionais (opcional)
Atenção: ESP8266 opera em 3.3V - use divisores de tensão para sensores 5V
3. Código principal ESP8266
Ver código completo
#include <ESP8266WiFi.h> #include <ESPAsyncWebServer.h> #include <ArduinoJson.h> #include <LiquidCrystal_I2C.h> #include <OneWire.h> #include <DallasTemperature.h> #include <NTPClient.h> #include <WiFiUdp.h> #include <WiFiManager.h> // Configurações de pinos ESP8266 #define SOIL_SENSOR_PIN A0 #define PH_SENSOR_PIN_ANALOG A0 // Via multiplexer ou divisor #define TEMP_SENSOR_PIN D1 #define RELAY_PUMP_PIN D4 #define RELAY_ZONE1_PIN D5 #define RELAY_ZONE2_PIN D6 #define RELAY_ZONE3_PIN D7 #define RELAY_ZONE4_PIN D8 // Configurações do sistema #define MAX_ZONES 4 #define DATA_POINTS_MAX 2880 // 24h em intervalos de 30s #define WIFI_TIMEOUT 30000 #define API_UPDATE_INTERVAL 300000 // 5 minutos // Estrutura para dados de cada zona struct ZoneData { int soilMoisture; float phValue; float temperature; bool isWatering; unsigned long lastWatering; int dryThreshold; float phMin, phMax; bool enabled; String plantType; }; // Componentes globais LiquidCrystal_I2C lcd(0x27, 16, 2); OneWire oneWire(TEMP_SENSOR_PIN); DallasTemperature tempSensor(&oneWire); AsyncWebServer server(80); WiFiUDP ntpUDP; NTPClient timeClient(ntpUDP, "pool.ntp.org", -3*3600, 60000); // Dados das zonas ZoneData zones[MAX_ZONES]; int activeZones = 1; // Histórico de dados struct DataPoint { unsigned long timestamp; float ph, temperature; int moisture; bool watering; }; DataPoint dataHistory[DATA_POINTS_MAX]; int dataIndex = 0; // Variáveis de calibração pH float ph4_voltage = 3.2; float ph7_voltage = 2.5; // Status do sistema bool wifiConnected = false; String systemStatus = "Inicializando..."; unsigned long lastApiUpdate = 0; void setup() { Serial.begin(115200); initializeZones(); initializeHardware(); initializeWiFi(); initializeWebServer(); timeClient.begin(); Serial.println("Sistema IoT iniciado!"); systemStatus = "Sistema Online"; } void loop() { timeClient.update(); readAllSensors(); processZones(); updateDisplay(); storeDataPoint(); // Processar requisições web assíncronas yield(); delay(30000); // 30 segundos entre ciclos } void initializeZones() { // Zona 1 - Hortaliças (padrão) zones[0] = {0, 7.0, 25.0, false, 0, 300, 6.0, 7.5, true, "Hortaliças"}; // Zonas adicionais (desabilitadas inicialmente) for (int i = 1; i < MAX_ZONES; i++) { zones[i] = {0, 7.0, 25.0, false, 0, 300, 6.0, 7.5, false, "Inativa"}; } } void initializeHardware() { lcd.init(); lcd.backlight(); tempSensor.begin(); // Configurar relés pinMode(RELAY_PUMP_PIN, OUTPUT); pinMode(RELAY_ZONE1_PIN, OUTPUT); pinMode(RELAY_ZONE2_PIN, OUTPUT); pinMode(RELAY_ZONE3_PIN, OUTPUT); pinMode(RELAY_ZONE4_PIN, OUTPUT); // Desligar todos os relés digitalWrite(RELAY_PUMP_PIN, LOW); digitalWrite(RELAY_ZONE1_PIN, LOW); digitalWrite(RELAY_ZONE2_PIN, LOW); digitalWrite(RELAY_ZONE3_PIN, LOW); digitalWrite(RELAY_ZONE4_PIN, LOW </>); } void initializeWiFi() { lcd.setCursor(0, 0); lcd.print("Config. WiFi..."); WiFiManager wifiManager; // Timeout para configuração wifiManager.setConfigPortalTimeout(300); // Criar Access Point para configuração se necessário if (!wifiManager.autoConnect("PlantSystem_Setup")) { Serial.println("Falha na conexão WiFi - reiniciando"); ESP.restart(); } wifiConnected = true; Serial.println("WiFi conectado!"); Serial.print("IP local: "); Serial.println(WiFi.localIP()); lcd.clear(); lcd.setCursor(0, 0); lcd.print("WiFi: OK"); lcd.setCursor(0, 1); lcd.print(WiFi.localIP()); delay(3000); // Informações de acesso para o usuário Serial.println("=== ACESSO REMOTO ==="); Serial.println("Local: http://" + WiFi.localIP().toString()); Serial.println("Para acesso externo, configure:"); Serial.println("1. Port forwarding (porta 80) no roteador"); Serial.println("2. DNS dinâmico (DuckDNS, No-IP)"); Serial.println("3. Ou use VPN/Tailscale para acesso seguro"); Serial.println("===================="); } void readAllSensors() { tempSensor.requestTemperatures(); float globalTemp = tempSensor.getTempCByIndex(0); for (int i = 0; i < activeZones; i++) { if (!zones[i].enabled) continue; // Ler umidade (implementar multiplexing para múltiplas zonas) zones[i].soilMoisture = analogRead(SOIL_SENSOR_PIN); // Ler pH (mesmo sensor para todas as zonas no reservatório) float voltage = analogRead(SOIL_SENSOR_PIN) * (3.3 / 1024.0); zones[i].phValue = calculatePH(voltage, globalTemp); zones[i].temperature = globalTemp; } } float calculatePH(float voltage, float temperature) { // Compensação de temperatura float tempCompensation = (temperature - 25.0) * 0.03; // Calibração linear float slope = (7.0 - 4.0) / (ph7_voltage - ph4_voltage); float intercept = 7.0 - slope * ph7_voltage; float rawPH = slope * voltage + intercept; return rawPH - tempCompensation; } void processZones() { for (int i = 0; i < activeZones; i++) { if (!zones[i].enabled) continue; bool needsWater = evaluateWateringNeed(i); if (needsWater && !zones[i].isWatering) { startZoneWatering(i); } } } bool evaluateWateringNeed(int zoneIndex) { ZoneData &zone = zones[zoneIndex]; bool soilDry = zone.soilMoisture < zone.dryThreshold; bool phOK = (zone.phValue >= zone.phMin && zone.phValue <= zone.phMax); bool timeOK = (millis() - zone.lastWatering) > 3600000; // 1 hora bool tempOK = (zone.temperature > 5.0 && zone.temperature < 45.0); // Log de emergência if (soilDry && !phOK) { logEmergency(zoneIndex, "pH inadequado", zone.phValue); sendNotification("ALERTA", "Zona " + String(zoneIndex + 1) + " - pH inadequado: " + String(zone.phValue)); return false; } return soilDry && phOK && timeOK && tempOK; } void startZoneWatering(int zoneIndex) { zones[zoneIndex].isWatering = true; zones[zoneIndex].lastWatering = millis(); // Ativar bomba principal digitalWrite(RELAY_PUMP_PIN, HIGH); // Ativar relé da zona específica int relayPin = RELAY_ZONE1_PIN + zoneIndex; digitalWrite(relayPin, HIGH); Serial.println("Regando zona " + String(zoneIndex + 1)); systemStatus = "Regando zona " + String(zoneIndex + 1); // Rega por 5 segundos (ajustável via web) delay(5000); digitalWrite(relayPin, LOW); digitalWrite(RELAY_PUMP_PIN, LOW); zones[zoneIndex].isWatering = false; systemStatus = "Sistema Online"; logActivity(zoneIndex, "Rega automática concluída"); }
Interface Web Completa
1. Servidor web e endpoints
Ver código completo
void initializeWebServer() { // Servir página principal server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){ request->send_P(200, "text/html", getMainPage()); }); // API para dados dos sensores server.on("/api/sensors", HTTP_GET, [](AsyncWebServerRequest *request){ String json = generateSensorJSON(); request->send(200, "application/json", json); }); // API para histórico server.on("/api/history", HTTP_GET, [](AsyncWebServerRequest *request){ String json = generateHistoryJSON(); request->send(200, "application/json", json); }); // API para configuração server.on("/api/config", HTTP_POST, [](AsyncWebServerRequest *request){ // Processar dados de configuração handleConfigUpdate(request); }); // Rega manual server.on("/api/water", HTTP_POST, [](AsyncWebServerRequest *request){ int zone = request->arg("zone").toInt(); if (zone >= 0 && zone < activeZones) { startZoneWatering(zone); request->send(200, "application/json", "{"status":"ok"}"); } }); server.begin(); Serial.println("Servidor web iniciado"); } String generateSensorJSON() { DynamicJsonDocument doc(2048); doc["timestamp"] = timeClient.getEpochTime(); doc["systemStatus"] = systemStatus; doc["wifiStrength"] = WiFi.RSSI(); doc["localIP"] = WiFi.localIP().toString(); doc["uptime"] = millis(); JsonArray zonesArray = doc.createNestedArray("zones"); for (int i = 0; i < activeZones; i++) { if (!zones[i].enabled) continue; JsonObject zone = zonesArray.createNestedObject(); zone["id"] = i; zone["plantType"] = zones[i].plantType; zone["soilMoisture"] = zones[i].soilMoisture; zone["soilMoisturePercent"] = map(zones[i].soilMoisture, 0, 1023, 0, 100); zone["phValue"] = zones[i].phValue; zone["temperature"] = zones[i].temperature; zone["isWatering"] = zones[i].isWatering; zone["lastWatering"] = zones[i].lastWatering; zone["needsAttention"] = (zones[i].phValue < zones[i].phMin || zones[i].phValue > zones[i].phMax); } String jsonString; serializeJson(doc, jsonString); return jsonString; }
2. Interface HTML responsiva
Ver código completo
const char* getMainPage() { 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>Sistema de Rega Inteligente</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; padding: 20px; } .container { max-width: 1200px; margin: 0 auto; background: rgba(255, 255, 255, 0.95); border-radius: 20px; box-shadow: 0 20px 40px rgba(0,0,0,0.1); backdrop-filter: blur(10px); overflow: hidden; } .header { background: linear-gradient(45deg, #4CAF50, #45a049); color: white; padding: 30px; text-align: center; } .header h1 { font-size: 2.5em; margin-bottom: 10px; } .status-bar { display: flex; justify-content: space-between; align-items: center; padding: 20px; background: #f8f9fa; border-bottom: 1px solid #e9ecef; } .status-item { display: flex; align-items: center; gap: 10px; } .status-indicator { width: 12px; height: 12px; border-radius: 50%; background: #4CAF50; animation: pulse 2s infinite; } @keyframes pulse { 0% { box-shadow: 0 0 0 0 rgba(76, 175, 80, 0.7); } 70% { box-shadow: 0 0 0 10px rgba(76, 175, 80, 0); } 100% { box-shadow: 0 0 0 0 rgba(76, 175, 80, 0); } } .zones-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; padding: 30px; } .zone-card { background: white; border-radius: 15px; padding: 25px; box-shadow: 0 10px 30px rgba(0,0,0,0.1); transition: transform 0.3s ease, box-shadow 0.3s ease; border-left: 5px solid #4CAF50; } .zone-card:hover { transform: translateY(-5px); box-shadow: 0 15px 40px rgba(0,0,0,0.15); } .zone-card.warning { border-left-color: #ff9800; } .zone-card.error { border-left-color: #f44336; } .zone-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; } .zone-title { font-size: 1.4em; font-weight: 600; color: #333; } .zone-plant-type { background: #e8f5e8; color: #4CAF50; padding: 5px 12px; border-radius: 20px; font-size: 0.9em; } .sensor-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 15px; margin-bottom: 20px; } .sensor-item { text-align: center; padding: 15px; background: #f8f9fa; border-radius: 10px; } .sensor-value { font-size: 1.8em; font-weight: bold; color: #333; margin-bottom: 5px; } .sensor-label { font-size: 0.9em; color: #666; text-transform: uppercase; letter-spacing: 1px; } .action-buttons { display: flex; gap: 10px; } .btn { flex: 1; padding: 12px 20px; border: none; border-radius: 8px; font-weight: 600; cursor: pointer; transition: all 0.3s ease; } .btn-primary { background: #4CAF50; color: white; } .btn-primary:hover { background: #45a049; transform: translateY(-2px); } .btn-secondary { background: #6c757d; color: white; } .chart-container { margin: 30px; padding: 20px; background: white; border-radius: 15px; box-shadow: 0 10px 30px rgba(0,0,0,0.1); } .chart-title { font-size: 1.4em; font-weight: 600; margin-bottom: 20px; color: #333; } @media (max-width: 768px) { .zones-grid { grid-template-columns: 1fr; padding: 20px; } .sensor-grid { grid-template-columns: 1fr; } .action-buttons { flex-direction: column; } } </style> </head> <body> <div class="container"> <div class="header"> <h1>🌱 Sistema de Rega Inteligente</h1> <p>Monitoramento IoT com pH e múltiplas zonas</p> </div> <div class="status-bar"> <div class="status-item"> <div class="status-indicator"></div> <span>Sistema Online</span> </div> <div class="status-item"> <span>Última atualização: <span id="lastUpdate">--:--</span></span> </div> <div class="status-item"> <span>Acesso: <strong><span id="localIP">Carregando...</span></strong></span> </div> <div class="status-item"> <span>WiFi: <span id="wifiStrength">--</span> dBm</span> </div> </div> <div class="zones-grid" id="zonesContainer"> <!-- Zonas serão carregadas dinamicamente --> </div> <div class="chart-container"> <div class="chart-title">📊 Histórico das Últimas 24h</div> <canvas id="historyChart" width="400" height="200"></canvas> </div> <div class="chart-container"> <div class="chart-title">💾 Backup e Configuração</div> <div style="display: flex; gap: 10px; flex-wrap: wrap;"> <button class="btn btn-secondary" onclick="downloadBackup('csv')"> 📥 Download Dados CSV </button> <button class="btn btn-secondary" onclick="downloadBackup('config')"> ⚙️ Download Configuração </button> <button class="btn btn-primary" onclick="backupToCloud()"> ☁️ Backup Dropbox </button> </div> <div style="margin-top: 15px; padding: 15px; background: #f8f9fa; border-radius: 8px; font-size: 0.9em;"> <strong>🔗 Acesso Remoto:</strong><br> • <strong>Local:</strong> http://<span id="localIPAddress">---.---.---.---</span><br> • <strong>Externo:</strong> Configure port forwarding (porta 80) no roteador<br> • <strong>Seguro:</strong> Use DuckDNS, No-IP ou VPN (Tailscale recomendado) </div> </div> </div> <script> let sensorData = {}; function updateDisplay() { fetch('/api/sensors') .then(response => response.json()) .then(data => { sensorData = data; updateZones(data.zones); updateStatus(data); }) .catch(error => console.error('Erro:', error)); } function updateZones(zones) { const container = document.getElementById('zonesContainer'); container.innerHTML = ''; zones.forEach((zone, index) => { const card = createZoneCard(zone, index); container.appendChild(card); }); } function createZoneCard(zone, index) { const card = document.createElement('div'); card.className = `zone-card ${zone.needsAttention ? 'warning' : ''}`; card.innerHTML = ` <div class="zone-header"> <div class="zone-title">Zona ${index + 1}</div> <div class="zone-plant-type">${zone.plantType}</div> </div> <div class="sensor-grid"> <div class="sensor-item"> <div class="sensor-value">${zone.soilMoisturePercent}%</div> <div class="sensor-label">Umidade</div> </div> <div class="sensor-item"> <div class="sensor-value">${zone.phValue.toFixed(1)}</div> <div class="sensor-label">pH</div> </div> <div class="sensor-item"> <div class="sensor-value">${zone.temperature.toFixed(1)}°C</div> <div class="sensor-label">Temperatura</div> </div> </div> <div class="action-buttons"> <button class="btn btn-primary" onclick="waterZone(${index})"> 🚿 Regar Agora </button> <button class="btn btn-secondary" onclick="configZone(${index})"> ⚙️ Configurar </button> </div> `; return card; } function updateStatus(data) { document.getElementById('lastUpdate').textContent = new Date(data.timestamp * 1000).toLocaleTimeString(); document.getElementById('wifiStrength').textContent = data.wifiStrength; document.getElementById('localIP').textContent = data.localIP || 'Detectando...'; document.getElementById('localIPAddress').textContent = data.localIP || '---'; } function downloadBackup(type) { if (type === 'csv') { window.open('/api/backup?method=download', '_blank'); } else if (type === 'config') { fetch('/api/backup?method=config') .then(response => response.blob()) .then(blob => { const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'plant_system_config_' + new Date().toISOString().slice(0,10) + '.json'; a.click(); }); } } function backupToCloud() { if (confirm('Iniciar backup automático para Dropbox?')) { fetch('/api/backup?method=dropbox', {method: 'POST'}) .then(response => response.json()) .then(data => { alert('Backup iniciado! Verifique o log do sistema para confirmação.'); }); } } function waterZone(zoneIndex) { if (confirm(`Iniciar rega manual da Zona ${zoneIndex + 1}?`)) { fetch('/api/water', { method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'}, body: `zone=${zoneIndex}` }) .then(response => response.json()) .then(data => { alert('Rega iniciada com sucesso!'); updateDisplay(); }); } } function configZone(zoneIndex) { // Implementar modal de configuração alert(`Configuração da Zona ${zoneIndex + 1} - Em desenvolvimento`); } // Atualizar dados a cada 30 segundos setInterval(updateDisplay, 30000); // Carregar dados iniciais updateDisplay(); </script> </body> </html> )rawliteral"; }
Sistema de Notificações
1. Integração com serviços de notificação
Ver código completo
#include <ESP8266HTTPClient.h> // Configurações de notificação String telegramBotToken = "SUA_BOT_TOKEN"; String telegramChatId = "SEU_CHAT_ID"; String webhookUrl = ""; // Para integrações personalizadas void sendNotification(String title, String message) { if (telegramBotToken.length() > 0) { sendTelegramMessage(title + ": " + message); } if (webhookUrl.length() > 0) { sendWebhook(title, message); } // Log local sempre logActivity(-1, "NOTIFICAÇÃO: " + title + " - " + message); } void sendTelegramMessage(String message) { WiFiClient client; HTTPClient http; String url = "https://api.telegram.org/bot" + telegramBotToken + "/sendMessage"; http.begin(client, url); http.addHeader("Content-Type", "application/json"); DynamicJsonDocument doc(512); doc["chat_id"] = telegramChatId; doc["text"] = "🌱 " + message; doc["parse_mode"] = "Markdown"; String jsonString; serializeJson(doc, jsonString); int httpCode = http.POST(jsonString); if (httpCode > 0) { Serial.println("Telegram enviado: " + String(httpCode)); } http.end(); } void logEmergency(int zoneIndex, String issue, float value) { String emergency = "EMERGÊNCIA Zona " + String(zoneIndex + 1) + ": " + issue + " = " + String(value); Serial.println(emergency); sendNotification("EMERGÊNCIA", emergency); // Salvar em log persistente se SD card disponível // saveToSDLog(emergency); } void logActivity(int zoneIndex, String activity) { String timestamp = timeClient.getFormattedTime(); String logEntry = "[" + timestamp + "] "; if (zoneIndex >= 0) { logEntry += "Zona " + String(zoneIndex + 1) + ": "; } logEntry += activity; Serial.println(logEntry); // Adicionar ao histórico em memória // Implementar rotação de logs se necessário }
2. Integração com APIs externas
Ver código completo
void updateWeatherData() { WiFiClient client; HTTPClient http; // Usando OpenWeatherMap API (gratuita) String apiKey = "SUA_API_KEY_OPENWEATHER"; String city = "Guimaraes,PT"; String url = "http://api.openweathermap.org/data/2.5/weather?q=" + city + "&appid=" + apiKey + "&units=metric"; http.begin(client, url); int httpCode = http.GET(); if (httpCode == 200) { String payload = http.getString(); DynamicJsonDocument doc(1024); deserializeJson(doc, payload); float humidity = doc["main"]["humidity"]; float rainProbability = 0; // Ajustar thresholds baseado no clima if (humidity > 80) { // Clima muito úmido - reduzir rega for (int i = 0; i < activeZones; i++) { zones[i].dryThreshold = zones[i].dryThreshold * 0.8; } systemStatus = "Modo clima úmido ativo"; } // Verificar previsão de chuva (API adicional necessária) checkRainForecast(); } http.end(); } void checkRainForecast() { // Implementar integração com API de previsão // Se chuva prevista nas próximas 6h, suspender rega WiFiClient client; HTTPClient http; String apiKey = "SUA_API_KEY_OPENWEATHER"; String city = "Guimaraes,PT"; String url = "http://api.openweathermap.org/data/2.5/forecast?q=" + city + "&appid=" + apiKey + "&units=metric&cnt=8"; http.begin(client, url); int httpCode = http.GET(); if (httpCode == 200) { String payload = http.getString(); DynamicJsonDocument doc(4096); deserializeJson(doc, payload); JsonArray list = doc["list"]; bool rainExpected = false; for (int i = 0; i < 2; i++) { // Próximas 6 horas if (list[i]["weather"][0]["main"] == "Rain") { rainExpected = true; break; } } if (rainExpected) { systemStatus = "Rega suspensa - chuva prevista"; sendNotification("INFO", "Rega automática suspensa devido à previsão de chuva"); // Suspender rega por 6 horas for (int i = 0; i < activeZones; i++) { zones[i].lastWatering = millis(); // Reset timer } } } http.end(); }
Armazenamento de dados e histórico
1. Sistema de dados em memória e SD Card
Ver código completo
void storeDataPoint() { // Armazenar ponto de dados no histórico circular dataHistory[dataIndex] = { timeClient.getEpochTime(), zones[0].phValue, zones[0].temperature, zones[0].soilMoisture, zones[0].isWatering }; dataIndex = (dataIndex + 1) % DATA_POINTS_MAX; // Salvar no SD Card a cada 10 pontos (5 minutos) if (dataIndex % 10 == 0) { saveToSDCard(); } } void saveToSDCard() { // Implementar se módulo SD disponível /* File dataFile = SD.open("plant_data.csv", FILE_WRITE); if (dataFile) { String dataString = String(timeClient.getEpochTime()) + ","; dataString += String(zones[0].phValue) + ","; dataString += String(zones[0].temperature) + ","; dataString += String(zones[0].soilMoisture) + ","; dataString += String(zones[0].isWatering ? 1 : 0); dataFile.println(dataString); dataFile.close(); // Auto-backup para cloud a cada 100 linhas static int backupCounter = 0; if (++backupCounter >= 100) { uploadToCloud(); backupCounter = 0; } } */ } void uploadToCloud() { // Upload automático para Dropbox via API WiFiClient client; HTTPClient http; // Ler arquivo SD completo File dataFile = SD.open("plant_data.csv", FILE_READ); if (!dataFile) return; String csvContent = dataFile.readString(); dataFile.close(); // Upload para Dropbox (requer token API) String dropboxToken = "SUA_DROPBOX_TOKEN"; http.begin(client, "https://content.dropboxapi.com/2/files/upload"); http.addHeader("Authorization", "Bearer " + dropboxToken); http.addHeader("Dropbox-API-Arg", "{"path":"/PlantSystem/backup_" + String(timeClient.getEpochTime()) + ".csv","mode":"add"}"); http.addHeader("Content-Type", "application/octet-stream"); int httpCode = http.POST(csvContent); if (httpCode == 200) { Serial.println("Backup Dropbox realizado com sucesso"); logActivity(-1, "Backup automático enviado para Dropbox"); } http.end(); } // API para backup manual server.on("/api/backup", HTTP_GET, [](AsyncWebServerRequest *request){ String method = request->arg("method"); if (method == "download") { // Download direto do CSV File dataFile = SD.open("plant_data.csv", FILE_READ); if (dataFile) { request->send(dataFile, "plant_data.csv", "text/csv"); dataFile.close(); } else { request->send(404, "text/plain", "Arquivo não encontrado"); } } else if (method == "dropbox") { uploadToCloud(); request->send(200, "application/json", "{"status":"backup_initiated"}"); } else if (method == "config") { // Backup da configuração String config = generateConfigBackup(); request->send(200, "application/json", config); } }); String generateConfigBackup() { DynamicJsonDocument doc(2048); doc["timestamp"] = timeClient.getEpochTime(); doc["version"] = "2.0"; doc["device_id"] = WiFi.macAddress(); // Configurações do sistema doc["activeZones"] = activeZones; doc["ph4_voltage"] = ph4_voltage; doc["ph7_voltage"] = ph7_voltage; // Configurações das zonas JsonArray zonesArray = doc.createNestedArray("zones"); for (int i = 0; i < activeZones; i++) { JsonObject zone = zonesArray.createNestedObject(); zone["plantType"] = zones[i].plantType; zone["dryThreshold"] = zones[i].dryThreshold; zone["phMin"] = zones[i].phMin; zone["phMax"] = zones[i].phMax; zone["enabled"] = zones[i].enabled; } String backup; serializeJson(doc, backup); return backup; } String generateHistoryJSON() { DynamicJsonDocument doc(8192); JsonArray data = doc.createNestedArray("data"); // Últimas 24 horas (720 pontos máximo) int pointsToSend = min(720, DATA_POINTS_MAX); int startIndex = (dataIndex - pointsToSend + DATA_POINTS_MAX) % DATA_POINTS_MAX; for (int i = 0; i < pointsToSend; i++) { int index = (startIndex + i) % DATA_POINTS_MAX; if (dataHistory[index].timestamp > 0) { JsonObject point = data.createNestedObject(); point["timestamp"] = dataHistory[index].timestamp; point["ph"] = dataHistory[index].ph; point["temperature"] = dataHistory[index].temperature; point["moisture"] = dataHistory[index].moisture; point["watering"] = dataHistory[index].watering; } } String jsonString; serializeJson(doc, jsonString); return jsonString; }
2. Interface web com gráficos Chart.js
Ver código completo
// Adicionar ao HTML principal - seção de scripts const char* getChartScript() { return R"rawliteral( <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.9.1/chart.min.js"></script> <script> let chart; function initChart() { const ctx = document.getElementById('historyChart').getContext('2d'); chart = new Chart(ctx, { type: 'line', data: { labels: [], datasets: [{ label: 'pH', data: [], borderColor: '#4CAF50', backgroundColor: 'rgba(76, 175, 80, 0.1)', yAxisID: 'y' }, { label: 'Umidade (%)', data: [], borderColor: '#2196F3', backgroundColor: 'rgba(33, 150, 243, 0.1)', yAxisID: 'y1' }, { label: 'Temperatura (°C)', data: [], borderColor: '#FF9800', backgroundColor: 'rgba(255, 152, 0, 0.1)', yAxisID: 'y2' }] }, options: { responsive: true, maintainAspectRatio: false, interaction: { mode: 'index', intersect: false, }, scales: { x: { type: 'time', time: { unit: 'hour', displayFormats: { hour: 'HH:mm' } } }, y: { type: 'linear', display: true, position: 'left', min: 0, max: 14, title: { display: true, text: 'pH' } }, y1: { type: 'linear', display: true, position: 'right', min: 0, max: 100, title: { display: true, text: 'Umidade (%)' }, grid: { drawOnChartArea: false, } }, y2: { type: 'linear', display: false, min: 0, max: 50 } } } }); } function updateChart() { fetch('/api/history') .then(response => response.json()) .then(data => { const labels = data.data.map(point => new Date(point.timestamp * 1000)); const phData = data.data.map(point => point.ph); const moistureData = data.data.map(point => point.moisture); const tempData = data.data.map(point => point.temperature); chart.data.labels = labels; chart.data.datasets[0].data = phData; chart.data.datasets[1].data = moistureData; chart.data.datasets[2].data = tempData; chart.update(); }); } // Inicializar gráfico window.addEventListener('load', () => { initChart(); updateChart(); setInterval(updateChart, 60000); // Atualizar a cada minuto }); </script> )rawliteral"; }
Sistema multi-zonas avançado
1. Multiplexador para múltiplos sensores
Ver código completo
// Usando CD4051 multiplexer para 8 sensores de umidade #define MUX_S0 D5 #define MUX_S1 D6 #define MUX_S2 D7 #define MUX_ENABLE D8 void initializeMultiplexer() { pinMode(MUX_S0, OUTPUT); pinMode(MUX_S1, OUTPUT); pinMode(MUX_S2, OUTPUT); pinMode(MUX_ENABLE, OUTPUT); digitalWrite(MUX_ENABLE, LOW); // Habilitar multiplexer } int readSoilMoisture(int channel) { // Selecionar canal do multiplexer digitalWrite(MUX_S0, (channel & 0x01)); digitalWrite(MUX_S1, (channel & 0x02) >> 1); digitalWrite(MUX_S2, (channel & 0x04) >> 2); delay(10); // Aguardar estabilização return analogRead(SOIL_SENSOR_PIN); } void readAllSensorsMultiplexed() { tempSensor.requestTemperatures(); float globalTemp = tempSensor.getTempCByIndex(0); // Ler pH uma vez (mesmo reservatório) float voltage = analogRead(PH_SENSOR_PIN_ANALOG) * (3.3 / 1024.0); float globalPH = calculatePH(voltage, globalTemp); // Ler umidade de cada zona via multiplexer for (int i = 0; i < activeZones; i++) { if (!zones[i].enabled) continue; zones[i].soilMoisture = readSoilMoisture(i); zones[i].phValue = globalPH; // Mesmo pH para todas zones[i].temperature = globalTemp; } }
2. Configuração específica por tipo de planta
Ver código completo
struct PlantProfile { String name; int moistureMin, moistureMax; float phMin, phMax; int wateringDuration; int wateringInterval; String description; }; PlantProfile plantProfiles[] = { {"Hortaliças", 400, 700, 6.0, 7.5, 5000, 3600000, "Alface, tomate, pepino"}, {"Suculentas", 200, 400, 6.5, 7.5, 2000, 7200000, "Cactos, echeveria, jade"}, {"Tropicais", 500, 800, 5.5, 6.5, 8000, 1800000, "Antúrio, bromélia, filodendro"}, {"Ácidas", 450, 750, 5.0, 6.0, 6000, 2700000, "Azaleia, hortênsia, mirtilo"}, {"Aromáticas", 300, 600, 6.5, 7.5, 4000, 3600000, "Manjericão, alecrim, tomilho"}, {"Orquídeas", 250, 500, 5.5, 6.5, 3000, 5400000, "Phalaenopsis, cattleya"} }; void applyPlantProfile(int zoneIndex, int profileIndex) { if (profileIndex < 0 || profileIndex >= 6) return; PlantProfile &profile = plantProfiles[profileIndex]; ZoneData &zone = zones[zoneIndex]; zone.plantType = profile.name; zone.dryThreshold = profile.moistureMin; zone.phMin = profile.phMin; zone.phMax = profile.phMax; // Salvar configuração (implementar EEPROM/preferências) saveZoneConfig(zoneIndex); logActivity(zoneIndex, "Perfil aplicado: " + profile.name); } // API endpoint para configuração server.on("/api/configure", HTTP_POST, [](AsyncWebServerRequest *request){ int zone = request->arg("zone").toInt(); int profile = request->arg("profile").toInt(); if (zone >= 0 && zone < activeZones && profile >= 0 && profile < 6) { applyPlantProfile(zone, profile); request->send(200, "application/json", "{"status":"ok"}"); } else { request->send(400, "application/json", "{"error":"Invalid parameters"}"); } });
Recursos avançados e automações
1. Sistema de fertilização automatizada
Ver código completo
#define FERTILIZER_PUMP_PIN D9 #define PH_UP_PUMP_PIN D10 #define PH_DOWN_PUMP_PIN D11 struct FertilizerSchedule { int dayOfWeek; int hour; int duration; bool enabled; }; FertilizerSchedule fertilizerSchedule = {1, 6, 2000, true}; // Segunda, 6h, 2s void checkFertilizerSchedule() { if (!fertilizerSchedule.enabled) return; time_t now = timeClient.getEpochTime(); struct tm* timeinfo = localtime(&now); if (timeinfo->tm_wday == fertilizerSchedule.dayOfWeek && timeinfo->tm_hour == fertilizerSchedule.hour && timeinfo->tm_min == 0 && timeinfo->tm_sec < 30) { startFertilization(); } } void startFertilization() { logActivity(-1, "Iniciando fertilização automática"); sendNotification("INFO", "Fertilização semanal iniciada"); digitalWrite(FERTILIZER_PUMP_PIN, HIGH); delay(fertilizerSchedule.duration); digitalWrite(FERTILIZER_PUMP_PIN, LOW); logActivity(-1, "Fertilização concluída"); } void adjustPH() { float currentPH = zones[0].phValue; float targetPH = (zones[0].phMin + zones[0].phMax) / 2.0; if (currentPH < targetPH - 0.3) { // pH muito baixo - adicionar pH UP digitalWrite(PH_UP_PUMP_PIN, HIGH); delay(1000); digitalWrite(PH_UP_PUMP_PIN, LOW); logActivity(-1, "Correção pH UP aplicada"); } else if (currentPH > targetPH + 0.3) { // pH muito alto - adicionar pH DOWN digitalWrite(PH_DOWN_PUMP_PIN, HIGH); delay(1000); digitalWrite(PH_DOWN_PUMP_PIN, LOW); logActivity(-1, "Correção pH DOWN aplicada"); } }
2. Integração com assistentes virtuais
Ver código completo
// Integração com Google Assistant/Alexa via IFTTT void setupIFTTTIntegration() { server.on("/api/ifttt/trigger", HTTP_POST, [](AsyncWebServerRequest *request){ String action = request->arg("action"); String zone = request->arg("zone"); if (action == "water") { int zoneNum = zone.toInt(); if (zoneNum >= 0 && zoneNum < activeZones) { startZoneWatering(zoneNum); request->send(200, "text/plain", "Rega iniciada na zona " + zone); } } else if (action == "status") { String status = "Zona 1: pH " + String(zones[0].phValue) + ", Umidade " + String(zones[0].soilMoisture) + ", Temp " + String(zones[0].temperature) + "°C"; request->send(200, "text/plain", status); } }); } // Webhook para IFTTT void triggerIFTTTEvent(String eventName, String value1, String value2, String value3) { WiFiClient client; HTTPClient http; String iftttKey = "SUA_IFTTT_KEY"; String url = "https://maker.ifttt.com/trigger/" + eventName + "/with/key/" + iftttKey; http.begin(client, url); http.addHeader("Content-Type", "application/json"); DynamicJsonDocument doc(256); doc["value1"] = value1; doc["value2"] = value2; doc["value3"] = value3; String jsonString; serializeJson(doc, jsonString); http.POST(jsonString); http.end(); }
3. Sistema de backup e recuperação
Ver código completo
void saveSystemConfig() { DynamicJsonDocument doc(2048); // Configurações gerais doc["activeZones"] = activeZones; doc["ph4_voltage"] = ph4_voltage; doc["ph7_voltage"] = ph7_voltage; // Configurações das zonas JsonArray zonesArray = doc.createNestedArray("zones"); for (int i = 0; i < activeZones; i++) { JsonObject zone = zonesArray.createNestedObject(); zone["plantType"] = zones[i].plantType; zone["dryThreshold"] = zones[i].dryThreshold; zone["phMin"] = zones[i].phMin; zone["phMax"] = zones[i].phMax; zone["enabled"] = zones[i].enabled; } // Salvar em EEPROM/Flash String configString; serializeJson(doc, configString); // Implementar salvamento persistente // EEPROM.begin(2048); // EEPROM.put(0, configString); // EEPROM.commit(); } void loadSystemConfig() { // Implementar carregamento do EEPROM // String configString; // EEPROM.get(0, configString); // Aplicar configurações carregadas // DynamicJsonDocument doc(2048); // deserializeJson(doc, configString); }
Solução de problemas avançados
1. Diagnóstico automático
Ver código completo
struct SystemDiagnostic { bool wifiConnection; bool sensorsWorking; bool pumpsWorking; bool calibrationValid; String lastError; unsigned long uptime; }; SystemDiagnostic runDiagnostic() { SystemDiagnostic result; // Teste WiFi result.wifiConnection = (WiFi.status() == WL_CONNECTED); // Teste sensores result.sensorsWorking = (zones[0].temperature > -50 && zones[0].temperature < 80); // Teste calibração pH result.calibrationValid = (abs(ph7_voltage - ph4_voltage) > 0.3); // Uptime result.uptime = millis(); if (!result.wifiConnection) { result.lastError = "WiFi desconectado"; } else if (!result.sensorsWorking) { result.lastError = "Sensores com problema"; } else if (!result.calibrationValid) { result.lastError = "Calibração pH inválida"; } else { result.lastError = "Sistema OK"; } return result; } // API para diagnóstico server.on("/api/diagnostic", HTTP_GET, [](AsyncWebServerRequest *request){ SystemDiagnostic diag = runDiagnostic(); DynamicJsonDocument doc(512); doc["wifi"] = diag.wifiConnection; doc["sensors"] = diag.sensorsWorking; doc["calibration"] = diag.calibrationValid; doc["error"] = diag.lastError; doc["uptime"] = diag.uptime; doc["freeMemory"] = ESP.getFreeHeap(); String response; serializeJson(doc, response); request->send(200, "application/json", response); });
2. Sistema de recuperação automática
Ver código completo
void handleSystemFailure() { SystemDiagnostic diag = runDiagnostic(); if (!diag.wifiConnection) { // Tentar reconectar WiFi WiFi.reconnect(); delay(5000); if (WiFi.status() != WL_CONNECTED) { // Reiniciar WiFiManager se falha persistir WiFiManager wifiManager; wifiManager.autoConnect("PlantSystem_Recovery"); } } if (!diag.sensorsWorking) { // Desabilitar rega automática temporariamente systemStatus = "Modo segurança - sensores com problema"; sendNotification("ALERTA", "Sistema em modo segurança - verificar sensores"); } if (!diag.calibrationValid) { // Solicitar recalibração systemStatus = "Recalibração necessária"; sendNotification("ALERTA", "Sensor pH precisa ser recalibrado"); } } // Verificar saúde do sistema a cada 5 minutos void checkSystemHealth() { static unsigned long lastHealthCheck = 0; if (millis() - lastHealthCheck > 300000) { // 5 minutos SystemDiagnostic diag = runDiagnostic(); if (!diag.wifiConnection || !diag.sensorsWorking || !diag.calibrationValid) { handleSystemFailure(); } lastHealthCheck = millis(); } }
Análise de custos completa
Comparação investimento vs. benefícios
Item | Investimento | Sistemas Comerciais |
---|---|---|
Parte 1 | R$ 320-560 | - |
Parte 2 básica | R$ 70-190 | - |
Parte 2 completa | R$ 260-470 | - |
Total | R$ 390-1030 | - |
Básico com pH | - | R$ 1500-2500 |
Profissional multi-zona | - | R$ 3000-8000+ |
Economia | 60-85% | - |
ROI estimado:
- Redução perda de plantas: R$ 200-500/ano
- Otimização crescimento: 20-40% aumento produtividade
- Economia tempo: 5-10h/mês
- Payback: 6-18 meses
Próximas evoluções possíveis
1. Integração com IA
Ver código completo
// Conceito para machine learning local void analyzeGrowthPatterns() { // Analisar correlações entre: // - pH vs crescimento // - Frequência rega vs saúde planta // - Temperatura vs absorção nutrientes // Sugerir otimizações automáticas // Detectar padrões anômalos // Predição de necessidades futuras }
2. Câmeras e visão computacional
Ver código completo
// ESP32-CAM para monitoramento visual void plantHealthAnalysis() { // Detectar: // - Amarelamento folhas // - Pragas e doenças // - Crescimento e floração // - Stress hídrico visual }
3. Sensores adicionais
- Condutividade elétrica (EC): Nutrientes dissolvidos
- Oxigênio dissolvido: Saúde raízes
- Luz PAR: Fotossíntese
- CO2: Crescimento otimizado
Conclusão da Parte 2
Transformamos nosso sistema básico em uma solução IoT profissional e completa. As principais conquistas desta segunda parte:
Recursos implementados:
- Conectividade WiFi robusta com ESP8266
- Interface web responsiva e profissional
- Sistema de múltiplas zonas configuráveis
- Notificações automáticas (Telegram, webhooks)
- Integração com APIs meteorológicas
- Histórico de dados com gráficos interativos
- Perfis específicos por tipo de planta
- Sistema de diagnóstico e recuperação
- Controle remoto completo via celular
Benefícios obtidos:
- Monitoramento 24/7 de qualquer lugar
- Dados científicos para otimização contínua
- Proteção automática contra condições adversas
- Escalabilidade para jardins inteiros
- Interface igual a sistemas profissionais
- Economia de 60-85% vs. soluções comerciais
Sistema final oferece:
- Precisão científica no monitoramento
- Automação inteligente baseada em dados
- Flexibilidade total de configuração
- Integração com casa inteligente
- Manutenção mínima e operação autônoma
Próximos passos recomendados
- Teste por 2-4 semanas para validar estabilidade
- Configure notificações para seu celular
- Analise dados históricos para otimizações
- Experimente perfis diferentes de plantas
- Documente resultados para melhorias futuras
Recursos opcionais para futuro
- Fertilização automatizada com dosadores precisos
- Câmeras ESP32-CAM para monitoramento visual
- Sensores EC e PAR para análise avançada
- Machine learning para predições inteligentes
- Integração total com Google Home/Alexa
Dica final: Mantenha logs detalhados das primeiras semanas. Esses dados serão valiosos para calibrar o sistema às necessidades específicas das suas plantas e ambiente.
Parabéns! Você agora possui um sistema de automação para plantas com nível profissional, gastando uma fração do preço de soluções comerciais. Suas plantas nunca estiveram em melhores mãos!