Comme toujours, après la théorie, la pratique...
Bibliothèque LcdUi (2)
. Par :
. URL : https://www.locoduino.org/spip.php?article201L’exemple concret
Pour mieux illustrer l’usage de LcdUi et les bénéfices à en tirer, voyons comment coder une petite centrale DCC de pilotage de réseau construite sur un Arduino. La partie DCC ne sera pas traitée, ce n’est pas le but de cet article, juste l’interface utilisateur avec ses boutons et ses fenêtres... A la place, des messages seront envoyés sur la console pour simuler les changements opérés par la configuration ou l’activation des fonctions.
Côté matériel, on parle d’un encodeur (Vitesse+Ok sur la photo) qui va fixer la vitesse, avec une confirmation par le poussoir greffé sur l’axe de l’encodeur. Deux boutons à bascule permettent d’activer ou pas les fonctions F0 et F1, un bouton poussoir d’annulation (Echap), et un bouton poussoir d’urgence (Stop) pour tout arrêter si besoin. Il aurait du être rouge, mais je n’en avais pas en stock !
Après un écran d’introduction, la centrale permet de contrôler la vitesse, la direction et les deux premières fonctions de la loco courante dont l’adresse est modifiable via l’interface. En plus de l’adresse, on peut changer le nom de la loco ou l’incrément de changement de vitesse. Une option rétro éclairage pour l’écran Lcd est présente.
Voici ce que nous allons tenter de mettre en place :
Oui mais comment ?
Mettre tous ses œufs dans le même panier n’est jamais une bonne idée. Pourquoi je dis ça ? Parce que diviser pour mieux régner est une bonne pratique en programmation. Dans le cas qui nous occupe, la division va servir à améliorer la lisibilité et simplifier la maintenance, chaque source étant dédié à une partie bien identifiée du programme.
Le résultat du schisme, ce sont trois sources :
- A gauche, Dcc.ino le croquis, le point d’entrée du programme. Il s’occupe de la partie Commanders, la bibliothèque qui va s’occuper des boutons et des événements associés, et de renvoyer son résultat au source suivant via
setupUI()
etloopUI()
. - Au centre, UI.cpp (et son pendant UI.hpp) qui s’occupe de toutes les fenêtres, des textes et des événements associés de l’interface utilisateur.
- A droite, WindowLocoControl.cpp (et son pendant WindowLocoControl.hpp) qui décrit une classe de fenêtre spécifique à cette application DCC, et qui sera détaillée plus tard. Cette classe est uniquement utilisée par UI.cpp .
J’aurais tout pu mettre dans le même source Dcc.ino, mais clairement il aurait été trop long et difficile à comprendre...
Le croquis : Dcc.ino
/*************************************************************
project: <LCD User Interface>
author: <Thierry PARIS/Locoduino>
description: <LCDUI Dcc demo>
*************************************************************/
#include "Commanders.h"
#include "LcdUi.h"
#include "UI.hpp"
ButtonsCommanderEncoder vitesse;
ButtonsCommanderPush boutonOk;
ButtonsCommanderPush boutonEchap;
ButtonsCommanderSwitch boutonFonction0;
ButtonsCommanderSwitch boutonFonction1;
ButtonsCommanderPush boutonStop;
#define ENCODEUR 1000
#define FONCTION0 1010
#define FONCTION1 1011
void setup()
{
Serial.begin(115200);
Commanders::begin(LED_BUILTIN);
vitesse.begin(ENCODEUR, 12, 8);
boutonOk.begin(EVENT_SELECT, A0);
boutonEchap.begin(EVENT_CANCEL, A3);
boutonFonction0.AddEvent(FONCTION0, A1, COMMANDERS_EVENT_MOVE, COMMANDERS_MOVE_ON);
boutonFonction0.begin();
boutonFonction1.AddEvent(FONCTION1, A2, COMMANDERS_EVENT_MOVE, COMMANDERS_MOVE_ON);
boutonFonction1.begin();
boutonStop.begin(EVENT_STOP, A4);
setupUI();
}
void loop()
{
unsigned long evenement = Commanders::loop();
byte evenementUI = EVENT_NONE;
switch (evenement)
{
case EVENT_MORE:
case EVENT_LESS:
case EVENT_SELECT:
case EVENT_CANCEL:
case EVENT_STOP:
evenementUI = (byte)evenement;
break;
case ENCODEUR:
if (Commanders::GetLastEventType() == COMMANDERS_EVENT_MOVE)
{
if (Commanders::GetLastEventData() == +1)
evenementUI = EVENT_MORE;
else
evenementUI = EVENT_LESS;
}
break;
case FONCTION0:
if (Commanders::GetLastEventType() == COMMANDERS_EVENT_MOVE)
{
if (Commanders::GetLastEventData() == COMMANDERS_MOVE_ON)
Serial.println("Function 0 activated");
else
Serial.println("Function 0 disactivated");
}
break;
case FONCTION1:
if (Commanders::GetLastEventType() == COMMANDERS_EVENT_MOVE)
{
if (Commanders::GetLastEventData() == COMMANDERS_MOVE_ON)
Serial.println("Function 1 activated");
else
Serial.println("Function 1 disactivated");
}
break;
}
loopUI(evenementUI);
}
Le rôle du croquis va être de fixer les boutons, et de transmettre à UI les événements qui en découlent. Sa compacité le rendra facile à maintenir. L’utilisation de Commanders pour la gestion des divers boutons va aussi considérablement simplifier le croquis...
Le schéma est assez simple : déclaration des boutons et des constantes avant le setup. Dans le setup initialisation desdits boutons avec les bonnes broches et les événements à envoyer pour les interrupteurs de fonction, avant l’appel à la fonction setupUI()
. Elle fait partie du fichier UI.cpp et initialise toute la partie LcdUi.
Guère plus de complication dans le loop()
. Commanders est interrogé pour savoir si un bouton a été actionné. Le switch
qui suit sert à transformer un événement Commanders en événement attendu par LcdUi limité aux déplacements dans les menus. Les événements supplémentaires, comme l’activation des fonctions, sont gérés directement ici dans le switch sans passer par LcdUi. L’activation ou non n’a en effet aucune influence sur les fenêtres de LcdUi. Enfin, la fonction loopUI()
est appelée pour laisser LcdUi faire son travail d’affichage si besoin.
Les broches utilisées sont des broches analogiques parce que mon matériel, celui en photo, était déjà monté comme cela pour libérer les broches usuelles du Nano embarqué...
La partie LcdUI : UI.hpp
#define EVENT_STOP 10
void setupUI();
void loopUI(byte inEvenement);
Le rôle d’un fichier include est de déclarer ce que d’autres fichiers pourront vouloir utiliser, comme des constantes ou des fonctions. C’est exactement ce qui est fait dans UI.hpp, avec la déclaration d’un nouveau type d’événement local pour le bouton d’urgence. Puis la déclaration simplissime de l’interface des fonctions setupUI()
et loopUI()
. Cette dernière reçoit en argument l’événement trouvé grâce à Commanders.
La partie LcdUi : UI.cpp
L’entête
/*************************************************************
project: <LCD User Interface>
author: <Thierry PARIS/Locoduino>
description: <LCDUI DCC demo>
*************************************************************/
#include "Commanders.h"
#include "LcdUi.h"
#include "ScreenLiquid.hpp"
#include "WindowLocoControl.hpp"
#include "UI.hpp"
Après les commentaires d’usage, les includes. D’abord les bibliothèques utilisées, Commanders et bien sûr LcdUi. Ensuite vient ScreenLiquid, qui fait partie de LcdUi et déclare un écran Lcd utilisant la bibliothèque LiquidCrystal livrée avec l’IDE Arduino. WindowLocoControl
définit un nouveau type de fenêtre personnalisée. Nous verrons en détail ce que cela implique. Enfin UI.hpp, surtout pour sa constante EVENT_STOP
qui doit être connue à la fois par le croquis et par UI.cpp. La déclaration des fonctions contenue dans UI.hpp est superflue, mais ne pose pas de problème.
Comme expliqué dans le premier article, il y a trois parties à déclarer pour fabriquer une interface utilisateur. Une liste de textes qui vont constituer les choix possibles et les questions posées, la succession d’écrans qui va décrire le cheminement de l’utilisateur, et la gestion des événements des différentes fenêtres dans le loop()
.
La prose
Bon, avec seize ou vingt caractères selon l’écran, l’envolée lyrique risque d’être limitée... Je rappelle que la liste des textes à utiliser dans les fenêtres doit suivre trois règles : la longueur des textes doit rester inférieure à la taille de l’écran utilisé, les textes doivent être en mémoire programme, et la liste des textes doit elle aussi être en mémoire programme. Pour faire simple, il suffit de copier/coller à partir des exemples fournis et de remplacer par vos textes...
// Declarations des textes
const char str_stop[] PROGMEM = "Arret Urgence";
const char str_stop2[] PROGMEM = "Appuyer Annuler";
const char str_choixprincipal[] PROGMEM = "Menu principal:";
const char str_controleloco[] PROGMEM = "Controle loco";
const char str_choixconfig[] PROGMEM = "Configuration";
const char str_resetconfig[] PROGMEM = "Reset Config";
const char str_oui[] PROGMEM = "oui";
const char str_non[] PROGMEM = "non";
const char str_confirmer[] PROGMEM = "Sur ?";
const char str_retroeclairage[] PROGMEM = "Retro eclairage";
const char str_increment[] PROGMEM = "Vit Increment";
const char str_nomloco[] PROGMEM = "Nom";
const char str_adresseloco[] PROGMEM = "Adresse Loco";
const char str_splash1[] PROGMEM = "LcdUI Demo";
const char str_splash2[] PROGMEM = "For ... you !";
const char str_supprimer[] PROGMEM = "Supprimer";
D’abord on déclare toutes les chaînes qui vont servir pendant le fonctionnement. Le PROGMEM
est bien là et atteste qu’elles seront stockées dans la mémoire programme.
// Liste des textes
const char * const string_table[] PROGMEM =
{
str_stop,
str_stop2,
str_choixprincipal,
str_controleloco,
str_choixconfig,
str_resetconfig,
str_oui,
str_non,
str_confirmer,
str_retroeclairage,
str_increment,
str_nomloco,
str_adresseloco,
str_splash1,
str_splash2,
str_supprimer
};
Ensuite on fabrique une liste string_table
de pointeurs sur ces chaînes pour les rassembler. Cette liste est elle même en mémoire programme grâce à PROGMEM
.
// Indices dans la liste des textes
#define STR_STOP 0
#define STR_STOP2 1
#define STR_CHOIXPRINCIPAL 2
#define STR_CONTROLELOCO 3
#define STR_CHOIXCONFIG 4
#define STR_RESETCONFIG 5
#define STR_OUI 6
#define STR_NON 7
#define STR_CONFIRMER 8
#define STR_RETROECLAIRAGE 9
#define STR_INCREMENT 10
#define STR_NOM 11
#define STR_ADRESSELOCO 12
#define STR_SPLASH1 13
#define STR_SPLASH2 14
#define STR_SUPPRIMER 15
Enfin comme on va utiliser les indices de ces chaînes dans la liste un peu partout, je préfère avoir STR_INCREMENT
plutôt que 10
dans le reste du programme, il est fortement conseillé de suivre cette pratique, mais ce n’est pas une obligation...
Les fenêtres
// Objets principaux
LiquidCrystal lcd(7, 6, 5, 4, 3, 2);
LcdUi lcdui;
ScreenLiquid ecran;
// Variables modifiees par les fenêtres
bool retroEclairage;
Choice choixPrincipal;
Choice choixConfiguration;
WindowSplash winSplash;
WindowChoice winChoixPrincipal;
WindowChoice winChoixConfiguration;
WindowInt winAdresse;
WindowInt winIncrement;
WindowText winNom;
WindowYesNo winRetroEclairage;
WindowConfirm winReset;
WindowLocoControl winLoco;
WindowInterrupt winStop;
Au début, il y a quelques déclarations de variables comme l’écran LiquidCrystal qui sera utilisé, LcdUi lui même, l’écran associé ecran
, des variables dont les valeurs seront modifiées par le menu, et les différentes fenêtres de ce menu. Je reviendrais sur WindowLocoControl
encore plus tard.
Passons au setupUI
:
void setupUI()
{
ecran.begin(16, 2, string_table, &lcd);
lcdui.begin(&ecran);
LcdScreen::YesMsg = STR_OUI;
LcdScreen::NoMsg = STR_NON;
LcdScreen::BackspaceMsg = STR_SUPPRIMER;
Au début, l’écran est initialisé avec 16 caractères, 2 lignes, la liste des textes de l’interface, et l’écran LiquidCrystal. Après le begin()
de lcdUi, on donne l’indice de trois messages essentiels à l’interface : YesMsg, NoMsg et BackspaceMsg, stockés dans des variables globales (statiques pour être précis) à la classe LcdScreen
!
winSplash.begin(STR_SPLASH1, STR_SPLASH2, 2000);
winChoixPrincipal.begin(STR_CHOIXPRINCIPAL, &choixPrincipal);
winChoixConfiguration.begin(STR_CHOIXCONFIG, &choixConfiguration);
winAdresse.begin(STR_ADRESSELOCO, &winLoco.Adresse);
winIncrement.begin(STR_INCREMENT, &winLoco.Increment128);
winNom.begin(STR_NOM, winLoco.Nom, 14);
winRetroEclairage.begin(STR_RETROECLAIRAGE, &retroEclairage);
winReset.begin(STR_RESETCONFIG, STR_CONFIRMER);
winLoco.begin(STR_CONTROLELOCO);
winStop.begin(STR_STOP, STR_STOP2, EVENT_STOP);
Les fenêtres sont initialisées une par une par un appel à leur begin()
, avec la plupart du temps un identifiant qui est aussi le numéro du message principal de cette fenêtre, souvent suivi par la variable qui sera modifiée lorsque l’utilisateur manipulera cette fenêtre.
Par exemple, c’est la variable choixPrincipal
qui sera modifiée lorsqu’une nouvelle sélection sera faite dans la fenêtre winChoixPrincipal
.
La première fenêtre de l’interface est l’affichage d’un copyright pendant deux secondes, une WindowSplash
avec le texte STR_SPLASH1
sur la première ligne, et STR_SPLASH2
sur la seconde. STR_SPLASH1
est aussi l’identifiant de cette fenêtre. Lorsque le temps est écoulé, LcdUi passe tout seul à la fenêtre suivante [1].
Viennent ensuite les deux menus, le principal
et la configuration,
puis les écrans de saisie de cette config : l’adresse de la loco pilotée en DCC,
l’incrément de vitesse utilisé,
son nom avec quatorze caractères maxi à cause de l’écran,
l’activation, ou pas, du rétro-éclairage de l’écran Lcd,
et la fenêtre de réinitialisation de la configuration.
suit la fenêtre spécifique winLoco
dont on finira bien par parler !
La dernière fenêtre winStop
est une interruption qui envoie un événement bien particulier : EVENT_STOP
...
lcdui.AddWindow(&winSplash);
lcdui.AddWindow(&winChoixPrincipal);
lcdui.AddWindow(&winChoixConfiguration);
lcdui.AddWindow(&winAdresse);
lcdui.AddWindow(&winIncrement);
lcdui.AddWindow(&winNom);
lcdui.AddWindow(&winRetroEclairage);
lcdui.AddWindow(&winReset);
lcdui.AddWindow(&winLoco);
lcdui.AddWindow(&winStop);
L’interface utilisateur est constituée de fenêtres qui se succèdent. C’est une liste ordonnée et hiérarchique des écrans présentés à l’utilisateur. Pour construire cette hiérarchie, il faut ajouter de nouvelles fenêtres à la liste générale de lcdui
à l’aide de AddWindow()
. L’indentation (le décalage à droite...) illustre la hiérarchie des fenêtres d’une manière purement graphique et sans effet sur le code produit. C’est juste plus compréhensible comme cela.
winChoixPrincipal.AddChoice(STR_CHOIXCONFIG, &winChoixConfiguration);
winChoixConfiguration.AddChoice(STR_ADRESSELOCO, &winAdresse);
winChoixConfiguration.AddChoice(STR_INCREMENT, &winIncrement);
winChoixConfiguration.AddChoice(STR_NOM, &winNom);
winChoixConfiguration.AddChoice(STR_RETROECLAIRAGE, &winRetroEclairage);
winChoixConfiguration.AddChoice(STR_RESETCONFIG, &winReset);
winChoixPrincipal.AddChoice(STR_CONTROLELOCO, &winLoco);
Une fois la liste construite il faut fixer les liens entre les différentes fenêtres : winChoixPrincipal
est le menu principal, celui qui va permettre de choisir ce que l’on fait. Il est rempli avec deux options : STR_CHOIXCONFIG
le choix secondaire pour la configuration qui ouvre la fenêtre winChoixConfiguration
, et STR_CONTROLELOCO
la fenêtre de pilotage, la fameuse winLoco
[2]...
Pour winChoixConfiguration
aussi, les différentes options/fenêtres de configuration sont reliées.
Puis on va initialiser les variables de travail :
// Valeurs de départ des variables globales
retroEclairage = false;
winChoixPrincipal.SetCurrentChoiceById(STR_CONTROLELOCO);
}
Le choix principal est tout de suite forcé sur l’option STR_CONTROLELOCO
, encore winLoco
!
Enfin vient loopUI()
. Cette fonction reçoit en argument l’événement LcdUi que le croquis Dcc.ino a identifié grâce à Commanders à partir des interactions de l’utilisateur sur les boutons. Je rappelle que de base LcdUi ne connait que six événements :
-
EVENT_NONE
: il ne se passe rien ! -
EVENT_MORE
: option du menu ou valeur +1 -
EVENT_LESS
: option du menu ou valeur -1 -
EVENT_SELECT
: un ’Ok’ -
EVENT_CANCEL
: un ’échappe’
Un autre événement, local à notre exemple a été créé. Il s’agit de EVENT_STOP
utilisé lorsque le bouton d’urgence est pressé. Il est possible de créer autant d’événements que l’on souhaite, avec la seule limite de ne pas réutiliser les valeurs des événements définis par LcdUi elle même... Il suffit pour cela d’utiliser des valeurs supérieures à dix.
Chaque fenêtre interprète ces événements selon son bon vouloir...
void loopUI(byte inEvent)
{
if (inEvent == EVENT_STOP)
{
Serial.println("Arret d'urgence !");
}
if (lcdui.loop(inEvent))
{
Window *pCurrent = lcdui.GetGlobalCurrentWindow();
// Faire ce qui doit etre fait apres une confirmation
if (lcdui.GetState() == STATE_CONFIRMED)
{
switch (pCurrent->GetWindowId())
{
case STR_ADRESSELOCO:
Serial.print("Nouvelle adresse: ");
Serial.println(winLoco.Adresse);
break;
case STR_INCREMENT:
Serial.print("Nouvel incrément: ");
Serial.println(winLoco.Increment128);
break;
case STR_NOM:
Serial.print("Nouveau nom: ");
Serial.println(winLoco.Nom);
break;
case STR_RETROECLAIRAGE:
Serial.print("Retro-eclairage: ");
Serial.println(retroEclairage);
break;
case STR_RESETCONFIG:
Serial.println("Reset Config.");
retroEclairage = false;
winLoco.Adresse = 3;
winLoco.FormatAdresse = 3;
winLoco.Vitesse = 0;
winLoco.VitesseMax = 128;
winLoco.Increment128 = 10;
winLoco.Direction = true;
strcpy(winLoco.Nom, "Locoduino");
break;
case STR_STOP:
Serial.println("Fin de l'urgence");
break;
}
}
}
}
Le premier test vérifie que l’événement n’est pas une urgence. Si c’est le cas, un message console est envoyé dans cet exemple, mais sur une vraie centrale il faudrait donner l’ordre DCC d’arrêt immédiat puis couper l’alimentation.
L’événement reçu, urgence ou pas, est ensuite transmis à lcdui.loop()
qui va le traiter et faire réagir les fenêtres en conséquence. loop()
renvoie un booléen qui signale si l’événement a été traité ou non. S’il n’a pas été traité, c’est sans doute qu’il n’a pas de signification pour la fenêtre en cours, donc inutile de s’en préoccuper ici. Par exemple EVENT_MORE
et EVENT_LESS
ne signifient rien pour une fenêtre d’urgence... S’il a été traité, on récupère l’état courant de la fenêtre courante avec lcdui.GetState()
. L’état qui nous intéresse ici est juste STATE_CONFIRMED
qui signale que quelque chose a été validé dans la fenêtre en cours. Selon l’identifiant de la fenêtre, son premier message, on va décider quoi faire. Dans notre exemple, les modifications des valeurs de configuration seront confirmées par un texte affiché sur la console. Sur une véritable centrale DCC, il faudrait adapter la configuration de la partie DCC avec les données saisies. Par exemple lui dire que l’adresse de la loco à piloter n’est plus 3 mais 25. Le cas de l’urgence est aussi traité. Si une urgence est en cours et que l’état de la fenêtre est passé à STATE_CONFIRMED
, c’est que l’urgence a été levée. Un message console est affiché pour l’exemple, mais la centrale DCC devrait rétablir l’alimentation et envoyer l’ordre de reprendre le cours normal des opérations.
Et si je veux ouvrir ma fenêtre ?
Il est possible de définir complètement une fenêtre selon des besoins particuliers, soit en modifiant légèrement le comportement d’une fenêtre de base, soit en en créant une de toutes pièces.
Dans notre exemple, il nous faut piloter une loco. Nous avons besoin d’une fenêtre donnant le sens de marche et la vitesse de la loco courante. Nous y mettrons aussi l’adresse DCC de la loco, et son nom. Voici WindowLocoControl.hpp :
#define WINDOWTYPE_CONTROLELOCO 100
class WindowLocoControl : public Window
{
public:
int Adresse;
byte FormatAdresse; // nombre de caractères pour l'affichage : 3 pour '025'
byte Vitesse; // Entre 0 et vitesseMax
byte VitesseMax; // 14, 28 ou 128
int Increment128; // un entier juste parce que la fenêtre ne sait pas gérer autre chose
bool Direction;
char Nom[20];
public:
WindowLocoControl();
inline byte GetType() const { return WINDOWTYPE_CONTROLELOCO; }
void Event(byte inEvenement, LcdUi *inpLcd);
void printWindow() { Serial.println("WindowLocoControl"); }
};
La classe de la fenêtre WindowLocoControl
est dérivée de la classe de base de toutes les fenêtres : LcdUi::Window
. Chaque fenêtre dispose d’un type qui permet d’identifier son comportement. C’est le rôle de WINDOWTYPE_CONTROLELOCO
, retourné par la fonction GetType()
.
Nous sommes ici dans de l’objet pur sucre ! Si vous ne l’avez pas déjà fait, je vous conseille de vous plonger dans Le monde des objets (1) pour comprendre la suite... Dans les données membres, on trouve les valeurs de configuration liées à la locomotive pilotée, comme son adresse DCC ou son nom. On y trouve aussi des données qui sont locales comme la vitesse courante Vitesse
, ou le sens du mouvement Direction
. Le constructeur n’a pas d’argument.
GetType()
est une fonction qui surcharge la fonction virtuelle de base de Window
. En effet chaque type de fenêtre retournera un type différent. Comme cette fonction ne fait que renvoyer une valeur sans toucher à l’objet propriétaire this
, le mot clé const
est employé. Enfin la fonction principale, Event()
, qui surcharge aussi la virtuelle de Window
, va recevoir les événements du programme principal et les traiter comme elle l’entend. Voyons WindowLocoControl.cpp :
/*************************************************************
project: <Dcc Controler>
author: <Thierry PARIS/Locoduino>
description: <Class for a loco control window>
*************************************************************/
#include "WindowLocoControl.hpp"
WindowLocoControl::WindowLocoControl()
{
this->Adresse = 3;
this->FormatAdresse = 3;
this->Vitesse = 0;
this->VitesseMax = 128;
this->Increment128 = 10;
this->Direction = true;
strcpy(this->Nom, "Locoduino");
}
Au début de WinLocoControl.cpp, après l’include de base, le constructeur ne fait rien d’autre qu’initialiser toutes ses données avec des valeurs basiques. C’est une partie à ne pas négliger. Si vous n’initialisez pas vos données, n’importe quoi peut s’y trouver, et le comportement du programme sera complètement erratique !
Les événements
La ’vie’ d’une fenêtre passe par cinq états :
-
STATE_START
: la fenêtre est ouverte, mais l’écran contient le texte de l’ancienne fenêtre. Il faut tracer le contenu de la nouvelle fenêtre. -
STATE_INITIALIZE
: la fenêtre est ouverte, il faut initialiser ses variables si besoin. -
STATE_NONE
: c’est un état d’attente pour les événements de LcdUi commeEVENT_MORE
,EVENT_LESS
,EVENT_SELECT
ouEVENT_CANCEL
. -
STATE_CONFIRMED
: la fenêtre a été validée ou -
STATE_ABORTED
: la fenêtre a été annulée !
Event()
va réagir à ces états et aux événements extérieurs. Chaque fenêtre connait son état courant, et l’éventuel événement est passé en argument, avec l’interface générale LcdUi concernée. En effet pour gérer plusieurs écrans, il faudrait déclarer autant instances de LcdUi
, avec pour chaque sa liste de fenêtres, sa liste de textes, etc... C’est d’ailleurs ce que j’avais mis en place pour la vidéo du premier article. La fonction Event()
a besoin de l’écran concerné pour afficher son texte, et elle le retrouve depuis son LcdUi propriétaire inpLcd
.
void WindowLocoControl::Event(byte inEvenement, LcdUi *inpLcd)
{
bool afficheValeur = false;
LcdScreen *pEcran = inpLcd->GetScreen();
if (this->state == STATE_START)
{
pEcran->clear();
LcdScreen::BuildString(this->Adresse, LcdScreen::buffer, this->FormatAdresse);
pEcran->DisplayText(LcdScreen::buffer, 0, 0);
byte len = LcdScreen::BuildString(this->Nom, pEcran->GetSizeX() - (this->FormatAdresse + 1), LcdScreen::buffer);
pEcran->DisplayText(LcdScreen::buffer, pEcran->GetSizeX() - len, 0);
this->state = STATE_NONE;
afficheValeur = true;
}
byte inc = 1;
if (this->VitesseMax == 128)
inc = this->Increment128;
switch (inEvenement)
{
case EVENT_MORE:
{
unsigned int nouvelleValeur = this->Vitesse + inc;
if (nouvelleValeur > this->VitesseMax)
nouvelleValeur = this->VitesseMax;
this->Vitesse = nouvelleValeur;
}
afficheValeur = true;
break;
case EVENT_LESS:
{
int nouvelleValeur = this->Vitesse - inc;
if (nouvelleValeur < 0)
nouvelleValeur = 0;
this->Vitesse = nouvelleValeur;
}
afficheValeur = true;
break;
case EVENT_SELECT:
this->Direction = !this->Direction;
afficheValeur = true;
break;
case EVENT_CANCEL:
this->state = STATE_ABORTED;
break;
}
if (afficheValeur)
{
// 01234567879012345
// 0 Dcc 003
// 1 +>>>>> -
// 01234567879012345
int vitesse = this->Vitesse;
if (vitesse == 1)
vitesse = 0;
LcdScreen::BuildProgress(vitesse, this->Vitesse, this->Direction, pEcran->GetSizeX(), LcdScreen::buffer);
pEcran->DisplayText(LcdScreen::buffer, 0, 1);
}
}
Un variable booléenne afficheValeur
est utilisée pour savoir si la partie variable de l’écran doit être rafraîchie ou pas. Une autre variable locale pEcran
est utilisée pour simplifier le codage plutôt que d’appeler inpLcd->GetScreen()
à chaque ligne ou presque...
Lorsque la fenêtre est ouverte pour la première fois, le noyau LcdUi l’a placée en état STATE_START
. C’est le démarrage. Le première chose à faire est un pEcran->clear()
pour nettoyer l’écran du texte de la fenêtre précédente. Une chaîne de caractères est construite d’après l’adresse DCC. La fonction LcdScreen::BuildString()
formatte le contenu de la chaîne LcdScreen::buffer
avec un nombre entier, ici l’adresse de la loco, cadré à droite sur trois caractères de long, ce qui donne ’025’. La longueur est donnée par la donnée membre this->FormatAdresse
. LcdScreen::buffer
est un tableau de caractères fourni par la bibliothèque et utilisé partout comme fourre-tout pour éviter de devoir redéfinir des ’buffer’ partout...
pEcran->DisplayText()
va placer le texte demandé aux coordonnées voulues. Ici encore c’est LcdScreen::buffer
que l’on vient de remplir qui doit être placée en haut à gauche de l’écran. Les coordonnées sont donc 0 pour le numéro de caractère de départ, et 0 pour la première ligne.
L’état STATE_START
est achevé, il faut passer la fenêtre à l’état suivant. L’état STATE_INITIALIZE
n’est pas utile ici, c’est donc directement à STATE_NONE
que l’on passe. Pour être sûr que la barre de vitesse est bien affichée dès le départ, on met afficheValeur
à true
. Dans le cas STATE_START
, l’événement passé en argument est EVENT_NONE
, c’est à dire qu’il ne se passe rien... On va donc passer le switch sans rien faire et juste traiter le afficheValeur
pour afficher la barre de vitesse avec la fonction LcdScreen::BuildProgress()
. Cette fonction construit une chaîne représentant une quantité de ’>’ proportionnelle à la valeur Vitesse
passée en argument. En DCC, une vitesse de 0 n’existe pas, c’est pourquoi une vitesse de 1 est convertie pour l’affichage en 0. Pour calculer la proportion, la valeur mini est fixée arbitrairement à 0, le maxi this->VitesseMax
est passé en argument. La direction permet de choisir entre des ’>’ et des ’<’ mais aussi d’inverser les ’+’ et ’-’ aux extrémités de la barre. Enfin la taille maxi de la barre et la chaîne de caractères à remplir (LcdScreen::buffer
bien sûr) ferment la liste des arguments. Le texte est affiché sur la seconde ligne, numérotée 1 puisque la première est la zéro.
Au passage suivant dans Event()
, on va tester sur son type pour dire quoi faire :
-
EVENT_MORE
etEVENT_LESS
vont ajouter ou retirer de la vitesse courante l’incrément donné par la donnée membrethis->Increment128
, lequel ne s’applique que si la vitesse maxi est bien de 128. Dans les autres cas, c’est un incrément de 1 qui est utilisé. Cette modification de la vitesse s’assurera aussi qu’elle reste dans les limites permises de 0 et 128. -
EVENT_SELECT
change la direction -
EVENT_CANCEL
sort de la fenêtre et retourne au menu précédent.
Ce programme de démo est bien sûr livré en français [3] avec la bibliothèque (Exemples/LcdUi//Locoduino/Dcc) pour vous permettre de juger sur pièce et pourra aussi vous servir de point de départ pour une nouvelle interface. Pour vous éviter de piétiner sur un problème de définition, n’oubliez pas d’activer le mode DEBUG en modifiant la ligne #define LCDUI_DEBUG_MODE
au début de lcdui.h . La bibliothèque vérifie pour vous la cohérence de votre interface et signale tout problème sur la console. C’est une aide précieuse tant que l’Arduino n’est pas en production. Veillez simplement à l’enlever avant de compiler la version définitive de votre programme. Le mémoire s’en trouvera sensiblement allégée !