Commande d’aiguilles et tracé d’itinéraires avec Arduino

En 2021, l’ami « Yannick », avec qui j’étais déjà en contact dans le cadre de la réalisation du programme Arduino de son pont tournant m’a recontacté pour un nouveau « Projet ».

En effet il souhaitait cette fois pouvoir commander des servos moteurs pour ses aiguilles tout en ayant la possibilité de définir des itinéraires. Bref, réaliser un TCO.

Le projet : Un T.C.O. (Tableau de Contrôle Optique)

La première phase a été évidemment de bien comprendre le cahier de charges et nous avons travaillé ensemble par un jeu de questions, réponses pour le définir.

Idée de départ

Réaliser un T.C.O. qui afficherait l’itinéraire tracé suite à l’appuie sur un bouton. Ainsi un bouton et un seul serait associé à un itinéraire et à un élément visuel (Led). Un itinéraire étant alors composé de 2 à n aiguilles.

Pourquoi le choix d’Arduino®

Le choix d’Arduino s’est imposé assez facilement car il permet de s’affranchir d’un ordinateur, mais surtout permet de commander facilement des servos-moteur en quelques lignes de code.

L’idée était également de créer un programme très simple et lisible. Pour être facilement modifié par une personne débutant la programmation. Et donc d’être facilement adapté puisque le projet final comporterait à termes 3 TCO (Gare cachée, sortie gare cachée, « La Miouze ») et pour le plus petit TCO, un Arduino Nano® suffirait et serait mis en œuvre vs un Arduino Uno® des autres TCO.

Les règles

Pour les règles de gestion, l’appuie sur le bouton devait déclencher la mise en place de l’itinéraire tout en faisant clignoter la LED de l’itinéraire en cours de mise en place. En cours de tracé, si l’on appuie de nouveau sur le bouton, cela aura pour effet d’annuler la commande et de revenir dans l’état précédent.

 
 
 
 

Un exemple concret avec 8 aiguilles et 5 itinéraires

Pour cet exemple, nous allons nous baser sur le besoin le plus large à savoir les itinéraires de la gare cachée. Ainsi nous aurons :

  • 5 itinéraires possible (donc 5 boutons et 5 Leds)
  • 8 aiguilles (donc 8 servos à piloter)

Sur la base de ces besoins, nous utiliserons donc 18 sorties d’un Arduino Uno®

Sur le papier

Toujours dans l’idée du travail collaboratif, nous avons utilisé l’outil en ligne : Tinkercad® qui permet de :

  • Faire le schéma structurel de la solution
  • Mais également de tester et « runner » le programme Arduino®

Partie programmation

Le programme peut-être copié et collé directement dans Tinkercad®

/**
 * Commande SERVOs avec Arduino pour aiguillages
 * Et tracé d'itinéraires
 * Version: 1.0
 * Date: 14.03.2021
*/

#include <Servo.h>
#include <EEPROM.h>

#define noBounce 25       // Délai anti-rebond boutons
#define delaiRotation 10  // Délai entre 2 crans moteurs
#define pasRotation 0.5   // Précision (lenteur) de la rotation

#define nbServos 8        // Nbre de Servos
#define nbItineraires 5   // Nbre itinéraires = Nb Boutons
#define nbLeds 5          // Nbre de Leds

int etatItineraire = 1;   // 0: RAS attente clic bouton, 1: Iti en cours, 2: Demande de changement en cours de tracé
int adresseStockage = 0;  // Valeur entre 0 et 127 (UNO = 1024Ko)

int ptrClignoLed = 0;
int tpsClignoLed = 350;   // millisecondes

const int PinOutLed     [nbLeds] = {A1,A2,A3,A4,A5}; 
const int PinInBouton   [nbItineraires] = {2,3,4,5,6};
int dernierEtatBouton   [nbItineraires] = {1,1,1,1,1};
int etatBouton          [nbItineraires] = {1,1,1,1,1};

Servo S0, S1, S2, S3, S4, S5, S6, S7;

// le 1er chiffre = position aiguille à gauche
// le 2nd chiffre = position aiguille à droite
// (ou inversement :-)
float servosPositions[nbServos][2] = {
  {130, 60 },
  {130, 60 },
  {130, 60 },
  {130, 60 },
  {130, 60 },
  {130, 60 },
  {130, 60 },
  {130, 60 }
};

// Définition des 5 itinéraires possibles
// 1 ligne = 1 itinéraire dans lequel on indique la position dans laquelle doit se trouver l'aiguille
// G  = Aiguille à Gauche
// D  = Aiguille à Droite
// - = Le servo n'entre pas en jeu dans l'itinéraire
const String itineraires [nbItineraires][nbServos] = {
  {"G","-","-","-","G","D","G","D"},
  {"D","G","-","-","D","D","G","D"},
  {"D","D","G","-","-","G","G","D"},
  {"D","D","D","G","-","-","D","D"},
  {"D","D","D","D","-","-","-","G"} 
};

const bool seqLeds [nbItineraires][nbLeds] = {
  {1, 0, 0, 0, 0},
  {0, 1, 0, 0, 0},
  {0, 0, 1, 0, 0},
  {0, 0, 0, 1, 0},    
  {0, 0, 0, 0, 1}
};


/*
 * Fonction : Lecture des boutons pour
 * - La gestion du changement d'itinéraire en cours (sur bouton enfoncé)
 * - La gestion du tracé d'un itinéraire (sur bouton relaché)
 */

void checkBoutons () {

// Balaye tous les boutons pour savoir
// s'ils sont enfoncés

  for (int zz=0; zz<nbItineraires; zz++) {

    etatBouton[zz] = digitalRead(PinInBouton[zz]);

    // Compare l'état de chaque bouton à
    // son état précédent
    if (etatBouton[zz] != dernierEtatBouton[zz]) {

      if (etatBouton[zz] == LOW) {
        // Si état courant est "Pressé 
        // Le bouton passe de off à on
        // Arret ? Iti en cours
        if (etatItineraire == 1) {
            etatItineraire = 2;
        }

      } else {
        // Si état courant est LOW alors 
        // Le bouton passe de on à off
        if (etatItineraire == 0) {
          etatItineraire = 1;
          jouerScenario (zz);
        }
      }

      // Sauvegarde de l'état courant comme étant 
      // désormais le dernier état
      dernierEtatBouton[zz] = etatBouton[zz];
    }
  }
}


/*
 * Fonction principale loop : 
 * Vérifier en permanence l'état enfoncé des boutons
 * Lorsque les servos ne sont pas en mouvement
 */
void loop () {
  checkBoutons();
}

/*
 * Fonction : Faire clignoter la séquence de Leds
 * Durant la mise en place d'un itinéraire
 */
void clignoterLedsScenario(int quelScenario) {
  
  if (ptrClignoLed % (tpsClignoLed/delaiRotation) == 0) {
    for (int ii = 0 ; ii < nbLeds ; ii++) {
      if (seqLeds [quelScenario][ii] == 1) {;
        digitalWrite(PinOutLed [ii], !digitalRead(PinOutLed [ii]));
            }
        }
    }
  ptrClignoLed ++;
}

/*
 * Fonction : Gérer la rotation d'un servo
 * Dans un sens ou dans l'autre 
 */

bool rotateServo (String posAiguille, int quelServo, int quelScenario) {

  Servo servoToRotate;  // Le servo à actionner
  float posServoDest;   // La position du Servo à atteindre
  float posCourante;    // Sa position courante
  bool etatLedScenario; // Savoir si la LEd intervient dans cet iti
  
  // Sur quel servo on agit ? 
  switch (quelServo) {
    case 0: servoToRotate = S0; break;
    case 1: servoToRotate = S1; break;
    case 2: servoToRotate = S2; break;
    case 3: servoToRotate = S3; break;
    case 4: servoToRotate = S4; break;
    case 5: servoToRotate = S5; break;
    case 6: servoToRotate = S6; break;
    case 7: servoToRotate = S7; break;
  }
  
  // On récupère la valeur de la position du servo à atteindre
  // en fonction D / G / -
  
  if (posAiguille == "G") {
    posServoDest = servosPositions[quelServo][0];  
  } else if (posAiguille == "D") { 
    posServoDest = servosPositions[quelServo][1];    
  } else {
    return false; // BYE BYE
  }
  
  // Lire la position courante du servo
  posCourante = servoToRotate.read();  
  
  // Par contre si le servo est déjà en "place" => On ne va rien faire
  if (posCourante == posServoDest) return false; // BYE BYE

  // On détermine le sens dans lequel
  // Le servo doit tourner
  if (posServoDest - posCourante > 0) {
    
    for (float ii=posCourante+pasRotation; ii<posServoDest; ii+=pasRotation) {

      // Au cours du placement de l'itinéraire
      // On vérifie si un autre bouton n'est pas enclenché pour changer l'itinéraire
      checkBoutons();
      if (etatItineraire == 2) return false;

      servoToRotate.write(ii);
      delay(delaiRotation);
      
    // On fait clignoter la séquence de leds du scénario
      clignoterLedsScenario(quelScenario);
     
    }
    
  } else {
    
    for (float ii=posCourante-pasRotation; ii>posServoDest; ii-=pasRotation) {

      // Au cours du placement de l'itinéraire
      // On vérifie si un autre bouton n'est pas enclenché pour changer l'itinéraire
      checkBoutons();
      if (etatItineraire == 2) return false;

      servoToRotate.write(ii);
      delay(delaiRotation);
      
    // On fait clignoter la séquence de leds du scénario
      clignoterLedsScenario(quelScenario);
     
      
    }
  }
  
  // On fixe la destination finale dans le servo
  // pour être sûr qu'il se place bien sur l'une des limitest (G ou D )
  servoToRotate.write(posServoDest);
  
  return true;
}


/*
 * Fonction : Lire le scénario (itinéraire) demandé
 * Pour savoir les servos à actionner sur l'itinéraire demandé
 */
void jouerScenario (int quelScenario) {
 
  String posAiguille; // D ou G ou -
  
  // Remise à 0 du compteur pour clignoter la Led de l'iti
  // en cours de tracé !
  ptrClignoLed = 0;

  // Aucun itinéraire de tracé = Leds éteintes
  for (int ii = 0 ; ii < nbItineraires ; ii++) {
    digitalWrite(PinOutLed [ii], LOW);  
  }
  
  // Lecture de l'itinéraire du scénario XX
  for (int ii = 0 ; ii < nbServos ; ii++) {

    posAiguille = itineraires [quelScenario][ii];
    rotateServo (posAiguille, ii, quelScenario);
  }  
  
  if (etatItineraire != 2) {
    // Ici l'itinéraire n'a pas été interrompu
    
    // On affiche la séquence de Leds 
    for (int ii = 0 ; ii < nbLeds ; ii++) {
      digitalWrite(PinOutLed [ii], seqLeds [quelScenario][ii]);  
    }    
    
    etatItineraire = 0;   // =0 Itinéraire démandé en place
    EEPROM.update(adresseStockage, quelScenario) ;  // Sauvegarde en mémoire du n° Iti tracé
    
  } else {
    
    // Demande de changement Iti en cours de tracé
    // On au
    etatItineraire = 0;
    
  }
 
}

/*
 * Fonction : Initialisation
 * Initialise les leds, boutons, servos sur les "pattes" de l'arduino
 * Rappel du dernier scénario mémorisé
 */

void setup() {
  
  Serial.begin(9600);
  
  int itiMemo = 0;
  float initServosPositions[nbServos];
  
  // Initialistion Boutons
  // Toutes les leds OFF 
  for (int ii = 0 ; ii < nbItineraires ; ii++) {
    pinMode(PinOutLed [ii], OUTPUT);
    pinMode(PinInBouton [ii], INPUT_PULLUP);
  }
  
  // Initialistion Leds : Toutes les leds OFF
  for (int ii = 0 ; ii < nbLeds ; ii++) {
    digitalWrite(PinOutLed [ii], LOW);  
  }
  
  
  // Récupération du dernier itinéraire mémorisé.
  itiMemo = EEPROM.read (adresseStockage);
  
  // Pour le simu on force un itinéraire 
  // Car on ne peut pas simuler la mémoire EEPROM
  // Ligne ci-dessous à supprimer en réél
  itiMemo = 0;
  
  // Lecture de l'itinéraire du scénario itiMemo
  for (int ii = 0 ; ii < nbServos ; ii++) {

    if (itineraires [itiMemo][ii] == "G") {
       initServosPositions[ii] = servosPositions[ii][0];  
    } else {
      initServosPositions[ii] = servosPositions[ii][1];   
    }
   
  }  
  
  // Init Servos
  S0.write( initServosPositions[0] );
  S0.attach(7);
  delay(300);
  
  S1.write( initServosPositions[1] );
  S1.attach(8);
  delay(300);
  
  S2.write( initServosPositions[2] );  
  S2.attach(9);
  delay(300); 

  S3.write( initServosPositions[3] );
  S3.attach(10);
  delay(300);
  
  S4.write( initServosPositions[4] );  
  S4.attach(11);
  delay(300);
  
  S5.write( initServosPositions[5] );
  S5.attach(12);
  delay(300);
  
  S6.write( initServosPositions[6] );
  S6.attach(13);
  delay(300);
  
  S7.write( initServosPositions[7] );
  S7.attach(A0);
  delay(300);
  
  // On affiche la séquence de Leds lorsque l'itinéraire est tracé
  for (int ii = 0 ; ii < nbLeds ; ii++) {
    digitalWrite(PinOutLed [ii], seqLeds [itiMemo][ii]);  
  } 
  
  
  etatItineraire = 0;
  
    
}

Aller encore plus loin

Pour aller plus loin, il est possible d’ajoindre à ce montage le système pour décoder les trames DCC. Ainsi il acterait comme un décodeur d’accessoires et les itinéraires pourraient en plus être actionnés depuis la centrale de commande.

En esperant que cet article vous ai été utile et qu’il vous ai plu, n’hésitez pas à le partager !

Partager sur les réseaux sociaux en un clic.