You are currently viewing Programmation Arduino : Code complet pour TCO et dispatcher ferroviaire en DCC

Programmation Arduino : Code complet pour TCO et dispatcher ferroviaire en DCC

  • Auteur/autrice de la publication :
  • Post category:Arduino / DCC
  • Commentaires de la publication :0 commentaire

Introduction

Cet article fait référence à la présentation de mon second poste d’aiguillage où il est question d’intégrer un TCO opérationnel dans un poste d’aiguillage à l’échelle N : Modélisation et impression 3D du poste d’aiguillage n°3 d’Ambérieu-en-Bugey

Le TCO est chargé d’afficher l’état des aiguillages et des signaux lumineux sur un petit écran (ici, un écran 4D Systems IoD-09TH connecté à un ESP8266).

Sur cet écran, les éléments du réseau sont représentés graphiquement, avec des couleurs qui indiquent leur état :

  • Les aiguillages changent de couleur selon leur position (directe ou déviée),
  • Les signaux passent au rouge, jaune ou vert selon les ordres reçus.

Principe de fonctionnement

Le TCO

Le TCO se connecte en WiFi sur le point d’accès offert par le dispatcher et demande régulièrement à celui-ci l’état des équipements le concernant via une requête HTTP. Il reçoit alors l’état de ses équipements au format JSON. Si l’état d’aiguilles ou de signaux a changé, le TCO modifie l’affichage de l’équipement à l’écran.

Le dispatcher

Le dispatcher, quant à lui, écoute les ordres DCC, provenant de la centrale DCC et les stocke en mémoire RAM. Il fournit un point d’accès WiFi aux TCO et sur leur demande, via HTTP, il leur envoie l’état de leurs équipements.

Il agit donc comme une passerelle entre le réseau DCC et les équipements connectés :

Ressources utiles

Vous avez une questions ou une remarque concernant les programmes TCO et dispatcher ? Si vous souhaitez échanger autour de ces programmes, je vous recommande rejoindre le forum Passionnément.

Si vous êtes débutant en programmation Arduino, je vous conseille de suivre au préalable les excellents tutos réalisés par Patrick de la chaine YouTube « Le petit train du Berry » :

Historique des versions

2023-03-14
Version initiale.
Dans cette version, le dispatcher envoie chaque commande DCC reçue au TCO concerné.

2025-07-22
Première version publiée à l’occasion de la réalisation du second poste d’aiguillage.

2025-08-19

  • Le dispatcher ne transmet plus les commandes DCC au TCO au fil de l’eau (en push).
  • Les TCO font du polling pour récupérer l’état de leurs équipements.
  • L’écran ne se réinitialise pas en cas de coupure de la connexion WiFi.
  • Le dispatcher enregistre l’état des équipements dans sa mémoire flash, sur réception d’une commande DCC, envoyée par le logiciel de gestion des trains, à la fermeture de celui-ci et arrêt de l’ensemble du réseau.
  • Animation de l’affichage des équipements dont l’état vient de changer.

Code source du TCO

But du programme

Ce code tourne sur un microcontrôleur ESP8266 équipé d’un écran 4D Systems IoD-09TH, pour afficher un Tableau de Commande Optique (TCO). Le TCO représente des aiguillages et signaux ferroviaires, dont l’état est mis à jour en consultant le dispatcher via des requêtes HTTP (via WiFi).

Pour mettre en œuvre l’environnement Arduino pour le IoD-09TH, veuillez consulter le site du fabriquant :

Accès à la documentation et aux ressources fournies par 4D Systems concernant le Iod-09TH

/**
 * TCO graphique avec polling régulier du dispatcher
 * Auteur : Marc (gare-alesia.fr)
 * Version 2025-08-19
 */

#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>
#include <ArduinoJson.h>
#include "GFX4dIoD9.h"

const char* ssid = "DCC-DISPATCHER";
const char* password = "mot de passe super secret";

GFX4dIoD9 gfx;

constexpr int POLLING_INTERVAL_MS = 1000;
unsigned long dernierPolling = 0;

constexpr int CLIGNOTEMENTS = 3;
constexpr int CLIGNO_INTERVAL = 150;

constexpr int ECRAN_LARGEUR = 160;
constexpr int ENTRAXE = 12;
constexpr int HAUTEUR_AIG = 6;
constexpr int LONGUEUR_AIG = 12;

constexpr int COULEUR_VOIE = WHITE;
constexpr int COULEUR_ACTIF = CHARTREUSE;
constexpr int COULEUR_INACTIF = DARKRED;
constexpr int COULEUR_VERT = CHARTREUSE;
constexpr int COULEUR_JAUNE = YELLOW;
constexpr int COULEUR_ROUGE = RED;
constexpr int COULEUR_NEUTRE = LIGHTGREY;

int VOIE6 = 0 * ENTRAXE;
int VOIE5 = 1 * ENTRAXE;
int VOIE1 = 2 * ENTRAXE;
int VOIE2 = 3 * ENTRAXE;
int VOIE3 = 4 * ENTRAXE;
int VOIE4 = 5 * ENTRAXE;

int X1 = 36;
int X2 = X1 + 2 * LONGUEUR_AIG;
int X3 = X2 + 2 * LONGUEUR_AIG;
int X4 = X3 + 2 * LONGUEUR_AIG;
int X5 = X4 + 2 * LONGUEUR_AIG;

int XS1 = X1 / 2;
int XS2 = X4 - 6;
int XS3 = ECRAN_LARGEUR - XS1;

struct Animation {
  int adresse;
  unsigned long lastToggle = 0;
  int count = 0;
  bool visible = true;
};

Animation animations[20];
int nbAnimations = 0;

bool wifiOK = false;
bool pollingOK = false;
unsigned long dernierAffichageActivite = 0;
bool etatAffichageActivite = false;

void afficherActivite() {
  uint16_t couleur = BLACK;
  if (!wifiOK) couleur = ORANGE;
  else if (!pollingOK) couleur = MAGENTA;

  if (millis() - dernierAffichageActivite >= 300) {
    dernierAffichageActivite = millis();
    etatAffichageActivite = !etatAffichageActivite;
    if (etatAffichageActivite) gfx.CircleFilled(4, 4, 3, couleur);
    else gfx.CircleFilled(4, 4, 3, BLACK);
  }
}

void voie(int x, int y, int l, int y2 = -1) {
  if (y2 >= 0) gfx.Line(x, y, l, y2, COULEUR_VOIE);
  else gfx.Line(x, y, x + l, y, COULEUR_VOIE);
}

void tracerVoies() {
  voie(X5, VOIE6, ECRAN_LARGEUR);
  voie(X4, VOIE5, ECRAN_LARGEUR);
  voie(X3, VOIE1, ECRAN_LARGEUR);
  voie(0, VOIE2, ECRAN_LARGEUR);
  voie(0, VOIE3, ECRAN_LARGEUR);
  voie(0, VOIE4, ECRAN_LARGEUR);
  voie(X1, VOIE3, X5, VOIE6);
}

struct Aiguillage {
  int adresse, x, y, inverser;
  char type;
  String etat = "";

  Aiguillage(int adresse, int x, int y, char type, int inverser)
    : adresse(adresse), x(x), y(y), type(type), inverser(inverser) {}

  void afficher(bool visible = true) {
    int cDirect = visible ? ((etat == "DIRECT") ? COULEUR_ACTIF : COULEUR_INACTIF) : BLACK;
    int cDevie  = visible ? ((etat == "DEVIE") ? COULEUR_ACTIF : COULEUR_INACTIF) : BLACK;
    if (type == 'D') {
      if (inverser) {
        gfx.Line(x, y, x - LONGUEUR_AIG, y - HAUTEUR_AIG, cDevie);
        gfx.Line(x, y, x - LONGUEUR_AIG, y, cDirect);
      } else {
        gfx.Line(x, y, x + LONGUEUR_AIG, y, cDirect);
        gfx.Line(x, y, x + LONGUEUR_AIG, y + HAUTEUR_AIG, cDevie);
      }
    } else {
      if (inverser) {
        gfx.Line(x, y, x - LONGUEUR_AIG, y, cDirect);
        gfx.Line(x, y, x - LONGUEUR_AIG, y + HAUTEUR_AIG, cDevie);
      } else {
        gfx.Line(x, y, x + LONGUEUR_AIG, y, cDirect);
        gfx.Line(x, y, x + LONGUEUR_AIG, y - HAUTEUR_AIG, cDevie);
      }
    }
  }
};

struct Signal {
  int adresse, x, y, inverser;
  String etat = "ROUGE";

  Signal(int adresse, int x, int y, int inverser)
    : adresse(adresse), x(x), y(y), inverser(inverser) {}

  void afficher(bool visible = true) {
    uint color = COULEUR_NEUTRE;
    if (etat == "VERT") color = COULEUR_VERT;
    else if (etat == "JAUNE") color = COULEUR_JAUNE;
    else if (etat == "ROUGE") color = COULEUR_ROUGE;

    if (!visible) {
      color = BLACK; // ou COULEUR_NEUTRE si on préfère
    }
    int dir = inverser ? -1 : 1;
    gfx.Circle(x - 5 * dir, y - 6 * dir, 4, COULEUR_NEUTRE);
    gfx.Circle(x + 5 * dir, y - 6 * dir, 4, COULEUR_NEUTRE);
    gfx.RectangleFilled(x - 5 * dir, y - 2 * dir, x + 5 * dir, y - 10 * dir, BLACK);
    gfx.Line(x - 5 * dir, y - 2 * dir, x + 5 * dir, y - 2 * dir, COULEUR_NEUTRE);
    gfx.Line(x - 5 * dir, y - 10 * dir, x + 5 * dir, y - 10 * dir, COULEUR_NEUTRE);
    gfx.Line(x - 9 * dir, y - 6 * dir, x - 15 * dir, y - 6 * dir, COULEUR_NEUTRE);
    gfx.Line(x - 15 * dir, y, x - 15 * dir, y - 6 * dir, COULEUR_NEUTRE);
    gfx.CircleFilled(x, y - 6 * dir, 2, color);
  }
};

Aiguillage* aiguillages[9];
Signal* signaux[9];

void ajouterAnimation(int adresse) {
  for (int i = 0; i < nbAnimations; ++i) {
    if (animations[i].adresse == adresse) return;
  }
  if (nbAnimations < 20) {
    animations[nbAnimations++] = {adresse, millis(), 0, true};
  }
}

void mettreAJourAnimations() {
  unsigned long maintenant = millis();
  for (int i = 0; i < nbAnimations; ++i) {
    if (maintenant - animations[i].lastToggle >= CLIGNO_INTERVAL) {
      animations[i].lastToggle = maintenant;
      animations[i].visible = !animations[i].visible;
      animations[i].count++;

      int adresse = animations[i].adresse;
      for (auto aig : aiguillages) {
        if (aig && aig->adresse == adresse) aig->afficher(animations[i].visible);
      }
      for (auto sig : signaux) {
        if (sig && sig->adresse == adresse) sig->afficher(animations[i].visible);
      }

      yield();

      if (animations[i].count >= CLIGNOTEMENTS * 2) {
        for (int j = i; j < nbAnimations - 1; ++j)
          animations[j] = animations[j + 1];
        nbAnimations--;
        i--;
      }
    }
  }
}

void setup() {
  Serial.begin(38400);
  gfx.begin();
  gfx.Cls();
  gfx.Orientation(LANDSCAPE);
  gfx.TextWindow(0, 0, 162, 90, ORANGE, BLACK);
  gfx.Font(1);
  gfx.TextSize(1);

  WiFi.mode(WIFI_STA);
  IPAddress local_IP(192, 168, 4, 3);
  IPAddress gateway(192, 168, 4, 1);
  IPAddress subnet(255, 255, 255, 0);
  WiFi.config(local_IP, gateway, subnet);
  WiFi.begin(ssid, password);
  WiFi.setAutoReconnect(true);
  WiFi.persistent(true);

  tracerVoies();

  int i = 0;
  aiguillages[i++] = new Aiguillage(176, X1, VOIE2, 'D', 0);
  aiguillages[i++] = new Aiguillage(177, X2, VOIE2, 'G', 1);
  aiguillages[i++] = new Aiguillage(178, X2, VOIE3, 'D', 1);
  aiguillages[i++] = new Aiguillage(179, X1, VOIE3, 'G', 0);
  aiguillages[i++] = new Aiguillage(184, X2, VOIE2, 'G', 0);
  aiguillages[i++] = new Aiguillage(185, X2, VOIE3, 'D', 0);
  aiguillages[i++] = new Aiguillage(186, X3, VOIE4, 'D', 1);
  aiguillages[i++] = new Aiguillage(187, X3, VOIE1, 'G', 0);
  aiguillages[i++] = new Aiguillage(171, X4, VOIE5, 'G', 0);

  i = 0;
  signaux[i++] = new Signal(1516, XS2, VOIE1, 1);
  signaux[i++] = new Signal(1518, XS2, VOIE2, 1);
  signaux[i++] = new Signal(1520, XS2, VOIE3, 1);
  signaux[i++] = new Signal(2016, XS2, VOIE4, 1);
  signaux[i++] = new Signal(1524, XS3, VOIE5, 1);
  signaux[i++] = new Signal(1526, XS3, VOIE6, 1);
  signaux[i++] = new Signal(1528, XS1, VOIE2, 0);
  signaux[i++] = new Signal(1530, XS1, VOIE3, 0);
  signaux[i++] = new Signal(1532, XS1, VOIE4, 0);

  for (auto aig : aiguillages) if (aig) aig->afficher();
  for (auto sig : signaux) if (sig) sig->afficher();
}

void loop() {
  wifiOK = (WiFi.status() == WL_CONNECTED);

  if (wifiOK && millis() - dernierPolling > POLLING_INTERVAL_MS) {
    dernierPolling = millis();
    pollingOK = false;
    WiFiClient client;
    HTTPClient http;
    if (http.begin(client, "http://192.168.4.1/etat/poste2")) {
      int code = http.GET();
      if (code == 200) {
        pollingOK = true;
        String data = http.getString();
        StaticJsonDocument<1024> doc;
        DeserializationError err = deserializeJson(doc, data);
        if (!err) {
          for (JsonPair kv : doc.as<JsonObject>()) {
            int adresse = atoi(kv.key().c_str());
            String nouvelEtat = kv.value().as<String>();
            for (auto aig : aiguillages) {
              if (aig && aig->adresse == adresse && aig->etat != nouvelEtat) {
                aig->etat = nouvelEtat;
                aig->afficher();
                ajouterAnimation(adresse);
              }
            }
            for (auto sig : signaux) {
              if (sig && sig->adresse == adresse && sig->etat != nouvelEtat) {
                sig->etat = nouvelEtat;
                sig->afficher();
                ajouterAnimation(adresse);
              }
            }
            yield();
          }
        }
      }
      http.end();
    }
  }

  mettreAJourAnimations();
  afficherActivite();
  yield();
}

Code source du dispatcher

La connexion du Dispatcher au signal DCC a été décrite dans un article précédent : Un poste d’aiguillage modélisé et imprimé en 3d avec TCO opérationnel.

/**
 * Dispatcher DCC complet : écoute DCC, communication HTTP, sauvegarde flash
 * Auteur : Marc (gare-alesia.fr)
 * Version 2025-08-19
 */

#include <WiFi.h>
#include <WiFiClient.h>
#include <WebServer.h>
#include <Preferences.h>
#include <HTTPClient.h>
#include <NmraDcc.h>
#include <ArduinoJson.h>

#define DCC_PIN 2
#define SERIAL_SPEED 38400
#define HTTP_TIMEOUT_MS 500
#define ADRESSE_SAUVEGARDE 1999
#define DEBUG_ENABLED false

WebServer server(80);
Preferences prefs;
NmraDcc dcc;
DCC_MSG dccPacket;

unsigned long derniereSauvegarde = 0;
const unsigned long delaiProtectionSauvegarde = 2000;

class Equipement {
public:
  String nom, type, ip, etat;
  int adresse;
  unsigned long modifie = 0;
  const int timeout = HTTP_TIMEOUT_MS;

  Equipement(String nom, String type, int adresse, String ip)
    : nom(nom), type(type), adresse(adresse), ip(ip), etat("") {}

  void enregistrer(int addr, const String& nouvelEtat) {
    if (addr == adresse && nouvelEtat != etat) {
      etat = nouvelEtat;
      modifie = millis();
    }
  }
};

Equipement* equipements[] = {
  new Equipement("poste2", "aiguillage", 171, "192.168.4.3"),
  new Equipement("poste2", "aiguillage", 187, "192.168.4.3"),
  new Equipement("poste2", "aiguillage", 176, "192.168.4.3"),
  new Equipement("poste2", "aiguillage", 177, "192.168.4.3"),
  new Equipement("poste2", "aiguillage", 184, "192.168.4.3"),
  new Equipement("poste2", "aiguillage", 179, "192.168.4.3"),
  new Equipement("poste2", "aiguillage", 178, "192.168.4.3"),
  new Equipement("poste2", "aiguillage", 185, "192.168.4.3"),
  new Equipement("poste2", "aiguillage", 186, "192.168.4.3"),
  new Equipement("poste2", "signal", 1516, "192.168.4.3"),
  new Equipement("poste2", "signal", 1518, "192.168.4.3"),
  new Equipement("poste2", "signal", 1520, "192.168.4.3"),
  new Equipement("poste2", "signal", 2016, "192.168.4.3"),
  new Equipement("poste2", "signal", 1524, "192.168.4.3"),
  new Equipement("poste2", "signal", 1526, "192.168.4.3"),
  new Equipement("poste2", "signal", 1528, "192.168.4.3"),
  new Equipement("poste2", "signal", 1530, "192.168.4.3"),
  new Equipement("poste2", "signal", 1532, "192.168.4.3")
};

void notifyDccAccTurnoutOutput(uint16_t adresse, uint8_t etat, uint8_t outputPower) {
  if (adresse == ADRESSE_SAUVEGARDE) {
    unsigned long maintenant = millis();
    if (maintenant - derniereSauvegarde > delaiProtectionSauvegarde) {
      derniereSauvegarde = maintenant;
      debug("Commande de sauvegarde reçue via DCC");
      prefs.begin("dcc", false);
      for (auto e : equipements) {
        prefs.putString(String(e->adresse).c_str(), e->etat);
      }
      prefs.end();
      debug("États sauvegardés en mémoire flash.");
    }
  } else {
    for (auto e : equipements) {
      if (e->adresse == adresse || (e->type == "signal" && e->adresse + 1 == adresse)) {
        String etatStr;
        if (e->type == "aiguillage") {
          etatStr = (etat > 0) ? "DIRECT" : "DEVIE";
        } else if (e->type == "signal") {
          etatStr = (adresse % 2 == 0) ? ((etat == 0) ? "ROUGE" : "VERT") : "JAUNE";
        }
        e->enregistrer(e->adresse, etatStr);
        return;
      }
    }
  }
}

void chargerEtatsDepuisFlash() {
  prefs.begin("dcc", true);
  for (auto e : equipements) {
    String etat = prefs.getString(String(e->adresse).c_str(), "");
    if (etat != "") e->etat = etat;
  }
  prefs.end();
}

void envoyerEtatsPourPoste() {
  String uri = server.uri();
  String poste = uri.substring(uri.lastIndexOf('/') + 1);
  debug("Requête états JSON pour poste: " + poste);

  StaticJsonDocument<1024> doc;
  for (auto e : equipements) {
    if (e->nom == poste) {
      doc[String(e->adresse)] = e->etat;
    }
  }

  String output;
  serializeJson(doc, output);
  server.send(200, "application/json", output);
}

void debug(const String& message) {
  if (DEBUG_ENABLED) {
    Serial.println(message);
  }
}

void setup() {
  Serial.begin(SERIAL_SPEED);

  WiFi.mode(WIFI_AP);
  WiFi.softAP("DCC-DISPATCHER", "mot de passe super secret");

  chargerEtatsDepuisFlash();

  dcc.pin(DCC_PIN, 0);
  dcc.init(MAN_ID_DIY, 10, CV29_ACCESSORY_DECODER | CV29_OUTPUT_ADDRESS_MODE, 0);

  server.on("/etat/poste1", HTTP_GET, envoyerEtatsPourPoste);
  server.on("/etat/poste2", HTTP_GET, envoyerEtatsPourPoste);
  server.begin();

  debug("Dispatcher prêt. IP: " + WiFi.softAPIP().toString());
}

void loop() {
  dcc.process();
  server.handleClient();
}

Laisser un commentaire

Résoudre : *
32 ⁄ 8 =