Note : cette page ne décrit pas un système complet, prêt à l’emploi, mais présente seulement quelques solutions, accompagnées de leurs tests de faisabilité.
Rappel :
Comme indiqué sur la page précédente, il s’agit ici de localiser et d’identifier les trains en installant un émetteur infrarouge à l’intérieur des locomotives, et des détecteurs le long de la voie.
Les émetteurs embarqués
Ils peuvent être réalisés comme un montage autonome, pour installation dans un engin moteur, à coté du décodeur DCC traditionnel, ou bien logés dans un véhicule auxiliaire.
Ils peuvent être alimentés depuis le courant de voie, via un circuit ad-hoc, ou bien depuis le décodeur DCC, comme un accessoire embarqué. Pour les tests, nos transmetteurs sont alimentés par batterie.
Dans le cas où on construit ses décodeurs soi-même (mais oui, ça se fait!), le signal peut être généré par le microcontrôleur du décodeur, et seule la LED infrarouge est à ajouter.
Cet émetteur est construit autour d’un PIC 12F675, simplement parce que nous en avons plein nos tiroirs.
Un ATtiny ou autre petite bête à huit pattes convient aussi bien.
(Un condensateur de découplage CMS de 1µF n’est pas représenté sur ce schéma).
Le plus encombrant, ce sont le support du circuit intégré, l’inter et les connecteurs…
… mais l’ensemble se loge dans un tombereau à l’échelle N.
Pour les test préliminaires, la LED est orientée latéralement.
L’alimentation se fait par par batterie LiPo. Dans ce cas la résistance est diminuée à 220 ohms.
La consommation n’étant que de 1,5mA, l’autonomie est acceptable.
programme de test d’un émetteur embarqué
Les détecteurs le long de la voie
Les détecteurs sont basés sur un Arduino Pro Mini. L’Arduino Pro Mini permet d’avoir un accès direct aux port série sans être gêné par l’interface USB.
Ils renvoient leurs information via un genre de bus de rétrosignalisation vers une station maître, basée sur un Arduino Mega. L’arduino Mega permet d’avoir plusieurs ports série.
Pour les tests, le bus et son protocole utilisés sont une version très simplifiée (il n’y a par exemple pas de correction de collision) d’un bus série que nous utilisons depuis longtemps, et qui a l’avantage de ne nécessiter aucune électronique ni aucun protocole.
Pour une utilisation plus sérieuse on peut adopter un des bus classiques en modélisme ferroviaire, ce n’est pas ce qui manque.
Exemples d’applications
- annonce d’entrée en gare d’un train (sans action sur le train)
- déclenchement de l’avertisseur ou du sifflet lors du passage devant la pancarte S (action sur le train)
- allumage de l’éclairage à l’entrée d’un tunnel, extinction à la sortie (action sur le train)
- obéissance aux signaux, bloc-système simplifié (action sur le train)
- exploitation plus avancée d’un réseau (action sur le train).
Ces exemples sont détaillés sur la page suivante, dans le cadre de la « solution 2« .
installation de test simplifiée
On peut tester très simplement les fonctions principales du système (identification des trains et commande de la centrale) de la façon suivante :
Toutes les fonctions sont réalisées par un NodeMCU: détection des émetteurs, filtrage du signal, identification du train, décision de l’action à accomplir en fonction du train (ici, avertisseur sonore), et envoi de la commande appropriée à la centrale.
Le module constitue donc une « balise intelligente ».
Pour simplifier le câblage nous utilisons une centrale compatible Z21.
On peut de plus se connecter au module depuis un PC ou un smartphone pour paramétrer l’application en utilisant un client UDP (Packet Sender, UDP Terminal, etc).
Pour les tests préliminaires, le NodeMCU est logé dans une boîte en forme de bâtiment technique posé le long de la voie. De la sorte on n’a même pas à faire de trou entre les traverses.
La boîte contient un NodeMCU encore sur son « breadboad », une batterie Li-Ion comme alimentation, un interrupteur et le détecteur infrarouge.
Ce dernier est monté de façon à être réglable en hauteur.
Rappelons que ceci n’est qu’une installation de test, et que diode et détecteur sont prévus pour être installés entre les rails.
program Lotir
‘ 12F675 à 4MHz
‘ émet un octet à 19200bps
‘ un octet: 0,52ms
‘ x8 pour faire comme Milou: 4ms
‘===================================================================== SYMBOLES
symbol PinTxJPM = GPIO.4 ‘ pour SoftTransmitJPM
‘==================================================================== VARIABLES
‘à 9600bps le temps bit theorique est de 104 µs
’97 est le milieu de la fourchette d’acceptation par le PC: 92-102
‘const TempsBit as longword = 86 ‘pour 9600bps
‘à 19200bps le temps bit theorique est de 52 µs
const TempsBit as longword = 36 ‘pour 19200bps
‘=================================================================== PROCEDURES
sub procedure SoftTransmitJPM (dim X as byte) ‘************** soft transmit JPM
‘moins encombrant en ROM que le soft_transmit de MikroElectronika
‘envoi du caractère X sur le port SoftTx, à 9600bps.
dim N as byte
ClearBit(INTCON, GIE)
PinTxJPM = 0 ‘bit de start
delay_us(TempsBit) ‘à régler et tester
for N = 0 to 7 ‘envoi des 8 bits de l’octet, LSB en premier
PinTxJPM = (X and 1)
delay_us(TempsBit)
X = X >> 1
next N
‘2 bits de stop
PinTxJPM = 1
delay_us(TempsBit)
delay_us(TempsBit)
SetBit(INTCON,GIE)
end sub
main: ‘=================================================================== MAIN
CMCON = 7
OSCCAL = 36 ‘calibration horloge
TRISIO = %11101110
‘Soft_UART_Init(GPIO, 1, 0, 19200, 0) ‘ port, Rx, Tx, baud, inverted ‘KO
‘============================================================ boucle permanente
while true
» test
‘ SoftTransmitJPM(0)
‘ SoftTransmitJPM(%01010101) ’85 ‘0x55
‘ SoftTransmitJPM(%10101010) ‘170 0xAA
‘ SoftTransmitJPM(255)
‘ Delay_ms(1)
‘normal
SoftTransmitJPM(71)
Delay_ms(4)
wend
end.
// ================ programme Lotir-demo (balise intelligente) ======================
// Ce NodeMCU/ESP-12, installé à poste fixe, reçoit les signaux des émetteurs embarqués via le port série,
// les traite, puis envoie les commandes appropriées à la centrale DCC (par exemple: siffler).
// Il fonctionne en mode station vis-à-vis de la centrale DCC.
// Pour les tesst il fonctionne en mode station vis à vis d’un PC.
// Pour le paramétrage il fonctionne en mode point d’accès pour les smartphones et tablettes.
// ==================================================================================
#include <ESP8266WiFi.h>
#include <WiFiUDP.h>
#include <EEPROM.h>
// informations remontées au PC pour affichage
#define sp Serial.print
#define spl Serial.println
// personnalisation du module
#define EEPROM_SIZE 256
int EEPROMcentraleAddress = 0; // n° de la centrale DCC
byte centrale; // n° de la centrale DCC
int EEPROMtableAddress = 1; //table d’action
const byte LED = LED_BUILTIN; // NodeMCU
//const byte LED = 16; // NodeMCU
//const byte LED = 2; // LED externe ESP-01 (impossible de commander la LED interne)
byte AdresseLocale; // …c’est l’adresse de l’engin moteur associé
char NbEssais; // vérification de la connexion toutes les X ms
char reseau = 0; // réseau auquel on est connecté
// commandes reçues du PC en UDP
int NbMots;
String Mots[8];
// tables d’action: le numéro du module émetteur IR étant dificile à modifier, la table indique
// pour le n° du module qui passe, l’adresse DCC associée, fonction à activer, durée avant reset, etc
unsigned int tableAction[5][4];
// gestion des octets reçus sur le port série
byte DernierOctet, compte, precedente;
unsigned long previousMillis = 0;
const long interval = 1000;
bool EnableTransmission = true;
// remise à zéro de la fonction activée
byte derniereAction;
unsigned long previousMillis2 = 0;
unsigned long interval2 = 2000;
bool EnableResetAction = false;
// transmission des paquets UDP vers la centrale ou le PC pour tests
char PaquetUDP[16]; // paquets transmis vers la centrale
// mode point d’accès pour les détecteurs
#define AP_SSID « lotir »
#define AP_PASS « 12345678 »
// mode station vers le PC + Packet Sender pour test
#define WIFI_SSID2 « Livebox-9052 »
#define WIFI_PASS2 « 00112233445566778899AABBCC »
// #define IP_CENTRALE {192,168,1,10}
// pour récupération des paquets UDP
const uint16_t PORT = 50000; // Port d’écoute UDP
const uint16_t BUFFER_SIZE = 64; // Taille du tampon de réception
char buffer[BUFFER_SIZE]; // Tampon de réception
uint16_t len = 0;// Taille du paquet reçu;
// pour envoi des paquetsUDP
char MessageUDP[32];
IPAddress IP_CENTRALE;
// instanciation du serveur UDP
WiFiUDP udp;
void setup() { // =================================================================================== setup
pinMode (LED, OUTPUT);
digitalWrite (LED, HIGH); // Led connectée au plus
Serial.begin(19200);
spl();
EEPROM.begin(EEPROM_SIZE);
EEPROM.get (0, centrale);
EEPROM.get (1, tableAction);
// —————————————————————————– démarrage du point d’accès
WiFi.mode(WIFI_AP_STA);
IPAddress local_IP(192,168,4,1); // adresse IP du point d’accès
IPAddress gateway(192,168,4,1);
IPAddress subnet(255,255,255,0);
WiFi.softAPConfig(local_IP, gateway, subnet); // fixe l’adresse IP du point d’accès
//WiFi.softAPConfig({192,168,4,1},{192,168,4,1},{255,255,255,0}); // ou directement
WiFi.softAP(AP_SSID, AP_PASS); // démarrage du point d’accès
sp(« adresse IP sur réseau « ); sp(AP_SSID); sp( » : « ); spl(WiFi.softAPIP());
// ————————————————— connexion en mode station à la centrale DCC choisie
if (centrale == 1) {connexionCentrale(« DR5000-D0007142″, »12345678 »,{192,168,16,254});} // réseau HO d’en haut
if (centrale == 2) {connexionCentrale(« DR5000-3717″, »12345678 »,{192,168,16,254});} // réseau N
if (centrale == 3) {connexionCentrale(« MiniDCC », »amfn2019″,{192,168,16,254});} // réseau HO d’en bas
if (centrale == 4) {connexionCentrale(« MiniDCC », »amfn2019″,{192,168,1,1});} // réseau personnel
// —————————————– si pas de connexion à la centrale, connexion au réseau local
if(WiFi.status() != WL_CONNECTED){ // pas connecté:
spl(« pas de connexion à la centrale »);
sp(« tentative de connection à « ); spl(WIFI_SSID2); delay(100);
// pour simplifier, utilisation de la même adresse MAC (sinon bloquage par la box)
const uint8_t mac[6] = {0x2C, 0x3A, 0xE8, 0x26, 0xA3, 0x54}; // « 2c:3a:e8:26:a3:54 »
// changement de l’adresse MAC cf: https://circuits4you.com/2017/12/31/how-to-change-esp8266-mac-address/
sp(« ancienne adresse MAC: « ); sp(WiFi.macAddress()); // adresse MAC en cours
wifi_set_macaddr(0, const_cast<uint8*>(mac)); //changement MAC address pour Livebox
sp( » nouvelle adresse MAC: « ); spl(WiFi.macAddress());
// tentative de connexion au réseau local
WiFi.begin(WIFI_SSID2, WIFI_PASS2);
NbEssais = 20;
while (WiFi.status() != WL_CONNECTED && NbEssais > 0 ){ delay(500); sp(« . »); NbEssais–;}
if (WiFi.status() == WL_CONNECTED){
reseau = 2;
spl(« »); sp(« connecté à « ); sp(WIFI_SSID2); // <<<<<<<<<< prendre la vraie valeur
sp( » avec l’ip « ); spl(WiFi.localIP());
delay(100);
// cligner 10 fois
for (char I=1; I<=10; I++){digitalWrite(LED,LOW); delay(200); digitalWrite(LED,HIGH); delay(200);}
}
else {sp( » aucune connection! »);} // problème!
}
// —————————————————– démarrer l’écoute PC/smartphone pour le paramétrage
udp.begin(50000); // démarrer l’écoute sur port 50000
}
// ============================================================================== connexion à une centrale
void connexionCentrale(char ssid[20], char pass[20], IPAddress IPcentrale) {
sp(« tentative de connection à « ); spl(ssid); // tentative de connexion à la centrale
WiFi.begin(ssid, pass);
NbEssais = 10;
while (WiFi.status() != WL_CONNECTED && NbEssais > 0 ){ delay(500); sp(« . »); NbEssais–;}
if(WiFi.status() == WL_CONNECTED){ // si connecté:
reseau = 1; // réseau de la centrale
spl(« »); sp(« connecté à « ); sp(ssid); // <<<<<<<<<< prendre la vraie valeur
sp( » avec l’ip « ); spl(WiFi.localIP());
IP_CENTRALE = IPcentrale; // pour envoi
delay(100);
// cligner 5 fois
for (char I=1; I<=5; I++){digitalWrite(LED,LOW); delay(200); digitalWrite(LED,HIGH); delay(200);}
}
}
void loop() { // =================================================================================== loop
if (Serial.available() > 0) { ReceptionSerie(); } // réception des octets IR sur port série
if (udp.parsePacket() > 0) { readPacket(); } // réception des paquets UDP pour test et paramétrage
// timer de blocage des re-transmissions
if (millis() – previousMillis >= interval) { // ré-autoriser la lecture après 1s
EnableTransmission = true;
digitalWrite (LED, HIGH); // extinction mouchard
}
// timer de remise à zéro de la fonction activée (avertisseur)
if (millis() – previousMillis2 >= interval2) { // envoyer après 2s
if( EnableResetAction == true) {
CommandeFonction(tableAction[derniereAction][1], tableAction[derniereAction][2], false); // commande loco, fonction, off
EnableResetAction = false;
}
}
}
// ===================================================================================== sous programmes
void ReceptionSerie(){ // ——————————– réception des octets un par un sur port série
byte OctetBalise, vidage;
OctetBalise = Serial.read(); //réception d’un caractère
// attente de recevoir 3 octets identiques à la suite
if (OctetBalise != DernierOctet) { // nouvel octet
DernierOctet = OctetBalise;
compte = 1;
}
else { // même octet
compte++;
if ( compte == 3 ) { // transmission si N octets successifs identiques
// mais ne transmettre le résultat qu’une seule fois par seconde (train arrêté sur balise)
// sauf si c’est une autre balise (balises rapprochées)
if (EnableTransmission || (OctetBalise != precedente)) { // transmission
EnableTransmission = false;
digitalWrite (LED, LOW); // allumage LED pour test
precedente = OctetBalise; // mémorisation de la balise
Action(OctetBalise); // déclenchement de l’action programmée
}
else { while(Serial.available () > 0){vidage = Serial.read();} } //vidage du buffer de réception
previousMillis = millis(); // relancement tempo
DernierOctet = 255; // réinitialisation
}
}
}
// ———————————————————- déclenchement de l’action définie dans la table
void Action(char module) {
unsigned int tempo;
for(int i=0; i<=5; i++) { // lecture de la table
if (module == tableAction[i][0]) { // module qu’on vient de voir passer
//sp (« detection module « ); spl(module,DEC);
tempo = tableAction[i][3]; // 0 = off, 1 = on, autre = reset après x ms
if (tempo == 0) {
CommandeFonction(word(tableAction[i][1]), tableAction[i][2], false); // loco, fonction, off
}
if (tempo == 1) {
CommandeFonction(word(tableAction[i][1]), tableAction[i][2], true); // loco, fonction, on
}
if (tempo > 1) {
CommandeFonction(word(tableAction[i][1]), tableAction[i][2], true); // loco, fonction, off
// remise à zéro après timer
derniereAction = i;
interval2 = tempo;
previousMillis2 = millis();
EnableResetAction = true;
}
break; // au cas où la table serait redondante
}
}
}
// ——————————————————————— réception des commandes reçues du PC
void TraitementParametres(){
char reponse[40];
unsigned int Ligne, Module, Adresse, Fonction, Tempo; // ligne, module, adresse, fonction, tempo
if (Mots[0] == « ? ») { // ——————————————— commande ?
repondre (« commandes: q, s, i, t, c »);
repondre (« interrogation: q ligne »);
repondre (« prog: s ligne mod. #dcc fonc. tempo »);
repondre (« (re)init: i »);
repondre (« test: t ligne »);
repondre (« centrale: c ?|centrale »);
}
if (Mots[0] == « q ») { // —————————————————– query ligne
Ligne = Mots[1].toInt();
Module = tableAction [Ligne][0];
Adresse = tableAction [Ligne][1];
Fonction = tableAction [Ligne][2];
Tempo = tableAction [Ligne][3];
sprintf(reponse, « lig=%u, mod=%u, adr=%u, fonc=%u, tempo=%u », Ligne, Module, Adresse, Fonction, Tempo);
//spl(reponse);
repondre (reponse);
}
if (Mots[0] == « s ») { // —————————————————– set action
Ligne = Mots[1].toInt();
Module = Mots[2].toInt();
Adresse = Mots[3].toInt();
Fonction = Mots[4].toInt();
Tempo = Mots[5].toInt();
tableAction[Ligne][0] = Module;
tableAction[Ligne][1] = Adresse;
tableAction[Ligne][2] = Fonction;
tableAction[Ligne][3] = Tempo;
EEPROM.put (EEPROMtableAddress, tableAction);
EEPROM.commit();
repondre (« ok »);
}
if (Mots[0] == « i ») { // ————————————————— init table
unsigned int Temp[5][4] = {
{71, 67, 3, 2000}, // ligne 0: Jouef
{71, 80, 2, 2000 }, // ligne 1: Roco
{71, 72, 4, 2000}, // ligne 2: Bachmann
{3, 3, 2, 1000}, // ligne 3: décodeur JPM2. n° module = adresse DCC
{6, 7, 8, 1000} // ligne 4: dispo
};
EEPROM.put (EEPROMtableAddress, Temp);
EEPROM.commit();
EEPROM.get (EEPROMtableAddress, tableAction);
repondre (« ok »);
}
if (Mots[0] == « t ») { // ———————————————————- test
Ligne = Mots[1].toInt();
CommandeFonction(tableAction[Ligne][1], tableAction[Ligne][2], true); // commande loco, fonction, on
// remise à zéro après timer
derniereAction = Ligne;
previousMillis2 = millis();
EnableResetAction = true;
}
if (Mots[0] == « c ») { // ———————————————————- centrale
if (Mots[1] == « ? ») {
//sprintf(reponse, « c=%d, ssid=%s », centrale, WiFi.SSID());
sprintf(reponse, « c=%d », centrale);
repondre(reponse);
}
else {
centrale = Mots[1].toInt();
EEPROM.put (EEPROMcentraleAddress, centrale);
EEPROM.commit();
// EEPROM.get (EEPROMcentraleAddress, tableAction);
repondre (« ok, redemarrer »);
}
}
}
// —————————————————————————————— commandes DCC
void CommandeFonction(word loco, char fonction, bool etat){
char CheckSum;
MessageUDP[0] = 10; // longueur LSB = 0x0A
MessageUDP[1] = 0x00; // longueur MSB
MessageUDP[2] = 0x40; // header LSB(0x40)
MessageUDP[3] = 0x00; // header MSB
MessageUDP[4] = 0xE4; CheckSum = 0xE4; // X-header
MessageUDP[5] = 0xF8; CheckSum = CheckSum ^ MessageUDP[5]; // commande de fonction
MessageUDP[6] = loco >> 8; CheckSum = CheckSum ^ MessageUDP[6]; // adresse DCC MSB
MessageUDP[7] = loco & 127; CheckSum = CheckSum ^ MessageUDP[7]; // adresse DCC LSB
// DB3 = TTNNNNNN où TT = off/on/toggle/n.a. NNNNNN = fonction
MessageUDP[8] = fonction; // 00NNNNNNN
if (etat != 0) {MessageUDP[8] = MessageUDP[8] + 64; } // toggle traité comme on
CheckSum = CheckSum ^ MessageUDP[8];
//ajout de la checksum
MessageUDP[9] = CheckSum;
if (reseau == 2) {sendPacketL(MessageUDP, 10, udp.remoteIP(),{50000});} // envoi au PC / Packet Sender
else {sendPacketL(MessageUDP, 10, IP_CENTRALE,{21105});} // envoi à la centrale
}
/*
void CommandeVitesse(word loco, char sens, char cran) {
char CheckSum;
MessageUDP[0] = 10; // longueur LSB = 0x0A
MessageUDP[1] = 0x00; // longueur MSB
MessageUDP[2] = 0x40; // header LSB(0x40)
MessageUDP[3] = 0x00; // header MSB
MessageUDP[4] = 0xE4; CheckSum = 0xE4; // X-header
MessageUDP[5] = 0x13; CheckSum = CheckSum ^ MessageUDP[5]; // 128 steps
MessageUDP[6] = loco >> 8; CheckSum = CheckSum ^ MessageUDP[6]; // adresse DCC MSB
MessageUDP[7] = loco & 127; CheckSum = CheckSum ^ MessageUDP[7]; // adresse DCC LSB
// DB3 = Cran, If SensAvant Then DB3 = DB3 + 128, Arrêt urgent non traité
MessageUDP[8] = cran + 128 * sens ; CheckSum = CheckSum ^ MessageUDP[8];
//ajout de la checksum
MessageUDP[9] = CheckSum;
//sendPacketL(MessageUDP, 10, {192,168,1,10},{50000}); // envoi au PC / Packet Sender
//sendPacketL(MessageUDP, 10, {192,168,1,1},{21105}); // envoi à la centrale
sendPacketL(MessageUDP, 10, IP_CENTRALE,{21105}); // envoi à la centrale
}
*/
// ================================================================= récupération et affichage d’un paquet UDP
void readPacket() {
len = udp.available();
udp.read(buffer, len); // Mise en tampon du paquet
//AffichageMessage();
ReceptionCommandesPC(); //TraitementParametres();
// tous les paquets arrivent sur le même port 50000
//if (udp.remoteIP()[3] == 10){TraitementParametres();} // paquets en provenance du PC (192.168.1.10)
//else{TraitementMessage();} // paquets en provenance des Milous
}
// ==================================================================================== envoi d’un paquet UDP
void sendPacketL(const char content[], char longueur, IPAddress ip, uint16_t port) { // avec longueur
udp.beginPacket(ip, port);
udp.write(content, longueur);
udp.endPacket();
}
void sendPacketZ(char content[], IPAddress ip, uint16_t port) { // avec zéro à la fin
udp.beginPacket(ip, port);
udp.write(content);
udp.endPacket();
}
void repondre(char texte[]) {
sendPacketZ(texte, udp.remoteIP(), 50000);
delay(100);
}
// ============================================================================ traitement des messages Milou
void TraitementMessage(){
//AffichageMessage(); // pour test
// le numéro du train peut être calculé d’après l’adresse IP du détecteur, ou contenue dans le message
sp(« loco « ); sp(buffer[0],DEC); sp ( » balise « ); spl (buffer[1],DEC); // d’après le message
}
// ============================================================================== traitement des messages PC
void ReceptionCommandesPC() { //TraitementParametres(){
// AffichageMessage(); // pour test
for(int i=0; i<len; i++) {
if(buffer[i]==32) { // espace -> nouveau mot
if ( NbMots < 8) {++NbMots;} // sécurité
}
else {Mots[NbMots] = Mots[NbMots] + buffer[i];} // autre caractère -> concaténation
}
// traitement du message
//for(int i=0; i<=NbMots; i++){spl(Mots[i]);} // affichage
TraitementParametres(); //ReceptionCommandesPC(); // traitement de la commande
// reset
Mots[0]= » »; Mots[1]= » »; Mots[2]= » »; Mots[3]= » »; Mots[4]= » »;
Mots[5]= » »; Mots[6]= » »; Mots[7]= » »; NbMots = 0;
}
// ================================================================================== affichage d’un message
void AffichageMessage() {
sp(« reçu « ); sp(len); sp( » octets de « ); sp(udp.remoteIP());
sp(« : »); sp(udp.remotePort());
sp( » soit: « );
for(int i=1; i<=len; i++) {PrtHex(buffer[i-1]); sp( » « );} spl(« »);
}
// ======================================================================= affichage en hexadécimal amélioré
void PrtHex(char X) {
if (X<16){sp(« 0 »);} sp(X,HEX);
}
0 commentaires