// Projet: S.E.N.S.E.I. (Système Électronique de Niveau et Surveillance d'Eau Intelligent)
// Version: 2025-05-31 10:00:00 - Correction: Ligne 0 LCD2 (4 espaces fixes au début pour clignotement)
// Auteur: Daniel Talbot, Technicien pour C.I.P.A.D et Ez-Weekend-Projects
// Description: Système de surveillance intelligent du niveau d'eau avec contrôle de pompe, alarme et menu interactif.
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <RTClib.h>
#include <RotaryEncoder.h>
#include <Encoder.h>
#include <Ultrasonic.h>
#include <EEPROM.h>
// --- Définition des écrans LCD ---
LiquidCrystal_I2C lcd1(0x27, 20, 4), lcd2(0x26, 20, 4), lcdDebug(0x25, 20, 4);
// --- Autres composants ---
RTC_DS1307 rtc;
// L'encodeur rotatif KY-040 : CLK -> D2, DT -> D3, SW -> D4 (sur l'Arduino Uno)
#define ENCODER_CLK_PIN 2
#define ENCODER_DT_PIN 3
#define ENCODER_SW_PIN 4 // BROCHE DU BOUTON ENCODEUR (Bouton SELECT)
Encoder myEncoder(ENCODER_CLK_PIN, ENCODER_DT_PIN);
// Capteur ultrasonique HC-SR04 : TRIG -> D6, ECHO -> D5
#define ULTRASONIC_TRIG_PIN 6
#define ULTRASONIC_ECHO_PIN 5
Ultrasonic ultrasonic(ULTRASONIC_TRIG_PIN, ULTRASONIC_ECHO_PIN);
// Buzzer et Relais (Broches définies par votre circuit Wokwi)
#define BUZZER_PIN 7
#define RELAY_PIN 8
// --- Adresses EEPROM pour la sauvegarde des paramètres ---
#define EEPROM_PUMP_MODE_ADDR 0
#define EEPROM_LEVEL_HIGH_ADDR 1
#define EEPROM_LEVEL_LOW_ADDR 5
#define EEPROM_VERSION_ADDR 10
#define EEPROM_CURRENT_VERSION 9 // Version actuelle de la structure des données EEPROM (INC. POUR RE-CONFIRMATION)
// --- Variable globale pour la gestion des lignes de débogage ---
int debugLine = 0;
// --- Variables pour l'encodeur (rotation) ---
long oldPosition = -999;
const int ENCODER_DIRECTION = -1; // 1 pour CW incrémente, -1 pour CW décrémente (pour inverser le sens physique)
// --- Variables pour la détection d'appui unique du Bouton SELECT ---
unsigned long lastButtonActionTime = 0;
const long BUTTON_DEBOUNCE_MS = 200; // Délai d'anti-rebond plus long pour le bouton
bool buttonCurrentlyPressed = false;
bool buttonWasHandled = false;
// --- Variables pour la surveillance du niveau d'eau ---
const float RESERVOIR_HAUTEUR_CM = 400.0;
// --- Variables d'état pour la pompe et l'alarme ---
bool pompeActive = false;
bool alarmeActive = false;
bool lastPompeDisplayedState = false; // Stocke le dernier état de la pompe affiché sur LCD2 ligne 3
// --- Variables pour le clignotement de l'alarme LCD (pour le débordement uniquement) ---
unsigned long previousMillisAlarme = 0;
const long intervalAlarme = 300;
bool alarmeTextVisible = true;
// --- NOUVELLES VARIABLES POUR LE CLIGNOTEMENT DU MENU ---
unsigned long previousMillisMenu = 0;
const long intervalMenuBlink = 400; // Intervalle de clignotement pour le menu (en ms)
bool menuTextVisible = true; // État de visibilité de l'option sélectionnée
// --- Variables pour la gestion des messages temporaires non bloquants ---
unsigned long messageDisplayStartTime = 0;
const long MESSAGE_DISPLAY_DURATION_MS = 1500; // Durée d'affichage du message (1.5 secondes)
bool messageIsActive = false;
// --- DÉFINITION DES CARACTÈRES PERSONNALISÉS POUR LA BARRE DE PROGRESSION (Goutte d'eau) ---
byte emptyDrop[8] = {
0b00100, 0b01010, 0b10001, 0b10001, 0b10001, 0b10001, 0b01010, 0b00100
};
byte quarterDrop[8] = {
0b00100, 0b01010, 0b10001, 0b10001, 0b10001, 0b11111, 0b01110, 0b00100
};
byte halfDrop[8] = {
0b00100, 0b01010, 0b10001, 0b10001, 0b11111, 0b11111, 0b01110, 0b00100
};
byte threeQuarterDrop[8] = {
0b00100, 0b01010, 0b10001, 0b11111, 0b11111, 0b11111, 0b01110, 0b00100
};
byte fullDrop[8] = {
0b00100, 0b01110, 0b11111, 0b11111, 0b11111, 0b11111, 0b01110, 0b00100
};
// --- DÉFINITION DES MODES DE CONTRÔLE DE POMPE (SOUS-ÉTAT) ---
enum PumpControlMode {
PUMP_MODE_AUTO,
PUMP_MODE_MANUAL
};
PumpControlMode currentPumpMode = PUMP_MODE_AUTO; // Le système démarre en mode automatique de pompe
PumpControlMode lastDisplayedPumpMode = PUMP_MODE_AUTO; // Pour rafraîchir l'affichage du mode de pompe seulement s'il change
// --- NOUVEAUX ÉTATS DU SYSTÈME GLOBAL DU S.E.N.S.E.I. ---
enum SystemState {
STATE_NORMAL_DISPLAY, // Affichage normal du niveau d'eau, etc.
STATE_MAIN_MENU, // Affichage du menu principal sur LCD2
STATE_SET_PUMP_MODE, // Sous-menu pour changer le mode de la pompe
STATE_SET_LEVELS, // Sous-menu pour régler les niveaux (haut/bas)
STATE_SET_TIME // Sous-menu pour régler la date/heure
};
SystemState currentSystemState = STATE_NORMAL_DISPLAY; // Le système démarre en affichage normal
SystemState lastSystemState = STATE_NORMAL_DISPLAY; // Pour détecter les changements d'état
// --- Variables pour le menu principal ---
// Chaînes de caractères SANS espaces additionnels, alignement par défaut (gauche)
const char* mainMenuOptions[] = {
"Mode Pompe",
"Regler Niveaux",
"Regler Heure/Date",
"Quitter Menu"
};
const int NUM_MAIN_MENU_OPTIONS = sizeof(mainMenuOptions) / sizeof(mainMenuOptions[0]);
int selectedMenuOption = 0; // Index de l'option actuellement sélectionnée dans le menu
int lastSelectedMenuOption = -1; // Pour détecter si l'option sélectionnée a changé
// --- Variables pour le sous-menu MODE POMPE ---
// Chaînes de caractères ajustées SANS les espaces avant le texte
const char* pumpModeOptions[] = {
"Automatique",
"Manuel",
"Activer/Desact."
};
const int NUM_PUMP_MODE_OPTIONS = sizeof(pumpModeOptions) / sizeof(pumpModeOptions[0]);
int selectedPumpModeOption = 0; // 0 for Automatique, 1 for Manuel, 2 for Toggle Manual Pump
int lastSelectedPumpModeOption = -1; // Pour détecter si l'option sélectionnée a changé
// --- Variables pour le sous-menu REGLER NIVEAUX ---
float reservoirLevelHigh = 80.0; // Seuil haut par défaut en % (ou en cm si on préfère, mais % est plus intuitif)
float reservoirLevelLow = 20.0; // Seuil bas par défaut en %
float lastReservoirLevelHigh = -1.0; // Pour détecter les changements
float lastReservoirLevelLow = -1.0; // Pour détecter les changements
// État interne du sous-menu "Régler Niveaux"
enum SetLevelsSubState {
SET_LEVEL_HIGH, // En train de régler le niveau haut
SET_LEVEL_LOW, // En train de régler le niveau bas
SET_LEVEL_EXIT // Prêt à quitter ou valider
};
SetLevelsSubState currentSetLevelsSubState = SET_LEVEL_HIGH; // Commence par régler le niveau haut
SetLevelsSubState lastSetLevelsSubState = SET_LEVEL_HIGH; // Pour détecter les changements
// État interne du sous-menu "Régler Heure/Date"
enum SetTimeSubState {
SET_HOUR,
SET_MINUTE,
SET_DAY,
SET_MONTH,
SET_YEAR,
SET_TIME_EXIT // Option pour valider et quitter
};
SetTimeSubState currentSetTimeSubState = SET_HOUR; // Declare currentSetTimeSubState globally
// --- Variables pour le sous-menu REGLER HEURE/DATE ---
// Variables temporaires pour le réglage de l'heure et de la date
int tempSetHour;
int tempSetMinute;
int tempSetSecond; // Généralement 0 pour le réglage manuel
int tempSetDay;
int tempSetMonth;
int tempSetYear;
// For detecting changes in the Time/Date submenu
int lastTempSetHour;
int lastTempSetMinute;
int lastTempSetDay;
int lastTempSetMonth;
int lastTempSetYear;
SetTimeSubState lastSetTimeSubState = SET_HOUR; // For detecting changes
// --- Fonctions utilitaires pour lcdDebug ---
void debugClearLine(int line) {
// Cette fonction efface une ligne spécifique de LCDDebug.
// Elle est plus flexible que debugClear()
lcdDebug.setCursor(0, line);
lcdDebug.print(" "); // 20 espaces pour effacer la ligne
}
void debugPrint(int line, const char* message) {
// Affiche un message sur une ligne spécifique de LCDDebug.
// Le message est tronqué si trop long, et la ligne est remplie d'espaces.
lcdDebug.setCursor(0, line);
lcdDebug.print(message);
for (int i = strlen(message); i < 20; i++) {
lcdDebug.print(" "); // Remplir le reste de la ligne avec des espaces
}
}
// --- Fonctions utilitaires pour le centrage du texte sur les LCDs ---
void centerText(LiquidCrystal_I2C& lcd, int row, const char* text) {
int len = strlen(text);
if (len > 20) len = 20; // S'assurer que la longueur ne dépasse pas la largeur de l'écran
int startCol = (20 - len) / 2;
lcd.setCursor(startCol, row);
lcd.print(text);
for (int i = startCol + len; i < 20; i++) {
lcd.print(" "); // Remplir le reste de la ligne avec des espaces pour effacer le contenu précédent
}
}
// --- Fonction utilitaire pour la direction de l'encodeur ---
// Retourne +1 pour incrémentation, -1 pour décrémentation, 0 si pas de changement
int getEncoderDirection() {
long newPosition = myEncoder.read();
if (newPosition != oldPosition) {
int direction = 0;
if (newPosition > oldPosition) {
direction = 1; // Rotation CW
} else {
direction = -1; // Rotation CCW
}
oldPosition = newPosition; // Met à jour l'ancienne position
return direction * ENCODER_DIRECTION; // Applique la correction de direction
}
return 0; // Pas de changement
}
// --- Fonction utilitaire pour le nombre de jours dans un mois ---
int daysInMonth(int month, int year) {
switch (month) {
case 1: case 3: case 5: case 7: case 8: case 10: case 12:
return 31;
case 4: case 6: case 9: case 11:
return 30;
case 2: // Février
// Année bissextile si divisible par 4, sauf si divisible par 100 mais pas par 400
if ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)) {
return 29;
} else {
return 28;
}
default:
return 0; // Mois invalide
}
}
// --- Fonctions de gestion de l'EEPROM ---
void saveParameters() {
debugPrint(2, "Saving EEPROM..."); // Ligne 2 pour les messages de sauvegarde
EEPROM.put(EEPROM_PUMP_MODE_ADDR, (byte)currentPumpMode); // Sauvegarder l'enum comme un byte
EEPROM.put(EEPROM_LEVEL_HIGH_ADDR, reservoirLevelHigh);
EEPROM.put(EEPROM_LEVEL_LOW_ADDR, reservoirLevelLow);
EEPROM.put(EEPROM_VERSION_ADDR, (byte)EEPROM_CURRENT_VERSION); // Sauvegarder la version
debugPrint(2, "EEPROM Saved.");
}
void loadParameters() {
debugPrint(2, "Loading EEPROM..."); // Ligne 2 pour les messages de chargement
byte storedVersion;
EEPROM.get(EEPROM_VERSION_ADDR, storedVersion);
if (storedVersion == EEPROM_CURRENT_VERSION) {
debugPrint(2, "EEPROM Version OK.");
byte loadedPumpMode;
EEPROM.get(EEPROM_PUMP_MODE_ADDR, loadedPumpMode);
currentPumpMode = (PumpControlMode)loadedPumpMode; // Cast back to enum
EEPROM.get(EEPROM_LEVEL_HIGH_ADDR, reservoirLevelHigh);
EEPROM.get(EEPROM_LEVEL_LOW_ADDR, reservoirLevelLow);
debugPrint(2, "EEPROM Loaded.");
// Appliquer les valeurs chargées pour les seuils de pompe, avec contraintes
reservoirLevelHigh = constrain(reservoirLevelHigh, 0.0, 100.0);
reservoirLevelLow = constrain(reservoirLevelLow, 0.0, 100.0);
if (reservoirLevelLow >= reservoirLevelHigh - 5.0) { // S'assurer d'une marge minimum de 5%
reservoirLevelLow = reservoirLevelHigh - 5.0;
if (reservoirLevelLow < 0.0) reservoirLevelLow = 0.0;
}
} else {
debugPrint(2, "EEPROM New/Old Ver.");
debugPrint(2, "Default params used.");
// Initialiser avec les valeurs par défaut si aucune version valide n'est trouvée
currentPumpMode = PUMP_MODE_AUTO;
reservoirLevelHigh = 80.0;
reservoirLevelLow = 20.0;
saveParameters(); // Sauvegarder les valeurs par défaut une fois pour les prochaines fois
}
}
// --- Fonctions de contrôle du S.E.N.S.E.I. ---
void activerPompe() {
if (!pompeActive) {
digitalWrite(RELAY_PIN, HIGH);
pompeActive = true;
// Mise à jour de la ligne 3 du debug LCD avec l'état de la pompe
debugPrint(3, "Pompe: ON");
}
}
void desactiverPompe() {
if (pompeActive) {
digitalWrite(RELAY_PIN, LOW);
pompeActive = false;
// Mise à jour de la ligne 3 du debug LCD avec l'état de la pompe
debugPrint(3, "Pompe: OFF");
}
}
void activerAlarme() {
tone(BUZZER_PIN, 1000);
if (!alarmeActive) {
alarmeActive = true;
debugPrint(3, "Alarme: ACTIVE!"); // Ligne 3 pour l'état de l'alarme
}
unsigned long currentMillis = millis();
if (currentMillis - previousMillisAlarme >= intervalAlarme) {
previousMillisAlarme = currentMillis;
lcd2.setCursor(0, 2);
if (alarmeTextVisible) {
lcd2.print("!!! DEBORDEMENT !!!");
} else {
lcd2.print(" "); // 20 espaces pour effacer
}
alarmeTextVisible = !alarmeTextVisible;
}
}
void desactiverAlarme() {
if (alarmeActive) {
noTone(BUZZER_PIN);
alarmeActive = false;
debugPrint(3, "Alarme: INACTIVE."); // Ligne 3 pour l'état de l'alarme
}
lcd2.setCursor(0, 2);
lcd2.print(" "); // Efface la ligne d'alarme
alarmeTextVisible = true;
}
// --- Fonction pour afficher le mode de contrôle de pompe actuel sur LCD1 ---
void displayPumpControlMode() {
lcd1.setCursor(0, 1); // Ligne 1 de LCD1 pour le mode
char modeStr[21];
if (currentPumpMode == PUMP_MODE_AUTO) {
sprintf(modeStr, "MODE: AUTOMATIQUE");
} else { // PUMP_MODE_MANUAL
sprintf(modeStr, "MODE: MANUEL");
}
centerText(lcd1, 1, modeStr); // Utilise centerText pour la ligne 1
lastDisplayedPumpMode = currentPumpMode;
}
// --- Fonctions de gestion du menu principal ---
// Cette fonction est appelée UNIQUEMENT lors d'un changement d'option ou d'une entrée/sortie du menu
void redrawMainMenu(bool forceClear = false) {
if (forceClear) {
lcd2.clear(); // Efface l'écran LCD2 pour le menu
for (int i = 0; i < NUM_MAIN_MENU_OPTIONS; i++) {
lcd2.setCursor(0, i);
lcd2.print(mainMenuOptions[i]); // Affiche toutes les options initialement
// Remplir le reste de la ligne avec des espaces pour éviter les artefacts d'affichages précédents
for (int k = strlen(mainMenuOptions[i]); k < 20; k++) {
lcd2.print(" ");
}
}
}
// Ne met à jour que la ligne qui clignote
int targetRow = selectedMenuOption;
// Efface d'abord la ligne sélectionnée (pour éviter les artefacts)
lcd2.setCursor(0, targetRow);
lcd2.print(" "); // 20 espaces pour effacer la ligne complète
if (menuTextVisible) {
// Affiche l'option sélectionnée si elle est visible, alignée à gauche (colonne 0)
lcd2.setCursor(0, targetRow);
lcd2.print(mainMenuOptions[selectedMenuOption]);
}
}
// --- Fonction: Affichage du sous-menu Mode Pompe ---
void drawSetPumpModeMenu(bool forceClear = false) {
if (forceClear) {
lcd2.clear(); // Efface l'écran LCD2 pour le sous-menu
// Afficher le titre du sous-menu
centerText(lcd2, 0, "--- Mode Pompe ---");
// Afficher toutes les options une fois pour la première entrée
for (int i = 0; i < NUM_PUMP_MODE_OPTIONS; i++) {
int targetRow = i + 1;
centerText(lcd2, targetRow, pumpModeOptions[i]);
}
}
// Gérer le clignotement seulement de la ligne sélectionnée (ou la ligne du message)
int targetRow = selectedPumpModeOption + 1; // L'option clignotante
// Efface la ligne de l'option précédemment sélectionnée pour éviter les artefacts visuels
// C'est nécessaire si l'utilisateur change rapidement d'option avec l'encodeur.
if (!forceClear && lastSelectedPumpModeOption != -1 && lastSelectedPumpModeOption != selectedPumpModeOption) {
centerText(lcd2, lastSelectedPumpModeOption + 1, pumpModeOptions[lastSelectedPumpModeOption]); // Réaffiche l'ancienne option sans clignoter
}
if (currentSystemState == STATE_SET_PUMP_MODE) {
if (!messageIsActive) { // Pas de clignotement si un message est actif sur la ligne 3
if (menuTextVisible) {
centerText(lcd2, targetRow, pumpModeOptions[selectedPumpModeOption]);
} else {
// Efface l'option sélectionnée pour le clignotement
lcd2.setCursor(0, targetRow);
for (int k = 0; k < 20; k++) {
lcd2.print(" ");
}
}
} else {
// Si un message est actif, assurez-vous que la ligne clignotante est visible
// si ce n'est pas la ligne du message lui-même.
if (targetRow != 3) { // Si la ligne clignotante n'est PAS la ligne du message
centerText(lcd2, targetRow, pumpModeOptions[selectedPumpModeOption]);
} else { // Si la ligne clignotante EST la ligne du message, alors le message est prioritaire.
// On ne fait rien ici, handleTemporaryMessages s'occupe de la ligne 3
}
}
}
lastSelectedPumpModeOption = selectedPumpModeOption; // Met à jour pour la prochaine itération
}
// --- Fonction: Affichage du sous-menu Régler Niveaux ---
void drawSetLevelsMenu(bool forceClear = false) {
// Re-calculer les chaînes pour qu'elles soient à jour avec les valeurs modifiées
char highLevelStr[21];
char lowLevelStr[21];
sprintf(highLevelStr, "Haut: %d%%", (int)reservoirLevelHigh);
sprintf(lowLevelStr, "Bas: %d%%", (int)reservoirLevelLow);
if (forceClear || currentSetLevelsSubState != lastSetLevelsSubState || reservoirLevelHigh != lastReservoirLevelHigh || reservoirLevelLow != lastReservoirLevelLow) {
lcd2.clear(); // Efface l'écran LCD2 pour le sous-menu
// Afficher le titre du sous-menu
centerText(lcd2, 0, "--- Regler Niveaux ---");
// Afficher les options une fois
centerText(lcd2, 1, highLevelStr);
centerText(lcd2, 2, lowLevelStr);
centerText(lcd2, 3, "Valider et Quitter");
}
// Logique de clignotement pour l'option sélectionnée ou la valeur en cours d'édition
// Effacer la ligne de l'élément précédemment sélectionné
if (lastSetLevelsSubState != currentSetLevelsSubState) {
if (lastSetLevelsSubState == SET_LEVEL_HIGH) centerText(lcd2, 1, highLevelStr);
else if (lastSetLevelsSubState == SET_LEVEL_LOW) centerText(lcd2, 2, lowLevelStr);
else if (lastSetLevelsSubState == SET_LEVEL_EXIT) centerText(lcd2, 3, "Valider et Quitter");
}
if (currentSystemState == STATE_SET_LEVELS) {
if (!messageIsActive) { // Pas de clignotement si un message est actif sur la ligne 3
if (menuTextVisible) {
if (currentSetLevelsSubState == SET_LEVEL_HIGH) {
centerText(lcd2, 1, highLevelStr);
} else if (currentSetLevelsSubState == SET_LEVEL_LOW) {
centerText(lcd2, 2, lowLevelStr);
} else if (currentSetLevelsSubState == SET_LEVEL_EXIT) {
centerText(lcd2, 3, "Valider et Quitter");
}
} else { // menuTextVisible est false, donc l'élément sélectionné est masqué
// Efface la ligne de l'élément qui clignote
if (currentSetLevelsSubState == SET_LEVEL_HIGH) {
lcd2.setCursor(0, 1); lcd2.print(" ");
} else if (currentSetLevelsSubState == SET_LEVEL_LOW) {
lcd2.setCursor(0, 2); lcd2.print(" ");
} else if (currentSetLevelsSubState == SET_LEVEL_EXIT) {
lcd2.setCursor(0, 3); lcd2.print(" ");
}
}
} else {
// Si un message est actif, assurez-vous que la ligne clignotante est visible
// si ce n'est pas la ligne du message lui-même.
if (currentSetLevelsSubState == SET_LEVEL_HIGH) {
centerText(lcd2, 1, highLevelStr);
} else if (currentSetLevelsSubState == SET_LEVEL_LOW) {
centerText(lcd2, 2, lowLevelStr);
} else if (currentSetLevelsSubState == SET_LEVEL_EXIT && !messageIsActive) { // Only blink if no message
centerText(lcd2, 3, "Valider et Quitter");
}
}
}
lastSetLevelsSubState = currentSetLevelsSubState; // Met à jour pour la prochaine itération
lastReservoirLevelHigh = reservoirLevelHigh;
lastReservoirLevelLow = reservoirLevelLow;
}
// --- Fonction: Affichage du sous-menu Régler Heure/Date ---
void drawSetTimeMenu(bool forceClear = false) {
// Re-calculer les chaînes pour qu'elles soient à jour avec les valeurs modifiées
char timeEditStr[21];
sprintf(timeEditStr, "Heure: %02d:%02d", tempSetHour, tempSetMinute);
char dateEditStr[21];
sprintf(dateEditStr, "Date: %02d/%02d/%04d", tempSetDay, tempSetMonth, tempSetYear);
// Détecter si un changement majeur (changement d'état ou de valeur) justifie un redraw complet
bool majorChange = forceClear ||
currentSetTimeSubState != lastSetTimeSubState ||
tempSetHour != lastTempSetHour ||
tempSetMinute != lastTempSetMinute ||
tempSetDay != lastTempSetDay ||
tempSetMonth != lastTempSetMonth ||
tempSetYear != lastTempSetYear;
if (majorChange) {
lcd2.clear(); // Efface l'écran LCD2 pour le sous-menu
// Afficher le titre initial ou mis à jour
if (currentSetTimeSubState == SET_HOUR) {
centerText(lcd2, 0, "--- Regler HEURE ---");
} else if (currentSetTimeSubState == SET_MINUTE) {
centerText(lcd2, 0, "--- Regler MINUTES ---");
} else if (currentSetTimeSubState == SET_DAY) {
centerText(lcd2, 0, "--- Regler JOUR ---");
} else if (currentSetTimeSubState == SET_MONTH) {
centerText(lcd2, 0, "--- Regler MOIS ---");
} else if (currentSetTimeSubState == SET_YEAR) {
centerText(lcd2, 0, "--- Regler ANNEE ---");
} else { // SET_TIME_EXIT
centerText(lcd2, 0, "--- Valider Heure ---");
}
// Afficher toutes les valeurs
centerText(lcd2, 1, timeEditStr);
centerText(lcd2, 2, dateEditStr);
centerText(lcd2, 3, "Valider et Quitter");
}
// Gérer le clignotement de l'élément en cours d'édition
if (currentSystemState == STATE_SET_TIME) {
if (!messageIsActive) { // Pas de clignotement si un message est actif sur la ligne 3
if (menuTextVisible) {
if (currentSetTimeSubState == SET_HOUR) { // Condition spécifique pour clignotement de l'heure
centerText(lcd2, 1, timeEditStr);
} else if (currentSetTimeSubState == SET_MINUTE) {
centerText(lcd2, 1, timeEditStr);
} else if (currentSetTimeSubState == SET_DAY) { // Condition spécifique pour clignotement de la date
centerText(lcd2, 2, dateEditStr);
} else if (currentSetTimeSubState == SET_MONTH) {
centerText(lcd2, 2, dateEditStr);
} else if (currentSetTimeSubState == SET_YEAR) {
centerText(lcd2, 2, dateEditStr);
} else if (currentSetTimeSubState == SET_TIME_EXIT) {
centerText(lcd2, 3, "Valider et Quitter");
}
} else { // menuTextVisible est false, donc l'élément sélectionné est masqué
// Efface la ligne de l'élément qui clignote
if (currentSetTimeSubState == SET_HOUR || currentSetTimeSubState == SET_MINUTE) {
lcd2.setCursor(0, 1); lcd2.print(" ");
} else if (currentSetTimeSubState == SET_DAY || currentSetTimeSubState == SET_MONTH || currentSetTimeSubState == SET_YEAR) {
lcd2.setCursor(0, 2); lcd2.print(" ");
} else if (currentSetTimeSubState == SET_TIME_EXIT) {
lcd2.setCursor(0, 3); lcd2.print(" ");
}
}
} else {
// Si un message est actif, assurez-vous que la ligne clignotante est visible
// si ce n'est pas la ligne du message lui-même.
if (currentSetTimeSubState == SET_HOUR) {
centerText(lcd2, 1, timeEditStr);
} else if (currentSetTimeSubState == SET_MINUTE) {
centerText(lcd2, 1, timeEditStr);
} else if (currentSetTimeSubState == SET_DAY) {
centerText(lcd2, 2, dateEditStr);
} else if (currentSetTimeSubState == SET_MONTH) {
centerText(lcd2, 2, dateEditStr);
} else if (currentSetTimeSubState == SET_YEAR) {
centerText(lcd2, 2, dateEditStr);
} else if (currentSetTimeSubState == SET_TIME_EXIT && !messageIsActive) { // Only blink if no message
centerText(lcd2, 3, "Valider et Quitter");
}
}
}
// Mettre à jour les variables "last"
lastSetTimeSubState = currentSetTimeSubState;
lastTempSetHour = tempSetHour;
lastTempSetMinute = tempSetMinute;
lastTempSetDay = tempSetDay;
lastTempSetMonth = tempSetMonth;
lastTempSetYear = tempSetYear;
}
// --- Fonction pour gérer l'affichage et l'effacement des messages temporaires non bloquants ---
void handleTemporaryMessages(unsigned long currentMillis) {
if (messageIsActive && (currentMillis - messageDisplayStartTime >= MESSAGE_DISPLAY_DURATION_MS)) {
// Le temps d'affichage est écoulé, efface le message
lcd2.setCursor(0, 3); // Ligne 3 est utilisée pour ce type de message
for (int k = 0; k < 20; k++) { // Efface toute la ligne
lcd2.print(" ");
}
messageIsActive = false; // Le message n'est plus actif
debugPrint(2, " "); // Efface le message sur LCD Debug Ligne 2
// Si on était dans le sous-menu pompe, il faut rafraîchir son affichage
if (currentSystemState == STATE_SET_PUMP_MODE) {
drawSetPumpModeMenu(true); // Force un rafraîchissement complet du menu pour que l'option 3 réapparaisse
}
}
}
// --- Affichage de l'écran normal sur LCD2 ---
void drawNormalDisplay(bool forceClear = false) {
if (forceClear) {
lcd2.clear();
// Nettoyage explicite de chaque ligne pour éviter les artefacts résiduels
for (int r = 0; r < 4; r++) {
lcd2.setCursor(0, r);
for (int c = 0; c < 20; c++) {
lcd2.print(" ");
}
}
}
// Mesure du niveau d'eau avec HC-SR04
long distanceCm = ultrasonic.read();
if (distanceCm == -1) {
lcd2.setCursor(0, 0);
lcd2.print("Capteur ERREUR! ");
lcd2.setCursor(0, 1);
lcd2.print(" ");
debugPrint(2, "Dist: ERR! "); // Affiche l'erreur sur le debug LCD ligne 2
desactiverPompe();
desactiverAlarme();
} else {
float niveauCm = RESERVOIR_HAUTEUR_CM - distanceCm;
if (niveauCm < 0) niveauCm = 0;
if (niveauCm > RESERVOIR_HAUTEUR_CM) niveauCm = RESERVOIR_HAUTEUR_CM;
int niveauPourcentage = (int)((niveauCm / RESERVOIR_HAUTEUR_CM) * 100.0);
// --- LOGIQUE DE CONTRÔLE DE POMPE ET ALARME (EN MODE AUTO SEULEMENT) ---
if (currentPumpMode == PUMP_MODE_AUTO) {
// Les seuils utiliseront les variables réglées par l'utilisateur
if (niveauPourcentage > reservoirLevelHigh) { // Seuil haut d'alarme
activerAlarme();
} else {
desactiverAlarme();
}
if (niveauPourcentage >= reservoirLevelHigh - 1) { // Seuil de déclenchement pompe (haut, un peu avant l'alarme)
activerPompe();
}
if (niveauPourcentage < reservoirLevelLow + 1) { // Seuil d'arrêt pompe (bas, un peu après le seuil bas)
desactiverPompe();
}
} else { // PUMP_MODE_MANUAL
desactiverAlarme();
// En mode manuel, la pompe est contrôlée par l'utilisateur.
// La logique d'activation/désactivation est gérée dans le switch du bouton SELECT.
}
// --- AFFICHAGE SUR LCD2 ---
// Ligne 0: "Niveau d'eau XX%" - NE PAS CENTRER CETTE LIGNE
lcd2.setCursor(0, 0); // Positionne le curseur au début de la ligne
lcd2.print(" "); // Écrit 4 espaces pour "purger" les premières positions
char niveauStr[21]; // Buffer suffisant pour la chaîne complète
sprintf(niveauStr, "Niveau d'eau %d%%", niveauPourcentage);
lcd2.print(niveauStr);
// Remplir le reste de la ligne avec des espaces pour effacer le contenu précédent
for (int i = strlen(niveauStr) + 4; i < 20; i++) { // +4 pour les espaces de début
lcd2.print(" ");
}
// Ligne 1: Barre de progression avec caractères Goutte d'eau
lcd2.setCursor(0, 1);
int totalGouttes = 20;
for (int i = 0; i < totalGouttes; i++) {
int goutteMinPourcentage = i * 5;
if (niveauPourcentage >= goutteMinPourcentage + 5) {
lcd2.write(4); // Goutte pleine (5%)
} else if (niveauPourcentage >= goutteMinPourcentage + 3) {
lcd2.write(3); // Trois quarts de goutte (3-4%)
} else if (niveauPourcentage >= goutteMinPourcentage + 2) {
lcd2.write(2); // Moitié de goutte (2-3%)
} else if (niveauPourcentage >= goutteMinPourcentage + 1) {
lcd2.write(1); // Un quart de goutte (1-2%)
} else {
lcd2.write(0); // Goutte vide
}
}
// Ligne 2: Débordement (géré par activerAlarme/desactiverAlarme)
// Assurer que la ligne 2 est vide si l'alarme est OFF
if (!alarmeActive) {
lcd2.setCursor(0, 2);
lcd2.print(" ");
}
// Ligne 3: État de la pompe
// Cette ligne doit toujours afficher l'état de la pompe, indépendamment de l'alarme sur Ligne 2.
// NE PAS AFFICHER L'ETAT DE LA POMPE SI UN MESSAGE EST ACTIF SUR CETTE LIGNE
if (!messageIsActive) {
lcd2.setCursor(0, 3);
char pumpStateStr[21];
if (currentPumpMode == PUMP_MODE_MANUAL) {
if (pompeActive) {
sprintf(pumpStateStr, "Pompe: MANUEL ON ");
} else {
sprintf(pumpStateStr, "Pompe: MANUEL OFF ");
}
} else { // PUMP_MODE_AUTO
if (pompeActive) {
sprintf(pumpStateStr, "Pompe: MARCHE ");
} else {
sprintf(pumpStateStr, "Pompe: ARRET ");
}
}
lcd2.print(pumpStateStr);
}
// Mettre à jour les variables de 'last' ici pour éviter des re-dessins inutiles dans la loop
lastPompeDisplayedState = pompeActive;
lastDisplayedPumpMode = currentPumpMode;
// Affiche la distance et le niveau en % sur LCD Debug Ligne 2
char debugLigne2Str[21];
sprintf(debugLigne2Str, "Dist:%4ldcm Niv:%3d%%", distanceCm, niveauPourcentage); // Formatage pour alignement
debugPrint(2, debugLigne2Str);
// Affiche l'état de l'alarme et de la pompe sur LCD Debug Ligne 3
char debugLigne3Str[21];
sprintf(debugLigne3Str, "Alarme:%s Pompe:%s",
alarmeActive ? "ON" : "OFF",
pompeActive ? "ON" : "OFF");
debugPrint(3, debugLigne3Str);
}
}
// --- Configuration initiale ---
void setup() {
// --- Initialisation des LCDs ---
lcd1.init();
lcd1.backlight();
lcd2.init();
lcd2.backlight();
// --- Chargement des caractères personnalisés dans LCD2 ---
lcd2.createChar(0, emptyDrop);
lcd2.createChar(1, quarterDrop);
lcd2.createChar(2, halfDrop);
lcd2.createChar(3, threeQuarterDrop);
lcd2.createChar(4, fullDrop);
lcdDebug.init();
lcdDebug.backlight();
// --- Configuration du bouton de l'encodeur ---
pinMode(ENCODER_SW_PIN, INPUT_PULLUP);
// --- Configuration des broches de sortie pour le buzzer et le relais ---
pinMode(BUZZER_PIN, OUTPUT);
pinMode(RELAY_PIN, OUTPUT);
desactiverPompe();
desactiverAlarme();
// AJOUT ICI: Charger les paramètres depuis l'EEPROM au démarrage
loadParameters();
// --- Affichage initial des écrans principaux (avec centrage) ---
centerText(lcd1, 0, "S.E.N.S.E.I.");
displayPumpControlMode(); // Ligne 1: Affiche le mode de pompe (chargé depuis EEPROM ou par défaut)
// AJOUT ICI: Affiche l'état normal initial sur LCD2
drawNormalDisplay(true); // Ceci affichera la ligne 3 au démarrage, avec un effacement forcé
// AJOUT ICI: Affichage des seuils sur lcdDebug dès le démarrage (Lignes 0 et 1)
lcdDebug.setCursor(0, 0);
lcdDebug.print("SEUIL HAUT: "); // Correction ici, début de la ligne
lcdDebug.print((int)reservoirLevelHigh);
lcdDebug.print("%");
for (int i = strlen("SEUIL HAUT: XX%"); i < 20; i++) lcdDebug.print(" "); // Remplir le reste
lcdDebug.setCursor(0, 1);
lcdDebug.print("SEUIL BAS : ");
lcdDebug.print((int)reservoirLevelLow);
lcdDebug.print("%");
for (int i = strlen("SEUIL BAS : XX%"); i < 20; i++) lcdDebug.print(" "); // Remplir le reste
// Initialiser la ligne 2 (messages dynamiques) et la ligne 3 (état actionneurs)
debugPrint(2, "SYSTEME DEMARRE ");
// debugPrint(3, "RTC Verif... "); // Message RTC initial commenté
// --- Vérification et réglage de l'RTC ---
delay(1000); // Laisse le temps au système de s'initialiser
if (!rtc.begin()) {
// Bloque le programme si RTC non trouvé, sans affichage sur lcdDebug
while (true) {
delay(10);
}
} else {
// Pas de message RTC sur debug LCD
}
if (!rtc.isrunning()) {
// rtc.adjust(DateTime(F(__DATE__), F(__TIME__))); // Décommenter pour régler l'heure à la compilation
delay(2000); // Laisser le temps de lire le message (si activé)
} else {
delay(1000); // Laisser le temps de lire le message (si activé)
}
}
// --- Boucle principale du programme ---
void loop() {
DateTime now = rtc.now(); // Récupère l'heure/date une fois par boucle
unsigned long currentMillis = millis(); // Récupère le temps actuel une seule fois par boucle
// --- GESTION DES MESSAGES TEMPORAIRES (NON BLOQUANTE) ---
handleTemporaryMessages(currentMillis);
// --- Gestion du Bouton SELECT (Encodeur) ---
int rawButtonState = digitalRead(ENCODER_SW_PIN);
// Détection d'appui (passage de HIGH à LOW)
if (rawButtonState == LOW && !buttonCurrentlyPressed) {
buttonCurrentlyPressed = true;
lastButtonActionTime = currentMillis; // Enregistre l'heure de l'appui initial
}
// Détection de relâchement
else if (rawButtonState == HIGH && buttonCurrentlyPressed) {
buttonCurrentlyPressed = false; // Le bouton est relâché
// Vérifie si l'appui a été maintenu assez longtemps pour être un "clic" valide (anti-rebond)
if ((currentMillis - lastButtonActionTime) >= BUTTON_DEBOUNCE_MS) {
// C'est un clic valide !
debugPrint(2, "BOUTON SELECT CLIC!"); // Ligne 2 pour les actions importantes
// --- LOGIQUE DE GESTION DES ÉTATS DU SYSTÈME S.E.N.S.E.I. ---
if (currentSystemState == STATE_NORMAL_DISPLAY) {
currentSystemState = STATE_MAIN_MENU; // Passe en mode menu
selectedMenuOption = 0; // Réinitialise la sélection à la première option (0)
lastSelectedMenuOption = -1; // Force le redessin complet initial
debugPrint(2, "Entree Menu ");
lcd2.clear(); // Efface LCD2 pour revenir à l'affichage du menu
redrawMainMenu(true); // Affiche le menu principal avec un effacement forcé
previousMillisMenu = currentMillis; // Initialise le clignotement
menuTextVisible = true; // S'assure que le texte est visible au démarrage du clignotement
} else if (currentSystemState == STATE_MAIN_MENU) {
// En mode menu principal, l'appui sélectionne une option
debugPrint(2, "Option menu sel. ");
switch (selectedMenuOption) {
case 0: // "1. Mode Pompe"
currentSystemState = STATE_SET_PUMP_MODE;
selectedPumpModeOption = (currentPumpMode == PUMP_MODE_AUTO) ? 0 : 1; // Prépare l'option sélectionnée par défaut
lastSelectedPumpModeOption = -1; // Force redessin initial
debugPrint(2, "Sous-menu Pompe ");
lcd2.clear(); // Efface LCD2
delay(50); // Délai accru pour que l'écran se stabilise
drawSetPumpModeMenu(true); // Affiche le sous-menu mode pompe avec un effacement forcé
previousMillisMenu = currentMillis; // Réinitialise le timer de clignotement pour le sous-menu
menuTextVisible = true; // S'assure que le texte est visible
break;
case 1: // "2. Regler Niveaux"
currentSystemState = STATE_SET_LEVELS;
currentSetLevelsSubState = SET_LEVEL_HIGH; // Commence par régler le niveau haut
lastSetLevelsSubState = SET_LEVEL_HIGH; // Force redessin initial
lastReservoirLevelHigh = -1.0; lastReservoirLevelLow = -1.0; // Force redessin initial
debugPrint(2, "Sous-menu Niveaux ");
lcd2.clear(); // Efface LCD2
delay(50); // Délai accru pour que l'écran se stabilise
drawSetLevelsMenu(true); // Affiche le sous-menu niveaux avec un effacement forcé
previousMillisMenu = currentMillis; // Réinitialise le timer de clignotement pour le sous-menu
menuTextVisible = true; // S'assure que le texte est visible
break;
case 2: // "3. Regler Heure/Date"
currentSystemState = STATE_SET_TIME;
// Initialise les variables temporaires avec l'heure/date actuelle de la RTC
tempSetHour = now.hour();
tempSetMinute = now.minute();
tempSetSecond = 0; // On ne règle pas les secondes directement
tempSetDay = now.day();
tempSetMonth = now.month();
tempSetYear = now.year();
currentSetTimeSubState = SET_HOUR; // Commence par régler l'heure
lastSetTimeSubState = SET_HOUR; // Force redessin initial
lastTempSetHour = -1; lastTempSetMinute = -1; lastTempSetDay = -1; lastTempSetMonth = -1; lastTempSetYear = -1; // Force redessin initial
debugPrint(2, "Sous-menu Heure ");
lcd2.clear(); // Efface LCD2
delay(50); // Délai accru pour que l'écran se stabilise
drawSetTimeMenu(true); // Affiche le sous-menu heure/date avec un effacement forcé
previousMillisMenu = currentMillis; // Réinitialise le timer de clignotement pour le sous-menu
menuTextVisible = true; // S'assure que le texte est visible
break;
case 3: // "4. Quitter Menu"
// Réinitialise la visibilité du texte et le timer de clignotement AVANT le changement d'état
menuTextVisible = true;
previousMillisMenu = currentMillis;
lcd2.clear(); // Efface LCD2 avant de revenir à l'affichage normal
delay(50); // Délai accru pour que l'écran se stabilise
currentSystemState = STATE_NORMAL_DISPLAY; // Retour à l'affichage normal
drawNormalDisplay(true); // Efface et redessine l'affichage normal
debugPrint(2, "Sortie Menu ");
break;
}
} else if (currentSystemState == STATE_SET_PUMP_MODE) {
// En mode sous-menu "Mode Pompe", l'appui sélectionne le mode
if (selectedPumpModeOption == 0) { // Option "1. Automatique"
currentPumpMode = PUMP_MODE_AUTO;
saveParameters(); // Sauvegarder le mode de pompe
debugPrint(2, "Mode Pompe: Auto "); // Ligne 2 debug LCD
// Réinitialise la visibilité du texte et le timer de clignotement AVANT le changement d'état
menuTextVisible = true;
previousMillisMenu = currentMillis;
lcd2.clear(); // Efface LCD2 avant de revenir à l'affichage normal
delay(50); // Délai accru pour que l'écran se stabilise
currentSystemState = STATE_NORMAL_DISPLAY; // Retour à l'affichage normal
drawNormalDisplay(true); // Efface et redessine l'affichage normal
displayPumpControlMode(); // Met à jour le mode sur LCD1
} else if (selectedPumpModeOption == 1) { // Option "2. Manuel"
currentPumpMode = PUMP_MODE_MANUAL;
saveParameters(); // Sauvegarder le mode de pompe
debugPrint(2, "Mode Pompe: Manuel "); // Ligne 2 debug LCD
// Réinitialise la visibilité du texte et le timer de clignotement AVANT le changement d'état
menuTextVisible = true;
previousMillisMenu = currentMillis;
lcd2.clear(); // Efface LCD2 avant de revenir à l'affichage normal
delay(50); // Délai accru pour que l'écran se stabilise
currentSystemState = STATE_NORMAL_DISPLAY; // Retour à l'affichage normal
drawNormalDisplay(true); // Efface et redessine l'affichage normal
displayPumpControlMode(); // Met à jour le mode sur LCD1
} else if (selectedPumpModeOption == 2) { // Option "3. Activer/Desact."
// Gérer l'activation/désactivation de la pompe seulement si le mode est manuel
if (currentPumpMode == PUMP_MODE_MANUAL) {
if (pompeActive) {
desactiverPompe(); // Cette fonction met à jour debug LCD Ligne 3
} else {
activerPompe(); // Cette fonction met à jour debug LCD Ligne 3
}
// Pas besoin de changer d'état du système, on reste dans le sous-menu pour d'autres actions manuelles
} else {
// Afficher un message temporaire si l'utilisateur essaie d'activer/désactiver en mode AUTO
centerText(lcd2, 3, "Mode AUTO actif! ");
debugPrint(2, "Action refusee (Auto)"); // Ligne 2 debug LCD
messageDisplayStartTime = currentMillis; // Démarre le timer du message
messageIsActive = true; // Indique qu'un message est actif
}
// Si on a togglé la pompe manuellement, on veut rafraîchir l'affichage du sous-menu pour le clignotement
// On reste dans le sous-menu, donc on garde le clignotement actif pour l'option sélectionnée.
menuTextVisible = true; // S'assure que le texte est visible après action
previousMillisMenu = currentMillis; // Réinitialise le timer de clignotement pour cette option
drawSetPumpModeMenu(false); // Rafraîchit l'affichage (sans effacer tout le menu)
}
} else if (currentSystemState == STATE_SET_LEVELS) {
// En mode sous-menu "Régler Niveaux", l'appui bascule entre les options ou valide
if (currentSetLevelsSubState == SET_LEVEL_HIGH) {
currentSetLevelsSubState = SET_LEVEL_LOW; // Passe au réglage du niveau bas
} else if (currentSetLevelsSubState == SET_LEVEL_LOW) {
currentSetLevelsSubState = SET_LEVEL_EXIT; // Passe à l'option "Valider et Quitter"
} else if (currentSetLevelsSubState == SET_LEVEL_EXIT) {
// Valider les changements et revenir à l'affichage normal
saveParameters(); // AJOUT: Sauvegarder les niveaux
debugPrint(2, "Niveaux Sauvegardes."); // Ligne 2 debug LCD
// Mettre à jour les seuils sur LCD Debug (Ligne 0 et 1)
lcdDebug.setCursor(0, 0);
lcdDebug.print("SEUIL HAUT: ");
lcdDebug.print((int)reservoirLevelHigh);
lcdDebug.print("%");
for (int i = strlen("SEUIL HAUT: XX%"); i < 20; i++) lcdDebug.print(" ");
lcdDebug.setCursor(0, 1);
lcdDebug.print("SEUIL BAS : ");
lcdDebug.print((int)reservoirLevelLow);
lcdDebug.print("%");
for (int i = strlen("SEUIL BAS : XX%"); i < 20; i++) lcdDebug.print(" ");
// Réinitialise la visibilité du texte et le timer de clignotement AVANT le changement d'état
menuTextVisible = true;
previousMillisMenu = currentMillis;
lcd2.clear(); // Efface LCD2 avant de revenir à l'affichage normal
delay(50); // Délai accru pour que l'écran se stabilise
currentSystemState = STATE_NORMAL_DISPLAY; // Retour à l'affichage normal
drawNormalDisplay(true); // Efface et redessine l'affichage normal
}
// Lors d'un changement de sous-état, on veut que le texte soit visible
menuTextVisible = true;
previousMillisMenu = currentMillis; // Réinitialise le timer de clignotement
drawSetLevelsMenu(false); // Rafraîchit l'affichage (sans effacer tout le menu)
} else if (currentSystemState == STATE_SET_TIME) {
// En mode sous-menu "Régler Heure/Date", l'appui bascule entre les champs ou valide
if (currentSetTimeSubState == SET_HOUR) {
currentSetTimeSubState = SET_MINUTE;
} else if (currentSetTimeSubState == SET_MINUTE) {
currentSetTimeSubState = SET_DAY;
} else if (currentSetTimeSubState == SET_DAY) {
currentSetTimeSubState = SET_MONTH;
} else if (currentSetTimeSubState == SET_MONTH) {
currentSetTimeSubState = SET_YEAR;
} else if (currentSetTimeSubState == SET_YEAR) {
currentSetTimeSubState = SET_TIME_EXIT;
} else if (currentSetTimeSubState == SET_TIME_EXIT) {
// Valider et régler l'heure/date sur la RTC
rtc.adjust(DateTime(tempSetYear, tempSetMonth, tempSetDay, tempSetHour, tempSetMinute, tempSetSecond));
debugPrint(2, "Heure/Date reglee! "); // Ligne 2 debug LCD
// Réinitialise la visibilité du texte et le timer de clignotement AVANT le changement d'état
menuTextVisible = true;
previousMillisMenu = currentMillis;
lcd2.clear(); // Efface LCD2 avant de revenir à l'affichage normal
delay(50); // Délai accru pour que l'écran se stabilise
currentSystemState = STATE_NORMAL_DISPLAY; // Retour à l'affichage normal
drawNormalDisplay(true); // Efface et redessine l'affichage normal
}
menuTextVisible = true;
previousMillisMenu = currentMillis; // Réinitialise le timer de clignotement
drawSetTimeMenu(false); // Rafraîchit l'affichage (le clignotement est géré à l'intérieur)
}
}
}
// --- Lecture de l'encodeur rotatif (pour navigation et réglage) ---
int encoderDir = getEncoderDirection(); // Récupère la direction de l'encodeur
if (encoderDir != 0) { // S'il y a eu une rotation
if (currentSystemState == STATE_MAIN_MENU) {
// Réaffiche l'ancienne option sélectionnée pour éviter les artefacts avant de changer
lcd2.setCursor(0, selectedMenuOption);
lcd2.print(mainMenuOptions[selectedMenuOption]);
for (int k = strlen(mainMenuOptions[selectedMenuOption]); k < 20; k++) {
lcd2.print(" ");
}
selectedMenuOption = (selectedMenuOption + encoderDir);
// Gérer le débordement pour les indices du menu
if (selectedMenuOption < 0) selectedMenuOption = NUM_MAIN_MENU_OPTIONS - 1;
if (selectedMenuOption >= NUM_MAIN_MENU_OPTIONS) selectedMenuOption = 0;
menuTextVisible = true; // S'assure que la nouvelle option est visible initialement
previousMillisMenu = currentMillis; // Réinitialise le timer de clignotement pour la nouvelle sélection
redrawMainMenu(false); // N'efface pas tout, juste gère la ligne sélectionnée
} else if (currentSystemState == STATE_SET_PUMP_MODE) {
// L'ancienne option doit être réaffichée normalement
centerText(lcd2, selectedPumpModeOption + 1, pumpModeOptions[selectedPumpModeOption]);
selectedPumpModeOption = (selectedPumpModeOption + encoderDir);
if (selectedPumpModeOption < 0) selectedPumpModeOption = NUM_PUMP_MODE_OPTIONS - 1;
if (selectedPumpModeOption >= NUM_PUMP_MODE_OPTIONS) selectedPumpModeOption = 0;
menuTextVisible = true;
previousMillisMenu = currentMillis; // Réinitialise le timer de clignotement
drawSetPumpModeMenu(false); // Rafraîchit l'affichage du sous-menu (sans effacer tout le menu)
} else if (currentSystemState == STATE_SET_LEVELS) {
if (currentSetLevelsSubState == SET_LEVEL_HIGH) {
reservoirLevelHigh += encoderDir;
// S'assurer que le niveau haut est >= au niveau bas + 5% et <= 100%
reservoirLevelHigh = constrain(reservoirLevelHigh, reservoirLevelLow + 5.0, 100.0);
} else if (currentSetLevelsSubState == SET_LEVEL_LOW) {
reservoirLevelLow += encoderDir;
// S'assurer que le niveau bas est >= 0% et <= au niveau haut - 5%
reservoirLevelLow = constrain(reservoirLevelLow, 0.0, reservoirLevelHigh - 5.0);
}
menuTextVisible = true;
previousMillisMenu = currentMillis;
drawSetLevelsMenu(false); // Rafraîchit l'affichage (le clignotement est géré à l'intérieur)
} else if (currentSystemState == STATE_SET_TIME) {
// Les changements de valeurs sont maintenant gérés par l'encodeur dans ce bloc
if (currentSetTimeSubState == SET_HOUR) {
tempSetHour = (tempSetHour + encoderDir + 24) % 24;
} else if (currentSetTimeSubState == SET_MINUTE) {
tempSetMinute = (tempSetMinute + encoderDir + 60) % 60;
} else if (currentSetTimeSubState == SET_DAY) {
int maxDays = daysInMonth(tempSetMonth, tempSetYear);
tempSetDay += encoderDir;
if (tempSetDay < 1) tempSetDay = maxDays;
if (tempSetDay > maxDays) tempSetDay = 1;
} else if (currentSetTimeSubState == SET_MONTH) {
tempSetMonth += encoderDir;
if (tempSetMonth < 1) tempSetMonth = 12;
if (tempSetMonth > 12) tempSetMonth = 1;
// Après avoir changé le mois, s'assurer que le jour est toujours valide pour ce nouveau mois
int maxDays = daysInMonth(tempSetMonth, tempSetYear);
if (tempSetDay > maxDays) tempSetDay = maxDays;
} else if (currentSetTimeSubState == SET_YEAR) {
tempSetYear += encoderDir;
if (tempSetYear < 2024) tempSetYear = 2024; // Année minimum raisonnable
if (tempSetYear > 2100) tempSetYear = 2100; // Année maximum arbitraire
// Après avoir changé l'année, s'assurer que le jour est toujours valide (spécialement pour février)
int maxDays = daysInMonth(tempSetMonth, tempSetYear);
if (tempSetDay > maxDays) tempSetDay = maxDays;
}
// Pas de `menuTextVisible = true; previousMillisMenu = currentMillis;` ici, car le clignotement est géré par drawSetTimeMenu()
drawSetTimeMenu(false); // Rafraîchit l'affichage (le clignotement est géré à l'intérieur)
}
}
// --- LOGIQUE GLOBALE DU S.E.N.S.E.I. selon l'état du système ---
// Un "forceClear" est maintenant envoyé seulement si l'état principal du système change,
// ou si un message temporaire vient d'être effacé (géré par handleTemporaryMessages)
if (currentSystemState == STATE_NORMAL_DISPLAY) {
// Si nous venons de passer en mode normal OU si les états importants ont changé
// La condition currentSystemState != lastSystemState est suffisante pour déclencher un effacement forcé
// car le passage à STATE_NORMAL_DISPLAY depuis un menu est toujours un changement d'état.
if (currentSystemState != lastSystemState) {
drawNormalDisplay(true); // Efface et redessine l'affichage complet de l'état normal
debugPrint(2, "SYSTEME EN MARCHE ");
} else {
// Si aucun changement d'état majeur, on appelle drawNormalDisplay() sans forcer l'effacement
// pour que le niveau d'eau (qui est dynamique) soit mis à jour en continu.
drawNormalDisplay(false);
}
}
else if (currentSystemState == STATE_MAIN_MENU) {
// Si l'option sélectionnée a changé, ou si l'état du système a changé pour ce menu, force un redessin complet
if (selectedMenuOption != lastSelectedMenuOption || currentSystemState != lastSystemState) {
redrawMainMenu(true); // Redessine TOUT le menu pour éviter les artefacts de l'ancienne ligne
lastSelectedMenuOption = selectedMenuOption;
menuTextVisible = true; // S'assure que la nouvelle option est visible
previousMillisMenu = currentMillis; // Réinitialise le timer de clignotement
}
// Logique de clignotement pour le menu principal
if (currentMillis - previousMillisMenu >= intervalMenuBlink) {
previousMillisMenu = currentMillis;
menuTextVisible = !menuTextVisible; // Inverse l'état de visibilité
// On manipule SEULEMENT la ligne sélectionnée pour le clignotement.
int targetRow = selectedMenuOption;
lcd2.setCursor(0, targetRow);
if (menuTextVisible) {
lcd2.print(mainMenuOptions[selectedMenuOption]);
} else {
lcd2.print(" "); // Efface la ligne pour le clignotement
}
}
}
else if (currentSystemState == STATE_SET_PUMP_MODE) {
// Si l'option sélectionnée a changé, ou si l'état du système a changé pour ce menu, force un redessin complet
if (selectedPumpModeOption != lastSelectedPumpModeOption || currentSystemState != lastSystemState) {
drawSetPumpModeMenu(true); // Redessine TOUT le sous-menu pour éviter les artefacts
lastSelectedPumpModeOption = selectedPumpModeOption;
menuTextVisible = true; // S'assure que la nouvelle option est visible
previousMillisMenu = currentMillis; // Réinitialise le timer de clignotement
}
// Logique de clignotement pour le sous-menu Mode Pompe
if (!messageIsActive && (currentMillis - previousMillisMenu >= intervalMenuBlink)) {
previousMillisMenu = currentMillis;
menuTextVisible = !menuTextVisible; // Inverse l'état de visibilité
drawSetPumpModeMenu(false); // Redessine SEULEMENT la ligne clignotante
} else if (messageIsActive) {
menuTextVisible = true; // Assure que le texte est visible si message actif
drawSetPumpModeMenu(false); // Affiche la ligne clignotante au cas où elle serait masquée par le clignotement
}
}
else if (currentSystemState == STATE_SET_LEVELS) {
// Si l'état du sous-menu change, ou les valeurs changent, ou l'état du système change, force un redessin complet
if (currentSetLevelsSubState != lastSetLevelsSubState ||
reservoirLevelHigh != lastReservoirLevelHigh ||
reservoirLevelLow != lastReservoirLevelLow ||
currentSystemState != lastSystemState) {
drawSetLevelsMenu(true); // Redessine TOUT le sous-menu
menuTextVisible = true; // S'assure que le texte est visible
previousMillisMenu = currentMillis; // Réinitialise le timer de clignotement
}
// Logique de clignotement pour le sous-menu Régler Niveaux
if (currentMillis - previousMillisMenu >= intervalMenuBlink) {
previousMillisMenu = currentMillis;
menuTextVisible = !menuTextVisible; // Inverse l'état de visibilité
}
drawSetLevelsMenu(false); // Appelle pour la mise à jour du clignotement seul
}
else if (currentSystemState == STATE_SET_TIME) {
// Si l'état du sous-menu change, ou les valeurs changent, ou l'état du système change, force un redessin complet
if (currentSetTimeSubState != lastSetTimeSubState ||
tempSetHour != lastTempSetHour ||
tempSetMinute != lastTempSetMinute ||
tempSetDay != lastTempSetDay ||
tempSetMonth != lastTempSetMonth ||
tempSetYear != lastTempSetYear ||
currentSystemState != lastSystemState) {
drawSetTimeMenu(true); // Redessine TOUT le sous-menu
menuTextVisible = true; // S'assure que le texte est visible
previousMillisMenu = currentMillis; // Réinitialise le timer de clignotement
}
// Logique de clignotement pour le sous-menu Régler Heure/Date
if (currentMillis - previousMillisMenu >= intervalMenuBlink) {
previousMillisMenu = currentMillis;
menuTextVisible = !menuTextVisible; // Inverse l'état de visibilité
}
drawSetTimeMenu(false); // Appelle pour la mise à jour du clignotement seul
}
// --- Affichage de l'heure et de la date sur LCD1 (lignes 2 et 3) ---
char timeStr[9];
sprintf(timeStr, "%02d:%02d:%02d", now.hour(), now.minute(), now.second());
centerText(lcd1, 2, timeStr);
char dateStr[11];
sprintf(dateStr, "%02d/%02d/%04d", now.day(), now.month(), now.year());
centerText(lcd1, 3, dateStr);
lastSystemState = currentSystemState; // Met à jour l'état du système pour la prochaine itération
delay(50); // Délai augmenté pour une navigation plus douce (était 10ms)
}
// Projet: S.E.N.S.E.I.
Système
Électronique de
Niveau et
Surveillance d'
Eau
Intelligent)