Episode 4.5 - Automatisme et finitions
Il est temps de finir notre fosse de visite. La partie qui suit est optionnelle, elle consiste à ajouter un automatisme pour l’éclairage de la fosse. Une solution plus simple consiste juste à brancher + et - des bandeaux de leds à l’alimentation des accessoires.
14 mai 2021 : simplification du montage et amélioration du filtre pour lire l’état de la voie (cf Tuto05)
Pour cet automatisme je me suis donnée le cahier des charges suivants :
- si la voie est STOP, la fosse est éteinte, quelque soit son état d’occupation.
- lorsqu’une locomotive se présente sur la fosse, elle ne s’éclaire qu’après un laps de temps (8s avec mon paramétrage). Cela permet de laisser la fosse éteinte quand la locomotive ne fait que passer.
- de manière symétrique, lorsqu’une locomotive quitte la fosse allumée, celle-ci s’éteint après un laps de temps (8s avec mon paramétrage).
- lorsque la fosse est allumée, un mode veille se déclenche qui éteint la fosse au bout de 6 minutes.
- deux entrées en pull up (mettre un interrupteur à GND) permettent de forcer l’allumage ou l’extinction.
Un afficheur optionnel affiche l’état de la fosse et l’éventuel décompte en cours :
- “5 -” : la voie est STOP
- “E xx” : la fosse est éteinte mais la séquence d’allumage est enclenchée - il reste xx secondes avant allumage
- “A xx” : la fosse est allumée mais la séquence d’extinction est enclenchée - il reste xx secondes avant extinction
- “Ax:xx” : la fosse est allumée mais la séquence de mise en veille est enclenchée - il reste xxx secondes avant la mise en veille
- " : " : l’éclairage de la fosse est en veille
- “U xx” : l’état xx n’est pas connu (ça ne devrait jamais s’afficher !)
- “FA” : la fosse est forcée allumée
- “FE” : la fosse est forcée éteinte
La liste du matériel comprend (on peut aussi acheter directement chez AZ Delivery mais j’ai profité de mon abonnement Prime chez Amazon pour ne pas payer de frais de port) :
- un Arduino Uno (AZ Delivery) : https://www.amazon.fr/gp/product/B0755XYBG4/
- une carte relais 1 canal 5V (AZ Delivery) : https://www.amazon.fr/gp/product/B07TYGBN7C/
- une carte capteur de tension 0-25 V (Stemedu) : https://www.amazon.fr/gp/product/B07L81QJ75/
- un lot de connecteurs (AZ Delivery) : https://www.amazon.fr/gp/product/B07KCFG5YX/
- un afficheur optionnel 4 digits 7 segments TM1637 (AZ Delivery) : https://www.amazon.fr/gp/product/B078S8SGW2/
Soit un automatisme qui va couter unitairement moins de 14 euros.
Le schéma de connexion est une simple évolution du tutoriel Tuto07, avec deux entrées de forçage supplémentaire :
Le sketch (pour utiliser la terminologie Arduino, c’est à dire le programme) est assez conséquent mais facile à suivre (très commenté et la suite logique du tutoriel progressif de l’épisode 6).
La vraie nouveauté est l’implémentation d’une machine à état finis (Finite State Machine) pour s’assurer de l’exhaustivité des situations rencontrées par l’automatisme. C’est un objet informatique moins classique mais que toute personne pratiquant l’automatisme devrait connaitre.
On trouvera plus d’information sur les machines à états finis sur Google, et on commencera par la page Wikipedia Automate fini — Wikipédia
// Fosse v1.5
// (c) Julie Dumortier, 2020-2021
// licence GPL
//
// Evolutions
// v1.0 première version sur plateforme de démonstration
// v1.5 détection STOP sur la voie et afficheur optionnel (v20201222)
// 14 mai 2021 : lecture de l'état STOP directement sur le rail !
// mettre à 1 pour un debug dans la console série
#define debug 1
// D3 : input : forçage allumé (pullup) (par défaut, l'allumage n'est pas forcé)
// D4 : input : forçage éteint (pullup) (par défaut, l'extinction n'est pas forcée)
// D5 : output : allumage des bandeaux de leds (COMMON - NC)
int Pin_forcageAllume = 3;
int Pin_forcageEteint = 4;
int Pin_allumageLeds = 5;
// A3 : input : état d'occupation de la fosse (rail contact / fil bleu)
int Pin_etatRail = 3;
// Temps avant allumage (en secondes) : 4 (debug) ou 8
int tempsAvantAllumage = 8/(1+debug);
// Compteur de temps avant l'ellumage (en millisecondes)
unsigned long timeAllumage = 0;
// Mise en veille (en secondes) : 8 (debug) ou 180
int tempsAvantVeille = 60*6/(1+(43*debug));
// Compteur de temps avant la Veille (en millisecondes)
unsigned long timeVeille = 0;
// Temps avant extinction (en secondes) : 4 (debug) ou 8
int tempsAvantExtinction = 8/(1+debug);
// Compteur de temps avant l'extinction (en millisecondes)
unsigned long timeExtinction = 0;
// Filtre bas du signal d'occupation
int msFiltre = 192;
// variable pour compter les déclenchements
int nbFiltre = 16;
// Seuil de déclenchement du signal d'occupation
int seuilSignal = 384;
// inclus une librairie toute simple pour l'afficheur
#include <Arduino.h>
#include <TM1637Display.h>
// Connexion de l'afficheur sur lAarduino : CLK sur D9 et DIO sur D8
#define CLK 9
#define DIO 8
// Crée l'objet display pour interagir avec l'afficheur
TM1637Display display(CLK, DIO);
// structure globale avec le contenu de l'afficheur
uint8_t segments[] = {0xff, 0xff, 0xff, 0xff};
// FSM (Machine à etats finis) Etats possibles des lumières de la fosse
// cf https://fr.wikipedia.org/wiki/Automate_fini
// Ce type d'approche permet d'implémenter un automatisme robuste
#define state_Eteint 0
#define state_Allume 1
#define state_ForceEteint 2
#define state_ForceAllume 3
#define state_enAllumage 4
#define state_enExtinction 5
#define state_enVeille 6
// La FSM est complétée par l'état de la voie
// Les différents états possibles pour la voie sur le rail contact
// voie_Occupee la voie est occupée
// voie_Libre la voie est libre
// voie_STOP la voie est en STOP (CSx ou MSII)
#define voie_STOP 99
#define voie_Occupee 0
#define voie_Libre 1
// AfficheSegVal
// affiche des segments sur le premier digit et une valeur sur les trois autres digits
// seg : encodage du 1er digit
// val : valeurs à afficher sur les trois digits suivant
// dp : allume le double point
void AfficheSegVal(uint8_t seg,int val,boolean dp=false)
{
uint8_t b1;
uint8_t b2;
// prépare la structure des segments
segments[0] = seg;
if (val>0) {
b1 = (val / 100) % 10;
segments[1] = b1?display.encodeDigit(b1):0x00 | (dp?0x80:0x00);
segments[2] = (b1||b2) ? display.encodeDigit((val / 10) % 10) : 0x00;
segments[3] = display.encodeDigit(val % 10);
} else {
segments[1] = (dp?0x80:0x00);
segments[2] = 0x00;
segments[3] = SEG_G;
}
// appel la librairie pour envoyer les segments sur l'afficheur
display.setSegments(segments);
}
// AfficheEtatVal
// affiche l'état de la voie et une valeur (tension sur la voie) en dV
// etat : etat de la voie
// val : valeur de la tension (0 - 1023)
void AfficheEtatVal(int etat,int val)
{
uint8_t seg; // segments du 1er digit
float temp; // variable temporaire pour la conversion
// conversion de val en dixième de Volts (dV)
// capteur de tension est sur une plage 0-25V
temp = (250.0*val)/1024.0;
val = (int)temp;
// construire le 1er segment selon l'état de la voie
switch (etat) {
case voie_Libre:
// (L)ibre
seg = SEG_F | SEG_E | SEG_D;
if (debug) { Serial.print("voie_Libre val="); Serial.println(val);}
break;
case voie_Occupee:
// (O)ccupée
seg = SEG_A | SEG_B | SEG_C | SEG_D | SEG_E | SEG_F;
if (debug) { Serial.print("voie_Occupee val="); Serial.println(val);}
break;
case voie_STOP:
// (S)top + val
seg = SEG_A | SEG_C | SEG_D | SEG_F | SEG_G;
if (debug) { Serial.print("voie_STOP val="); Serial.println(val);}
break;
default:
// (U)nknown + etat
seg = SEG_B | SEG_C |SEG_D | SEG_E |SEG_F ;
val = etat;
if (debug) { Serial.print("voie_"); Serial.print(val); Serial.print("? val="); Serial.println(val);}
break;
}
// affiche le contenu
AfficheSegVal(seg,val);
}
// EtatVoie
// retourne l'état de la voie (voie_Occupee,voie_Libre, voie_STOP)
//
// L'algorithme est assez trivial
// Lecture des entrées analogiques vr Rail contact et vt Rail traction
// Si on trouve au moins un vr supérieur à un seuil, le rail contact est occupé
// Si on ne trouve aucun vt supérieur à un seuil, le rail traction est STOP
// Sinon le rail contact est libre
//
// NB : quand le rail traction est STOP, il n'est pas possible de savoir si le
// rail contact est libre ou occupé.
int EtatVoie()
{
int n = msFiltre; // nombre de fois que l'on va s'assurer que le rail contact est libre
int c = 0;
int vr = 0; // valeur analogique sur le Rail Contact
int vt = 0; // valeur analogique sur le Rail traction
// flag pour savoir si on a recu un signal de traction
int traction = 0;
// tant que le signal est bas on continue
while (n>0) {
// lit les deux entrées : rail contact et rail normal
vr = analogRead(Pin_etatRail);
vt = vr; // analogRead(Pin_etatTraction);
// la voie de traction est-elle alimentée ?
if (vt>1) {
traction = vt;
}
// le rail de contact est-il libre ?
if (vr>seuilSignal) {
c = c + 1;
}
if (c>nbFiltre) {
// la voie est encore occupée
// attends la fin du compteur pour une fonction à temps défini (environ filtreBas ms)
delay(n);
// met à jour l'afficheur
if (debug) AfficheEtatVal(voie_Occupee,vr);
// et retourne l'état d'occupation
return voie_Occupee;
}
// attends 1 ms
delay(1);
// et boucle
n = n - 1;
}
// on a eu un signal de traction
if (traction>0) {
// le rail contact est resté bas --> la voie est libre
// met à jour l'afficheur
if (debug) AfficheEtatVal(voie_Libre,traction);
return voie_Libre;
}
// pas de traction
// met à jour l'afficheur
if (debug) AfficheEtatVal(voie_STOP,0);
return voie_STOP;
}
// Etat courant de la machine à état finis (FSM)
// A l'initialisation de l'Arduino, tout est censé être éteint
int state = state_Eteint;
// FSM Séauences (cahier des charges) :
// 1. fosse normalement éteinte sauf si forçage par D3 (M83 - bouton vert - au commun==GND)
// 2. une locomotive passe sans s’arrêter, la fosse reste éteinte sauf si le forçage est en cours
// 3. une locomotive stationne, au bout d’un temps programmable, la fosse s’allume
// 4. la fosse allumée s’éteint au bout d’un certain temps programmable, économie d’énergie oblige
// 5. une locomotive repart, la fosse allumée s’éteint au bout d’un temps programmable
// 6. D4 pour un forçage éteint même si des locomotives stationnent (M83 - bouton rouge - au commun==GND)
// allumeLeds
void allumeLeds()
{
// relais COMMON - NC (led verte allumée)
digitalWrite(Pin_allumageLeds,LOW);
}
// eteintLeds
void eteintLeds()
{
// relais COMMON - NO (led verte eteinte)
digitalWrite(Pin_allumageLeds,HIGH);
}
// Eteint la fosse
void EteintFosse()
{
switch (state) {
case state_enAllumage:
// la fosse était en phase d'allumage quand l'ordre d'éteindre arrive
// on stoppe le timer d'allumage et on passe en statut Eteint
timeAllumage = 0;
state = state_Eteint;
// par acquis de conscience, on éteint les leds (censées être eteintes)
eteintLeds();
if (debug) Serial.println("EteintFosse() state:enAllumage -> Eteint");
break;
case state_ForceEteint:
// éteint immédiatement les leds si ce n'est pas déjà fait
eteintLeds();
if (debug) Serial.println("EteintFosse() state:ForceEteint");
break;
case state_enVeille:
// la fosse passe en veille
// on stoppe le timer de veille et on passe en statut Eteint
state = state_Eteint;
timeVeille = 0;
// éteint immédiatement les leds si ce n'est pas déjà fait
eteintLeds();
if (debug) Serial.println("EteintFosse() state:enVeille -> Eteint");
break;
case state_Eteint:
// la fosse est déjà Eteint, ça ne change pas grand chose
// par acquis de conscience, on éteint les leds (censées être eteintes)
eteintLeds();
if (debug) Serial.println("EteintFosse() state:Eteint");
break;
case state_Allume:
// La fosse est allumée, il faut démarrer la séquence d'extinction
// et amorcer le timer d'extinction
state = state_enExtinction;
timeExtinction = millis()+tempsAvantExtinction*1000L;
if (debug) Serial.println("EteintFosse() state:Allume -> enExtinction");
break;
case state_ForceAllume:
// l'ordre d'éteindre doit être ignoré et
// allume immédiatement les leds si ce n'est pas déjà fait
allumeLeds();
if (debug) Serial.println("EteintFosse() state:ForceAllume");
break;
case state_enExtinction:
// l'ordre d'extinction est donné alors que la fosse se prépare à s'éteindre
// il faut vérifier si le timer est échu -> phase d'extinction terminée ?
if (timeExtinction < millis()) {
// effectivement, le temps est écoulé, on remet le timer à 0 et on éteint tout
timeExtinction = 0;
state = state_Eteint;
eteintLeds();
if (debug) Serial.println("EteintFosse() state:enExtinction -> Eteint");
} else {
// le temps n'est pas écoulé, on attend avant d'éteindre
if (debug) {
Serial.print("EteintFosse() state:enExtinction millis=");
Serial.print(millis());
Serial.print(" timeExtinction=");
Serial.println(timeExtinction);
}
}
break;
default:
if (debug) {
Serial.print("EteintFosse() state:unknown state ");
Serial.println(state);
}
// ne fait rien
break;
}
}
// Allume la fosse
void AllumeFosse()
{
switch (state) {
case state_enAllumage:
// l'ordre d'allumage est donné alors que la fosse se prépare à s'allumer
// il faut vérifier si le timer est échu -> phase d'eallumage terminée ?
if (timeAllumage < millis()) {
// effectivement, le temps est écoulé, on remet le timer à 0 et on allume tout
timeAllumage = 0;
state = state_Allume;
allumeLeds();
// et on amorce le timer pour le mode veille !
timeVeille = millis() + tempsAvantVeille*1000L;
if (debug) Serial.println("AllumeFosse() state:enAllumage -> Allume");
} else {
// le temps n'est pas écoulé, on attend avant d'allumer
if (debug) {
Serial.print("AllumeFosse() state:enAllumage millis=");
Serial.print(millis());
Serial.print(" timeAllumage=");
Serial.println(timeAllumage);
}
}
break;
case state_ForceEteint:
// éteint immédiatement les leds si ce n'est pas déjà fait
eteintLeds();
if (debug) Serial.println("AllumeFosse() state:ForceEteint");
break;
case state_Eteint:
// les leds sont éteintes, il faut démarrer la séquence d'allumage
// avec le timer d'allumage
state = state_enAllumage;
timeAllumage = millis()+tempsAvantAllumage*1000L;
if (debug) Serial.println("AllumeFosse() state:Eteint -> enAllumage");
break;
case state_Allume:
// en état allumé, nous devons vérifier l'échéance du timer de veille
// le timer de veille est-il échu ?
if (timeVeille<millis()) {
// oui -> la veille doit se déclencher
timeVeille = 0;
state = state_enVeille;
// on peut éteindre les leds
eteintLeds();
if (debug) Serial.println("AllumeFosse() state:Allume -> EnVeille");
} else {
// le timer veille n'est pas encore échu
// par acquis de conscience, on allume les leds (censées être allumées)
allumeLeds();
if (debug) {
Serial.print("AllumeFosse() state:Allume millis=");
Serial.print(millis());
Serial.print(" timeVeille=");
Serial.println(timeVeille);
}
}
break;
case state_ForceAllume:
// allume immédiatement les leds si ce n'est pas déjà fait
allumeLeds();
if (debug) Serial.println("AllumeFosse() state:forceAllume");
break;
case state_enExtinction:
// l'ordre d'allumage est reçu alors que nous étions en phase d'extinction
// annule la phase d'extinction
timeExtinction = 0;
state = state_Allume;
// par acquis de conscience, on allume les leds (censées être allumées)
allumeLeds();
if (debug) Serial.println("AllumeFosse() state:enExtinction -> Allume");
break;
case state_enVeille:
// éteint immédiatement les leds si ce n'est pas déjà fait
eteintLeds();
if (debug) Serial.println("AllumeFosse() state:enVeille");
break;
default:
if (debug) {
Serial.print("AllumeFosse() state:unknown state ");
Serial.println(state);
}
// ne fait rien
break;
}
}
// AfficheEtatVal
// affiche l'état de la FSM et un décompte représentatif
// le mode veille affiche le double point
//
void AfficheEtatFSM(int ev)
{
// vérifie si la voie est sous-tension
if (ev==voie_STOP) {
// (5)top
AfficheSegVal(SEG_A | SEG_C | SEG_D | SEG_F | SEG_G,0);
return;
}
// sinon, affiche l'état de la FSM et de l'éventuel décompte en cours
switch (state) {
case state_Eteint:
// (E)teint
AfficheSegVal(SEG_A | SEG_G | SEG_D | SEG_E | SEG_F,0);
break;
case state_Allume:
// (A)llume + décompte veille
AfficheSegVal(SEG_A | SEG_F | SEG_B | SEG_E | SEG_C | SEG_G,(timeVeille-millis())/1000,true);
break;
case state_ForceEteint:
// FE
segments[0] = SEG_A | SEG_F | SEG_G | SEG_E ;
segments[1] = SEG_A | SEG_G | SEG_D | SEG_E | SEG_F ;
segments[2] = 0x00;
segments[3] = 0x00;
display.setSegments(segments);
break;
case state_ForceAllume:
// FA
segments[0] = SEG_A | SEG_F | SEG_G | SEG_E ;
segments[1] = SEG_A | SEG_B | SEG_C | SEG_G | SEG_E | SEG_F;
segments[2] = 0x00;
segments[3] = 0x00;
display.setSegments(segments);
break;
case state_enAllumage:
// (E)teint + décompte d'allumage
AfficheSegVal(SEG_A | SEG_G | SEG_D | SEG_E |SEG_F,(timeAllumage-millis())/1000);
break;
case state_enExtinction:
// (A)llumé + décompte d'extinction
AfficheSegVal(SEG_A | SEG_F | SEG_B | SEG_E | SEG_C | SEG_G,(timeExtinction-millis())/1000);
break;
case state_enVeille:
// :
AfficheSegVal(0,0,true);
break;
default:
// (U)nknown + state
AfficheSegVal(SEG_B | SEG_C |SEG_D | SEG_E |SEG_F,state);
break;
}
}
// code executé une seule fois au démarrage du module (ou après un reset)
void setup() {
// programme les différentes Pin en entrée ou sortie
pinMode(Pin_forcageAllume,INPUT_PULLUP);
pinMode(Pin_forcageEteint,INPUT_PULLUP);
pinMode(Pin_allumageLeds,OUTPUT);
eteintLeds();
// au démarrage il est probable que la voie soit STOP
pinMode(LED_BUILTIN,OUTPUT);
digitalWrite(LED_BUILTIN,HIGH);
// ouvre le port série (console de l'outil) avec la vitesse 57600 bauds
// attention que le paramètre sur la console soit bien 57600 !
if (debug) Serial.begin(57600);
// Annonce la version
Serial.println("Fosse v1.5 - (c) Julie Dumortier - Licence GPL");
// On démarre avec la fosse éteinte et la voie STOP
state = state_Eteint;
timeExtinction = 0;
timeVeille = 0;
timeAllumage = 0;
// regle l'intensité de l'affichage
display.setBrightness(debug?0x0f:0x0d);
display.clear();
EteintFosse();
}
// code executé en permanence (une boucle)
void loop() {
int ev; // état de la voie
// demande de forçage eteint ?
if (digitalRead(Pin_forcageEteint)==LOW) {
// demande d'etre eteint en permanence
state = state_ForceEteint;
if (debug) Serial.println("loop() IN: state:ForceEteint");
} else {
if (state==state_ForceEteint) {
// fin du forçage eteint
state = state_Eteint;
if (debug) Serial.println("loop() state:ForceEteint -> Eteint");
}
}
// demande de forçage allumé ?
if (state!= state_ForceEteint) {
if (digitalRead(Pin_forcageAllume)==LOW) {
// demande d'etre allumé en permanence
state = state_ForceAllume;
if (debug) Serial.println("loop() IN: state:ForceAllume");
} else {
if (state==state_ForceAllume) {
// fin du forçage allumé, amorce le timer de veille
state = state_Allume;
timeVeille = millis() + tempsAvantVeille*1000L;
if (debug) Serial.println("loop() state:ForceAllume -> Allume");
}
}
}
ev = EtatVoie();
switch (ev) {
case voie_Occupee:
// indique que la voie est sous-tension
digitalWrite(LED_BUILTIN,LOW);
// déclenche la FSM d'allumage de la Fosse
AllumeFosse();
break;
case voie_STOP:
// Modifie l'état de la FSM pour demander une extinction immédiate
state = state_Eteint;
timeExtinction = 0;
// déclenche la FSM d'extinction de la Fosse
EteintFosse();
// indique que la voie est STOP
digitalWrite(LED_BUILTIN,HIGH);
break;
case voie_Libre:
// indique que la voie est sous-tension
digitalWrite(LED_BUILTIN,LOW);
// déclenche la FSM d'extinction de la Fosse
EteintFosse();
break;
default:
if (debug) {
Serial.print("loop() EtatVoie : unknown state =");
Serial.println(ev);
}
break;
}
// pour avoir le temps de lire l'état de la voie
if (debug) delay(500);
// affiche l'état de la FSM
AfficheEtatFSM(ev);
// pour avoir le temps de lire l'état de la FSM
if (debug) delay(250);
// délai normal : la FSM est déclenchée 4 fois par seconde environ
delay(250-msFiltre);
}
Pour télécharger le programme : fosse_v1.5.pdf (43,0 Ko)
Et comme d’habitude, une petite vidéo de démonstration de l’ensemble :
Ce chapitre clos la réalisation de notre fosse d’inspection. Mais le sujet n’est pas totalement clos, je travaille à une intégration propre des fosses Auhagen que je vais utiliser dans ma double remise.
Moins visible dans la remise, elles ne seront pas automatisées, seulement éclairées et fonctionnelles avec un “discret” rail minitrix en rail central.
A suivre donc …