#include <Arduino.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <RTClib.h>
#include <Preferences.h>
/************ Configurações ************/
#define NUM_ZONES 6
#define MAX_PROGS 8
const int RELAY_PINS[NUM_ZONES] = {12, 13, 14, 27, 26, 25};
bool RELAY_ACTIVE_LOW = false;
// Botões
const int BTN_UP = 16;
const int BTN_DOWN = 17;
const int BTN_OK = 19;
const int BTN_BACK = 18;
// Sensor de chuva
const int RAIN_PIN = 32;
bool RAIN_LOW_MEANS_WET = true;
// LCD + RTC
LiquidCrystal_I2C lcd(0x27, 16, 2);
RTC_DS3231 rtc;
Preferences prefs;
/************ Estruturas ************/
struct Program {
uint8_t startHour;
uint8_t startMinute;
uint8_t endHour;
uint8_t endMinute;
uint8_t daysMask;
uint8_t enabled;
};
struct ZoneCfg {
Program progs[MAX_PROGS];
char name[8]; // Nome mais curto
};
ZoneCfg zones[NUM_ZONES];
// Execução
int currentZone = -1;
unsigned long currentEndEpoch = 0;
// Fila
const int QMAX = 8; // Reduzido para evitar problemas
int qZones[QMAX];
int qDurMin[QMAX];
int qHead=0, qTail=0;
// Botões
unsigned long lastBtnMs = 0;
const unsigned long DEBOUNCE_MS = 200; // Aumentado para evitar travamento
// Telas
enum Screen {
SCR_HOME, SCR_MENU, SCR_CONFIG_POINTS, SCR_SELECT_DAYS,
SCR_SET_START_TIME, SCR_SET_START_MIN, SCR_SET_END_TIME, SCR_SET_END_MIN, SCR_SAVE_PROG,
SCR_VIEW_SCHEDULE, SCR_ZONE_STATUS, SCR_SIMULATION_PANEL
};
Screen screen = SCR_HOME;
// Seletor
int selZone = 0;
int selProg = 0;
int selDay = 0;
// Variáveis temporárias
int tmpStartHour = 0, tmpStartMin = 0;
int tmpEndHour = 0, tmpEndMin = 15;
uint8_t tmpDays = 0x7F;
int lastCheckedMinute = -1;
// Simulação
bool simulationMode = false;
int simHour = 12;
int simMinute = 0;
int simSecond = 0;
bool simRain = false;
// Controle de tempo
unsigned long lastUpdate = 0;
const unsigned long UPDATE_INTERVAL = 1000; // 1 segundo
/************ Enum botões ************/
enum Btn { BTN_NONE, PRESS_UP, PRESS_DOWN, PRESS_OK, PRESS_BACK };
/************ Funções utilitárias ************/
bool qEmpty() { return qHead==qTail; }
int qNext(int x){ return (x+1)%QMAX; }
bool qFull() { return qNext(qTail)==qHead; }
void qPush(int z, int dur) {
if (qFull()) return;
qZones[qTail]=z; qDurMin[qTail]=dur; qTail=qNext(qTail);
}
bool qPop(int &z, int &dur) {
if (qEmpty()) return false;
z=qZones[qHead]; dur=qDurMin[qHead]; qHead=qNext(qHead); return true;
}
void relayWrite(int zone, bool on) {
if (zone < 0 || zone >= NUM_ZONES) return; // Proteção
int pin = RELAY_PINS[zone];
if (RELAY_ACTIVE_LOW) digitalWrite(pin, on ? LOW : HIGH);
else digitalWrite(pin, on ? HIGH : LOW);
}
void allOff() {
for (int i=0;i<NUM_ZONES;i++) relayWrite(i,false);
currentZone = -1;
currentEndEpoch = 0;
}
unsigned long nowEpoch() {
if (simulationMode) {
return simHour * 3600UL + simMinute * 60UL + simSecond;
} else {
DateTime now = rtc.now();
return now.unixtime();
}
}
bool isRainWet() {
if (simulationMode) {
return simRain;
} else {
int v = digitalRead(RAIN_PIN);
return RAIN_LOW_MEANS_WET ? (v==LOW) : (v==HIGH);
}
}
/************ Dias da Semana ************/
struct DaysOpt { uint8_t mask; const char* name; };
DaysOpt DAYS_OPTS[] = {
{0x7F, "Todos"}, {0x3E, "Uteis"}, {0x41, "FDS"},
{0x01, "Dom"}, {0x02, "Seg"}, {0x04, "Ter"},
{0x08, "Qua"}, {0x10, "Qui"}, {0x20, "Sex"}, {0x40, "Sab"}
};
const int DAYS_OPTS_N = sizeof(DAYS_OPTS)/sizeof(DaysOpt);
const char* daysMaskName(uint8_t mask) {
for (int i=0;i<DAYS_OPTS_N;i++)
if (DAYS_OPTS[i].mask==mask) return DAYS_OPTS[i].name;
return "Perso";
}
/************ Verificação de Sobreposição ************/
bool checkOverlap(int zone, int startH, int startM, int endH, int endM, uint8_t days) {
int startMinutes = startH * 60 + startM;
int endMinutes = endH * 60 + endM;
for (int z = 0; z < NUM_ZONES; z++) {
if (z == zone) continue;
for (int p = 0; p < MAX_PROGS; p++) {
Program &pr = zones[z].progs[p];
if (!pr.enabled) continue;
if (!(pr.daysMask & days)) continue;
int prStartMinutes = pr.startHour * 60 + pr.startMinute;
int prEndMinutes = pr.endHour * 60 + pr.endMinute;
if ((startMinutes < prEndMinutes) && (endMinutes > prStartMinutes)) {
return true;
}
}
}
return false;
}
/************ Persistência ************/
void saveConfig() {
prefs.begin("sched", false);
prefs.putBytes("zones", zones, sizeof(zones));
prefs.end();
}
void clearAllConfig() {
for (int z=0; z<NUM_ZONES; z++) {
snprintf(zones[z].name, sizeof(zones[z].name), "Z%d", z+1);
for (int p=0; p<MAX_PROGS; p++) {
zones[z].progs[p].startHour = 0;
zones[z].progs[p].startMinute = 0;
zones[z].progs[p].endHour = 0;
zones[z].progs[p].endMinute = 0;
zones[z].progs[p].daysMask = 0x00;
zones[z].progs[p].enabled = 0;
}
}
saveConfig();
}
void loadConfig() {
prefs.begin("sched", true);
size_t n = prefs.getBytes("zones", zones, sizeof(zones));
prefs.end();
if (n != sizeof(zones)) {
clearAllConfig();
}
}
/************ Execução ************/
void startZone(int z, int durMin) {
if (z < 0 || z >= NUM_ZONES) return; // Proteção
allOff();
currentZone = z;
relayWrite(z, true);
currentEndEpoch = nowEpoch() + (unsigned long)durMin * 60UL;
}
void tickRunner() {
unsigned long now = nowEpoch();
if (currentZone >= 0 && now >= currentEndEpoch) {
relayWrite(currentZone, false);
currentZone = -1;
}
if (currentZone < 0 && !qEmpty()) {
int z, d;
qPop(z, d);
startZone(z, d);
}
}
void checkSchedules() {
int minuteNow, wday, h, m;
if (simulationMode) {
minuteNow = simMinute;
wday = 1; // Segunda-feira para simulação
h = simHour;
m = simMinute;
} else {
DateTime dt = rtc.now();
minuteNow = dt.minute();
wday = dt.dayOfTheWeek();
h = dt.hour();
m = dt.minute();
}
if (minuteNow == lastCheckedMinute) return;
lastCheckedMinute = minuteNow;
if (isRainWet()) return;
for (int z=0; z<NUM_ZONES; z++) {
for (int p=0; p<MAX_PROGS; p++) {
Program &pr = zones[z].progs[p];
if (!pr.enabled) continue;
if (!(pr.daysMask & (1 << wday))) continue;
if (pr.startHour == h && pr.startMinute == m) {
int duration = (pr.endHour * 60 + pr.endMinute) - (pr.startHour * 60 + pr.startMinute);
if (duration > 0) {
qPush(z, duration);
}
}
}
}
}
/************ Botões ************/
Btn readButton() {
unsigned long now = millis();
if (now - lastBtnMs < DEBOUNCE_MS) return BTN_NONE;
if (digitalRead(BTN_UP)==LOW) { lastBtnMs=now; return PRESS_UP; }
if (digitalRead(BTN_DOWN)==LOW) { lastBtnMs=now; return PRESS_DOWN; }
if (digitalRead(BTN_OK)==LOW) { lastBtnMs=now; return PRESS_OK; }
if (digitalRead(BTN_BACK)==LOW) { lastBtnMs=now; return PRESS_BACK; }
return BTN_NONE;
}
/************ Telas ************/
void drawHome() {
lcd.clear();
if (simulationMode) {
char line1[17];
snprintf(line1, sizeof(line1), "SIM %02d:%02d:%02d", simHour, simMinute, simSecond);
lcd.setCursor(0,0); lcd.print(line1);
} else {
DateTime now = rtc.now();
char line1[17];
snprintf(line1, sizeof(line1), "%02d:%02d %02d/%02d",
now.hour(), now.minute(), now.day(), now.month());
lcd.setCursor(0,0); lcd.print(line1);
}
lcd.setCursor(0,1);
if (currentZone >= 0) {
long left = (long)currentEndEpoch - (long)nowEpoch();
if (left < 0) left = 0;
int mm = left / 60;
char buf[17];
snprintf(buf, sizeof(buf), "Z%d %02d:%02d", currentZone+1, mm, 0);
lcd.print(buf);
} else {
lcd.print("Sistema Parado");
}
}
void drawMenu(int idx) {
const char* items[] = {"Manual", "Config", "Agendas", "Status", "Simul", "Limpar", "Salvar"};
const int N=7;
if (idx<0) idx=0; if (idx>=N) idx=N-1;
lcd.clear();
lcd.setCursor(0,0); lcd.print("MENU:");
lcd.setCursor(0,1);
if (idx < N) {
lcd.print(">");
lcd.print(items[idx]);
}
}
void drawSimulationPanel() {
lcd.clear();
lcd.setCursor(0,0); lcd.print("SIMULACAO");
lcd.setCursor(0,1);
lcd.print(simHour);
lcd.print(":");
if (simMinute < 10) lcd.print("0");
lcd.print(simMinute);
lcd.print(" Chuva:");
lcd.print(simRain ? "SIM" : "NAO");
}
void drawConfigPoints() {
lcd.clear();
lcd.setCursor(0,0); lcd.print("CONFIG PONTOS");
lcd.setCursor(0,1);
lcd.print("Z");
lcd.print(selZone+1);
lcd.print(" Progs:");
int activeProgs = 0;
for (int p=0; p<MAX_PROGS; p++) {
if (zones[selZone].progs[p].enabled) {
activeProgs++;
}
}
lcd.print(activeProgs);
lcd.print("/");
lcd.print(MAX_PROGS);
}
void drawSelectDays() {
lcd.clear();
lcd.setCursor(0,0); lcd.print("SELECIONAR DIAS");
lcd.setCursor(0,1);
lcd.print("Z");
lcd.print(selZone+1);
lcd.print(" ");
lcd.print(daysMaskName(tmpDays));
}
void drawSetStartTime() {
lcd.clear();
lcd.setCursor(0,0); lcd.print("HORA INICIO");
lcd.setCursor(0,1);
lcd.print("Z");
lcd.print(selZone+1);
lcd.print(" ");
lcd.print(tmpStartHour);
lcd.print(":00 OK=Min");
}
void drawSetStartMin() {
lcd.clear();
lcd.setCursor(0,0); lcd.print("MIN INICIO");
lcd.setCursor(0,1);
lcd.print("Z");
lcd.print(selZone+1);
lcd.print(" ");
lcd.print(tmpStartHour);
lcd.print(":");
if (tmpStartMin < 10) lcd.print("0");
lcd.print(tmpStartMin);
}
void drawSetEndTime() {
lcd.clear();
lcd.setCursor(0,0); lcd.print("HORA FIM");
lcd.setCursor(0,1);
lcd.print("Z");
lcd.print(selZone+1);
lcd.print(" ");
lcd.print(tmpEndHour);
lcd.print(":00 OK=Min");
}
void drawSetEndMin() {
lcd.clear();
lcd.setCursor(0,0); lcd.print("MIN FIM");
lcd.setCursor(0,1);
lcd.print("Z");
lcd.print(selZone+1);
lcd.print(" ");
lcd.print(tmpEndHour);
lcd.print(":");
if (tmpEndMin < 10) lcd.print("0");
lcd.print(tmpEndMin);
}
void drawSaveProg() {
lcd.clear();
lcd.setCursor(0,0); lcd.print("SALVAR PROG");
lcd.setCursor(0,1);
lcd.print("Z");
lcd.print(selZone+1);
lcd.print(" P");
lcd.print(selProg+1);
lcd.print(" OK=Salvar");
}
void drawViewSchedule() {
lcd.clear();
lcd.setCursor(0,0); lcd.print("AGENDAMENTOS");
lcd.setCursor(0,1);
lcd.print("Z");
lcd.print(selZone+1);
int count = 0;
for (int p=0; p<MAX_PROGS && count<1; p++) {
Program &pr = zones[selZone].progs[p];
if (pr.enabled) {
lcd.print(" P");
lcd.print(p+1);
lcd.print(":");
lcd.print(pr.startHour);
lcd.print(":");
if (pr.startMinute < 10) lcd.print("0");
lcd.print(pr.startMinute);
count++;
}
}
if (count == 0) {
lcd.print(" Nenhum");
}
}
void drawZoneStatus() {
lcd.clear();
lcd.setCursor(0,0); lcd.print("STATUS SETORES");
lcd.setCursor(0,1);
lcd.print("Z");
lcd.print(selZone+1);
lcd.print(" ");
lcd.print(currentZone == selZone ? "LIGADO" : "DESLIGADO");
}
/************ Setup ************/
void setup() {
Serial.begin(115200);
Serial.println("Sistema Irrigacao - Estavel");
// Configurar pinos
for (int i=0;i<NUM_ZONES;i++) {
pinMode(RELAY_PINS[i], OUTPUT);
relayWrite(i,false);
}
pinMode(BTN_UP, INPUT_PULLUP);
pinMode(BTN_DOWN, INPUT_PULLUP);
pinMode(BTN_OK, INPUT_PULLUP);
pinMode(BTN_BACK, INPUT_PULLUP);
pinMode(RAIN_PIN, INPUT_PULLUP);
lcd.init(); lcd.backlight();
lcd.clear();
lcd.setCursor(0,0); lcd.print("Sistema Irrigacao");
lcd.setCursor(0,1); lcd.print("ESP32 - Estavel");
Wire.begin();
if (!rtc.begin()) {
lcd.setCursor(0,1); lcd.print("RTC NAO encontrado");
simulationMode = true;
} else if (rtc.lostPower()) {
rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
lcd.setCursor(0,1); lcd.print("RTC ajustado");
}
loadConfig();
delay(1000);
drawHome();
}
/************ Loop Principal ************/
void loop() {
unsigned long now = millis();
// Controle de tempo para evitar travamento
if (now - lastUpdate >= UPDATE_INTERVAL) {
lastUpdate = now;
// Simulação de tempo
if (simulationMode) {
simSecond++;
if (simSecond >= 60) {
simSecond = 0;
simMinute++;
if (simMinute >= 60) {
simMinute = 0;
simHour++;
if (simHour >= 24) simHour = 0;
}
}
}
checkSchedules();
tickRunner();
// Só atualiza a tela home se estiver na tela home
if (screen == SCR_HOME) {
drawHome();
}
}
Btn b = readButton();
static int menuIdx = 0;
switch (screen) {
case SCR_HOME:
if (b==PRESS_OK) {
screen = SCR_MENU;
drawMenu(menuIdx=0);
}
else if (b==PRESS_UP) {
if (simulationMode) {
screen = SCR_SIMULATION_PANEL;
drawSimulationPanel();
} else {
screen = SCR_ZONE_STATUS;
selZone = 0;
drawZoneStatus();
}
}
break;
case SCR_MENU:
if (b==PRESS_UP) {
menuIdx--;
if(menuIdx<0) menuIdx=6;
drawMenu(menuIdx);
}
if (b==PRESS_DOWN) {
menuIdx++;
if(menuIdx>6) menuIdx=0;
drawMenu(menuIdx);
}
if (b==PRESS_BACK) {
screen=SCR_HOME;
drawHome();
}
if (b==PRESS_OK) {
switch(menuIdx) {
case 0: // Manual
screen=SCR_CONFIG_POINTS;
selZone=0;
drawConfigPoints();
break;
case 1: // Config
screen=SCR_CONFIG_POINTS;
selZone=0;
drawConfigPoints();
break;
case 2: // Agendas
screen=SCR_VIEW_SCHEDULE;
selZone=0;
drawViewSchedule();
break;
case 3: // Status
screen=SCR_ZONE_STATUS;
selZone=0;
drawZoneStatus();
break;
case 4: // Simul
screen=SCR_SIMULATION_PANEL;
drawSimulationPanel();
break;
case 5: // Limpar
clearAllConfig();
screen=SCR_HOME;
drawHome();
break;
case 6: // Salvar
saveConfig();
screen=SCR_HOME;
drawHome();
break;
}
}
break;
case SCR_SIMULATION_PANEL:
if (b==PRESS_UP) {
simHour++;
if (simHour >= 24) simHour = 0;
drawSimulationPanel();
}
if (b==PRESS_DOWN) {
simHour--;
if (simHour < 0) simHour = 23;
drawSimulationPanel();
}
if (b==PRESS_OK) {
simRain = !simRain;
drawSimulationPanel();
}
if (b==PRESS_BACK) {
screen=SCR_HOME;
drawHome();
}
break;
case SCR_CONFIG_POINTS:
if (b==PRESS_UP) {
selZone++;
if (selZone>=NUM_ZONES) selZone=0;
drawConfigPoints();
}
if (b==PRESS_DOWN) {
selZone--;
if (selZone<0) selZone=NUM_ZONES-1;
drawConfigPoints();
}
if (b==PRESS_OK) {
selProg = 0;
for (int p=0; p<MAX_PROGS; p++) {
if (!zones[selZone].progs[p].enabled) {
selProg = p;
break;
}
}
tmpDays = 0x7F;
selDay = 0;
screen = SCR_SELECT_DAYS;
drawSelectDays();
}
if (b==PRESS_BACK) {
screen=SCR_MENU;
drawMenu(menuIdx);
}
break;
case SCR_SELECT_DAYS:
if (b==PRESS_UP) {
selDay++;
if (selDay >= DAYS_OPTS_N) selDay = 0;
tmpDays = DAYS_OPTS[selDay].mask;
drawSelectDays();
}
if (b==PRESS_DOWN) {
selDay--;
if (selDay < 0) selDay = DAYS_OPTS_N - 1;
tmpDays = DAYS_OPTS[selDay].mask;
drawSelectDays();
}
if (b==PRESS_OK) {
tmpStartHour = 0;
tmpStartMin = 0;
tmpEndHour = 0;
tmpEndMin = 15;
screen = SCR_SET_START_TIME;
drawSetStartTime();
}
if (b==PRESS_BACK) {
screen=SCR_CONFIG_POINTS;
drawConfigPoints();
}
break;
case SCR_SET_START_TIME:
if (b==PRESS_UP) {
tmpStartHour++;
if (tmpStartHour >= 24) tmpStartHour = 0;
drawSetStartTime();
}
if (b==PRESS_DOWN) {
tmpStartHour--;
if (tmpStartHour < 0) tmpStartHour = 23;
drawSetStartTime();
}
if (b==PRESS_OK) {
screen = SCR_SET_START_MIN;
drawSetStartMin();
}
if (b==PRESS_BACK) {
screen=SCR_SELECT_DAYS;
drawSelectDays();
}
break;
case SCR_SET_START_MIN:
if (b==PRESS_UP) {
tmpStartMin++;
if (tmpStartMin >= 60) tmpStartMin = 0;
drawSetStartMin();
}
if (b==PRESS_DOWN) {
tmpStartMin--;
if (tmpStartMin < 0) tmpStartMin = 59;
drawSetStartMin();
}
if (b==PRESS_OK) {
tmpEndHour = tmpStartHour;
tmpEndMin = tmpStartMin + 15;
if (tmpEndMin >= 60) {
tmpEndMin -= 60;
tmpEndHour++;
if (tmpEndHour >= 24) tmpEndHour = 0;
}
screen = SCR_SET_END_TIME;
drawSetEndTime();
}
if (b==PRESS_BACK) {
screen=SCR_SET_START_TIME;
drawSetStartTime();
}
break;
case SCR_SET_END_TIME:
if (b==PRESS_UP) {
tmpEndHour++;
if (tmpEndHour >= 24) tmpEndHour = 0;
drawSetEndTime();
}
if (b==PRESS_DOWN) {
tmpEndHour--;
if (tmpEndHour < 0) tmpEndHour = 23;
drawSetEndTime();
}
if (b==PRESS_OK) {
screen = SCR_SET_END_MIN;
drawSetEndMin();
}
if (b==PRESS_BACK) {
screen=SCR_SET_START_MIN;
drawSetStartMin();
}
break;
case SCR_SET_END_MIN:
if (b==PRESS_UP) {
tmpEndMin++;
if (tmpEndMin >= 60) tmpEndMin = 0;
drawSetEndMin();
}
if (b==PRESS_DOWN) {
tmpEndMin--;
if (tmpEndMin < 0) tmpEndMin = 59;
drawSetEndMin();
}
if (b==PRESS_OK) {
Program &pr = zones[selZone].progs[selProg];
pr.startHour = tmpStartHour;
pr.startMinute = tmpStartMin;
pr.endHour = tmpEndHour;
pr.endMinute = tmpEndMin;
pr.daysMask = tmpDays;
pr.enabled = 1;
screen = SCR_CONFIG_POINTS;
drawConfigPoints();
}
if (b==PRESS_BACK) {
screen=SCR_SET_END_TIME;
drawSetEndTime();
}
break;
case SCR_VIEW_SCHEDULE:
if (b==PRESS_UP) {
selZone++;
if (selZone>=NUM_ZONES) selZone=0;
drawViewSchedule();
}
if (b==PRESS_DOWN) {
selZone--;
if (selZone<0) selZone=NUM_ZONES-1;
drawViewSchedule();
}
if (b==PRESS_BACK) {
screen=SCR_MENU;
drawMenu(menuIdx);
}
break;
case SCR_ZONE_STATUS:
if (b==PRESS_UP) {
selZone++;
if (selZone>=NUM_ZONES) selZone=0;
drawZoneStatus();
}
if (b==PRESS_DOWN) {
selZone--;
if (selZone<0) selZone=NUM_ZONES-1;
drawZoneStatus();
}
if (b==PRESS_BACK) {
screen=SCR_HOME;
drawHome();
}
break;
}
}