// SpaPellet Manager - Reglage Option 1 : Temps ON / Temps OFF + EEPROM + Minuteur Relais + Consommation
// Date : 2025-07-22
// Version : GranuloCompteur_v1_88 // Correction de la logique de répétition du joystick pour les réglages de temps
// Affichage du nombre de btu/h calculé sur Lcd_Infos ligne 3 (correction sprintf) + Unité de consommation par défaut "Par heure" au démarrage
// Centrage "Moteur Actif/Inactif" avec 3 caractères animés de chaque côté sur Lcd_Infos
// Renommage de lcd3 en Lcd_Infos, lcd1 en Lcd_Menu, lcd2 en Lcd_Rapport + Ajout du défilement pour le sous-menu de consommation + Format de la date JJ/MM/AAAA sur Lcd_Infos
// Ajout du décompte du temps ON/OFF sur la 3e ligne (index 2) de Lcd_Infos
// Ajout d'un libellé "ON:" ou "OFF:" devant le décompte sur Lcd_Infos ligne 2
// --- 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 Lcd_Menu(0x27, 20, 4); // Déclaration du premier écran LCD (20 colonnes, 4 lignes) - Renommé de lcd1
LiquidCrystal_I2C Lcd_Rapport(0x26, 20, 4); // Déclaration du deuxième écran LCD (20 colonnes, 4 lignes) - Renommé de lcd2
LiquidCrystal_I2C Lcd_Infos(0x20, 20, 4); // Déclaration du troisième écran LCD (20 colonnes, 4 lignes) - Renommé de lcd3
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 = 11; // Relais pour le chauffage/pompe
const int pinAlarmBuzzer = 12; // 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)
const float KWH_TO_BTU_FACTOR = 3412.14f; // Facteur de conversion de kWh en BTU
// 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_MENU_LINES = 4; // Nombre de lignes affichables sur le Lcd_Menu (votre écran fait 4 lignes)
bool inSubMenu = false; // Vrai si nous sommes dans un sous-menu de réglage (T.ON/T.OFF)
bool adjustingMinutes = true; // NOUVEAU : Vrai si nous réglons les minutes, Faux 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 = 16; // Durée du cycle ON en secondes (valeur par défaut)
unsigned int dureeOffSec = 20; // Durée du cycle OFF en secondes (valeur par défaut)
const int LCD_RAPPORT_WIDTH = 20; // Largeur de l'écran Lcd_Rapport (pour la ligne 0)
const int LCD_INFOS_WIDTH = 20; // Largeur de l'écran Lcd_Infos
// --- BUFFERS POUR AFFICHAGE SANS SCINTILLEMENT (pour Lcd_Rapport ligne 0 et Lcd_Infos ligne 0, 1 et 2) ---
static char previousUnitLineLcdRapport[LCD_RAPPORT_WIDTH + 1] = " "; // Stocke la ligne 0 précédente pour éviter le scintillement
static char previousTimeDateLineLcdInfos[LCD_INFOS_WIDTH + 1] = " "; // Stocke la ligne 0 de Lcd_Infos pour éviter le scintillement
static char previousAnimationLineLcdInfos[LCD_INFOS_WIDTH + 1] = " "; // Stocke la ligne 1 de Lcd_Infos pour éviter le scintillement
static char previousCountdownLineLcdInfos[LCD_INFOS_WIDTH + 1] = " "; // Stocke la ligne 2 de Lcd_Infos pour éviter le scintillement
static char previousBtuLineLcdInfos[LCD_INFOS_WIDTH + 1] = " "; // Stocke la ligne 3 de Lcd_Infos 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
unsigned long lastTimeDateUpdate = 0; // Dernier rafraîchissement de l'heure/date sur Lcd_Infos
const long TIME_DATE_UPDATE_INTERVAL = 1000; // Rafraîchissement toutes les 1 seconde (en ms)
// Variables pour l'animation du relais sur Lcd_Infos
unsigned long lastAnimationUpdateTime = 0;
const long ANIMATION_INTERVAL = 150; // Intervalle de temps entre les frames de l'animation (en ms)
int currentAnimationCharIndex = 0; // Index du caractère d'animation actuel (0, 1, 2 ou 3)
const int NUM_ANIMATION_FRAMES = 4; // Nombre de frames d'animation
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;
float btu_Hourly; // Nouvelle variable pour BTU/h
// --- DÉFINITION DES CARACTÈRES PERSONNALISÉS POUR L'ANIMATION DU RELAIS (Hélice 4 frames) ---
// Cadre 0: Ligne verticale
byte propellerChar0[8] = {
B00100,
B00100,
B00100,
B00100,
B00100,
B00100,
B00100,
B00100
};
// Cadre 1: Ligne diagonale (haut-gauche vers bas-droite)
byte propellerChar1[8] = {
B10000,
B01000,
B00100,
B00010,
B00010,
B00100,
B01000,
B10000
};
// Cadre 2: Ligne horizontale
byte propellerChar2[8] = {
B00000,
B00000,
B00000,
B11111,
B11111,
B00000,
B00000,
B00000
};
// Cadre 3: Ligne diagonale (haut-droite vers bas-gauche)
byte propellerChar3[8] = {
B00001,
B00010,
B00100,
B01000,
B01000,
B00100,
B00010,
B00001
};
// --- 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;
// Calcul des BTU/h (ajouté)
btu_Hourly = kwh_Hourly * KWH_TO_BTU_FACTOR;
}
// --- FONCTIONS D'AFFICHAGE (Lcd_Rapport - Heure/Date et Consommation) ---
void updateLcdRapportTopLineDisplay() {
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[LCD_RAPPORT_WIDTH + 1];
int textLength = strlen(unitStr);
int startCol = (LCD_RAPPORT_WIDTH - textLength) / 2;
// Remplir le buffer avec des espaces et le texte centré
memset(currentFullLine, ' ', LCD_RAPPORT_WIDTH); // Remplir d'espaces
currentFullLine[LCD_RAPPORT_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 - LCD_RAPPORT_WIDTH), LCD_RAPPORT_WIDTH);
}
// Comparer avec la ligne précédente pour éviter le scintillement
for (int i = 0; i < LCD_RAPPORT_WIDTH; i++) {
if (currentFullLine[i] != previousUnitLineLcdRapport[i]) {
Lcd_Rapport.setCursor(i, 0);
Lcd_Rapport.print(currentFullLine[i]);
previousUnitLineLcdRapport[i] = currentFullLine[i];
}
}
}
void updateConsumptionDisplayLcdRapport() { // Affichage sur Lcd_Rapport 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
Lcd_Rapport.setCursor(0, 1); Lcd_Rapport.print(" ");
Lcd_Rapport.setCursor(0, 2); Lcd_Rapport.print(" ");
Lcd_Rapport.setCursor(0, 3); Lcd_Rapport.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);
Lcd_Rapport.setCursor(0, 1); // Position de départ pour la ligne
Lcd_Rapport.print("Gr: "); // Imprime le libellé
// Imprime les espaces de remplissage
for (int i = 4; i < startColGranules; ++i) { // Commence après "Gr: " (colonne 4)
Lcd_Rapport.print(" ");
}
Lcd_Rapport.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);
Lcd_Rapport.setCursor(0, 2);
Lcd_Rapport.print("Cy: ");
for (int i = 4; i < startColCycles; ++i) {
Lcd_Rapport.print(" ");
}
Lcd_Rapport.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);
Lcd_Rapport.setCursor(0, 3);
Lcd_Rapport.print("En: ");
for (int i = 4; i < startColEnergy; ++i) { // Fix: Changed ++i to i < startColEnergy
Lcd_Rapport.print(" "); // CORRECTED TYPO HERE: Lcd_Rapport.print
}
Lcd_Rapport.print(combinedValueUnitBuffer);
}
// --- FONCTIONS D'AFFICHAGE (Lcd_Menu - Menu Principal et Sous-menus) (Déplacées ici pour être déclarées avant utilisation) ---
void afficherMenu() {
Lcd_Menu.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_MENU_LINES) { // S'il y a plus d'options que de lignes sur le Lcd_Menu
if (selectedOption >= (LCD_MENU_LINES -1)) { // Si l'option sélectionnée est vers le bas de l'écran
startIndex = selectedOption - (LCD_MENU_LINES -1); // Décaler le début pour que l'option sélectionnée soit à l'avant-dernier ligne
}
// S'assurer que startIndex ne dépasse pas la limite supérieure
if (startIndex > nbOptions - LCD_MENU_LINES) {
startIndex = nbOptions - LCD_MENU_LINES;
}
}
for (int i = 0; i < LCD_MENU_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
Lcd_Menu.setCursor(0, i); // Positionne le curseur sur la ligne actuelle de l'écran
// Affiche le ">" si c'est l'option sélectionnée
Lcd_Menu.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);
Lcd_Menu.print(ligne);
break;
case 1: // T. OFF
sprintf(ligne, "%s %04d sec", mainMenuOptions[optionIndex].text, dureeOffSec);
Lcd_Menu.print(ligne);
break;
case 2: // Marche/Arrêt
sprintf(ligne, "%s: %s", mainMenuOptions[optionIndex].text, isAutomated ? "ON" : "OFF");
Lcd_Menu.print(ligne);
break;
case 3: // Consommation
Lcd_Menu.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
Lcd_Menu.print(optionLine);
break;
// Ajoutez d'autres cas ici si vous ajoutez plus d'options au tableau
default:
Lcd_Menu.print(mainMenuOptions[optionIndex].text);
break;
}
} else {
// Effacer les lignes restantes si moins d'options que de lignes d'affichage
Lcd_Menu.setCursor(0, i);
Lcd_Menu.print(" "); // Efface la ligne complète
}
}
}
void afficherConsumptionUnitMenu() {
Lcd_Menu.clear();
Lcd_Menu.setCursor(0, 0);
Lcd_Menu.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 (Lcd_Menu lignes 1-3)
int optionIndex = startIndexConsumptionSubMenu + i; // Index réel de l'option
Lcd_Menu.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
Lcd_Menu.print((optionIndex == selectedConsumptionSubOption) ? ">" : " ");
switch (optionIndex) { // Utilise optionIndex pour afficher le texte
case MINUTE:
Lcd_Menu.print("Par minute");
break;
case HOURLY:
Lcd_Menu.print("Par heure");
break;
case DAILY:
Lcd_Menu.print("Par jour");
break;
case WEEKLY:
Lcd_Menu.print("Par semaine");
break;
case MONTHLY:
Lcd_Menu.print("Par mois");
break;
case YEARLY:
Lcd_Menu.print("Par annee");
break;
}
} else {
// Effacer les lignes restantes si moins d'options que d'espace d'affichage
Lcd_Menu.print(" "); // Efface la ligne complète
}
}
}
// Fonction unique pour afficher le réglage du temps (ON ou OFF)
void updateReglageTimeDisplay() {
Lcd_Menu.setCursor(0, 0); // Positionne le curseur pour le titre
if (selectedOption == 0) { // Si réglage T.ON
Lcd_Menu.print("Reglage du temps ON");
} else { // selectedOption == 1, Réglage T.OFF
Lcd_Menu.print("Reglage du temps OFF");
}
Lcd_Menu.print(" "); // Efface le reste de la ligne
char buffer[21]; // Buffer pour la ligne d'affichage complète
if (adjustingMinutes) {
sprintf(buffer, "Min: [%02d] Sec: %02d", regMin, regSec);
} else {
sprintf(buffer, "Min: %02d Sec: [%02d]", regMin, regSec);
}
Lcd_Menu.setCursor(0, 2); // Affichage sur la ligne 2
Lcd_Menu.print(buffer);
Lcd_Menu.print(" "); // Efface les caractères restants sur la ligne
}
// --- FONCTIONS D'AFFICHAGE (Lcd_Infos - Sous-menu Qualité Granulés) (Déplacées ici pour être déclarées avant utilisation) ---
void afficherPelletGradeMenu() {
Lcd_Menu.clear();
Lcd_Menu.setCursor(0, 0);
Lcd_Menu.print("Qualite Granules:");
for (int i = 0; i < 3; i++) { // 3 options: SUPREME, MEDIUM, COMMON
Lcd_Menu.setCursor(0, i + 1); // Commencer à la ligne 1
Lcd_Menu.print((i == selectedPelletGradeSubOption) ? ">" : " ");
switch (i) {
case SUPREME:
Lcd_Menu.print("Supreme");
break;
case MEDIUM:
Lcd_Menu.print("Moyen");
break;
case COMMON:
Lcd_Menu.print("Commun");
break;
}
}
}
// --- NOUVELLE FONCTION POUR AFFICHER L'HEURE ET LA DATE SUR LCD_INFOS ---
void updateLcdInfosTimeDateDisplay() {
DateTime now = rtc.now(); // Obtenir l'heure et la date actuelles du RTC
char timeDateBuffer[20]; // HH:MM DD/MM/AAAA\0 = 19 caractères + null
// Formatage de l'heure (HH:MM)
sprintf(timeDateBuffer, "%02d:%02d", now.hour(), now.minute());
// Ajout de la date (DD/MM/AAAA)
char dateBuffer[12]; // DD/MM/AAAA\0 = 11 caractères + null
sprintf(dateBuffer, " %02d/%02d/%04d", now.day(), now.month(), now.year()); // Utilise now.year() pour l'année complète
strcat(timeDateBuffer, dateBuffer); // Concaténer la date à l'heure
char currentFullLine[LCD_INFOS_WIDTH + 1]; // Buffer pour la ligne complète avec padding
int textLength = strlen(timeDateBuffer);
int startCol = (LCD_INFOS_WIDTH - textLength) / 2;
// Remplir le buffer avec des espaces
memset(currentFullLine, ' ', LCD_INFOS_WIDTH);
currentFullLine[LCD_INFOS_WIDTH] = '\0';
// Copier le texte centré
if (startCol >= 0) {
strncpy(currentFullLine + startCol, timeDateBuffer, textLength);
} else {
// Si le texte est trop long, le tronquer
strncpy(currentFullLine, timeDateBuffer + (textLength - LCD_INFOS_WIDTH), LCD_INFOS_WIDTH);
}
// Comparer avec la ligne précédente pour éviter le scintillement
for (int i = 0; i < LCD_INFOS_WIDTH; i++) {
if (currentFullLine[i] != previousTimeDateLineLcdInfos[i]) {
Lcd_Infos.setCursor(i, 0);
Lcd_Infos.print(currentFullLine[i]);
previousTimeDateLineLcdInfos[i] = currentFullLine[i];
}
}
}
// --- FONCTION MISE À JOUR POUR GÉRER L'AFFICHAGE DU RELAIS SUR LCD_INFOS LIGNE 1 ---
void animateRelayStatus(unsigned long currentMillis) {
char motorLabel[] = "Moteur "; // Texte "Moteur " (avec l'espace)
char statusPart[10]; // Pour "Actif" ou "Inactif"
if (relayState == HIGH) {
strcpy(statusPart, "Actif");
} else {
strcpy(statusPart, "Inactif");
}
// Combine "Moteur " and statusPart for length calculation
char fullMotorStatusText[20]; // Max length "Moteur Inactif" + null
strcpy(fullMotorStatusText, motorLabel);
strcat(fullMotorStatusText, statusPart);
int fullMotorStatusTextLength = strlen(fullMotorStatusText);
// Mettre à jour l'index d'animation si les conditions sont remplies
if (isAutomated && relayState == HIGH) {
if (currentMillis - lastAnimationUpdateTime >= ANIMATION_INTERVAL) {
currentAnimationCharIndex = (currentAnimationCharIndex + 1) % NUM_ANIMATION_FRAMES; // 0, 1, 2, 3
lastAnimationUpdateTime = currentMillis;
}
} else {
// Si non actif, on peut laisser le caractère à son premier état ou le fixer
currentAnimationCharIndex = 0;
}
// Gérer l'affichage ligne par ligne pour le flicker-free
int currentColumn = 0;
// Calculate total content length for centering (3 animation chars + text + 3 animation chars)
int totalContentLength = 3 + fullMotorStatusTextLength + 3;
// Calculate padding for centering. Integer division truncates, which is fine for left padding.
int startPadding = (LCD_INFOS_WIDTH - totalContentLength) / 2;
// --- Partie 1: Rembourrage initial pour le centrage ---
for (int i = 0; i < startPadding; ++i) {
if (' ' != previousAnimationLineLcdInfos[currentColumn]) {
Lcd_Infos.setCursor(currentColumn, 1);
Lcd_Infos.print(' ');
previousAnimationLineLcdInfos[currentColumn] = ' ';
}
currentColumn++;
}
// --- Partie 2: 3 caractères animés (côté GAUCHE) ---
for (int i = 0; i < 3; ++i) {
char charToDisplay = ' '; // Par défaut un espace
if (isAutomated && relayState == HIGH) {
charToDisplay = currentAnimationCharIndex; // ID du caractère personnalisé (0, 1, 2 ou 3)
}
if (charToDisplay != previousAnimationLineLcdInfos[currentColumn]) {
Lcd_Infos.setCursor(currentColumn, 1);
if (charToDisplay == ' ') {
Lcd_Infos.print(' ');
} else {
Lcd_Infos.write(charToDisplay); // Afficher le caractère personnalisé
}
previousAnimationLineLcdInfos[currentColumn] = charToDisplay;
}
currentColumn++;
}
// --- Partie 3: "Moteur Actif/Inactif" (centré) ---
for (int i = 0; i < fullMotorStatusTextLength; ++i) {
if (fullMotorStatusText[i] != previousAnimationLineLcdInfos[currentColumn]) {
Lcd_Infos.setCursor(currentColumn, 1);
Lcd_Infos.print(fullMotorStatusText[i]);
previousAnimationLineLcdInfos[currentColumn] = fullMotorStatusText[i];
}
currentColumn++;
}
// --- Partie 4: 3 caractères animés (côté DROIT) ---
for (int i = 0; i < 3; ++i) {
char charToDisplay = ' '; // Par défaut un espace
if (isAutomated && relayState == HIGH) {
charToDisplay = currentAnimationCharIndex; // ID du caractère personnalisé (0, 1, 2 ou 3)
}
if (charToDisplay != previousAnimationLineLcdInfos[currentColumn]) {
Lcd_Infos.setCursor(currentColumn, 1);
if (charToDisplay == ' ') {
Lcd_Infos.print(' ');
} else {
Lcd_Infos.write(charToDisplay); // Afficher le caractère personnalisé
}
previousAnimationLineLcdInfos[currentColumn] = charToDisplay;
}
currentColumn++;
}
// --- Partie 5: Remplir les espaces restants jusqu'à la fin de la ligne ---
while (currentColumn < LCD_INFOS_WIDTH) {
if (' ' != previousAnimationLineLcdInfos[currentColumn]) {
Lcd_Infos.setCursor(currentColumn, 1);
Lcd_Infos.print(' ');
previousAnimationLineLcdInfos[currentColumn] = ' ';
}
currentColumn++;
}
}
// --- NOUVELLE FONCTION POUR AFFICHER LE DÉCOMPTE SUR LCD_INFOS LIGNE 2 ---
void updateLcdInfosCountdownDisplay(unsigned long currentMillis) {
char timeStr[6]; // "MM:SS\0"
char statusLabel[5]; // "ON: \0" or "OFF:\0"
char combinedCountdownStr[20]; // Buffer pour "ON: MM:SS\0" ou "OFF: MM:SS\0"
long remainingSeconds;
if (isAutomated) {
unsigned long elapsedMillis = currentMillis - lastRelayStateChange;
unsigned long currentPhaseDurationMillis;
if (relayState == HIGH) { // Moteur Actif (ON)
currentPhaseDurationMillis = (unsigned long)dureeOnSec * 1000;
strcpy(statusLabel, "ON: ");
} else { // Moteur Inactif (OFF)
currentPhaseDurationMillis = (unsigned long)dureeOffSec * 1000;
strcpy(statusLabel, "OFF:");
}
// Calcul du temps restant
if (elapsedMillis < currentPhaseDurationMillis) {
remainingSeconds = (currentPhaseDurationMillis - elapsedMillis) / 1000;
} else {
remainingSeconds = 0; // Le temps est écoulé, ou en transition
}
int minutes = remainingSeconds / 60;
int seconds = remainingSeconds % 60;
// Formatage du temps "MM:SS"
sprintf(timeStr, "%02d:%02d", minutes, seconds);
// Combinaison du label et du temps
sprintf(combinedCountdownStr, "%s%s", statusLabel, timeStr);
char currentFullLine[LCD_INFOS_WIDTH + 1];
int textLength = strlen(combinedCountdownStr);
int startCol = (LCD_INFOS_WIDTH - textLength) / 2;
memset(currentFullLine, ' ', LCD_INFOS_WIDTH);
currentFullLine[LCD_INFOS_WIDTH] = '\0';
if (startCol >= 0) {
strncpy(currentFullLine + startCol, combinedCountdownStr, textLength);
} else {
// Si le texte est trop long, le tronquer
strncpy(currentFullLine, combinedCountdownStr + (textLength - LCD_INFOS_WIDTH), LCD_INFOS_WIDTH);
}
// Comparaison et affichage pour éviter le scintillement
for (int i = 0; i < LCD_INFOS_WIDTH; i++) {
if (currentFullLine[i] != previousCountdownLineLcdInfos[i]) {
Lcd_Infos.setCursor(i, 2);
Lcd_Infos.print(currentFullLine[i]);
previousCountdownLineLcdInfos[i] = currentFullLine[i];
}
}
} else {
// Si l'automatisation n'est pas activée, efface la ligne
for (int i = 0; i < LCD_INFOS_WIDTH; i++) {
if (' ' != previousCountdownLineLcdInfos[i]) {
Lcd_Infos.setCursor(i, 2);
Lcd_Infos.print(' ');
previousCountdownLineLcdInfos[i] = ' ';
}
}
}
}
// --- NOUVELLE FONCTION POUR AFFICHER LES BTU/h SUR LCD_INFOS LIGNE 3 ---
void updateLcdInfosBtuDisplay() {
char btuBuffer[15]; // Buffer pour la conversion de float en string
char currentFullLine[LCD_INFOS_WIDTH + 1]; // Buffer pour la ligne complète avec padding
// Utilisation de dtostrf pour convertir le float en chaîne,
// avec 0 décimale pour une valeur entière de BTU/h
dtostrf(btu_Hourly, 1, 0, btuBuffer); // Valeur, largeur minimale, nombre de décimales, buffer
// Construction de la chaîne complète "VALEUR BTU/h"
char fullBtuString[20];
sprintf(fullBtuString, "%s BTU/h", btuBuffer);
int textLength = strlen(fullBtuString);
int startCol = (LCD_INFOS_WIDTH - textLength) / 2;
// Remplir le buffer avec des espaces
memset(currentFullLine, ' ', LCD_INFOS_WIDTH);
currentFullLine[LCD_INFOS_WIDTH] = '\0';
// Copier le texte centré
if (startCol >= 0) {
strncpy(currentFullLine + startCol, fullBtuString, textLength);
} else {
// Si le texte est trop long, le tronquer
strncpy(currentFullLine, fullBtuString + (textLength - LCD_INFOS_WIDTH), LCD_INFOS_WIDTH);
}
// Comparer avec la ligne précédente pour éviter le scintillement
for (int i = 0; i < LCD_INFOS_WIDTH; i++) {
if (currentFullLine[i] != previousBtuLineLcdInfos[i]) {
Lcd_Infos.setCursor(i, 3);
Lcd_Infos.print(currentFullLine[i]);
previousBtuLineLcdInfos[i] = currentFullLine[i];
}
}
}
// --- FONCTION SETUP ---
void setup() {
Wire.begin(); // Initialisation de la communication I2C
rtc.begin(); // Initialisation du module RTC
// Initialisation des LCDs dans l'ordre spécifié: Lcd_Infos, Lcd_Menu, Lcd_Rapport
Lcd_Infos.init();
Lcd_Menu.init();
Lcd_Rapport.init();
// Allumage du rétroéclairage des LCDs dans l'ordre spécifié: Lcd_Infos, Lcd_Menu, Lcd_Rapport
Lcd_Infos.backlight();
Lcd_Menu.backlight();
Lcd_Rapport.backlight();
// Nettoyage initial des écrans dans l'ordre spécifié: Lcd_Infos, Lcd_Menu, Lcd_Rapport
Lcd_Infos.clear();
Lcd_Menu.clear();
Lcd_Rapport.clear();
// Création des caractères personnalisés pour l'animation du relais sur Lcd_Infos
Lcd_Infos.createChar(0, propellerChar0);
Lcd_Infos.createChar(1, propellerChar1);
Lcd_Infos.createChar(2, propellerChar2);
Lcd_Infos.createChar(3, propellerChar3);
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 : Mardi 22 juillet 2025, 06h16:35 EDT
if (!rtc.isrunning()) {
rtc.adjust(DateTime(2025, 7, 22, 6, 16, 35)); // 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 = HOURLY; // 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 Lcd_Rapport pour s'assurer qu'elles sont propres au démarrage
Lcd_Rapport.setCursor(0, 1);
Lcd_Rapport.print(" ");
Lcd_Rapport.setCursor(0, 2);
Lcd_Rapport.print(" ");
Lcd_Rapport.setCursor(0, 3);
Lcd_Rapport.print(" ");
// Efface la ligne de décompte sur Lcd_Infos pour s'assurer qu'elle est propre au démarrage
Lcd_Infos.setCursor(0, 2);
Lcd_Infos.print(" ");
// Efface la ligne BTU sur Lcd_Infos pour s'assurer qu'elle est propre au démarrage
Lcd_Infos.setCursor(0, 3);
Lcd_Infos.print(" ");
calculateConsumptionData(); // Premier calcul des données de consommation
updateLcdRapportTopLineDisplay(); // Met à jour l'affichage de l'unité de consommation sur Lcd_Rapport ligne 0
updateConsumptionDisplayLcdRapport(); // Premier affichage des données de consommation sur Lcd_Rapport
// Le message "Initialisation..." sur Lcd_Infos est remplacé par l'heure/date dans loop()
}
// --- 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 Lcd_Rapport (MAINTENANT AVEC L'UNITÉ DE CONSOMMATION SEULEMENT)
updateLcdRapportTopLineDisplay();
// Recalcul et affichage des données de consommation périodiquement sur Lcd_Rapport et Lcd_Infos (BTU)
if (now - lastConsumptionCalcTime >= CONSUMPTION_CALC_INTERVAL) {
calculateConsumptionData(); // Recalcule les données de consommation (incluant BTU)
updateConsumptionDisplayLcdRapport(); // Met à jour l'affichage de la consommation sur Lcd_Rapport
updateLcdInfosBtuDisplay(); // Met à jour l'affichage des BTU/h sur Lcd_Infos
lastConsumptionCalcTime = now;
}
// Mise à jour de l'heure et de la date sur Lcd_Infos périodiquement
if (now - lastTimeDateUpdate >= TIME_DATE_UPDATE_INTERVAL) {
updateLcdInfosTimeDateDisplay();
lastTimeDateUpdate = now;
}
// Appel de la fonction d'animation du relais sur Lcd_Infos (ligne 1)
animateRelayStatus(now);
// Appel de la fonction de décompte sur Lcd_Infos (ligne 2)
updateLcdInfosCountdownDisplay(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) {
bool changed = false;
// Gestion du joystick X (Gauche/Droite) pour basculer entre minutes et secondes
if (x < 200 && joystickReadyX && now - lastMove > 200) { // Joystick Gauche
adjustingMinutes = true; // Passe au réglage des minutes
changed = true;
lastMove = now; // Re-enregistre le temps pour le délai de 200ms
joystickReadyX = false;
} else if (x > 800 && joystickReadyX && now - lastMove > 200) { // Joystick Droite
adjustingMinutes = false; // Passe au réglage des secondes
changed = true;
lastMove = now; // Re-enregistre le temps pour le délai de 200ms
joystickReadyX = false;
} else if (x >= 300 && x <= 700) { // Joystick X au repos
joystickReadyX = true;
}
// Gestion du joystick Y (Haut/Bas) pour ajuster les valeurs
if (y < 200 || y > 800) { // Joystick est activé (Haut ou Bas)
if (joystickReadyY) { // Première détection de la pression après le repos
// Exécute l'action immédiatement pour la première pression
if (adjustingMinutes) {
if (y < 200) regMin = (regMin + 1) % 60;
else regMin = (regMin == 0) ? 59 : regMin - 1;
} else {
if (y < 200) regSec = (regSec + 1) % 60;
else regSec = (regSec == 0) ? 59 : regSec - 1;
}
changed = true;
lastMove = now; // Enregistre le temps de la première activation
lastRepeatTime = now; // Initialise le temps de répétition
joystickReadyY = false; // Indique que le joystick n'est plus "prêt" pour une nouvelle première pression
} else { // Joystick est toujours maintenu, vérifie les répétitions
if (now - lastMove > INITIAL_REPEAT_DELAY && now - lastRepeatTime > REPEAT_INTERVAL) {
// Exécute l'action pour une répétition
if (adjustingMinutes) {
if (y < 200) regMin = (regMin + 1) % 60;
else regMin = (regMin == 0) ? 59 : regMin - 1;
} else {
if (y < 200) regSec = (regSec + 1) % 60;
else regSec = (regSec == 0) ? 59 : regSec - 1;
}
changed = true;
lastRepeatTime = now; // Met à jour lastRepeatTime pour la prochaine répétition
}
}
} else { // Joystick Y est au repos (position centrale)
joystickReadyY = true; // Réinitialise pour la prochaine première pression
}
if (changed) {
updateReglageTimeDisplay(); // Met à jour l'affichage avec la nouvelle valeur
}
// Bouton pressé : valider et sauvegarder
if (bouton == LOW) {
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
updateConsumptionDisplayLcdRapport(); // Mettre à jour l'affichage de la consommation sur Lcd_Rapport
updateLcdRapportTopLineDisplay(); // Mettre à jour l'unité sur la ligne 0 (au cas où elle changeait dans le futur)
updateLcdInfosBtuDisplay(); // Mettre à jour l'affichage des BTU/h après changement de T.ON/T.OFF
inSubMenu = false; // Sort du sous-menu
afficherMenu(); // Retour au menu principal
delay(200); // Délai anti-rebond pour le bouton de validation
// Réinitialiser les flags du joystick pour une utilisation future
joystickReadyX = true;
joystickReadyY = true;
return; // Sort de la boucle de gestion du joystick pour ce cycle
}
return; // Important pour ne pas traiter d'autres entrées si dans un sous-menu
}
// --- 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é
updateConsumptionDisplayLcdRapport(); // Mettre à jour l'affichage sur Lcd_Rapport
updateLcdRapportTopLineDisplay(); // 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
updateConsumptionDisplayLcdRapport(); // Mettre à jour l'affichage sur Lcd_Rapport
updateLcdInfosBtuDisplay(); // Mettre à jour l'affichage des BTU/h
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;
adjustingMinutes = true; // Commence toujours par ajuster les minutes
regMin = dureeOnSec / 60; // Initialise minutes avec la valeur actuelle
regSec = dureeOnSec % 60; // Initialise secondes avec la valeur actuelle
Lcd_Menu.clear(); // Nettoie l'écran avant le nouvel affichage
updateReglageTimeDisplay(); // Affiche le titre et les valeurs combinées
// Réinitialiser les flags du joystick pour une utilisation future
joystickReadyX = true;
joystickReadyY = true;
} else if (selectedOption == 1) { // Réglage T.OFF
inSubMenu = true;
adjustingMinutes = true; // Commence toujours par ajuster les minutes
regMin = dureeOffSec / 60;
regSec = dureeOffSec % 60;
Lcd_Menu.clear(); // Nettoie l'écran avant le nouvel affichage
updateReglageTimeDisplay(); // Affiche le titre et les valeurs combinées
// Réinitialiser les flags du joystick pour une utilisation future
joystickReadyX = true;
joystickReadyY = 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
}
}