// Réveil intelligent Maestro Version 2025-05-27 06:21
// Ce code a été conçu pour un réveil intelligent utilisant deux écrans LCD I2C, un module RTC (Horloge Temps Réel)
// et un encodeur rotatif pour la navigation et le réglage.
// Il inclut des fonctionnalités comme l'affichage de l'heure/date, le réglage d'une alarme,
// la gestion de la fonction 'snooze' et la sauvegarde des paramètres de l'alarme dans la mémoire EEPROM.
// --- INCLUSION DES BIBLIOTHÈQUES NÉCESSAIRES ---
// Les bibliothèques sont des ensembles de fonctions pré-écrites qui nous simplifient la vie.
// Elles nous permettent de contrôler des composants complexes sans écrire tout le code à partir de zéro.
#include <Wire.h> // Nécessaire pour la communication I2C (Inter-Integrated Circuit).
// C'est le protocole que nos écrans LCD et le module RTC utilisent pour "parler" à l'Arduino.
#include <LiquidCrystal_I2C.h> // Pour contrôler les écrans LCD qui communiquent via I2C.
#include <RTClib.h> // Pour interagir avec le module Horloge Temps Réel (RTC), qui garde l'heure et la date.
#include <EEPROM.h> // Pour lire et écrire des données dans la mémoire EEPROM de l'Arduino.
// Cette mémoire est spéciale : les données y restent même quand l'Arduino est éteint !
// --- INITIALISATION DES COMPOSANTS MATÉRIELS ---
// Crée un objet pour le premier écran LCD.
// 0x27 est l'adresse I2C de l'écran (peut varier, 0x27 ou 0x3F sont les plus courants).
// 20, 4 signifie que c'est un écran de 20 colonnes et 4 lignes.
LiquidCrystal_I2C lcd1(0x27, 20, 4);
// Crée un objet pour le deuxième écran LCD.
// 0x26 est l'adresse I2C de ce deuxième écran.
// 20, 4 signifie également 20 colonnes et 4 lignes.
LiquidCrystal_I2C lcd2(0x26, 20, 4);
// Crée un objet pour le module RTC de type DS1307.
RTC_DS1307 rtc;
// --- DÉFINITION DES DONNÉES UTILES ---
// Tableaux pour afficher les noms des jours et des mois en français.
// 'char daysOfTheWeek[7][12]' signifie un tableau de 7 chaînes de caractères (jours),
// où chaque chaîne peut contenir jusqu'à 11 caractères + 1 pour le caractère de fin (null terminator).
char daysOfTheWeek[7][12] = {"Dimanche", "Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi", "Samedi"};
char monthsOfTheYear[12][12] = {"Janvier", "Fevrier", "Mars", "Avril", "Mai", "Juin", "Juillet", "Aout", "Septembre", "Octobre", "Novembre", "Decembre"};
// --- GESTION DES ÉTATS DU MENU (Machine à états) ---
// Un 'enum' (énumération) est une façon de donner des noms compréhensibles à des nombres.
// Cela nous aide à suivre dans quel "état" se trouve le réveil à un moment donné (ex: affichage normal, menu de réglage, etc.).
enum MenuState {
STATE_NORMAL_DISPLAY, // État par défaut : affiche l'heure et la date sur l'écran principal.
STATE_MAIN_MENU, // L'utilisateur est dans le menu principal.
STATE_SET_ALARM_HOUR, // L'utilisateur règle l'heure de l'alarme.
STATE_SET_ALARM_MINUTE, // L'utilisateur règle les minutes de l'alarme.
STATE_ALARM_ENABLE_DISABLE, // L'utilisateur active ou désactive l'alarme.
STATE_ALARM_RINGING, // L'alarme est en train de sonner.
STATE_SET_DATETIME // L'utilisateur règle la date et l'heure du module RTC.
};
// Variable qui stocke l'état actuel du réveil. Au démarrage, il est en affichage normal.
MenuState currentMenuState = STATE_NORMAL_DISPLAY;
int menuCursor = 0; // Gère la position du curseur dans le menu principal.
int alarmMenuCursor = 0; // Gère la position du curseur dans le menu d'activation/désactivation de l'alarme.
int currentSetPart = 0; // Utilisé pour savoir si on règle les heures ou les minutes de l'alarme.
// --- PARAMÈTRES DE L'ALARME ET DE L'EEPROM ---
// Adresses dans la mémoire EEPROM où nous allons stocker les paramètres de l'alarme.
// Chaque adresse est un "emplacement" où stocker une valeur (un octet ici).
#define ALARM_HOUR_ADDR 0 // Adresse pour l'heure de l'alarme.
#define ALARM_MINUTE_ADDR 1 // Adresse pour les minutes de l'alarme.
#define ALARM_ENABLED_ADDR 2 // Adresse pour savoir si l'alarme est activée ou non.
int alarmHour = 7; // Heure de l'alarme par défaut (7h du matin).
int alarmMinute = 0; // Minute de l'alarme par défaut (00 minutes).
bool alarmEnabled = false; // État de l'alarme : activée (true) ou désactivée (false).
bool alarmTriggeredToday = false; // Pour s'assurer que l'alarme ne sonne qu'une fois par jour.
unsigned long alarmStartTime = 0; // Heure à laquelle l'alarme a commencé à sonner.
// Durée de la fonction 'snooze' en millisecondes (2 minutes).
// 'UL' signifie 'unsigned long' pour éviter les problèmes de dépassement de capacité.
const unsigned long SNOOZE_DURATION_MILLIS = 2UL * 60UL * 1000UL;
bool snoozeActive = false; // Indique si la fonction 'snooze' est active.
unsigned long snoozeEndTimeMillis = 0; // Le moment où le snooze doit se terminer.
// Variables temporaires pour le réglage de la date et de l'heure du RTC.
int setYear, setMonth, setDay, setHour, setMinute, setSecond;
int dateTimeSetPart = 0; // Pour savoir quelle partie de la date/heure on règle (année, mois, jour, etc.).
// --- MÉLODIE DE L'ALARME ---
// Définition des notes de musique (fréquences en Hz).
// Ces valeurs sont des fréquences pour le buzzer.
#define NOTE_C4 262
#define NOTE_D4 294
#define NOTE_E4 330
#define NOTE_F4 349
#define NOTE_G4 392
#define NOTE_A4 440
#define NOTE_B4 494
#define NOTE_C5 523
// La mélodie : une séquence de notes (0 signifie une pause).
int melody[] = {
NOTE_C4, NOTE_E4, NOTE_G4, NOTE_C5,
NOTE_G4, NOTE_E4, NOTE_C4,
NOTE_C4, NOTE_C4,
0 // Une pause à la fin pour la mélodie
};
// La durée de chaque note dans la mélodie (par exemple, 4 pour une noire, 8 pour une croche, etc.).
// C'est la durée inverse, plus le chiffre est petit, plus la note est longue.
int noteDurations[] = {
4, 4, 4, 4, // 4 noires
4, 4, 4,
8, 8, // 2 croches
4 // une noire pour la pause
};
int noteIndex = 0; // Index de la note actuelle dans la mélodie.
unsigned long noteStartTime = 0; // Moment où la note actuelle a commencé à jouer.
// --- DÉFINITION DES BROCHES ARDUINO ---
// Nous utilisons des constantes pour rendre le code plus lisible et facile à modifier.
const int ENCODER_CLK_PIN = 2; // Broche CLK de l'encodeur rotatif (interruption possible sur 2 ou 3).
const int ENCODER_DT_PIN = 3; // Broche DT de l'encodeur rotatif (interruption possible sur 2 ou 3).
const int ENCODER_SW_PIN = 4; // Broche du bouton-poussoir de l'encodeur.
const int MODE_BUTTON_PIN = 9; // Broche du bouton 'MODE'.
const int BUZZER_PIN = 12; // Broche du buzzer (pour le son de l'alarme).
// --- VARIABLES POUR L'ENCODEUR ET LES BOUTONS ---
volatile long encoderCount = 0; // Compteur des impulsions de l'encodeur. 'volatile' est important car
// cette variable est modifiée dans une fonction d'interruption.
const int ENCODER_SENSITIVITY_THRESHOLD = 2; // Sensibilité de l'encodeur (combien de "tics" avant de réagir).
// Variables pour le "débouncing" des boutons (éviter les fausses détections dues aux rebonds mécaniques).
long lastDebounceTime[2] = {0, 0}; // Dernier moment où l'état du bouton a été changé.
long debounceDelay = 50; // Délai en millisecondes pour le débouncing.
byte lastSteadyState[2] = {HIGH, HIGH}; // Dernier état stable connu du bouton.
byte lastReading[2] = {HIGH, HIGH}; // Dernière lecture brute du bouton.
// --- CARACTÈRE PERSONNALISÉ POUR L'AFFICHAGE ---
// Crée un caractère spécial pour une cloche (symbole d'alarme) sur l'écran LCD.
// Chaque ligne représente 5 pixels (0 = éteint, 1 = allumé).
byte bellChar[] = {
0b00100, // X
0b01110, // XXX
0b01110, // XXX
0b01110, // XXX
0b11111, // XXXXX
0b00000, //
0b00100, // X
0b00000 //
};
// --- DÉCLARATIONS DES FONCTIONS (Prototypes) ---
// Ceci permet au compilateur de savoir que ces fonctions existent avant qu'elles ne soient définies plus tard dans le code.
void displayTimeAndDateOnLCD1(DateTime now); // Affiche l'heure et la date sur le premier LCD.
void displayMainMenu(bool forceFullRedraw = false); // Affiche le menu principal sur le deuxième LCD.
void displaySetAlarmTime(int selectedPart); // Affiche l'écran de réglage de l'alarme.
void displayAlarmEnableDisable(bool forceFullRedraw = false); // Affiche l'écran d'activation/désactivation de l'alarme.
void displayAlarmStatusOnLCD2(); // Affiche le statut de l'alarme sur le deuxième LCD en mode normal.
void displaySetDateTime(); // Affiche l'écran de réglage de la date/heure du RTC.
void saveAlarmSettings(); // Sauvegarde les paramètres de l'alarme dans l'EEPROM.
void loadAlarmSettings(); // Charge les paramètres de l'alarme depuis l'EEPROM.
// Gère l'incrémentation/décrémentation des valeurs avec l'encodeur.
void handleEncoderChange(bool up, bool down, int* value, int max_value);
// Lit l'état d'un bouton en gérant le "débouncing".
byte readButton(int buttonPin, int buttonIndex);
// --- FONCTION DE LECTURE DES BOUTONS AVEC DÉBOUNCING ---
// Cette fonction est très importante pour éviter que l'Arduino ne détecte plusieurs appuis
// pour un seul vrai appui sur un bouton physique (à cause des "rebonds" électriques).
byte readButton(int buttonPin, int buttonIndex) {
byte reading = digitalRead(buttonPin); // Lit l'état actuel de la broche du bouton (HIGH ou LOW).
// Si l'état du bouton a changé depuis la dernière lecture...
if (reading != lastReading[buttonIndex]) {
lastDebounceTime[buttonIndex] = millis(); // ...on enregistre le temps de ce changement.
}
// Si le temps écoulé depuis le dernier changement est supérieur au délai de débouncing...
if ((millis() - lastDebounceTime[buttonIndex]) > debounceDelay) {
// ...et que l'état actuel est différent de l'état stable connu...
if (reading != lastSteadyState[buttonIndex]) {
lastSteadyState[buttonIndex] = reading; // ...alors on met à jour l'état stable.
// Si l'état stable est LOW (bouton pressé, car INPUT_PULLUP), on retourne true.
if (lastSteadyState[buttonIndex] == LOW) {
return true; // Le bouton a été pressé de manière valide.
}
}
}
lastReading[buttonIndex] = reading; // Met à jour la dernière lecture brute.
return false; // Le bouton n'est pas pressé ou l'appui n'est pas stable.
}
// --- FONCTION D'INTERRUPTION POUR L'ENCODEUR ROTATIF ---
// Cette fonction est exécutée automatiquement et très rapidement dès qu'un changement
// est détecté sur la broche CLK de l'encodeur. C'est crucial pour ne pas rater les mouvements.
void encoderISR() {
int CLK_state = digitalRead(ENCODER_CLK_PIN); // Lit l'état de la broche CLK.
int DT_state = digitalRead(ENCODER_DT_PIN); // Lit l'état de la broche DT.
// Si CLK est HIGH (montant) et DT est LOW, c'est une rotation dans un sens.
if (CLK_state == HIGH) {
if (DT_state == LOW) {
encoderCount++; // Incrémente le compteur de l'encodeur.
} else {
encoderCount--; // Décrémente le compteur de l'encodeur.
}
}
}
// --- FONCTION DE SAUVEGARDE DES PARAMÈTRES DE L'ALARME ---
// Enregistre l'heure, la minute et l'état de l'alarme dans la mémoire EEPROM de l'Arduino.
// EEPROM.update() est mieux que EEPROM.write() car il n'écrit que si la valeur a changé,
// ce qui prolonge la durée de vie de la mémoire EEPROM (qui a un nombre limité d'écritures).
void saveAlarmSettings() {
EEPROM.update(ALARM_HOUR_ADDR, (byte)alarmHour); // Sauvegarde l'heure.
EEPROM.update(ALARM_MINUTE_ADDR, (byte)alarmMinute); // Sauvegarde les minutes.
EEPROM.update(ALARM_ENABLED_ADDR, (byte)alarmEnabled); // Sauvegarde l'état (activée/désactivée).
// Affichage d'un message de confirmation sur l'écran LCD2.
lcd2.clear();
lcd2.setCursor(0,0);
lcd2.print("Sauvegarde OK!");
delay(1000); // Laisse le message visible pendant 1 seconde.
}
// --- FONCTION DE CHARGEMENT DES PARAMÈTRES DE L'ALARME ---
// Lit les paramètres de l'alarme depuis la mémoire EEPROM au démarrage de l'Arduino.
void loadAlarmSettings() {
alarmHour = (int)EEPROM.read(ALARM_HOUR_ADDR); // Charge l'heure.
alarmMinute = (int)EEPROM.read(ALARM_MINUTE_ADDR); // Charge les minutes.
alarmEnabled = (bool)EEPROM.read(ALARM_ENABLED_ADDR); // Charge l'état.
// Vérifie si les valeurs chargées sont valides. Si elles sont "bizarres" (ex: après un premier flashage),
// on les réinitialise à des valeurs par défaut et on les sauvegarde.
if (alarmHour < 0 || alarmHour > 23 || alarmMinute < 0 || alarmMinute > 59) {
lcd2.clear();
lcd2.setCursor(0,0);
lcd2.print("EEPROM reinitialisee");
delay(1500);
alarmHour = 7;
alarmMinute = 0;
alarmEnabled = false;
saveAlarmSettings(); // Sauvegarde les nouvelles valeurs par défaut.
}
}
// --- FONCTIONS D'AFFICHAGE SUR LES LCD ---
// Affiche le menu principal sur l'écran LCD2.
// 'forceFullRedraw' permet de tout redessiner, utile quand on arrive dans ce menu.
void displayMainMenu(bool forceFullRedraw) {
static int lastMenuCursor = -1; // 'static' signifie que cette variable conserve sa valeur entre les appels de la fonction.
// Si on doit tout redessiner ou si c'est la première fois qu'on affiche le menu...
if (forceFullRedraw || lastMenuCursor == -1) {
lcd2.clear(); // Efface l'écran.
lcd2.setCursor(0, 0); // Place le curseur en haut à gauche.
lcd2.print("Menu Principal:");
// Affiche les options du menu, avec des espaces pour effacer le texte précédent si besoin.
lcd2.setCursor(1, 0); lcd2.print("Regler Reveil ");
lcd2.setCursor(1, 1); lcd2.print("Activer/Desactiver ");
lcd2.setCursor(1, 2); lcd2.print("Regler Date/Heure ");
lcd2.setCursor(1, 3); lcd2.print("Quitter ");
lastMenuCursor = -1; // Réinitialise pour forcer le redessin du ">" au bon endroit.
}
// Efface l'ancien ">" si le curseur a bougé.
if (lastMenuCursor != -1 && lastMenuCursor != menuCursor) {
lcd2.setCursor(0, lastMenuCursor); // Va à l'ancienne position du curseur.
lcd2.print(" "); // Efface le ">".
}
lcd2.setCursor(0, menuCursor); // Va à la nouvelle position du curseur.
lcd2.print(">"); // Dessine le nouveau ">".
lastMenuCursor = menuCursor; // Met à jour la dernière position du curseur.
}
// Affiche l'écran de réglage de l'heure de l'alarme sur LCD2.
// 'selectedPart' indique si on est en train de régler les heures ou les minutes.
void displaySetAlarmTime(int selectedPart) {
lcd2.clear(); // Efface l'écran.
char lineBuffer[21]; // Buffer pour créer des chaînes de caractères (20 caractères + 1 pour fin de chaîne).
// Affiche le titre de l'écran.
sprintf(lineBuffer, "Regler Reveil: "); // 'sprintf' formate une chaîne de caractères.
lcd2.setCursor(0,0);
lcd2.print(lineBuffer);
// Formate l'heure et la minute de l'alarme (ex: "07:00").
char timeBuffer[6]; // Assez grand pour "HH:MM\0".
sprintf(timeBuffer, "%02d:%02d", alarmHour, alarmMinute); // %02d formate un nombre avec 2 chiffres, en ajoutant un zéro si nécessaire.
// Affiche l'heure de l'alarme au centre de la ligne 1.
sprintf(lineBuffer, " %s ", timeBuffer);
lcd2.setCursor(0, 1);
lcd2.print(lineBuffer);
lcd2.cursor(); // Active le curseur.
lcd2.blink(); // Fait clignoter le curseur.
// Positionne le curseur clignotant sous la partie que l'utilisateur est en train de régler (heures ou minutes).
if (selectedPart == 0) { // Si on règle les heures.
lcd2.setCursor(7, 1); // Positionne le curseur sous les heures.
} else { // Si on règle les minutes.
lcd2.setCursor(10, 1); // Positionne le curseur sous les minutes.
}
}
// Affiche l'écran d'activation/désactivation de l'alarme sur LCD2.
void displayAlarmEnableDisable(bool forceFullRedraw) {
static int lastAlarmMenuCursor = -1;
if (forceFullRedraw || lastAlarmMenuCursor == -1) {
lcd2.clear();
lcd2.setCursor(0,0);
lcd2.print("Reveil: ");
lcd2.setCursor(1,1); lcd2.print("ACTIF "); // Option ACTIF
lcd2.setCursor(1,2); lcd2.print("DESACTIVE "); // Option DESACTIVE
lastAlarmMenuCursor = -1;
}
// Efface l'ancien ">" si le curseur a bougé.
if (lastAlarmMenuCursor != -1 && lastAlarmMenuCursor != alarmMenuCursor) {
lcd2.setCursor(0, 1 + lastAlarmMenuCursor); // Ligne 1+0 pour ACTIF, 1+1 pour DESACTIVE.
lcd2.print(" ");
}
lcd2.setCursor(0, 1 + alarmMenuCursor); // Place le ">" à la bonne position.
lcd2.print(">");
lastAlarmMenuCursor = alarmMenuCursor;
}
// Affiche l'état de l'alarme (activée/désactivée, heure de l'alarme, ou snooze) sur LCD2.
// Cette fonction est appelée en continu en mode NORMAL_DISPLAY.
void displayAlarmStatusOnLCD2() {
char line1Buffer[21]; // Buffer pour la ligne 1.
char line2Buffer[21]; // Buffer pour la ligne 2.
// Si l'alarme est activée...
if (alarmEnabled) {
char alarmTimeBuffer[6];
sprintf(alarmTimeBuffer, "%02d:%02d", alarmHour, alarmMinute); // Formate l'heure de l'alarme.
sprintf(line1Buffer, "ALARME: %s ", alarmTimeBuffer); // Affiche "ALARME: HH:MM".
// Si le snooze est actif...
if (snoozeActive) {
long remainingMillis = (long)snoozeEndTimeMillis - (long)millis(); // Calcule le temps restant pour le snooze.
if (remainingMillis < 0) remainingMillis = 0; // S'assure que le temps restant ne soit pas négatif.
int displayMinutes = remainingMillis / (60L * 1000L); // Convertit en minutes.
int displaySeconds = (remainingMillis % (60L * 1000L)) / 1000L; // Convertit le reste en secondes.
char snoozeCountBuffer[6];
sprintf(snoozeCountBuffer, "%02d:%02d", displayMinutes, displaySeconds); // Formate le temps de snooze.
sprintf(line2Buffer, "SNOOZE: %s ", snoozeCountBuffer); // Affiche "SNOOZE: MM:SS".
} else {
// Si pas de snooze, la ligne 2 est vide.
sprintf(line2Buffer, " ");
}
} else {
// Si l'alarme est désactivée.
sprintf(line1Buffer, "ALARME DESACTIVE ");
sprintf(line2Buffer, " "); // Ligne 2 vide.
}
// Ces variables 'static' permettent de ne rafraîchir l'écran que si le contenu a changé,
// ce qui évite les scintillements et est plus efficace.
static char lastLine1Buffer[21] = "";
static char lastLine2Buffer[21] = "";
if (strcmp(line1Buffer, lastLine1Buffer) != 0) { // 'strcmp' compare deux chaînes. Si elles sont différentes (résultat != 0).
lcd2.setCursor(0,0);
lcd2.print(line1Buffer);
strcpy(lastLine1Buffer, line1Buffer); // Copie le nouveau contenu pour la prochaine comparaison.
}
if (strcmp(line2Buffer, lastLine2Buffer) != 0) {
lcd2.setCursor(0,1);
lcd2.print(line2Buffer);
strcpy(lastLine2Buffer, line2Buffer);
}
}
// Affiche l'écran de réglage de la date et de l'heure sur LCD2.
void displaySetDateTime() {
char lineBuffer[21];
// Variables statiques pour ne rafraîchir l'écran que si les valeurs ont changé.
static int lastSetYear = -1, lastSetMonth = -1, lastSetDay = -1;
static int lastSetHour = -1, lastSetMinute = -1, lastSetSecond = -1;
static int lastDateTimeSetPart = -1;
// Si c'est le premier affichage de ce menu, on efface tout.
if (lastDateTimeSetPart == -1) {
lcd2.clear();
lcd2.setCursor(0,0);
lcd2.print("Regler Date/Heure: ");
}
// Formate la date et l'heure actuelle du réglage (ex: "AAAA-MM-JJ HH:MM:SS").
char dateTimeString[20];
sprintf(dateTimeString, "%04d-%02d-%02d %02d:%02d:%02d",
setYear, setMonth, setDay, setHour, setMinute, setSecond);
// Vérifie si le contenu affiché a changé.
bool contentChanged = (setYear != lastSetYear || setMonth != lastSetMonth || setDay != lastSetDay ||
setHour != lastSetHour || setMinute != lastSetMinute || setSecond != lastSetSecond);
// Si le contenu a changé ou si c'est le premier affichage, on met à jour la ligne.
if (contentChanged || lastDateTimeSetPart == -1) {
// Remplir le buffer d'espaces pour effacer l'ancienne valeur.
for (int i = 0; i < 20; i++) {
lineBuffer[i] = ' ';
}
lineBuffer[20] = '\0'; // Ajoute le caractère de fin de chaîne.
// Copie la chaîne de date/heure formatée dans le buffer.
memcpy(&lineBuffer[0], dateTimeString, strlen(dateTimeString));
lcd2.setCursor(0, 1); // Va à la ligne 1.
lcd2.print(lineBuffer); // Affiche la date/heure.
}
// Gère la position du curseur clignotant.
if (dateTimeSetPart != lastDateTimeSetPart || lastDateTimeSetPart == -1) {
lcd2.noCursor(); // Cache le curseur avant de le repositionner.
lcd2.noBlink(); // Arrête le clignotement.
// Positionne le curseur selon la partie que l'on règle.
lcd2.setCursor(0, 1); // Position de base
switch (dateTimeSetPart) {
case 0: lcd2.setCursor(0, 1); break; // Année
case 1: lcd2.setCursor(5, 1); break; // Mois
case 2: lcd2.setCursor(8, 1); break; // Jour
case 3: lcd2.setCursor(11, 1); break; // Heure
case 4: lcd2.setCursor(14, 1); break; // Minute
case 5: lcd2.setCursor(17, 1); break; // Seconde
}
lcd2.cursor(); // Affiche le curseur.
lcd2.blink(); // Fait clignoter le curseur.
}
// Met à jour les valeurs statiques pour la prochaine comparaison.
lastSetYear = setYear; lastSetMonth = setMonth; lastSetDay = setDay;
lastSetHour = setHour; lastSetMinute = setMinute; lastSetSecond = setSecond;
lastDateTimeSetPart = dateTimeSetPart;
}
// --- FONCTION SETUP : EXÉCUTÉE UNE SEULE FOIS AU DÉMARRAGE DE L'ARDUINO ---
void setup() {
Wire.begin(); // Initialise la communication I2C. Indispensable pour LCD et RTC.
// Initialise les écrans LCD, allume le rétroéclairage et efface leur contenu.
lcd1.init(); lcd1.backlight(); lcd1.clear();
lcd2.init(); lcd2.backlight(); lcd2.print("Chargement..."); delay(1000); // Affiche un message de chargement.
lcd1.createChar(0, bellChar); // Charge le caractère personnalisé (la cloche) dans la mémoire de l'écran LCD1 à l'emplacement 0.
// Définit les broches des boutons et de l'encodeur comme des entrées avec résistance de "pull-up" interne.
// Cela signifie que la broche est normalement HIGH, et devient LOW quand le bouton est pressé.
pinMode(ENCODER_SW_PIN, INPUT_PULLUP);
pinMode(MODE_BUTTON_PIN, INPUT_PULLUP);
pinMode(BUZZER_PIN, OUTPUT); // Définit la broche du buzzer comme une sortie.
// Attache une interruption à la broche CLK de l'encodeur.
// Cela signifie que la fonction 'encoderISR' sera appelée automatiquement
// dès qu'un changement d'état (HIGH ou LOW) est détecté sur cette broche.
attachInterrupt(digitalPinToInterrupt(ENCODER_CLK_PIN), encoderISR, CHANGE);
// Initialisation du module RTC.
if (!rtc.begin()) { // Si le RTC n'est pas détecté ou ne démarre pas...
lcd2.clear(); lcd2.print("RTC introuvable!"); while (1); // ...affiche une erreur et bloque le programme.
}
// Si le RTC n'a pas été réglé (ex: première utilisation ou pile vide)...
if (!rtc.isrunning()) {
// ...on le règle à la date et heure de compilation du programme.
// F(__DATE__) et F(__TIME__) sont des macros Arduino qui donnent la date et l'heure de compilation.
rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
} else {
// Si le RTC fonctionne, on vérifie si l'année est valide (pour éviter des dates erronées après un arrêt).
DateTime checkTime = rtc.now(); // Récupère l'heure actuelle du RTC.
if (checkTime.year() < 2024) { // Si l'année est avant 2024 (une valeur arbitraire pour détecter un problème).
rtc.adjust(DateTime(F(__DATE__), F(__TIME__))); // On le règle de nouveau.
}
}
loadAlarmSettings(); // Charge les paramètres de l'alarme depuis l'EEPROM au démarrage.
}
// --- FONCTION LOOP : EXÉCUTÉE EN CONTINU APRÈS LE SETUP ---
void loop() {
DateTime now = rtc.now(); // Récupère l'heure et la date actuelles du module RTC.
// Lit l'état des boutons avec la fonction de débouncing.
bool selectPressed = readButton(ENCODER_SW_PIN, 0); // Bouton de l'encodeur (SELECT).
bool modePressed = readButton(MODE_BUTTON_PIN, 1); // Bouton MODE.
bool encoder_up = false; // Indique si l'encodeur a tourné vers le haut.
bool encoder_down = false; // Indique si l'encodeur a tourné vers le bas.
long currentEncoderCount;
noInterrupts(); // Désactive temporairement les interruptions pour lire la variable 'encoderCount' en toute sécurité.
currentEncoderCount = encoderCount;
interrupts(); // Réactive les interruptions.
// Vérifie si l'encodeur a tourné suffisamment (selon le seuil de sensibilité).
if (currentEncoderCount >= ENCODER_SENSITIVITY_THRESHOLD) {
encoder_up = true;
noInterrupts();
encoderCount -= ENCODER_SENSITIVITY_THRESHOLD; // Réduit le compteur.
interrupts();
} else if (currentEncoderCount <= -ENCODER_SENSITIVITY_THRESHOLD) {
encoder_down = true;
noInterrupts();
encoderCount += ENCODER_SENSITIVITY_THRESHOLD; // Augmente le compteur.
interrupts();
}
static unsigned long lastDisplayUpdate = 0; // Temps du dernier rafraîchissement de LCD1.
static unsigned long lastAlarmDisplayUpdate = 0; // Temps du dernier rafraîchissement de LCD2 en mode normal.
// GESTION DU BOUTON MODE
// Si le bouton MODE est pressé et qu'on est en affichage normal...
if (currentMenuState == STATE_NORMAL_DISPLAY && modePressed) {
currentMenuState = STATE_MAIN_MENU; // On passe au menu principal.
menuCursor = 0; // Réinitialise le curseur du menu.
displayMainMenu(true); // Affiche le menu principal (force un redessin complet).
noTone(BUZZER_PIN); // Arrête le buzzer si l'alarme sonnait.
lcd2.noCursor(); // Cache le curseur sur LCD2.
lcd2.noBlink(); // Arrête le clignotement.
snoozeActive = false; // Désactive le snooze.
}
// Si le bouton MODE est pressé dans un autre état du menu (pour revenir au menu principal ou quitter).
else if (modePressed) {
if (currentMenuState == STATE_MAIN_MENU) {
currentMenuState = STATE_NORMAL_DISPLAY; // Quitte le menu pour revenir à l'affichage normal.
lcd2.clear(); // Efface l'écran 2.
} else {
currentMenuState = STATE_MAIN_MENU; // Revient au menu principal depuis un sous-menu.
displayMainMenu(true);
}
noTone(BUZZER_PIN);
lcd2.noCursor();
lcd2.noBlink();
currentSetPart = 0; // Réinitialise la partie de réglage pour l'alarme.
dateTimeSetPart = 0; // Réinitialise la partie de réglage pour la date/heure.
snoozeActive = false; // Désactive le snooze.
}
// --- LOGIQUE PRINCIPALE DU PROGRAMME BASÉE SUR L'ÉTAT ACTUEL ---
// Un "switch-case" est utilisé pour exécuter différentes actions en fonction de l'état actuel (currentMenuState).
switch (currentMenuState) {
case STATE_NORMAL_DISPLAY: // État : Affichage normal de l'heure et de l'alarme.
// Met à jour l'affichage de l'heure/date sur LCD1 toutes les secondes.
if (millis() - lastDisplayUpdate >= 1000) {
displayTimeAndDateOnLCD1(now);
lastDisplayUpdate = millis();
}
// Met à jour l'affichage du statut de l'alarme sur LCD2.
// Plus fréquent en mode snooze (250ms) pour une mise à jour rapide du compte à rebours, sinon 1s.
if (millis() - lastAlarmDisplayUpdate >= (snoozeActive ? 250 : 1000)) {
displayAlarmStatusOnLCD2();
lastAlarmDisplayUpdate = millis();
}
// VÉRIFICATION DE L'ALARME
if (alarmEnabled) { // Si l'alarme est activée.
bool shouldRingNow = false; // Variable pour savoir si l'alarme doit sonner maintenant.
if (!snoozeActive) { // Si le snooze n'est PAS actif.
// Vérifie si l'heure actuelle correspond à l'heure de l'alarme et qu'elle n'a pas encore sonné aujourd'hui.
if (now.hour() == alarmHour && now.minute() == alarmMinute && now.second() == 0 && !alarmTriggeredToday) {
shouldRingNow = true; // L'alarme doit sonner.
}
} else { // Si le snooze EST actif.
// Vérifie si le temps de snooze est écoulé.
if (millis() >= snoozeEndTimeMillis) {
shouldRingNow = true; // L'alarme doit sonner de nouveau.
snoozeActive = false; // Le snooze est terminé.
alarmTriggeredToday = false; // Permet à l'alarme de sonner si elle était désactivée par snooze avant minuit
}
}
if (shouldRingNow) {
currentMenuState = STATE_ALARM_RINGING; // Passe à l'état "alarme en train de sonner".
alarmStartTime = millis(); // Enregistre le début de la sonnerie.
noteIndex = 0; // Réinitialise l'index de la mélodie.
noteStartTime = millis(); // Démarre la lecture de la première note.
alarmTriggeredToday = true; // Marque l'alarme comme déclenchée pour aujourd'hui.
// Affiche un message de réveil sur LCD2.
lcd2.clear();
char alarmMsgBuffer[21];
sprintf(alarmMsgBuffer, "REVEIL ! ");
lcd2.setCursor(0,0); lcd2.print(alarmMsgBuffer);
char alarmTimeMsgBuffer[21];
sprintf(alarmTimeMsgBuffer, "%02d:%02d ", alarmHour, alarmMinute);
lcd2.setCursor(0,1); lcd2.print(alarmTimeMsgBuffer);
}
}
// Réinitialise 'alarmTriggeredToday' à minuit pour permettre à l'alarme de sonner le jour suivant.
// On vérifie maintenant.second() == 1 pour s'assurer que ça ne se déclenche qu'une fois après minuit pile.
if (now.hour() == 0 && now.minute() == 0 && now.second() == 1 && alarmTriggeredToday) {
alarmTriggeredToday = false;
snoozeActive = false; // S'assure que le snooze est aussi désactivé.
}
break; // Fin du STATE_NORMAL_DISPLAY
case STATE_ALARM_RINGING: // État : L'alarme est en train de sonner.
{ // Les accolades créent une "portée" pour les variables temporaires de ce case.
int currentNote = melody[noteIndex]; // Récupère la note actuelle.
int currentNoteDuration = 1000 / noteDurations[noteIndex]; // Calcule la durée de la note en ms.
// Si le temps de la note actuelle est écoulé...
if (millis() - noteStartTime >= currentNoteDuration + 30) { // +30ms pour une petite pause entre les notes.
noTone(BUZZER_PIN); // Arrête la note précédente.
noteIndex++; // Passe à la note suivante.
// Si on a atteint la fin de la mélodie, on recommence depuis le début.
if (noteIndex >= sizeof(melody)/sizeof(melody[0])) {
noteIndex = 0;
}
noteStartTime = millis(); // Enregistre le début de la nouvelle note.
// Si la nouvelle note n'est pas une pause (0), on la joue.
if (melody[noteIndex] != 0) {
tone(BUZZER_PIN, melody[noteIndex], 0); // Joue la note (durée 0 pour un contrôle manuel par le code).
}
}
}
// Affiche le message de réveil en continu.
char alarmRingLine1[21];
char alarmRingLine2[21];
sprintf(alarmRingLine1, "REVEIL ! ");
sprintf(alarmRingLine2, "%02d:%02d ", alarmHour, alarmMinute);
lcd2.setCursor(0,0); lcd2.print(alarmRingLine1);
lcd2.setCursor(0,1); lcd2.print(alarmRingLine2);
// Si l'alarme a sonné pendant plus que la durée du snooze (et qu'elle n'a pas été coupée par l'utilisateur)...
// C'est une sécurité si l'utilisateur ne fait rien, l'alarme finit par s'arrêter.
if (millis() - alarmStartTime > SNOOZE_DURATION_MILLIS) {
noTone(BUZZER_PIN); // Arrête le buzzer.
currentMenuState = STATE_NORMAL_DISPLAY; // Revient à l'affichage normal.
lcd2.clear(); // Efface LCD2.
alarmTriggeredToday = true; // Marque l'alarme comme déclenchée (pour ne pas sonner à nouveau).
snoozeActive = false; // S'assure que le snooze est désactivé.
}
// Si le bouton SELECT est pressé pendant que l'alarme sonne (pour faire un SNOOZE).
if (selectPressed) {
noTone(BUZZER_PIN); // Arrête le buzzer.
snoozeActive = true; // Active le snooze.
currentMenuState = STATE_NORMAL_DISPLAY; // Revient à l'affichage normal.
lcd2.clear(); // Efface LCD2.
snoozeEndTimeMillis = millis() + SNOOZE_DURATION_MILLIS; // Calcule la fin du snooze.
}
// Si le bouton MODE est pressé pendant que l'alarme sonne (pour l'arrêter complètement).
else if (modePressed) {
noTone(BUZZER_PIN); // Arrête le buzzer.
currentMenuState = STATE_NORMAL_DISPLAY; // Revient à l'affichage normal.
lcd2.clear(); // Efface LCD2.
alarmEnabled = false; // Désactive l'alarme.
snoozeActive = false; // Désactive le snooze.
alarmTriggeredToday = true; // Marque l'alarme comme déclenchée (pour ne pas sonner à nouveau).
saveAlarmSettings(); // Sauvegarde les paramètres (l'alarme est maintenant désactivée).
}
break; // Fin du STATE_ALARM_RINGING
case STATE_MAIN_MENU: // État : L'utilisateur est dans le menu principal.
if (encoder_up) { // Si l'encodeur tourne vers le haut.
menuCursor = (menuCursor + 1) % 4; // Incrémente le curseur, revient à 0 après 3 (0, 1, 2, 3).
displayMainMenu(); // Rafraîchit l'affichage du menu.
}
if (encoder_down) { // Si l'encodeur tourne vers le bas.
menuCursor = (menuCursor == 0) ? 3 : menuCursor - 1; // Décrémente, passe de 0 à 3.
displayMainMenu();
}
if (selectPressed) { // Si le bouton SELECT est pressé.
// Agit en fonction de l'option sélectionnée par le curseur.
if (menuCursor == 0) { // Option "Régler Réveil".
currentMenuState = STATE_SET_ALARM_HOUR; // Passe à l'état de réglage de l'heure.
currentSetPart = 0; // Commence par régler les heures.
displaySetAlarmTime(currentSetPart); // Affiche l'écran de réglage.
} else if (menuCursor == 1) { // Option "Activer/Désactiver".
currentMenuState = STATE_ALARM_ENABLE_DISABLE; // Passe à l'état d'activation/désactivation.
alarmMenuCursor = alarmEnabled ? 0 : 1; // Positionne le curseur sur ACTIF si alarme activée, sinon sur DESACTIVE.
displayAlarmEnableDisable(true); // Affiche l'écran.
} else if (menuCursor == 2) { // Option "Régler Date/Heure".
currentMenuState = STATE_SET_DATETIME; // Passe à l'état de réglage de la date/heure.
// Initialise les variables de réglage avec l'heure et la date actuelles du RTC.
setYear = now.year();
setMonth = now.month();
setDay = now.day();
setHour = now.hour();
setMinute = now.minute();
setSecond = now.second();
dateTimeSetPart = 0; // Commence par régler l'année.
displaySetDateTime(); // Affiche l'écran de réglage.
} else if (menuCursor == 3) { // Option "Quitter".
currentMenuState = STATE_NORMAL_DISPLAY; // Revient à l'affichage normal.
lcd2.clear(); // Efface LCD2.
}
}
break; // Fin du STATE_MAIN_MENU
case STATE_SET_ALARM_HOUR: // État : Réglage de l'heure de l'alarme.
if (encoder_up) {
handleEncoderChange(true, false, &alarmHour, 24); // Incrémente l'heure (max 23, revient à 0).
displaySetAlarmTime(currentSetPart); // Rafraîchit l'affichage.
} else if (encoder_down) {
handleEncoderChange(false, true, &alarmHour, 24); // Décrémente l'heure (min 0, revient à 23).
displaySetAlarmTime(currentSetPart);
}
if (selectPressed) { // Si SELECT est pressé.
currentMenuState = STATE_SET_ALARM_MINUTE; // Passe au réglage des minutes.
currentSetPart = 1; // Indique qu'on règle les minutes.
displaySetAlarmTime(currentSetPart); // Rafraîchit l'affichage pour les minutes.
}
break; // Fin du STATE_SET_ALARM_HOUR
case STATE_SET_ALARM_MINUTE: // État : Réglage des minutes de l'alarme.
if (encoder_up) {
handleEncoderChange(true, false, &alarmMinute, 60); // Incrémente les minutes (max 59, revient à 0).
displaySetAlarmTime(currentSetPart);
} else if (encoder_down) {
handleEncoderChange(false, true, &alarmMinute, 60); // Décrémente les minutes (min 0, revient à 59).
displaySetAlarmTime(currentSetPart);
}
if (selectPressed) { // Si SELECT est pressé.
saveAlarmSettings(); // Sauvegarde les nouvelles heures/minutes de l'alarme dans l'EEPROM.
currentMenuState = STATE_MAIN_MENU; // Revient au menu principal.
displayMainMenu(true); // Affiche le menu principal.
lcd2.noCursor(); // Cache le curseur.
lcd2.noBlink(); // Arrête le clignotement.
}
break; // Fin du STATE_SET_ALARM_MINUTE
case STATE_ALARM_ENABLE_DISABLE: // État : Activation/Désactivation de l'alarme.
if (encoder_up || encoder_down) { // Si l'encodeur est tourné (peu importe le sens).
alarmMenuCursor = (alarmMenuCursor == 0) ? 1 : 0; // Bascule le curseur entre ACTIF (0) et DESACTIVE (1).
displayAlarmEnableDisable(); // Rafraîchit l'affichage.
}
if (selectPressed) { // Si SELECT est pressé.
alarmEnabled = (alarmMenuCursor == 0); // Active l'alarme si le curseur est sur ACTIF (0), sinon la désactive.
saveAlarmSettings(); // Sauvegarde l'état de l'alarme.
currentMenuState = STATE_MAIN_MENU; // Revient au menu principal.
displayMainMenu(true); // Affiche le menu principal.
}
break; // Fin du STATE_ALARM_ENABLE_DISABLE
case STATE_SET_DATETIME: // État : Réglage de la date et de l'heure du RTC.
{ // Accolades pour les variables locales.
// Tableau pour connaître le nombre de jours dans chaque mois (utile pour la validation du jour).
// Le premier élément (index 0) est mis à 0 car les mois vont de 1 à 12.
int daysInMonth[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
// Gère les années bissextiles pour Février.
if (setYear % 4 == 0 && (setYear % 100 != 0 || setYear % 400 == 0)) {
daysInMonth[2] = 29; // Février a 29 jours en année bissextile.
}
if (encoder_up) { // Si l'encodeur tourne vers le haut.
switch (dateTimeSetPart) { // Incrémente la partie sélectionnée.
case 0: setYear = (setYear >= 2099) ? 2000 : setYear + 1; break; // Année (entre 2000 et 2099).
case 1: setMonth = (setMonth >= 12) ? 1 : setMonth + 1; break; // Mois (entre 1 et 12).
case 2: setDay = (setDay >= daysInMonth[setMonth]) ? 1 : setDay + 1; break; // Jour (entre 1 et max jours du mois).
case 3: setHour = (setHour >= 23) ? 0 : setHour + 1; break; // Heure (entre 0 et 23).
case 4: setMinute = (setMinute >= 59) ? 0 : setMinute + 1; break; // Minute (entre 0 et 59).
case 5: setSecond = (setSecond >= 59) ? 0 : setSecond + 1; break; // Seconde (entre 0 et 59).
}
displaySetDateTime(); // Rafraîchit l'affichage.
} else if (encoder_down) { // Si l'encodeur tourne vers le bas.
switch (dateTimeSetPart) { // Décrémente la partie sélectionnée.
case 0: setYear = (setYear <= 2000) ? 2099 : setYear - 1; break;
case 1: setMonth = (setMonth <= 1) ? 12 : setMonth - 1; break;
case 2: setDay = (setDay <= 1) ? daysInMonth[setMonth] : setDay - 1; break;
case 3: setHour = (setHour <= 0) ? 23 : setHour - 1; break;
case 4: setMinute = (setMinute <= 0) ? 59 : setMinute - 1; break;
case 5: setSecond = (setSecond <= 0) ? 59 : setSecond - 1; break;
}
displaySetDateTime();
}
if (selectPressed) { // Si SELECT est pressé.
dateTimeSetPart++; // Passe à la partie suivante à régler.
if (dateTimeSetPart > 5) { // Si toutes les parties ont été réglées (année, mois, jour, heure, minute, seconde).
// Met à jour le module RTC avec la nouvelle date et heure.
rtc.adjust(DateTime(setYear, setMonth, setDay, setHour, setMinute, setSecond));
// Affiche un message de confirmation.
lcd2.clear();
lcd2.setCursor(0,0);
lcd2.print("RTC mis a jour !");
delay(1500);
currentMenuState = STATE_MAIN_MENU; // Revient au menu principal.
displayMainMenu(true); // Affiche le menu.
lcd2.noCursor(); // Cache le curseur.
lcd2.noBlink(); // Arrête le clignotement.
dateTimeSetPart = 0; // Réinitialise pour la prochaine fois.
} else {
displaySetDateTime(); // Sinon, rafraîchit l'affichage pour la nouvelle partie à régler.
}
}
}
break; // Fin du STATE_SET_DATETIME
}
} // Fin de la fonction loop()
// --- AUTRES FONCTIONS D'AFFICHAGE ET UTILITAIRES ---
// Affiche l'heure, la date et le statut de l'alarme sur le premier écran LCD (LCD1).
void displayTimeAndDateOnLCD1(DateTime now) {
char timeBuffer[9]; // Buffer pour "HH:MM:SS\0".
sprintf(timeBuffer, "%02d:%02d:%02d", now.hour(), now.minute(), now.second()); // Formate l'heure.
int timeLength = strlen(timeBuffer); // Longueur de la chaîne.
int startColTime = (20 - timeLength) / 2; // Calcule la position pour centrer le texte.
lcd1.setCursor(startColTime, 0); // Va à la ligne 0 (première ligne).
lcd1.print(timeBuffer); // Affiche l'heure.
for (int i = startColTime + timeLength; i < 20; i++) lcd1.print(' '); // Efface le reste de la ligne.
char dayOfWeekBuffer[13]; // Buffer pour le nom du jour de la semaine.
sprintf(dayOfWeekBuffer, "%s", daysOfTheWeek[now.dayOfTheWeek()]); // Récupère le nom du jour.
int dayOfWeekLength = strlen(dayOfWeekBuffer);
int startColDayOfWeek = (20 - dayOfWeekLength) / 2;
lcd1.setCursor(startColDayOfWeek, 1); // Va à la ligne 1.
lcd1.print(dayOfWeekBuffer);
for (int i = startColDayOfWeek + dayOfWeekLength; i < 20; i++) lcd1.print(' ');
char dateOnlyBuffer[17]; // Buffer pour la date (ex: "JJ Mois AAAA").
// now.month() - 1 car les index des mois commencent à 0 dans notre tableau 'monthsOfTheYear'.
sprintf(dateOnlyBuffer, "%02d %s %04d", now.day(), monthsOfTheYear[now.month() - 1], now.year());
int dateOnlyLength = strlen(dateOnlyBuffer);
int startColDateOnly = (20 - dateOnlyLength) / 2;
lcd1.setCursor(startColDateOnly, 2); // Va à la ligne 2.
lcd1.print(dateOnlyBuffer);
for (int i = startColDateOnly + dateOnlyLength; i < 20; i++) lcd1.print(' ');
// Affiche le statut de l'alarme sur la ligne 3.
if (alarmEnabled) { // Si l'alarme est activée.
char alarmTimeOnlyBuffer[6];
sprintf(alarmTimeOnlyBuffer, "%02d:%02d", alarmHour, alarmMinute); // Formate l'heure de l'alarme.
int totalDisplayLength = 15; // Longueur totale de l'affichage incluant les cloches et les espaces.
int startColAlarmDisplay = (20 - totalDisplayLength) / 2; // Calcule la position pour centrer.
lcd1.setCursor(startColAlarmDisplay, 3); // Va à la ligne 3.
lcd1.write(0); // Affiche le caractère personnalisé "cloche" (que nous avons créé à l'emplacement 0).
lcd1.print(" "); // Espaces.
lcd1.print(alarmTimeOnlyBuffer); // Affiche l'heure de l'alarme.
lcd1.print(" "); // Espaces.
lcd1.write(0); // Affiche une deuxième cloche.
for (int i = startColAlarmDisplay + totalDisplayLength; i < 20; i++) lcd1.print(' '); // Efface le reste de la ligne.
} else { // Si l'alarme est désactivée.
char line3Buffer[21];
const char* disabledMessage = "Reveil desactive";
int messageLength = strlen(disabledMessage);
int startColMessage = (20 - messageLength) / 2;
for (int i = 0; i < 20; i++) {
line3Buffer[i] = ' ';
}
line3Buffer[20] = '\0';
memcpy(&line3Buffer[startColMessage], disabledMessage, messageLength); // Copie le message au centre.
lcd1.setCursor(0, 3);
lcd1.print(line3Buffer); // Affiche "Reveil desactive".
}
}
// Gère l'incrémentation ou la décrémentation d'une valeur (heure, minute, année, etc.).
// Prend en paramètres :
// - 'up' : vrai si l'encodeur tourne vers le haut.
// - 'down' : vrai si l'encodeur tourne vers le bas.
// - '*value' : un pointeur vers la variable à modifier (permet de modifier la variable originale).
// - 'max_value' : la valeur maximale que 'value' peut atteindre avant de revenir à 0.
void handleEncoderChange(bool up, bool down, int* value, int max_value) {
if (up) {
(*value)++; // Incrémente la valeur pointée par 'value'.
if (*value >= max_value) { // Si la valeur dépasse le maximum, elle revient à 0.
*value = 0;
}
} else if (down) {
(*value)--; // Décrémente la valeur.
if (*value < 0) { // Si la valeur est inférieure à 0, elle revient à (max_value - 1).
*value = max_value - 1;
}
}
}