LOCODUINO

Un gestionnaire en C++ pour votre réseau

Un gestionnaire en C++ pour votre réseau (2)

Signaux

.
Par : Pierre59

DIFFICULTÉ :

Après les classes aiguille et zone on va aborder la classe signal ou plutôt les classes signaux. Les signaux sont très polymorphes (il y a beaucoup de variétés) et leur mode de fonctionnement est très varié. L’héritage, et le polymorphisme qui en découle, vont permettre de prendre en compte relativement facilement les variantes rencontrées dans la signalisation SNCF réelle. Cela va permettre aussi de monter les mécanismes du polymorphisme, et il y en a de très subtils. Il ne faut pas s’inquiéter, ces mécanismes du polymorphisme font, automatiquement, ce qu’on aimerait bien que cela fasse et même au delà des espérances.

On distinguera ici deux catégories de signaux, ceux sans BAL (Bloc Automatique Lumineux) et ceux avec BAL.
Pour pouvoir mélanger des signaux avec et sans BAL (utile pour les réseaux ayant des parties avec BAL et des parties sans) le fonctionnement des deux catégories sera identique, on aura juste des méthodes pour ouvrir ou fermer les signaux, tout le reste sera automatique, c’est à dire que le calcul des feux et la présentation des cibles pour les signaux mécaniques sera fait automatiquement en fonction de l’environnement (autres signaux, aiguilles, zones, …). L’aubinage est aussi pris en compte. L’aubinage est la fermeture automatique d’un signal lors de son franchissement par un train (le nom vient de l’invention, par Mr Aubine, de la pédale mécanique qui fermait les signaux mécaniques au passage d’un train, et le nom est resté pour les signaux lumineux) .

Des objets signaux sont nécessaires dans le gestionnaire pour assurer la sécurité, c’est par leur biais que les arrêts et ralentissements des trains sont gérés. Par contre les signaux réels ne sont là que pour faire joli, mais ils sont pris en compte (aussi bien les lumineux que les mécaniques).

Les signaux lumineux peuvent présenter différents feux, en voici quelques uns (avec les abréviations utilisées) :

  • voie libre (Vl) : feu vert
  • avertissement (A) : feu jaune
  • sémaphore (S) : feu rouge
  • carré (C) : deux feux rouges
  • carré violet (Cv) : feu violet
  • manœuvre (M) : feu blanc
  • ralentissement (R) : deux feux jaunes horizontaux
  • rappel de ralentissement (RR) : deux feux jaunes verticaux
  • disque (D) : un feu jaune et un feu rouge

Certains feux peuvent êtres clignotants (Vlc,Ac,Sc,Mc,Rc,RRc). Certains signaux peuvent aussi présenter un petit feu blanc appelé œilleton.

Les signaux mécaniques peuvent présenter différentes cibles, en voici quelques unes (avec les abréviations utilisées) :

En plus des cibles, les signaux mécaniques présentent aussi des feux ; quand aucune cible n’est présentée le feu est à voie libre (Vl).

Signaux sans BAL

Commençons par la première catégorie, les signaux sans BAL. Il y a pas mal de variété dans cette catégorie, voici une liste non exhaustive (avec les feux) :

  • carre violet (Cv M)
  • avertissement (A Vl)
  • avertissement avec ralentissement (A Vl R)
  • carré (C Vl)
  • carré avec avertissement (C A Vl)
  • carre avec rappel de ralentissement (C Vl RR)
  • carre avec avertissement et rappel de ralentissement (C A Vl RR)
  • sémaphore (S Vl)
  • disque (D Vl)

Ces signaux pouvant êtres mécaniques ou lumineux. Les carrés avec « avertissement » réalisent l’avertissement du carré suivant.

On va détailler ici les classes pour quelques signaux manuels (on en trouvera d’autres dans les fichiers joints). Ces signaux sont judicieusement choisis pour illustrer les mécanismes de l’héritage et du polymorphisme, tout en montrant la facilité de mise en oeuvre (je n’ose imaginer comment on ferait en programmation classique !).

Comme pour la classe zone on va écrire une classe signal qui regroupe tout ce qui est commun à tous les signaux. Il faut commencer par coder les feux, il y a beaucoup de feux différents. Une façon bien pratique est d’écrire une énumération, en s’arrangeant pour que chaque feu soit codé sur un bit, le numéro de chaque feu est alors une puissance de 2 (1, 2, 4, 8, 16, 32, 64, …) qui peut aussi s’écrire en utilisant l’opérateur de décalage à gauche (1<<0, 1<<1, 1<<2, 1<<3, 1<<4, …), ceci pour faciliter l’utilisation :

enum { E=0,  // eteint
  Vl=1<<0, A=1<<1, S=1<<2, C=1<<3, R=1<<4, RR=1<<5, M=1<<6, Cv=1<<7,  
  Vlc=1<<8, Ac=1<<9, Sc=1<<10, Rc=1<<11, RRc=1<<12, Mc=1<<13, D=1<<14, X=1<<15
}; 

Il est prévu presque tous les feux (SNCF), même les clignotants (avec un petit c). Il y a 15 feux possibles plus un libre (X), cela tient dans un type int. Pour la portabilité on déclare un type spécial sur 16 bits donc un int pour l’Arduino :

typedef unsigned int typeFeux; // 16 cas possibles ( 32 avec un long )

Outre les feux présentés par le signal, on a besoin d’un entier pour l’interface avec l’électronique (commande des feux et/ou des moteurs), plus en option un nom (pour la mise au point) et un type (pour la mise au point et pour distinguer éventuellement les mécaniques des lumineux, ou d’autres usages). Cela donne :

class Signal {
  typeFeux feux; // les feux
  int no; // numero du signal
  char* nom;  // le nom du signal   OPTION
  int type; // le type du signal   OPTION
};

Il faut ensuite une méthode d’accès et des méthodes de test :

  typeFeux getFeux() { return feux; } // obtention des feux
  boolean ferme() { return feux&(S|C|Cv); } // test signal ferme
  boolean ouvert() { return !ferme(); } // test signal ouvert

Il faut aussi des méthodes pour ouvrir et fermer les signaux ainsi que les aubiner :

  virtual void ouvrir() {} // ouverture manuelle 
  virtual void fermer() {} // fermeture manuelle 
  virtual void aubiner() { fermer(); } // fermeture automatique (retrosignalisation)

Ces méthodes seront redéfinies (si besoin) dans les classes des signaux particuliers (d’où le mot clé virtual).

Pour gérer les ralentissements (à 30 ou 60km/h ) il faut des méthodes :

  virtual boolean ralentissement30() { return false; } // true si ralentissement a 30
  virtual boolean ralentissement60() { return false; } // true si ralentissement a 60

Ces méthodes seront redéfinies (si besoin) dans les classes des signaux particuliers.

Pour assurer l’automatisme du calcul des feux, il faut pouvoir, en général, accéder aux signaux précédents et suivants. Il faut aussi, quand un signal est manœuvré (manuellement ou automatiquement), qu’il avertisse le précédent pour qu’il mette à jour ses feux, c’est le rôle de la méthode majFeux() (Mise A Jour Feux) :

  virtual Signal* suivant()   { return NULL; } // le signal suivant
  virtual Signal* precedent() { return NULL; } // le signal precedent

  virtual void majFeux() {} // appelee par le signal suivant

Ces méthodes seront redéfinies (si besoin) dans les classes signaux particulières.

Pour finir il faut une méthode pour manœuvrer le signal, cette méthode fait l’interface avec l’électronique (leds, moteurs, …). Elle doit être complétée par l’utilisateur (en utilisant la variable no) :

void manoeuvrer() { … } // manoeuvre du signal

Cette méthode peut aussi gérer la zone d’arrêt (si elle existe).

La classe Signal comporte aussi quelques méthodes utilitaires et des méthodes en option.

Voici la classe Signal complète :

typedef unsigned int typeFeux; // 16 cas possibles ( 32 avec long )

// les feux possibles
enum { E=0,  // eteint
  Vl=1<<0, A=1<<1, S=1<<2, C=1<<3, R=1<<4, RR=1<<5, M=1<<6, Cv=1<<7,  
  Vlc=1<<8, Ac=1<<9, Sc=1<<10, Rc=1<<11, RRc=1<<12, Mc=1<<13, D=1<<14, X=1<<15
};

enum {MECANIQUE,LUMINEUX,BAS,BAL, ... }; // des types de signaux
  
class Signal {
  typeFeux feux; 
  int no; // numero du signal
  char* nom; // le nom du signal   OPTION
  int type; // le type du signal   OPTION
   
  Signal(typeFeux f,int n) { feux=f; no=n; } // constructeur mini
  Signal(int t,typeFeux f,int n) { feux=f; no=n; } // constructeur 
  Signal(char* nm,int t,typeFeux f,int n) { nom=nm; type=t; feux=f; no=n; } // constructeur maxi
  
  typeFeux getFeux() { return feux; } // methode d'acces    
  
  boolean ferme() { return feux&(S|C|Cv); } // methode de test
  boolean ouvert() { return !ferme(); }     // methode de test
  
  virtual void ouvrir() {} // ouverture manuelle 
  virtual void fermer() {} // fermeture manuelle 
  virtual void aubiner() { fermer(); } // fermeture automatique (retrosignalisation)
  
  void manoeuvrer() { ... } // manoeuvre effective du signal 

  virtual boolean ralentissement30() { return false; } // true si ralentissement a 30
  virtual boolean ralentissement60() { return false; } // true si ralentissement a 60

  virtual void majFeux() {} // mise a jour du signal precedent
  
  virtual Signal* suivant()   { return NULL; } // le signal suivant  
  virtual Signal* precedent() { return NULL; } // le signal precedent  
  
  Signal* selonAiguille(Aiguille* a,Signal* s1,Signal* s2) { return a->directe()?s1:s2; } 
};  

La méthode utilitaire selonAiguille() facilite le choix des signaux suivants. On trouvera aussi dans les fichiers joints des méthodes pour gérer la présentation des cibles pour les signaux mécaniques.

Quelques signaux sans BAL

Avec cette classe de base, on peut maintenant écrire quelques classe de signaux particuliers.

Carré violet

Commençons par un carré violet, c’est le signal le plus simple car il ne dépend pas d’autres signaux :

class CarreViolet:Signal {
  CarreViolet(int n):Signal(Cv,n) {} // constructeur mini
  CarreViolet(char* nm,int t,int n):Signal(nm,t,Cv,n) {} // constructeur maxi
  
  virtual void ouvrir() { feux=M;  manoeuvrer(); }
  virtual void fermer() { feux=Cv; manoeuvrer(); }
};

Cette classe hérite de la classe Signal. Deux constructeurs sont proposés, un constructeur minimum (juste ce qui est indispensable) et un constructeur maximum (avec les options : nom et type), l’utilisateur peut adapter les constructeurs en fonction de ses besoins.

Les méthodes redéfinies ouvrir() et fermer() modifient l’état du signal en deux phases :

  • calcul des feux (ici c’est facile)
  • manœuvre du signal (changement des allumages de leds et/ou commande des moteurs, contrôle de la zone d’arrêt).

En pratique (voir le fichier joint) un test pour savoir si le signal est déjà dans la bonne position est ajouté.

Cette classe peut être instanciée directement, pour chaque carré violet nécessaire :

Signal* cv3=new CarreViolet("Cv3",BAS,1); // constructeur maxi

Avertissement

Le signal d’avertissement est un peu plus compliqué car il dépend du signal suivant qui doit être un carré ou un sémaphore. Conformément aux choix qui ont été faits, le fonctionnement de ce signal est entièrement automatique, il est lié à son carré ou sémaphore, il n’y a donc pas de méthode de manœuvre manuelle (ouvrir() ou fermer()). Le signal peut être lumineux ou mécanique, il peut être aubiné :

class Avertissement:Signal { 
  Avertissement(int n):Signal(A,n) {} // constructeur mini   
  Avertissement(char* nm,int t,int n):Signal(nm,t,A,n) {} // constructeur maxi   
  
  virtual void aubiner() { // appelé par les méthodes actions de zone  
    feux=A;  
    manoeuvrer();
  } 

  virtual void majFeux() { // appelé par le signal suivant           
   if (suivant()->ferme()) feu=A; 
   else feu=Vl; 
   manoeuvrer(); 
  } 
};

On retrouve deux variantes du constructeur, une méthode aubiner() (fermeture automatique). La méthode majFeux() est nouvelle, elle est appelée par le signal suivant lors d’un changement de celui-ci, fait le calcul des feux et la manœuvre du signal (leds et/ou moteur).

En pratique (voir le fichier joint), un test est effectué sur le suivant, s’il est NULL une erreur est déclenchée, c’est à l’utilisateur d’afficher un message. Ce test est indispensable pour éviter d’inextricables erreurs à l’exécution. Un test est aussi effectué pour n’appeler la méthode manoeuvrer() que si c’est nécessaire.

Pour utiliser cette classe, comme on le fait pour la classe Zone, il faut écrire une nouvelle classe héritant de la classe Avertissement et redéfinir la méthode suivant(), l’écriture d’un ou plusieurs constructeurs est aussi nécessaire. Exemple :

class A09: Avertissement { // attention aux pins analogiques A0, A1, A2, …
  A09(int n):Avertissement(n) {} // constructeur mini
  A09(char* nm,int t,int n):Avertissement(nm,t,n) {} // constructeur maxi

  virtual Signal* suivant() { return c2; }
};

Exemple d’utilisation :
Signal* a9=new A09("A9",MECANIQUE,11);

Carré

Le signal carré est simple :

class Carre:Signal {
  Carre(int n):Signal(C,n) {} // constructeur mini
  Carre(char* nm,int t,int n):Signal(nm,t+CARRE,C,n) {} // constructeur maxi
  
  virtual void ouvrir() { feux=Vl; manoeuvrer(); precedent()->majFeux(); }    
  virtual void fermer() { feux=C; manoeuvrer(); precedent()->majFeux(); } 
};

Outre deux variantes de constructeurs, il y a deux méthodes pour manœuvrer manuellement le carré, ouvrir() et fermer(). Ces deux méthodes font la manœuvre du signal d’avertissement précédent en appelant la méthode majFeux() sur l’avertissement.

En pratique, comme pour la classe précédente, des tests sont ajoutés pour éviter des manœuvres inutiles ou des pointeurs NULL. Une méthode utilitaire (de la classe Signal) manoeuvreEtMajFeux() est utilisée pour simplifier les écritures.

Pour utiliser cette classe, toujours la même technique, écrire une classe qui hérite de la classe Carre et qui redéfinit la méthode precedent(). Exemple :

class C2:Carre {
  C2(int n):Carre(n) {} // constructeur mini
  C2(char* nm,int t,int n):Carre(nm,t,n) {} // constructeur maxi

  virtual Signal* precedent() { return a9; }
};

Exemple d’utilisation :
Signal* c2=new C2(3);

Carré avec rappel de ralentissement

Le carré avec rappel de ralentissement est une variante de carré. Tout naturellement, en programmation objet, on écrit une classe qui hérite de Carre et qui ne redéfinit que les méthodes nécessaires, ici c’est la méthode ouvrir() :

class CarreRappelRalentissement:Carre {
  CarreRappelRalentissement(int n):Carre(n) {} // constructeur mini
  CarreRappelRalentissement(char* nm,int t,int n):Carre(nm,t,n) {} // constructeur maxi

  virtual void ouvrir() { 
    if (ralentissement60()) feux=RRc; else
    if (ralentissement30()) feux=RR; 
    else                    feux=Vl;
    manoeuvrer(); 
    precedent()->majFeux();
  } 
};

La méthode ouvrir() fait le calcul des feux, les méthodes booléennes ralentissement60() et ralentissement30() sont héritées de Signal, elles retournent false (par défaut). Comme pour la classe Carre, pour utiliser la classe CarreRappelRalentissement il faut écrire une nouvelle classe qui redéfinit la méthode precedent() et qui redéfinit une des méthodes ralentissement60() et ralentissement30() (ou les deux). Exemple :

class C3:CarreRappelRalentissement {
  C3(int n):CarreRappelRalentissement(n) {} // constructeur mini
  C3(char* nm,int t,int n):CarreRappelRalentissement(nm,t,n) {} // constructeur maxi

  virtual Signal* precedent() { return c2; }
  virtual boolean ralentissement30() { return aig1->deviee(); }  // si l'aig1 est deviee
};

Exemple d’utilisation :

Signal* c3=new C3(5);

Arbitrairement l’information de ralentissement est attachée au carré rappel de ralentissement. Bien évidemment le ralentissement sera affiché sur le signal précédent qui doit pouvoir présenter le ralentissement (avertissement avec ralentissement ou carre avec ralentissement).

On trouvera dans le fichier signaux.h les SNCF les plus courants sans BAL.

Signaux avec BAL

La deuxième catégorie est celle des signaux avec BAL, ce sont uniquement des signaux lumineux, pouvant tous présenter le sémaphore, l’avertissement et la voie libre. Voici un liste non exhaustive (avec les feux) :

  • sémaphore (S A Vl)
  • sémaphore avec ralentissement (S A Vl R)
  • carré (C S A Vl)
  • carre avec ralentissement (C S A Vl R)
  • carre avec rappel de ralentissement (C S A Vl RR)

On trouve aussi souvent des carrés pouvant présenter un feu de manœuvre (M) qui ne sont pas prévus ici mais que l’on pourrait facilement prendre en compte.

Pour ne pas alourdir la classe Signal une classe SignalBAL héritant de la classe Signal est écrite, cette classe comprend ce qui est spécifique aux signaux de BAL :

class SignalBAL:Signal {
  boolean modePermanent=false; // ouverture permanente
  
  SignalBAL(typeFeux f,int n):Signal(f,n) {} // constructeur mini
  SignalBAL(int t,typeFeux f,int n):Signal(t,f,n) {} // constructeur 
  SignalBAL(char* nm,int t,typeFeux f,int n):Signal(nm,t,f,n) {} // constructeur maxi

  virtual void aubiner() {} // fermeture automatique par retrosignalisation
  virtual void desaubiner() {} // ouverture automatique par retrosignalisation   

  virtual typeFeux calculFeux() { return E; } // calcul des feux

  void setPermanent(boolean b) { modePermanent=b; } // ouverture permanente
  
  virtual boolean cantonOccupe() { return false; } // occupation du "canton" 
};

Le gestion automatique des signaux se fait par l’appel des méthodes aubiner() et desaubiner(). Ces méthodes sont appelées par la rétrosignalisation. La méthode cantonOccupe() permet de tester l’occupation du "canton".

Pour pouvoir utiliser les méthodes de SignalBAL il faut déclarer les signaux de BAL du type SignalBAL et non Signal, sinon il y aura des erreurs de compilation.

Sémaphore

Commençons par le plus simple, le sémaphore de BAL. Le fonctionnement de ce signal est entièrement automatique sans aucune action manuelle, il n’y a donc pas de méthode ouvrir() ou fermer(), la fermeture et l’ouverture du signal se font par la rétrosignalisation, par le biais des méthodes aubiner() et desaubiner(). Le calcul des feux devant être fait dans deux méthodes, une méthode annexe calculFeux() est introduite :

class SemaphoreBAL:SignalBAL { 
  SemaphoreBAL(int n):Signal(Vl,n) {} // constructeur mini
  SemaphoreBAL(char* nm,int t,int n):Signal(nm,t+BAL,Vl,n) {} // constructeur maxi
 
  virtual void aubiner() {// appelé par les methodes actions de zone 
    feu=S; manoeuvrer(); precedent()->maj(); 
  } 
    
  virtual void desaubiner() {  // appelé par les methodes des actions de zone
    feux=calculFeux(); manoeuvrer(); precedent()->maj(); 
  }
 
  virtual typeFeux calculFeux() { // feux a l'ouverture                                                    
     if (cantonOccupe()) return S; // "canton" occupe ( precaution )
     if (suivant()->ferme()) return A; else return Vl; 
  }

  void majFeux() { Signal* s; typeFeux f; // appelé par le signal suivant  
    feux=calculFeux(); manoeuvrer(); precedent()->maj();  } 
  }
  
  virtual boolean cantonOccupe() { return true; } // test d'occupation du "canton"
};

Normalement la fermeture du signal se fait lors de l’occupation de la zone suivant le signal par la méthode aubiner() et l’ouverture se fait lors de la libération de la dernière zone du "canton" par la méthode desaubiner(). C’est la rétrosignalisation qui contrôle cela, suivant le cheminement suivant :

De même pour l’ouverture :

Pour palier les anomalies de circulation de nos trains (ruptures d’attelages, mise ou retrait de matériels sur une zone sans précautions, …), une vérification de l’occupation du "canton" est faite par le biais de la méthode cantonOccupe().

Comme d’habitude, pour un signal particulier il faut écrire une classe qui hérite de SemaphoreBAL, et qui redéfinit les méthodes nécessaires :

class S5:SemaphoreBAL{
  S5(int n):SemaphoreBAL(n) {} // constructeur mini
  S5(char* nm,int t,int n):SemaphoreBAL(nm,t,n) {} // constructeur maxi

  virtual Signal* suivant() { return c3; }
  virtual Signal* precedent() { return c2; }
  virtual boolean cantonOccupe() { return occupees(z8,z9); }
};

Exemple d’utilisation :
Signal* s5=new S5("S5",BAL,3);

Carré

Passons, comme avant dernier exemple, à un carré avec BAL. Cela peut sembler curieux à certains, mais c’est nécessaire dans les postes d’aiguillage du type PRS (Poste tout Relais à transit Souple) et dans le cas d’itinéraires permanents. Le fonctionnement de tels carrés est le suivant :

  • à l’ouverture (manuelle), calcul des feux comme pour un sémaphore de BAL (S possible)
  • à la fermeture (manuelle), mise au carré
  • à l’aubinage (automatique par occupation du "canton"), si l’ouverture est permanente, mise au sémaphore sinon mise au carré
  • au désaubinage (automatique par libération du "canton"), si l’ouverture est permanente, calcul des feux comme pour l’ouverture sinon rien à faire

On retrouve tout ce qu’il y avait dans sémaphore avec BAL, plus les ouvertures et fermetures manuelles et la gestion du mode permanent :

class CarreBAL:Signal { 
  CarreBAL(int n):Signal(C,n) {} // constructeur mini
  CarreBAL(char* nm,int t,int n):Signal(nm,t+BAL,C,n) {} // constructeur maxi
  
  virtual void ouvrir() { // ouverture manuelle     
    feux=calculFeux(); manoeuvrer(); precedent()->maj(); 
  }
  
  virtual void fermer() { // fermeture manuelle
    feux=C; manoeuvrer(); precedent()->maj(); 
  }
  
  virtual void aubiner() { // fermeture automatique (retrosignalisation)
    if (modePermanent) feux=S; else feux=C; 
    manoeuvrer(); precedent()->maj();
  }

  virtual void desaubiner() { // ouverture automatique (retrosignalisation)
    if (modePermanent) feux=calculFeux(); else return;
    manoeuvrer(); precedent()->maj(); 
  }

 virtual typeFeux calculFeux() { // feux a l'ouverture
    if (cantonOccupe()) return S; // "canton" occupe ( precaution )
    if (suivant()->ferme()) return A; else return Vl; 
 }

 virtual void majFeux() { // appelé par le signal suivant
    if (ferme()) return; 
    feux=calculFeux(); 
    manoeuvrer(); 
    precedent()->maj(); 
  }
};

Comme d’habitude il faudra écrire une classe, héritant de CarreBAL, pour chaque signal particulier.

Carre rappel de ralentissement

Pour finir en beauté, un carré avec BAL et rappel de ralentissement, qui, bien évidemment, hérite de CarreBAL :

class CarreBALRappelRalentissement: CarreBAL {
  CarreBALRappelRalentissement(int n):CarreBAL(n) {} // constructeur mini
  CarreBALRappelRalentissement(char* nm,int t,int n):CarreBAL(nm,t+BAL,n) {} // constructeur maxi
  
  virtual typeFeux calculFeux() { // feux a l'ouverture                                                                
    if (ralentissement60()) if (suivant()->ferme()) return A+RRc; else return RRc; else
    if (ralentissement30()) if (suivant()->ferme()) return A+RR;  else return RR;  
    else                    if (suivant()s->ferme()) return A;     else return Vl;
  }
};

Seule la méthode calculFeux() doit être redéfinie. Un exemple complexe de signal particulier, pour bien montrer toutes les possibilités :

class C6:CarreBALRappelRalentissement {
  C6(int n):CarreBALRappelRalentissement(n) {} // constructeur mini
  C6(char* nm,int t,int n):CarreBALRappelRalentissement(nm,t,n) {} // constructeur maxi

  virtual Signal* suivant() { return selonAiguille(aig2,c2,c3); } // selon aig2
  virtual Signal* precedent() { return c2; }

  virtual boolean cantonOccupe() { // canton dynamique
    if (aig1->directe()) return occupee(z10); 
    return occupees(z8,z9); 
  } 

  virtual boolean ralentissement60() { return aig1->deviee(); } // si aig1 deviee
  virtual boolean ralentissement30() { return aig1->directe() && aig2->deviee(); } // si aig1 directe et aig2 deviee
};

Exemple d’utilisation :

SignalBAL* c6=new C6(5);

La méthode cantonOccupe() montre que l’on peut avoir des "cantons" dynamiques, les zones qui le composent dépendent de la position d’aiguilles. On peut aussi avoir un ralentissement à 30 et/ou à 60 suivant la position des aiguilles.

Polymorphisme

Il est intéressant ici de regarder, sur ce dernier exemple, comment fonctionnent les mécanismes objet. Regardons pour commencer les relations d’héritages :

  • C6 hérite de CarreBALRappelRalentissement qui hérite de CarreBAL qui hérite de SignalBAL qui hérite de Signal

Ces relations d’héritage constituent une branche de l’arbre formé par toutes les relations d’héritage. Voici l’arbre des signaux de cet article :

Arbre d’héritage des signaux

Les informaticiens ont l’habitude de représenter leurs arbres à l’envers, la racine est en haut, les feuilles sont en bas, la branche qui nous intéresse est la plus à droite.

Polymorphisme d’affectation

Premier constat, lors de l’instanciation du signal, la variable c6 est déclarée de type pointeur de SignalBAL et on lui affecte une instance d’une classe de type pointeur de CarreBALRappelRalentissement. On devrait plutôt écrire, pour respecter les types :

C6* c6=new C6(5);

Si, au niveau des déclarations c’est plutôt utilisé par "fainéantise", le mécanisme est essentiel dans les classes. Si on regarde bien les classes zones et signaux, dans les méthodes on a des paramètres ou des résultats de type Signal* et lors des appels de méthodes, dans les exemples, il n’y a jamais de pointeur de Signal (la classe n’est pas instanciable), mais ce sont toujours des pointeurs sur des classes héritant de Signal. C’est essentiel, parce que cela permet de faire des actions sur les signaux sans se préoccuper du type réel du signal, tous les signaux fonctionnent globalement de la même façon, c’est là toute la puissance de la programmation objet.

On peut affecter à un pointeur sur une classe de base, un pointeur sur une classe dérivée (par héritage).

Polymorphisme de méthode

Plus subtil, regardons ce qui se passe à l’exécution quand on ouvre le signal avec, par exemple, l’instruction :

c6->ouvrir();

Le type de c6 est pointeur de C6, classe qui n’a pas de méthode ouvrir(), on regarde alors dans la classe dont elle hérite CarreBALRappelRalentissement qui n’a pas de méthode ouvrir() non plus, on regarde alors dans la classe dont elle hérite CarreBAL, qui elle a une méthode ouvrir(). C’est celle là qui est utilisée, cette méthode fait plusieurs choses :

ouvrir() {   feux=calculFeux();   precedent()->maj();   manoeuvrer();    }

Détaillons :

  • Premièrement, quand la méthode calculFeux() est appelée, on est dans la méthode ouvrir() de CarreBAL, classe qui a une méthode calculFeux(), mais ce n’est pas celle là qui est utilisée car le type réel de la variable c6 est C6 qui n’a pas de méthode calculFeux(), mais la classe dont elle hérite CarreBALRappelRalentissement a une méthode calculFeux(), donc c’est celle qui est utilisée. C’est très subtil comme mécanisme, l’exécution de la méthode calculFeux() va utiliser les mêmes mécanismes et ainsi de suite (on ne détaillera pas plus, ce serait vite inextricable).

En résumé bien qu’étant dans la classe CarreBAL, on n’utilise pas sa méthode calculFeux() (qui ne gère pas le rappel de ralentissement) mais bien la méthode de calculFeux() de CarreBALRappelRalentissement (qui elle gère le rappel de ralentissement) et c’est bien cela que l’on veut.

  • Deuxièmement, quand la méthode precedent() est appelée, elle existe dans C6, donc c’est celle là qui est utilisée.
  • Troisièmement, quand la méthode majFeux() est appelée, comme elle n’existe pas dans C6, ni dans CarreBALRappelRalentissement, mais existe dans CarréBAL, donc c’est celle là qui est utilisée.
  • Finalement elle appelle la méthode manoeuvrer() qui n’existe pas dans C6, ni dans CarreBALRappelRalentissement, ni dans CarreBAL, ni dans SignalBAL, mais qui existe dans Signal, donc c’est celle là qui est utilisée.

C’est inextricable, je me suis trompé plusieurs fois en écrivant l’article, et on n’a pas regardé, pour simplifier, ce qui se passait dans les méthodes appelées dans les méthodes.

En pratique on écrit en respectant les règles simples de la programmation objet et c’est C++ qui fait ce qu’il faut à l’exécution, et il ne se trompe pas, lui. Pour bénéficier de ce mécanisme il suffit de mettre le mot clé virtual devant la méthode.

Si la méthode est virtuelle alors le choix de la méthode est fait à l’exécution en fonction du type réel de l’objet, sinon le choix est fait à la compilation en fonction du type déclaré de l’objet.

Avec ce mécanisme, dit de liaison dynamique ou liaison à l’exécution, tout se passe comme si à l’exécution, on partait du type réel de l’objet sur lequel la méthode est appliquée, et on recherchait dans l’arbre d’héritage en partant du type réel de l’objet, en remontant la branche vers la racine, la première occurrence de la méthode : c’est cette méthode qui est utilisée. On est sûr de trouver une méthode, car sinon il y aurait une erreur de compilation (en pratique le C++ utilise des tables de méthodes virtuelles).

Ce deuxième mécanisme de la programmation objet, complémentaire du premier, bien que plus subtil que le premier, est tout aussi essentiel et très puissant.

Bilan

Ces classes signaux sont assez compliquées, mais ce n’est pas la faute de la programmation objet, mais celle de la réglementation de la signalisation SNCF. A contrario, la programmation objet permet de prendre en compte relativement facilement toutes les contraintes de la signalisation.

Bien évidemment d’autres signaux sont possibles, français comme étrangers. Si je connais bien les subtilités de la signalisation française, je ne connais pas les signalisations étrangères et j’aurais du mal à écrire les classes correspondantes.

Les classes signaux sont assez compliquées et elles interfèrent beaucoup entre elles, certaines ont été testées (avec le locodrome), mais toutes n’ont pas été testées, il peut dont rester des erreurs.

Avec les classes aiguilles et zones, vues dans l’article précédent, et les classes signaux de cet article on a tout ce qu’il faut pour mettre en oeuvre facilement des itinéraires. Ce sera le sujet du prochain article et un exemple complet de mise en oeuvre sera aussi présenté (avec le locodrome).

Comme dans l’article précédent on trouvera dans le fichier ci-dessous toutes les classes bien écrites en C++, d’autres classes de signaux et d’autres exemples. Les classes comportent aussi des méthodes optionnelles (test de clignotement des feux, cibles à présenter, …).

Pour les signaux il y a quatre fichiers :

  • un fichier Signal.h qui contient la classe Signal
  • un fichier Signaux.h qui contient des signaux sans BAL
  • un fichier SignalBAL.h qui contient la classe SignalBAL
  • un fichier SignauxBAL.h qui contient des signaux avec BAL

Dans le prochain article, je terminerai avec les classes itineraire et une mise en oeuvre dans l’exemple du Locodrome.

En attendant relisez bien les articles 1 et 2 et rejoignez nous sur le forum :
Modélisation logicielle d’un réseau - le système de Pierre59

6 Messages

  • Un gestionnaire en C++ pour votre réseau (2) 4 janvier 2017 11:26, par Christian

    Bravo pour cet article qui nous fait bien toucher du doigt l’intérêt de l’héritage et du polymorphisme. Le fait qu’il n’y a eu aucun message depuis sa publication montre tout de même que le sujet est ardu, non pas par la programmation mais par le côté compliqué de la signalisation SNCF.
    C’est un sujet que je maitrise mal (malgré le cadeau d’un copain cadre à la SNCF : un livre de cours sur la signalisation, moins facile à lire qu’un polar !) : est-ce que le sujet est complètement traité par la rédaction de ce programme ou bien y a-t-il d’autres possibilités que peut rajouter (éventuellement) le programmeur ? (quand je dis complètement traité, je veux parler de tous les cas possibles pour un même signal parce que tu as bien dit qu’il y avait d’autres signaux possibles).

    Répondre

  • Un gestionnaire en C++ pour votre réseau (2) 4 janvier 2017 17:35, par Pierre59

    Bonjour

    Bien évidemment tous les signaux ne sont pas traités (on peut facilement en rajouter), mais pour certains signaux tous les cas possibles ne sont pas traités non plus. Par exemple les feux clignotants pour la voie-libre, l’avertissement et le sémaphore, cela impacte pas mal de signaux.

    En pratique seuls les signaux courants sont traités.

    Pierre

    Répondre

  • Un gestionnaire en C++ pour votre réseau (2) 15 août 2022 07:23, par Olivier

    Bonjour Pierre,
    A propos des signaux, j’ai une question métaphysique sur leur combinaison :)
    D’après le site référencé ci-dessous, on utilise par exemple le ralentissement 30 ou 60 (deux feux jaunes horizontaux en signalisation lumineuse) lorsqu’on prend un aiguillage en position déviée. Il indique aussi que ce signal peut être combiné avec d’autres signaux...Est-ce que ça veut dire qu’on aura aussi le feu vert si la voie est libre, ou bien le fait qu’elle soit libre est implicite ? (Je pencherais plutôt pour la deuxième option, car si j’ai bien compris le feu vert signifie qu’on peut rouler plein pot...)
    Dans le même esprit, si la voie suivante est au carré, le ralentissement à 30 ou 60 est-il suffisant ou bien faut-il le feu jaune d’avertissement en plus ? (Là aussi je pencherais pour la deuxième option, il y a ralentissement quoi qu’il arrive, mais le feu jaune apporte quand même une information supplémentaire non négligeable)
    Question subsidiaire : quid de la signalisation lorsque l’aiguillage est pris en talon sur sa partie déviée ? Par exemple, lorsqu’un train sort de la petite voie à l’intérieur de la boucle du locoduinodrome, quel signal voit-il ? Un feu de ralentissement (du fait qu’il franchit un aiguillage dévié) ou bien un feu de manoeuvre par exemple ?

    Voir en ligne : Signaux

    Répondre

  • Un gestionnaire en C++ pour votre réseau (2) 17 août 2022 14:26, par Pierre59

    Un signal avec un feu de ralentissement (R) à 30 ou 60 peut être un sémaphore ou un carré en signalisation lumineuse, le signal suivant sera un carré avec rappel de ralentissement (RR). Si le ralentissement est présenté le feu voie libre (Vl) est éteint ainsi que l’avertissement (A).

    Le signal suivant qui doit être un carré doit présenter le rappel de ralentissement (RR) mais pas de voie libre (Vl). Par contre si le signal encore suivant est un carré fermé, voire un sémaphore fermé, il présentera normalement en plus du RR un feu d’avertissement (A).

    On parlait auparavant de signaux devant une, ou plusieurs, aiguille(s) prise(s) en pointe. Pour une aiguille, ou des aiguilles, prise(s) en talon il y aura un carré pour la voie directe et un par voie déviée. Les limitation de vitesses nécessaires, surtout pour les voies déviés se font alors avec des TIV (Tableaux Indicateurs de Vitesse). Les carrés violets ne sont utilisés que dans les zones de manoeuvres.

    En fait il y a une hiérarchie de feux pouvant être présentés par un signal voir sur l’url suivante le message du 29 novembre 2016.

    Pierre

    Voir en ligne : https://www.cheminots.net/topic/425...

    Répondre

  • Un gestionnaire en C++ pour votre réseau (2) 18 août 2022 14:03, par trimarco232

    Bonjour,
    attention aux forums, n’importe qui peut y raconter n’importe quoin ; notamment la combinaison R+A n’existe pas, et est d’ailleurs insensée
    les 5 seules combinaisons possibles sont RR+A, RR+Acli, RRcli+A, RRcli+Acli, et Rcli+Acli
    il me semble aussi qu’un signal avec un feu de ralentissement à 30 ou 60 peut être un disque
    il faut obéir à ça : http://www.rmb.asso.fr/signalisation.pdf

    Répondre

  • Un gestionnaire en C++ pour votre réseau (2) 18 août 2022 14:15, par Pierre59

    Bonjour

    Oui la combinaison R+A me semblait aussi bizarre, mais je n’ai pas eut le temps d’approfondir.

    Effectivement un ralentissement à 30 ou 60 peut être sur un disque, il y en a notamment sur des voies unique en BAPR (par exemple sur St Pierre d’Albigny-Bourg St Maurice ou Chambéry-St André le Gaz).

    Pierre

    Répondre

Réagissez à « Un gestionnaire en C++ pour votre réseau (2) »

Qui êtes-vous ?
Votre message

Pour créer des paragraphes, laissez simplement des lignes vides.

Lien hypertexte

(Si votre message se réfère à un article publié sur le Web, ou à une page fournissant plus d’informations, vous pouvez indiquer ci-après le titre de la page et son adresse.)

Rubrique « Projets »

Les derniers articles

Les articles les plus lus