Bibliothèque Accessories (2)

Que mettre dans son croquis ?

. Par : Thierry. URL : https://www.locoduino.org/spip.php?article181

Accessories n’est pas qu’un assemblage de classes et de lignes de code. Le but est quand même de faire bouger des moteurs ou d’allumer des DELs ! Alors après un premier article plutôt théorique, voyons maintenant le côté pratique.

Cas d’école

Prenons un cas simple, la gestion d’une simple DEL via un bouton poussoir et comparons diverses possibilités de codage.

D’abord la version classique sans bibliothèques :

// Programme 1

const int buttonPin = 2;     // broche du poussoir
 
void setup() 
{
  // initialise la DEL interne de l'Arduino comme sortie
  pinMode(LED_BUILTIN, OUTPUT);
  // initialise le poussoir comme entrée
  pinMode(buttonPin, INPUT);
}
 
void loop() 
{
  // Selon l'état du bouton, on allume ou éteint la DEL
  if (digitalRead(buttonPin)== HIGH) {
    digitalWrite(LED_BUILTIN, HIGH);  // Allume la DEL
  } else {
    digitalWrite(LED_BUILTIN, LOW);  // Eteint la DEL
  }
}

C’est la version minimaliste : pas de gestion des rebonds sur le poussoir, la DEL directement reliée à l’état de sa broche... La constante LED_BUILTIN qui définit la broche de la DEL interne utilisée est à 13 pour la plupart des Arduino, mais par exemple elle est à 6 pour un MKR1000 !

Ajoutons maintenant la bibliothèque Accessories :

// Programme 2

#include <Accessories.h>

const int buttonPin = 2;     // broche du poussoir

AccessoryLight light;  // La DEL
PortOnePin port;  // La connexion entre l'Arduino et la DEL.

void setup() 
{
  Accessories::begin();

  port.begin(LED_BUILTIN, DIGITAL);  // broche 13 sur un Uno, broche digitale.

  light.begin(&port, 100); // On branche la DEL sur le port, et on lui assigne le No 100

  // initialise le poussoir comme entrée
  pinMode(buttonPin, INPUT);
}

void loop() 
{
  // Selon l'état du bouton, on allume ou éteint la DEL
  if (digitalRead(buttonPin)== HIGH) {
    light.LightOn();  // Allume la DEL
  } else {
    light.LightOff();  // Eteint la DEL
  }
}

Accessories va permettre d’utiliser un objet light identifié par un entier (100 ici). L’exemple est très simple, il n’y a toujours pas de gestion des rebonds du bouton, mais ajouter un allumage ou une extinction progressive façon vieille ampoule en passant par une broche PWM ne coûterais qu’une légère modification : signaler le port comme analogique, puis spécifier les paramètres pour la vitesse de passage d’un état à l’autre :

void setup() 
{
  ...

  // déclaration de la broche 3 en PWM (analogique)
  port.begin(3, ANALOG);

  ...
  // déclaration des vitesses (pas, durée sur ce pas...)
  light.SetFading(5, 10);  
  ...
}

Par exemple pour allumer une DEL raccordée à une broche PWM et passer d’une valeur analogique de 0 (éteint) à 255 (allumé pleine puissance) par pas de 5 pendant 10ms chacun et inversement pour l’extinction...

Troisième option, ajoutons Commanders pour gérer le bouton et la communication avec les accessoires :

// Programme 3

#include <Accessories.h>
#include <Commanders.h>

const int buttonPin = 2;     // broche du poussoir

ButtonsCommanderPush push; // Le bouton
AccessoryLight light; // La DEL
PortOnePin port;    // La connexion entre l'Arduino et la DEL.

void setup() 
{
  Commanders::begin();
  Accessories::begin();

  port.begin(LED_BUILTIN, DIGITAL);  // broche 13 sur un Uno, broche digitale.

  light.begin(&port, 100); // On branche la DEL sur le port, et on lui assigne le No 100

  // initialise le poussoir comme entrée
  // l'identifiant du bouton à 100 indique que c'est l'accessoire '100' qui sera affecté si le bouton est pressé.
  push.begin(100, buttonPin);
}

void loop() 
{
  unsigned long id = Commanders::loop();

  if (id != UNDEFINED_ID)
  {
    // Renvoie l'événement reçu de Commanders, vers les accessoires...
    Accessories::RaiseEvent(id, (ACCESSORIES_EVENT_TYPE) Commanders::GetLastEventType(), Commanders::GetLastEventData());
  }

  Accessories::loop();
}

La version Commanders+Accessories peut sembler plus complexe que la toute première, mais elle assure en interne la prise en compte des événements via le RaiseEvent(), plus la gestion des rebonds pour le poussoir.
Et ajouter le traitement via un bus DCC par exemple, ne réclame qu’une seule ligne supplémentaire :

void setup() 
{
  ...

  // identifiants fabricant et produit, puis le numéro d'interruption, ici sur la broche 3.
  DccCommander.begin(0x00, 0x00, digitalPinToInterrupt(3));

  ...
}

Si un message DCC arrive, il sera décodé et s’il désigne l’identifiant 100, la DEL va réagir aussi ! Et le bouton est toujours actif... Qui a dit que c’était compliqué ?

Sur un exemple très simple, il est difficile de voir le gain apporté par l’utilisation de ces bibliothèques, alors parlons de vrais cas...

Des cas concrets

Deux aiguillages simples, et un rail de découplage

Imaginons une partie de réseau avec une voie d’évitement équipée à une extrémité d’un rail électrique de découplage. Les aiguillages sont de modèles courants, à deux solénoïdes et donc trois fils.

PNG - 13.2 kio

Nous allons le coder à la fois pour un TCO analogique, et une entrée DCC. Les deux ne sont pas incompatibles et peuvent cohabiter. A l’inverse, même si le codage des deux versions (TCO+DCC) est inclus, l’absence de l’un ou l’autre ne posera pas de problème...
Il est bien évident que dans le cas d’un contrôle extérieur (gestionnaire de trajets, logiciel de pilotage...) la présence d’une possibilité manuelle de basculement d’aiguille peut être perturbatrice. Dans ce cas le mieux est de retirer la partie TCO.

Le programme :

// Programme 4

#include <Accessories.h>
#include <Commanders.h>

// Les boutons du TCO...
ButtonsCommanderPush boutonDecoupleur;
ButtonsCommanderSwitch boutonAiguillageGauche;
ButtonsCommanderSwitch boutonAiguillageDroite;

// Les trois moteurs
AccessoryMotorTwoWays aiguillageGauche;
AccessoryMotorTwoWays aiguillageDroite;
AccessoryMotorOneWay decouplage;

// Les ports pour connecter les moteurs.
PortTwoPins portAiguillageGauche;
PortTwoPins portAiguillageDroite;
PortOnePin portDecoupleur;

void setup()
{
  Commanders::begin(LED_BUILTIN);
  // Memoriser les positions des moteurs dans l'EEPROM.
  Accessories::begin(0, 500);

  // Activation de la reception de messages DCC
  // Ne fera rien en analogique... Mais on peut carrement enlever la ligne si besoin de mémoire.
  // Le dernier argument à true, c'est pour voir la DEL LED_BUILTIN déclarée plus haut clignoter si un signal DCC est présent.
  // Mieux vaut remettre à false une fois le programme mis au point.
  DccCommander.begin(0x00, 0x00, digitalPinToInterrupt(3), true);

  // Gestion des boutons pour le TCO.
  // Ne fera rien si aucun bouton n'est branché, mais on peut enlever si besoin de mémoire.

  // Le switch d'aiguillage a deux positions, chacune associée à un Id DCC.
  boutonAiguillageGauche.begin();
  // DCC 15 /0 et broche 2
  boutonAiguillageGauche.AddEvent(DCCINT(15, 0), 2);
  // DCC 15 / 1 et broche 7
  boutonAiguillageGauche.AddEvent(DCCINT(15, 1), 7);

  boutonAiguillageDroite.begin();
  boutonAiguillageDroite.AddEvent(DCCINT(16, 0), 4);
  boutonAiguillageDroite.AddEvent(DCCINT(16, 1), 5);

  // Un seul evenement pour le découpleur
  boutonDecoupleur.begin(DCCINT(17, 0), 6);

  // Les ports avec leurs broches en digital (pas PWM)
  portAiguillageGauche.begin(8, 9, DIGITAL);
  portAiguillageDroite.begin(10, 11, DIGITAL);
  portDecoupleur.begin(12, DIGITAL);

  // Les accessoires avec pour chacun le port utilisé et le ou les identifiants associés à chaque position. 
  // 255 est la vitesse, ici au maxi, et 400 est la durée d'activation du moteur en millisecondes.
  // C'est encore un peu long pour des solénoïdes. Il faudra adapter selon le modèle de moteur...
  aiguillageGauche.beginTwoWays(&portAiguillageGauche, DCCINT(15, 0), DCCINT(15, 1), 255, 400);
  aiguillageDroite.beginTwoWays(&portAiguillageDroite, DCCINT(16, 0), DCCINT(16, 1), 255, 400);
  decouplage.begin(&portDecoupleur, DCCINT(17, 0), 255, 400);
}

void loop()
{
  unsigned long id = Commanders::loop();

  if (id != UNDEFINED_ID)
  {
    // Renvoie l'événement reçu de Commanders, vers les accessoires...
    Accessories::RaiseEvent(id, (ACCESSORIES_EVENT_TYPE) Commanders::GetLastEventType(), Commanders::GetLastEventData());
  }

  Accessories::loop();
}

Le schéma est toujours le même : dans l’entête les déclarations pour les boutons, les accessoires, et les ports pour les piloter.
Dans le setup(), des begin() sur les bibliothèques, puis les begin() sur les boutons, les accessoires et les ports. C’est à ce moment que l’on va fixer les broches utilisées, dire qui est connecté à quoi, adapter les comportements des boutons, des accessoires et des ports, comme le clignotement d’une DEL ou une durée d’activation. Les boutons vont recevoir des identifiants, et les accessoires associés à ces boutons vont recevoir les mêmes identifiants.
Enfin le loop() qui ne fait que prendre les événements reçus par Commanders et les renvoyer à Accessories avec RaiseEvent(). A la fin du loop général, c’est le loop() de Accessories qui va véritablement traiter ces événements et faire bouger ou changer l’état des accessoires concernés.

Ainsi quand le bouton du découpleur sera pressé, qu’il y ait ou pas une centrale DCC branchée, l’événement généré aura l’identifiant DCCINT(17, 0). Et lorsque Accessories recevra l’événement (renvoyé par RaiseEvent(), rappelons-le), il ira activer l’accessoire dont l’identifiant est DCCINT(17, 0), et c’est bien du découpleur qu’il s’agit ! Bien sûr, si un centrale est présente et que c’est elle qui envoie une demande de mouvement de l’accessoire 17, Accessories recevra le même identifiant DCC(17, 0), et le découpleur réagira de la même manière.

Un croisement de quatre aiguillages plus un servo

Imaginons un croisement de quatre aiguillages animés ensembles, avec deux positions seulement : droit ou dévié, activés par un interrupteur.

PNG - 20 kio

On va bien déclarer les quatre moteurs à deux solénoïdes, puis un groupe qui les réunit pour créer deux états, un droit avec un identifiant DCC, et un dévié avec un autre identifiant. Enfin, comme ces moteurs ont tendance à beaucoup consommer d’énergie pendant leurs mouvements, on va s’assurer qu’ils fonctionnent en séquence, et pas simultanément. Ainsi la consommation sera d’un seul moteur à la fois, mais il faudra un peu plus de temps pour changer d’état.

// Programme 5

#include <Accessories.h>
#include <Commanders.h>
 
// Les boutons du TCO...
ButtonsCommanderSwitch boutonCroisement;
 
// Les moteurs
AccessoryMotorTwoWays aiguillageHautGauche;
AccessoryMotorTwoWays aiguillageHautDroite;
AccessoryMotorTwoWays aiguillageBasGauche;
AccessoryMotorTwoWays aiguillageBasDroite;
 
// Le groupe pour les moteurs
AccessoryGroup groupeCroisement;
 
// Les ports pour connecter les moteurs.
PortTwoPins portAiguillageHautGauche;
PortTwoPins portAiguillageHautDroite;
PortTwoPins portAiguillageBasGauche;
PortTwoPins portAiguillageBasDroite;
 
void setup()
{
  Commanders::begin(LED_BUILTIN);
  // Memoriser les positions des moteurs dans l'EEPROM.
  Accessories::begin(0, 500);
 
  // Activation de la reception de messages DCC
  // Ne fera rien en analogique... Mais on peut carrement enlever la ligne si besoin de mémoire.
  // Le dernier argument à true, c'est pour voir la DEL LED_BUILTIN clignoter si un signal DCC est présent.
  // Mieux vaut remettre à false une fois le programme mis au point.
  DccCommander.begin(0x00, 0x00, digitalPinToInterrupt(3), true);
 
  // Gestion des boutons pour le TCO.
  // Ne fera rien si aucun bouton n'est branché, mais on peut enlever si besoin de mémoire.
 
  // Le switch de croisement a deux positions, chacune associée à un Id DCC.
  boutonCroisement.begin();
  // DCC 15 /0 et broche 4 pour l'interrupteur
  boutonCroisement.AddEvent(DCCINT(15, 0), 4);
  // dcc 15 / 1 et broche 5 pour l'interrupteur
  boutonCroisement.AddEvent(DCCINT(15, 1), 5);
 
  // Les ports avec leurs broches en digital (pas PWM)
  portAiguillageHautGauche.begin(7, 8, DIGITAL);
  portAiguillageHautDroite.begin(9, 10, DIGITAL);
  portAiguillageBasGauche.begin(11, 12, DIGITAL);
  portAiguillageBasDroite.begin(14, 15, DIGITAL);
 
  // Les accessoires avec pour chaque, le port utilisé et le ou les identifiants associés à chaque position. 
  // 255 est la vitesse, ici au maxi, et 400 est la durée d'activation du moteur en millisecondes.
  // C'est encore un peu long pour des solénoïdes. Il faudra adapter selon le modèle de moteur...
  // Les identifiants sont volontairement non significatifs (1000 et plus...)
  // puisqu'aucun événement ne doit faire bouger ces moteurs individuellement.
  aiguillageHautGauche.beginTwoWays(&portAiguillageHautGauche, 1000, 1001, 255, 400);
  aiguillageHautDroite.beginTwoWays(&portAiguillageHautDroite, 1002, 1003, 255, 400);
  aiguillageBasGauche.beginTwoWays(&portAiguillageBasGauche, 1004, 1005, 255, 400);
  aiguillageBasDroite.beginTwoWays(&portAiguillageBasDroite, 1006, 1007, 255, 400);
 
  // Fabriquons le groupe de quatre moteurs à deux états
  groupeCroisement.begin();
 
  // Premier état, tous les moteurs droits.
  // L'argument 'false' signifie pas d'exécution simultanée. Les moteurs bougeront les uns après les autres.
  groupeCroisement.AddState(DCCINT(15, 0), false);
  // Arbitrairement, LEFT signifie droit (première broche activée)
  groupeCroisement.AddStateItem(DCCINT(15, 0), aiguillageHautGauche, LEFT);
  groupeCroisement.AddStateItem(DCCINT(15, 0), aiguillageHautDroite, LEFT);
  groupeCroisement.AddStateItem(DCCINT(15, 0), aiguillageBasGauche, LEFT);
  groupeCroisement.AddStateItem(DCCINT(15, 0), aiguillageBasDroite, LEFT);
 
  // Second état, tous les moteurs déviés.
  // Arbitrairement, RIGHT signifie dévié (seconde broche activée)
  groupeCroisement.AddState(DCCINT(15, 1), false);
  groupeCroisement.AddStateItem(DCCINT(15, 1), aiguillageHautGauche, RIGHT);
  groupeCroisement.AddStateItem(DCCINT(15, 1), aiguillageHautDroite, RIGHT);
  groupeCroisement.AddStateItem(DCCINT(15, 1), aiguillageBasGauche, RIGHT);
  groupeCroisement.AddStateItem(DCCINT(15, 1), aiguillageBasDroite, RIGHT);
}
 
void loop()
{
  unsigned long id = Commanders::loop();
 
  if (id != UNDEFINED_ID)
  {
    // Renvoie l'événement reçu de Commanders, vers les accessoires...
    Accessories::RaiseEvent(id, (ACCESSORIES_EVENT_TYPE) Commanders::GetLastEventType(), Commanders::GetLastEventData());
  }
 
  Accessories::loop();
}

Le code est assez direct, surtout que pour le laisser compréhensible, je n’ai pas utilisé de boucles qui auraient sans doute un peu réduit la taille du programme.
Comme d’habitude, on déclare les boutons, les moteurs, le groupe et les ports. Puis dans le setup() on va initialiser les boutons, les moteurs et les ports. Une fois la partie matérielle fixée, reste à déclarer le groupe et ses différents états. Le premier, activé par un code DCC 15/0 activera la première broche de tous les aiguillages, tour à tour grâce au dernier argument du AddState() à false. Le second, activé par 15/1, fera de même avec la seconde broche. Bien sûr, cela suppose que les aiguillages soient branchés dans le bon sens ! Sinon il suffit d’inverser deux fils, ou encore plus simple, d’inverser les deux broches dans le begin() du port correspondant. Rien à changer dans la loop()...

Un passage à niveau avec feu clignotant

Prenons maintenant un passage à niveau sur une voie unique et mono directionnelle (on fait simple...), activé par un ils posé sur la voie un peu avant le passage, et qui active un moteur pour faire descendre les deux barrières tout en faisant clignoter deux feux sur la route, un de chaque côté. Au bout d’une tempo, le tout revient en position de repos : barrières ouvertes et feux éteints.
Le montage est autonome et n’est piloté par personne : pas de bouton, pas de DCC !

PNG - 7.8 kio

Nous allons déclarer un bouton poussoir. En effet, un ILS est assimilable à un poussoir activé par le passage d’un aimant. On y ajoute un servo et deux DELs.

// Programme 6

#include <Accessories.h>
#include <Commanders.h>
 
#define TEMPO  20000

// L'ILS ...
ButtonsCommanderPush boutonILS;
 
// Le moteur
AccessoryServo PN;

// Les DELS
AccessoryLightMulti Dels;
 
// Les ports pour connecter le moteur et les DELs.
PortServo portPN;
PortOnePin portLight1;
PortOnePin portLight2;
 
void setup()
{
  Commanders::begin(LED_BUILTIN);

  Accessories::begin();
 
  // Un seul bouton, mais deux événements pour le PN
  boutonILS.begin(1234, 6); // premier événement pour le déclanchement
 
  // Les ports avec leurs broches en digital (pas PWM)
  portPN.begin(14);
  portLight1.begin(7, DIGITAL_INVERTED);
  portLight2.begin(8, DIGITAL_INVERTED);

  // begin de l'accessoire de 2 Dels qui doivent clignoter à 500ms d'intervalle. 
  Dels.begin(0, 2, 500);

  // Les DELs avec pour chaque, le port utilisé. 
  Dels.beginLight(0, &portLight1);
  Dels.beginLight(1, &portLight2);

  // On fait clignoter les leds...
  Dels.SetBlinking(0, 500);
  Dels.SetBlinking(1, 500);

  // Le servo : pas de durée de mouvement, un débattement entre 95 et 135 degres
  // et deux positions stables annoncées avec des identifiants inutiles (mais obligatoires)
  PN.begin(&portPN, 50, 95, 135, 1);
  // Les deux positions sont au mini et au maxi :
  PN.AddMinMaxMovingPositions(456, 789);
}
 
unsigned long dateFermeture = 0;

void loop()
{
  Accessories::loop();

  unsigned long id = Commanders::loop();
 
  // si l'ils est activé !
  if (id == 1234)
  {
    if (dateFermeture== 0)
    {
      dateFermeture = millis();

      // mise en route clignotement
      Dels.Blink();

      // mouvement servo : fermeture
      PN.MoveMinimum();

      // Plus rien à faire, attendons l'étape suivante.
      return;
    }
    else
    {
      dateFermeture = millis();
    }
  }

  if (dateFermeture > 0)
  {
	  if (millis() - dateFermeture > TEMPO)
	  {
		  // Fin de l'animation. On ouvre les barrières et on éteint les Dels...
		  dateFermeture = 0;
		  PN.MoveMaximum();
		  Dels.LightOff();
	  }
  }
}

Les déclarations sont assez simples, avec juste l’apparition du AccessoryLightMulti qui permet de piloter plusieurs DELs à la fois. L’ordre Blink() permet de les faire toutes clignoter à la fréquence fixée par le délai du lights.begin() .
Le loop() utilise une date de mise en route du cycle. A début du cycle on lance l’ouverture des barrières. J’ai supposé que la position minimum du servo correspond à la position haute des barrières. On lance en même temps le clignotement des Dels. Une TEMPO plus tard, on lance l’ouverture avec le MoveMaximum(), et on arrête le clignotement avec le LightOff. Petite astuce : si l’ILS est à nouveau activé alors que le cycle est en cours (delaiFermeture != 0), la date de départ est rafraîchie pour laisser le temps au nouveau train de passer !

Pour rappel, le bibliothèque est disponible ici : Forge Locoduino

A noter que tous les programmes de cet article sont présents dans les exemples livrés avec la bibliothèque sous le nom de locoduino.org .