. Par : Pierre59. URL : https://www.locoduino.org/spip.php?article154
On va, dans cette série d’articles, développer pas à pas le noyau d’un programme de gestion de réseau en utilisant la programmation orientée objet en C++.
La programmation orientée objet peut se résumer en trois mots : encapsulation, héritage, polymorphisme. Il faut faire les trois choses pour vraiment faire de la programmation objet.
L’accent sera essentiellement mis sur la conception objet, les exemples seront donc écrits dans un C++ simplifié. Cela reste du vrai C++, mais pour pouvoir le compiler, il faudra ajouter quelques trucs techniques qui polluent un peu le programme et qui nuisent un peu à sa compréhension. On trouvera en fin d’article le programme C++ complet compilable avec aussi plus d’exemples.
Pour réaliser un programme de gestion de réseau à partir de ce noyau, il faut écrire des classes (en suivant les modèles joints), déclarer des objets, choisir les options désirées et écrire toute la partie d’interface avec le matériel : commande des aiguilles, des alimentations (analogiques ou digitales), ou des signaux, rétro-signalisation, gestion du TCO, gestion des bus, …
Pour bénéficier de toutes les possibilités du noyau, toutes les actions de l’utilisateur (commande d’aiguilles, d’itinéraires, de sens et vitesse des trains, ...) doivent êtres contrôlées par le programme avant leur application.
Ce noyau objet est très puissant et très facilement modifiable. On peut modifier le réseau facilement, sans tout remettre en cause dans le programme. On peut ajouter progressivement, suivant ses envies, des "gadgets" comme le suivi des trains (par exemple, afficher le nom du train sur le TCO, mais aussi le contrôle des sons pour les machines sonores, ..), la poursuite en canton (appelée aussi conduite sélective, elle concerne principalement le fonctionnement en analogique, un train étant contrôlé sur tout le réseau avec la même souris par commutation automatique des alimentations sur les zones), le cabsignal (l’affichage des signaux en cabine de conduite des machines, par exemple sur la souris qui affiche le signal réel que voit le train), etc …
Ce noyau objet est plutôt orienté vers la gestion de réseaux avec commande par itinéraires, signalisation, rétro-signalisation, circulation manuelle et automatique, .. ceci quelque soit leur taille. Mais il peut être aussi utilisé sur des réseaux simples en laissant tomber les parties optionnelles.
Comme le noyau est écrit en C++, il peut être utilisé avec un Arduino, un mini PC (genre PCduino) ou un vrai PC. Un Arduino UNO sera très vite insuffisant au niveau mémoire vive voire au niveau mémoire flash, un méga ou mieux un due est plus réaliste.
Première étape : l’encapsulation
La première chose à faire en programmation objet, c’est d’identifier les objets potentiels.
Dans notre cas, ceux ci ne manquent pas : aiguilles, zones, cantons, signaux, trains, TCOs, souris, …
Ensuite il faut écrire des classes pour chacun des objets retenus. Dans ce premier article on va s’intéresser aux objets aiguille et zone.
Les classes sont des modèles pour fabriquer des objets, à partir d’une classe on peut fabriquer autant d’objets que l’on veut, ces objets sont appelés instances de la classe. Par exemple si on a 10 aiguilles on fera 10 instances de la classe ’aiguille’ pour avoir 10 objets, un pour chaque aiguille.
On va commencer avec la classe aiguille, pour cela on écrit une classe en C++ :
class Aiguille {};
Dans une classe les variables sont souvent appelées "attributs" et les fonctions "méthodes".
Il faut maintenant remplir la classe. Pour cela il faut se demander quelles sont les informations que l’on a besoin de savoir sur une aiguille et quelles sont les actions que l’on veut effectuer sur une aiguille.
Pour les informations, on a besoin de savoir si l’aiguille est en position directe ou en position déviée.
On peut faire cela avec deux méthodes à résultat booléen : directe() et deviee(), l’état de l’aiguille étant mémorisé dans une variable booléenne : état.
Pour les actions, on a besoin de manœuvrer l’aiguille, cela peut se faire avec deux méthodes : directer() et devier(). Ces méthodes ne font la manœuvre que si nécessaire, elles renvoient un booléen pour savoir comment cela s’est passé (true=OK). Bien évidemment ces méthodes changent l’état. La méthode : manoeuvrer() fera la commande effective (voir plus loin).
Pour rendre la classe utilisable, il faut une méthode : manoeuvrer() qui va faire l’interface avec le matériel. Cette méthode a besoin d’un numéro pour pouvoir commander le matériel (directement ou par un bus). On va donc rajouter à la classe un entier (ou plusieurs si besoin) qui est une sorte de numéro d’aiguille. La méthode : manoeuvrer() utilisera ce numéro pour manœuvrer effectivement l’aiguille. Cette méthode devra être complétée par l’utilisateur. Ce numéro d’aiguille va être donné en utilisant un constructeur.
Reste un problème d’initialisation de l’état. Deux cas se présentent, si on peut connaitre la position des aiguilles à l’initialisation du programme de gestion (dans le setup() ) alors on appelle la méthode : init1(), sinon il faut appeler la méthode : init2() qui force la position de l’aiguille en la manœuvrant.
On va profiter de la présence du constructeur pour ajouter une information optionnelle qui est la zone dans laquelle est l’aiguille. Cela peut être utile pour réaliser un PRS (Poste à Relais à transit Souple) où on a besoin de savoir si l’aiguille est manœuvrable (si la zone correspondante n’est pas occupée), la méthode : manoeuvrable() est prévue pour cela.
De façon générale les objets seront manipulés par le biais de pointeurs. Cela permet de profiter des mécanismes objets que nous utiliserons par la suite. Manipuler les objets (ou les structures) avec des pointeurs c’est normal en C++, il existe d’ailleurs un opérateur( -> ) prévu pour cela.
Ce qui donne :
class Aiguille {
boolean etat; // directe (true) ou déviée (false)
int no; // numéro de l'aiguille
Zone* zone; // la Zone ou est l'aiguille (optionnel)
Aiguille(int n) { no=n; } // constructeur mini
Aiguille(int n,Zone* z) { no=n; zone=z; } // constructeur maxi
boolean directe() { return etat; } // méthode d’accès
boolean deviee() { return !etat; } // méthode d’accès
boolean directer() {
if (directe()) return true;
etat=true;
return manoeuvrer(true);
}
boolean devier() {
if (deviee()) return true;
etat=false;
return manoeuvrer(false);
}
boolean manoeuvrable() { return zone->libre(); } // (optionnel)
boolean manoeuvrer(boolean e) { // commande de l'aiguille avec le numéro
return true; // return false si cela c'est mal passe
}
void init1(boolean e) { etat=e; }
void init2(boolean e) { if (e) commander(e); else commander(!e); etat=e; }
};
Voici deux exemples d’aiguilles (instanciations) :
Aiguille* a1=new Aiguille(1); // aiguille N°1
Aiguille* a2=new Aiguille(2, z9); // aiguille N°2 dans la zone z9
Maintenant passons à la classe Zone.
Comme pour Aiguille, la zone a deux états : libre et occupé, un booléen convient donc.
A l’instar d’Aiguille, on va avoir deux méthodes, libre() et occupee() pour tester l’état. Deux méthodes occuper() et liberer(), pour modifier l’état, ces deux méthodes doivent êtres appelées par la rétro-signalisation, une table de pointeurs sur les zones facilitera la tâche.
Ces deux méthodes appellent les méthodes actions() et desactions() qui sont destinées à faire les actions spécifiques à une zone particulière à l’occupation de la zone particulière et à sa libération.
class Zone {
boolean etat; // libre (false) ou occupé (true)
boolean occupee() { return etat; } // méthode d’accès
boolean libre() { return !etat; } // méthode d’accès
void occuper() { // appelée par la rétro-signalisation
// fait tout ce qu'il y a à faire en cas d'occupation (actions communes à toutes les zones)
actions(); // fait les actions spécifiques à une zone
}
void liberer() { // appelée par la rétro-signalisation
// fait tout ce qu'il y a à faire en cas de libération (actions communes à toutes les zones)
desactions(); // fait les actions spécifiques à une zone
}
void actions() {} // les actions spécifiques à faire en cas d'occupation
void desactions() {} // les actions spécifiques à faire en cas de libération
void init(boolean e) { etat=e; }
};
Les deux méthodes : occuper() et liberer(), font tout ce qu’il y a faire lors de l’occupation d’une zone et de sa libération. Pour l’instant c’est presque vide mais il faudra faire la mise à jour du TCO, le suivi des trains, le cabsignal, etc …
Ces deux méthodes font tout ce qu’il y a de commun à toutes les zones. Mais pour chaque zone il y a des choses particulières à faire : aubiner les signaux, gérer le BAL, gérer les itinéraires. … .
Cela va être le rôle de deux méthodes, la méthode : actions() qui fait les actions à faire lors de l’occupation et : desactions() lors de la libération.
Comme pour aiguille il reste un problème d’initialisation de l’état. A l’initialisation du programme de gestion (dans le setup() ) la rétro-signalisation doit appeler, pour chaque zone, la méthode : init() pour indiquer l’état initial de la zone.
Deuxième étape : l’héritage
Bien que complète la classe zone n’est pas utilisable, d’autant que les méthodes : actions() et : desactions() ne font rien.
Lors de l’occupation et de la libération d’une zone, outre des actions communes à toutes les zones, en pratique il y a des actions spécifiques à chaque zone à faire : aubiner les signaux, gérer le BAL, gérer les itinéraires. …
C’est le rôle des deux méthodes : actions() et : desactions(). Pour cela on pourrait utiliser deux pointeurs sur des fonctions passés en paramètres du constructeur, et utilisés dans ces méthodes mais il y a une façon plus naturelle en programmation objet, c’est l’héritage.
Pour chaque zone du réseau on va écrire une classe spécifique qui héritera de la classe de base Zone et qui va redéfinir les méthodes : actions() et : desactions() pour leur faire faire les actions propres à chaque zone.
class Z8 : Zone { // héritage de Zone
void actions() { … } // les actions spécifiques à faire en cas d'occupation
void desactions() { … } // les actions spécifiques à faire en cas de libération
};
class Z9 : Zone { // héritage de Zone
void actions() { … } // les actions spécifiques à faire en cas d'occupation
void desactions() { … } // les actions spécifiques à faire en cas de libération
};
Pour faire des choses plus intéressantes il faut enrichir la classe. Deux choses sont bien utiles, d’une part les deux signaux éventuels implantés sur la zone. D’autre part l’accès à la zone suivante et à la zone précédente.
Pour les signaux c’est facile, on ajoute deux variables de type pointeur de signal (on verra les signaux dans un prochain article), un constructeur à deux paramètres (pointeur de signal) et deux méthodes d’accès, les sens sont notés comme à la SNCF (pair et impair), en l’absence d’un signal on met NULL (le pointeur vide) :
class Zone { // les ajouts a la classe zone
…
Signal* signalPair; // les signaux éventuels de la zone
Signal* signalImpair;
Zone(Signal* sp,Signal* si) { // constructeur
signalPair=sp;
signalImpair=si;
}
Signal* signalPair() { return signalPair; } // méthode d'accès
Signal* signalImpair() { return signalImpair; } // méthode d'accès
…
};
Pour les zones suivantes et précédentes on pourrait aussi envisager de les passer au constructeur mais dans notre cas deux problèmes empêchent d’avoir recours à cette technique.
Le premier problème est que les zones s’appellent mutuellement : Par exemple, si on a deux zones z1 et z2 la suivante paire de z1 peut être z2 et la suivante impaire de z2 être z1, c’est impossible à déclarer, car il faudrait déclarer z1 avant z2 et z2 avant z1 !
Le deuxième problème vient du fait que les zones suivantes dépendent éventuellement de la position d’une (ou plusieurs) aiguille. Cela reflète l’aspect dynamique du réseau et cet aspect est essentiel pour la modélisation.
On ne peut donc pas passer par le constructeur directement. On pourrait envisager de passer des pointeurs sur des fonctions, mais il est plus pratique d’utiliser l’héritage d’autant plus que l’on a déjà commencé.
On va donc pour chaque zone réelle compléter la classe spécifique déjà écrite en lui ajoutant deux méthodes : suivantePaire() et : suivanteImpaire(). Ces deux noms de méthode sont préférés aux noms suivante() et precedente() car il ne dépendent pas du sens de circulation.
Pour que l’héritage et les mécanismes objets se passent bien en C++ (on en parlera dans le prochain article), il faut ajouter aussi ces méthodes à la classe de base Zone. Il faut aussi, pour toutes les méthodes qui sont redéfinies ainsi, mettre le mot clé virtual devant les méthodes (actions(), desactions(), suivantePaire(), suivanteImpaire() ).
Il ne reste plus qu’a tout réunir, la classe de base zone :
class Zone {
boolean etat; // libre (false) ou occupe (true)
Signal* signalPair; // les signaux éventuels de la zone
Signal* signalmpair;
Zone(Signal* sp,Signal* si) { // constructeur
signalPair=sp;
signalImpair=si;
}
boolean occupee() { return etat; } // méthode d’accès
boolean libre() { return !etat; } // méthode d’accès
void occuper() { // appelée par la rétrosignalisation
// fait tout ce qu'il y a à faire en cas d'occupation
// (actions communes à toutes les zones)
actions(); // fait les actions spécifiques à une zone
}
void liberer() { // appelée par la rétrosignalisation
// fait tout ce qu'il y a à faire en cas de libération
// (actions communes à toutes les zones)
desactions(); // fait les actions spécifiques à une zone
}
virtual void actions() {} // les actions spécifiques à faire en cas d'occupation
virtual void desactions() {} // les actions spécifiques à faire en cas de libération
Signal* signalPair() { return signalPair; } // méthode d'acces
Signal* signalImpair() { return signalImpair; } // méthode d'acces
virtual Zone* suivantePaire() { return NULL; } // la zone suivante paire (éventuellement vide)
virtual Zone* suivanteImpaire() { return NULL; } // la zone suivante impaire (éventuellement vide)
void init1(boolean e) { etat=e; } // initialisation
Zone* selonAiguille(Aiguille* a,Zone* z1,Zone* z2) { // méthode utilitaire
return a->directe()?z1:z2;
}
};
La méthode utilitaire selonAiguille() de la classe zone permet de prendre en compte l’aspect dynamique du réseau en fonction de la position réelle des aiguilles, cet aspect est essentiel au fonctionnement du gestionnaire de réseau.
Exemples de zones réelles (les exemples sont là pour montrer tous les cas caractéristiques d’utilisation, mais ils ne correspondent à aucun réseau concret) :
class Z8 : Zone { // héritage de Zone
Z8(Signal* sp,Signal* si):Zone(sp,si) {} // constructeur
virtual void actions() { c2.aubiner(); }
// les actions spécifiques à faire en cas d'occupation
virtual void desactions() { }
// les actions spécifiques à faire en cas de libération
virtual Zone* suivantePaire() { return NULL; }
//pas de zone suivante paire
virtual Zone* suivanteImpaire() { return z9; }
// la zone suivante impaire
} ;
class Z9 : Zone { // héritage de Zone
Z9(Signal* sp,Signal* si):Zone(sp,si) {} // constructeur
virtual void actions() {s2.aubiner(); }
// les actions spécifiques à faire en cas d'occupation
virtual void desactions() { … }
// les actions spécifiques à faire en cas de libération
virtual Zone* suivantePaire() { return selonAiguille( a1,z8,z9 }
// la zone suivante paire depends de a1
virtual Zone* suivanteImpaire() { return selonAiguille( a1,selonAiguille( a2,z8,z9),z9; }
// la zone suivante impaire depend de a1 et a2
};
Exemples de déclarations d’objets signaux, zones, aiguilles :
Signal* c2=new C2(); // signaux
Signal* s2=new S2();
Zone* z8=new Z8(s2,c2); // zones
Zone* z9=new Z9(s2,NULL);
Aiguille* a1=new Aiguille(1,z8); // aiguilles
Aiguille* a2=new Aiguille(2,z9);
Zone* tableZones[] {z8,z9} // la table des zones pour la retrosignalisation
L’exemple précédent n’est pas directement compilable, il faut mettre des protections (mots clés : public, protected, …), il faut respecter un ordre très précis des déclarations de classes et de variables car les objets s’utilisent les uns les autres.
Les méthodes qui sont écrites complètement dans la classe sont réputées "inline", c’est à dire que le compilateur ne fait pas un vrai appel de méthode mais met les instructions de la méthode directement dans le programme. C’est très bien pour les méthodes courtes comme les méthodes d’accès ou certaines méthodes utilitaires. Pour les autres il faut juste les spécifier dans la classe et les écrire en dehors de la classe en rappelant le nom de la classe. Dans notre cas cela facilite la prise en compte des inextricables problèmes avec l’ordre des déclarations.
On trouvera ci dessous un exemple compilable plus complet, on a rajouté tout ce qu’il faut pour que cela soit compilable et ajusté l’ordre des déclarations qu’il faut scrupuleusement respecter pour que cela reste compilable.
On a aussi diversifié les exemples, ajouté la possibilité de donner des noms et des types aux objets avec les constructeurs nécessaires. Il y a aussi des choses (variables et méthodes) qui seront utilisées dans les prochains articles.
Dans les articles suivant on verra les classes Signal, Train, Itinéraire ainsi que des gadgets comme le suivi des trains, la poursuite en canton, le cabsignal, …
Si vous souhaitez tester cette première partie avec une modélisation réelle de votre propre réseau, vous pouvez suivre le fil « modélisation logicielle d’un réseau, ici : http://forum.locoduino.org/index.ph...