Skyduino:~#
Articles
arduino, programmation, tutoriel

[Arduino & lcd] Faire un menu sous forme de liste

Bonjour tout le monde !

À de nombreuses reprises l’on m’a demandé mon avis sur comment implémenter un menu sous forme de liste avec une carte Arduino et un petit écran LCD 2×16 caractères.

Le sujet est on ne peut plus intéressant, mais je n’avais pas eu le temps de me pencher dessus jusqu’à présent.
Devant répondre une nouvelle fois à cette question par mail je me suis dit qu’il serait temps de prendre une demi-journée pour faire un article sur le sujet 😉

Vous l’aurez compris, le programme du jour est : « comment implémenter un menu sous forme de liste avec une carte Arduino et un écran LCD ».

Préface

Avant de partir tête baissée dans de l’explication de code et d’algorithme je souhaitai préciser quelques petites choses.

Premièrement, ce que je vais vous présenter aujourd’hui est un algorithme, une méthode parmi tant d’autres.
Il existe des méthodes bien plus simples (automate fini), ou bien plus compliquées (pattern arbre et parcours).
Pour cet article j’ai choisi une méthode ni trop simple (vous n’auriez pas pu reprendre le code tel quel), ni trop compliqué (il me faudrait plusieurs articles et une vidéo avec un tableau blanc pour expliquer en détail le pattern arbre et l’algo de parcours).
En cherchant sur Google vous trouverez bien d’autre façon de faire, à vous de faire votre choix 😉

Deuxièmement, j’ai choisi de faire un code réutilisable en langage C. Excepté quelques petites notions de C++ tout le code est écrit en C.
Comme je sais que plusieurs personnes m’ont posé le même genre de question pour du code PIC ou ARM (avec un compilateur C uniquement), je me suis dit qu’il serait plus facile de faire tout le code de base en C.
Il est clair que si j’avais du faire un code « propre » j’aurai utilisé des classes C++ et fait un pattern arbre. Mais ce n’est pas mon but pour cet article.

Le hardware

Avant de parler du code, je vais vous présenter le montage de test.

DSCF2987

J’ai volontairement choisi d’utiliser du matériel tout prêt, facile à trouver et (relativement) fiable.
Mon but n’est pas de vous apprendre à câbler un écran LCD ni de faire du debug toute une après-midi 😉

Pour ce tutoriel, il suffira donc de se munir d’une shield LCD (ici de DFRobots) et d’une carte Arduino UNO.

Le résultat escompté

Maintenant que l’on sait sur quoi l’on va travailler, il faut définir ce que l’on souhaite obtenir.
Si on ne sait pas ce que l’on souhaite, on ne risque pas d’y arriver 😉

DSCF2990

J’ai choisi de rester très sobre et très classique pour le menu.
La première ligne sera le titre du menu, la seconde ligne le choix en cours.
La touche gauche permettra de revenir au menu précédent (il y aura un sous-menu pour que ce tutoriel soit un minimum réaliste).
La touche droite ou SELECT permettra de sélectionner un choix du menu, ou d’ouvrir un sous-menu.
Les touches haut / bas permettront de naviguer dans la liste des choix.

DSCF2993

Pour pouvoir montrer le bon fonctionnement du menu, une fois le choix validé un écran affichera le choix en question.
Dans une véritable application, cet écran serait remplacé par l’action à réaliser en fonction du choix.

La théorie et le code

Pour ce tutoriel on va avoir besoin de deux fichiers : tuto_menu_liste.ino et Menu.h.
Le .ino contiendra le code du menu et Menu.h les diverses déclarations liées au menu.
Si l’IDE Arduino n’était pas buggé jusqu’à la moelle on pourrait tout mettre dans un même fichier, mais pour le moment on ne peut pas.

Maintenant réfléchissons à la structure d’un menu.
Un menu est composé :
– d’un titre,
– d’une série de choix (choix pouvant pointer vers un sous-menu),
– d’une fonction à appeler lors de la validation du choix par l’utilisateur.

Traduit en C cela donne la structure suivante :

/* Structure d'un menu */
typedef struct {
  const char* prompt;     // Titre du menu
  const char** items;     // Tableau de choix du menu
  const uint8_t nbItems;  // Nombre de choix possibles
  void (*callbackFnct)(uint8_t menuItemSelected); // Pointeur sur fonction pour gérer le choix de l'utilisateur
} Menu_t;

Avec cette déclaration de type (typedef) on déclare que « Menu_t » est la représentation en mémoire d’un menu, avec : un titre, un tableau de choix, un nombre de choix et une fonction de retour pour la validation.

Je sais, le pointeur sur fonction en fin de structure va faire un scandale parmi les débutants en langage C.
Pour faire simple, c’est un pointeur qui peut pointer vers n’importe quelle fonction de la forme void blabla(byte).

J’aurais aussi pu éviter les chaines de caractères en mémoire RAM en utilisant le mot clef PROGMEM.
Mais si je l’avais fait, j’aurais passer plus de temps à vous expliquer pourquoi utiliser PROGMEM et comment ça marche, que de vous expliquer comment faire un menu 😉

Le résultat final de Menu.h :

#ifndef _MENU_H_
#define _MENU_H_

/* Structure d'un menu */
typedef struct {
  const char* prompt;     // Titre du menu
  const char** items;     // Tableau de choix du menu
  const uint8_t nbItems;  // Nombre de choix possibles
  void (*callbackFnct)(uint8_t menuItemSelected); // Pointeur sur fonction pour gérer le choix de l'utilisateur
} Menu_t;

/* Listes des touches de la shield lcd DFrobots */
typedef enum {
  BP_NONE,   // Aucun bouton appuyé
  BP_SELECT, // Bouton SELECT
  BP_LEFT,   // Bouton gauche
  BP_RIGTH,  // Bouton droite
  BP_UP,     // Bouton haut
  BP_DOWN    // Bouton bas
} Button_t;

#endif /* _MENU_H_ */

Comme sur ce blog on fait les choses bien, on va ajouter un « include guard » à notre .h (la série de #ifndef #define #endif).
Si vous ne savez pas ce qu’est un include guard ce n’est pas grave, dite vous simplement que c’est une bonne habitude à prendre quand on écrit un fichier .h.

On va aussi ajouter une énumération qui nous sera utile plus tard pour la fonction permettant de lire l’état des boutons de la shield LCD de DFRobots.

Maintenant que l’on a notre fichier Menu.h on va passer au code du .ino 😉

/* Inclut la librairie LiquidCrystal pour le LCD */
#include <LiquidCrystal.h>
#include "Menu.h" // Fichier d'entête avec les types pour le menu

On va commencer par inclure les fichiers d’entêtes nécessaires à notre code.
Nous aurons ici besoin de la librairie LiquidCrystal pour l’écran LCD, et de notre fichier Menu.h pour la structure d’un menu.

/* Objet LCD sur les broches utilisées par la shield LCD DFrobots */
static LiquidCrystal lcd(8, 9, 4, 5, 6, 7);

On va ensuite instancier un objet de type LiquidCrystal sur les broches adéquates pour pouvoir afficher par la suite du texte sur notre écran LCD.

/* Menu principal */
static const char* MAIN_MENU_ITEMS[] = {
  "John Doe",
  "Skywodd",
  "Dr Freeman",
  "Nyan Cat",
  "Obiwan Kenobi"
};
static const Menu_t MAIN_MENU = {
  "Votre choix ?",
  MAIN_MENU_ITEMS,
  5,
  &doMainMenuAction
};

/* Sous menu pour Dr Freeman */
static const char* FREEMAN_MENU_ITEMS[] = {
  "Gordon Freeman",
  "Morgan Freeman",
  "Martin Freeman"
};
static const Menu_t FREEMAN_MENU = {
  "Votre choix ?",
  FREEMAN_MENU_ITEMS,
  3,
  &doFreemanMenuAction
};

Pour finir, on va déclarer deux menus : un menu principal et un sous-menu (pour l’exemple).
Ces menus vous permettront de choisir votre personnage préféré.
(Oui je sais, je n’étais pas franchement inspiré …)

On déclare donc deux menus : un menu principal contenant 5 choix qui sera géré par la fonction « doMainMenuAction ».
Et un sous-menu spécial « Dr Freeman », contenant seulement 3 choix et qui sera géré par la fonction « doFreemanMenuAction ».
Les fonctions « doMainMenuAction » et « doFreemanMenuAction » seront appelées par notre code lorsque l’utilisateur validera son choix.

/** Setup */
void setup() {

  /* Configuration du LCD */
  lcd.begin(16, 2);
}

La fonction setup() n’aura dans ce tutoriel pas beaucoup de chose à configurer.
On va uniquement configurer l’écran LCD en mode 2×16 caractères, rien de plus.

/** Programme principal */
void loop() {

  /* Affiche le menu principal */
  displayMenu(MAIN_MENU);

  /* Démo pour montrer la sortie du menu */
  lcd.clear();
  lcd.print(F("Menu ferme"));
  delay(2000);
}

On va ensuite passer à la fonction loop() qui elle aussi ne sera pas bien garnie pour ce tutoriel.

La fonction loop() ne fera que deux choses :
– afficher le menu principal,
– afficher un petit message pour montrer la fermeture du menu (avant de le rouvrir 2 secondes plus tard).

/** Fonction retournant le bouton appuyé (s’il y en a un). */
Button_t readPushButton(void) {

  /* Lecture de l'entrée A0 */
  unsigned int val = analogRead(A0);

  /* Test suivant les fourchettes de valeurs */
  if (val > 1000) return BP_NONE;
  if (val < 50) return BP_RIGTH;  
  if (val < 195) return BP_UP; 
  if (val < 380) return BP_DOWN; 
  if (val < 555) return BP_LEFT; 
  if (val < 790) return BP_SELECT;

  /* Par défaut aucun bouton n'est appuyé */
  return BP_NONE;
}

Je passe rapidement sur cette fonction. C’est la fonction de base pour lire les boutons de la shield LCD de DFRobots.
À modifier en conséquence si vous utilisez une autre shield ou un montage maison.

/** Affiche le choix de l'utilisateur */
void doMainMenuAction(byte selectedMenuItem) {

  /* Cas spécial pour Dr Freeman */
  if(selectedMenuItem == 2) {

    /* Affiche le sous-menu pour Dr Freeman */
    displayMenu(FREEMAN_MENU);

  } else {

    /* Affiche le choix de l'utilisateur */
    displayChoice(selectedMenuItem, MAIN_MENU_ITEMS);
  }
}

/** Affiche le choix de l'utilisateur */
void doFreemanMenuAction(byte selectedMenuItem) {

  /* Affiche le choix de l'utilisateur */
  displayChoice(selectedMenuItem, FREEMAN_MENU_ITEMS);
}

/** Affiche le choix de l'utilisateur */
void displayChoice(byte selectedMenuItem, const char** items) {

  /* Affiche le choix de l'utilisateur */
  lcd.clear();
  lcd.print(F("Z'avez choisi :"));
  lcd.setCursor(0, 1);
  lcd.print(items[selectedMenuItem]);

  /* Attend l'appui sur le bouton gauche ou SELECT */
  byte buttonPressed;
  do {
    buttonPressed = readPushButton();
  } 
  while(buttonPressed != BP_LEFT && buttonPressed != BP_SELECT);
}

Avant d’entrer en détail dans la fonction d’affichage des menus, on va définir de suite les fonctions de gestion des deux menus.

« doMainMenuAction » utilisera la fonction « displayChoice » pour afficher le choix de l’utilisateur.
La fonction « displayChoice » ne fait qu’afficher le choix de l’utilisateur avant d’attendre que celui-ci appui sur la touche gauche ou SELECT pour revenir au menu précédent.

Dans le cas où l’utilisateur choisit le sous-menu « Dr Freeman », la fonction « doMainMenuAction » lancera l’affichage du sous-menu en question et restera bloqué dans ce sous-menu tant que l’utilisateur ne le quittera pas.
Dans l’hypothèse où l’utilisateur valide un choix dans le sous-menu « Dr Freeman » c’est la fonction « doFreemanMenuAction » qui sera appelée et utilisera la fonction « displayChoice » pour afficher le choix de l’utilisateur.

Dans une véritable application, on utiliserait un switch/case pour gérer chaque choix de l’utilisateur, sous-menu inclus.
Ici ce n’est qu’un exemple bateau donc je reste assez simpliste.

Voici un exemple de fonction plus « réaliste » :

/** Gère le choix de l'utilisateur */
void doMainMenuAction(byte selectedMenuItem) {

  /* Gère le choix de l'utilisateur */
  switch(selectedMenuItem) {
  case 0:
    // Code pour le choix 1
    break;

  case 1:
    // Code pour le choix 2
    break;

  case 2:
    // Code pour le choix 3
    break;

    // etc ...
  }
}

Ça devient sérieux là, passons à la fonction d’affichage d’un menu.

/** Affiche le menu passé en argument */
void displayMenu(const Menu_t &menu) {

  /* Variable pour le menu */
  byte selectedMenuItem = 0;   // Choix sélectionné
  byte shouldExitMenu = false; // Devient true quand l'utilisateur veut quitter le menu
  Button_t buttonPressed;      // Contient le bouton appuyé

  /* Tant que l'utilisateur ne veut pas quitter pas le menu */
  while(!shouldExitMenu) {

    /* Affiche le menu */
    lcd.clear();
    lcd.print(menu.prompt);
    lcd.setCursor(0, 1);
    lcd.print(menu.items[selectedMenuItem]);

    /* Attend le relâchement du bouton */
    while(readPushButton() != BP_NONE);

    /* Attend l'appui sur un bouton */
    while((buttonPressed = readPushButton()) == BP_NONE);

    /* Anti rebond pour le bouton */
    delay(30);

    /* Attend le relâchement du bouton */
    while(readPushButton() != BP_NONE);

    /* Gére l'appui sur le bouton */
    switch(buttonPressed) {
    case BP_UP: // Bouton haut = choix précédent

      /* S'il existe un choix précédent */
      if(selectedMenuItem > 0) {

        /* Passe au choix précédent */
        selectedMenuItem--;
      }
      break;

    case BP_DOWN: // Bouton bas = choix suivant

      /* S'il existe un choix suivant */
      if(selectedMenuItem < (menu.nbItems - 1)) {

        /* Passe au choix suivant */
        selectedMenuItem++;
      }
      break;

    case BP_LEFT: // Bouton gauche = sorti du menu
      shouldExitMenu = true;
      break;

    case BP_SELECT: //
    case BP_RIGTH:  // Bouton droit ou SELECT = validation du choix
      menu.callbackFnct(selectedMenuItem);
      break;
    }
  }
}

Le code parait complexe, mais en réalité il est très simple (et largement améliorable / personnalisable).

  /* Variable pour le menu */
  byte selectedMenuItem = 0;   // Choix sélectionné
  byte shouldExitMenu = false; // Devient true quand l'utilisateur veut quitter le menu
  Button_t buttonPressed;      // Contient le bouton appuyé

On commence par déclarer un certain nombre de variables qui nous seront utiles par la suite.
Comme aucune de ces variables n’est statique, on peut sans problème appeler un sous-menu depuis un menu parent sans que celui-ci ne soit affecté par le sous-menu.

  /* Tant que l'utilisateur ne veut pas quitter pas le menu */
  while(!shouldExitMenu) {
    // ...
  }

Ensuite, toute la magie se trouve encapsuler dans une boucle infinie qui peut être stoppée en plaçant la variable « shouldExitMenu » à true dans la suite du code.
C’est cette boucle et cette variable « shouldExitMenu » qui permettra de quitter le sous-menu lorsque l’on appuiera sur la touche gauche.

    /* Affiche le menu */
    lcd.clear();
    lcd.print(menu.prompt);
    lcd.setCursor(0, 1);
    lcd.print(menu.items[selectedMenuItem]);

La première véritable action du code sera d’afficher le titre du menu et le choix courant.
Par défaut le premier choix affiché sera le choix n°1 (indice 0 dans le tableau de choix).
Par la suite le choix affiché sera lié à la valeur présente dans « selectedMenuItem ».

    /* Attend le relâchement du bouton */
    while(readPushButton() != BP_NONE);

    /* Attend l'appui sur un bouton */
    while((buttonPressed = readPushButton()) == BP_NONE);

    /* Anti rebond pour le bouton */
    delay(30);

    /* Attend le relâchement du bouton */
    while(readPushButton() != BP_NONE);

Une fois le menu affiché il va bien falloir laisser l’utilisateur faire son choix.
Les 4 lignes de code si dessus ont pour but de lire l’état des boutons, tout en évitant qu’un appui prolongé sur un bouton ne fasse partir l’utilisateur dans une dizaine de sous-menus en une fraction de seconde.

Pour éviter ce genre de désagrément (que j’ai pu constater sur certains projets disponibles sur le web) l’algorithme est très simple :
– attendre que le bouton soit relâché,
– attendre que le bouton soit appuyé,
– garder en mémoire le bouton appuyé,
– attendre de nouveau que le bouton soit relâché.

Idéalement avant la dernière attente, il convient d’ajouter un petit délai entre 20ms et 30ms pour éviter les « rebonds » des boutons.
En suivant cette logique, on garantit qu’un seul appui sera traité à chaque appui sur un bouton.

    /* Gére l'appui sur le bouton */
    switch(buttonPressed) {
    case BP_UP: // Bouton haut = choix précédent
      // ...
      break;

    case BP_DOWN: // Bouton bas = choix suivant
      // ...
      break;

    case BP_LEFT: // Bouton gauche = sorti du menu
       // ...
      break;

    case BP_SELECT: //
    case BP_RIGTH:  // Bouton droit ou SELECT = validation du choix
      // ...
      break;
    }

Vient ensuite la gestion de l’appui sur ledit bouton.
Pour ce faire, le moyen le plus rapide et le plus efficace est d’utiliser un switch/case.

    case BP_UP: // Bouton haut = choix précédent

      /* Si il existe un choix précédent */
      if(selectedMenuItem > 0) {

        /* Passe au choix précédent */
        selectedMenuItem--;
      }
      break;

Pour chaque appui sur le bouton haut on décrémentera la valeur de « selectedMenuItem ».
On prendra quand même le temps d’éviter de partir dans des valeurs négatives.
Ce serait quand même dommage de voir le programme planter une fois arrivé à -1 😉

    case BP_DOWN: // Bouton bas = choix suivant

      /* Si il existe un choix suivant */
      if(selectedMenuItem < (menu.nbItems - 1)) {

        /* Passe au choix suivant */
        selectedMenuItem++;
      }
      break;

Pour chaque appui sur le bouton bas on incrémentera la valeur de « selectedMenuItem ».
Pareil que pour le bouton haut, on évitera de dépasser le nombre maximum de choix possibles.

    case BP_LEFT: // Bouton gauche = sorti du menu
      shouldExitMenu = true;
      break;

Le bouton gauche permet de sortir du menu, pour ce faire il nous suffit de placer « shouldExitMenu » à true.
La boucle while() en début de code s’arrêtera en sortant du switch/case et on quittera ainsi naturellement la fonction « displayMenu ».

    case BP_SELECT: //
    case BP_RIGTH:  // Bouton droit ou SELECT = validation du choix
      menu.callbackFnct(selectedMenuItem);
      break;

Le bouton droit et le bouton SELECT sont gérés de la même façon.
Quand on appuiera sur un de ces boutons, la fonction de gestion renseignée dans la structure du menu sera appelée avec en argument le choix de l’utilisateur.

Il est possible que certains d’entre vous aient besoin de quitter le menu une fois l’action choisie réalisée. Dans ce cas il suffit de rajouter la ligne : « shouldExitMenu = true; » juste avant le break.

Comme vous pouvez le voir, une fois décris point par point le code n’est pas si compliqué que ça.
De plus il est générique, vous pouvez copier/coller cette fonction et le fichier Menu.h dans vos projets sans avoir besoin de modifier quoi que ce soit.

Le code final

Au final l’on obtient le code suivant :

/* Inclut la librairie LiquidCrystal pour le LCD */
#include <LiquidCrystal.h>
#include "Menu.h" // Fichier d'entête avec les types pour le menu

/* Objet LCD sur les broches utilisées par la shield LCD DFrobots */
static LiquidCrystal lcd(8, 9, 4, 5, 6, 7);

/* Menu principal */
static const char* MAIN_MENU_ITEMS[] = {
  "John Doe",
  "Skywodd",
  "Dr Freeman",
  "Nyan Cat",
  "Obiwan Kenobi"
};
static const Menu_t MAIN_MENU = {
  "Votre choix ?",
  MAIN_MENU_ITEMS,
  5,
  &doMainMenuAction
};

/* Sous menu pour Dr Freeman */
static const char* FREEMAN_MENU_ITEMS[] = {
  "Gordon Freeman",
  "Morgan Freeman",
  "Martin Freeman"
};
static const Menu_t FREEMAN_MENU = {
  "Votre choix ?",
  FREEMAN_MENU_ITEMS,
  3,
  &doFreemanMenuAction
};

/** Setup */
void setup() {

  /* Configuration du LCD */
  lcd.begin(16, 2);
}

/** Programme principal */
void loop() {

  /* Affiche le menu principal */
  displayMenu(MAIN_MENU);

  /* Démo pour montrer la sortie du menu */
  lcd.clear();
  lcd.print(F("Menu ferme"));
  delay(2000);
}

/** Fonction retournant le bouton appuyé (s’il y en a un). */
Button_t readPushButton(void) {

  /* Lecture de l'entrée A0 */
  unsigned int val = analogRead(A0);

  /* Test suivant les fourchettes de valeurs */
  if (val > 1000) return BP_NONE;
  if (val < 50) return BP_RIGTH;  
  if (val < 195) return BP_UP; 
  if (val < 380) return BP_DOWN; 
  if (val < 555) return BP_LEFT; 
  if (val < 790) return BP_SELECT;

  /* Par défaut aucun bouton n'est appuyé */
  return BP_NONE;
}

/** Affiche le menu passé en argument */
void displayMenu(const Menu_t &menu) {

  /* Variable pour le menu */
  byte selectedMenuItem = 0;   // Choix selectionné
  byte shouldExitMenu = false; // Devient true quand l'utilisateur veut quitter le menu
  Button_t buttonPressed;      // Contient le bouton appuyé

  /* Tant que l'utilisateur ne veut pas quitter pas le menu */
  while(!shouldExitMenu) {

    /* Affiche le menu */
    lcd.clear();
    lcd.print(menu.prompt);
    lcd.setCursor(0, 1);
    lcd.print(menu.items[selectedMenuItem]);

    /* Attend le relâchement du bouton */
    while(readPushButton() != BP_NONE);

    /* Attend l'appui sur un bouton */
    while((buttonPressed = readPushButton()) == BP_NONE);

    /* Anti rebond pour le bouton */
    delay(30);

    /* Attend le relâchement du bouton */
    while(readPushButton() != BP_NONE);

    /* Gére l'appui sur le bouton */
    switch(buttonPressed) {
    case BP_UP: // Bouton haut = choix précédent

      /* Si il existe un choix précédent */
      if(selectedMenuItem > 0) {

        /* Passe au choix précédent */
        selectedMenuItem--;
      }
      break;

    case BP_DOWN: // Bouton bas = choix suivant

      /* Si il existe un choix suivant */
      if(selectedMenuItem < (menu.nbItems - 1)) {

        /* Passe au choix suivant */
        selectedMenuItem++;
      }
      break;

    case BP_LEFT: // Bouton gauche = sorti du menu
      shouldExitMenu = true;
      break;

    case BP_SELECT: //
    case BP_RIGTH:  // Bouton droit ou SELECT = validation du choix
      menu.callbackFnct(selectedMenuItem);
      break;
    }
  }
}

/** Affiche le choix de l'utilisateur */
void doMainMenuAction(byte selectedMenuItem) {

  /* Cas spécial pour Dr Freeman */
  if(selectedMenuItem == 2) {

    /* Affiche le sous-menu pour Dr Freeman */
    displayMenu(FREEMAN_MENU);

  } else {

    /* Affiche le choix de l'utilisateur */
    displayChoice(selectedMenuItem, MAIN_MENU_ITEMS);
  }
}

/** Affiche le choix de l'utilisateur */
void doFreemanMenuAction(byte selectedMenuItem) {

  /* Affiche le choix de l'utilisateur */
  displayChoice(selectedMenuItem, FREEMAN_MENU_ITEMS);
}

/** Affiche le choix de l'utilisateur */
void displayChoice(byte selectedMenuItem, const char** items) {

  /* Affiche le choix de l'utilisateur */
  lcd.clear();
  lcd.print(F("Z'avez choisi :"));
  lcd.setCursor(0, 1);
  lcd.print(items[selectedMenuItem]);

  /* Attend l'appui sur le bouton gauche ou SELECT */
  byte buttonPressed;
  do {
    buttonPressed = readPushButton();
  } 
  while(buttonPressed != BP_LEFT && buttonPressed != BP_SELECT);
}

Bonne fin de WE à toutes et à tous !

Discussion

42 réflexions sur “[Arduino & lcd] Faire un menu sous forme de liste

  1. super comme toujours! un grand merci

    Publié par laum | 6 juillet 2014, 15 h 46 min
  2. Très intéressante ! Merci 🙂

    Publié par Adrien | 6 juillet 2014, 16 h 48 min
  3. Merci pour ton article je vais etudier ca de suite…

    Tu es un as de la prog

    A bientot

    Question tu es de quelle endroit en france ? Moi a cote de clermont fd… 63

    Publié par stephane roussel | 6 juillet 2014, 22 h 41 min
  4. Bonjour,
    Très bon taff comme d’habitude.
    il y a une petite erreur de casse.
    Tu déclare LCD et ensuite tu utiliser lcd.***
    ce qui fait une erreur de compilation avec l’IDE Arduino

    Publié par lolive | 9 juillet 2014, 16 h 09 min
    • Hop, corrigé, merci d’avoir prévenu 😉

      (pour les curieux : je test tout mes codes avant publication. Ici c’est le correcteur ortho que j’utilise qui c’est mis en tête de mettre LCD en majuscule pour cette ligne … allez savoir pourquoi)

      Publié par Skywodd | 10 juillet 2014, 9 h 28 min
      • Merci le correcteur.
        Je te suis depuis très longtemps avec ton test de la stml32 discovery.
        A l’heure actuelle j’implemente un RTOS (Trampoline) sur l’arduino.
        Et cela fonctionne très bien 😀

        Publié par lolive | 10 juillet 2014, 9 h 31 min
  5. chez moi ca compile pas….

    Publié par borel | 29 décembre 2014, 12 h 17 min
  6. Bonjour,
    Juste une question:
    Pour rajouter un sous menu à un sous menu, comment fait-on?
    Merci de me répondre par mail.

    Publié par H | 18 février 2015, 14 h 20 min
    • Suffit d’ajouter un niveau en plus avec une fonction de gestion.

      Exemple :

      /* Menu principal */
      static const char* MAIN_MENU_ITEMS[] = {
        "Piou piou",
        "Plop",
        "SOUS MENU",
        "Lolcat",
        "RTFM"
      };
      static const Menu_t MAIN_MENU = {
        "Votre choix ?",
        MAIN_MENU_ITEMS,
        5,
        &doMainMenuAction
      };
      
      /* Sous menu principal */
      static const char* SUBMAIN_MENU_ITEMS[] = {
        "John Doe",
        "Skywodd",
        "Dr Freeman",
        "Nyan Cat",
        "Obiwan Kenobi"
      };
      static const Menu_t SUBMAIN_MENU = {
        "Votre choix ?",
        SUBMAIN_MENU_ITEMS,
        5,
        &doSubMainMenuAction
      };
       
      /* Sous menu pour Dr Freeman */
      static const char* FREEMAN_MENU_ITEMS[] = {
        "Gordon Freeman",
        "Morgan Freeman",
        "Martin Freeman"
      };
      static const Menu_t FREEMAN_MENU = {
        "Votre choix ?",
        FREEMAN_MENU_ITEMS,
        3,
        &doFreemanMenuAction
      };
      
      // (...)
      
      /** Affiche le choix de l'utilisateur */
      void doMainMenuAction(byte selectedMenuItem) {
       
        /* Cas spécial pour le sous menu #2 */
        if(selectedMenuItem == 2) {
       
          /* Affiche le sous-menu d'exemple #2 */
          displayMenu(SUBMAIN_MENU );
       
        } else {
       
          /* Affiche le choix de l'utilisateur */
          displayChoice(selectedMenuItem, MAIN_MENU_ITEMS);
        }
      }
      
      /** Affiche le choix de l'utilisateur */
      void doSubMainMenuAction(byte selectedMenuItem) {
       
        /* Cas spécial pour Dr Freeman */
        if(selectedMenuItem == 2) {
       
          /* Affiche le sous-menu pour Dr Freeman */
          displayMenu(FREEMAN_MENU);
       
        } else {
       
          /* Affiche le choix de l'utilisateur */
          displayChoice(selectedMenuItem, MAIN_MENU_ITEMS);
        }
      }
      
      /** Affiche le choix de l'utilisateur */
      void doFreemanMenuAction(byte selectedMenuItem) {
       
        /* Affiche le choix de l'utilisateur */
        displayChoice(selectedMenuItem, FREEMAN_MENU_ITEMS);
      }
      

      Publié par Skywodd | 18 février 2015, 17 h 58 min
      • bonjour, merci d’avoir publié ce code, cela semble limpide maintenant pour moi.
        J’ai un petit souci: j’ai créé un onglet .h afin d’y mettre le premier bout de code, copié le programme dans le .ino et à l’essai de compilation j’ai ce message: ‘doMainMenuAction’ was not declared in this scope.
        Me manque t il une librairie?

        cordialement, Arnaud

        Publié par arnaud | 2 octobre 2015, 11 h 44 min
      • Le fichier s’appelle Menu.h (avec la majuscule) ?
        D’après l’erreur il semblerait que le compilateur ne trouve pas le fichier .h.

        Publié par Skywodd | 12 octobre 2015, 11 h 12 min
  7. Bonjour et merci d’avoir répondu à ma question.
    J’ai fait plusieurs essais avec : Menu.h , MENU.h et menu.h et rien ne se passe, aucune différence.
    J’avoue être perplexe.

    Publié par arnaud | 12 octobre 2015, 12 h 07 min
    • Une petite capture d’écran de l’erreur et de l’ide (avec les différents onglets), c’est possible ?
      A mon avis c’est juste une erreur de #include, ce sera vite résolu si c’est la cas.

      Publié par Skywodd | 12 octobre 2015, 12 h 44 min
  8. Merci pour ce code j’ ai un problème de compilation avec les erreurs suivante :

    This report would have more information with
    « Show verbose output during compilation »
    enabled in File > Preferences.
    Arduino: 1.0.6 (Windows 7), Board: « Arduino Uno »
    Teste_menu:7: error: expected ‘,’ or ‘…’ before ‘&’ token
    Teste_menu:7: error: ISO C++ forbids declaration of ‘Menu_t’ with no type
    Teste_menu:16: error: expected `}’ before ‘;’ token
    Teste_menu:16: error: cannot convert ‘const char**’ to ‘const char*’ in initialization
    Teste_menu:16: error: uninitialized const member ‘Menu_t::nbItems’
    Teste_menu:17: error: expected unqualified-id before numeric constant
    Teste_menu:19: error: expected declaration before ‘}’ token

    pouvez vous m’ apporter votre aide

    merci par avance

    Publié par Vever 51 | 1 novembre 2015, 21 h 40 min
    • La structure Menu_t n’ai pas reconnu. Causes possibles : pas de fichier Menu.h, include manquant, erreur de copier/coller de la déclaration de la structutre.

      Publié par Skywodd | 11 novembre 2015, 18 h 20 min
  9. Bonjour skywodd, votre topic est parfait pour mon projet, néanmoins j’ai une erreur également avec le menu :

    C:\Users\juju\AppData\Local\Temp\arduino_bcb36d786c28b6123d4ee68919c15263\sketch_dec11a.ino:3:67: fatal error: Menu.h: No such file or directory

    #include « Menu.h » // Fichier d’entete avec les types pour le menu

    ^

    compilation terminated.

    exit status 1
    Erreur lors de la compilation.

    es-ce en rapport avec la librairie?
    version d’arduino : 1.6.6

    Publié par burlin | 11 décembre 2015, 17 h 42 min
  10. Bonjour Skywood, merci pour ton code. Je l’ai adapté pour une petite appli et la en le reprenant avec la version 1.6.6 ca merde sur le doMainMenuAction’ comme pour les autres. Avez vous solutionné la chose? Si oui comment? Merci

    Publié par Brolensky | 29 décembre 2015, 19 h 54 min
  11. De même, il ne me prend plus LiquidCrytal pourtant copié dans le répertoire??

    Publié par Brolensky | 29 décembre 2015, 20 h 20 min
  12. Non c’est bon les .h sont bien intégré maintenant, mais il replante sur doMainMenuAction… je vais tenter de revenir à la 1.5.7

    Publié par Brolensky | 29 décembre 2015, 22 h 00 min
  13. Bonjour, avec la 1.57 ça marche. S’il y a des balaises parmis vous merci de faire le necessaire pour tansmettre l’info.

    Publié par Brolensky | 30 décembre 2015, 9 h 52 min
    • Toute la branche 1.6.x de l’ide Arduino est buggé jusqu’à la moelle. Pour mes tuto arduino en cours de rédaction, j’ai dû rester en version 1.5.x. Impossible de faire marcher correctement la nouvelle version …

      Publié par Skywodd | 1 janvier 2016, 11 h 58 min
    • Salut,
      Bon, je code depuis deux mois (mesures dht22, liquide cristal, (sorties relais et sms par sim900) et le uno en pète ! J ai taillé dans la masse, mais cela ne suffit pas. Avant de virer le lcd, j ai tenté de passer en progmem mais ça m… On fait const char* const PROGMEM []={string1, string2,….} ?ou pas?
      PS merci pour ton code pour dht.

      Publié par BROLENSKY | 16 février 2016, 23 h 31 min
  14. Génial ce tuto, exactement ce qu’il me fallais… +1 pour BROLENSKY, le soucis vient de la version de l’IDE arduino… n’hesitez pas a revenir aux séries 1.5.X!

    Publié par Loumis | 7 janvier 2016, 16 h 54 min
  15. Bonjour, dans le cadre de mon projet je dois faire un menu de configuration comparable au vôtre mais j’ai un petit problème je ne comprends pas comment créer ma propre bibliothèque … une petite (ou grande..) aide ne serait pas de refus 🙂

    Publié par Gia | 5 février 2016, 18 h 47 min
    • j’imagine que tu es en terminale, je me trompe ?
      dans tout les cas, pour le créer, il suffit de copier ce code dans ton logiciel arduino, une fois enregistré, il suffit de renommer le nom de ton fichier en enlevant le .ino et en le remplaçant en .h
      une fois fait tu place ton fichier Menu.h dans un dossier nommé Menu_h
      et tu copie/colle ce fichier dans la librairie de ton logiciel en suivant ce chemin là normalement C:\Program Files (x86)\Arduino\libraries
      :

      #ifndef _MENU_H_
      #define _MENU_H_

      /* Structure d’un menu */
      typedef struct {
      const char* prompt; // Titre du menu
      const char** items; // Tableau de choix du menu
      const uint8_t nbItems; // Nombre de choix possibles
      void (*callbackFnct)(uint8_t menuItemSelected); // Pointeur sur fonction pour gérer le choix de l’utilisateur
      } Menu_t;

      /* Listes des touches de la shield lcd DFrobots */
      typedef enum {
      BP_NONE, // Aucun bouton appuyé
      BP_SELECT, // Bouton SELECT
      BP_LEFT, // Bouton gauche
      BP_RIGTH, // Bouton droite
      BP_UP, // Bouton haut
      BP_DOWN // Bouton bas
      } Button_t;

      #endif /* _MENU_H_ */

      Publié par pucheu | 25 février 2016, 20 h 36 min
  16. Bonjour j’ai essayé de compiler votre code mais il me retourne ca comme erreur :

    Ecran_de_selection:20: error: ‘doMainMenuAction’ was not declared in this scope
    &doMainMenuAction
    ^
    Ecran_de_selection:33: error: ‘doFreemanMenuAction’ was not declared in this scope
    &doFreemanMenuAction
    ^
    exit status 1
    ‘doMainMenuAction’ was not declared in this scope

    Un petit peu d’aide ? 🙂

    Publié par LEJEUNE Simon | 1 avril 2016, 13 h 40 min
    • bonjour à tous et à toutes,

      tout d’abord un grand merci à notre yoda numérique j’ai nommé Sky(non pas walker)Wood.

      @LEJEUNE SIMON :

      as-tu régler ton problème de compilation ? si oui j’aimerais savoir comment s’il te plait ? car mon IDE (Xcode)
      me renvoie la meme issue et j’ai essayé de declarer : void doMainMenuAction(); mais ca n’as pas l’air de lui plaire !

      merci par avance à tous ceux qui prendront quelques seconde précieuse pour répondre 🙂

      Publié par Jung Mehdi | 6 juillet 2016, 16 h 10 min
      • Bonjour,
        Je ne suis pas (encore) guru sur la compilation Arduino, mais, étant tombé sur le même problème, je l’ai résolu comme je le faisais en langage C à l’époque de ma jeunesse. J’ai ajouté les prototypes de fonctions en haut du fichier INO :

        #include
        #include « menu.h »

        void doMainMenuAction(byte selectedMenuItem);
        void doFreemanMenuAction(byte selectedMenuItem);
        void doSubMainMenuAction(byte selectedMenuItem);

        Cela a résolu le problème de compilation hez moi

        Publié par KURZ | 27 mars 2017, 11 h 53 min
  17. Bonjour.
    comment implanter une scrutation par un appui bouton select par exemple, pour quitter une des fonction du menu item une fois sélectionner, pour eu retour dans le choix des menu item

    Publié par sylvie | 2 avril 2016, 9 h 38 min
    • Salut,
      il faut appeller un sous menu ou fonction dans lequel on defini Continuer =true
      Puis faire un do while (Continuer).
      Enfin a l’interieur de cette boucle on met Continuer = false si readpuschbutton==BP_Select
      Pour parfaire il faiudrait pas entrer dans la boucle tant qu’un appuye bouton est en cours, donc ajouter
      while( readpushbutton() ! = BP_NONE)
      , juste à l’entré de ce sous menu/fcontion
      A++

      Publié par Brolensky | 7 avril 2016, 18 h 07 min
  18. Merci pour ces explications, mais J’ai un peu de mal à suivre.

    (Continuer =true ou continue = true )

    Publié par sylvie | 17 avril 2016, 8 h 45 min
    • Continuer est une variable booléan que tu définie en début de routine

      Publié par cedric cabrol | 23 avril 2016, 6 h 09 min
  19. Bonsoir

    Je viens de découvrir votre code. Un grand merci, c’est exactement ce que je cherchais.

    Bonne continuation et merci pour le partage.

    Publié par SONNECK Christophe | 9 Mai 2016, 21 h 00 min
  20. Bonjour,

    Comme beaucoup de commentaires, je découvre ce code qui va certainement me servir énormément !!!
    Donc, un grand merci d’avance !

    Ceci étant dit, une chose non explorée qui devrait m’être très utile est de pouvoir ajouter ou supprimer des entrées dans les sous menus par l’utilisateur… Est-ce possible ?

    Merci d’avance.

    Publié par Thibaut Fournier | 30 septembre 2016, 15 h 57 min
  21. Bonjour,
    tout d’abord merci pour ce partage,je débute dans la programation,ce menu est exactement ce que je cherche ,j’ai essayer de faire quelque modification mais mais bouton ne fonctionne pas,j’ai un lcd simple,j’ai juste rajouter 4 boutons poussoir que j’ai mis en analogique,je ne sais pas si c,est bon
    /* Inclut la librairie LiquidCrystal pour le LCD */
    #include
    #include « Menu.h » // Fichier d’entête avec les types pour le menu
    void doMainMenuAction(byte selectedMenuItem);
    void doForetMenuAction(byte selectedMenuItem);
    void doSubMainMenuAction(byte selectedMenuItem);
    /* Objet LCD sur les broches utilisées par la shield LCD DFrobots */
    const int rs = 12, en = 11, d4 = 5, d5 = 4, d6 = 3, d7 = 2;
    LiquidCrystal lcd(rs, en, d4, d5, d6, d7);

    /* Menu principal */
    static const char* MAIN_MENU_ITEMS[] = {
    « taille foret »,
    « manuel »,
    };
    static const Menu_t MAIN_MENU = {
    « Votre choix ? »,
    MAIN_MENU_ITEMS,
    2,
    &doMainMenuAction
    };

    /* Sous menu pour Dr Freeman */
    static const char* Foret_MENU_ITEMS[] = {
    « Gordon Freeman »,
    « Morgan Freeman »,
    « Martin Freeman »
    };
    static const Menu_t Foret_MENU = {
    « Votre choix ? »,
    Foret_MENU_ITEMS,
    3,
    &doForetMenuAction
    };

    /** Setup */
    void setup() {

    /* Configuration du LCD */
    lcd.begin(16, 2);

    }

    /** Programme principal */
    void loop() {

    /* Affiche le menu principal */
    displayMenu(MAIN_MENU);

    /* Démo pour montrer la sortie du menu */
    lcd.clear();
    lcd.print(F(« Menu ferme »));
    delay(2000);
    }

    /** Fonction retournant le bouton appuyé (s’il y en a un). */
    Button_t readPushButton(void) {

    /* Lecture de l’entrée des boutons */
    unsigned int bg = analogRead(A0);// bouton bas
    unsigned int bd = analogRead(A1);// bouton haut
    unsigned int select = analogRead(A2);
    unsigned int retour = analogRead(A3);// bouton retour

    /* Test suivant les fourchettes de valeurs */

    if (bd) return BP_UP;
    if (bg) return BP_DOWN;
    if (select) return BP_SELECT;
    if (retour) return BP_NONE;

    /* Par défaut aucun bouton n’est appuyé */

    }

    /** Affiche le menu passé en argument */
    void displayMenu(const Menu_t &menu) {

    /* Variable pour le menu */
    byte selectedMenuItem = 0; // Choix selectionné
    byte shouldExitMenu = false; // Devient true quand l’utilisateur veut quitter le menu
    Button_t buttonPressed; // Contient le bouton appuyé

    /* Tant que l’utilisateur ne veut pas quitter pas le menu */
    while(!shouldExitMenu) {

    /* Affiche le menu */
    lcd.clear();
    lcd.print(menu.prompt);
    lcd.setCursor(0, 1);
    lcd.print(menu.items[selectedMenuItem]);
    /* Attend le relâchement du bouton */
    while(readPushButton() != BP_NONE);

    /* Attend l’appui sur un bouton */
    while((buttonPressed = readPushButton()) == BP_NONE);

    /* Anti rebond pour le bouton */
    delay(30);

    /* Attend le relâchement du bouton */
    while(readPushButton() != BP_NONE);

    /* Gére l’appui sur le bouton */
    switch(buttonPressed) {
    case BP_UP: // Bouton haut = choix précédent

    /* Si il existe un choix précédent */
    if(selectedMenuItem > 0) {

    /* Passe au choix précédent */
    selectedMenuItem–;
    }
    break;

    case BP_DOWN: // Bouton bas = choix suivant

    /* Si il existe un choix suivant */
    if(selectedMenuItem < (menu.nbItems – 1)) {

    /* Passe au choix suivant */
    selectedMenuItem++;
    }
    break;

    case BP_LEFT: // Bouton gauche = sorti du menu
    shouldExitMenu = true;
    break;

    case BP_SELECT: // Bouton droit ou SELECT = validation du choix
    menu.callbackFnct(selectedMenuItem);
    break;
    }
    }
    }

    /** Affiche le choix de l'utilisateur */
    void doMainMenuAction(byte selectedMenuItem) {

    /* Cas spécial pour Dr Freeman */
    if(selectedMenuItem == 2) {

    /* Affiche le sous-menu pour Dr Freeman */
    displayMenu(Foret_MENU);

    } else {

    /* Affiche le choix de l'utilisateur */
    displayChoice(selectedMenuItem, MAIN_MENU_ITEMS);
    }
    }

    /** Affiche le choix de l'utilisateur */
    void doForetMenuAction(byte selectedMenuItem) {

    /* Affiche le choix de l'utilisateur */
    displayChoice(selectedMenuItem, Foret_MENU_ITEMS);
    }

    /** Affiche le choix de l'utilisateur */
    void displayChoice(byte selectedMenuItem, const char** items) {

    /* Affiche le choix de l'utilisateur */
    lcd.clear();
    lcd.print(F("Z'avez choisi :"));
    lcd.setCursor(0, 1);
    lcd.print(items[selectedMenuItem]);

    /* Attend l'appui sur le bouton gauche ou SELECT */
    byte buttonPressed;
    do {
    buttonPressed = readPushButton();
    }
    while(buttonPressed != BP_SELECT);
    }

    Publié par Christophe Gauchet | 5 septembre 2017, 23 h 20 min
  22. Bonjour, Trop trop trop fort, merci beaucoup pour ce partage et ses explications plus que claire, je voulais gerer la température avec arduino mega, dht22, shield dfrobot avec boutons et relais 2 canaux (temp. et humiditer) mais je ne trouvais pas comment inserer un sous menu, je vais lire et relire merci encore grâce à vous je reprends espoir, tres bon week end.

    Publié par beckert68 | 21 octobre 2017, 18 h 04 min
  23. Bonjour,
    Alors je sais que ce post date d’il y a un bon moment mais je tente quand même ma chance ! J’ai essayé de reproduire ton code, étape par étape pour essayer de le comprendre au mieux. Lorsque j’essaie de lancer la vérification, j’ai ce message d’erreur qui apparaît : Arduino : 1.8.6 (Windows 10), Carte : « Arduino/Genuino Uno »

    Menu_deroulant:19:14: error: ‘Menu_t’ does not name a type

    static const Menu_t MAIN_MENU = {

    ^

    Menu_deroulant:32:15: error: ‘FREEMAN_MENU’ does not name a type

    static const FREEMAN_MENU = {

    ^
    exit status 1
    ‘Menu_t’ does not name a type

    Pourtant, je suis convaincu d’avoir repris ton code puisque j’ai réessayé plusieurs fois en repartant à zéro. Est-ce que tu pourrais m’aider ?

    Bonne soirée !

    Publié par Nathan TROMETER | 18 octobre 2018, 18 h 33 min

Rétroliens/Pings

  1. Pingback: Tuto Arduino, Technologie - poulailler porte automatique - 17 février 2016

  2. Pingback: Pensine - Strofe, expérimentations à la Française - 9 Mai 2016

  3. Pingback: Arduino – lcd menu | fullmetaltechno - 12 juin 2016

Skyduino devient Carnet du Maker

Le site Carnet du Maker remplace désormais le blog Skyduino pour tout ce qui touche à l'Arduino, l'informatique et au DIY.