Tenez j'ai retrouvé le code - C'est assez commenté - ça devrait se lire sans trop de soucis.
L'idée de base c'est de définir une structure d'un élément de menu (un choix possible) qui contient un libellé, une fonction a appeler, et une valeur à passer en paramètre à la fonction lors de l'appel (ce qui permet d'avoir plusieurs menus appelant la même fonction mais ayant un comportement différent en fonction de la valeur reçue).
L'autre point important de cette approche c'est qu'elle était non bloquante, la loop() peut continuer à tourner en attendant que l'utilisateur choisisse quelque chose dans un menu. on peut donc continuer à faire d'autres choses (écouter le port série, faire clignoter des LEDs ou ce que vous voulez du moment que ce n'est pas trop lent sinon on ratera des appuis sur les boutons du menu)
Pour faire simple j'avais utilisé la librairie OneButton pour les boutons. On en a 3, un pour aller à droite, un pour aller à Gauche un pour valider. Ils seront en INPUT_PULLUP donc câblage Pin <--> bouton <--> GND
/* ************************************ */
/* Gestion Menu sur un LCD */
/* ************************************ */
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
// adresse LDC 0x3F 20 caractères, 4 lignes
LiquidCrystal_I2C lcd(0x3F, 20, 4);
// les bouton pour la gestion du choix dans le menu
// On importe une librairie de gestion des boutons (vous pouvez bien sûr gérer cela vous même à la main)
#include <OneButton.h> // https://github.com/mathertel/OneButton
const byte aDroitePin = 5; // bouton pour aller à droite dans les menus
OneButton aDroite_b(aDroitePin, true); // true pour le mettre en INPUT_PULLUP
const byte aGauchePin = 6; // bouton pour aller à gauche dans les menus
OneButton aGauche_b(aGauchePin, true);
const byte validePin = 7;// bouton pour valider une entrée dans les menus
OneButton valide_b(validePin, true);
//========================================
const char separateur = ' ';
const char vide = ' ';
enum etat_t : byte {normal, attenteMenu} etatMenu = normal;
struct menu_t {
const char * etiquette;
byte valeur;
void (* fonction)(byte valeur);
};
struct {
unsigned long chrono;
menu_t *menu;
byte nbChoix;
byte xMenu;
byte yMenu;
byte choix;
byte element;
} menusEnCours;
//========================================
// On pré-déclare les fonctions pour qu'elles
// soient utilisables dans la définition des menus
//========================================
void fonctionMenuA1(byte valeur);
void fonctionMenuA2(byte valeur);
void fonctionMenuB(byte valeur);
//========================================
// On crée nos menus
//========================================
menu_t menusA[] = {
{"Vide", 10, fonctionMenuA1},
{"Transport ?", 30, fonctionMenuA2}
};
const byte nbElementsMenuA = sizeof(menusA) / sizeof( menusA[0]);
menu_t menusB[] = {
{"Bus", 22, fonctionMenuB},
{"Voiture", 33, fonctionMenuB},
{"Avion", 77, fonctionMenuB}
};
const byte nbElementsMenuB = sizeof(menusB) / sizeof( menusB[0]);
//========================================
// On implémente les fonctions callback
//========================================
void fonctionMenuA1(byte valeur)
{
Serial.print(F("Menu A1 - valeur = "));
Serial.println(valeur);
}
void fonctionMenuA2(byte valeur)
{
Serial.print(F("Menu A3 - valeur = "));
Serial.println(valeur);
afficherMenu(0, 1, nbElementsMenuB, 0, menusB);
}
void fonctionMenuB(byte valeur)
{
Serial.print(F("Le prix du Transport est de = "));
Serial.println(valeur);
lcd.setCursor(0, 2);
lcd.print(F("Prix = ")); lcd.print(valeur); lcd.print(F(" euros"));
delay(2000);
}
//========================================
// gestion de l'interaction dans le menu
//========================================
void afficherEnCours(boolean estVisible)
{
// on cherche le début de la chaîne en cours
byte x = menusEnCours.xMenu + menusEnCours.choix; // on démarre avec le nombre d'espaces
for (byte i = 0; i < menusEnCours.choix; i++) x += strlen(menusEnCours.menu[i].etiquette); // on ajoute les longueurs
lcd.setCursor(x, menusEnCours.yMenu); // on s'y positionne
if (estVisible) {
for (byte i = 0; i < strlen(menusEnCours.menu[menusEnCours.choix].etiquette); i++)
lcd.write(vide); // on met le bon nombre de case vide pour cacher cette étiquette de menu
} else {
lcd.print(menusEnCours.menu[menusEnCours.choix].etiquette); // on affiche l'étiquette
}
}
void clignote()
{
static boolean estVisible = true;
const unsigned long demiPeriode = 500;
if (millis() - menusEnCours.chrono >= demiPeriode) {
afficherEnCours(estVisible);
estVisible = !estVisible;
menusEnCours.chrono += demiPeriode;
}
}
//========================================
// la fonction a appeler pour afficher
// un menu sur le LCD
// x et y sont la colonne et ligne du LCD
// nbChoix le nombre de choix dans le menu
// choixDefaut celui qui clignotte
// monMenu est un tableau de menus_t
//========================================
void afficherMenu(byte x, byte y, byte nbChoix, byte choixDefaut, menu_t monMenu[])
{
lcd.setCursor(x, y);
for (byte i = 0; i < nbChoix; i++) {
lcd.print(monMenu[i].etiquette);
if (i != nbChoix - 1) lcd.write(separateur);
}
menusEnCours.chrono = millis();
menusEnCours.menu = monMenu;
menusEnCours.nbChoix = nbChoix;
menusEnCours.xMenu = x;
menusEnCours.yMenu = y;
if (choixDefaut < nbChoix) menusEnCours.choix = choixDefaut;
else menusEnCours.choix = 0;
menusEnCours.element = 0;
etatMenu = attenteMenu;
}
//========================================
// la gestion des boutons du menu
//========================================
void aDroite()
{
afficherEnCours(false); // s'assurer que le texte n'est pas éteint
if (menusEnCours.choix < menusEnCours.nbChoix - 1) menusEnCours.choix++; // passer au suivant si possible
}
void aGauche()
{
afficherEnCours(false); // s'assurer que le texte n'est pas éteint
if (menusEnCours.choix > 0) menusEnCours.choix--; // passer au suivant si possible
}
void valider()
{
afficherEnCours(false); // s'assurer que le texte n'est pas éteint
etatMenu = normal; // on quitte le mode menu
// on déclenche la fonction call back en passant un paramètre
menusEnCours.menu[menusEnCours.choix].fonction(menusEnCours.menu[menusEnCours.choix].valeur);
}
//========================================
void verifierBoutons()
{
// on vérifie si un des boutons est appuyé, si oui ça déclenche automatiquement le call back du bouton
aDroite_b.tick(); // appellera la fonction aDroite() si appui
aGauche_b.tick(); // appellera la fonction aGauche() si appui
valide_b.tick(); // appellera la fonction valider() si appui
}
//========================================
void setup() {
Serial.begin(115200);
lcd.begin();
lcd.backlight();
// on attache les fonctions appelées par les boutons en cas de click
aDroite_b.attachClick(aDroite);
aGauche_b.attachClick(aGauche);
valide_b.attachClick(valider);
lcd.clear();
afficherMenu(0, 0, nbElementsMenuA, 0, menusA);
}
void loop() {
// avec cette approche, si on est en mode sélection de menu la loop() écoute les boutons
// mais ne bloque pas le programme globalement en attente du choix d'un bouton
// on permet ainsi de traiter de manière asynchrone une validation de menu
if (etatMenu == attenteMenu) {
clignote();
verifierBoutons(); // ceci déclenche l'appel des callback si un bouton est appuyé
if (etatMenu == normal) {
// si on arrive ici en étant sorti du mode menu c'est que l'utilsateur a validé son entrée
// et n'a pas déclenché d'autre (sous) menu
// la fonction finale associée au menu a été exécutée
// la structure menusEnCours contient dans <.choix> la selection effectuée
lcd.clear();
lcd.setCursor(0, 0);
lcd.print(F("Action accomplie"));
delay(2000);
// pour notre exemple on relance le menu
lcd.clear();
afficherMenu(0, 0, nbElementsMenuA, 0, menusA);
}
}
// ici on peut faire autre-chose du moment que c'est rapide si on ne veut pas rater des clicks sur les boutons en mode menu
}
voilà en espérant que ça vous donne peut être des idées pour être plus souple!