// SpaPellet Manager - Reglage Option 1 : Temps ON / Temps OFF + EEPROM + Minuteur Relais + Consommation
// Date : 2025-07-20
// Version : GranuloCompteur_v1_69 // Ajout du défilement pour le sous-menu de consommation + Ajout 3e LCD (adresse 0x20)
// --- INCLUDES ---
#include <Wire.h> // Nécessaire pour la communication I2C (LCD et RTC)
#include <LiquidCrystal_I2C.h> // Bibliothèque pour les écrans LCD I2C
#include <RTClib.h> // Bibliothèque pour le module horloge temps réel (RTC)
#include <EEPROM.h> // Bibliothèque pour la mémoire EEPROM
// --- DÉCLARATION DES OBJETS ---
LiquidCrystal_I2C lcd1(0x27, 20, 4); // Déclaration du premier écran LCD (20 colonnes, 4 lignes) - Pour le menu
LiquidCrystal_I2C lcd2(0x26, 20, 4); // Déclaration du deuxième écran LCD (20 colonnes, 4 lignes) - Pour l'heure/date et consommation
LiquidCrystal_I2C lcd3(0x20, 20, 4); // Déclaration du troisième écran LCD (20 colonnes, 4 lignes) - Nouvelle addition (adresse 0x20)
RTC_DS1307 rtc; // Déclaration de l'objet RTC
// --- DÉCLARATION DES BROCHES (PINS) ---
const int pinJoystickY = A1;
const int pinJoystickX = A0;
const int pinJoystickBtn = 2; // Bouton du joystick
const int pinRelay = 7; // Relais pour le chauffage/pompe
const int pinAlarmBuzzer = 8; // Buzzer d'alarme
// --- CONSTANTES ET VARIABLES GLOBALES POUR LES CALCULS DE CONSOMMATION ---
const float GRANULES_PER_MINUTE_G = 45.0f; // Grammes de granulés par minute (à ajuster si besoin)
// Nouvelle énumération pour les grades de granulés
enum PelletGrade { SUPREME, MEDIUM, COMMON };
// Variables globale pour le grade actuel et le KWh par kg (maintenant non-constante)
PelletGrade currentPelletGrade = SUPREME; // Par défaut, qualité suprême
float KWH_PER_KG_GRANULES; // Cette valeur sera définie en fonction du grade
// Définition des pouvoirs calorifiques pour chaque grade
const float KWH_SUPREME = 5.0f; // Exemple : 5.0 kWh/kg pour le suprême (ENplus A1)
const float KWH_MEDIUM = 4.7f; // Exemple : 4.7 kWh/kg pour le moyen (ENplus A2)
const float KWH_COMMON = 4.0f; // Exemple : 4.0 kWh/kg pour le commun (ENplus B)
// --- VARIABLES GLOBALES POUR LE MENU ET LA NAVIGATION ---
// NOUVEAU : Structure pour les options du menu
struct MenuItem {
const char* text;
};
// NOUVEAU : Tableau des options du menu principal
MenuItem mainMenuOptions[] = {
{"1. T. ON"},
{"2. T. OFF"},
{"3. Marche/Arr"},
{"4. Consommation"},
{"5. Qualite Granules"}
};
const int nbOptions = sizeof(mainMenuOptions) / sizeof(mainMenuOptions[0]); // Calcule automatiquement le nombre d'options
int selectedOption = 0; // Option actuellement sélectionnée dans le menu principal
const int LCD_LINES = 4; // Nombre de lignes affichables sur le LCD1 (votre écran fait 4 lignes)
bool inSubMenu = false; // Vrai si nous sommes dans un sous-menu de réglage (T.ON/T.OFF)
bool reglMin = false; // Vrai si nous réglons les minutes
bool reglSec = false; // Vrai si nous réglons les secondes
// --- VARIABLES GLOBALES POUR LE SOUS-MENU CONSOMMATION ---
// Ajout de nouvelles unités de consommation
enum ConsumptionUnit { MINUTE, HOURLY, DAILY, WEEKLY, MONTHLY, YEARLY }; // Enumération des unités de temps pour la consommation
const int NUM_CONSUMPTION_UNITS = 6; // Nombre total d'unités de consommation
ConsumptionUnit currentConsumptionUnit = DAILY; // Unité de consommation actuellement affichée (par défaut : journalier)
int selectedConsumptionSubOption = 0; // Option sélectionnée dans le sous-menu consommation
bool inConsumptionSubMenu = false; // Vrai si nous sommes dans le sous-menu consommation
int startIndexConsumptionSubMenu = 0; // Index de la première option visible dans le sous-menu consommation
// --- NOUVELLES VARIABLES GLOBALES POUR LE SOUS-MENU QUALITÉ GRANULÉS ---
int selectedPelletGradeSubOption = 0; // Option sélectionnée dans le sous-menu qualité granulés
bool inPelletGradeSubMenu = false; // Vrai si nous sommes dans le sous-menu qualité granulés
// --- ADRESSES EEPROM ---
const int EEPROM_ADDR_DUREE_ON = 0; // Adresse pour stocker la durée ON
const int EEPROM_ADDR_DUREE_OFF = sizeof(unsigned int); // Adresse pour stocker la durée OFF
const int EEPROM_ADDR_CONSUMPTION_UNIT = EEPROM_ADDR_DUREE_OFF + sizeof(unsigned int); // Adresse pour stocker l'unité de consommation
const int EEPROM_ADDR_PELLET_GRADE = EEPROM_ADDR_CONSUMPTION_UNIT + sizeof(int); // Adresse pour stocker le grade des granulés
// --- VARIABLES DE DURÉE ET MINUTEUR ---
unsigned int dureeOnSec = 5; // Durée du cycle ON en secondes (valeur par défaut)
unsigned int dureeOffSec = 10; // Durée du cycle OFF en secondes (valeur par défaut)
const int LCD2_WIDTH = 20; // Largeur de l'écran LCD2 (pour la ligne 0)
const int LCD3_WIDTH = 20; // Largeur de l'écran LCD3
// --- BUFFERS POUR AFFICHAGE SANS SCINTILLEMENT (pour LCD2 ligne 0 et LCD3 ligne 0) ---
static char previousUnitLineLcd2[LCD2_WIDTH + 1] = " "; // Stocke la ligne 0 précédente pour éviter le scintillement
static char previousTimeDateLineLcd3[LCD3_WIDTH + 1] = " "; // Stocke la ligne 0 du LCD3 pour éviter le scintillement
// --- VARIABLES GLOBALES POUR LA RÉPÉTITION AUTOMATIQUE DU JOYSTICK ---
unsigned long lastRepeatTime = 0;
const long INITIAL_REPEAT_DELAY = 500; // Délai avant le début de la répétition (en ms)
const long REPEAT_INTERVAL = 100; // Intervalle entre les répétitions (en ms)
unsigned long lastMove = 0; // Dernier mouvement du joystick pour éviter les défilements trop rapides
unsigned long lastConsumptionCalcTime = 0; // Dernier calcul des données de consommation
const long CONSUMPTION_CALC_INTERVAL = 10000; // Recalcul des données de consommation toutes les 10 secondes
int regMin = 0; // Variable temporaire pour le réglage des minutes
int regSec = 0; // Variable temporaire pour les secondes
bool joystickReadyY = true; // Empêche les déclenchements multiples du joystick Y
bool joystickReadyX = true; // Empêche les déclenchements multiples du joystick X
// --- ÉTAT DU RELAIS ---
bool relayState = LOW; // État actuel du relais (LOW = éteint, HIGH = allumé)
unsigned long lastRelayStateChange = 0; // Dernier changement d'état du relais
bool isAutomated = false; // Vrai si le système d'automatisation est activé
// --- VARIABLES POUR STOCKER LES RÉSULTATS DES CALCULS DE CONSOMMATION ---
float granulesKg_Minute, kwh_Minute; unsigned long cycles_Minute;
float granulesKg_Hourly, kwh_Hourly; unsigned long cycles_Hourly;
float granulesKg_Daily, kwh_Daily; unsigned long cycles_Daily;
float granulesKg_Weekly, kwh_Weekly; unsigned long cycles_Weekly;
float granulesKg_Monthly, kwh_Monthly; unsigned long cycles_Monthly;
float granulesKg_Yearly, kwh_Yearly; unsigned long cycles_Yearly;
// --- FONCTIONS DU JOYSTICK ---
int getJoystickY() {
long sum = 0;
for (int i = 0; i < 5; i++) { // Lecture multiple pour stabiliser la valeur
sum += analogRead(pinJoystickY);
delay(2);
}
return sum / 5;
}
int getJoystickX() {
long sum = 0;
for (int i = 0; i < 5; i++) { // Lecture multiple pour stabiliser la valeur
sum += analogRead(pinJoystickX);
delay(2);
}
return sum / 5;
}
// --- FONCTIONS DU BUZZER ---
void playUsbConnectSound() {
tone(pinAlarmBuzzer, 523, 50); // Do5
delay(60);
tone(pinAlarmBuzzer, 659, 75); // Mi5
}
void playUsbDisconnectSound() {
tone(pinAlarmBuzzer, 659, 50); // Mi5
delay(60);
tone(pinAlarmBuzzer, 523, 75); // Do5
}
// --- FONCTIONS UTILITAIRES POUR LES GRANULÉS (Déplacées ici pour être déclarées avant utilisation) ---
void setKwhPerKgGranules(PelletGrade grade) {
switch (grade) {
case SUPREME:
KWH_PER_KG_GRANULES = KWH_SUPREME;
break;
case MEDIUM:
KWH_PER_KG_GRANULES = KWH_MEDIUM;
break;
case COMMON:
KWH_PER_KG_GRANULES = KWH_COMMON;
break;
}
}
// --- FONCTIONS DE CALCUL DE CONSOMMATION (Déplacées ici pour être déclarées avant utilisation) ---
void calculateConsumptionData() {
float cycleCompleteSec = (float)dureeOnSec + dureeOffSec;
// CORRECTION ICI : Diviser GRANULES_PER_MINUTE_G par 60.0f pour obtenir des g/seconde
float granulesPerCycleG = (float)dureeOnSec * (GRANULES_PER_MINUTE_G / 60.0f);
// Calculs Horaires
if (cycleCompleteSec > 0) {
float cyclesPerSecond = 1.0f / cycleCompleteSec; // cycles par seconde
// Calculs par Minute
cycles_Minute = (unsigned long)(cyclesPerSecond * 60.0f); // 60 sec dans 1 minute
granulesKg_Minute = (granulesPerCycleG * cycles_Minute) / 1000.0f; // Grammes vers Kilogrammes
kwh_Minute = granulesKg_Minute * KWH_PER_KG_GRANULES;
// Calculs Horaires
cycles_Hourly = (unsigned long)(cyclesPerSecond * 3600.0f); // 3600 sec dans 1 heure
granulesKg_Hourly = (granulesPerCycleG * cycles_Hourly) / 1000.0f; // Grammes vers Kilogrammes
kwh_Hourly = granulesKg_Hourly * KWH_PER_KG_GRANULES;
} else { // Évite la division par zéro
cycles_Minute = 0; granulesKg_Minute = 0.0f; kwh_Minute = 0.0f;
cycles_Hourly = 0; granulesKg_Hourly = 0.0f; kwh_Hourly = 0.0f;
}
// Calculs Journaliers (24 heures par jour)
cycles_Daily = cycles_Hourly * 24;
granulesKg_Daily = granulesKg_Hourly * 24;
kwh_Daily = kwh_Hourly * 24;
// Calculs Hebdomadaires (7 jours par semaine)
cycles_Weekly = cycles_Daily * 7;
granulesKg_Weekly = granulesKg_Daily * 7;
kwh_Weekly = kwh_Daily * 7;
// Calculs Mensuels (approximativement 30 jours par mois)
cycles_Monthly = cycles_Daily * 30; // Approximation pour un mois
granulesKg_Monthly = granulesKg_Daily * 30;
kwh_Monthly = kwh_Daily * 30;
// Calculs Annuels (365 jours par an)
cycles_Yearly = cycles_Daily * 365;
granulesKg_Yearly = granulesKg_Hourly * 365;
kwh_Yearly = kwh_Hourly * 365;
}
// --- FONCTIONS D'AFFICHAGE (LCD2 - Heure/Date et Consommation) ---
void updateLcd2TopLineDisplay() {
const char* unitStr;
switch (currentConsumptionUnit) {
case MINUTE: unitStr = "PAR MINUTE"; break;
case HOURLY: unitStr = "PAR HEURE"; break;
case DAILY: unitStr = "PAR JOUR"; break;
case WEEKLY: unitStr = "PAR SEMAINE"; break;
case MONTHLY: unitStr = "PAR MOIS"; break;
case YEARLY: unitStr = "PAR ANNEE"; break;
default: unitStr = ""; break;
}
char currentFullLine[LCD2_WIDTH + 1];
int textLength = strlen(unitStr);
int startCol = (LCD2_WIDTH - textLength) / 2;
// Remplir le buffer avec des espaces et le texte centré
memset(currentFullLine, ' ', LCD2_WIDTH); // Remplir d'espaces
currentFullLine[LCD2_WIDTH] = '\0'; // Terminer la chaîne
if (startCol >= 0) { // S'assurer que le texte ne déborde pas à gauche
strncpy(currentFullLine + startCol, unitStr, textLength);
} else { // Si le texte est plus long que l'écran, le tronquer au début
strncpy(currentFullLine, unitStr + (textLength - LCD2_WIDTH), LCD2_WIDTH);
}
// Comparer avec la ligne précédente pour éviter le scintillement
for (int i = 0; i < LCD2_WIDTH; i++) {
if (currentFullLine[i] != previousUnitLineLcd2[i]) {
lcd2.setCursor(i, 0);
lcd2.print(currentFullLine[i]);
previousUnitLineLcd2[i] = currentFullLine[i];
}
}
}
void updateConsumptionDisplayLcd2() { // Affichage sur LCD2 pour la consommation
char valueBuffer[15]; // Buffer temporaire pour la conversion dtostrf ou ultoa
char combinedValueUnitBuffer[20]; // Buffer pour la valeur et l'unité combinées
char unitSuffix[10]; // Suffixe pour l'unité de temps (/min, /h, /j, /sem, /mois, /a)
float displayGranules, displayKwh;
unsigned long displayCycles;
// Sélection des données à afficher en fonction de l'unité choisie
switch (currentConsumptionUnit) {
case MINUTE:
displayGranules = granulesKg_Minute;
displayCycles = cycles_Minute;
strcpy(unitSuffix, "/min");
displayKwh = kwh_Minute;
break;
case HOURLY:
displayGranules = granulesKg_Hourly;
displayCycles = cycles_Hourly;
strcpy(unitSuffix, "/h"); // Raccourcis pour les suffixes ici, car l'espace est plus limité sur les lignes 1-3
displayKwh = kwh_Hourly;
break;
case DAILY:
displayGranules = granulesKg_Daily;
displayCycles = cycles_Daily;
strcpy(unitSuffix, "/j");
displayKwh = kwh_Hourly;
break;
case WEEKLY:
displayGranules = granulesKg_Weekly;
displayCycles = cycles_Weekly;
strcpy(unitSuffix, "/sem");
displayKwh = kwh_Weekly;
break;
case MONTHLY:
displayGranules = granulesKg_Monthly;
displayCycles = cycles_Monthly;
strcpy(unitSuffix, "/mois");
displayKwh = kwh_Monthly;
break;
case YEARLY:
displayGranules = granulesKg_Yearly;
displayCycles = cycles_Yearly;
strcpy(unitSuffix, "/a");
displayKwh = kwh_Yearly;
break;
}
// Effacer les lignes avant de les réécrire pour éviter les artefacts
lcd2.setCursor(0, 1); lcd2.print(" ");
lcd2.setCursor(0, 2); lcd2.print(" ");
lcd2.setCursor(0, 3); lcd2.print(" ");
// --- Affichage Granulés (Gr: [valeur] kg/unité) ---
dtostrf(displayGranules, 1, 2, valueBuffer); // Convertit float en string, 2 décimales
sprintf(combinedValueUnitBuffer, "%s kg%s", valueBuffer, unitSuffix); // Ex: "123.45 kg/h"
// Calculer la position de début pour la valeur alignée à droite
// Longueur du libellé "Gr: " = 4
int startColGranules = 20 - strlen(combinedValueUnitBuffer);
lcd2.setCursor(0, 1); // Position de départ pour la ligne
lcd2.print("Gr: "); // Imprime le libellé
// Imprime les espaces de remplissage
for (int i = 4; i < startColGranules; ++i) { // Commence après "Gr: " (colonne 4)
lcd2.print(" ");
}
lcd2.print(combinedValueUnitBuffer); // Imprime la valeur et l'unité
// --- Affichage Cycles (Cy: [valeur]/unité) ---
ultoa(displayCycles, valueBuffer, 10); // Convertit unsigned long en string
sprintf(combinedValueUnitBuffer, "%s%s", valueBuffer, unitSuffix); // Ex: "12345/h"
int startColCycles = 20 - strlen(combinedValueUnitBuffer);
lcd2.setCursor(0, 2);
lcd2.print("Cy: ");
for (int i = 4; i < startColCycles; ++i) {
lcd2.print(" ");
}
lcd2.print(combinedValueUnitBuffer);
// --- Affichage Énergie (En: [valeur] kWh/unité) ---
dtostrf(displayKwh, 1, 2, valueBuffer); // Convertit float en string, 2 décimales
sprintf(combinedValueUnitBuffer, "%s kWh%s", valueBuffer, unitSuffix); // Ex: "12.34 kWh/h"
int startColEnergy = 20 - strlen(combinedValueUnitBuffer);
lcd2.setCursor(0, 3);
lcd2.print("En: ");
for (int i = 4; i < startColEnergy; ++i) {
lcd2.print(" ");
}
lcd2.print(combinedValueUnitBuffer);
}
// --- FONCTIONS D'AFFICHAGE (LCD1 - Menu Principal et Sous-menus) (Déplacées ici pour être déclarées avant utilisation) ---
void afficherMenu() {
lcd1.clear();
// Calcule l'index de la première option à afficher sur l'écran
// L'objectif est de garder l'option sélectionnée visible, idéalement centrée ou à la 2ème/3ème ligne
int startIndex = 0;
if (nbOptions > LCD_LINES) { // S'il y a plus d'options que de lignes sur le LCD
if (selectedOption >= (LCD_LINES -1)) { // Si l'option sélectionnée est vers le bas de l'écran
startIndex = selectedOption - (LCD_LINES -1); // Décaler le début pour que l'option sélectionnée soit à l'avant-dernière ligne
}
// S'assurer que startIndex ne dépasse pas la limite supérieure
if (startIndex > nbOptions - LCD_LINES) {
startIndex = nbOptions - LCD_LINES;
}
}
for (int i = 0; i < LCD_LINES; i++) { // Boucle sur les lignes de l'écran (4 lignes)
int optionIndex = startIndex + i; // Index réel de l'option dans le tableau complet
if (optionIndex < nbOptions) { // S'assurer que l'index est valide
lcd1.setCursor(0, i); // Positionne le curseur sur la ligne actuelle de l'écran
// Affiche le ">" si c'est l'option sélectionnée
lcd1.print((optionIndex == selectedOption) ? ">" : " ");
char ligne[21]; // Buffer pour la ligne à afficher, y compris les valeurs dynamiques
// Utilise le texte de base du tableau et ajoute les informations dynamiques
switch (optionIndex) {
case 0: // T. ON
sprintf(ligne, "%s %04d sec", mainMenuOptions[optionIndex].text, dureeOnSec);
lcd1.print(ligne);
break;
case 1: // T. OFF
sprintf(ligne, "%s %04d sec", mainMenuOptions[optionIndex].text, dureeOffSec);
lcd1.print(ligne);
break;
case 2: // Marche/Arrêt
sprintf(ligne, "%s: %s", mainMenuOptions[optionIndex].text, isAutomated ? "ON" : "OFF");
lcd1.print(ligne);
break;
case 3: // Consommation
lcd1.print(mainMenuOptions[optionIndex].text); // Pas de valeur dynamique ici
break;
case 4: // Qualité Granulés
// Assurez-vous que la chaîne ne dépasse pas 20 caractères, y compris le ">" ou " " initial.
const char* gradeText;
switch (currentPelletGrade) {
case SUPREME: gradeText = "Supreme"; break;
case MEDIUM: gradeText = "Moyen"; break;
case COMMON: gradeText = "Commun"; break;
default: gradeText = "Inconnu"; break;
}
// Construction sécurisée pour ne pas dépasser la largeur de l'écran (20 caractères)
// "5. Qualite Granules: " (21 caractères) est déjà trop long. Il faut tronquer.
// Le libellé "5. Qualite Granules" est 19 caractères.
// On a 20 caractères max sur l'écran. 1 pour le ">" ou " ". Reste 19.
// "5. Qualite Granules" est 19 caractères. On ne peut pas mettre le grade en plus.
// Solution: Abréger le titre ou afficher le grade sur une autre ligne si disponible.
// Pour cet affichage de menu principal, on va simplement afficher le titre de l'option.
char optionLine[21];
// Copier le texte de l'option
strncpy(optionLine, mainMenuOptions[optionIndex].text, 20);
optionLine[20] = '\0'; // Assurez-vous que la chaîne est terminée
lcd1.print(optionLine);
break;
// Ajoutez d'autres cas ici si vous ajoutez plus d'options au tableau
default:
lcd1.print(mainMenuOptions[optionIndex].text);
break;
}
} else {
// Effacer les lignes restantes si moins d'options que de lignes d'affichage
lcd1.setCursor(0, i);
lcd1.print(" "); // Efface la ligne complète
}
}
}
void afficherConsumptionUnitMenu() {
lcd1.clear();
lcd1.setCursor(0, 0);
lcd1.print("Choisir unite:");
const int optionsPerScreen = 3; // Lignes 1, 2, 3 sont disponibles pour les options
// Ajuster startIndexConsumptionSubMenu pour que selectedConsumptionSubOption soit toujours visible
if (selectedConsumptionSubOption < startIndexConsumptionSubMenu) {
startIndexConsumptionSubMenu = selectedConsumptionSubOption;
} else if (selectedConsumptionSubOption >= startIndexConsumptionSubMenu + optionsPerScreen) {
startIndexConsumptionSubMenu = selectedConsumptionSubOption - (optionsPerScreen - 1);
}
// S'assurer que startIndexConsumptionSubMenu ne dépasse pas les bornes
if (startIndexConsumptionSubMenu > NUM_CONSUMPTION_UNITS - optionsPerScreen) {
startIndexConsumptionSubMenu = NUM_CONSUMPTION_UNITS - optionsPerScreen;
}
if (startIndexConsumptionSubMenu < 0) { // S'assurer qu'il ne descend pas en dessous de 0
startIndexConsumptionSubMenu = 0;
}
for (int i = 0; i < optionsPerScreen; i++) { // Boucle sur les 3 lignes d'options disponibles (LCD1 lignes 1-3)
int optionIndex = startIndexConsumptionSubMenu + i; // Index réel de l'option
lcd1.setCursor(0, i + 1); // Positionne le curseur sur la ligne actuelle de l'écran (1, 2 ou 3)
if (optionIndex < NUM_CONSUMPTION_UNITS) { // S'assurer que l'index est valide
// Affiche le ">" si c'est l'option sélectionnée
lcd1.print((optionIndex == selectedConsumptionSubOption) ? ">" : " ");
switch (optionIndex) { // Utilise optionIndex pour afficher le texte
case MINUTE:
lcd1.print("Par minute");
break;
case HOURLY:
lcd1.print("Par heure");
break;
case DAILY:
lcd1.print("Par jour");
break;
case WEEKLY:
lcd1.print("Par semaine");
break;
case MONTHLY:
lcd1.print("Par mois");
break;
case YEARLY:
lcd1.print("Par annee");
break;
}
} else {
// Effacer les lignes restantes si moins d'options que d'espace d'affichage
lcd1.print(" "); // Efface la ligne complète
}
}
}
void afficherReglageTitre(int option) {
lcd1.clear();
lcd1.setCursor(0, 0);
if (option == 0) {
lcd1.print("Reglage du temps ON");
} else if (option == 1) {
lcd1.print("Reglage du temps OFF");
}
}
void afficherMinutes() {
char buffer[21];
sprintf(buffer, "Minutes : %02d", regMin);
lcd1.setCursor(0, 2);
lcd1.print(buffer);
lcd1.print(" "); // Efface les caractères restants
}
void afficherSecondes() {
char buffer[21];
sprintf(buffer, "Secondes : %02d", regSec);
lcd1.setCursor(0, 3);
lcd1.print(buffer);
lcd1.print(" "); // Efface les caractères restants
}
// --- FONCTIONS D'AFFICHAGE (LCD1 - Sous-menu Qualité Granulés) (Déplacées ici pour être déclarées avant utilisation) ---
void afficherPelletGradeMenu() {
lcd1.clear();
lcd1.setCursor(0, 0);
lcd1.print("Qualite Granules:");
for (int i = 0; i < 3; i++) { // 3 options: SUPREME, MEDIUM, COMMON
lcd1.setCursor(0, i + 1); // Commencer à la ligne 1
lcd1.print((i == selectedPelletGradeSubOption) ? ">" : " ");
switch (i) {
case SUPREME:
lcd1.print("Supreme");
break;
case MEDIUM:
lcd1.print("Moyen");
break;
case COMMON:
lcd1.print("Commun");
break;
}
}
}
// --- FONCTION SETUP ---
void setup() {
Wire.begin(); // Initialisation de la communication I2C
rtc.begin(); // Initialisation du module RTC
lcd1.init(); // Initialisation du LCD1
lcd2.init(); // Initialisation du LCD2
lcd3.init(); // Initialisation du LCD3
lcd1.backlight(); // Allume le rétroéclairage du LCD1
lcd2.backlight(); // Allume le rétroéclairage du LCD2
lcd3.backlight(); // Allume le rétroéclairage du LCD3
// Nettoyage initial des écrans
lcd1.clear();
lcd2.clear();
lcd3.clear(); // Efface le contenu du troisième écran
pinMode(pinJoystickBtn, INPUT_PULLUP); // Active la résistance de pull-up interne pour le bouton
pinMode(pinRelay, OUTPUT); // Configure la broche du relais en sortie
pinMode(pinAlarmBuzzer, OUTPUT); // Configure la broche du buzzer en sortie
digitalWrite(pinRelay, LOW); // S'assure que le relais est éteint au démarrage
// Si le RTC n'est pas configuré, le règle à la date et heure de compilation
// Date et heure actuelles à Sainte-Adèle, Québec, Canada : Samedi 19 juillet 2025, 15h54:41 EDT
if (!rtc.isrunning()) {
rtc.adjust(DateTime(2025, 7, 19, 15, 54, 41)); // Année, Mois, Jour, Heure, Minute, Seconde
}
// Chargement des données EEPROM (durées ON/OFF, unité de consommation et grade de granulés)
EEPROM.get(EEPROM_ADDR_DUREE_ON, dureeOnSec);
EEPROM.get(EEPROM_ADDR_DUREE_OFF, dureeOffSec);
EEPROM.get(EEPROM_ADDR_CONSUMPTION_UNIT, (int&)currentConsumptionUnit); // Lire l'enum comme un int
EEPROM.get(EEPROM_ADDR_PELLET_GRADE, (int&)currentPelletGrade); // Lire l'enum comme un int
// Vérification et initialisation des valeurs par défaut si EEPROM est vide/corrompue
if (dureeOnSec == 0xFFFF || dureeOnSec > 3600) { // Si valeur invalide (0xFFFF est la valeur par défaut EEPROM vide)
dureeOnSec = 5; // Valeur par défaut
EEPROM.put(EEPROM_ADDR_DUREE_ON, dureeOnSec); // Écrit la valeur par défaut
}
if (dureeOffSec == 0xFFFF || dureeOffSec > 3600) {
dureeOffSec = 10;
EEPROM.put(EEPROM_ADDR_DUREE_OFF, dureeOffSec);
}
// Si la valeur lue pour l'unité de consommation n'est pas une valeur valide de l'enum
if (currentConsumptionUnit >= NUM_CONSUMPTION_UNITS || currentConsumptionUnit < MINUTE) { // Utilise NUM_CONSUMPTION_UNITS pour la borne supérieure
currentConsumptionUnit = DAILY; // Définit sur la valeur par défaut
EEPROM.put(EEPROM_ADDR_CONSUMPTION_UNIT, (int)currentConsumptionUnit); // Écrit la valeur par défaut
}
// Si la valeur lue pour le grade n'est pas valide
if (currentPelletGrade > COMMON || currentPelletGrade < SUPREME) {
currentPelletGrade = SUPREME; // Définit sur la valeur par défaut
EEPROM.put(EEPROM_ADDR_PELLET_GRADE, (int)currentPelletGrade); // Écrit la valeur par défaut
}
// Initialise KWH_PER_KG_GRANULES en fonction du grade chargé ou par défaut
setKwhPerKgGranules(currentPelletGrade);
// Affichage initial des menus et données
afficherMenu();
// Efface les lignes de consommation sur LCD2 pour s'assurer qu'elles sont propres au démarrage
lcd2.setCursor(0, 1);
lcd2.print(" ");
lcd2.setCursor(0, 2);
lcd2.print(" ");
lcd2.setCursor(0, 3);
lcd2.print(" ");
calculateConsumptionData(); // Premier calcul des données de consommation
updateLcd2TopLineDisplay(); // Met à jour l'affichage de l'unité de consommation sur LCD2 ligne 0
updateConsumptionDisplayLcd2(); // Premier affichage des données de consommation sur LCD2
// --- Initialisation du 3ème LCD ---
lcd3.setCursor(0,0);
lcd3.print("Initialisation..."); // Message temporaire
}
// --- FONCTION LOOP ---
void loop() {
int y = getJoystickY(); // Lecture de la position Y du joystick
int x = getJoystickX(); // Lecture de la position X du joystick
int bouton = digitalRead(pinJoystickBtn); // Lecture de l'état du bouton du joystick
unsigned long now = millis(); // Temps actuel en millisecondes
// Mise à jour de la ligne du haut du LCD2 (MAINTENANT AVEC L'UNITÉ DE CONSOMMATION SEULEMENT)
updateLcd2TopLineDisplay();
// Recalcul et affichage des données de consommation périodiquement sur LCD2
if (now - lastConsumptionCalcTime >= CONSUMPTION_CALC_INTERVAL) {
calculateConsumptionData(); // Recalcule les données de consommation
updateConsumptionDisplayLcd2(); // Met à jour l'affichage de la consommation sur LCD2
lastConsumptionCalcTime = now;
}
// --- Gestion du minuteur du relais (automatisation) ---
if (isAutomated) {
unsigned long currentDuration;
if (relayState == HIGH) {
currentDuration = (unsigned long)dureeOnSec * 1000; // Durée ON en ms
} else {
currentDuration = (unsigned long)dureeOffSec * 1000; // Durée OFF en ms
}
if (now - lastRelayStateChange >= currentDuration) {
relayState = !relayState; // Inverse l'état du relais
digitalWrite(pinRelay, relayState); // Applique le nouvel état
lastRelayStateChange = now; // Met à jour le temps du dernier changement
if (relayState == HIGH) {
playUsbConnectSound(); // Joue un son quand le relais s'allume
} else {
playUsbDisconnectSound(); // Joue un son quand le relais s'éteint
}
}
} else { // Si l'automatisation est désactivée, s'assure que le relais est éteint
if (relayState == HIGH) {
digitalWrite(pinRelay, LOW);
relayState = LOW;
}
}
// --- Gestion des sous-menus (Réglage T.ON/T.OFF) ---
if (inSubMenu) {
if (reglMin) { // Réglage des minutes
bool changed = false;
// Joystick vers la GAUCHE (diminuer)
if (x < 200) { // CORRECTION ICI : x < 200 pour physiquement GAUCHE (diminuer)
if (joystickReadyX || (now - lastRepeatTime > REPEAT_INTERVAL && now - lastMove > INITIAL_REPEAT_DELAY)) {
regMin = (regMin == 0) ? 59 : regMin - 1; // Décrémente
changed = true;
}
}
// Joystick vers la DROITE (augmenter)
else if (x > 800) { // CORRECTION ICI : x > 800 pour physiquement DROITE (augmenter)
if (joystickReadyX || (now - lastRepeatTime > REPEAT_INTERVAL && now - lastMove > INITIAL_REPEAT_DELAY)) {
regMin = (regMin + 1) % 60; // Incrémente
changed = true;
}
} else { // Joystick au repos sur l'axe X
joystickReadyX = true; // Réinitialise le drapeau pour le prochain appui
lastMove = now; // Réinitialise le temps de "premier appui"
lastRepeatTime = now; // Réinitialise le temps de répétition
}
if (changed) {
afficherMinutes();
joystickReadyX = false; // Pour ne déclencher qu'une fois au premier appui
lastRepeatTime = now; // Met à jour le temps de la dernière répétition
if (now - lastMove > INITIAL_REPEAT_DELAY) { // Si c'est une répétition
// Ne rien faire de plus, le délai est déjà géré par REPEAT_INTERVAL
} else { // Si c'est le premier mouvement
lastMove = now; // Enregistre le temps du premier mouvement
}
}
if (bouton == LOW) { // Bouton pressé : passer au réglage des secondes
reglMin = false;
reglSec = true;
afficherSecondes();
delay(200); // Délai anti-rebond
// Réinitialiser les timers pour la prochaine phase de réglage
lastMove = now;
lastRepeatTime = now;
joystickReadyX = true;
}
return;
}
if (reglSec) { // Réglage des secondes
bool changed = false;
// Joystick vers la GAUCHE (diminuer)
if (x < 200) { // CORRECTION ICI
if (joystickReadyX || (now - lastRepeatTime > REPEAT_INTERVAL && now - lastMove > INITIAL_REPEAT_DELAY)) {
regSec = (regSec == 0) ? 59 : regSec - 1; // Décrémente
changed = true;
}
}
// Joystick vers la DROITE (augmenter)
else if (x > 800) { // CORRECTION ICI
if (joystickReadyX || (now - lastRepeatTime > REPEAT_INTERVAL && now - lastMove > INITIAL_REPEAT_DELAY)) {
regSec = (regSec + 1) % 60; // Incrémente
changed = true;
}
} else { // Joystick au repos sur l'axe X
joystickReadyX = true;
lastMove = now;
lastRepeatTime = now;
}
if (changed) {
afficherSecondes();
joystickReadyX = false;
lastRepeatTime = now;
if (now - lastMove > INITIAL_REPEAT_DELAY) {
// Ne rien faire
} else {
lastMove = now;
}
}
if (bouton == LOW) { // Bouton pressé : valider et sauvegarder
if (selectedOption == 0) { // Si réglage T.ON
dureeOnSec = regMin * 60 + regSec;
EEPROM.put(EEPROM_ADDR_DUREE_ON, dureeOnSec); // Sauvegarde dans EEPROM
} else if (selectedOption == 1) { // Si réglage T.OFF
dureeOffSec = regMin * 60 + regSec;
EEPROM.put(EEPROM_ADDR_DUREE_OFF, dureeOffSec); // Sauvegarde dans EEPROM
}
calculateConsumptionData(); // Recalculer la consommation après changement de T.ON/T.OFF
updateConsumptionDisplayLcd2(); // Mettre à jour l'affichage de la consommation sur LCD2
updateLcd2TopLineDisplay(); // Mettre à jour l'unité sur la ligne 0 (au cas où elle changeait dans le futur)
reglSec = false;
inSubMenu = false;
afficherMenu(); // Retour au menu principal
delay(200);
// Réinitialiser les timers pour le joystick X et Y
lastMove = now;
lastRepeatTime = now;
joystickReadyX = true;
joystickReadyY = true;
}
return;
}
}
// --- Gestion du sous-menu Consommation ---
else if (inConsumptionSubMenu) {
// Joystick vers le haut (Monter dans le menu)
if (y < 200 && joystickReadyY && now - lastMove > 200) {
selectedConsumptionSubOption = (selectedConsumptionSubOption - 1 + NUM_CONSUMPTION_UNITS) % NUM_CONSUMPTION_UNITS; // Défilement circulaire
afficherConsumptionUnitMenu();
lastMove = now;
joystickReadyY = false;
}
// Joystick vers le bas (Descendre dans le menu)
else if (y > 800 && joystickReadyY && now - lastMove > 200) {
selectedConsumptionSubOption = (selectedConsumptionSubOption + 1) % NUM_CONSUMPTION_UNITS; // Défilement circulaire
afficherConsumptionUnitMenu();
lastMove = now;
joystickReadyY = false;
} else if (y >= 300 && y <= 700) { // Joystick au repos sur l'axe Y
joystickReadyY = true;
}
if (bouton == LOW) { // Bouton pressé : valider le choix de l'unité de consommation
currentConsumptionUnit = (ConsumptionUnit)selectedConsumptionSubOption;
EEPROM.put(EEPROM_ADDR_CONSUMPTION_UNIT, (int)currentConsumptionUnit); // Sauvegarde dans EEPROM
calculateConsumptionData(); // Recalculer avec la nouvelle unité
updateConsumptionDisplayLcd2(); // Mettre à jour l'affichage sur LCD2
updateLcd2TopLineDisplay(); // Mettre à jour l'unité sur la ligne 0
inConsumptionSubMenu = false; // Sort du sous-menu
afficherMenu(); // Retour au menu principal
delay(200);
}
return; // Important pour ne pas traiter d'autres entrées si dans un sous-menu
}
// --- Gestion du sous-menu Qualité Granulés ---
else if (inPelletGradeSubMenu) {
// Joystick vers le haut (Monter dans le menu)
if (y < 200 && joystickReadyY && now - lastMove > 200) {
selectedPelletGradeSubOption = (selectedPelletGradeSubOption - 1 + 3) % 3; // Défilement circulaire
afficherPelletGradeMenu();
lastMove = now;
joystickReadyY = false;
}
// Joystick vers le bas (Descendre dans le menu)
else if (y > 800 && joystickReadyY && now - lastMove > 200) {
selectedPelletGradeSubOption = (selectedPelletGradeSubOption + 1) % 3; // Défilement circulaire
afficherPelletGradeMenu();
lastMove = now;
joystickReadyY = false;
} else if (y >= 300 && y <= 700) { // Joystick au repos sur l'axe Y
joystickReadyY = true;
}
if (bouton == LOW) { // Bouton pressé : valider le choix du grade de granulés
currentPelletGrade = (PelletGrade)selectedPelletGradeSubOption;
EEPROM.put(EEPROM_ADDR_PELLET_GRADE, (int)currentPelletGrade); // Sauvegarde dans EEPROM
setKwhPerKgGranules(currentPelletGrade); // Mettre à jour la constante KWh en fonction du grade
calculateConsumptionData(); // Recalculer avec le nouveau KWh
updateConsumptionDisplayLcd2(); // Mettre à jour l'affichage sur LCD2
inPelletGradeSubMenu = false; // Sort du sous-menu
afficherMenu(); // Retour au menu principal
delay(200);
}
return; // Important pour ne pas traiter d'autres entrées si dans un sous-menu
}
// --- Gestion du menu principal ---
// Joystick vers le haut (Monter dans le menu)
if (y < 200 && joystickReadyY && now - lastMove > 200) {
selectedOption = (selectedOption - 1 + nbOptions) % nbOptions; // Défilement circulaire
afficherMenu();
lastMove = now;
joystickReadyY = false;
}
// Joystick vers le bas (Descendre dans le menu)
else if (y > 800 && joystickReadyY && now - lastMove > 200) {
selectedOption = (selectedOption + 1) % nbOptions; // Défilement circulaire
afficherMenu();
lastMove = now;
joystickReadyY = false;
} else if (y >= 300 && y <= 700) { // Joystick au repos sur l'axe Y
joystickReadyY = true;
}
if (bouton == LOW) { // Bouton pressé : sélectionne une option du menu principal
if (selectedOption == 0) { // Réglage T.ON
inSubMenu = true;
reglMin = true;
regMin = dureeOnSec / 60; // Initialise minutes avec la valeur actuelle
regSec = dureeOnSec % 60; // Initialise secondes avec la valeur actuelle
afficherReglageTitre(selectedOption);
afficherMinutes();
// Réinitialiser les timers pour le joystick X
lastMove = now;
lastRepeatTime = now;
joystickReadyX = true;
} else if (selectedOption == 1) { // Réglage T.OFF
inSubMenu = true;
reglMin = true;
regMin = dureeOffSec / 60;
regSec = dureeOffSec % 60;
afficherReglageTitre(selectedOption);
afficherMinutes();
// Réinitialiser les timers pour le joystick X
lastMove = now;
lastRepeatTime = now;
joystickReadyX = true;
} else if (selectedOption == 2) { // Marche/Arrêt automatisation
isAutomated = !isAutomated; // Inverse l'état
if (isAutomated) {
lastRelayStateChange = now; // Réinitialise le timer si on active
digitalWrite(pinRelay, HIGH);
relayState = HIGH;
playUsbConnectSound();
} else {
digitalWrite(pinRelay, LOW);
relayState = LOW;
playUsbDisconnectSound(); // Joue un son quand le relais s'éteint
}
afficherMenu(); // Rafraîchit le menu pour montrer le nouvel état
delay(200);
} else if (selectedOption == 3) { // Option "Consommation"
inConsumptionSubMenu = true; // Entre dans le sous-menu consommation
selectedConsumptionSubOption = (int)currentConsumptionUnit; // Pré-sélectionne l'unité actuelle dans le sous-menu
startIndexConsumptionSubMenu = 0; // Réinitialise l'index de début pour le sous-menu
afficherConsumptionUnitMenu(); // Affiche le sous-menu
delay(200);
} else if (selectedOption == 4) { // Option "Qualité Granulés"
inPelletGradeSubMenu = true; // Entre dans le sous-menu qualité granulés
selectedPelletGradeSubOption = (int)currentPelletGrade; // Pré-sélectionne le grade actuel
afficherPelletGradeMenu(); // Affiche le sous-menu
delay(200);
}
delay(200); // Délai anti-rebond général pour le bouton
}
// --- Code pour mettre à jour le LCD3 (Heure et Date sans scintillement) ---
DateTime nowRTC = rtc.now(); // Lecture de l'heure et la date actuelles
char currentDateTimeLine[LCD3_WIDTH + 1]; // Buffer pour la chaîne de caractères à afficher
// Formater l'heure et la date (ex: YYYY/MM/DD hh:mm:ss)
sprintf(currentDateTimeLine, "%04d/%02d/%02d %02d:%02d:%02d",
nowRTC.year(), nowRTC.month(), nowRTC.day(),
nowRTC.hour(), nowRTC.minute(), nowRTC.second());
// Mettre à jour le LCD3 caractère par caractère pour éviter le scintillement
for (int i = 0; i < LCD3_WIDTH; i++) {
if (currentDateTimeLine[i] != previousTimeDateLineLcd3[i]) {
lcd3.setCursor(i, 0); // Positionne le curseur
lcd3.print(currentDateTimeLine[i]); // Affiche le caractère modifié
previousTimeDateLineLcd3[i] = currentDateTimeLine[i]; // Met à jour le buffer
}
}
}