Avant de se lancer à programmer, il faut déterminer la façon dont les objets du réseau sont représentés dans le programme. Une bonne représentation permettra d’écrire un programme clair et un programme clair a de bien meilleures chances de fonctionner du premier coup.
Gestion d’une gare cachée (2)
« Show me the data »
. Par :
. URL : https://www.locoduino.org/spip.php?article156Cet article va mettre en œuvre des notions avancées de programmation objet. Je vous engage à lire les articles de Thierry à ce sujet :
Dans un premier temps nous allons nous occuper de faire fonctionner cette gare indépendamment des capteurs et des actionneurs. Nous ajouterons ces derniers plus tard. La première chose à faire est d’exploiter le graphe de la gare pour y trouver une voie. Cet article traite de ce problème d’exploitation du graphe. Ensuite, faire fonctionner la gare consistera à trouver une voie en gare libre pour y stocker un train, d’une part, et sélectionner un train de la gare pour le mettre en circulation d’autre part. Ce sont deux problèmes voisins qui seront traités dans le prochain article.
À la fin de « Gestion d’une gare cachée (1) », nous avons vu la représentation de notre gare sous forme de graphe.
Nous allons traduire cette représentation en objets.
Tout d’abord, il nous faut définir des classes. Un nœud du graphe est un élément de voie. Déclarons donc une classe pour représenter un élément de voie. Il n’y a pas besoin de grand chose, juste un identifiant permettant de nommer les éléments de voie afin de les retrouver dans le graphe ainsi qu’un constructeur pour initialiser cet identifiant avec l’argument qui lui est passé et une fonction membre permettant de lire cet identifiant. Et puis nous avons besoin d’une fonction membre permettant de créer un chemin entre l’entrée de la gare et une voie de la gare, appelons cette fonction membre creeCheminVers
, et entre la sortie de gare et une voie de la gare. Cette fonction membre prend pour argument l’identifiant de la voie vers laquelle on souhaite établir un chemin. Si la destination est trouvée, elle retourne true
sinon elle retourne false
. Il s’agit d’une fonction membre virtuelle car elle sera redéfinie concrètement pour un aiguillage, pour les voies de gare et pour les voies d’entrée et de sortie. Il s’agit même d’une fonction virtuelle pure, ce qui est dénoté par le = 0
à la fin de la déclaration. Non seulement elle est virtuelle mais, comme elle ne peut rien faire avec un objet de la classe Voie
qui n’est connecté à rien, elle n’existera concrètement que dans les classes dérivées de Voie
. Cela fait de Voie
une classe abstraite, c’est à dire qu’on ne peut pas créer (instancier) des objets de cette classe.
class Voie
{
private:
byte mIdentifiant;
public:
Voie(byte identifiant) { mIdentifiant = identifiant; }
byte idVoie() { return mIdentifiant; }
virtual bool creeCheminVers(byte identifiant) = 0;
};
Cette classe de base est maintenant employée pour définir nos trois autres classes qui vont donc en hériter.
Ces voies concrètes définissent concrètement la fonction membre creeCheminVers
comme nous allons le voir. Elle définissent également des fonctions membres permettant de connecter les voies entre elles. On est ici proche de ce que nous réalisons en construisant un réseau, nous connectons des voies entre elles. Ici, comme nous traitons le cas spécifique d’une gare cachée avec un graphe dédié à cette réalisation, les connexions sont simples à traiter. Une voie d’entrée connait, est connectée à, une seule autre voie, une voie en gare n’en connait aucune et un aiguillage connait deux autres voies.
Les voies d’entrée et de sortie
Commençons par la classe des voies d’entrée et de sortie. Ces voies sont particulières et différentes des voies de gare car elles ne sont connectées qu’à une seule autre voie. Nous allons appeler cette classe VoieExtreme
. Cette classe possède une donnée membre, mVoie
, qui est un pointeur vers une autre Voie, la fonction membre connecte
qui permet de réaliser la connexion à cette autre voie et creeCheminVers
.
Notez qu’on ne préjuge pas du type de voie connecté puisque mVoie
est un pointeur vers un objet de type Voie
. De fait, toute voie dérivée de Voie
peut être connectée, la différence viendra de la fonction membre creeCheminVers
qui sera différente selon la voie effectivement connectée. Comme cette fonction membre est virtuelle, c’est le creeCheminVers
de la voie effectivement connectée qui sera appelé même si le pointeur est un pointeur de Voie
.
Pour une VoieExtreme
, la fonction creeCheminVers
va comparer l’identifiant qui lui est passé en argument à l’identifiant de la voie d’entrée ou de sortie. Si il sont identiques, cela signifie que l’on est arrivé à destination, sinon on demande à la voie à laquelle elle est connectée de créer le chemin. Notez que cela permet de chercher un chemin vers une voie d’entrée, ce qui n’est guère utile car c’est le point de départ de la recherche mais c’est une généralisation du principe. Notez également l’initialisation de mVoie
à NULL. Elle va permettre de savoir si le graphe a été correctement initialisé ou si certaines voies ne sont connectées à aucune autre et d’éviter de suivre des chemins qui n’existent pas.
class VoieExtreme : public Voie
{
private:
Voie *mVoie;
public:
VoieExtreme(byte identifiant) : Voie(identifiant) { mVoie = NULL; }
void connecte(Voie *voie) { mVoie = voie; }
virtual bool creeCheminVers(byte identifiant);
};
bool VoieExtreme::creeCheminVers(byte identifiant)
{
if (identifiant == idVoie()) {
return true;
}
else {
if (mVoie != NULL) {
return mVoie->creeCheminVers(identifiant);
}
else {
return false;
}
}
}
Les voies de gare
Les voies de gare n’étant connectées à aucunes autres, rappelez vous que le graphe est orienté, elles sont très simples à représenter. Le constructeur se contente d’appeler le constructeur de Voie
et creeCheminVers
se contente de retourner true
ou false
selon que la voie de gare est la destination souhaitée ou non.
class VoieGare : public Voie
{
public:
VoieGare(byte identifiant) : Voie(identifiant) {}
virtual bool creeCheminVers(byte identifiant);
};
bool VoieGare::creeCheminVers(byte identifiant)
{
if (identifiant == idVoie()) {
return true;
}
else {
return false;
}
}
Les aiguillages
Les aiguillages sont un petit peu plus compliqués car ils sont connectés à 2 éléments de voie. Ils vont donc posséder deux données membres, mVoieDroite
et mVoieDeviee
, des pointeurs vers ces deux éléments de voie et deux fonctions membres, connecteDroite
et connecteDeviee
vont permettre de les connecter. creeCheminVers
cherche côté droit et côté dévié. Normalement un seul chemin est valable, ou aucun. On cherche d’abord côté droit et si on ne trouve pas, on cherche côté dévié. On va en profiter pour afficher, via la ligne série, le chemin qui a été trouvé afin de tester le programme.
class Aiguillage : public Voie
{
private:
Voie *mVoieDroite;
Voie *mVoieDeviee;
public:
Aiguillage(byte identifiant) : Voie(identifiant) { mVoieDroite = mVoieDeviee = NULL; }
void connecteDroite(Voie *voie) { mVoieDroite = voie; }
void connecteDeviee(Voie *voie) { mVoieDeviee = voie; }
virtual bool creeCheminVers(byte identifiant);
};
bool Aiguillage::creeCheminVers(byte identifiant)
{
if (identifiant == idVoie()) {
return true;
}
else {
if (mVoieDroite != NULL && mVoieDeviee != NULL) {
/* Uniquement si correctement connecte */
/* Cherche la destination cote voie droite */
if (mVoieDroite->creeCheminVers(identifiant)) {
/* Destination trouvee cote droit */
Serial.print(idVoie());
Serial.println(": Droit");
return true;
}
else {
/* Cherche la destination cote voie deviee */
if (mVoieDeviee->creeCheminVers(identifiant)) {
/* destination trouvee cote devie */
Serial.print(idVoie());
Serial.println(": Devie");
return true;
}
else {
/* destination non trouvee */
return false;
}
}
}
}
}
Mise en œuvre de ces objets
Nous avons notre base, construisons maintenant notre graphe. Si il apparaît que d’autre fonctions sont nécessaires nous les ajouterons par la suite.
Tout d’abord, il nous faut créer des identifiants pour nos éléments de voie. Le meilleur moyen de faire cela est de créer un enum
. Voir à ce propos « Trois façons de déclarer des constantes ».
enum {
VOIE_ENTREE,
VOIE_SORTIE,
AIG_ENTREE_0,
AIG_ENTREE_1,
AIG_ENTREE_2,
AIG_SORTIE_0,
AIG_SORTIE_1,
AIG_SORTIE_2,
VOIE_GARE_0,
VOIE_GARE_1,
VOIE_GARE_2,
VOIE_GARE_3,
AUCUNE_VOIE
};
Notez le dernier identifiant, AUCUNE_VOIE
qui va nous servir plus tard. En effet, lors de la recherche d’une voie libre, si cette voie est trouvée nous retournerons son identifiant. Si aucune voie libre n’est trouvée, nous retournerons AUCUNE_VOIE
.
Notez également que ces identifiants sont des nombres et que si on veut les afficher pour tester notre système, la lecture risque d’être difficile. Nous allons donc créer une fonction afficheVoie
qui à partir de cet identifiant va nous afficher en clair de quel élément de voie il s’agit. Seuls les aiguillages nous intéressent à priori.
void afficheVoie(byte identifiant)
{
switch (identifiant) {
case AIG_ENTREE_0:
Serial.print("Aiguillage E0"); break;
case AIG_ENTREE_1:
Serial.print("Aiguillage E1"); break;
case AIG_ENTREE_2:
Serial.print("Aiguillage E2"); break;
case AIG_SORTIE_0:
Serial.print("Aiguillage S0"); break;
case AIG_SORTIE_1:
Serial.print("Aiguillage S1"); break;
case AIG_SORTIE_2:
Serial.print("Aiguillage S2"); break;
default:
Serial.print("** INCONNU **"); break;
}
}
Et par conséquent nous allons modifier la fonction creeCheminVers
de la classe Aiguillage
en remplaçant les Serial.print(idVoie())
par afficheVoie(idVoie())
.
Nous allons ensuite créer nos objets.
VoieExtreme voieEntree(VOIE_ENTREE);
VoieExtreme voieSortie(VOIE_SORTIE);
Aiguillage aiguillageE0(AIG_ENTREE_0);
Aiguillage aiguillageE1(AIG_ENTREE_1);
Aiguillage aiguillageE2(AIG_ENTREE_2);
Aiguillage aiguillageS0(AIG_SORTIE_0);
Aiguillage aiguillageS1(AIG_SORTIE_1);
Aiguillage aiguillageS2(AIG_SORTIE_2);
VoieGare voieGare0(VOIE_GARE_0);
VoieGare voieGare1(VOIE_GARE_1);
VoieGare voieGare2(VOIE_GARE_2);
VoieGare voieGare3(VOIE_GARE_3);
Pour l’instant, ils ne sont pas connectés entre eux. Pour réaliser cette connexion, nous allons utiliser les fonctions connecte
, connecteDroite
et connecteDeviee
dans setup
.
/* Connexion des elements de voie du grill d'entree */
voieEntree.connecte(&aiguillageE0);
aiguillageE0.connecteDroite(&voieGare0);
aiguillageE0.connecteDeviee(&aiguillageE1);
aiguillageE1.connecteDroite(&aiguillageE2);
aiguillageE1.connecteDeviee(&voieGare1);
aiguillageE2.connecteDroite(&voieGare3);
aiguillageE2.connecteDeviee(&voieGare2);
/* Connexion des elements de voie du grill de sortie */
voieSortie.connecte(&aiguillageS0);
aiguillageS0.connecteDroite(&voieGare3);
aiguillageS0.connecteDeviee(&aiguillageS1);
aiguillageS1.connecteDroite(&aiguillageS2);
aiguillageS1.connecteDeviee(&voieGare2);
aiguillageS2.connecteDroite(&voieGare0);
aiguillageS2.connecteDeviee(&voieGare1);
...
Ajoutons maintenant dans setup
le test des chemins vers toutes les voies de la gare que ce soit depuis l’entrée ou depuis la sortie.
Serial.begin(9600);
Serial.println("***** TEST");
/* Test des chemins */
Serial.println("===== Test de l'entree");
Serial.println("Voie gare 0");
voieEntree.creeCheminVers(VOIE_GARE_0);
Serial.println("Voie gare 1");
voieEntree.creeCheminVers(VOIE_GARE_1);
Serial.println("Voie gare 2");
voieEntree.creeCheminVers(VOIE_GARE_2);
Serial.println("Voie gare 3");
voieEntree.creeCheminVers(VOIE_GARE_3);
Serial.println("===== Test de la sortie");
Serial.println("Voie gare 0");
voieSortie.creeCheminVers(VOIE_GARE_0);
Serial.println("Voie gare 1");
voieSortie.creeCheminVers(VOIE_GARE_1);
Serial.println("Voie gare 2");
voieSortie.creeCheminVers(VOIE_GARE_2);
Serial.println("Voie gare 3");
voieSortie.creeCheminVers(VOIE_GARE_3);
L’exécution conduit au résultat attendu comme vous pouvez le constater ci-dessous.
***** TEST
===== Test de l'entree
Voie gare 0
Aiguillage E0: Droit
Voie gare 1
Aiguillage E1: Devie
Aiguillage E0: Devie
Voie gare 2
Aiguillage E2: Devie
Aiguillage E1: Droit
Aiguillage E0: Devie
Voie gare 3
Aiguillage E2: Droit
Aiguillage E1: Droit
Aiguillage E0: Devie
===== Test de la sortie
Voie gare 0
Aiguillage S2: Droit
Aiguillage S1: Droit
Aiguillage S0: Devie
Voie gare 1
Aiguillage S2: Devie
Aiguillage S1: Droit
Aiguillage S0: Devie
Voie gare 2
Aiguillage S1: Devie
Aiguillage S0: Devie
Voie gare 3
Aiguillage S0: Droit
Comment cela fonctionne-il ?
Tous les chemins possibles sont explorés jusqu’à ce que la destination soit trouvée. Si nous prenons, par exemple, le chemin de la voieEntree
vers la voieGare1
, l’exécution du programme est la suivante :
-
voieEntree
demande àaiguillageE0
-
aiguillageE0
demande àvoieGare0
(droite) qui répondfalse
-
aiguillageE0
demande àaiguillageE1
(gauche)-
aiguillageE1
demande àaiguillageE2
(droite)-
aiguillageE2
demande àvoieGare3
(droite) qui répondfalse
-
aiguillageE2
demande àvoieGare2
(droite) qui répondfalse
-
-
aiguillageE1
demande àvoieGare1
(gauche) qui répondtrue
TROUVÉ !
-
- TROUVÉ !
-
- TROUVÉ !
Lors de l’exploration, la fonction membre creeCheminVers
des aiguillages affiche la position que doit prendre l’aiguillage pour conduire à la voie recherchée, le résultat est l’affichage que l’on voit ci-dessus.
Nous avons maintenant notre base pour automatiser notre gare cachée. Il nous manque la recherche d’une voie libre, ce qui n’est pas très compliqué à faire, la recherche d’une voie occupée, même chose, et l’intégration avec les capteurs et les actionneurs. Nous verrons tout ceci dans les articles qui suivent.