/*
Description du Projet : Menu Interactif Arduino LCD avec Encodeur Rotatif
Ce projet vise à créer une interface utilisateur interactive et intuitive pour vos projets Arduino.
Il s'agit d'un système de menu multifonction qui permet de naviguer à travers différentes options et de déclencher des
actions spécifiques à l'aide d'un écran LCD 20x4 I2C et d'un encodeur rotatif avec bouton poussoir.
Composants Clés :
Arduino Uno : Le microcontrôleur principal qui exécute le code du menu et gère les interactions avec les composants.
Écran LCD 20x4 I2C : Affiche les options du menu et les informations contextuelles.
L'interface I2C simplifie le câblage à seulement deux fils de données (SDA et SCL).
Encodeur Rotatif avec Bouton Poussoir : Permet une navigation facile dans le menu.
La rotation de l'encodeur déplace le curseur de sélection vers le haut ou vers le bas,
tandis que l'appui sur le bouton valide une option ou entre dans un sous-menu.
Fonctionnalités :
Navigation Intuitive : L'encodeur rotatif offre une expérience de navigation fluide pour parcourir les options du menu.
Affichage Dynamique : L'écran LCD 20x4 permet d'afficher plusieurs options de menu à la fois,
avec une gestion du défilement automatique si le nombre d'options dépasse la capacité de l'écran.
Gestion des Sous-menus : La structure du code supporte plusieurs niveaux de menus,
permettant une organisation logique des fonctionnalités (par exemple, Menu Principal -> Sous-menu 1 -> Option 1.1).
Exécution d'Actions : Chaque option du menu peut être associée à une fonction spécifique,
permettant de contrôler des capteurs, des actionneurs, d'afficher des données, ou de modifier des paramètres de votre projet.
Robustesse : L'utilisation de la librairie Bounce2 assure une détection fiable des appuis sur le bouton de l'encodeur
en éliminant les problèmes de rebonds.
*/
#include <Wire.h> // Requis pour l'I2C
#include <LiquidCrystal_I2C.h> // Pour l'écran LCD I2C
#include <Encoder.h> // Pour l'encodeur rotatif
#include <Bounce2.h> // Pour gérer les rebonds du bouton de l'encodeur
// --- Définition des Pins ---
#define ENCODER_CLK_PIN 2 // Pin CLK de l'encodeur (vers D2 de l'Arduino Uno)
#define ENCODER_DT_PIN 3 // Pin DT de l'encodeur (vers D3 de l'Arduino Uno)
#define ENCODER_SW_PIN 4 // Pin du bouton poussoir de l'encodeur (vers D4 de l'Arduino Uno)
// --- Configuration de l'écran LCD ---
// Adresse I2C de ton LCD (peut varier, essaie 0x27 ou 0x3F).
// Sur Wokwi, clique sur le LCD après l'avoir ajouté pour voir son adresse exacte.
LiquidCrystal_I2C lcd(0x27, 20, 4); // 20 colonnes, 4 lignes
// --- Configuration de l'encodeur et du bouton ---
// Les pins sont spécifiés comme (pin B, pin A) pour la librairie Encoder
Encoder encoder(ENCODER_DT_PIN, ENCODER_CLK_PIN);
Bounce2::Button encoderButton = Bounce2::Button(); // Crée une instance Bounce2
// --- Structure de Données du Menu ---
// Noms des options du menu principal
const char* mainMenuOptions[] = {
"Sous-menu 1",
"Option Principale 2",
"Option Principale 3",
"Plus d'options..."
};
const int mainMenuSize = sizeof(mainMenuOptions) / sizeof(mainMenuOptions[0]);
// Noms des options du sous-menu 1
const char* subMenu1Options[] = {
"Option 1.1",
"Option 1.2",
"Option 1.3",
"Retour" // Toujours avoir une option de retour pour les sous-menus
};
const int subMenu1Size = sizeof(subMenu1Options) / sizeof(subMenu1Options[0]);
// --- Variables d'état du Menu ---
enum MenuState {
MAIN_MENU,
SUB_MENU_1
// Ajoute d'autres états pour tes sous-menus ici (ex: SUB_MENU_2, SETTINGS_MENU)
};
MenuState currentMenuState = MAIN_MENU; // État actuel du menu au démarrage
long oldEncoderPosition = 0; // Position précédente de l'encodeur
int selectedOption = 0; // Option sélectionnée dans le menu actuel (index)
int firstVisibleLine = 0; // Première ligne affichée sur l'écran LCD pour le défilement
// --- Fonctions de prototype (déclarations forward) ---
// Ceci permet aux fonctions de s'appeler mutuellement sans problème de compilation
void displayMenu();
void handleEncoderRotation();
void handleButtonClick();
void enterMenu(MenuState targetMenu);
void goBack();
void executeAction(int menuState, int optionIndex);
void setup() {
// Serial.begin(9600); // Initialise la communication série pour le débogage
// Serial.println("Démarrage du menu Arduino...");
// Initialisation de l'écran LCD
lcd.init(); // Initialise l'objet LCD
lcd.backlight(); // Active le rétroéclairage
lcd.clear(); // Efface l'écran
// Initialisation du bouton de l'encodeur
pinMode(ENCODER_SW_PIN, INPUT_PULLUP); // Utilise la résistance de pull-up interne
encoderButton.attach(ENCODER_SW_PIN); // Attache le bouton à la librairie Bounce2
encoderButton.interval(25); // Intervalle de détection des rebonds en ms
encoderButton.setPressedState(LOW); // Le bouton est LOW quand pressé (avec INPUT_PULLUP)
// Afficher le menu initial
displayMenu();
}
void loop() {
// Met à jour l'état du bouton (gestion des rebonds)
encoderButton.update();
// Gère la rotation de l'encodeur
handleEncoderRotation();
// Gère le clic du bouton si un appui est détecté
if (encoderButton.pressed()) { // Utilise .pressed() pour détecter un appui court
handleButtonClick();
}
// Petit délai pour éviter de surcharger le processeur
delay(10);
}
/**
* Affiche le menu actuel sur l'écran LCD.
* Gère le défilement si le menu a plus d'options que de lignes.
*/
void displayMenu() {
lcd.clear();
lcd.setCursor(0, 0); // Positionne le curseur en haut à gauche
const char** currentMenuOptions; // Pointeur vers le tableau d'options du menu actuel
int currentMenuSize; // Taille du menu actuel
// Détermine quel menu est actif et son contenu
switch (currentMenuState) {
case MAIN_MENU:
currentMenuOptions = mainMenuOptions;
currentMenuSize = mainMenuSize;
lcd.print("Menu Principal:");
break;
case SUB_MENU_1:
currentMenuOptions = subMenu1Options;
currentMenuSize = subMenu1Size;
lcd.print("Sous-menu 1:");
break;
// Ajoute d'autres cas pour tes sous-menus ici
// case SUB_MENU_X:
// currentMenuOptions = subMenuXOptions;
// currentMenuSize = subMenuXSize;
// lcd.print("Titre du Sous-menu X:");
// break;
}
// --- Gestion du défilement des options ---
// S'assure que l'option sélectionnée est visible à l'écran
if (selectedOption < firstVisibleLine) {
firstVisibleLine = selectedOption;
}
// L'écran 20x4 a 4 lignes. La première ligne est pour le titre,
// il reste 3 lignes pour les options de menu (lignes 1, 2, 3).
// Si l'option sélectionnée sort des 3 lignes visibles, on ajuste firstVisibleLine.
if (selectedOption >= firstVisibleLine + 3) {
firstVisibleLine = selectedOption - 2; // Décale pour que l'option sélectionnée soit sur la 3ème ligne (index 2)
}
// S'assurer que firstVisibleLine ne dépasse pas la fin du menu
if (firstVisibleLine > currentMenuSize - 3) { // 3 options affichables max
firstVisibleLine = max(0, currentMenuSize - 3); // max(0, ...) pour ne pas aller en dessous de 0
}
// Affiche les options visibles du menu
// On parcourt seulement les options qui doivent être affichées sur les 3 lignes restantes de l'écran
for (int i = 0; i < min(currentMenuSize - firstVisibleLine, 3); i++) {
lcd.setCursor(0, i + 1); // Démarre à la ligne 1 (la ligne 0 est pour le titre)
if (i + firstVisibleLine == selectedOption) {
lcd.print(">"); // Curseur pour l'option sélectionnée
} else {
lcd.print(" ");
}
// Affiche le texte de l'option
lcd.print(currentMenuOptions[i + firstVisibleLine]);
}
}
/**
* Gère la rotation de l'encodeur pour naviguer dans les options du menu.
*/
void handleEncoderRotation() {
// Lit la nouvelle position de l'encodeur.
// La plupart des encodeurs produisent 4 "ticks" (impulsions) par "clic" physique.
// On divise par 4 pour avoir un pas par clic.
long newEncoderPosition = encoder.read() / 4;
if (newEncoderPosition != oldEncoderPosition) { // Si la position a changé
int direction = (newEncoderPosition > oldEncoderPosition) ? 1 : -1; // 1 pour avant, -1 pour arrière
int currentMenuSize; // Taille du menu actuel pour gérer les limites
switch (currentMenuState) {
case MAIN_MENU:
currentMenuSize = mainMenuSize;
break;
case SUB_MENU_1:
currentMenuSize = subMenu1Size;
break;
// Ajoute d'autres cas pour tes sous-menus
}
selectedOption += direction; // Met à jour l'option sélectionnée
// Limite la sélection aux bornes du menu (défilement en boucle)
if (selectedOption < 0) {
selectedOption = currentMenuSize - 1; // Passe de la première à la dernière
} else if (selectedOption >= currentMenuSize) {
selectedOption = 0; // Passe de la dernière à la première
}
Serial.print("Option sélectionnée: ");
Serial.println(selectedOption); // Affiche dans le moniteur série pour débogage
oldEncoderPosition = newEncoderPosition; // Met à jour l'ancienne position
displayMenu(); // Rafraîchit l'affichage du menu
}
}
/**
* Gère le clic sur le bouton de l'encodeur.
* Détermine l'action à exécuter (entrer dans un sous-menu, revenir, exécuter une fonction).
*/
void handleButtonClick() {
Serial.print("Bouton pressé - Option: ");
Serial.println(selectedOption); // Affiche dans le moniteur série
switch (currentMenuState) {
case MAIN_MENU:
if (selectedOption == 0) { // Si "Sous-menu 1" (index 0) est sélectionné
enterMenu(SUB_MENU_1); // Entre dans le sous-menu 1
} else {
// Pour les autres options du menu principal, exécute l'action correspondante
executeAction(MAIN_MENU, selectedOption);
}
break;
case SUB_MENU_1:
// Compare la chaîne de caractères pour l'option "Retour"
if (strcmp(subMenu1Options[selectedOption], "Retour") == 0) {
goBack(); // Revient au menu précédent
} else {
// Pour les autres options du sous-menu 1, exécute l'action correspondante
executeAction(SUB_MENU_1, selectedOption);
}
break;
// Ajoute d'autres cas pour tes sous-menus
}
}
/**
* Entre dans un sous-menu spécifié.
* Réinitialise l'option sélectionnée et le défilement.
*/
void enterMenu(MenuState targetMenu) {
currentMenuState = targetMenu; // Met à jour l'état du menu
selectedOption = 0; // Réinitialise l'option sélectionnée à la première
firstVisibleLine = 0; // Réinitialise le défilement
displayMenu(); // Affiche le nouveau menu
Serial.print("Entré dans le menu: ");
Serial.println(targetMenu);
}
/**
* Revient au menu précédent.
* Dans cet exemple simple, revient toujours au menu principal depuis un sous-menu.
* Pour une arborescence plus complexe, une pile (stack) d'états de menu serait préférable.
*/
void goBack() {
// Ici, tu dois définir comment tu "remontes" dans l'arborescence des menus.
// Pour cet exemple simple, nous revenons toujours au menu principal.
if (currentMenuState == SUB_MENU_1) {
currentMenuState = MAIN_MENU;
}
// ... ajouter d'autres conditions pour les sous-menus plus profonds
// if (currentMenuState == SUB_MENU_X) { currentMenuState = MAIN_MENU ou un autre menu parent; }
selectedOption = 0; // Réinitialise l'option sélectionnée
firstVisibleLine = 0; // Réinitialise le défilement
displayMenu(); // Affiche le menu parent
Serial.println("Retour au menu précédent.");
}
/**
* Exécute une action en fonction de l'état du menu et de l'option sélectionnée.
* C'est ici que tu mettras le code spécifique à chaque fonctionnalité réelle de ton projet.
*/
void executeAction(int menuState, int optionIndex) {
lcd.clear(); // Efface l'écran pour afficher le message de l'action
lcd.setCursor(0, 0);
// Affiche un message temporaire pour simuler l'exécution de l'action
switch (menuState) {
case MAIN_MENU:
lcd.print("Action du Menu P:");
lcd.setCursor(0, 1);
lcd.print(mainMenuOptions[optionIndex]);
break;
case SUB_MENU_1:
lcd.print("Action du Sous-M 1:");
lcd.setCursor(0, 1);
lcd.print(subMenu1Options[optionIndex]);
break;
// Ajoute d'autres cas pour les actions des autres sous-menus
}
lcd.setCursor(0, 3);
lcd.print("Execution...");
Serial.print("Exécution de l'action pour: ");
switch (menuState) {
case MAIN_MENU: Serial.println(mainMenuOptions[optionIndex]); break;
case SUB_MENU_1: Serial.println(subMenu1Options[optionIndex]); break;
}
// Simule une action qui prend du temps (remplace ceci par ton vrai code)
delay(2000); // Attend 2 secondes
displayMenu(); // Retourne à l'affichage du menu après l'action
}