Auteur Sujet: Gestion du réseau railyRabbit/Onzeroad  (Lu 269 fois)

railyRabbit

  • Newbie
  • *
  • Messages: 29
    • Voir le profil
Gestion du réseau railyRabbit/Onzeroad
« le: janvier 30, 2019, 02:24:40 pm »
Ça fait un moment que je bricole dans mon coin, évoquant par bribes ici ou là certains des aspects électroniques du projet auquel je participe. Je vais essayer de partager plus en détail les réalisations et projets en perspective.
 
Je participe à un réseau relativement simple à échelle N : 2 boucles, 1 voie de garage/belvédère, 3 aiguilles (voir le fil du réseau sur le Forum du N).

1. Matériel de départ
J’avais choisi un système au système complet, le Quad-Pic de Tam Valley Depot, pour piloter les 3 aiguilles du réseau : 3 servos pilotés par une carte à microprocesseur, des façades de contrôle et des cartes-filles équipées d’un relai pour inverser la polarité de la pointe de cœur à mi-parcours. La câblerie est entièrement basée sur des câbles servos, pratique pour une mise en oeuvre rapide. Quelques images pour les curieux :


Les façades de contrôle sont astucieusement conçues. Seuls 3 fils sont nécessaires pour les utiliser : 2 fils d’alimentation, 1 fil de signal. La couleur des LED bicolores (montées en opposition) est gérée via la polarité du fil de signal. En faisant varier rapidement cette polarité, on crée un clignotement alternatif des LED pendant le mouvement. Régulièrement on mesure si le fil de signal n’est pas relié à la masse, ce qui signifie que le bouton est appuyé.



C’est simple, économe en câblage. Je me suis construit un (tout) petit TCO avec 5 fils : 2 fils d’alim, 3 fils de signal, 1 pour chaque aiguille. Ça me permet de visualiser la position des aiguilles et de les commander.

Seulement voilà, entre l’effet de l’échelle N et sans doute la tringlerie maison de transmission entre le servo et l’aiguille, la polarité s’inversait souvent alors que l’aiguille était en contact avec le mauvais rail, causant des court-circuits.

2. Le cahier des charges
a) Mouvement et alimentation des aiguilles > lire
Je décide de remplacer l’électronique de pilotage des aiguilles et d’alimentation des cœurs, tout en gardant la motorisation par servo et les façades de contrôle Tam Valley.

b) Un peu d’automatisation
Je voulais aussi essayer de gérer le talonnage des aiguilles de façon automatique : si un train arrive et que l’aiguille n’est pas dans la bonne position, hop on corrige. Encore faut-il savoir dans quel sens circule le train.
Enfin, dernière piste qui s’est greffée récemment, gérer les itinéraires disponibles et pouvoir faire circuler un train sur un itinéraire aléatoire, quel que soit le sens de marche.

c) Yes we CAN
A l’heure actuelle, ayant fait le a) puis b), tout est géré en central sur un Arduino Uno où j’ai consommé toutes les entrées/sorties (et un peu plus de 50% de flash, rien que pour stocker le programme). J’envisage de décentraliser en mettant en œuvre 3 ou 4 satellites CAN autour d’un chef d’orchestre, plus 1 autre satellite dans le TCO.
Chaque satellite (sauf celui du TCO, un peu plus spécialisé) aura donc à gérer la détection infrarouge, les servos et l’alimentation des aiguilles, ainsi que très probablement des éclairages.

d) Radio Ga Ga
Étape suivante, rendre le TCO sans fil. Il faudra que je me repenche sur ce que Locoduino à démontré à Orléans. L’idée est que, en filaire, le TCO recharge sa batterie et communique en CAN. Sans fil, on passe en liaison radio. Wifi, Bluetooth, 2.4GHz ou 433MHz non licenciés, à voir. En revanche je n’ai pas nécessairement envie d’un TCO sur tablette ou smartphone, ce qui rend le choix du wifi moins évident (et plus de la consommation électrique).

e) Est-ce bien raisonnable ?
Non bien sûr, ça marche pas mal comme ça. Mais c’est aussi l’occasion de tester des nouvelles choses !

La suite très vite.
« Modifié: février 03, 2019, 04:39:46 pm par railyRabbit »

railyRabbit

  • Newbie
  • *
  • Messages: 29
    • Voir le profil
3. Mouvement et alimentation des aiguilles
« Réponse #1 le: février 03, 2019, 04:29:28 pm »
3. Mouvement et alimentation des aiguilles
a) Matériel

Comme expliqué en introduction, les aiguilles du réseau sont mues par des petits servos 4G, fournis avec le matériel Tam Valley. La polarité des cœurs étaient à l'origine manœuvrées par les cartes-filles.

Celles-ci sont dotées d'un relai DPDT qui n'autorise qu'une gestion 'binaire' de la polarité du cœur (elle correspond à l'un OU l'autre des rails) sans pouvoir totalement l'isoler pendant le mouvement, d'où les fréquents court-circuits.

J'ai donc acheté des cartes relais 2 relais − solution court-terme − sur un site chinois bien connu. Elles me permettent de gérer la polarité des cœurs en toute liberté :
  • j'alimente le relai 1 pour polariser le cœur sur le rail 1
  • j'alimente le relai 2 pour polariser le cœur sur le rail 2
  • si aucun des deux relais n’est alimenté, le coeur est isolé



Pour gérer les servos, j'avais une carte driver 12 servos Adafruit sous le coude. Cette carte communique avec l'Arduino par le bus I2C. C'est surdimensionné pour 3 servos, mais je comptais m'en servir pour gérer des éclairages sur les ports inutilisés.

b) Logiciel
C'est là que j'ai commencé à me frotter aux Classes, en suivant la série d'articles "Le monde des objets" de Locoduino bien sûr ! Soyez indulgents sur la mise en œuvre :) Je suis preneur des conseils pour améliorer le code. J’ai aussi pas mal joué sur les manipulations de bits pour la gestion des registres d’état.

Le fichier .h déclare une classe turnoutDriver avec ses propriétés, fonctions privées et publiques.
Il déclare aussi quelques fonctions “générales” pour initialiser la librairie servo, initialiser les objets, mettre à jour l’ensemble des objets ou encore configurer les objets pendant l'exécution.

Le rôle de cette librairie est de mouvement des aiguilles et l’alimentation des cœurs. A terme, elle doit également communiquer avec le gestionnaire des façades de contrôle, le gestionnaire de détecteurs infrarouges (qui vont également donner des ordres selon le sens de marche et la position des aiguilles) et enfin avec un automate (qui va générer des itinéraires aléatoires).

Pour cela, la librairie crée plusieurs registres globaux :
Le registre d’état turnoutDriver_states : c’est un byte qui représente la position des aiguilles. Chaque bit représente une aiguille, un 1 si elle est déviée, un 0 si elle est fermée. Lorsqu’une aiguille est en cours de mouvement, son état représente sa position en fin de mouvement : si nécessaire (un train arrive sur une aiguille qui va être talonnée), on peut lui donner un contre-ordre même si la manœuvre n’est pas terminée.
Ce registre est utilisé en lecture / écriture par le gestionnaire d’aiguilles, et en lecture uniquement par les autres librairies qui ont besoin de connaître la position des aiguilles.
Le registre de progression turnoutDriver_progress : encore un byte, qui mémorise les aiguilles en cours de manœuvre. Chaque bit représente une aiguille, un 1 si elle est en cours de mouvement, un 0 si elle est immobile.
Ce registre est utilisé en lecture/écriture par le gestionnaire d’aiguilles, et en lecture uniquement par le gestionnaire des façades (pour gérer le clignotement des LEDs).
Le registre d’ordres turnoutController_buttons, toujours un byte, sert à transmettre les boutons de contrôle activés les ordres d’inversion des aiguilles, qu’ils viennent de la détection des boutons des façades de contrôle, de l’automate, des détecteurs IR… Là encore, chaque bit représente une aiguille. Un bit à 1 signifie qu’il faut inverser la position de l’aiguille correspondante.
Ce registre est utilisé en lecture/écriture par à peu près tout le monde ! Le gestionnaire d’aiguilles lit les bits et les réinitialise un fois les ordres traités. Les autres librairies positionnent les bits à 1 pour transmettre un ordre de manœuvre.

La taille de ces registres limitent l’utilisation à 8 aiguilles (ou 8 servos).

La librairie se charge d’instancier le gestionnaire de servos (ici c’est la librairie Adafruit_PWMServoDriver qui pilote le PCA9685 de la carte Adafruit) et la fonction turnoutDriverStart() va l’initialiser :

Adafruit_PWMServoDriver servos = Adafruit_PWMServoDriver();

void turnoutDriverStart()
{
    servos.begin();
    servos.setPWMFreq(60);
}

L’objet turnoutDriver possède les propriétés suivantes :
  • le numéro de l’aiguille (qui correspond également au numéro de port sur la carte driver des servos)
  • la valeur numérique des positions fermée/déviée
  • la vitesse du mouvement
  • s’il y a un cœur à alimenter ou non
  • les pins sur lesquels les relais sont connectés
  • le signal pour activer les relais (0 ou 5v : HIGH ou LOW)
  • la valeur numérique de la position courante du servo
  • le timestamp du dernier mouvement (également utilisé pour gérer le mouvement lent)

La fonction TurnoutDriverInit() va prendre en charge le processus d’instanciation et d’initialisation des aiguilles :

void turnoutDriverInit(int positionClosed, int positionThrown, byte pinFeederClosed, byte pinFeederThrown, bool feederOnLevel, int tempo)
{
    turnoutDriver_register[turnoutDriver_count] = new turnoutDriver (positionClosed, positionThrown, pinFeederClosed, pinFeederThrown, feederOnLevel, tempo);
    turnoutDriver_register[turnoutDriver_count]->begin();
    turnoutDriver_count++;
}

Elle instancie d’abord l’objet turnoutDriver, et s’assure que le courant est coupé dans le coeur :

turnoutDriver::turnoutDriver(int positionClosed, int positionThrown, byte pinFeederClosed, byte pinFeederThrown, bool feederOnLevel, int tempo)
{
    _driverId = turnoutDriver_count;
    _closed = positionClosed;
    _thrown = positionThrown;
 
    if (_closed > _thrown)
    {
        _closed *= -1;
        _thrown *= -1;
    }

    _tempo = tempo;
    _lastMove = 0;

    if (pinFeederClosed == 255)
        _hasFeeder = false;
    else
    {
        _hasFeeder = true;
        _pinFeederClosed = pinFeederClosed;
        _pinFeederThrown = pinFeederThrown;
        _feederOnLevel = feederOnLevel;
        pinMode(_pinFeederClosed, OUTPUT);
        pinMode(_pinFeederThrown, OUTPUT);
        power(0);
    }
}

La fonction récupère la référence de l’objet créé et le stocke dans le tableau turnoutDriver_register. Ainsi, il est possible de créer successivement les aiguilles depuis le programme principal et de sous-traiter la mise à jour de l’ensemble des objets. C’est ce que fait la fonction turnoutDriverUpdate() en parcourant ce tableau et invoquant successivement la fonction update() de chaque objet :

void turnoutDriverUpdate()
{
    for (byte i = 0; i < turnoutDriver_count; i++)
        turnoutDriver_register[i]->update();
}

Le programme principal peut donc ressembler donc à ça :

#include "turnoutDriver.h"

void setup()
{
    turnoutDriverStart();
    turnoutDriverInit(394, 334, 8, 12, HIGH, 40);
    turnoutDriverInit(347, 375, 8, 12, HIGH, 75);
    turnoutDriverInit(357, 329, 8, 12, HIGH, 95);
}

void loop()
{
    turnoutDriverUpdate();
}

La fonction d’initialisation va ensuite initialiser l’aiguille en position fermée :

void turnoutDriver::begin()
{
    _position = _closed;
    servos.setPWM(_driverId, 0, abs(_closed));
    turnoutDriver_states &= ~(1 << _driverId);
    turnoutDriver_progress &= ~(1 << _driverId);
    power(1);
}

A chaque tour de programme, la fonction update() regarde si un ordre a été transmis à l’aiguille via le registre turnoutController_buttons. Si c’est le cas, on appelle la fonction d’inversion (toggle()) puis on réinitialise le bit du registre d’ordre :

if (readButton())
{
    toggle();
    turnoutController_buttons &= ~(1 << _driverId);
}

Tous les mouvements (inversion, ouverture, fermeture) sont traités de la même façon.

D’abord on initialise le mouvement :
  • on renseigne le registre d’état avec la position de destination (ouverte ou fermée)
  • on renseigne le registre de progression, pour indiquer que l’aiguille est en mouvement
  • on coupe de courant dans le cœur

void turnoutDriver::initiateMove(bool _direction)
{
    if (_direction)
        turnoutDriver_states |= 1 << _driverId;
    else
        turnoutDriver_states &= ~(1 << _driverId);
    power(0);
    turnoutDriver_progress |= 1 << _driverId;
}

A chaque tour de programme, on met à jour la position des aiguilles en mouvement. Voici donc la fonction update() complète :

void turnoutDriver::update()
{
    if (readButton())
    {
        toggle();
        turnoutController_buttons &= ~(1 << _driverId);
    }
    if (watchProgress())
        move();
}

La fonction move() évalue la position réelle des aiguilles en mouvement par rapport à la position de destination.Si l’aiguille doit bouger et si l’intervalle de temps souhaité entre deux mouvements est atteint (pour gérer la vitesse), on incrémente/décrémente la position d’une unité. Et ainsi de suite.

void turnoutDriver::move()
{
    if (millis() - _lastMove > _tempo)
    {

        if (state())
            if (_position < _thrown)
            {
                _position++;
                servos.setPWM(_driverId, 0, abs(_position));
            }
            else
                terminateMove();

        else

            if (_position > _closed)
            {
                _position--;
                servos.setPWM(_driverId, 0, abs(_position));
            }
            else
                terminateMove();

        _lastMove = millis();
    }
}

Lorsque la position de destination est atteinte, on réinitialise le bit de progression et on remet le courant.

void turnoutDriver::terminateMove()
{
  turnoutDriver_progress &= ~(1 << _driverId);
  power(1);
}

Voilà pour l’essentiel.  ;)
« Modifié: février 03, 2019, 04:32:16 pm par railyRabbit »