📚 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!