LOCODUINO

Parlons Arduino => Modélisation, Architectures logicielles et matérielles => Discussion démarrée par: savignyexpress le mai 23, 2019, 09:58:52 pm

Titre: Réseau hivernal en N et machines d'états
Posté par: savignyexpress le mai 23, 2019, 09:58:52 pm
Bonjour à tous,

Comme annoncé dans le sujet http://forum.locoduino.org/index.php?topic=98.msg8459#new (http://forum.locoduino.org/index.php?topic=98.msg8459#new), voici une présentation de mon réseau hivernal en N.

Description du réseau et des cartes de détection
Le réseau dans son état quasi actuel se présente comme suit. Il s'agit d'une gare sur une voie unique avec une gare cachée permettant le croisement.

(https://i62.servimg.com/u/f62/15/74/35/09/20171110.jpg) (https://servimg.com/view/15743509/343)

Le réseau est découpé en zones comme suit:

(https://i42.servimg.com/u/f42/15/74/35/09/zones_10.jpg) (https://servimg.com/view/15743509/359)


Le réseau est en DCC. Toutes les zones sont équipées pour détecter les trains par consommation de courant. Les zones des 2 gares sont en plus équipées de diodes pour le freinage ABC. Les aiguilles A1 à A5 sont actionnées par une carte à 8 relais.

Les cartes de détection se composent de diodes pour d'une part assurer la détection de la consommation de courant, d'autre part générer l'asymétrie du signal DCC requise pour le freinage. Ces diodes supplémentaires sont commutées par des relais.

(https://i42.servimg.com/u/f42/15/74/35/09/dzotec10.png) (https://servimg.com/view/15743509/360)

La tension de détection actionne un optocoupleur qui génère le signal logique. Le réseau fonctionnant en DCC, la détection est mono-alternance, c'est ensuite par logiciel que l'on assure la continuité de la détection. L'Arduino, un Mega, tourne suffisamment vite pour ne rien manquer.

(https://i42.servimg.com/u/f42/15/74/35/09/dzotec11.png) (https://servimg.com/view/15743509/361)

Programme
La structure du programme s'inspire des principes décrits ici: https://paulmurraycbr.github.io/ArduinoTheOOWay.html (https://paulmurraycbr.github.io/ArduinoTheOOWay.html).

Classe Tache
La première classe définit la notion de tâche.

class Tache
{
public:
        static Tache *pListeTaches;
        Tache ();
        static void setupTaches (void);
        static void loopTaches (void);

        virtual void setup (void) = 0;
        virtual void loop (void) = 0;

private:
        Tache *pProchaineTache;
};

Toutes les autres classes du projet héritent de Tache et doivent définir les méthodes setup et loop. Le corps de cette classe est:

#include <Arduino.h>

#include "Tache.h"


Tache *Tache::pListeTaches = NULL;

Tache::Tache ()
{
        pProchaineTache = pListeTaches;
        pListeTaches = this;
} // Tache


void Tache::setupTaches (void)
{
        for (Tache *p = pListeTaches; p; p = p->pProchaineTache)
        {
                p->setup ();
        } // for
} // setupTaches


void Tache::loopTaches (void)
{
        for (Tache *p = pListeTaches; p; p = p->pProchaineTache)
        {
                p->loop ();
        } // for
} // loopTaches

À chaque appel du constructeur, la nouvelle tâche est ajoutée en tête de la liste de tâches. Les méthodes de classe setupTaches et loopTaches appelées respectivement dans les fonctions principales setup et loop parcourent cette liste pour appeler les méthodes virtuelles redéfinies par les classes descendantes de celle-ci.

Cette technique est utilisée entre autres dans les bibliothèques Commanders et Accessories de Thierry et dans ScheduleTable, SlowMotionServo et LightDimmer de Jean-Luc.

Classe Led
Il est toujours intéressant de vérifier visuellement que l'Arduino ne s'est pas planté. Faire clignoter la led de la carte est un bon moyen de vérifier cela. Tant qu'elle clignote, aucune tâche ne bloque le processeur et tout va bien.

#include <stdint.h>

#include "Tache.h"

class LED: public Tache
{                   
public:
        LED (uint8_t xPin, uint32_t nxPeriode);
       
        void setup ();
        void loop ();

        void allumer (void);
        void eteindre (void);
        void clignoter (void);

private:
        const uint8_t   Pin;
        uint8_t         eEtat;
        const uint32_t  nPeriode;
        uint32_t        tDernier;
};

#include "LED.h"


#define E_OFF   0
#define E_ON    1
#define E_BLINK_ON      2
#define E_BLINK_OFF     3

LED::LED (uint8_t xPin, uint32_t nxPeriode) :
        Pin (xPin),
        nPeriode (nxPeriode) {}


void LED::setup (void)
{
        eEtat = E_OFF;
        pinMode (Pin, OUTPUT);
} // LED::setup


void LED::loop (void)
{
uint32_t nDeltaTemps;

        switch (eEtat)
        {
                case E_OFF:
                break;

                case E_ON:
                break;
               
                case E_BLINK_OFF:                       
                        nDeltaTemps = millis () - tDernier;
                        if (nDeltaTemps > nPeriode)
                        {
                                eEtat = E_BLINK_ON;
                                tDernier = millis ();
                        } // if
                break;

                case E_BLINK_ON:
                        nDeltaTemps = millis () - tDernier;
                        if (nDeltaTemps > nPeriode)
                        {
                                eEtat = E_BLINK_OFF;
                                tDernier = millis ();
                        } // if
                break;
        } // switch

        digitalWrite (Pin, (eEtat == E_ON) || (eEtat == E_BLINK_ON));
} // LED::loop


void LED::allumer (void)
{
        eEtat = E_ON;
} // LED::allumer


void LED::eteindre (void)
{
        eEtat = E_OFF;
} // LED::eteindre


void LED::clignoter (void)
{
        if (eEtat == E_OFF)
        {
                eEtat = E_BLINK_OFF;
                tDernier = millis ();
        }
        else if (eEtat == E_ON)
        {
                eEtat = E_BLINK_ON;
                tDernier = millis ();               
        } // if
} // LED::clignoter

Le constructeur reçoit le numéro de la pin et la période de clignotement en paramètre. Tout se passe principalement dans la méthode loop qui implante une machine d'états. On remarque l'importance de gérer le temps à l'aide de millis () et non avec delay (). Il est en effet important de ne pas bloquer dans cette fonction.

Les états possibles sont définis par des instructions #define plutôt qu'avec un enum. Le but était de s'assurer que les états soient codés dans une variable de type uint8_t. Je ne connaissais pas les enum class de C++ 11 et leur possibilité de spécifier le type sous-jacent lorsque j'ai commencé ce projet !

La led s'utilise comme suit dans le fichier .ino.

---
LED led_board (LED_BOARD, PER_CLIGN);
...
void setup ()
{
   ...
   Tache::setupTaches ();

   led_board.clignoter ();
} // setup

Le prochain post décrira les autres classes plus spécifiques à cette application.

Meilleures salutations.

Marc-Henri
Titre: Re : Réseau hivernal en N et machines d'états
Posté par: Dominique le mai 25, 2019, 04:31:14 pm
La structure du programme s'inspire des principes décrits ici: https://paulmurraycbr.github.io/ArduinoTheOOWay.html.

Merci Marc-Henri pour ce rappel du lien qui permet de progresser dans la programmation objet et se rapprocher des "pros" que sont Jean-Luc et Thierry  ;D
Titre: Re : Réseau hivernal en N et machines d'états
Posté par: savignyexpress le mai 27, 2019, 10:32:01 pm
Bonjour à tous,

Voici la suite du programme de contrôle de mon réseau. Avant de décrire les classes, commençons par le programme principal qui me permettra d'illustrer les principes mis en oeuvre.

//
//      Reseau_hivernal.ino
//
//      2019.02.15      Création.

#include <stdint.h>

#include "Affichage.h"
#include "Aiguilles.h"
#include "Boutons.h"
#include "ChefDeGare.h"
#include "GareCachee.h"
#include "Hardware.h"
#include "Itineraire.h"
#include "LED.h"
#include "Signal.h"
#include "Tache.h"
#include "Zone.h"


#define PER_CLIGN       250
LED led_board (LED_BOARD, PER_CLIGN);
Affichage affichage;
Boutons boutons;
Modes selMode;
Aiguilles aiguilles;


#ifdef DEBUG_ZONE
Zone zoneL1 (L1_DET, DELAI_ENTREE_LIGNE, DELAI_SORTIE_LIGNE, "Ligne 1");
Zone zoneL2 (L2_DET, DELAI_ENTREE_LIGNE, DELAI_SORTIE_LIGNE, "Ligne 2");
Zone zoneG1 (G1_DET, DELAI_ENTREE_GARE, DELAI_SORTIE_GARE, "Gare 1");
Zone zoneG2 (G2_DET, DELAI_ENTREE_GARE, DELAI_SORTIE_GARE, "Gare 2");
Zone zoneGC1G (GC1G_DET, 0, DELAI_SORTIE_GARE_CACHEE, "Gare cachée 1G");
Zone zoneGC1D (GC1D_DET, 0, DELAI_SORTIE_GARE_CACHEE, "Gare cachée 1D");
Zone zoneGC2G (GC2G_DET, 0, DELAI_SORTIE_GARE_CACHEE, "Gare cachée 2G");
Zone zoneGC2D (GC2D_DET, 0, DELAI_SORTIE_GARE_CACHEE, "Gare cachée 2D");       
#else
Zone zoneL1 (L1_DET, DELAI_ENTREE_LIGNE, DELAI_SORTIE_LIGNE);
Zone zoneL2 (L2_DET, DELAI_ENTREE_LIGNE, DELAI_SORTIE_LIGNE);
Zone zoneG1 (G1_DET, DELAI_ENTREE_GARE, DELAI_SORTIE_GARE);
Zone zoneG2 (G2_DET, DELAI_ENTREE_GARE, DELAI_SORTIE_GARE);
Zone zoneGC1G (GC1G_DET, 0, DELAI_SORTIE_GARE_CACHEE);
Zone zoneGC1D (GC1D_DET, 0, DELAI_SORTIE_GARE_CACHEE);
Zone zoneGC2G (GC2G_DET, 0, DELAI_SORTIE_GARE_CACHEE);
Zone zoneGC2D (GC2D_DET, 0, DELAI_SORTIE_GARE_CACHEE);       
#endif

VoieGareCachee voieGC1 (GC1_ABC, &zoneGC1G, &zoneGC1D);
VoieGareCachee voieGC2 (GC2_ABC, &zoneGC2G, &zoneGC2D);

GareCachee gareCachee (&aiguilles, &affichage, &voieGC1, &voieGC2, &zoneL1, &zoneL2);

Signal sigA1 (&aiguilles, C_A1, &zoneL1, SIG_A1_ROUGE, SIG_A1_ORANGE, SIG_A1_VERT);
Signal sigA2 (&aiguilles, C_A2, &zoneL2, SIG_A2_ROUGE, SIG_A2_ORANGE, SIG_A2_VERT);

VoieGare voieG1 (G1_ABC, &zoneG1);
VoieGare voieG2 (G2_ABC, &zoneG2);


#ifdef DEBUG_ITIN
ItinGareVersCachee it_G1_GC_Gauche (&zoneL2, &aiguilles, &gareCachee, &voieG1, &voieG2, SENS_GAUCHE, C_A2, DEVIE,  POUS_A2_DEV,  "Itinéraire G1 GC gauche");
ItinGareVersCachee it_G2_GC_Gauche (&zoneL2, &aiguilles, &gareCachee, &voieG2, &voieG1, SENS_GAUCHE, C_A2, NORMAL, POUS_A2_NORM, "Itinéraire G2 GC gauche");
ItinGareVersCachee it_G1_GC_Droite (&zoneL1, &aiguilles, &gareCachee, &voieG1, &voieG2, SENS_DROITE, C_A1, DEVIE,  POUS_A1_DEV,  "Itinéraire G1 GC droite");
ItinGareVersCachee it_G2_GC_Droite (&zoneL1, &aiguilles, &gareCachee, &voieG2, &voieG1, SENS_DROITE, C_A1, NORMAL, POUS_A1_NORM, "Itinéraire G2 GC droite");

ItinCacheeVersGare it_GC_G1_Gauche (&zoneL1, &aiguilles, &gareCachee, &voieG1, &voieG2, SENS_GAUCHE, C_A1, DEVIE,  POUS_A1_DEV,  "Itinéraire GC G1 gauche");
ItinCacheeVersGare it_GC_G2_Gauche (&zoneL1, &aiguilles, &gareCachee, &voieG2, &voieG1, SENS_GAUCHE, C_A1, NORMAL, POUS_A1_NORM, "Itinéraire GC G2 gauche");
ItinCacheeVersGare it_GC_G1_Droite (&zoneL2, &aiguilles, &gareCachee, &voieG1, &voieG2, SENS_DROITE, C_A2, DEVIE,  POUS_A2_DEV,  "Itinéraire GC G1 droite");
ItinCacheeVersGare it_GC_G2_Droite (&zoneL2, &aiguilles, &gareCachee, &voieG2, &voieG1, SENS_DROITE, C_A2, NORMAL, POUS_A2_NORM, "Itinéraire GC G2 droite");
#else
ItinGareVersCachee it_G1_GC_Gauche (&zoneL2, &aiguilles, &gareCachee, &voieG1, &voieG2, SENS_GAUCHE, C_A2, DEVIE,  POUS_A2_DEV);
ItinGareVersCachee it_G2_GC_Gauche (&zoneL2, &aiguilles, &gareCachee, &voieG2, &voieG1, SENS_GAUCHE, C_A2, NORMAL, POUS_A2_NORM);
ItinGareVersCachee it_G1_GC_Droite (&zoneL1, &aiguilles, &gareCachee, &voieG1, &voieG2, SENS_DROITE, C_A1, DEVIE,  POUS_A1_DEV);
ItinGareVersCachee it_G2_GC_Droite (&zoneL1, &aiguilles, &gareCachee, &voieG2, &voieG1, SENS_DROITE, C_A1, NORMAL, POUS_A1_NORM);

ItinCacheeVersGare it_GC_G1_Gauche (&zoneL1, &aiguilles, &gareCachee, &voieG1, &voieG2, SENS_GAUCHE, C_A1, DEVIE,  POUS_A1_DEV);
ItinCacheeVersGare it_GC_G2_Gauche (&zoneL1, &aiguilles, &gareCachee, &voieG2, &voieG1, SENS_GAUCHE, C_A1, NORMAL, POUS_A1_NORM);
ItinCacheeVersGare it_GC_G1_Droite (&zoneL2, &aiguilles, &gareCachee, &voieG1, &voieG2, SENS_DROITE, C_A2, DEVIE,  POUS_A2_DEV);
ItinCacheeVersGare it_GC_G2_Droite (&zoneL2, &aiguilles, &gareCachee, &voieG2, &voieG1, SENS_DROITE, C_A2, NORMAL, POUS_A2_NORM);
#endif

Itineraire *apItin[NB_ITIN] = {
        &it_G1_GC_Gauche,
        &it_G2_GC_Gauche,
        &it_G1_GC_Droite,
        &it_G2_GC_Droite,
        &it_GC_G1_Gauche,
        &it_GC_G2_Gauche,
        &it_GC_G1_Droite,
        &it_GC_G2_Droite
};


ChefDeGare chef (&aiguilles, &gareCachee, &affichage, &selMode, &boutons, &voieG1, &voieG2, apItin);


void setup() {
        #ifdef DEBUG
        delay (2000);
        Serial.begin (115200);
        Serial.println ("*** DEBUG ***");
        #endif
       
        Tache::setupTaches ();

        led_board.clignoter ();
        gareCachee.rafraichirLcd ();
} // setup


void loop() {
        Tache::loopTaches ();               
} // loop


Le mode debug
J'ai regroupé tous les #define liés au matériel, ceux d'usage général ainsi que ceux qui enclenchent le mode debug dans le fichier Hardware.h.

// Debug.
//#define DEBUG_BOUTONS_AN
//#define DEBUG_BOUTONS_STATE
//#define DEBUG_MAIN_LOOP
//#define DEBUG_BOUTONS_ROT
//#define DEBUG_ZONE
//#define DEBUG_GC
//#define DEBUG_AIG
//#define DEBUG_AIG_DEM
//#define DEBUG_LCD
//#define DEBUG_CHEF
//#define DEBUG_ITIN


#define DEBUG   (defined DEBUG_MAIN_LOOP || defined DEBUG_BOUTONS_AN || defined DEBUG_BOUTONS_STATE || defined DEBUG_BOUTONS_ROT || \
defined DEBUG_ZONE || defined DEBUG_GC || defined DEBUG_AIG || defined DEBUG_AIG_DEM) || defined DEBUG_LCD || defined DEBUG_CHEF || \
defined DEBUG_ITIN

Chaque instruction #define DEBUG_XXX peut être décommentée pour permettre d'enclencher le mode debug de la classe correspondante. La dernière instruction #define DEBUG ... teste si au-moins un des autres #define DEBUG_XXX est défini. Ainsi, le programme principal peut initialiser la liaison série.

Classe Zone
Le réseau comporte 8 zones de détection: 4 en gare cachée, 2 en gare, 2 en lignes. Les états de l'automate de cette classe sont:
Le graphe d'états correspondant est:

(https://i42.servimg.com/u/f42/15/74/35/09/zotats10.jpg) (https://servimg.com/view/15743509/362)

La réservation préalable des zones est utilisée par les itinéraires. Cela permet de protéger les zones de ligne à voie unique et de déterminer dans quel sens les trains sont en gare. La gare cachée dont les voies comportent 2 zones de détection permettant de déterminer le sens des trains n'a pas besoin de la réservation.

class Zone: public Tache
{
public:
        #ifdef DEBUG_ZONE
        Zone (uint8_t xPin, uint32_t nxDelaiEntree, uint32_t nxDelaiSortie, const char sxNom[]);
        #else
        Zone (uint8_t xPin, uint32_t nxDelaiEntree, uint32_t nxDelaiSortie);
        #endif
       
        void setup ();
        void loop ();

        uint8_t bLibre (void);
        uint8_t bOccupee (void);
        uint8_t bOccupee (uint8_t exSens);
        uint8_t bReservee (void);
        uint8_t bReservee (uint8_t exSens);
        uint8_t bEtatStable (void);
        void reserver (uint8_t exSens);
private:
        const uint8_t Pin;
        const uint32_t nDelaiEntree, nDelaiSortie;
        uint32_t tDebut;

        #ifdef DEBUG_ZONE
        char sNom[32];
        #endif

        uint8_t eEtat;
};

#include "Hardware.h"
#include "Zone.h"


#define E_LIBRE         0
#define E_RESERVE_G     1
#define E_RESERVE_D     2
#define E_ENTREE_G      3
#define E_ENTREE_D      4
#define E_OCCUPEE_G     5
#define E_OCCUPEE_D     6
#define E_ENTREE        7
#define E_OCCUPEE       8



#ifdef DEBUG_ZONE
Zone::Zone (uint8_t xPin, uint32_t nxDelaiEntree, uint32_t nxDelaiSortie, const char sxNom[]) : Pin (xPin), nDelaiEntree (nxDelaiEntree), nDelaiSortie (nxDelaiSortie)
{
        strcpy (sNom, sxNom);
} // Zone::Zone
#else
Zone::Zone (uint8_t xPin, uint32_t nxDelaiEntree, uint32_t nxDelaiSortie) : Pin (xPin), nDelaiEntree (nxDelaiEntree), nDelaiSortie (nxDelaiSortie) {};
#endif


void Zone::setup ()
{
        pinMode (Pin, INPUT_PULLUP);
        eEtat = E_LIBRE;
} // Zone::setup


void Zone::loop ()
{
uint8_t bCapteur;

        bCapteur = digitalRead (Pin);

        switch (eEtat)
        {
                case E_LIBRE:
                        // Zone initialement libre, train détecté sans réservation préalable.
                        if (bCapteur == LOW)
                        {
                                tDebut = millis ();
                                eEtat = E_ENTREE;
                        } // if
                break;

                case E_ENTREE:
                        if ((millis () - tDebut) > nDelaiEntree)
                        {
                                #ifdef DEBUG_ZONE
                                Serial.print (sNom);
                                Serial.println (": occupée");                               
                                #endif

                                tDebut = millis ();
                                eEtat = E_OCCUPEE;
                        } // if
                break;

                case E_RESERVE_G:
                        // Zone libre, mais réservée pour un train tournant à gauche.
                        if (bCapteur == LOW)
                        {
                                tDebut = millis ();
                                eEtat = E_ENTREE_G;
                        } // if
                break;

                case E_ENTREE_G:
                        if ((millis () - tDebut) > nDelaiEntree)
                        {
                                #ifdef DEBUG_ZONE
                                Serial.print (sNom);
                                Serial.println (": occupée G");                               
                                #endif

                                tDebut = millis ();
                                eEtat = E_OCCUPEE_G;
                        } // if
                break;
 
                case E_RESERVE_D:
                        // Zone libre, mais réservée pour un train tournant à droite.
                        if (bCapteur == LOW)
                        {
                                tDebut = millis ();
                                eEtat = E_ENTREE_D;
                        } // if
                break;             
 
                case E_ENTREE_D:
                        if ((millis () - tDebut) > nDelaiEntree)
                        {
                                #ifdef DEBUG_ZONE
                                Serial.print (sNom);
                                Serial.println (": occupée D");                               
                                #endif

                                tDebut = millis ();
                                eEtat = E_OCCUPEE_D;
                        } // if
                break;

                // Ces 3 états peuvent être traités de la même manière.
                case E_OCCUPEE_G:
                case E_OCCUPEE_D:
                case E_OCCUPEE:
                        // Si le train est toujours présent, reset du timer.
                        if (bCapteur == LOW)
                        {
                                tDebut = millis ();
                        }
                        else if ((millis () - tDebut) > nDelaiSortie)
                        {
                                // Plus de train.
                                #ifdef DEBUG_ZONE
                                Serial.print (sNom);
                                Serial.println (": libre");
                                #endif
                               
                                eEtat = E_LIBRE;
                        } // if
                break;
                 
        } // switch
} // Zone::loop


uint8_t Zone::bLibre (void)
{
        return (eEtat == E_LIBRE);       
} // Zone::bLibre


uint8_t Zone::bOccupee (void)
{
        return ((eEtat == E_OCCUPEE) || (eEtat == E_OCCUPEE_G) || (eEtat == E_OCCUPEE_D));
} // Zone::setup


uint8_t Zone::bOccupee (uint8_t exSens)
{
        return (
                ((exSens == SENS_GAUCHE) && (eEtat == E_OCCUPEE_G)) ||
                ((exSens == SENS_DROITE) && (eEtat == E_OCCUPEE_D))
               );
} // Zone::bOccupee


uint8_t Zone::bReservee (void)
{
        return (
                (eEtat == E_RESERVE_G) || (eEtat == E_RESERVE_D)
               );       
} // Zone::bReservee


uint8_t Zone::bReservee (uint8_t exSens)
{
        return (
                ((exSens == SENS_GAUCHE) && (eEtat == E_RESERVE_G)) ||
                ((exSens == SENS_DROITE) && (eEtat == E_RESERVE_D))
               );       
} // Zone::bReservee


uint8_t Zone::bEtatStable (void)
{
        // La zone ne doit pas être dans un état où un train entre,
        // risque de conflit avec le train sortant.
        return (
                (eEtat != E_ENTREE) &&
                (eEtat != E_ENTREE_G) &&
                (eEtat != E_ENTREE_D)
                );                     
} // Zone::bEtatStable


void Zone::reserver (uint8_t exSens)
{
        if (eEtat == E_LIBRE)
        {
                if (exSens == SENS_GAUCHE)
                {
                        #ifdef DEBUG_ZONE
                        Serial.print (sNom);
                        Serial.println (": réservée G");                               
                        #endif
                       
                        eEtat = E_RESERVE_G;
                }
                else if (exSens == SENS_DROITE)
                {
                        #ifdef DEBUG_ZONE
                        Serial.print (sNom);
                        Serial.println (": réservée D");                               
                        #endif
                       
                        eEtat = E_RESERVE_D;
                } // if
        } // if
} // Zone::reserver

La logique de l'automate est répartie entre la méthode loop et les méthodes publiques appelées par les autres objets. Comme toutes les autres classes, celle-ci hérite de la classe Tache.
Cette classe comporte 2 valeurs de timeout qui sont utilisées comme suit:
Ces timeouts permettent également de gérer l'absence de détection sur les derniers wagons des trains, seules les locos sont détectées.
Titre: Re : Réseau hivernal en N et machines d'états
Posté par: savignyexpress le mai 27, 2019, 10:54:20 pm
Classe Aiguilles
Les 5 aiguilles du réseau sont commandées en 2 groupes:Prenons l'exemple de la gare cachée, les 2 aiguilles ont chacune un relais pour le choix de la position: normal ou dévié. En série avec ces 2 relais, un 3ème relais prend en charge l'impulsion. En gare principale, c'est le même principe avec une aiguille de plus pour la voie de garage.

Le graphe d'états est:

(https://i42.servimg.com/u/f42/15/74/35/09/zotats11.jpg) (https://servimg.com/view/15743509/363)

#define NORMAL  HIGH
#define DEVIE   LOW

#define NB_AIG  5
#define C_A1    0
#define C_A2    1
#define C_A3    2
#define C_A4    3
#define C_A5    4

struct Taig
{
        uint8_t eEtat;
        uint8_t eEtatNouv;
        uint8_t nRelais;
        uint8_t nRelaisGroupe;
        uint8_t nMin, nMax;     // Aiguilles min et max du groupe.
};
       

class Aiguilles: public Tache
{
public:
        void setup ();
        void loop ();

        void changer (uint8_t nxAig, uint8_t exEtat);
        uint8_t eEtatAig (uint8_t nxAig);
        uint8_t bChangee (uint8_t nxAig);

private:
        struct Taig aig[NB_AIG];
        uint8_t nAigCourante, eEtatGlobal;
        uint32_t tDebut;
};

#include "Aiguilles.h"
#include "Hardware.h"


#define DUREE_ATTENTE_AIG   250
#define DUREE_ATTENTE_COM       150
#define DUREE_ACTIVE    70

#define E_INACTIF       0
#define E_ATTENTE       1
#define E_ATTENTE_COM   2
#define E_ACTIF         3


#ifdef DEBUG_AIG
static void tracerEtat (uint8_t exEtat)
{
        Serial.print (millis (), DEC);
        Serial.print (" - Aig: ");
        switch (exEtat)
        {
                case E_INACTIF: Serial.println ("E_INACTIF"); break;
                case E_ATTENTE: Serial.println ("E_ATTENTE"); break;
                case E_ATTENTE_COM: Serial.println ("E_ATTENTE_COM"); break;
                case E_ACTIF: Serial.println ("E_ACTIF"); break;
        } // switch
} // tracerEtat
#else
#define tracerEtat(x)
#endif

#ifdef DEBUG_AIG
static void afficherAig (uint8_t nxAig, uint8_t exEtat)
{
        Serial.print ("Aig: ");
        Serial.print (nxAig+1, DEC);
        Serial.print (" - ");
        if (exEtat == NORMAL)
        {
                Serial.println ("normal");
        }
        else
        {
                Serial.println ("dévié");
        } // if
} // afficherAig
#else
#define afficherAig(x, y)
#endif


void Aiguilles::setup ()
{
        #ifdef ACTIVER_AIG
        pinMode (REL_COM_GC, OUTPUT);
        pinMode (REL_A4, OUTPUT);
        pinMode (REL_A5, OUTPUT);

        pinMode (REL_COM_GARE, OUTPUT);
        pinMode (REL_A1, OUTPUT);
        pinMode (REL_A2, OUTPUT);
        pinMode (REL_A3, OUTPUT);
        #endif

        aig[C_A1].nRelais = REL_A1;
        aig[C_A2].nRelais = REL_A2;
        aig[C_A3].nRelais = REL_A3;
        aig[C_A4].nRelais = REL_A4;
        aig[C_A5].nRelais = REL_A5;

        aig[C_A1].nRelaisGroupe = REL_COM_GARE;
        aig[C_A2].nRelaisGroupe = REL_COM_GARE;
        aig[C_A3].nRelaisGroupe = REL_COM_GARE;       
        aig[C_A4].nRelaisGroupe = REL_COM_GC;
        aig[C_A5].nRelaisGroupe = REL_COM_GC;

        // Au démarrage, pour positionner toutes les aiguilles, on considère
        // que le nouvel état est systématiquement différent de l'état courant.
        aig[C_A1].eEtat = NORMAL;
        aig[C_A2].eEtat = DEVIE;
        aig[C_A3].eEtat = NORMAL;
        aig[C_A4].eEtat = DEVIE;
        aig[C_A5].eEtat = NORMAL;

        aig[C_A1].eEtatNouv = DEVIE;
        aig[C_A2].eEtatNouv = NORMAL;
        aig[C_A3].eEtatNouv = DEVIE;
        aig[C_A4].eEtatNouv = NORMAL;
        aig[C_A5].eEtatNouv = DEVIE;

        aig[C_A1].nMin = C_A1;
        aig[C_A2].nMin = C_A1;       
        aig[C_A3].nMin = C_A1;
        aig[C_A4].nMin = C_A4;               
        aig[C_A5].nMin = C_A4;

        aig[C_A1].nMax = C_A3;
        aig[C_A2].nMax = C_A3;       
        aig[C_A3].nMax = C_A3;
        aig[C_A4].nMax = C_A5;               
        aig[C_A5].nMax = C_A5;             
               
        nAigCourante = C_A1;
        eEtatGlobal = E_INACTIF;
        tracerEtat (eEtatGlobal);
} // Aiguilles::setup


void Aiguilles::loop ()
{
        switch (eEtatGlobal)
        {
                case E_INACTIF:
                        // Demande d'activation d'une aiguille.
                        if (aig[nAigCourante].eEtatNouv != aig[nAigCourante].eEtat)
                        {
                                eEtatGlobal = E_ATTENTE;
                                tracerEtat (eEtatGlobal);
                                tDebut = millis ();     
                               
                                #ifdef DEBUG_AIG
                                Serial.println ("************");
                                Serial.print ("Aig: ");
                                Serial.print (nAigCourante+1, DEC);
                                Serial.print (" - changement demandé - ");
                                if (aig[nAigCourante].eEtatNouv == NORMAL)
                                {
                                        Serial.println ("normal");
                                }
                                else
                                {
                                        Serial.println ("dévié");
                                } // if
                                #endif
                        }
                        else
                                nAigCourante = (nAigCourante + 1) % NB_AIG;
                break;
               
                case E_ATTENTE:
                        if ((millis () - tDebut) > DUREE_ATTENTE_AIG)
                        {
                                eEtatGlobal = E_ATTENTE_COM;
                                tracerEtat (eEtatGlobal);
                                tDebut = millis ();
                               
                                // On ne change que l'aiguille en question, les autres du même groupe gardent leurs anciennes valeurs.
                                for (uint8_t i = aig[nAigCourante].nMin; i <= aig[nAigCourante].nMax; i++)
                                {
                                        if (nAigCourante == i)
                                        {       
                                                #ifdef ACTIVER_AIG
                                                digitalWrite (aig[i].nRelais, aig[i].eEtatNouv);
                                                #endif

                                                afficherAig (i, aig[i].eEtatNouv);
                                        }
                                        else
                                        {
                                                #ifdef ACTIVER_AIG
                                                digitalWrite (aig[i].nRelais, aig[i].eEtat);
                                                #endif

                                                afficherAig (i, aig[i].eEtat);
                                        } // if
                                } // for
                        } // if
                break;

                case E_ATTENTE_COM:
                        if ((millis () - tDebut) > DUREE_ATTENTE_COM)
                        {
                                eEtatGlobal = E_ACTIF;
                                tracerEtat (eEtatGlobal);             
                                tDebut = millis ();
                               
                                #ifdef ACTIVER_AIG
                                digitalWrite (aig[nAigCourante].nRelaisGroupe, HIGH);
                                #endif

                                #ifdef DEBUG_AIG
                                if (aig[nAigCourante].nRelaisGroupe == REL_COM_GARE)
                                {
                                        Serial.println ("Aig: relais gare HIGH");
                                }
                                else
                                {
                                        Serial.println ("Aig: relais gare cachée HIGH");               
                                } // if
                                #endif       
                        } // if
                break;

                case E_ACTIF:
                        if ((millis () - tDebut) > DUREE_ACTIVE)
                        {
                                eEtatGlobal = E_INACTIF;             
                                tracerEtat (eEtatGlobal);
                                                               
                                // Une aiguille est en cours d'activation, on peut relacher le relais du groupe correspondant.
                                #ifdef ACTIVER_AIG
                                digitalWrite (aig[nAigCourante].nRelaisGroupe, LOW);
                                #endif

                                #ifdef DEBUG_AIG
                                if (aig[nAigCourante].nRelaisGroupe == REL_COM_GARE)
                                {
                                        Serial.println ("Aig: relais gare LOW");
                                }
                                else
                                {
                                        Serial.println ("Aig: relais gare cachée LOW");
                                } // if
                                #endif                               

                                // On peut désactiver tous les relais.
                                #ifdef DEBUG_AIG
                                Serial.println ("Aig: désactivation des relais");
                                #endif
                       
                                for (uint8_t i = C_A1; i <= C_A5; i++)
                                {
                                        #ifdef ACTIVER_AIG
                                        digitalWrite (aig[i].nRelais, DEVIE);
                                        #endif                               
                                } // for

                                // L'aiguille courante a été changée, passer à la suivante.
                                aig[nAigCourante].eEtat = aig[nAigCourante].eEtatNouv;
                                nAigCourante = (nAigCourante + 1) % NB_AIG;
                        } // if
                break;
        } // switch
} // Aiguilles::loop


void Aiguilles::changer (uint8_t nxAig, uint8_t exEtat)
{
        aig[nxAig].eEtatNouv = exEtat;

        #ifdef DEBUG_AIG_DEM
        Serial.print ("Aig: ");
        Serial.print (nxAig+1, DEC);
        Serial.print (" - méthode Changer appelée - ");
        if (exEtat == NORMAL)
        {
                Serial.println ("normal");
        }
        else
        {
                Serial.println ("dévié");       
        } // if
        #endif
} // Aiguilles::changer


uint8_t Aiguilles::eEtatAig (uint8_t nxAig)
{
        if (aig[nxAig].eEtatNouv != aig[nxAig].eEtat)
        {
                return (aig[nxAig].eEtatNouv);
        }
        else
        {
                return (aig[nxAig].eEtat);
        } // if
} // Aiguilles::eEtatAig


uint8_t Aiguilles::bChangee (uint8_t nxAig)
{
        return (aig[nxAig].eEtatNouv == aig[nxAig].eEtat);
} // Aiguilles::bChangee

Cette classe parcourt continuellement le tableau des 5 aiguilles et elle teste si une nouvelle position est demandée. Elle fait en sorte de n'activer qu'une aiguille à la fois. Les états permettent des attentes entre les activations des relais de position et d'impulsion.
Titre: Re : Réseau hivernal en N et machines d'états
Posté par: savignyexpress le mai 27, 2019, 11:08:59 pm
Classe Signal
Nous avons suffisamment d'éléments pour gérer les 2 signaux aux extrémités de la gare. J'ai implanté des signaux groupés CFF à 3 leds: rouge, vert et jaune, avec les significations suivantes:

(https://i58.servimg.com/u/f58/15/74/35/09/signal11.jpg) (https://servimg.com/view/15743509/339)
Rouge: sortie interdite.

(https://i58.servimg.com/u/f58/15/74/35/09/signal13.jpg) (https://servimg.com/view/15743509/341)
Vert: sortie autorisée sur voie 1

(https://i58.servimg.com/u/f58/15/74/35/09/signal12.jpg) (https://servimg.com/view/15743509/340)
Vert et jaune (orange dans la réalité, mais je n'ai pas trouvé de led orange de cette taille): sortie autorisée sur voie 2.

#include "Aiguilles.h"
#include "Tache.h"
#include "Zone.h"


class Signal: public Tache
{
public:
        Signal (Aiguilles *pxAiguilles, uint8_t xAig, Zone *pxZone, uint8_t xPinR, uint8_t xPinO, uint8_t xPinV);

        void setup ();
        void loop ();

private:
        Aiguilles *pAiguilles;
        const uint8_t nAig;
        Zone *pZone;       
        const uint8_t pinR, pinO, pinV;
};

La classe comporte un pointeur sur l'objet qui gère les aiguilles ainsi qu'un pointeur sur la zone correspondante. Les 2 signaux sont définis comme suit dans le fichier .ino en passant les adresses de l'objet aiguilles et des objets zoneL1 et zoneL2 en plus du numéro de l'aiguille et des pins correspondant aux leds.

Signal sigA1 (&aiguilles, C_A1, &zoneL1, SIG_A1_ROUGE, SIG_A1_ORANGE, SIG_A1_VERT);
Signal sigA2 (&aiguilles, C_A2, &zoneL2, SIG_A2_ROUGE, SIG_A2_ORANGE, SIG_A2_VERT);

Le code de la classe est simple, une machine à états n'est pas nécessaire dans ce cas. Les cathodes des leds sont reliées à l'Arduino, donc la valeur LOW allume la led et HIGH l'éteint:

#include "Aiguilles.h"
#include "Hardware.h"
#include "Signal.h"
#include "Zone.h"


Signal::Signal (Aiguilles *pxAiguilles, uint8_t xAig, Zone *pxZone, uint8_t xPinR, uint8_t xPinO, uint8_t xPinV) :
        pAiguilles (pxAiguilles), nAig (xAig), pZone (pxZone), pinR (xPinR), pinO (xPinO), pinV (xPinV) {}

void Signal::setup ()
{
        pinMode (pinR, OUTPUT);
        pinMode (pinO, OUTPUT);       
        pinMode (pinV, OUTPUT);       
} // Signal::setup


void Signal::loop ()
{
        if (pZone->bOccupee())
        {
                digitalWrite (pinR, LOW);
                digitalWrite (pinO, HIGH);
                digitalWrite (pinV, HIGH);       
        }
        else
        {
                digitalWrite (pinR, HIGH);
                digitalWrite (pinV, LOW);

                if (pAiguilles->eEtatAig (nAig) == DEVIE)
                {
                        digitalWrite (pinO, HIGH);
                }
                else
                {
                        digitalWrite (pinO, LOW);
                } // if
        } // if
} // Signal::loop

L'état de la zone définit s'il faut allumer la led verte ou la rouge. La led jaune quant à elle dépend en plus de l'état de l'aiguille.
Titre: Re : Réseau hivernal en N et machines d'états
Posté par: savignyexpress le novembre 24, 2019, 12:48:41 pm
Bonjour à tous,

Voici le dernier état de mon réseau Secondaire et ligne de bus.

https://www.youtube.com/watch?v=ZSrhL8LwRko (https://www.youtube.com/watch?v=ZSrhL8LwRko)

Les travaux effectués à ce jour sont:

Il reste à faire le décor hivernal ainsi que la pose de l'éclairage public et des bâtiments.

Bon dimanche à tous.
Titre: Re : Réseau hivernal en N et machines d'états
Posté par: Dominique le novembre 25, 2019, 02:57:41 pm
Bonjour Marc-Henri,

Il est remarquable de pouvoir lire la conception et la réalisation d'un projet de A jusqu'à Z avec les détails permettant à d'autres modélistes de pouvoir s'en inspirer. Merci Marc-Henri car la pédagogie est excellente  ;D

J'avais bien compris que les couples en bois permettraient de réaliser une gare cachée sous la montagne et une route pour permettre à un autocar de circuler entre le bas et le haut du village. Avec les descriptions des objets logiciels (zones, aiguilles, signaux, ..) je m'attendais à un fonctionnement impeccable, ce que la vidéo confirme avec brio : Bravo et merci encore pour le partage sur Locoduino.

J'avoue que le système Magnorail pour le bus est très réaliste, sa vitesse est faible ce qui est plus beau à voir. Quels composants as-tu utilisé sur (https://www.magnorail.com/fr/products (https://www.magnorail.com/fr/products))
Les automatismes de circulation m'impressionnent également, avec ces caractères spéciaux sur l'écran LCD ! (je vais devoir faire un effort sur mon réseau, cela me donne du courage !)

Bravo ! et la suite avec impatience, avec la neige qui commence tôt cette année  8)
Titre: Re : Réseau hivernal en N et machines d'états
Posté par: savignyexpress le novembre 27, 2019, 07:33:44 am
Bonjour Dominique,

Un tout grand merci pour tous ces compliments !

Je n'ai pas terminé la description des objets mis en oeuvre, il reste: les boutons, l'affichage LCD, la gare cachée, les itinéraires et le chef de gare. Ce dernier objet permet de choisir et gérer le mode d'exploitation du réseau, soient:
Le système Magnorail se compose de:
Meilleures salutations et à bientôt.
Titre: Re : Réseau hivernal en N et machines d'états
Posté par: Dominique le janvier 07, 2020, 04:59:05 pm
Bonjour Marc-Henri,

Tout d’abord, je t’adresse mes meilleurs vœux pour cette nouvelle année, au nom de toute l’équipe Locoduino.

Cette année, forcément nous te décernerons la note 20/20 pour ton réseau hivernal qui est un bel exemple de ce que quelques Arduino peuvent faire (avec la complicité du modélisme quand même).

Mais ma curiosité se porte sur ton chef de gare, pour animer ton réseau.

Je vais attendre patiemment !

Bon courage et que le beau temps dure sur les Alpes.

Bien cordialement
Dominique
Titre: Re : Réseau hivernal en N et machines d'états
Posté par: Dominique le janvier 07, 2020, 07:29:52 pm
En complément de ce que je viens d'écrire, j'avoue que la lecture de la page de Paul Murray https://paulmurraycbr.github.io/ArduinoTheOOWay.html (https://paulmurraycbr.github.io/ArduinoTheOOWay.html) est une introduction nécessaire pour entrer dans la suite (ces notions ne m'étaient pas encore acquises, j'ai honte !)

Pour avancer un peu j'ai assemblé les morceaux de ton code dans un programme Arduino et, j'ai déjà la LED_BUILTIN clignotante sur un Due. C'est un bon début !

Je joins ci-dessous le programme obtenu dans lequel j'ai désactivé tout sauf la Led (pour me dire si j'ai assemblé tout cela comme il faut), étant donné que je devrais adapter le programme à mon réseau, ce qui passe par une description des éléments zones, aiguilles et signaux conforme à mon réseau et leurs interactions sous forme d'un fichier de description, j'imagine, que je n'ai pas trouvé.

Bien cordialement
Dominique
Titre: Re : Réseau hivernal en N et machines d'états
Posté par: savignyexpress le janvier 08, 2020, 11:21:05 am
Bonjour Dominique,

À mon tour je t'adresse mes meilleurs voeux pour 2020 avec de belles réalisations, bonheur et santé.

Tu as fait le choix de regrouper le code de chaque classe avec l'entête, cela a l'avantage de réduire le nombre de fichiers et du moment que cela compile, cela devrait marcher.

Je n'ai pas encore eu le temps de décrire les autres objets, notamment dessiner au propre les graphes d'états qui n'existent que dans un cahier de notes manuscrites. Dans les prochains jours, je posterai déjà le zip avec tous les sources de la version actuelle de mon projet.

Meilleures salutations.
Marc-Henri
Titre: Re : Réseau hivernal en N et machines d'états
Posté par: savignyexpress le janvier 09, 2020, 09:23:37 am
Bonjour à tous,

Voici, en pièce jointe, le fichier zip contenant les sources de la version actuelle du logiciel de mon réseau hivernal.

Meilleures salutations.
Titre: Re : Réseau hivernal en N et machines d'états
Posté par: Dominique le janvier 09, 2020, 12:17:19 pm
Bonjour Marc-Henri,

Un grand merci de partager ces sources dans lesquels j'ai cherché une forme de description de la topographie du réseau (liste des zones et aiguilles et et connexions entre les zones et aiguilles pour indiquer les objets adjacents).

C'est probablement en dur dans l'automate, mais je commence seulement à explorer cet immense et beau travail et cela m'a probablement échappé !

Je dois dire qu'étant en vacances dans mes montagnes aux Contamines, je ne suis équipé que d'un vieux Mac et de l'IDE 1.6.0 dans lequel Java ne veut pas ouvrir le programme. Donc je regarde avec TextWrangler.
Titre: Re : Réseau hivernal en N et machines d'états
Posté par: savignyexpress le janvier 09, 2020, 01:53:05 pm
Bonjour Dominique,

La topographie du réseau est implantée par les classes Itineraire, ItinGareVersCachee et ItinCacheeVersGare, les 2 dernières classes héritant de la 1ère. La gare cachée ayant une certaine autonomie par rapport au reste du réseau, il n'était pas possible d'utiliser exactement le même code pour les itinéraires entrant et sortant de la gare cachée, d'où les 2 classes.

La classe Itineraire contient:
Les autres pointeurs se réfèrent aux objets communs indispensables: le gestionnaire des aiguilles et la gare cachée. Tous ces pointeurs sont instanciés dans le module principal lors de la définition des objets.

Il y a en tout 8 itinéraires possibles qui sont ensuite regroupés dans le tableau de pointeurs apItin. Ce tableau permet au chef de gare de traiter les itinéraires de manière générique. Avec un petit réseau tel que celui-ci, cette approche codée en dur reste raisonnable. Mais pour un système plus général, les structures de données décrites sur le forum, notamment par Jean-Luc et Denis, sont plus adéquates.

Aux Contamines tu n'es pas très loin de la Suisse romande. Nous voyons le Mont-Blanc depuis beaucoup d'endroits: Jura vaudois, plateau vaudois, Morges, préalpes fribourgeoises, domaine skiable de Villars, etc... C'est donc aussi un peu notre sommet !

Meilleures salutations.
Titre: Re : Réseau hivernal en N et machines d'états
Posté par: Dominique le janvier 11, 2020, 05:22:33 pm
Bonjour Marc-Henri,

Je prends mon temps pour étudier les objets de ton réseau (et marcher dans la neige pendant la courte période de soleil de 11H à 14H dans la vallée de Notre-Dame-De-La-Gorge). On comprend bien qu'il y a plusieurs machines à états, chaque objet gérant ses capteurs (zones) et ses relais (aiguilles).

Je n'ai pas (encore) trouvé comment sont commandés les trains en DCC, accélérations, paliers, ralentissements (par dissymétrie du signal DCC, par diodes et relais), arrêts, en manuel ou automatique ?. Quelle est l'interface avec la centrale DCC ?

Je suis d'accord pour cette approche des itinéraires et scenarii de circulation codés en durs dans ce cas de réseau simple mais riche en possibilités du fait des deux réseaux rail et route : la vidéo est très intéressante.

Je ne veux pas bousculer ton planning de publications sur Locoduino, il n'y a aucune urgence à répondre  ;D

J'imagine bien cette réalisation faire l'objet d'un article sur le site rédactionnel !

Bien cordialement
Dominique
Titre: Re : Réseau hivernal en N et machines d'états
Posté par: savignyexpress le janvier 13, 2020, 08:42:29 am
Bonjour Dominique,

Voici déjà quelques réponses rapides:
Profite bien du beau temps, bon début de semaine et meilleures salutations.
Marc-Henri
Titre: Re : Réseau hivernal en N et machines d'états
Posté par: savignyexpress le novembre 27, 2020, 11:47:54 am
Bonjour à tous,

J'ai profité d'un coup de soleil éclairant le réseau pour tourner cette vidéo. La neige est tombée presque partout, il reste à peaufiner les détails, placer les personnages et quelques véhicules routiers immobiles.

https://www.youtube.com/watch?v=pDbFbupgoAk (https://www.youtube.com/watch?v=pDbFbupgoAk)

Bonne fin de semaine à tous.
Titre: Re : Réseau hivernal en N et machines d'états
Posté par: Dominique le novembre 27, 2020, 11:59:01 am
Merci Marc-Henri,

Je me suis permis de modifier l'url de ta video pour qu'elle apparaisse : enlever le "youtu.be" et trouver l'url  avec "youtube.com". J'avoue que je ne sais pas pourquoi (faute de recherche).

C'est vraiment très joli, quel décor superbe  ;D
La vitesse de l'autocar me semble parfaite mais celle des trains me semble un peu trop rapide par rapport à la réalité.

J'aime beaucoup les automatismes et merci pour cet exemple qui donne peut-être une idée de ce qu'on pourrait faire avec la centrale ESP32 /wifi / can (désolé je fais la pub).

La suisse est belle !

Bien amicalement
Dominique
Titre: Re : Réseau hivernal en N et machines d'états
Posté par: savignyexpress le novembre 28, 2020, 10:55:08 pm
Merci beaucoup Dominique pour tes compliments et pour la correction de l'URL YouTube.

C'est vrai qu'en faisant circuler les trains plus lentement, c'est mieux. C'est simple à faire, il suffit de réduire la vitesse sur la centrale. Cela ne pose aucun problème à l'Arduino qui gère cela car il "attend" les trains dans les zones de détection avant de prendre la prochaine décision.

Il faudra que je prenne du temps pour me familiariser avec l'ESP32 !

Bon dimanche.
Titre: Re : Réseau hivernal en N et machines d'états
Posté par: savignyexpress le janvier 02, 2021, 05:01:07 pm
Bonjour à tous,

Je vous souhaite mes meilleurs voeux pour une année 2021 moins perturbée que 2020, de belles réalisations en modélisme et Arduino, mais surtout une bonne santé.

J'ai terminé mon réseau hivernal, voici une vidéo montrant le tout dernier état.

https://www.youtube.com/watch?v=IscK2AdH820 (https://www.youtube.com/watch?v=IscK2AdH820)

Le dernier article de mon blog montre des photos du réseau: https://savignyexpress.wordpress.com/2021/01/02/reseau-hivernal-suite-et-fin/ (https://savignyexpress.wordpress.com/2021/01/02/reseau-hivernal-suite-et-fin/)

Je reprendrai la description du programme en C++ qui gère la circulation des trains.

Bon début d'année et meilleures salutations.
Titre: Re : Réseau hivernal en N et machines d'états
Posté par: Dominique le janvier 02, 2021, 05:22:36 pm
Bonjour Marc-Henri,

Mes meilleurs voeux également pour une nouvelle année enfin normale, on le mérite bien vu les efforts qu'on a fait pour rester confinés à souder, programmer, etc.. seuls dans notre atelier !

Ton réseau est splendide : le décor, les automatismes et la vidéo sont parfaits : il y a plein d'enseignements dans ce beau projet.

Amicalement
Dominique
Titre: Re : Réseau hivernal en N et machines d'états
Posté par: Thierry le janvier 02, 2021, 06:18:13 pm
Je plussoie, très joli réseau, et une sympathique ambiance bucolique hivernale ! Meilleurs voeux pour cette nouvelle année, et plein de projets modélistiques et Arduinesques !
Titre: Re : Réseau hivernal en N et machines d'états
Posté par: savignyexpress le janvier 22, 2021, 11:17:24 pm
Merci à tous pour vos voeux et pour vos compliments à propos de mon réseau.

Bonne fin de semaine et meilleures salutations.
Titre: Re : Réseau hivernal en N et machines d'états
Posté par: savignyexpress le mai 20, 2021, 02:26:00 pm
Bonjour à tous,

Après une très longue pause, je reprends la description du logiciel de mon réseau hivernal.

Classe Affichage
Comme le montre la vidéo de 2019, le réseau comporte un affichage LCD en face avant destiné à afficher l'état de la gare cachée ainsi que le mode de fonctionnement du réseau: manuel, semi-automatique, automatique.

Par exemple, voies vides, mode automatique:
(https://i.servimg.com/u/f42/15/74/35/09/lcd_vi10.jpg) (https://servimg.com/view/15743509/404)

Un train dans chaque sens sur chaque voie, mode manuel:
(https://i.servimg.com/u/f42/15/74/35/09/lcd_oc10.jpg) (https://servimg.com/view/15743509/403)

La classe Affichage sera utilisée par la classe GareCachee. Le header est le suivant:

//
//      Affichage.h
//
//      Gestion de l'affichage LCD.
//
//      2019.02.19      Création.

#ifndef AFFICHAGE_H
#define AFFICHAGE_H

#include <LiquidCrystal_I2C.h>
#include <stdint.h>

#include "Hardware.h"
#include "Tache.h"


// États de la gare cachée.
#define E_INCONNU       0
#define E_LIBRE         1

// Entrée par la gauche et sortie par la droite.
#define E_ENTREE_G      2
#define E_ARRIVE_G      3
#define E_ARRETE_D      4
#define E_SORTIE_D_DEM  5
#define E_AIG_SORT_D    6
#define E_SORTANT_D1    7
#define E_SORTANT_D2    8

// Entrée par la droite et sortie par la gauche.
#define E_ENTREE_D      9
#define E_ARRIVE_D      10
#define E_ARRETE_G      11
#define E_SORTIE_G_DEM  12
#define E_AIG_SORT_G    13
#define E_SORTANT_G1    14
#define E_SORTANT_G2    15

#define E_TRAVERS       16

#define NB_VOIES_GC        2
#define GC1     0
#define GC2     1

#define NB_AIG_GC       2
#define A4      0
#define A5      1

class Affichage: public Tache
{
public:

        Affichage ();
        void setup ();
        void loop ();

        void afficherVoieGC (uint8_t nxVoie, uint8_t exEtat);
        void afficherAigGC (uint8_t nxAig, uint8_t exEtat);
        void afficherMode (uint8_t nxMode);

        void afficherTempo (const char acxLcd0[], const char acxLcd1[], uint32_t nxDuree);
private:
        uint8_t aEtatVoies[NB_VOIES_GC];
        uint8_t aEtatAig[NB_AIG_GC];

        uint8_t eEtat;

        uint32_t tDebut;
        uint32_t nDureeTempo;

        uint8_t nMode;  // Mode de fonctionnement.

        char acLcd [LCD_LIGNES][LCD_CHARS];

        LiquidCrystal_I2C lcd;

        void afficherGC (void);

        void rafraichir (void);
};

#endif

Comme l'affichage doit tenir compte de l'état de la gare cachée, il m'a semblé naturel que les états possibles de la gare cachée soient définis ici.

À l'instar des autres classes du programme, celle-ci hérite de la classe Tache, ce qui permet l'appel automatique de ses méthodes virtuelles setup et loop. Les autres méthodes publiques affichent l'état des voies de la gare cachée, l'état des aiguilles, le mode de fonctionnement, ainsi que des messages temporaires.

Dans la partie privée, on remarquera la déclaration d'une instance de la classe LiquidCrystal_I2C. La classe fonctionnerait aussi très bien avec un affichage LCD connecté à 4 ou 8 broches de l'Arduino. J'ai fait le choix d'une liaison I2C par un câble blindé car elle est insensible aux perturbations causées par les moteurs d'aiguilles à solénoïde.

Le fichier .cpp de la classe est:

#include <LiquidCrystal.h>

//
//      Affichage.cpp
//
//      2019.02.19      Création.

#include <Arduino.h>
#include <LiquidCrystal_I2C.h>
#include <stdint.h>

#include "Affichage.h"
#include "Aiguilles.h"
#include "Boutons.h"
#include "Hardware.h"


const uint8_t wagonChar[] = {
        0b00000,
        0b11111,
        0b10101,
        0b11111,
        0b11111,
        0b01010,
        0b00000,
        0b00000
};
const uint8_t locoDChar[] = {
        0b00000,
        0b11000,
        0b11010,
        0b11111,
        0b11111,
        0b01010,
        0b00000,
        0b00000
};
const uint8_t locoGChar[] = {
        0b00000,
        0b00011,
        0b01011,
        0b11111,
        0b11111,
        0b01010,
        0b00000,
        0b00000
};
const uint8_t A4devieChar[] = {
        0b00000,
        0b00000,
        0b00000,
        0b10000,
        0b01000,
        0b00100,
        0b00010,
        0b00001         
};
const uint8_t A5devieChar[] = {
        0b00000,
        0b00000,
        0b00000,
        0b00001,
        0b00010,
        0b00100,
        0b01000,
        0b10000                               
};
const uint8_t jonctA4Char[] = {
        0b10000,
        0b01000,
        0b00100,
        0b00011,
        0b00000,
        0b00000,
        0b00000,
        0b00000                       
};
const uint8_t jonctA5Char[] = {
        0b00001,
        0b00010,
        0b00100,
        0b11000,
        0b00000,
        0b00000,
        0b00000,
        0b00000               
};

#define C_WAGON 0
#define C_LOCO_D 1
#define C_LOCO_G 2
#define C_A4_DEV        3
#define C_A5_DEV        4
#define C_JCT_A4        5
#define C_JCT_A5        6

#define C_FLEC_G        0b01111111
#define C_FLEC_D        0b01111110

// Pour centrer l'affichage de la gare cachée.
#define POS_INIT        2
#define POS_A4          (POS_INIT+1)
#define POS_TRAIN       (POS_INIT+4)
#define LONG_TRAIN      4
#define POS_A5          (POS_TRAIN+LONG_TRAIN+2)
#define POS_JCT_A4      (POS_A4+1)
#define POS_JCT_A5      (POS_A5-1)

#define PERIODE 20


// États possibles de l'affichage.

#define E_GARE_CACHEE           0
#define E_GARE_CACHEE_NOUV      1
#define E_MSG_TEMPO             2


#define DUREE_MSG_DEMARRAGE     7000

//Affichage::Affichage () : lcd (LiquidCrystal (LCD_RS, LCD_RW, LCD_EN, LCD_D4, LCD_D5, LCD_D6, LCD_D7)) {}
Affichage::Affichage () : lcd (LiquidCrystal_I2C (0x27, LCD_CHARS, LCD_LIGNES)) {}


void Affichage::setup ()
{
        for (uint8_t nVoie = 0; nVoie < NB_VOIES_GC; nVoie++)
        {
                aEtatVoies[nVoie] = E_INCONNU;
        } // for

        for (uint8_t nAig = 0; nAig < NB_AIG_GC; nAig++)
        {
                aEtatAig[nAig] = NORMAL;
        } // for

        for (uint8_t nLig = 0; nLig < LCD_LIGNES; nLig++)
        {
                for (uint8_t nChar = 0; nChar < LCD_CHARS; nChar++)
                {
                        acLcd [nLig][nChar] = ' ';
                } // for
        } // for
       
        lcd.init ();
        lcd.backlight ();
        lcd.clear ();

        lcd.createChar (C_WAGON, (uint8_t *)wagonChar);
        lcd.createChar (C_LOCO_D, (uint8_t *)locoDChar);
        lcd.createChar (C_LOCO_G, (uint8_t *)locoGChar);
        lcd.createChar (C_A4_DEV, (uint8_t *)A4devieChar);
        lcd.createChar (C_A5_DEV, (uint8_t *)A5devieChar);
        lcd.createChar (C_JCT_A4, (uint8_t *)jonctA4Char);               
        lcd.createChar (C_JCT_A5, (uint8_t *)jonctA5Char);

        eEtat = E_GARE_CACHEE_NOUV;
        afficherTempo (RESEAU_HIVERNAL, VERSION, DUREE_MSG_DEMARRAGE);
} // Affichage::setup


#define BLANCS_1ERE     2
#define TRAITS_1ERE     12
#define BLANCS_2EME     4
#define TRAITS_2EME     6


void Affichage::loop ()
{
static uint8_t ePrev = 255;

        #ifdef DEBUG_LCD
        if (eEtat != ePrev)
        {
                Serial.print (millis (), DEC);
                Serial.print (" - entrée loop - ");
                Serial.println (eEtat, DEC);
        }
        ePrev = eEtat;
        #endif
       
        switch (eEtat)
        {
                case E_MSG_TEMPO:
                        if ((millis () - tDebut) > nDureeTempo)
                        {
                                eEtat = E_GARE_CACHEE_NOUV;

                                #ifdef DEBUG_LCD
                                Serial.print (millis (), DEC);
                                Serial.println (" - loop - E_GARE_CACHEE_NOUV");
                                #endif
                        } // if
                break;

                case E_GARE_CACHEE_NOUV:
                        afficherGC ();
                        eEtat = E_GARE_CACHEE;             
                        tDebut = millis ();

                        #ifdef DEBUG_LCD
                        Serial.print (millis (), DEC);
                        Serial.println (" - loop - E_GARE_CACHEE");
                        #endif                       
                break;

                case E_GARE_CACHEE:
                        if ((millis () - tDebut) > PERIODE)
                        {       
                                tDebut = millis ();
                                rafraichir ();
                        } // if
                break;
        } // switch
} // Affichage::loop


void Affichage::afficherVoieGC (uint8_t nxVoie, uint8_t exEtat)
{
        aEtatVoies[nxVoie] = exEtat;

        #ifdef DEBUG_LCD
        Serial.print (millis (), DEC);
        Serial.print (" - entrée afficherVoieGC: ");
        Serial.println (eEtat, DEC);
        #endif
       
        if (eEtat != E_MSG_TEMPO)
        {
                eEtat = E_GARE_CACHEE_NOUV;

                #ifdef DEBUG_LCD
                Serial.print (millis (), DEC);
                Serial.println (" - afficherVoieGC - E_GARE_CACHEE_NOUV");
                #endif                                       
        } // if
} // Affichage::afficherVoieGC


void Affichage::afficherAigGC (uint8_t nxAig, uint8_t exEtat)
{
        aEtatAig[nxAig] = exEtat;       
        if (eEtat != E_MSG_TEMPO)
        {
                eEtat = E_GARE_CACHEE_NOUV;

                #ifdef DEBUG_LCD
                Serial.print (millis (), DEC);
                Serial.println (" - afficherAigGC - E_GARE_CACHEE_NOUV");
                #endif                                       
        } // if               
} // Affichage::afficherAigGC


void Affichage::afficherMode (uint8_t nxMode)
{
        nMode = nxMode;
        if (eEtat != E_MSG_TEMPO)
        {
                eEtat = E_GARE_CACHEE_NOUV;       

                #ifdef DEBUG_LCD
                Serial.print (millis (), DEC);
                Serial.println (" - afficherMode - E_GARE_CACHEE_NOUV");
                #endif                                       
        } // if               
} // Affichage::afficherMode



void Affichage::afficherTempo (const char acxLcd0[], const char acxLcd1[], uint32_t nxDuree)
{
uint8_t nChar;

        #ifdef DEBUG_LCD
        Serial.print (millis (), DEC);
        Serial.print (" - E_MSG_TEMPO: ");
        Serial.print (acxLcd0);
        Serial.print (" / ");
        Serial.println (acxLcd1);
        #endif
       
        if ((eEtat == E_GARE_CACHEE) || (eEtat == E_GARE_CACHEE_NOUV))
                eEtat = E_MSG_TEMPO;

        nDureeTempo = nxDuree;
        tDebut = millis ();

        for (nChar = 0; nChar < LCD_CHARS; nChar++)
        {
                acLcd[0][nChar] = ' ';
                acLcd[1][nChar] = ' ';               
        } // for

        for (nChar = 0; nChar < (strlen (acxLcd0) <= LCD_CHARS ? strlen (acxLcd0) : LCD_CHARS); nChar++)
        {
                acLcd[0][nChar] = acxLcd0[nChar];
        } // for
               
        for (nChar = 0; nChar < (strlen (acxLcd1) <= LCD_CHARS ? strlen (acxLcd1) : LCD_CHARS); nChar++)
        {
                acLcd[1][nChar] = acxLcd1[nChar];
        } // for

        rafraichir ();
} // Affichage::afficherTempo


void Affichage::afficherGC (void)
{
uint8_t nLig, nChar, n;
       
        // 1ère ligne: caractères fixes.                       
        nLig = 0;
        nChar = 0;
        for (n = 0; n < BLANCS_1ERE; n++)
        {
                acLcd [nLig][nChar++] = ' ';
        } // for

        for (n = 0; n < TRAITS_1ERE; n++)
        {
                acLcd [nLig][nChar++] = '-';
        } // for

        for (n = 0; n < BLANCS_1ERE; n++)
        {
                acLcd [nLig][nChar++] = ' ';
        } // for

        // 2ème ligne: caractères fixes.
        nLig = 1;
        nChar = 0;
        for (n = 0; n < BLANCS_2EME; n++)
        {
                acLcd [nLig][nChar++] = ' ';
        } // for

        acLcd [nLig][nChar++] = C_JCT_A4;

        for (n = 0; n < TRAITS_2EME; n++)
        {
                acLcd [nLig][nChar++] = '-';
        } // for

        acLcd [nLig][nChar++] = C_JCT_A5;

        for (n = 0; n < BLANCS_2EME; n++)
        {
                acLcd [nLig][nChar++] = ' ';
        } // for

        // L'état des aiguilles.
        if (aEtatAig[A4] == NORMAL)
        {
                acLcd [0][POS_A4] = '-';
        }
        else
        {
                acLcd [0][POS_A4] = C_A4_DEV;
        } // if

        if (aEtatAig[A5] == NORMAL)
        {
                acLcd [0][POS_A5] = '-';
        }
        else
        {
                acLcd [0][POS_A5] = C_A5_DEV;
        } // if

        // Puis l'état d'occupation des voies.
        for (uint8_t nVoie = 0; nVoie < NB_VOIES_GC; nVoie++)
        {       
                nLig = nVoie;
                nChar = POS_TRAIN;

                switch (aEtatVoies[nVoie])
                {
                        case E_INCONNU:
                                acLcd [nLig][nChar++] = '?';
                                acLcd [nLig][nChar++] = '?';
                                acLcd [nLig][nChar++] = '?';
                                acLcd [nLig][nChar++] = '?';                                       
                        break;
                               
                        case E_LIBRE:
                                acLcd [nLig][nChar++] = '-';
                                acLcd [nLig][nChar++] = '-';
                                acLcd [nLig][nChar++] = '-';
                                acLcd [nLig][nChar++] = '-';
                        break;

                        case E_ARRETE_G:
                                acLcd [nLig][nChar++] = C_LOCO_G;
                                acLcd [nLig][nChar++] = C_WAGON;
                                acLcd [nLig][nChar++] = C_WAGON;
                                acLcd [nLig][nChar++] = C_WAGON;
                        break;

                        case E_ARRETE_D:
                                acLcd [nLig][nChar++] = C_WAGON;
                                acLcd [nLig][nChar++] = C_WAGON;
                                acLcd [nLig][nChar++] = C_WAGON;
                                acLcd [nLig][nChar++] = C_LOCO_D;
                        break;                               

                        case E_TRAVERS:
                                acLcd [nLig][nChar++] = C_FLEC_G;
                                acLcd [nLig][nChar++] = '-';     
                                acLcd [nLig][nChar++] = '-';
                                acLcd [nLig][nChar++] = C_FLEC_D;
                        break;                                       
                } // switch
        } // for

        // Terminer par le mode de fonctionnement.
        switch (nMode)
        {
                case MODE_MANUEL: acLcd[1][0] = 'M'; break;
                case MODE_SEMI:   acLcd[1][0] = 'S'; break;               
                case MODE_AUTO:   acLcd[1][0] = 'A'; break;
        } // switch

        rafraichir ();
} // Affichage::afficherGC


void Affichage::rafraichir (void)
{
        for (uint8_t nLig = 0; nLig < LCD_LIGNES; nLig++)
        {
                lcd.setCursor (0, nLig);

                for (uint8_t nChar = 0; nChar < LCD_CHARS; nChar++)
                {
                        lcd.write (acLcd [nLig][nChar]);               
                } // for
        } // for       
} // Affichage::rafraichir

Au début de ce fichier, des tableaux d'octets contiennent les caractères personnalisables utilisés pour afficher les trains et les positions des aiguilles. Une seule version du wagon est suffisante, mais il faut des versions gauche et droite de la loco pour distinguer le sens de marche des trains. Les octets en binaire définissent les bits à allumer pour chaque ligne, on retrouve bien les caractères affichés sur les 2 exemples ci-dessus. Il aurait été possible de déclarer ces caractères en mémoire programme plutôt qu'en RAM à l'aide de la directive PROGMEM, mais il me reste encore bien assez de RAM !
La création de ces caractères se fait dans la méthode setup.

Les données à afficher sont préparées dans les tableaux aEtatVoies, aEtatAig et acLcd. C'est ensuite la méthode privée rafraichir qui effectue l'envoi à l'affichage LCD.

La classe comporte elle-même 3 états:
Portez-vous bien et meilleures salutations.