Bibliothèque LcdUi (1)

. Par : Thierry. URL : https://www.locoduino.org/spip.php?article141

Construire une interface utilisateur n’est pas simple avec un Arduino. Il n’y a pas de fenêtrage façon ordinateur moderne, ni même de ligne de commande, pas de clavier ou de souris. Alors comment faire ?

Avec un écran LCD et quelques boutons, il devient possible de poser des questions à un utilisateur. Mais comment faire des menus, répondre à des questions ? Et pourquoi faudrait-il refaire à chaque fois une saisie d’entier, un choix dans une liste, une confirmation ?
Le but de la bibliothèque LcdUi est de répondre à ce besoin. Elle fournit un moyen de construire une interface utilisateur simple, basée sur des fenêtres types à configurer et qui réagissent à des événements extérieurs. En effet ce n’est pas LcdUi qui traite les boutons, interrupteurs et autres encodeurs, c’est le programme qui l’utilise. LcdUi sera informé par ce programme qu’une action a été demandée, peu importe comment.
Le choix d’écran est ouvert mais aujourd’hui ne sont gérés que les écrans accessibles par la bibliothèque LiquidCrystal, son dérivé NewLiquidCrystal et l’écran Nokia 5110, quelle qu’en soit la taille. La porte reste ouverte pour ajouter d’autres types d’écran.

Ouvrez la fenêtre !

Une interface utilisateur dans LcdUi s’organise autour de plusieurs fenêtres qui s’enchaînent à l’écran. Chaque fenêtre est issue d’une classe de la bibliothèque : fenêtre de titre, saisie d’un entier ou d’un texte, choix d’une option, etc... Le flux de ces fenêtres peut être suspendu par des fenêtres d’interruption. C’est par exemple un arrêt d’urgence de l’Arduino qui va couper toutes les tensions en attendant de pouvoir reprendre, tout en affichant un message d’information. Lorsque l’utilisateur annule l’interruption, alors le programme revient où il en était... Les fenêtres fournies par LcdUi s’adapteront toutes seules au nombre de lignes de l’écran utilisé. A noter qu’il est possible de gérer plusieurs écrans simultanément avec autant d’interfaces différentes. Seule la mémoire SRAM vous limitera...

Des fenêtres partout !

La fenêtre est l’unité de travail de LcdUi. La bibliothèque maintient une liste des fenêtres et un curseur qui se déplace dans cette liste, la fenêtre courante. Cette liste est en réalité une arborescence : selon les choix validés, la fenêtre suivante sera peut-être différente...

Prenons un exemple théorique simple :

PNG - 8.9 kio

L’application exemple va commencer par montrer un titre pendant quelques secondes dans une fenêtre temporaire, puis s’ouvre un menu avec deux options. La première ouvre un choix de valeur entière, comme une durée de clignotement, une intensité, etc... Une fois la saisie de la valeur terminée, et comme il n’y a pas de fenêtre suivante, LcdUi revient à la fenêtre précédente, c’est à dire au menu. La seconde ouvre une demande de confirmation, comme ’Activer le rétro éclairage ?’ ou ’Allumer les feux ?’... Là encore LcdUi revient au menu lorsque la réponse est confirmée. Bien sûr le croquis aura récupéré l’information saisie, l’entier ou la confirmation, et aura réagi en conséquence... Il est possible d’enchaîner plusieurs fenêtres pour une seule option.

Le code présent dans cet article est volontairement abrégé pour se focaliser sur LcdUi. Le code complet et fonctionnel de cet exemple est disponible dans les exemples de la bibliothèque sous Locoduino/Sample, grâce à l’option Fichier/Exemples de l’IDE !

Voyons dans le détail les types de fenêtres qui sont disponibles par défaut :

  • WindowSplash est une fenêtre temporaire (appelée aussi Splash Screen dans la langue de Benny Hill). Elle apparaît, puis disparaît après un délai déterminé. Elle peut servir à afficher un message d’erreur, un copyright ou une info. Deux textes sont nécessaires pour la première et la seconde ligne...
PNG - 1.8 kio
  • WindowChoice/WindowChoiceText est sans doute la plus utilisée. Elle permet à l’utilisateur de choisir parmi une liste de choix à proposer. C’est la base des menus. Un texte est défini pour la question posée (’Choix principal’ dans l’exemple), puis chaque option donne son texte : ’Option 1’ ou ’Option 2’. WindowChoiceText en est une variante qui utilise des textes d’option construits par le croquis au moment de la question, au lieu de chaînes stockées en dur.
PNG - 2.5 kio
  • WindowInt permet de saisir un entier. Une chaîne est nécessaire pour la question. Un pointeur sur un entier doit également être passé. C’est la variable qui sera mise à jour par la fenêtre.
PNG - 1.6 kio
  • WindowText permet de saisir un texte. Encore une chaîne pour la question, et un pointeur vers une chaîne de caractères à modifier. Il faut aussi donner la longueur maxi autorisée pour la réponse.
PNG - 1.6 kio
  • WindowYesNo est une réponse booléenne à une question. Toujours une chaîne pour la question. Les chaînes ’oui’ et ’non’ pour la réponse sont fixées d’une manière globale pour l’application. Inutile de les repréciser à chaque fenêtre de confirmation... L’adresse d’un booléen qui sera modifié par la fenêtre est également nécessaire.
PNG - 2.1 kio
  • WindowConfirm demande une confirmation. Outre les deux chaînes ’oui’ et ’non’ utilisées ici aussi, il lui faut un texte de demande de confirmation comme ’Sur ?’. Le vrai texte en français serait plutôt ’Sûr ?’, mais les accents sont la plupart du temps absents de ce que savent afficher ce type d’écrans Lcd. De plus la place est limitée pour pouvoir insérer un texte comme ’Confirmer ?’ qui serait trop long pour un écran 16 caractères comme celui dont je dispose si l’on considère le ’oui’, le ’non’, les ’><’ de sélection et les espaces pour la compréhension qu’il faut mettre derrière. Ce type de fenêtre pourrait être utilisé par exemple si la dernière option du menu était ’Reinit config’, il faudrait confirmer pour effectivement réinitialiser la configuration du programme.
PNG - 2.5 kio
  • WindowInterrupt interrompt la fenêtre en cours, comme le ferai l’appui sur un bouton d’urgence ou un problème général. Une chaîne pour la première ligne, une seconde pour la deuxième ligne sont nécessaires.
PNG - 2.5 kio
  • WindowInterruptConfirm agit comme la fenêtre précédente, c’est une interruption, mais cette fenêtre demande une confirmation avant de retourner d’où elle vient. Dans ce cas, une chaîne pour la première ligne, ’oui’ ’et ’non’ seront utilisés pour la seconde ligne après une chaîne de question de type ’Sur ?’ .
PNG - 2.6 kio

Arduino, option littéraire !

Comme on vient de le voir, une fenêtre c’est du texte et un comportement. Beaucoup de fenêtres, c’est donc beaucoup de texte !
Pour pouvoir fonctionner, LcdUi va avoir besoin d’une liste de chaînes de caractères. Et pour économiser la mémoire vive (la SRAM), on va mettre tout ça dans la mémoire programme (mémoire Flash), la plus abondante.
Déclarons les chaînes de caractères pour notre petit exemple :

const char str_titre[] PROGMEM = "Titre";
const char str_copyright[] PROGMEM = "Copyright";
const char str_choix[] PROGMEM = "Choix principal";
const char str_option1[] PROGMEM = "Option 1";
const char str_option2[] PROGMEM = "Option 2";
const char str_oui[] PROGMEM = "Oui";
const char str_non[] PROGMEM = "Non";

On retrouve toutes les chaines utilisées par notre exemple.

  • Chacune est déclarée const, c’est à dire que personne ne les modifiera pendant l’exécution. Une donnée dans PROGMEM (voir plus loin...) ne peut pas être modifiée.
  • Pour mieux les identifier, j’ai décidé arbitrairement de préfixer tous leurs noms avec ’str’ comme ’string’ : chaîne de caractères dans la langue de Donald...
  • Une chaîne est un tableau, donc déclaré avec des crochets. Mais je laisse le soin au compilateur de calculer tout seul la taille de ce tableau avec [].
  • Chacune est bien entendu déclarée en mémoire programme par PROGMEM.
  • Attention de tenir compte de l’écran qui sera utilisé pour remplir ces chaînes. Pour la taille d’abord. Aucune chaîne ne doit dépasser la taille de l’écran ! Par ailleurs, il y a en a très peu qui connaissent les accents à la française, et ils risquent d’afficher des hiéroglyphes à la place ! L’écran doit gérer l’Unicode, ou au moins la page ’Fr’ de l’ASCII étendu pour afficher les accents. Si rien de tout ça n’est présent dans la documentation de l’écran, oubliez les accents, les trémas, les cédilles et les E dans l’O ! Sauf rare exception, c’est généralement le cas des écrans que l’on trouve dans le commerce.

Manipuler des pointeurs (str_copyright ou str_non sont des pointeurs) n’est pas aisé, et plutôt dangereux. J’ai donc préféré manipuler une liste de pointeurs avec les indices dans cette liste :

const char * const string_table[] PROGMEM =
{
 	str_titre,
	str_copyright,
	str_choix,
	str_option1,
	str_option2,
	str_oui,
	str_non
};

Elle est aussi en mémoire programme grâce à PROGMEM, et ce n’est que l’énumération des pointeurs const char * de chaines déclarées plus tôt... Là encore, la taille de la liste est automatique. Notez que je mets volontairement les pointeurs dans la liste dans le même ordre que leur déclaration un peu plus tôt. Ce n’est pas obligatoire, c’est juste plus compréhensible.

Enfin lorsqu’il va falloir utiliser la chaîne ’oui’, je n’ai pas envie de me rappeler qu’il s’agit de l’indice 5 (on part de zéro...) dans la liste. Je vais donc déclarer des constantes avec #define, une par ligne.

// Indices des chaines dans la liste 
#define STR_TITRE	0
#define STR_COPYRIGHT	1
#define STR_CHOIX	2
#define STR_OPTION1	3
#define STR_OPTION2	4
#define STR_OUI		5
#define STR_NON		6

Si demain il fallait ajouter une nouvelle chaîne au milieu de la liste (pour garder une certaines cohérence des messages, que ce soit un ordre alphabétique ou un rangement par thème...), il me suffirait d’ajouter un #define au milieu, et de renuméroter les define suivants. Si par exemple STR_OPTION1 se retrouvait à 5 au lieu de 3, comme le reste du code utilise STR_OPTION1 et non pas 3, le passage se fera sans douleur. Si j’avais mis des 3 partout, il aurait fallu les retrouver et les modifier... Et aussi mettre à jour tous les suivants pour les décaler : 4 en 6, 5 en 7, etc...
Ces numéros nous serviront également à identifier les fenêtres pendant l’exécution. Par exemple : à la sortie de la fenêtre de saisie d’entier, c’est son indice de texte STR_OPTION1 qui va me permettre de savoir que c’est bien cette valeur entière qui a été modifiée par LcdUi. Nous verrons cela dans la gestion d’événements.
Pour finir, le nombre de chaînes de caractères possibles est limité à 254. 255 signifiant en interne de la bibliothèque qu’il n’y a pas de chaîne. Une ergonomie qui aurait besoin de plus aurait tout intérêt à passer sur un support plus gros comme un Raspberry Pi ou un PC !

Fenêtres de l’exemple

Voyons comment déclarer les fenêtres de notre petit croquis :

WindowSplash ecranTitre;
WindowChoice ecranChoix;
WindowInt ecranOption1;
WindowYesNo ecranOption2;

Rien de bien transcendant, si ce n’est que le préfixe des objets est ’ecran’ parce que c’est plus court que ’fenetre’ ! Passons au setup :

	ecranTitre.begin(STR_TITRE, STR_COPYRIGHT, 500);
	ecranChoix.begin(STR_CHOIX, &choix);
	ecranOption1.begin(STR_OPTION1, &option1);
	ecranOption2.begin(STR_OPTION2, &option2);

Des begin doivent être faits sur chaque fenêtre pour en fixer le contenu et le comportement. On retrouve les arguments cités plus haut avec en tête les indices de chaîne dans la liste générale de textes. Parfois suit aussi le pointeur sur la variable à modifier.

	lcdui.AddWindow(&ecranTitre);
	lcdui.AddWindow(&ecranChoix);
	lcdui.AddWindow(&ecranOption1);
	lcdui.AddWindow(&ecranOption2);

Comme précisé plus haut, l’interface utilisateur est une suite ordonnée de fenêtres qui vont respecter cet ordre pour s’ouvrir : la fenêtre temporaire, puis le choix, et si besoin les éditions d’option.

	ecranChoix.AddChoice(STR_OPTION1);
		ecranOption1.SetFather(&ecranChoix, STR_OPTION1);
	ecranChoix.AddChoice(STR_OPTION2);
		ecranOption2.SetFather(&ecranChoix, STR_OPTION2);

Enfin, il faut construire l’arborescence des fenêtres, c’est à dire l’enchaînement des écrans. Les AddChoice vont ajouter des options à un WindowChoice, un menu à choix multiple. Ici, il y a deux choix, un pour option1, l’autre pour option2, à ajouter à la fenêtre ecranChoix. Il faut aussi préciser à chaque fenêtre fille (ecranOption1, ecranOption2) de quel choix et de quel menu elles dépendent. Les deux ici sont des ’enfants’ de ecranChoix, l’une pour l’option typée STR_OPTION1, l’autre pour STR_OPTION2.

L’événementiel, c’est un métier !

LcdUi réagit en fonction d’événements qui lui arrivent. Peu importe le canal : que ce soit un bouton simple, une liaison série, un bus CAN ou une interface bâtie avec Commanders, le principe est le même.

Les événements possibles sont au nombre de quatre importants :

  • EVENT_MORE : On veut passer à l’option suivante, à la valeur suivante, bref on veut avancer...
  • EVENT_LESS : on veut au contraire reculer dans les choix proposés ou dans la valeur à saisir.
  • EVENT_SELECT On a choisi !
  • EVENT_CANCEL Sortie de l’écran en cours, retour à l’écran précédent.

Et trois non utilisés dans les fenêtres de base, mais qui pourraient l’être dans des fenêtres personnalisées :

  • EVENT_MOVE On s’est déplacé d’une valeur absolue, par exemple via un potentiomètre ;
  • EVENT_START On va au début,
  • EVENT_END On va à la fin.

Selon le contexte, chaque action a un rôle particulier. Par exemple dans un choix de menu : MORE et LESS (plus ou moins dans la langue de Gaston Lagaffe) vont nous permettre de nous déplacer dans la liste des choix. SELECT va confirmer le choix en cours, et CANCEL (échappe) va retourner au menu précédent si c’est possible.
Par contre, pendant la saisie de l’adresse DCC d’une loco ou d’un accessoire, MORE et LESS vont modifier la valeur en cours, SELECT confirmera le choix, tandis que CANCEL refusera de choisir et retournera au menu précédent.
MORE et LESS auraient pu s’appeler UP et DOWN (haut et bas). Mais j’ai préféré faire référence aux touches qui vont augmenter ou diminuer la vitesse d’une loco, ou la valeur entière à saisir, plutôt qu’aux menus...

Dans le loop, il faut appeler le loop de LcdUi qui renvoie ’true’ si quelque chose s’est passé, que ce soit à partir de l’événement event passé en argument ou de la boite qui se rafraîchit toute seule, puis traiter les boites validées pour tenir compte des nouvelles valeurs. Par exemple afficher les valeurs saisies sur la console pour montrer qu’elles ont été modifiées :

	if (lcdui.loop(event))
	{
		// Do things when a window is confirmed:
		if (lcdui.GetState() == STATE_CONFIRMED)
		{
			switch (lcdui.GetGlobalCurrentWindowId())
			{
			case STR_OPTION1:
				Serial.print("Option1: ");
				Serial.println(option1);
				break;
			case STR_OPTION2:
				Serial.print("Option2: ");
				Serial.println(option2);
				break;
			}
		}
	}

On va se fier pour ça à l’état STATE_CONFIRMED de la dernière fenêtre ouverte. Il est important de bien tester l’état, parce que l’on va fréquemment passer ici en cours de traitement de la fenêtre et que le seul cas intéressant est celui de sa confirmation, et éventuellement de son annulation avec STATE_ABORTED.
L’identifiant de la fenêtre en cours, celle qui est sur l’écran, est en réalité le numéro du texte de sa première ligne. Si deux fenêtres avec le même texte doivent apparaître (pas de bol...) il faudra doubler le message dans la liste pour ne pas avoir deux indices identiques et confondre les deux fenêtres.

Les fenêtres interruptives

Dans l’exemple et pour simplifier le code, je n’ai pas défini ce type de fenêtre. Mais imaginons sur un TCO un gros bouton rouge d’arrêt d’urgence pour gérer un accident sur la voie. Ce bouton émet un événement particulier EVENT_URGENCE déclaré dans le croquis. Lorsque cet événement est envoyé, donc que le bouton a été pressé, le croquis doit couper le courant, afficher un message prévenant du problème et attendre la fin de l’alerte. Lorsque l’utilisateur a résolu le problème et s’est assuré que rien ne peut gêner la bonne marche du réseau, il appuie sur le bouton ’Annuler’ pour sortir du mode d’urgence. La fenêtre disparaît, le courant est rétabli, et l’afficheur revient à la situation qui était la sienne avant l’appui sur le bouton d’urgence. Comment gérer cela avec LcdUi ? D’abord, déclarez une fenêtre d’interruption :

WindowInterrupt ecranUrgence;

Il faut lui donner deux chaînes dans le begin : la première et la seconde ligne. Il faut aussi lui préciser à quel événement elle doit réagir. Ici EVENT_URGENCE. Lorsque vous définissez cet événement attention à ne pas entrer en collision avec les événements normaux comme MORE, LESS et les autres. Le mieux est de commencer les vôtres à partir de 100.

ecranUrgence(STR_URGENCE, STR_ANNULER, EVENT_URGENCE);

Enfin, il faut l’ajouter à la liste des fenêtres, mais son positionnement est indifférent. En effet par nature, une interruption peut intervenir à n’importe quel moment, donc le placement de cette fenêtre dans la liste générale n’est pas significatif, et ne gêne pas non plus le flux du reste des fenêtres.

lcdui.AddWindow(&ecranUrgence);

PNG - 2.5 kio

Reste à gérer les événements pour utiliser cette fenêtre.

	if (lcdui.loop(inEvent))
	{
		switch (lcdui.GetGlobalCurrentWindowId())
		{
		case STR_INT1:
			// Faire ce qui doit être fait à l'ouverture de la fenêtre
			if (lcdui.GetState() == STATE_INITIALIZE)
			{
				Serial.println("Coupure du courant");
				break;
			}

			// Faire ce qui doit être fait à la confirmation/fermeture de la fenêtre
			if (lcdui.GetState() == STATE_CONFIRMED)
			{
				Serial.println("Remise du courant");
				break;
			}
		}
	}

Pour toutes les fenêtres, l’état STATE_INITIALIZE correspond au moment de l’ouverture de la fenêtre, l’état STATE_CONFIRMED à sa fermeture. Si la fenêtre est bien celle de l’interruption STR_INT1, alors il faut couper ou remettre le courant !

DIY

Sous cet acronyme bien connu sur un site comme le nôtre, se cache la possibilité de définir de nouvelles fenêtres comme vous l’entendez, en dérivant un comportement existant ou en créant tout à partir de rien (’from scratch’ dans la langue de Mickey). Mais nous verrons cela dans le second article consacré à un exemple concret.

Voici le résultat de cet exemple sous forme de vidéo.

Demo bibliothèque LcdUi sur deux écrans
Thierry

Le bibliothèque est disponible ici : Forge Locoduino

Les autres bibliothèques

Les bibliothèques qui peuvent être utilisées pour les écrans pilotables par LcdUi sont disponibles facilement :

  • LiquidCrystal est livrée avec l’IDE Arduino. Elle permet de piloter les écrans LCD texte utilisants un contrôleur HD44780 ou compatible.
  • NewliquidCrystal est un fork. C’est à dire une bibliothèque dérivée, en l’occurrence du LiquidCrystal vue précédemment. Elle accélère notablement les accès à l’écran tout en fournissant une interface strictement identique. Enfin pas tout à fait strictement. En réalité, il s’agit d’une évolution de mon cru d’une bibliothèque dont son auteur parle ici sur le forum Arduino. Mais sa procédure d’installation impliquait de modifier l’installation de l’IDE Arduino pour détruire l’originale LiquidCrystal qu’elle est sensée remplacer. Je n’aime pas toucher à l’IDE, notamment parce que cela pose de gros problèmes à chacune de ses mises à jour. J’ai donc préféré récupérer la bibliothèque en question, et renommer tous les fichiers et les objets à l’intérieur pour éviter des collisions de nom avec LiquidCrystal. Cette nouvelle bibliothèque est disponible dans le répertoire extras de LcdUi. Et elle s’installe comme n’importe quelle autre...
  • La bibliothèque Adafruit pour le pilotage d’un écran Nokia 5110 est également présente dans le répertoire extras. Je n’ai pas pu tester vraiment cet écran. N’hésitez pas à me communiquer les problèmes rencontrés s’il devait y en avoir.

Via quelques classes simples, LcdUi permet de se construire une interface utilisateur sophistiquée, en tout cas pour un Arduino... Dans le second article, nous verrons un exemple concret d’ergonomie, exemple qui sera aussi le point de départ d’une autre série d’articles. Mais j’en dis sans doute trop...