La carte Satellite V1 (5)

Un configurateur-testeur

. Par : Dominique. URL : https://www.locoduino.org/spip.php?article249

Après la présentation des principes fondateurs des Satellites Locoduino La carte Satellite V1 (1), la réalisation matérielle La carte Satellite V1 (2), les logiciels La carte Satellite V1 (4) et l’interface SAM La carte Satellite V1 (3), quand il s’agit de mettre en service plusieurs satellites dans lesquels un même logiciel a été gravé mais qui reste à être personnalisé (on dit "configuré"), on est bien obligé d’avoir recours à un appareil spécial pour faire ce travail.

C’est l’objet de cet article.

Quand, de plus, les organes essentiels de notre réseau ferré sont, eux-même, connectés sur un réseau CAN, l’affaire devient bigrement intéressante, car cet "appareil spécial" peut servir à des quantités de tâches : configurer les satellites, bien-sûr, mais aussi les tester, tester la centrale en envoyant des ordres de conduite, tester le TCO en simulant des événements sur le réseau, etc...

On va d’abord, ici, se limiter à la configuration et aux tests des satellites, mais ceux qui voudront aller plus loin auront une base qu’ils pourront étendre. Il y aura probablement, d’autres réalisations de configurateurs, pour ceux qui souhaiteront utiliser différents organes d’interface utilisateur.

Cet exemple de configurateur est construit sur un UNO avec carte CAN, écran LCD et juste un encodeur quadratique pour sélectionner et exécuter les opérations. On ne peut pas faire plus simple ! Nous prendrons l’exemple du Locoduinodrome comme alibi.

Dans le projet Locoduinodrome, nous avions besoin de 8 satellites identiques comme celui-là : La carte Satellite V1 (3)

La carte Satellite V1
La carte Satellite V1

Pour rappel, ces satellites sont connectés sur un bus CAN, au plus près des appareils de voie qu’ils commandent ou surveillent et sont alimentés par une ligne leur délivrant 9V. Certains d’entre-eux alimentent les rails en DCC pour détecter la présence d’un convoi.

Nous avons donc un schéma de branchement avec, au centre, les 3 réseaux qui relient les satellites (9V, CAN et DCC) et, à la périphérie, les connexions particulières de chaque satellite :

  • L’alimentation DCC dont sera déduite l’occupation de voie.
  • Une ou deux détections ponctuelles (zones d’arrêt).
  • Les commandes des DEL d’un ou deux signaux.
  • Une ou zéro commande de servo d’aiguille.
Représentation du Locoduinodrome
Représentation du Locoduinodrome

Comme les logiciels des satellites sont tous les mêmes (à l’exception du numéro d’identification), il va falloir paramétrer chacun d’eux pour qu’il fasse ce qu’on lui demande. On a regroupé ici l’ensemble des logiciels des satellites

Ce tableau montre ce que chaque satellite a en charge :

Table des satellites
Id Satellite Aiguille Signaux Détecteurs DCC
0 C3 P3, P5 Z0
1 C4 P4, P6 Z1
2 a0 C5 Z2
3 S2, S3 P8, P9 Z3
4 S1, S4 P7, P10 Z6
5 C2 P2 Z4
6 a1 C5 Z5
7 C1 P1

Le configurateur doit donc être capable de tester et de modifier les paramètres de tous ces éléments, à savoir :

  • afficher les détections (zones et ponctuelles)
  • régler les butées des débattements des servos correspondant aux positions droite et déviée des aiguilles
  • régler la vitesse de rotation des servos pour un effet réaliste
  • régler la luminosité de chaque led
  • régler les temps d’allumage et d’extinction des leds (inertie)
  • régler les périodes des clignotements des leds

On a donc à faire un appareil dont l’interface utilisateur va tenir la plus grande place dans le logiciel.

J’ai choisi un UNO auquel j’ai adjoint les périphériques suivants :

  • Une interface CAN (ici c’est la carte Can Locoduino mais d’autres cartes Can équivalentes existent comme celle qui est montrée dans l’article La bibliothèque ACAN (1)).
  • Un écran LCD de 4 lignes de 20 caractères.
  • Un encodeur quadratique (avec son bouton qui comporte un contact interrupteur).
  • Un inverseur unipolaire pour le choix test ou configuration.

Le tout tient sur une petite plaquette de plexi avec des entretoises :

Vue de coté
Vue de coté

A gauche le UNO recouvert par une plaque supportant les boutons et le plan du réseau tel que représenté plus haut (très pratique pour savoir ce que l’on fait), et à droite la carte CAN située sous l’écran LCD (sans oublier les pieds en feutre - une carte SD se glisse aussi sur le UNO sur cette photo, mais c’est seulement pour d’autres projets dont on ne parlera pas ici).

Un prototype de configurateur-testeur
Un prototype de configurateur-testeur

Voici le schéma de l’engin (cliquer pour agrandir l’image) :

Le schéma de câblage
Le schéma de câblage

et une affectation des pattes des composants qui peut faciliter les connexions et les vérifications après câblage.

affectations de pattes
affectations de pattes

Et un petit réseau CAN constitué de 2 satellites et du configurateur-testeur :

Une séance de configuration et tests
Une séance de configuration et tests

Ici on voit les satellites 2 et 4 choisis parce que j’ai pu brancher 3 signaux (un semaphore, 2 carrés) et un servo d’aiguille.

J’ai branché un fil entre le GND et une entrée capteur (ce qui fait apparaître le chiffre "6" sur cette photos et les photos d’écran suivantes. Ce chiffre "6" signifie qu’une détection a lieu sur le satellite N°6.

Les satellites et le configurateur-testeur sont reliés par un câble de type téléphonique pour établir la liaison CAN.
Il ne faut pas oublier les straps d’adaptation d’impédance (résistance de 120Ω intégrée à la carte CAN) aux 2 extrémités du bus CAN.

Voici le logiciel complet du configurateur de satellites.

Il se trouve sur le git Locoduino, dans le dossier "Locoduinodrome" :

Configurateur de Satellite

Ce sketch contient les fichiers suivants :

Liste des fichiers du programme
Liste des fichiers du programme

A coté du programme "Configurateur_de_satellites.ino", on trouve :

  • Le programme de test (A_Tests.h)
  • Le programme de configuration (B_Configuration.h)
  • Un fichier d’utilitaires pour affichage LCD
  • Et l’interface SAM (tous les autres fichiers)

Ce programme a été écrit en langage Arduino (donc du C simple, sans programmation objet). Son but est de configurer et de tester les 8 satellites installés pour la démonstration du Locoduinodrome à l’expo d’Orléans, et rien de plus.

Un inverseur permet de passer du mode test au mode configuration et vice versa.
Tout le reste est piloté par un encodeur quadratique avec son bouton intégré :
Un appui sur le bouton permet d’exécuter une action sélectionnée par rotation du bouton

Commençons par définir les ingrédients dont le logiciel a besoin :

Les PINS utilisées :

  • 0,1 = Tx, Rx (ne servent que pour le debug sur moniteur),
  • 2 = Interruption des réceptions sur le bus CAN,
  • 3,5 = L’encodeur, permet de connaitre le sens de rotation,
  • 4 = Le bouton de l’encoder (un clic !),
  • 7 = L’inverseur config/test,
  • 9,11,12,13, RST = Le bus SPI pour le CAN
  • A4 et A5 = Le bus I2C, SDA et SCL respectivement.

Les bibliothèques générales :

#define VERSION F("Satellites Test & Config") // pour le moniteur de l'IDE
#define SHORTVERSION F("TC_Satellites 0.5") // pour l'ecran LCD
#include <SPI.h>
#include <mcp_can.h>
#include <mcp_can_dfs.h>
#include <Wire.h> 
#include <LiquidCrystal_I2C.h>
#include <Encoder.h>
#include <Bounce2.h>     

On y trouve d’abord la version du logiciel à mettre à jour à chaque modification.

Puis les bibliothèques suivantes :

  • SPI pour le bus SPI
  • La bibliothèque Can à prendre sur le dépôt Git Locoduino (Bibliothèque Can)
  • La bibliothèque Wire pour l’I2C
  • LiquiCrystal_I2C pour l’afficheur LCD
  • Bounce2 pour calmer les boutons et inverseurs

On déclare dans la foulée l’objet canController qui communique avec la carte CAN via le SPI, et les variables globales nécessaires.

MCP_CAN canController(9);         // Set CS to pin 9 for CAN
volatile bool Flag_Recv = false;  // pour IRQ CAN
volatile boolean CANOK = false;
unsigned int messagesRecus = 0;   //nb de message reçus pour affichage

unsigned long IdR;
unsigned char lenR = 0;
unsigned char bufR[8];            // buffer reception

unsigned long IdS;
unsigned char lenS = 0;
unsigned char bufS[8];            // buffer emission

Puis les objets lcd et Encodeur ainsi que les 2 boutons. On en profite pour caser ici la routine de lecture de l’encodeur :

LiquidCrystal_I2C lcd(0x27,20,4);     // LCD address = 0x27 pour 4 lignes de 20 cars
Encoder Encodeur(3, 5);               // encodeur
long oldPosition  = -999;
const int SWencPin = 4;               // bouton encodeur
Bounce SWenc = Bounce();
bool gSWencState = false;
const int SWmodePin = 7;              // inverseur test/config
Bounce SWmode = Bounce();
bool gSWmodeState = false;

bool _debug = true;          // pour valider les "Serial.print" vers la Console

/*  le principe est de savoir s'il y a rotation ou non, et dans quel sens
 *  peu importe le nombre de crans.
 *  On obtient +1 = incrément, 0 = pas de rotation, -1 = décrement
 */

int lireEncodeur()
{
  int retour = 0;
  long newPosition = Encodeur.read()/4;
  if (newPosition != oldPosition) {
    if (newPosition > oldPosition) {
      retour = 1;
    } else {
      retour = -1;
    }
    oldPosition = newPosition;
  }
  return (retour);  
}

Maintenant on introduit l’interface SAM décrite dans l’article La carte Satellite V1 (3) et les programmes de tests et de configuration qui sont dans deux onglets (fichiers dans le dossier du programme) différents, avec un onglet "LCD.h" regroupant les routines d’affichage sur LCD :

////////////////// L'Interface SAM /////////////////////
#include "SatelliteWrapper.h" // gere les aiguilles (point) et les signaux
#include "Feux.h"             // défini les types de feux

///////////////// les programmes de tests ont besoin de la variable lcd et de l'interface SAM //////
#include "LCD.h"                    // gestion de l'afficheur LCD
#include "A_Tests.h"                  // programmes de test
#include "B_Configuration.h"          // programmes de configuration

Les déclarations suivantes décrivent la topologie du réseau, à savoir quels capteurs (présence zone et ponctuels-zone d’arrêt) et quels actionneurs (servo d’aiguille et signaux) sont connectés à chaque satellite.
C’est un moment très important dans la définition du réseau et de ses ingrédients.

/*
 *  Les aiguillages
 */
PointWrapper pointWrapper0(0, 2); /* aiguillage 0 du gestionnaire sur satellite 2 */
PointWrapper pointWrapper1(1, 6); /* aiguillage 1 du gestionnaire sur satellite 6 */

/*
 * Les signaux
 */
SemaphoreSignalWrapper                  S1wrapper(0, 4, SIGNAL_0); /* signal 0 du gestionnaire sur satellite 4, slot 0 */
SemaphoreSignalWrapper                  S2wrapper(1, 3, SIGNAL_0); /* signal 1 du gestionnaire sur satellite 3, slot 0 */
CarreSignalWrapper                      C3wrapper(4, 0, SIGNAL_0); /* signal 4 du gestionnaire sur satellite 0, slot 0 */
CarreSignalWrapper                      C4wrapper(5, 1, SIGNAL_0); /* signal 5 du gestionnaire sur satellite 1, slot 0 */
CarreSignalWrapper                      C5wrapper(6, 6, SIGNAL_0); /* signal 6 du gestionnaire sur satellite 6, slot 0 */
CarreSignalWrapper                      C6wrapper(7, 2, SIGNAL_0); /* signal 7 du gestionnaire sur satellite 2, slot 0 */
CarreRappelRalentissementSignalWrapper  C1wrapper(2, 7, SIGNAL_0); /* signal 2 du gestionnaire sur satellite 7, slot 0 */
CarreRappelRalentissementSignalWrapper  C2wrapper(3, 5, SIGNAL_0); /* signal 3 du gestionnaire sur satellite 5, slot 0 */
SemaphoreRalentissementSignalWrapper    S3wrapper(8, 3, SIGNAL_1); /* signal 8 du gestionnaire sur satellite 3, slot 1 */
SemaphoreRalentissementSignalWrapper    S4wrapper(9, 4, SIGNAL_1); /* signal 9 du gestionnaire sur satellite 4, slot 1 */

Les utilitaires de réception et d’envoi de messages Can :

On y trouve d’abord la routine d’interruption qui s’exécute à chaque réception de message Can. Elle est très courte comme il se doit. Elle fait simplement monter un flag (Flag_Recv) qui est testé à chaque boucle de la loop.

// le flag IRQ monte quand au moins un message est recu
// le flag IRQ ne retombe QUE si tous les messages sont lus

void MCP2515_ISR()  { Flag_Recv = true; }      // surveillance de l'IRQ

Ensuite nous trouvons des utilitaires faits pour simplifier l’interface avec le gestionnaire qui ne connait (dans le cas de la démo du Locoduinodrome) que des numéros d’appareil.

void sendTest(byte tId)
{
  uint32_t frameId = 0x20 + tId;
  canController.sendMsgBuf(frameId, 0, 3, bufS);
}

void sendAiguille(int a, bool i)
{
  PointWrapper::setPointPosition(a,i);
}

void sendSignal(int sig, unsigned int f)
{
  SignalWrapper::setSignalState(sig, f);
}

void sendConfig(byte cId, byte cLg)
{
  unsigned long longId = 0x1FFFFF20 + cId;
  canController.sendMsgBuf(longId, 1, cLg, bufS);
}

Cette dernière routine sert à envoyer un message de configuration contenu dans bufS de taille cLg au satellite cId.

Le SETUP du programme

Ici, il n’y a rien que du classique :

  • initialisation du port série, du LCD, affichage de la version du logiciel ;
  • initialisation de l’encodeur ;
  • initialisation du réseau CAN, sans filtre car tous les messages peuvent concerner le configurateur ;
  • initialisation de SAM
  • test de la clé CONFIG/TEST dans la variable gModeConfig
void setup()
{
  Serial.begin(115200);
  Serial.flush();
  Serial.println(SHORTVERSION);
  lcd.init();                         // initialise le lcd 
  lcd.backlight();
  lcd.setCursor(0, 0);                // LCD 1e ligne
  lcd.print(SHORTVERSION);
  
  oldPosition = Encodeur.read()/4;    // initialise encodeur et boutons
  pinMode(SWencPin,INPUT_PULLUP);
  SWenc.attach(SWencPin);
  SWenc.interval(100);
  pinMode(SWmodePin,INPUT_PULLUP);
  SWmode.attach(SWmodePin);
  SWmode.interval(100);

  const int baudrate = CAN_500KBPS;   // initialise le Can à 500 Kb/s  
  canController.start();  
  int repeat = 10;                    // open the CAN
  while (repeat > 0) {
    if (CAN_OK == canController.begin(baudrate)) {CANOK=true;break;}
    else {repeat--;}
    delay(200);
  }
  lcd.setCursor(0, 1);                // LCD 2e ligne
  if (CANOK) {lcd.print(F("Can OK"));}else{lcd.print(F("Can NOK!"));}
  attachInterrupt(0, MCP2515_ISR, FALLING); // start interrupt from pin 2

  /*
   * Pas de filtres Can
   */

  PointWrapper::begin();              // initialise SAM
  SignalWrapper::begin();

  Serial.println("Etat Initial");
  printOutBuffers();                  // etat initial des satellites

  sVit.f = 2.0;                       // vitesse des servos par défaut
      
  delay(2000);                        // le temps de lire le LCD 
  
  gModeConfig = digitalRead(SWmodePin);
  if (gModeConfig) {                  // clé T/C mode config ou test
    initConfig();                     // voir Configuration.h
  } else {
    initTest();                       // voir Tests.h
  }
}  // fin de Setup

La LOOP du programme

Comme d’habitude, c’est une fonction qui se répète indéfiniment et qui va exécuter plusieurs tâches, les unes après les autres :

  • Tester la réception d’un message CAN et afficher ce qu’il signifie, à savoir une occupation ou une détection ponctuelle, ainsi qu’un compteur de messages géré à la fin de la loop.
  • Tester le changement d’état de la clé TEST/CONFIG pour mettre à jour la variable gModeConfig et initialiser le mode correspondant.
  • Réaliser la tache doConfig() ou doTest(), selon le mode.
  • Et enfin, envoyer une interrogation aux satellites avec une récurrence que l’on peut définir.

Sur cette image et sur les copies d’écran suivantes on voit toutes les informations correspondant à chaque contexte :

  • Le compteur en haut à droite montre la circulation des messages Can sur le réseau Can.
  • Le texte en première ligne est le titre du test ou de la configuration particulière (aiguille avec son numéro en position Min ou Max, ou feu avec son numéro et le numéro de satellite sur lequel il est branché).
  • La deuxième ligne désigne l’état de l’élément testé ou le paramètre en cours de configuration.
  • La troisième ligne présente les détections d’occupation en mode test.
  • La quatrième ligne présente les détections ponctuelles en mode test.
  • Dans les 2 lignes du bas, la présence du satellite est représentée par un point, lorsqu’il n’y a pas de détection, et par son numéro lorsqu’il y a une détection.
Test de l'aiguille 0 - position déviée
Test de l’aiguille 0 - position déviée
void loop()
{    
  if (Flag_Recv)  {               // reception et traitement des messages Can
    while (CAN_MSGAVAIL == canController.checkReceive())  {
      canController.readMsgBuf(&lenR, bufR);    // read data,  lenR: data length, bufR: data buf
      IdR = canController.getCanId();
      if (_debug)  {
      Serial.print(" id:0x");Serial.print(IdR, HEX);Serial.print(" Data");
      for (int k = 0; k < lenR; k++)  {
        Serial.print(' ');Serial.print("0x");Serial.print(bufR[k], HEX);
        }
      Serial.println();
      }
      messagesRecus++;
      if (!gModeConfig) {         // en mode test on affiche l'état des capteurs lignes 3 et 4
        displayCapteurs(IdR, bufR);    // gestion des messages CAN reçus : affichage LCD (dans LCD.h)
      }
    }
    Flag_Recv = false;            // Flag MCP2515 pret pour un nouvel IRQ    
  }

  if ( SWmode.update() ) {        // test de la clé T/C
    if ( SWmode.fell() ) {
      gModeConfig = false;
      initTest();    
    } else {
      if ( SWmode.rose() ) {
        gModeConfig = true;
        initConfig();
      }
    }
  }

  if (gModeConfig) {              // mode config ou test ?
    doConfig();
  } else {
    doTest();
  }
    
  if (gModeConfig == false)       // uniquement en mode test
  {
    sendSatelliteMessage(); // emission periodique définie dans SatelliteConfig.h OUT_MESSAGE_PERIOD = 100;  
  }
  
  if (_debug) {
    if (gModeConfig == false) {lcd.setCursor(16, 0);} else {lcd.setCursor(16, 3);}
      if (messagesRecus > 9999) messagesRecus = messagesRecus - 10000;
      if (messagesRecus < 1000) lcd.print(' ');
      if (messagesRecus < 100) lcd.print(' ');
      if (messagesRecus < 10) lcd.print(' ');
      lcd.print(messagesRecus); 
  }
}  // fin de Loop

Voici les routines d’affichage des états des capteurs sur les 2 lignes du bas du LCD. Evidemment ces routines sont adaptées à la configuration choisie de notre réseau.

//-----------------------------------------------
void initDisplayCapteurs() {
  // lignes 2 et 3 affichent l'état des capteurs satellites
  lcd.setCursor(0, 2);  // 3e ligne
  lcd.print("Zones :             ");
  lcd.setCursor(0, 3);  // 4e ligne
  lcd.print("Detec :             "); 
}

//-------------------------------------------------------------------
void displayCapteurs(unsigned long canId, unsigned char *buf)
{
  /* 
   * Ligne x : 01234567890123456789
   * Ligne 2 : Test courant 
   * Ligne 2 : Zones : 
   * Ligne 3 : Detec :
   */
  switch (canId) 
  {
  case 0x10: // Z0,P2,P4
    lcd.setCursor(8, 2);  // 3e ligne col 8+0
    lcd.print(bitRead(buf[0],2) ? '0' : '.');
    lcd.setCursor(10, 3); // 4e ligne col 8+2
    lcd.print(bitRead(buf[0],1) ? '2' : '.');
    lcd.setCursor(12, 3); // 4e ligne col 8+4
    lcd.print(bitRead(buf[0],0) ? '4' : '.');
  break;
  case 0x11: // Z1,P3,P5
    lcd.setCursor(9, 2);  // 3e ligne col 8+1
    lcd.print(bitRead(buf[0],2) ? '1' : '.');
    lcd.setCursor(11, 3); // 4e ligne col 8+3
    lcd.print(bitRead(buf[0],1) ? '3' : '.');
    lcd.setCursor(13, 3); // 4e ligne col 8+5
    lcd.print(bitRead(buf[0],0) ? '5' : '.');
  break;
  case 0x12: // Z2
    lcd.setCursor(10, 2);  // 3e ligne col 8+2
    lcd.print(bitRead(buf[0],2) ? '2' : '.');
  break;
  case 0x13: // Z3,P7,P8
    lcd.setCursor(11, 2);  // 3e ligne col 8+3
    lcd.print(bitRead(buf[0],2) ? '3' : '.');
    lcd.setCursor(15, 3);// 4e ligne col 8+7
    lcd.print(bitRead(buf[0],1) ? '7' : '.');
    lcd.setCursor(16, 3); // 4e ligne col 8+8
    lcd.print(bitRead(buf[0],0) ? '8' : '.');
  break;
  case 0x14: // Z6,P6,P9
    lcd.setCursor(14, 2);  // 3e ligne col 8+6
    lcd.print(bitRead(buf[0],2) ? '6' : '.');
    lcd.setCursor(14, 3); // 4e ligne col 8+6
    lcd.print(bitRead(buf[0],1) ? '6' : '.');
    lcd.setCursor(17, 3); // 4e ligne col 8+9
    lcd.print(bitRead(buf[0],0) ? '9' : '.');
  break;
  case 0x15: // Z4,P1
    lcd.setCursor(12, 2);  // 3e ligne col 8+4
    lcd.print(bitRead(buf[0],2) ? '4' : '.');
    lcd.setCursor(9, 3); // 4e ligne col 8+1
    lcd.print(bitRead(buf[0],1) ? '1' : '.');
  break;
  case 0x16: // Z5
    lcd.setCursor(13, 2);  // 3e ligne col 8+5
    lcd.print(bitRead(buf[0],2) ? '5' : '.');
    break;
  case 0x17: // P0
    lcd.setCursor(8, 3); // 4e ligne col 8+0
    lcd.print(bitRead(buf[0],1) ? '0' : '.');
    break;
  default:
    lcd.setCursor(19, 2);
    lcd.print('?');                              // Id reçu inconnu
  break;
  }
}

Le programme de TEST

Comme je l’ai indiqué au début de cet article, toute l’interface utilisateur se résume à tourner le bouton de l’encodeur pour sélectionner un type de test puis d’appuyer sur le bouton pour le réaliser ou choisir dans une sous-sélection. Un dernier appui permet de revenir au départ.

Donc nous avons les notions de menu et sous-menu.
J’ai choisi (arbitrairement) de ne retenir que 3 menus : 1 pour chaque aiguille (2 cas : droit ou dévié) et 1 pour l’ensemble des feux (160 cas : 16 type de feux pour 10 signaux). C’est le choix que j’ai fait et qui m’a semblé le plus pertinent.

Voici donc les variables déclarées en commençant par les chaines de caractères à afficher sur le LCD.
Cela se traduit par quelques tableaux dont l’un (FeuSat[10]) permet de faire la correspondance entre un numéro de signal et le satellite qui le porte.
Les variables sont évidentes.

////////////// DoTest //////////
const String titreTest[]={
  "Aiguille 0 ",  //0 sous-menus Min, Max
  "Aiguille 1 ",  //1 sous-menus Min, Max
  "Feu # Sat #",  //2 sous-menus 0 à 9 (10 signaux)
  };
const int maxMenu = 3;

const String nomFeu[16]= {"Vl", "A", "S", "C", "R", "RR", "M", "Cv", "Vlc", "Ac", "Sc", "Rc", "RRc", "Mc", "D", "E"};
int indexFeu = 0;
unsigned int typeFeu[16] = {Vl, A, S, C, R, RR, M, Cv, Vlc, Ac, Sc, Rc, RRc, Mc, D, E};
int FeuSat[10] = {4,3,7,5,0,1,6,2,3,4};   // n° de satellite en fonction du N° de feu

int menuTest = 0;
int sousmenuTest = 0;
int etatTest = 0;           // gestion encodeur 
int noSat = 0;
int noAig = 0;              // numero d'aiguille 0..1
int noSig = 0;              // numero signal 0..9
const int maxSignaux = 9;

Cette fonction initialise les variables des tests. Elle est appelée chaque fois que la clé TEST/CONFIG est positionnée sur TEST.

void initTest()
{
  menuTest = 0;
  sousmenuTest = 0;
  etatTest = 0;
  lcd.clear();
  displayTitre();
  initDisplayCapteurs();                    // onglet LCD.h
  testAiguille(sousmenuTest);
}

Cette fonction displayTitre() sert à rafraîchir la 1ère ligne de l’écran du LCD chaque fois que nécessaire.

void displayTitre() {
  lcd.setCursor(0, 0);
  lcd.print(titreTest[menuTest]);
  switch (menuTest) { 
    case 0:
    case 1:
    // aiguilles 0 et 1 sous-menus Min, Max
    switch (sousmenuTest) {
      case 0:
      lcd.print("Min");
      break;
      case 1:
      lcd.print("Max");
      break;
    }
    break;
    case 2:
    //"Feu # Sat #  ",     //2 sous-menus 0 à 9 (10 signaux)
    lcd.print("   ");
    lcd.setCursor(4, 0);
    noSig = sousmenuTest;
    lcd.print(noSig);
    lcd.setCursor(10, 0);  //
    noSat = FeuSat[noSig];
    lcd.print(noSat);
    lcd.setCursor(0, 1);         ;            // 2 eme ligne
    lcd.print("Type ");
    break;
  }
}

Les 2 fonctions suivantes envoient une commande par message CAN au satellite sélectionné dans les menu et sous-menu pour positionner une aiguille ou afficher un type de signal typeFeu.

//-----------------------------------------------
void testAiguille(boolean s) {
  sendAiguille(menuTest, s);
  lcd.setCursor(0, 1);            // 2 eme ligne
  if (s) {
    lcd.print("Droit  ");
  } else {
    lcd.print("Devie  ");
  }
}

//-----------------------------------------------
void testFeu() {
  lcd.setCursor(0, 1);         ;            // 2 eme ligne
  lcd.print("Type ");
  lcd.print(nomFeu[indexFeu]);
  lcd.print("    ");
  SignalWrapper::setSignalState(noSig, typeFeu[indexFeu]);      
}

La fonction doTest()

Cette fonction teste la rotation de l’encodeur et l’appui sur son bouton.
Dans le cas des tests, comme ceux-ci ne provoquent pas de modification de la configuration des satellites, j’ai choisi d’exécuter un test à chaque rotation de l’encodeur.
Le bouton de l’encodeur ne sert qu’à passer en mode de choix de feu après la sélection du signal et du satellite.
La variable u indique si une rotation a lieu dans le sens horaire (u=1) ou anti-horaire (u=-1).
La fonction permet de balayer tous les cas possibles à l’intérieur des limites.
A chaque cas, outre la mise à jour des variables d’état et de l’afficheur LCD, elle appelle les fonctions testAiguille() et testFeu().

Organigramme du programme de tests
Organigramme du programme de tests
void doTest() { 
  // positionne menuTest et sousmenuTest et fait le test correspondant à chaque changement
  // menuTest et sousmenuTest définissent les tests elementaires
  // etatTest définit l'emploi de l'encodeur
  // etatTest = 0 : juste la selection de test par l'encodeur
  int u = lireEncodeur();
  // rotation de l'encodeur pour choix du test
  if ((u != 0) && (etatTest == 0 )) { 
    if (u > 0) {              // +1 encodeur
      if (menuTest == 2) {    // choix parmi 9 signaux
        if (sousmenuTest < 9) {
          sousmenuTest++;
        }
      }
      if (menuTest <= 1) {    // aiguilles 0 et 1
        sousmenuTest++;
        if (sousmenuTest > 1) {
          menuTest++;         // passage au test signaux
          sousmenuTest = 0;
        }
        if (menuTest <= 1) {
          testAiguille(sousmenuTest); 
        }
      }
    }
    if (u < 0) {              // -1 encodeur
      if (menuTest <= 1) {    // aiguille
        if (sousmenuTest > 0) {
          sousmenuTest--;
        } else {
          if (menuTest > 0) {
            menuTest--;
            sousmenuTest = 1;
          }
        }
        testAiguille(sousmenuTest);
      }
      if (menuTest == 2) {    // feux
        if (sousmenuTest > 0) {
          sousmenuTest--;
        } else { 
          menuTest = 1;       // passage en test aiguille
          sousmenuTest = 0;
          testAiguille(sousmenuTest);
        }
      }
    }        
    displayTitre();           // met à jour noSig pour testFeu()
  }
  if ((u != 0) && (etatTest == 2)) { // choix type de feu
    if (u > 0) {  // augmenter
      if (indexFeu < 15) indexFeu++;
    } else {
      if (indexFeu > 0) indexFeu--;
    }
    testFeu();
  }
  // un appui sur le bouton de l'encodeur change etatTest en fonction du contexte défini par etatTest et menuTest
  if (SWenc.update()) {
    if ( SWenc.fell() ) {     // appui sur bouton encodeur
      switch (etatTest) {
      case 0:
      if (menuTest == 2) {   // passage en choix type de feu
        etatTest = 2;
        indexFeu=0;           // Vl
        displayTitre();
        testFeu();
      }
      break;
      case 2:               // fin du choix type de feux
      etatTest = 0;
      lcd.clear();
      displayTitre();       // retour a l'etat précédent du choix test
      initDisplayCapteurs();    // rafraichissement LCD
      break;
      }
    }
  }
}

Voici quelques exemples :

Test du signal 9 sur le satellite 4 : voie libre
Test du signal 9 sur le satellite 4 : voie libre
Test du signal 9 sur le satellite 4 : ralentissement clignotant
Test du signal 9 sur le satellite 4 : ralentissement clignotant
Test du signal 7 sur le satellite 2 : voie libre
Test du signal 7 sur le satellite 2 : voie libre
Test du signal 7 sur le satellite 2 : avertissement
Test du signal 7 sur le satellite 2 : avertissement
Test du signal 7 sur le satellite 2 : carré
Test du signal 7 sur le satellite 2 : carré
Test de l'aiguille 0 - position droite
Test de l’aiguille 0 - position droite

Le programme de configuration

On commence par déclarer les constantes et variables nécessaires :

  • celles qui servent à la navigation dans les menus ;
  • les valeurs par défaut des paramètres de configuration
  • les chaines qui s’afficheront sur l’écran LCD : on voit qu’il y a 3 paramètres de configuration par servo et 5 par Led, avec 10 Leds, ce qui fait 56 paramètres au total.
  • le menu comporte une position "Choix Led" qui permet de sélectionner une Led parmi les 10 possibles et une position "Fin config" qui permet de revenir au menu principal après être entré dans les configurations des paramètres des Leds.

A ce stade du développement des satellites, ceux-ci ne remontent pas (encore) la valeur courante de leurs paramètres. Par conséquent le programme de configuration nécessite de refaire le réglage d’un paramètre dès lors qu’on le sélectionne pour le modifier.

On remarque en passant que les valeurs des butées initiales des servos sont choisies dans une plage très étroite près du milieu de l’amplitude totale du servo. Ceci a pour but d’éviter, au moment du lancement de la configuration, de forcer l’aiguille vers des positions dangereuses pour sa mécanique. Le réglage des butées d’aiguille consistera donc à élargir cette plage et dépassant légèrement (mais pas trop !) le point de contact des lames pour donner un léger effet ressort.

////////////// DoConfig //////////

bool gModeConfig = false;    // mode test
int valeurEncodeur = 0;      // valeur absolue (virtuelle) de l'encodeur
int gnumConfig = 0;          // numero de config
bool gModeSWConfig = false;  // choix ou true : execution
int satConfig = 0;            // choix du satellite (0..7)
int ledConfig = 0;           // choix de la led (0..9)

int sMin = 1300;    // servo min
int sMax = 1700;    // servo max
// pour les conversions entre 1 float et 4 bytes
union sVitType {float f; unsigned char b[sizeof(float)];} ;
sVitType sVit;      // servo vitesse
uint8_t sLum = 255; // intensité
uint8_t sAlt = 255; // temps d'allumage
uint8_t sFad = 255; // temps d'extinction
uint8_t sOnt = 255; // temps allumé
uint8_t sPer = 255; // periode


const String titreConfig[]={
  "Servo Max    ",  //0
  "Servo Min    ",  //1
  "Servo Vitesse",  //2
  "Choix Led    ",  //3
  "Led Maxi     ",  //4
  "Led T. allume",  //5
  "Led T. eteint",  //6
  "Led T. on    ",  //7
  "Led Periode  ",  //8
  "Fin config   "}; //9

Ensuite nous avons une fonction initConfig() qui sert à initialiser toutes les variables d’état du mode configuration.

Elle intègre une boucle infinie nécessaire au choix d’un satellite parmi les 8 possibles car on ne peut configurer qu’un seul satellite à la fois.

///////////////// initialisations /////////////////
  
void initConfig()    // menu principal
{
  lcd.clear();  // 1e ligne
  lcd.print("Config Satellite ");
  lcd.print(satConfig); // satellite en cours de configuration
  while (1) {
    int enc = lireEncodeur();
    if (enc > 0) satConfig++; 
    if (enc < 0) satConfig--;
    if (satConfig < 0) {satConfig = 0;}
    if (satConfig > 7) {satConfig = 7;}
    lcd.setCursor(17, 0);
    lcd.print(satConfig); // satellite en cours de configuration
    SWenc.update();
    if (SWenc.fell()) break;
    if ( SWmode.update() ) {        // test de la clé T/C
      if ( SWmode.fell() ) {
        gModeConfig = false;
        initTest();
        return;   
      } 
    }
  }
  gModeSWConfig = false;// mode choix fonction
  lcd.setCursor(0, 1);  // 2e ligne
  lcd.print(titreConfig[gnumConfig]);
}

Ensuite l’ordonnancement des étapes de configuration est représenté sur cet organigramme :

Organigramme du programme de configuration
Organigramme du programme de configuration

La rotation de l’encodeur se traduit, en fonction de la variable booléenne gModeSWConfig par :

  • soit la navigation dans les menus de configuration ;
  • soit la variation d’un paramètre de configuration.

L’appui sur le bouton de l’encodeur inverse la variable gModeSWConfig.

///////////////// configuration /////////////////
/*
 * Mode opératoire
 * Choisir un paramètre de configuration en tournant le bouton de l'encodeur (9 cas possibles).
 * Appuyer sur le bouton de l'encodeur pour passer en mode modification du paramètre sélectionné.
 * Tourner le bouton de l'encodeur pour changer la valeur du paramètre.
 * Un message Can de configuration est envoyé au satellite à chaque cran de l'encodeur. Ceci a pour but de voir l’effet immédiat sur l’objet configuré : le servo bouge dans un sens ou l’autre, la led éclaire plus ou moins. On s’arrête quand le résultat est satisfaisant.
 * Appuyer sur le bouton de l'encodeur pour terminer le mode modification du paramètre sélectionné.
 * Une message Can de fin de configuration est envoyé au satellite
 */
void doConfig()
{
  int u = lireEncodeur();
  if (u == 0) // pas de changement
  {
    SWenc.update();
    if ( SWenc.fell() )
    {
      gModeSWConfig = !gModeSWConfig; // inversion du mode CONFIG -> ENREGISTREMENT et retour MENU
      if (gModeSWConfig == false)     // fin du mode CONFIG -> ENREGISTREMENT
      {
        lcd.setCursor(14, 1);  // 2e ligne
        lcd.print("    ");     // effacement 
        switch (gnumConfig)
        {
        case 0: // Servo Max
        bufS[0]=0x80; // perma servo
        bufS[1]=1;  // aig max
        bufS[2]=(byte)(sMax>>8);
        bufS[3]=(byte)(sMax);
        sendConfig(satConfig, 4);
        break;
        case 1: // Servo Min
        bufS[0]=0x80; // perma servo
        bufS[1]=0;  // aig min
        bufS[2]=(byte)(sMin>>8);
        bufS[3]=(byte)(sMin);
        sendConfig(satConfig, 4);
        break;
        case 2: // Servo Vit
        bufS[0]=0x80; // perma servo
        bufS[1]=2;  // aig vit
        bufS[2]=sVit.b[3];
        bufS[3]=sVit.b[2];
        bufS[4]=sVit.b[1];
        bufS[5]=sVit.b[0];
        sendConfig(satConfig, 6);
        break;
        case 3: // choix de la led
        break;
        case 4: // Led Max 
        bufS[0]=0x81; // perma led
        sendConfig(satConfig, 3);
        break;
        case 5: // Led T. allume
        bufS[0]=0x81; // perma led
        sendConfig(satConfig, 3);
        break;
        case 6: // Led T. eteint
        bufS[0]=0x81; // perma led
        sendConfig(satConfig, 3);
        break;
        case 7: // Led T. On
        bufS[0]=0x81; // perma led
        sendConfig(satConfig, 3);
        break;
        case 8: // Led Period
        bufS[0]=0x81; // perma led
        sendConfig(satConfig, 3);
        break;
        }  
      } else    // passage en mode modif (gModeSWConfig = true) -> SELECTION du TYPE
      { 
        lcd.setCursor(13, 1);  // 2e ligne 
        lcd.print('>');
        switch (gnumConfig)
        {
        case 0:
        lcd.print(sMax);
        break;
        case 1:
        lcd.print(sMin);
        break;
        case 2:
        lcd.print(sVit.f, 2);
        break;
        case 3: // choix de la led 
        lcd.print(ledConfig);
        break;
        case 4:
        lcd.print(sLum); // Max
        break;
        case 5:
        lcd.print(sAlt); // T. allume
        break;
        case 6:
        lcd.print(sFad); // T. eteint
        break;
        case 7:
        lcd.print(sOnt); // T. on
        break;
        case 8:
        lcd.print(sPer); // Period
        break;
        case 9:
        gnumConfig = 0;
        initConfig();    // fin pour ce satellite
        break;
        }
        lcd.print("  ");
      }
    }
  return;
  }
  // cas de la rotation encodeur
  if (u > 0) valeurEncodeur++; 
  if (u < 0) valeurEncodeur--;
  if (gModeSWConfig == false)              // mode CHOIX CONFIG
  {
    gnumConfig = valeurEncodeur;
    if (gnumConfig < 0) {valeurEncodeur = gnumConfig = 0;}
    if (gnumConfig > 9) {valeurEncodeur = gnumConfig = 9;}
    lcd.setCursor(0, 1);  // 2e ligne
    lcd.print(titreConfig[gnumConfig]);
  } else {                                // mode MODIFICATION
    lcd.setCursor(14, 1);  // 2e ligne 
    switch (gnumConfig)
    {
    case 0: // Servo Max
      if (u > 0) sMax++; 
      if (u < 0) sMax--;
      lcd.print(sMax);lcd.print("  ");
      bufS[0]=0; // tempo servo
      bufS[1]=1; // a0 max
      bufS[2]=(byte)(sMax>>8);
      bufS[3]=(byte)(sMax);
      sendConfig(satConfig, 4);
    break;
    
    case 1: // Servo Min
      if (u > 0) sMin++; 
      if (u < 0) sMin--;
      lcd.print(sMin);lcd.print("  ");
      bufS[0]=0; // tempo servo
      bufS[1]=1; // a0 min
      bufS[2]=(byte)(sMin>>8);
      bufS[3]=(byte)(sMin);
      sendConfig(satConfig, 4);
    break;
    
    case 2: // Servo Vit
      if (u > 0) {sVit.f = sVit.f + 0.1;} 
      if (u < 0) {sVit.f = sVit.f - 0.1;}
      lcd.print(sVit.f, 2);lcd.print("  ");
      bufS[0]=0; // tempo servo
      bufS[1]=2; // a0 vitesse
      bufS[2]=sVit.b[3];
      bufS[3]=sVit.b[2];
      bufS[4]=sVit.b[1];
      bufS[5]=sVit.b[0];
      sendConfig(satConfig, 6);
    break;
    
    case 3: // choix de la Led
      if ((u > 0) && (ledConfig < 9)) ledConfig++; 
      if ((u < 0) && (ledConfig > 0)) ledConfig--;
      lcd.print(ledConfig);lcd.print("   ");
    break;  
    
    case 4: // Led Max luminosité
      if ((u > 0) && (sLum < 255)) sLum++; 
      if ((u < 0) && (sLum > 0)) sLum--;
      lcd.print(sLum);lcd.print("  ");
      bufS[0]=1; // reglage led
      bufS[1]= (ledConfig << 3) + 0; // numero de led + intensité
      bufS[2]=sLum;
      sendConfig(satConfig, 3);
    break;
    
    case 5: // Led allumage Time
    if ((u > 0) && (sAlt < 255)) sAlt++; 
      if ((u < 0) && (sAlt > 0)) sAlt--;
      lcd.print(sAlt);lcd.print("  ");
      bufS[0]=1; // reglage led
      bufS[1]= (ledConfig << 3) + 1; // numero de led + intensité
      bufS[2]=sAlt;
      sendConfig(satConfig, 3);
    break;
    
    case 6: // Led extinction Time
    if ((u > 0) && (sFad < 255)) sFad++; 
      if ((u < 0) && (sFad > 0)) sFad--;
      lcd.print(sFad);lcd.print("  ");
      bufS[0]=1; // reglage led
      bufS[1]= (ledConfig << 3) + 2; // numero de led + intensité
      bufS[2]=sFad;
      sendConfig(satConfig, 3);
    break;
    
    case 7: // Led On Time
    if ((u > 0) && (sOnt < 255)) sOnt++; 
      if ((u < 0) && (sOnt > 0)) sOnt--;
      lcd.print(sOnt);lcd.print("  ");
      bufS[0]=1; // reglage led
      bufS[1]= (ledConfig << 3) + 3; // numero de led + intensité
      bufS[2]=sOnt;
      sendConfig(satConfig, 3);
    break;
    
    case 8: // Led Period
    if ((u > 0) && (sPer < 255)) sPer++; 
      if ((u < 0) && (sPer > 0)) sPer--;
      lcd.print(sPer);lcd.print("  ");
      bufS[0]=1; // reglage led
      bufS[1]= (ledConfig << 3) + 4; // numero de led + intensité
      bufS[2]=sPer;
      sendConfig(satConfig, 3);
    break;

    case 9: // fin pour ce satellite
    break;
    }  
  }
}

L’interface utilisateur se présente comme suit :

Choix d'un satellite à configurer
Choix d’un satellite à configurer
Choix du paramètre "Servo Max"
Choix du paramètre "Servo Max"
Le nombre 31 en bas à droite affiche un compteur de messages CAN en mode configuration.
Valeur par défaut du paramètre "Servo Max"
Valeur par défaut du paramètre "Servo Max"
Réglage du paramètre "Servo Max"
Réglage du paramètre "Servo Max"
Validation du paramètre "Servo Max"
Validation du paramètre "Servo Max"
Reglage de la vitesse du servo d'aiguille
Reglage de la vitesse du servo d’aiguille
Choix d'une des Leds du satellite
Choix d’une des Leds du satellite
Réglage luminosité maximum
Réglage luminosité maximum
Réglage de la période de clignotement
Réglage de la période de clignotement
Réglage du temps d'allumage (rise time)
Réglage du temps d’allumage (rise time)
Réglage du temps d'extinction (fall time)
Réglage du temps d’extinction (fall time)
Réglage de la durée du temps "allumé"
Réglage de la durée du temps "allumé"
Fin de configuration
Fin de configuration

Le programme complet peut être téléchargé ici :

Télécharger le programme complet

Conclusions

Avec un seul bouton (encodeur quadratique), on peut faire des tas de choses, mais le programme peut sembler complexe justement pour faire toutes ces choses. Ceux qui sont un peu experts arriveront à l’adapter à leurs besoins spécifiques. Pour les autres, il y a là un exemple d’utilisation de ce type d’encodeur quadratique, comment y rattacher de multiples traitements.

Le logiciel et ses futures mises à jour sont mises à disposition sur le git : Configurateur de Satellite.

Nous vous invitons vivement à faire part de vos réalisations personnelles et vos remarques constructives sur le Forum (dossier "Vos projets/Satellite").