LOCODUINO

Aide
Forum de discussion
Dépôt GIT Locoduino
Flux RSS

mardi 19 mars 2024

Visiteurs connectés : 31

Le Locoduinodrome

La carte Satellite V1 (4)

Le logiciel de la carte

.
Par : bobyAndCo, Dominique, Jean-Luc, Thierry

DIFFICULTÉ :

Dans les articles précédents, après les principes fondateurs, nous avons décrit la partie matérielle de la carte Satellite. Puis nous avons développé la partie logicielle intermédiaire, SAM, qui se situe entre les Satellites et différents logiciels comme un gestionnaire chargé de gérer la circulation des trains et qui va piloter les feux et/ou les aiguillages ou encore un TCO qui lui va avoir besoin des informations transmises par les Satellites et relayées par SAM pour mettre à jour sa représentation graphique du réseau.

Voyons maintenant comment le programme du Satellite lui-même gère ces ordres et remonte l’information des capteurs de présence. Attention toutefois, il est préférable d’avoir quelques notions de développement et de structure objet pour mieux comprendre cet article. Si vous ne l’avez pas déjà fait, nous vous renvoyons à l’ensemble des articles des sections ’base de la programmation’ et ’programmation avancée’, et en particulier à la série Le monde des Objets pour appréhender les grandes notions utilisées ici.

<

Le but est de créer un logiciel pour un satellite reposant sur un Arduino Nano R3, doté d’un bus CAN et d’entrées et de sorties dont le rôle est connu dès le démarrage : pilotage de servos, de DEL et détections à remonter au gestionnaire. Le logiciel, commun à tous les satellites, doit pouvoir évoluer facilement, être maintenable, et lisible [1]. Bref, pour ce logiciel-ci, il faut penser ouverture, maintenabilité et documentation propre [2]. Différentes bibliothèques Locoduino doivent être utilisées pour ce projet. Ce n’est pas que les autres ne soient pas bien, mais d’une part on connait bien les nôtres, et d’autre part on n’est jamais si bien servi que par soit même. Ce vieil adage passe-partout s’est vérifié lorsque nous avons voulu avoir accès à des comportements et des données internes à ces bibliothèques : elles ont été adaptées pour répondre au besoin ! Même mcp_can, la bibliothèque permettant de gérer la puce MCP2515 utilisée pour le bus CAN du Satellite, a été modifiée pour combler un manque. Un ’Fork’ (une variante en bon français) a été créée à partir de la version officielle de SeeedStudio et elle, comme toutes les autres bibliothèques de Locoduino, sont disponibles sur notre LocoGit.

Figure 1 : Structure générale d'un Satellite.
Figure 1 : Structure générale d’un Satellite.

Sur la figure 1, on peut voir que l’entrée du système passe par un bus CAN qui reçoit des ordres depuis l’extérieur. La sortie est aussi via le même bus CAN pour renvoyer l’état des détecteurs, seul objet à renvoyer quelque chose aujourd’hui.
Au milieu, nous avons les listes de DEL, Aiguilles et Détecteurs, dont la configuration et l’état sont sauvegardés dans la mémoire EEPROM locale.

La structure objet

Figure 2 : Les objets gérés par un Satellite, leurs attributs et leurs méthodes
Figure 2 : Les objets gérés par un Satellite, leurs attributs et leurs méthodes

Voici, à la figure 2, côte à côte, un résumé de ce que devrait être chaque type d’objet du satellite. Tous les objets utilisent une et une seule broche, chaque objet doit être identifié d’une manière unique, chaque objet aura probablement besoin de pouvoir donner son état. Du côté des fonctions, chaque objet doit disposer de begin() pour l’initialisation, loop() pour le fonctionnement, et EEPROMsauvegarde() et EEPROMchargement() pour la mémorisation dans l’EEPROM. On voit bien l’architecture objet se dessiner (Figure 3).

Figure 3 : Graphe d'héritage des objets
Figure 3 : Graphe d’héritage des objets

La classe de base, très opportunément nommée ’objet’ (avec toute l’imagination et l’inventivité qui nous caractérisent), reprend le fonctionnement de base de tout objet raccordé au satellite : une broche, un numéro d’identification, et des fonctions de base begin(), loop(), EEPROMsauvegarde() et EEPROMchargement(). L’état courant, pourtant bon candidat à un partage ne fera pas partie de la classe de base. On verra pourquoi plus tard [3].
Les fonctions sus-nommées seront toutes virtuelles, afin de permettre à chaque classe dérivée d’en personnaliser le comportement. Par contre, la fonction de la classe de base a elle aussi un comportement, et il faudra probablement l’appeler aussi.

La forme compte aussi

Comme nous avons déjà eu l’occasion de l’écrire, en programmation ’Diviser pour mieux régner’ est une maxime à appliquer au pied de la lettre [4].
Dans notre cas, nous allons donc avoir :

Table 1 : Liste des fichiers
NomRôle
Satellite.ino le fichier principal
Satellite.h et Satellite.cpp pour la classe principale
Objet.h et Objet.cpp pour la classe de base des objets
Led.h et Led.cpp pour les DEL
Aiguille.h et Aiguille.cpp pour... les aiguilles. Bravo, il y en qui suivent...
Detecteur.h et Detecteur.cpp pour les détecteurs
CANBus.h et CANBus.cpp gestion des messages CAN
CANMessage.h et CANMessage.cpp Configuration des objets via le CAN
Messaging.h constantes liées à la messagerie et communs avec SAM

 [5]

La classe Objet

En réalité, cette classe est tellement simple que seul le fichier Objet.h est nécessaire. Voyons son code plus en détail :

class Satellite;
 
/** Cette classe est la classe de base de tous les objets utilisant une seule broche.
*/
class Objet
{
protected:
    uint8_t pin;
    uint8_t number;
 
public:
    Objet() { this->pin = 255; this->number = 255; }
 
    bool IsValid() { return this->pin != 255 && this->number != 255; }
 
    virtual void begin(uint8_t inPin, uint8_t inNumber) = 0;
    virtual void loop(Satellite *inpSat) = 0;
  
    virtual uint8_t GetEEPROMSize() { return 0;    }
 
    virtual uint8_t EEPROM_chargement(int inAddr) { return inAddr; }
    virtual uint8_t EEPROM_sauvegarde(int inAddr) { return inAddr; }
};
 
// Assure la compatibilite avec mes fonctions EEPROM dans Visual Studio...
#ifdef VISUALSTUDIO
#define EEPROMGET(SRC, DATA, SIZE)    EEPROM.get(SRC, &DATA, SIZE)
#define EEPROMPUT(SRC, DATA, SIZE)    EEPROM.put(SRC, &DATA, SIZE)
#else
#define EEPROMGET(SRC, DATA, SIZE)    EEPROM.get(SRC, DATA)
#define EEPROMPUT(SRC, DATA, SIZE)    EEPROM.put(SRC, DATA)
#endif
 
#endif

Dans la classe Objet, nous déclarons d’abord deux entiers non signés sur 8 bits, en clair des bytes, des octets non signés, dont la valeur se situe entre 0 et 255 pour le numéro de la broche associée et pour l’identifiant de l’objet. Cela signifie que la broche ne pourra jamais avoir un numéro supérieur à 255 (très peu probable...), et que le numéro de l’objet ne pourra pas non plus dépasser cette valeur. Comme le numéro est lié à un type (DEL 1, DEL 2, aiguille 1, etc...), et que l’on a pas trouvé l’Arduino capable de piloter 255 DEL, 255 aiguilles et 255 détecteurs, ce sera largement suffisant. Mieux, comme on est sûr de ne jamais atteindre 255, on va déclarer que les valeurs initiales de Pin et de Number sont fixées à 255. Ainsi, on pourra tester la validité de l’objet : si le numéro de broche ou le numéro d’ordre est égal à 255, alors cet objet n’a pas (encore) été initialisé.
C’est exactement ce qui se retrouve dans le constructeur qui fixe les deux valeurs à 255. Notez la manière de déclarer et de donner un corps à une fonction dans le fichier d’entête. Au lieu de mettre un ’ ;’ à la fin de la ligne Objet();, nous plaçons directement le contenu de la fonction ici Objet() { this->pin = 255; this->number = 255; }

C’est une manière de simplifier l’écriture d’une fonction. On pourrait mettre toutes les classes ainsi dans les fichiers d’entête, mais lorsque le code d’une fonction devient trop important, et là on parle d’une ligne ou deux maximum, ou que ces lignes dépassent l’appel d’une autre fonction ou autre chose que des affectations comme ici, alors il vaut mieux créer le fichier cpp. C’est une simple question de lisibilité.

Deuxième fonction : IsValid() qui s’assure que l’objet est valide en testant la broche et le numéro qui doivent être différents de 255. Là encore, écriture mono ligne...

Les fonctions décrites jusque là sont spécifiques à la classe et ne réclament pas d’adaptation dans les classes dérivées. Les suivantes sont toutes des virtuelles, c’est à dire des fonctions que la classe dérivée a le droit de redéfinir à sa convenance. Notez que ce n’est pas une obligation. Par contre la classe de base peut faire le choix d’obliger ses dérivées de fournir un codage pour une ou plusieurs fonctions particulières. C’est le cas de begin() et de loop(). Leur notation virtual void begin(uint8_t inPin, uint8_t inNumber) = 0; avec le ’= 0’ à la fin indique au compilateur que la classe Objet ne fournira pas de corps pour cette fonction begin(), chaque classe dérivée doit systématiquement fournir cette fonction. C’est une fonction "virtuelle pure". Du coup, et c’est heureux, il devient impossible d’instancier avec un ’new’ un objet de type ’Objet’, sinon il manquerait un bout de la classe !
On peut d’ailleurs comprendre que begin() et loop() soient fortement liés au type de l’objet, et que la classe de base ne puisse pas fournir de comportement par défaut.

EEPROM

Les fonctions de sauvegarde et de chargement vers et depuis l’EEPROM ne font rien non plus, mais on n’exclut pas un jour d’y faire quelque chose. On a préféré ne pas en faire des fonctions virtuelles pures pour permettre aux classes dérivées d’y faire appel, au cas où, et aussi parce certains objets pourraient ne rien avoir à sauver !
La sauvegarde EEPROM ressemble au remplissage d’une grosse liste d’octets. Depuis le point de départ et jusqu’au dernier octet, il faut savoir à quel emplacement de la liste on doit sauver le prochain octet. L’idée est donc que l’on maintienne un entier donnant le numéro de la prochaine case mémoire EEPROM à remplir et que ce numéro soit modifié à chaque écriture dans la liste. C’est pourquoi EEPROMsauvegarde() a un argument entier, c’est le numéro de départ de l’écriture, et une valeur de retour : la fonction doit retourner le nouveau numéro de case libre. Idem pour la chargement qui doit avoir un numéro de départ et progresser dans la mémoire EEPROM à chaque lecture.
La fonction GetEEPROMSize() est une sécurité pour éviter les accidents. Il sera question de la structure de la sauvegarde au moment d’étudier la classe principale Satellite. Disons simplement que la fonction doit retourner la taille en octets d’un objet.

Enfin, les dernières lignes de objet.h peuvent paraître absconses, et elles le sont, mais c’est pour nous la possibilité d’utiliser un simulateur d’Arduino dans Visual Studio Community pour tester notre code, plutôt que de téléverser le programme dans un Arduino à chaque essai...

L’état des objets

Comme il a été dit plus haut, c’est le gestionnaire qui sait dans quel état doit se trouver chaque DEL et chaque aiguille. Il envoie régulièrement et en continu un message contenant ces états sur le bus CAN. Le satellite reçoit ces états et les stocke chez lui sans rien faire de plus. A chaque tour de loop() un objet est traité. Cet objet va voir à son tour son loop() exécuté, et lui devra alors mettre son matériel en adéquation avec l’état reçu par le bus CAN et demandé par le gestionnaire. Le bus est rapide, et le loop aussi, ce qui fait que la latence de traitement entre la réception d’un état et la modification effective de cet état est très faible...

Les LED

S’il était clair qu’une DEL (LED en anglais) est bien un ’Objet’ au vu de ses caractéristiques générales, voyons maintenant ce qui la différencie des autres objets.

class Led : public Objet
{
private:
    // Gestion locale
    LightDimmerSoft dimmer;
 
public:
    Led();
 
    void begin(uint8_t inPin, uint8_t inNumber);
    void loop(Satellite *inpSat);
    static void loopPrioritaire() { LightDimmer::update();    }
 
    uint8_t GetEEPROMSize() { return Objet::GetEEPROMSize() + sizeof(unsigned int) + (2 * sizeof(unsigned long)); }
    uint8_t EEPROM_chargement(int inAddr);
    uint8_t EEPROM_sauvegarde(int inAddr);
};

Puisque tout le fonctionnement d’une DEL est géré par la bibliothèque Locoduino LightDimmer, une instance de la classe LightDimmerSoft est la seule donnée supplémentaire stockée dans la classe Led, en plus des données fournies par la classe de base ’Objet’. Cette donnée est privée, personne n’a besoin de regarder là-dedans à part la classe Led elle même [6]...

L’état courant, dont on a dit plus haut qu’il n’était pas dans ’Objet’ à cause de la DEL, n’est juste pas nécessaire. A aucun moment on a besoin de savoir si la diode est allumée ou pas. Elle doit simplement faire ce qu’on lui demande, peu importe son état précédent ! Le reste déclare simplement le constructeur et les fonctions nécessaires, celle décrites plus haut.

La fonction loopPrioritaire() est utilisée pour permettre à la bibliothèque LightDimmer de fonctionner. Si la DEL doit clignoter, le bibliothèque doit changer l’état de la broche à intervalle régulier pour ... clignoter ! De la même manière, si un allumage ou une extinction progressive est programmée, simulant les vieilles ampoules, la bibliothèque doit changer de manière régulière la valeur PWM qui règle l’intensité d’allumage d’une DEL. Il faut donc appeler la fonction LigthDimmer::update() aussi souvent que possible pour lui permettre de faire son travail. On verra dans la classe satellite comment est appelée cette fonction.

Passons le code de Led.cpp en revue :

Led::Led()
{
    this->number = 0;
}
 
void Led::begin(uint8_t inPin, uint8_t inNumber)
{
    this->pin = inPin;
    this->number = inNumber;
    dimmer.begin(this->pin, HIGH);
}

Première partie du code, on a un constructeur qui fixe l’identifiant à 0, histoire d’éviter le test d’invalidité de la classe de base, qui vérifie que la broche et l’identifiant sont à 255. Ce constructeur sans argument est celui qui sera automatiquement appelé par la langage C++ au moment de la création de la matrice de DEL contenue dans le satellite. Toutes les DEL de la liste seront donc sans broche (à 255), mais valides avec un identifiant à 0. Il faudra évidemment remplir correctement ces deux valeurs avant de pouvoir vraiment utiliser les DEL.
Le begin fait le reste du travail d’initialisation. La broche et l’identifiant sont passés en arguments, et le membre ’dimmer’ est initialisé à son tour en lui précisant la broche et l’état de broche qui allume cette Del.

void Led::loop(Satellite *inpSat)
{
    // le test inpSat->modeConfig est ajouté dans le if et enlevé dans les case
    if (inpSat->modeConfig && inpSat->ConfigMessage.IsConfig() && inpSat->ConfigMessage.LedToConfig())
    {
        uint8_t number = inpSat->ConfigMessage.NumLedToConfig();
        if (number == this->number) // Cette led est bien celle a regler...
            switch (inpSat->ConfigMessage.LedConfigType())
            {
                case LED_CONFIG_TYPE::Maximum:
                    this->dimmer.setMax(inpSat->ConfigMessage.ConfigByteValue());
                     this->dimmer.update();
                     this->dimmer.stopBlink();
                     this->dimmer.off();
                     this->dimmer.on();
                     break;
                case LED_CONFIG_TYPE::BrighteningTime:
                     this->dimmer.setBrighteningTime(inpSat->ConfigMessage.ConfigIntValue());
                     this->dimmer.stopBlink();
                     this->dimmer.off();
                     this->dimmer.on();
                     break;
                case LED_CONFIG_TYPE::FadingTime:
                     this->dimmer.setFadingTime(inpSat->ConfigMessage.ConfigIntValue());
                     this->dimmer.stopBlink();
                     this->dimmer.on();
                     this->dimmer.off();
                     break;
                 case LED_CONFIG_TYPE::OnTime:
                     this->dimmer.setOnTime(inpSat->ConfigMessage.ConfigIntValue());
                     this->dimmer.startBlink();
                     break;
                 case LED_CONFIG_TYPE::Period:
                     this->dimmer.setPeriod(inpSat->ConfigMessage.ConfigIntValue());
                     this->dimmer.startBlink();
                     break;
            }
            return;
        }

        uint8_t inNewState = inpSat->MessageIn.ledState(this->number);
        switch (inNewState) {
            case LED_BLINK:	this->dimmer.startBlink(); break;
            case LED_ON:	this->dimmer.on(); break;
            case LED_OFF:	this->dimmer.off(); break;
        }
}

De son côté, le loop(), plus compliqué, est en deux parties. Un gros switch au départ de la fonction conditionné par le type de message reçu gère les messages de configuration envoyés par le gestionnaire. Selon le paramètre fixé et envoyé à LightDimmer, il faut réinitialiser et remettre la DEL dans le bon mode.
La seconde partie consiste juste à regarder l’état de la DEL tel que le gestionnaire l’a demandé, et à y conformer la vraie DEL.

uint8_t Led::EEPROM_sauvegarde(int inAddr) 
{ 
    int addr = Objet::EEPROM_sauvegarde(inAddr); 
    uint8_t valeur8; 
    uint16_t valeur16; 
 
    valeur8 = this->dimmer.maximum(); 
    EEPROMPUT(addr, valeur8, sizeof(uint8_t)); 
    addr += sizeof(uint8_t); 
    valeur16 = this->dimmer.fadingTime(); 
    EEPROMPUT(addr, valeur16, sizeof(uint16_t)); 
    addr += sizeof(uint16_t); 
    valeur16 = this->dimmer.brighteningTime(); 
    EEPROMPUT(addr, valeur16, sizeof(uint16_t)); 
    addr += sizeof(uint16_t); 
    valeur16 = this->dimmer.onTime(); 
    EEPROMPUT(addr, valeur16, sizeof(uint16_t)); 
    addr += sizeof(uint16_t); 
    valeur16 = this->dimmer.period(); 
    EEPROMPUT(addr, valeur16, sizeof(uint16_t)); 
    addr += sizeof(uint16_t); 
 
    return addr; 
} 

Voyons maintenant la sauvegarde d’une instance de la classe Led dans l’EEPROM. La fonction reçoit inAddr en argument, qui est le numéro du premier octet d’EEPROM disponible.
Première chose à faire avant de sauvegarder les données propres de Led, il faut sauvegarder celles de sa classe de base Objet int addr = Objet::EEPROM_sauvegarde(inAddr);

Cette notation avec ’Objet: :’ devant permet d’appeler la fonction d’une classe de base particulière. Si seul int addr = EEPROM_sauvegarde(inAddr) avait été spécifié, la fonction se serait appelée elle même, et il en résulterait un plantage de l’Arduino. Bien sûr, ça ne fonctionne que pour la classe de base. Nous n’aurions pas pu écrire Aiguille::EEPROM_sauvegarde, le compilateur aurait refusé tout net !
Dans cette fonction, on est censé sauver ce qui doit l’être à partir de l’octet passé en argument, et récupérer le nouvel emplacement libre. Si vous retournez dans le code de Objet.h, vous verrez que son EEPROM_sauvegarde() se contente de retourner la valeur reçue, sans rien modifier puisqu’elle ne sauve rien ! En effet, dans notre cas inutile de sauver la broche et l’identifiant qui sont de toutes façons réaffectés à chaque démarrage avec des matrices en dur. On verra comment dans la classe Satellite.
Le retour de cet appel de fonction crée une variable locale addr qui représente le premier emplacement mémoire disponible, après la sauvegarde des données de ’Objet’. Ensuite, à chaque sauvegarde de quelque chose, on va incrémenter addr avec la taille de ce qui vient d’être sauvé. Et à la fin, on va retourner addr qui constituera l’adresse du prochain octet disponible pour une autre sauvegarde..
Après cet appel, on déclare deux variables locales qui vont contenir les valeurs à sauver. Une sur 8 bits pour un byte ou un char, l’autre sur 16 bits pour un int ou un unsigned int.
Détaillons la première sauvegarde de valeur, les autres suivent le même schéma :

valeur8 = this->dimmer.maximum(); 
EEPROMPUT(addr, valeur8, sizeof(uint8_t)); 
addr += sizeof(uint8_t); 

Première ligne, on récupère la valeur courante de réglage de la puissance de la DEL. Cette valeur, configurée via le gestionnaire, est stockée durant le fonctionnement du satellite directement dans son membre ’dimmer’. Inutile de prendre de la mémoire pour stocker cette valeur dans la classe Led, ’dimmer’ le fait pour nous.
Deuxième ligne, on va utiliser la fameuse macro vue dans Objet.h et qui donne un résultat différent selon la plateforme. Sur l’IDE Arduino, elle sera remplacée par EEPROM.put(addr, valeur8);

qui stocke l’octet contenu par la variable ’valeur8’ à l’emplacement ’addr’ de la mémoire EEPROM.

Dernière ligne, on incrémente addr de la quantité d’octets sauvés. Comme on a sauvé un seul octet (la taille d’un uint8_t qui désigne un entier non signé sur 8 bits), addr sera simplement incrémenté de 1.

Toutes les données pertinentes sont ensuite sauvées selon le même principe, sur un ou deux octets selon le type.

Petite info qui a son importance. L’EEPROM a un nombre de cycles de lecture/écriture limité, mais les fonctions d’écriture de la classe EEPROM ne sauvent un octet qui si et seulement si sa valeur est différente de celle déjà présente dans l’EEPROM. Dans la bibliothèque EEPROM, l’appel de put() est remplacé par un update(). Si la valeur est déjà présente, aucune écriture n’est faite. Grâce à ce mécanisme intelligent, on peut sans vergogne sauver encore et toujours les mêmes choses sans s’inquiéter de la durée de vie de la mémoire...

Une fois comprise la méthode pour sauver, celle pour relire est plutôt facile à interpréter...

uint8_t Led::EEPROM_chargement(int inAddr)
{
    int addr = Objet::EEPROM_chargement(inAddr);
    uint8_t valeur8;
    uint16_t valeur16;
 
    EEPROMGET(addr, valeur8, sizeof(uint8_t));
    this->dimmer.setMax(valeur8);
    Serial.print("lMax ");Serial.print(valeur8); // 255
    addr += sizeof(uint8_t);
    EEPROMGET(addr, valeur16, sizeof(uint16_t));
    this->dimmer.setFadingTime(valeur16);
    Serial.print(" lFad ");Serial.print(valeur16); // 250
    addr += sizeof(uint16_t);
    EEPROMGET(addr, valeur16, sizeof(uint16_t));
    this->dimmer.setBrighteningTime(valeur16);
    Serial.print(" lBri ");Serial.print(valeur16); // 250
    addr += sizeof(uint16_t);
    EEPROMGET(addr, valeur16, sizeof(uint16_t));
    this->dimmer.setOnTime(valeur16);
    Serial.print(" lTim ");Serial.print(valeur16); // 200
    addr += sizeof(uint16_t);
    EEPROMGET(addr, valeur16, sizeof(uint16_t));
    this->dimmer.setPeriod(valeur16);
    Serial.print(" lPer ");Serial.println(valeur16); // 900
    addr += sizeof(uint16_t);
 
    return addr;
}

Tout comme la fonction de sauvegarde, celle qui recharge a besoin de savoir à partir de quel octet lire (inAddr), et doit retourner cette position à jour (addr).

Tout comme la fonction de sauvegarde, il faut commencer par demander à la classe de base de faire son travail. Et tout comme celle de sauvegarde, la fonction de chargement de ’Objet’ ne fait rien.

Tout comme la fonction de sauvegarde (ou presque), chaque donnée est lue depuis l’EEPROM dans une variable intermédiaire de 8 ou 16 bits, puis le membre ’dimmer’ est mis à jour avec la donnée lue.
Tout comme la fonction de sauvegarde, addr est incrémenté de la taille de l’objet lu.
Tout comme la fonction de sauvegarde, la fonction de chargement retourne addr, qui est le numéro du prochain octet à lire.

Les aiguilles

Une aiguille est un servomoteur piloté par le satellite :

class Aiguille : public Objet
{
    // Configuration
 
    // Gestion locale
    SMSSmooth servoMoteur;
 
    bool estDroit;
    
public:
    Aiguille();
 
    void begin(uint8_t inPin, uint8_t inNumber);
    void loop(Satellite *inpSat);
    static void loopPrioritaire() { SlowMotionServo::update(); }
 
    uint8_t GetEEPROMSize()    { return Objet::GetEEPROMSize() + (2 * sizeof(unsigned int)) + sizeof(float); }
    uint8_t EEPROM_chargement(int inAddr);
    uint8_t EEPROM_sauvegarde(int inAddr);
};

Dans la classe Aiguille se trouve donc une instance de la classe SMSSmooth. cette classe appartient à la bibliothèque SlowMotionServo (SMS !) et s’occupe de la partie matérielle de l’aiguille.
Se trouve également un booléen ’estDroit’ disant si l’aiguille est droite ou déviée. C’est une version un peu plus précise et dédiée de la donnée ’EtatCourant’ que j’avais envisagé pour l’Aiguille dans la première description des classes tout en haut de l’article.
Les fonctions sont très similaires à celles de Led. On peut signaler dans loopPrioritaire() l’appel à la mise à jour du mouvement courant de SlowMotionServo, fonction à appeler aussi souvent que possible...

Aiguille::Aiguille()
{
    this->estDroit = true;
}

Le constructeur par défaut fixe un état de départ de l’instance.

void Aiguille::begin(uint8_t inPin, uint8_t inNumber)
{
    this->pin = inPin;    // inutile puisque stockée dans ServoMoteur, mais puisque Objet le permet...
    this->number = inNumber;
 
    pinMode(this->pin, OUTPUT);
    servoMoteur.setPin(this->pin);
    servoMoteur.setMin(1200);   // faible amplitude par défaut pour éviter 
    servoMoteur.setMax(1700);   // trop de contrainte sur l'aiguille avant configuration
    servoMoteur.setSpeed(2.0);
    //servoMoteur.setReverted(true);
    servoMoteur.setInitialPosition(0.5);
    servoMoteur.goTo(this->estDroit ? 1.0 : 0.0);
}

Le begin, va initialiser la broche et l’identifiant, et fixer des valeurs de réglage de base pour le servo et bouger pour se conformer à l’état du flag estDroit.

void Aiguille::loop(Satellite *inpSat)
{
    if (inpSat->modeConfig && inpSat->ConfigMessage.IsConfig() && inpSat->ConfigMessage.AiguilleToConfig())
    {
        uint8_t number = inpSat->ConfigMessage.NumAiguilleToConfig();
        if (number == this->number)    // Cette aiguille est bien celle à regler...
            switch (inpSat->ConfigMessage.AiguilleConfigType())
            {
            case AIGUILLE_CONFIG_TYPE::Min:
                this->servoMoteur.setupMin(inpSat->ConfigMessage.ConfigIntValue());
        this->servoMoteur.endSetup();
                break;
            case AIGUILLE_CONFIG_TYPE::Max:
                this->servoMoteur.setupMax(inpSat->ConfigMessage.ConfigIntValue());
              this->servoMoteur.endSetup();
                break;
            case AIGUILLE_CONFIG_TYPE::Speed:
                this->servoMoteur.setSpeed(inpSat->ConfigMessage.ConfigFloatValue());
                this->estDroit = !this->estDroit;
                this->servoMoteur.goTo(this->estDroit ? 0.0 : 1.0);
                break;
            }
        return;
    }
    // Execution
    bool inEstDroit = inpSat->MessageIn.pointState();
    if (inEstDroit == this->estDroit)
        return; // pas de changement
  
    this->estDroit = inEstDroit;
    this->servoMoteur.goTo(this->estDroit ? 1.0 : 0.0);
}

Comme pour Led, la loop() est en deux parties, un switch permettant de faire les réglages via le gestionnaire, et l’exécution proprement dite qui va faire ce que demande l’état stocké par le satellite et reçu par le bus CAN.

Les fonctions de sauvegarde EEPROM sont similaires à celle de Led, si ce n’est que bien sûr les données sont extraites du membre ’servoMoteur’, et y sont repoussées par le chargement.

Les détecteurs

Cette classe est un peu différente dans le sens où elle n’actionne rien, elle se contente d’observer l’état de sa broche et de remonter un résultat.

class Detecteur : public Objet
{
private:
	// Configuration
	unsigned long intervalle;
	unsigned long remanence;
	byte etatDetecte;

	// Gestion locale
	bool estDetecte;
	bool etatPrecedent;
	unsigned long precedentTest;
	unsigned long perteDetection;

public:
	Detecteur();

	void begin(uint8_t inPin, uint8_t inNumber);
	void loop(Satellite *inpSat);
	static void loopPrioritaire() { }

	bool EstActivee() {	return this->estDetecte; }

	uint8_t GetEEPROMSize() { return Objet::GetEEPROMSize() + (2 * sizeof(unsigned long)) + 1; }
	uint8_t EEPROM_chargement(int inAddr);
	uint8_t EEPROM_sauvegarde(int inAddr);
};

Ici, pas de bibliothèque qui va stocker pour nous les données du détecteur, donc tout est stocké dans la classe. L’intervalle donne le laps de temps entre deux mesures de l’état de la broche. Inutile de le faire trop souvent, mais il faut quand même le faire ! La rémanence donne le temps de non détection juste après une détection. Si l’on veut laisser le temps à un convoi de passer avant de re-mesurer... etatDetecte dit quel état, HIGH ou LOW, signifie détecté.
Les autres variables privées sont utilisées pour la mesure.

Dans les fonctions, notons le loopPrioritaire() qui ne fait rien, et la présence de la fonction EstActivee() qui est là pour masquer la variable interne privée [7].

Detecteur::Detecteur()
{
	this->intervalle = 100;
	this->remanence = 1000;
	this->etatDetecte = HIGH;
	this->etatPrecedent = HIGH;
	this->precedentTest = 0;
	this->perteDetection = 0;
}

void Detecteur::begin(uint8_t inPin, uint8_t inNumber)
{
	this->pin = inPin;
	this->number = inNumber;
	pinMode(this->pin, INPUT_PULLUP);
	this->estDetecte = digitalRead(this->pin) == this->etatDetecte;
}

Le constructeur va fixer des valeurs de départ pour toutes les données membres internes.
Le begin() va lui fixer le pinMode de la broche et récupérer un état de départ immédiatement.

void Detecteur::loop(Satellite *inpSat)
{
	if (millis() - this->precedentTest < this->intervalle)
		return;

	this->precedentTest = millis();
	int etat = digitalRead(this->pin);        // Occupation=LOW, liberation=HIGH

	bool activ = etat == this->etatDetecte;

	if (activ && activ != this->estDetecte)
	{
		if (this->perteDetection == 0)
		{
			this->perteDetection = millis();
			return;
		}

		if (millis() - this->perteDetection < this->remanence)
			return;

		this->perteDetection = 0;
	}

	if (activ != this->etatPrecedent)
	{
		if (activ)
		{
			inpSat->StatusMessage.setDetection(inpSat->Bus.TxBuf, this->number, false);
			inpSat->Bus.messageTx();
			Serial.println(F("Lib"));
		}
		else
		{
			inpSat->StatusMessage.setDetection(inpSat->Bus.TxBuf, this->number, true);
			inpSat->Bus.messageTx();
			Serial.println(F("Occ"));
		}
		this->etatPrecedent = activ;
	}
	this->estDetecte = activ;
}

Pas de partie réglage dans le loop(). Juste ce qu’il faut pour mettre à jour l’état courant en tenant compte des variables de temps déclarées plus tôt. Le nouvel état sera directement confié au Satellite passé en argument avec inpSat->StatusMessage.setDetection().

La sauvegarde et le chargement EEPROM se contentent de sauver la configuration courante, c’est à dire les trois variables intervalle, remanence et etatDetecte.

Le satellite

Le satellite lui même est une classe.
Un satellite, c’est une liste d’objets qui doit soit se conformer à un état demandé par le gestionnaire au bout du bus CAN, soit renvoyer son état à ce même gestionnaire. Ces objets doivent être mis à jour aussi souvent que possible sans oublier de faire exécuter aussi les loop() des bibliothèques utilisées...

#define NB_LEDS		9
#define NB_AIGUILLES	1
#define NB_DETECTEURS	3

class Satellite
{
private:
	// Configuration
	uint8_t		id;	// Identifiant du satellite

	// Gestion locale	
	Led		leds[NB_LEDS];
	Aiguille	aiguilles[NB_AIGUILLES];
	Detecteur	detecteurs[NB_DETECTEURS];

	Objet*	objets[NB_LEDS + NB_AIGUILLES + NB_DETECTEURS];	// Liste de tous les objets gérés
	uint8_t	nbObjets;
	byte		objetCourantLoop;

	void AddObjet(Objet *inpObjet, uint8_t inPin, uint8_t inNumber);
	void EEPROM_sauvegarde();
	bool EEPROM_chargement();

public:
	CANBus Bus;
	CommandCANMessage MessageIn;
	StatusCANMessage StatusMessage;
	ConfigCANMessage ConfigMessage;
	bool	modeConfig;

	Satellite();
	void begin(uint8_t inId);
	void loop();
};

Comme déjà dit au tout début de l’article, la quantité des différents objets est fixée dès le départ. Pourtant le satellite va être codé la manière la plus ’universelle’ possible pour rester malléable. Nous avons donc le compte des quantités de DEL, de servos et de détecteurs.
Dans la classe Satellite elle même, nous avons un identifiant. Chaque satellite est doté d’un identifiant fixé par le programme, mais qui pourrait aussi être donné par le gestionnaire, au moins lors du tout premier lancement. Suivent les listes des ’Led’s, des ’Aiguille’s et des ’Detecteur’s.
Pour que le satellite soit le plus versatile possible, et puisque ces trois classes dérivent directement de la même classe de base (Objet), nous allons construire une liste d’objets (objets[]), qui sera une liste de pointeurs faiblement typés (Objet *) sur les objets construits par ces trois listes. Ce sera sans doute plus clair avec le code du constructeur un peu plus tard. Sa taille sera évidemment la somme des trois quantités. Toujours pour être le plus souple possible, un entier nbObjets tient le compte des objets poussés dans la liste par la fonction AddObjet() qui vient un peu plus tard.
Un octet appelé ’objetCourantLoop’ est déclaré. Son rôle est dire quel sera le prochain objet à traiter par la loop() du satellite. En effet, la loop() du Satellite ne traitera qu’un seul objet à chaque passage, pour limiter le temps de latence entre deux lectures du CAN, et entre deux appels aux fonctions de mise à jour des différentes bibliothèques. Si l’on avait traité tous les objet à chaque appel de loop(), le bus CAN aurait pu manquer des octets, et les mouvement des servos seraient probablement saccadés.
La fonction AddObjet() dont j’ai parlé plus haut reçoit un pointeur sur un objet, donc en pratique un pointeur sur une instance de n’importe quelle classe dérivée de ’Objet’, un numéro de broche et un identifiant.
A la fin de la partie privée de la classe se trouvent la sauvegarde et le chargement d’EEPROM.

Côté membres publics, passons rapidement sur la partie CAN qui fera l’objet d’un article à lui tout seul, pour arriver sur les classiques constructeur, begin() et loop(). begin() a besoin du numéro d’identifiant du satellite, tandis que le loop() n’a pas d’argument.

Voyons le source Satellite.cpp :

const uint8_t leds_pins[] = { 3, 4, 5, 6, 7, 8, 9, A0, A1 };
const uint8_t aiguilles_pins[] = { A2 };
const uint8_t detecteurs_pins[] = { A3, A4, A5 };

const char *EEPROM_ID = { "LCDO" };

void Satellite::AddObjet(Objet *inpObjet, uint8_t inPin, uint8_t inNumber)
{
	if (inpObjet != NULL && inPin != 255)
	{
		inpObjet->begin(inPin, inNumber);
		this->objets[this->nbObjets] = inpObjet;
	}
	this->nbObjets++;
}

Après les includes, on va trouver des matrices donnant les broches utilisées par objet : celles pour les DEL, pour les aiguilles et les détecteurs.
Suit un texte qui représente une ’signature’ de l’application stockée dans l’EEPROM. Nous verrons dans les fonctions de sauvegarde/chargement comment elle est utilisée.
La première fonction du source, est AddObjet(). Comme son nom l’indique, elle permet d’ajouter un objet à la liste générale des objets. En argument un pointeur sur l’objet à ajouter, le numéro de broche associé et un identifiant. La fonction ne fait rien si la liste n’est pas allouée, ou si la broche est invalide (255). Si tout va bien, le begin() de l’objet est appelé et permet d’initialiser la broche et de fixer l’identifiant. L’objet est ensuite ajouté à la liste à l’emplacement donné par le compteur nbObjets. Le compteur est enfin incrémenté.

Satellite::Satellite()
{
	this->nbObjets = 0;
	for (int i = 0; i < NB_LEDS + NB_AIGUILLES + NB_DETECTEURS; i++)
		this->objets[i] = NULL;
	this->objetCourantLoop = 0;
	this->modeConfig = false;
}

Dans le constructeur, on va tout initialiser pour que la suite se passe bien [8].
On va mettre le compteur d’objets de la liste à 0 puisqu’elle est vide, on va initialiser chaque pointeur de la liste à NULL, et dire que le prochain objet à traiter dans le loop sera le 0, en espérant que la liste ne sera plus vide au moment du premier passage dans loop... Enfin, au départ, nous ne sommes pas en situation de configuration.

void Satellite::begin(uint8_t inId)
{
	this->id = inId;
	this->Bus.begin(this->id);

	this->nbObjets = 0;
	for (int i = 0; i < NB_LEDS; i++)
	{
		this->AddObjet(&this->leds[i], leds_pins[i], i);
		// Clignotement demo test des leds
		digitalWrite(leds_pins[i], HIGH);
		delay(250);
		digitalWrite(leds_pins[i], LOW);
	}
	for (int i = 0; i < NB_AIGUILLES; i++)	
	{
		this->AddObjet(&this->aiguilles[i], aiguilles_pins[i], i);
	}
	for (int i = 0; i < NB_DETECTEURS; i++)	
	{
		this->AddObjet(&this->detecteurs[i], detecteurs_pins[i], i);
	}

	if (this->EEPROM_chargement() == false)
		this->EEPROM_sauvegarde();
}

Le begin commence par prendre en compte l’identifiant de satellite passé en argument en le copiant dans le satellite. Le bus CAN est initialisé avec ce même identifiant.

Commencent ensuite les boucles devant créer les objets de la liste principale. Par exemple pour Led, les objets ont été créés par la ligne de déclaration présente dans Satellite.h Led leds[NB_LEDS];
A ce moment là, toutes les Leds sont allouées, leur constructeur est appelé et donc leur contenu est connu mais invalide puisque la broche et l’identifiant sont à 255.
Le rôle de la boucle va être de remplir la liste ’objets’ avec ces Leds, de faire appel à leur begin(), ce qui est fait dans AddObjet() à qui on a donné le numéro de broche issu de la matrice leds_pins déclarée tout en haut de Satellite.cpp, et un identifiant qui n’est qu’un numéro d’ordre de la Led : 0,1,2,3 ...
Parce que c’est assez visuel, on va se servir de l’initialisation pour faire clignoter les DEL et ainsi montrer que tout se passe bien.
Les boucles pour les aiguilles et les détecteurs sont strictement identiques, sans faire clignoter quoi que ce soit bien entendu.

Dernières lignes de begin(), on va vérifier la présence d’une configuration des objets dans l’EEPROM en tentant de les recharger. Si le chargement renvoie false, c’est que l’EEPROM est vide ou invalide, et dans ce cas il faut préparer le terrain en sauvant une configuration neuve issue de l’initialisation que nous venons de faire.

Voyons comment sauver une configuration valide :

void Satellite::EEPROM_sauvegarde()
{
	int addr = 0;

	////////////////////////////////////// Partie entete

	/* Un descripteur du contenu est sauve en premier.
	Si ce descripteur n'est pas conforme a la lecture, alors l'EEPROM sera consideree vierge.
	Ca signifie aussi que changer la forme (nombre d'objets, taille d'un objet) annule toute sauvegarde !
	*/

	eeprom_update_block(EEPROM_ID, (void *)addr, 4);
	addr += 4;

	EEPROM.update(addr++, this->nbObjets);

	EEPROM.update(addr++, leds[0].GetEEPROMSize());
	EEPROM.update(addr++, aiguilles[0].GetEEPROMSize());
	EEPROM.update(addr++, detecteurs[0].GetEEPROMSize());

	////////////////////////////////////// Partie satellite

	EEPROM.update(addr++, this->id);

	////////////////////////////////////// Partie objets

	for (int i = 0; i < this->nbObjets; i++)
		if (this->objets[i] != NULL)
			addr = this->objets[i]->EEPROM_sauvegarde(addr);
}

Le problème de l’EEPROM, c’est qu’il s’agit d’une simple suite d’octets. Il faut être capable de certifier que ce qui s’y trouve déjà est bien une sauvegarde valide d’une configuration d’un Satellite, de la même version et avec le même format de contenu pour chaque objet stocké. Sinon il faudra déclarer le contenu de l’EEPROM ’non initialisé’.

Figure 4 : Organisation des données sauvegardées en EEPROM.
Figure 4 : Organisation des données sauvegardées en EEPROM.

Première sécurité : on va sauver une petite chaîne de caractères certifiant que l’on parle bien d’un satellite :

eeprom_update_block(EEPROM_ID, (void *)addr, 4);

Rappelez vous la petite chaîne EEPROM_ID présente au début du source, c’est son rôle.

Deuxième sécurité : on va sauver le nombre d’objets concerné. Si le format est bon, mais qu’il y a plus ou moins d’objets à lire, on va au devant de problèmes. Il faudra que la lecture ait autant d’objets à lire que l’écriture en a sauvé.

EEPROM.update(addr++, this->nbObjets);

Troisième sécurité : il faut que les données écrites et lues soient les mêmes. On aurait pu sauver une petite chaîne représentant le format des données sauvées pour chaque type d’objet, comme "BII" pour un octet et deux entiers. Mais cela nous a semblé superfétatoire. Sauver la taille de chaque type d’objet donne une suffisante approximation du format et permet de probablement déterminer si une modification est intervenue dans un objet. Ce serait pas de chance qu’après une modification la taille soit la même alors que d’autres données sont sauvées dans l’objet (IIB au lieu de BII ...).

	EEPROM.update(addr++, leds[0].GetEEPROMSize());
	EEPROM.update(addr++, aiguilles[0].GetEEPROMSize());
	EEPROM.update(addr++, detecteurs[0].GetEEPROMSize());

et l’on découvre enfin le rôle de ces fonctions de taille...

Une fois l’entête sauvegardée, on sauve l’identifiant du satellite. Notez qu’au fur et à mesure que l’on sauve, addr est incrémenté pour que la suite s’écrive au bon endroit.

	for (int i = 0; i < this->nbObjets; i++)
		if (this->objets[i] != NULL)
			addr = this->objets[i]->EEPROM_sauvegarde(addr);

On va ici ’juste’ balayer la liste de objets, et pour tous ceux qui ne sont pas NULL, on va leur demander de se sauver en appelant leur fonction EEPROMSauvegarde() surchargée. Comme on l’a vu dans ces fonctions, chaque objet va d’abord demander à sa classe de base ’Objet’ de se sauver et va continuer d’écrire ses propres données au bout des autres. A chaque appel de la fonction, elle reçoit l’adresse de début d’écriture et renvoie l’adresse du prochain octet libre dans l’EEPROM. Cette adresse est renvoyée à l’objet suivant, et ainsi de suite.
On a ainsi tout sauvé sans savoir ce que l’on sauve, chaque objet étant en charge de sa propre sauvegarde.

bool Satellite::EEPROM_chargement()
{
	int addr = 0;
	char buf[5];

	////////////////////////////////////// Partie entete

	// ID EEPROM
	eeprom_read_block(buf, (const void *)addr, 4);
	addr += 4;
	buf[4] = 0;

	for (int i = 0; i < 4; i++)
		if (buf[i] != EEPROM_ID[i])
			return false;

	// Nombre d'objets
	uint8_t nb = EEPROM.read(addr++);
	if (nb != this->nbObjets)
		return false;

	// taille des objets
	eeprom_read_block(buf, (const void *)addr, 3);
	addr += 3;

	if (buf[0] != leds[0].GetEEPROMSize())	return false;
	if (buf[1] != aiguilles[0].GetEEPROMSize())	return false;
	if (buf[2] != detecteurs[0].GetEEPROMSize())	return false;

	////////////////////////////////////// Partie satellite

	this->id = EEPROM.read(addr++);

	////////////////////////////////////// Partie objets

	for (int i = 0; i < this->nbObjets; i++)
		if (this->objets[i] != NULL)
			addr = this->objets[i]->EEPROM_chargement(addr);

	return true;
}

L’essentiel du chargement va servir à s’assurer que les données que l’on tente de récupérer sont pertinentes, qu’elles ont bien été sauvées par un Satellite avec exactement les même données.

Première sécurité :

	eeprom_read_block(buf, (const void *)addr, 4);
	addr += 4;
	buf[4] = 0;

La fonction eeprom_read_block va lire quatre octets consécutifs et les stocke dans ’buf’ .

	for (int i = 0; i < 4; i++)
		if (buf[i] != EEPROM_ID[i])
			return false;

On va ensuite vérifier caractère par caractère qu’il s’agit bien du contenu de EEPROM_ID. si ce n’est pas le cas, la sauvegarde est invalide.

Deuxième sécurité :

	uint8_t nb = EEPROM.read(addr++);
	if (nb != this->nbObjets)
		return false;

On va vérifier qu’il y a bien autant d’objets que prévu. Si ce n’est pas le cas, la sauvegarde est invalide.

Troisième sécurité :

	// taille des objets
	eeprom_read_block(buf, (const void *)addr, 3);
	addr += 3;

	if (buf[0] != leds[0].GetEEPROMSize())	return false;
	if (buf[1] != aiguilles[0].GetEEPROMSize())	return false;
	if (buf[2] != detecteurs[0].GetEEPROMSize())	return false;

On va lire les trois octets suivants. Ils doivent représenter les tailles des trois types d’objet connus. Plutôt que de déclarer de nouvelles variables locales, on a réutilisé le ’buf’ de l’entête. Il est assez grand puisque déclaré à quatre octets de long. Si l’une des tailles ne correspond pas, la sauvegarde est invalide.

Et voilà, on est presque sûr que ce que l’on va lire est digne d’intérêt.

	////////////////////////////////////// Partie satellite

	this->id = EEPROM.read(addr++);

	////////////////////////////////////// Partie objets

	for (int i = 0; i < this->nbObjets; i++)
		if (this->objets[i] != NULL)
			addr = this->objets[i]->EEPROM_chargement(addr);

Comme pour la sauvegarde, on s’occupe de l’identifiant puis de tous les objets toujours sans se préoccuper des types réels des objets...

void Satellite::loop()
{
  byte IsMessage = this->Bus.messageRx(); // RxBuf : message de commande (1) ou configuration (2) ou rien (0)
  
	if (IsMessage == 1)
	{
		this->MessageIn.receive(this->Bus.RxBuf); // recup dans mData[] et synchronisation sur les réceptions periodiques
		this->Bus.messageTx();                    // des emissions periodiques concernant les capteurs
	} 
  if (IsMessage == 2)
  {
    this->ConfigMessage.receive(this->Bus.RxBuf); // recup dans cData[] 
    // Entre en mode config dès qu'un message de config temporaire est reçu.
    this->modeConfig = true; //(this->ConfigMessage.IsConfig()) && !this->ConfigMessage.IsPermanentConfig());
    Serial.print("cfg ");Serial.println(this->modeConfig);
  }
 

	// traite les loop prioritaires
	Aiguille::loopPrioritaire();
	Detecteur::loopPrioritaire();
	Led::loopPrioritaire();

	if (this->objets[this->objetCourantLoop] != NULL)
		this->objets[this->objetCourantLoop]->loop(this);

	// puis passe à l'objet suivant pour le prochain loop...
	this->objetCourantLoop++;

	// Si on est à la fin de la liste, on recommence !
	if (this->objetCourantLoop >= this->nbObjets)
		this->objetCourantLoop = 0;

	// Si le dernier message reçu est une config permanente, sauve l'EEPROM.
	// La sauvegarde EEPROM n'écrira pas les octets déjà à la bonne valeur,
	// donc pas de danger d'écrire pour rien.
	if (this->modeConfig && this->ConfigMessage.IsPermanentConfig())
	{
		this->EEPROM_sauvegarde();
		this->modeConfig = false;
    Serial.print("cfg ");Serial.print(this->modeConfig);Serial.println(" Save");
	}
}

Passons à la loop(), dernière fonction du satellite. Le début consiste à laisser le bus CAN se mettre à jour. On va commencer par observer la pile des messages, et la valeur de retour de MessageRx() donnera soit 0 quand la pile est vide, 1 lorsque l’on reçoit un message de commande, 2 pour un message de configuration.
Selon le type de message, on va en mémoriser le contenu pour une commande et en profiter pour renvoyer l’état actuel des capteurs. Ou passer en mode configuration [9].

On va ensuite s’assurer que les loopPrioritaire() sont bien tous appelés. Il faut absolument les traiter le plus souvent possible pour continuer les mouvements de servo en cours ou pour continuer de faire clignoter ou d’augmenter ou de diminuer la luminosité des DEL en cours de changement d’état...

On va ensuite exécuter la fonction loop() de l’objet courant, et juste après incrémenter le compteur pour que le prochain appel à la loop du Satellite traite l’objet suivant. Bien entendu, si le compteur atteint la fin de la liste on repasse le compteur à 0.

Enfin, on va sauver la configuration dans l’EEPROM au fur et à mesure des modifications demandées via le bus CAN. Il n’y a pas de procédure d’extinction des Satellites qui aurait pu être utilisée pour sauver la configuration du moment. Arrêter le réseau et ses satellites se fait en coupant le courant ! Pour éviter de devoir refaire la configuration à chaque fois, la configuration est sauvée en temps réel.

Nous avons fini d’étudier le logiciel du Satellite. Les sources sont disponibles sur notre LocoGit.

[1Vous allez dire que ce devrait être le cas pour tous les logiciels, et vous aurez raison. Mais on sait bien que lorsqu’un logiciel n’est pas destiné à être vu par d’autres, on peut s’autoriser des approximations, des formatages fantaisistes ou des commentaires quasi inexistants. Ce n’est pas bien, mais si on faisait que ce qui est bien, la vie serait bien triste (parenthèse philosophique...)

[2A ce sujet, tout le code a été réalisé en français, mais comme on ne se refait pas, il reste encore beaucoup d’anglicismes qui viennent naturellement à de vieux développeurs au moment de trouver un nom de fonction ou de variable. Nous avons tenté de résister, mais sans grand succès...
Accessoirement, le fonctionnement et la structure de ce petit logiciel se prêtent très bien à un bon exercice du Monde des Objets. C’est donc en C++ que sera rédigé ce code, en utilisant au maximum les capacités du langage objet. Les points qui sont plus de la méthodologie seront en notes externes comme celle-ci, pour faire la différence avec le logiciel lui-même.

[3Notez le refus (assumé) de ne pas utiliser le français pour begin et loop. Le but est là de nommer exactement de la même manière des fonctions déjà présentes par ailleurs. Dans un croquis, on connait bien les fonctions begin() abondamment utilisées sur les objets livrés de l’Arduino comme Serial, Wire ou LiquidCrystal. On connait évidemment le loop() du croquis standard. Difficile donc de ne pas comprendre au premier coup d’œil le rôle de ces fonctions pour nos nouveaux objets...

[4Plutôt que d’avoir un croquis .ino de plusieurs centaines de lignes, mieux vaut créer de multiples petits fichiers. L’IDE Arduino est tout à fait capable de gérer cela facilement.
En programmation objet, il est de bon ton de créer pour chaque classe un fichier d’entête MonObjet.h (ou MonObjet.hpp) et un fichier avec les fonctions et déclarations correspondantes MonObjet.cpp.

[5Notez la présence de majuscules. Pour une question de cohérence et de lisibilité, mettez des majuscules aux noms de classe et de structure, et donc de fichiers. C’est juste une convention que l’on est pas forcé de suivre, mais c’est plus agréable à lire. Toujours pour la lisibilité et la bonne compréhension du code, nommez vos fichiers du nom de la classe qui est dedans, exactement de la même manière. Et si vous renommez une classe, renommez les fichiers qui la contiennent ! Ce qui implique aussi une seule classe par fichier...
Notez ensuite l’absence d’accent sur le détecteur, parce le C++ ne supporte pas les caractères accentués dans les identifiants (classes, variables, fonctions...) et aussi pour éviter des problèmes de compilation éventuels sur certains systèmes.

[6C’est une notion importante qui justifie la nature même des objets : chaque classe doit être, autant que possible, autonome. Ainsi personne dans le satellite ou dans les autres classes dérivées d’Objet, ne sait que LightDimmer est utilisé ici. Si demain Jean-Luc (ou un autre...) nous invente une merveille plus efficace que LightDimmer, alors seuls les sources Led.h et Led.cpp seront à modifier pour intégrer cette nouvelle bibliothèque. A l’inverse, la classe Satellite ne devra jamais se préoccuper de savoir si l’objet qu’elle manipule est une DEL, une Aiguille ou un Detecteur ! Chaque classe d’objet doit rester une boite noire qui s’occupe d’elle même sans penser aux autres. Si d’autres objets devaient apparaître demain, (type de détecteur différent, moteur lent, DEL multicolore...) il suffirait d’ajouter la classe et de déclarer une liste d’objets.

[7Notez que l’utilisation d’une fonction n’aura très probablement ici aucune incidence sur la vitesse de fonctionnement puisque le compilateur remplacera les appels par son contenu directement.

[8Rappelons que le constructeur est la toute première fonction appelée par une classe. D’autre part, les variables, qu’elles soient globales (en haut du source), locales (dans les fonctions) ou membres de classe ne sont pas initialisées par le langage C++. Leur contenu est indéterminé, et c’est la source de nombreux bugs parfois difficiles à trouver.

[9Cette partie devrait en toute logique se trouver dans un CANBus::loop() et éviter ainsi de voir la tripaille de la communication au niveau du satellite.

4 Messages

Réagissez à « La carte Satellite V1 (4) »

Qui êtes-vous ?
Votre message

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

Lien hypertexte

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

Rubrique « Projets »

LaBox, Une Centrale DCC polyvalente et abordable (1)

LaBox, Une Centrale DCC polyvalente et abordable (2)

LaBox, Une Centrale DCC polyvalente et abordable (3)

Comment piloter trains et accessoires en DCC avec un Arduino (1)

Comment piloter trains et accessoires en DCC avec un Arduino (2)

Comment piloter trains et accessoires en DCC avec un Arduino (3)

Comment piloter trains et accessoires en DCC avec un Arduino (4)

SGDD : Système de Gestion DD (1)

SGDD : Système de Gestion DD (2)

SGDD : Système de Gestion DD (3)

La PWM : Qu’est-ce que c’est ? (1)

La PWM : Qu’est-ce que c’est ? (2)

La PWM : Qu’est-ce que c’est ? (3)

La PWM : Qu’est-ce que c’est ? (4)

Mise en oeuvre du Bus CAN entre modules Arduino (1)

Mise en oeuvre du Bus CAN entre modules Arduino (2)

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

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

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

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

Réalisation de centrales DCC avec le logiciel libre DCC++ (1)

Réalisation de centrales DCC avec le logiciel libre DCC++ (2)

Réalisation de centrales DCC avec le logiciel libre DCC++ (3)

Contrôleur à télécommande infrarouge pour centrale DCC++

Gestion d’une gare cachée (1)

Gestion d’une gare cachée (2)

Gestion d’une gare cachée (3)

Chenillard de DEL

Enseigne de magasin

Feux tricolores

Multi-animations lumineuses

L’Arduino et le système de commande numérique DCC

Un décodeur d’accessoire DCC versatile basé sur Arduino

Un moniteur de signaux DCC

Une barrière infrarouge

Un capteur RFID

Un TCO xpressnet

Une animation sonore

L’Arduino au coeur des systèmes de pilotage analogiques ou numériques

Calcul de la vitesse d’un train miniature avec l’Arduino

La génèse d’un réseau 100% Arduino

Une horloge à échelle H0

Simulateur de soudure à arc

Un automatisme de Passage à Niveau

Automatisation du pont FLEISCHMANN 6152 (HO) avec un ESP32 (1)

Identifier et localiser vos trains avec le RFID/NFC et un bus CAN.

Etude d’un passage à niveau multivoies

La rétro-signalisation sur Arduino

Décodeur pour aiguillage à solénoïdes sur Arduino

Un décodeur DCC pour les signaux à deux ou trois feux sur Arduino NANO/UNO

Etude d’un passage à niveau universel

Réalisation pratique d’un système de mesure de vitesse à l’échelle N

Une Passerelle entre le bus S88 et le bus CAN pour la rétro signalisation

Un décodeur DCC pour 16 feux tricolores

Block Automatique Lumineux avec la carte shield "Arduino 4 relays"

Réalisation d’un affichage de gare ARRIVEE DEPART

Ménage à trois (Ordinateur, Arduino, réseau)

Réalisation d’un va-et-vient automatique et réaliste

Souris et centrale sans fil

Communications entre JMRI et Arduino

Annonces en gare avec la RFID

Une croix de pharmacie animée avec Arduino UNO

Réalisation d’un wagon de mesure (distance et vitesse)

Passage à niveau géré par Arduino (1)

Passage à niveau géré par Arduino (2)

Passage à niveau géré par Arduino (3)

Passage à niveau géré par Arduino (4)

Passage à niveau géré par Arduino (5)

Une manette simple et autonome pour LaBox

Éclairer le réseau (1)

Éclairer le réseau (2)

Block Automatique Lumineux à 8 cantons analogiques

Un décodeur DCC pour les plaques tournantes Fleischmann et Roco

Éclairer le réseau (3)

Éclairer le réseau (4)

Éclairer le réseau (5)

JMRI pour Ma première centrale DCC

Rocrail pour Ma première centrale DCC

CDM-Rail pour Ma première centrale DCC (1)

CDM-Rail pour Ma première centrale DCC (2)

Banc de test pour les décodeurs DCC

Ma première manette pour les aiguillages DCC

Mon premier décodeur pour les aiguillages DCC

Boitier 3D pour la station DCC minimale

Va-et-vient pour deux trains

Un programme pour régler facilement les servos moteurs avec un ESP32

Affichage publicitaire avec Arduino (1)

Affichage publicitaire avec Arduino (2)

TCO Web interactif avec des ESP32 et des ESP8266 (1)

TCO Web interactif avec des ESP32 et des ESP8266 (2)

TCO Web interactif avec des ESP32 et des ESP8266 (3)

TCO Web interactif avec des ESP32 et des ESP8266 (4)

TCO Web interactif avec des ESP32 et des ESP8266 (5)

Les derniers articles

LaBox, Une Centrale DCC polyvalente et abordable (3)


Thierry

LaBox, Une Centrale DCC polyvalente et abordable (1)


Thierry

LaBox, Une Centrale DCC polyvalente et abordable (2)


Dominique, msport, Thierry

Un programme pour régler facilement les servos moteurs avec un ESP32


bobyAndCo

TCO Web interactif avec des ESP32 et des ESP8266 (5)


utpeca

TCO Web interactif avec des ESP32 et des ESP8266 (4)


utpeca

TCO Web interactif avec des ESP32 et des ESP8266 (3)


utpeca

TCO Web interactif avec des ESP32 et des ESP8266 (2)


utpeca

TCO Web interactif avec des ESP32 et des ESP8266 (1)


utpeca

Affichage publicitaire avec Arduino (2)


catplus, Christian

Les articles les plus lus

Réalisation de centrales DCC avec le logiciel libre DCC++ (3)

La PWM : Qu’est-ce que c’est ? (1)

Mon premier décodeur pour les aiguillages DCC

Chenillard de DEL

La rétro-signalisation sur Arduino

Mise en oeuvre du Bus CAN entre modules Arduino (2)

Comment piloter trains et accessoires en DCC avec un Arduino (1)

Réalisation de centrales DCC avec le logiciel libre DCC++ (1)

La PWM : Qu’est-ce que c’est ? (2)

Décodeur pour aiguillage à solénoïdes sur Arduino