LOCODUINO

TCO Web interactif

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

Les itinéraires (le logiciel)

.
Par : utpeca

DIFFICULTÉ :

Foin des délices de Capoue et des images pour ainsi dire "pieuses" de l’article TCO Web interactif avec des ESP32 et des ESP8266 (3) sur les principes des itinéraires dans TCO Web, nous allons maintenant nous colleter avec les divers logiciels à mettre en œuvre en utilisant les outils qui nous sont offerts comme HTML, CSS ou Javascript. Une fois ces outils pris en main puis maîtrisés, nous serons en mesure de concevoir un TCO, avec des itinéraires pouvant être tracés, puis alimentés.

Avertissement : l’article semble très long, mais c’est dû au fait que beaucoup de parties du code sont expliquées pour une meilleure compréhension (et ça prend de la place !).

L’architecture logicielle du TCO-Web.

Comme le montre la figure suivante, la gestion d’itinéraires nécessite trois dispositifs logiciels, qui sont :

  • un module émetteur, qui est un serveur Web affichant le TCO sur un browser et répercutant les diverses commandes à effectuer sur les modules récepteurs ;
  • un ou plusieurs modules récepteurs "Aiguilles" qui permettent de positionner les appareils de voie selon les desiderata de l’Aiguilleur ;
  • un ou plusieurs modules récepteurs "Cantons" qui alimentent les sections de voie concernées.
Figure 1
Figure 1
Organisation fonctionnelle des divers microprocesseurs.

Le module émetteur.

Les principales fonctions du module émetteur sont :

  • Préparation et envoi de la page HTML descriptive du TCO au browser de l’utilisateur ;
  • Récupération d’un éventuel changement de position d’un appareil de voie et envoi de la commande de changement au module récepteur "Aiguilles" ;
  • Traitement de la demande de constitution d’un itinéraire ;
  • Récupération d’une demande d’alimentation d’un itinéraire et envoi de la commande au module récepteur "Cantons" ;
  • Récupération d’une éventuelle demande d’arrêt d’urgence et envoi de la commande au module récepteur "Cantons".

Pour mettre en œuvre les fonctions citées, le logiciel du module émetteur doit pouvoir :

Comme indiqué plus haut, le lecteur doit être familier avec les possibilités offertes par HTML, CSS et Javascript :

  • HTML permet de définir l’entête de l’écran et le TCO ;
  • CSS sert à changer les présentations, couleurs, formes et géométries des vignettes et des boutons ;
  • Javascript gère l’interactivité, c’est-à-dire les actions de l’Aiguilleur.

Pour ceux qui voudraient en acquérir les bases ou se perfectionner, les sites suivants sont d’une grande aide :

Stockage des fichiers Html, CSS et Javascript dans la mémoire Flash.

En plus des vignettes des éléments du TCO, il est souhaitable de stocker dans la mémoire Flash, les fichiers HTML, CSS et Javascript de description du TCO.

Les fichiers index.html , TCOWeb4.css , TCOWeb4.js et MonTCO1.js sont mis dans le sous-répertoire Data du projet Arduino avec les vignettes des éléments du TCO, pour être chargé en mémoire Flash par le plugin ESPxxx Sketch Data Upload. La manipulation à accomplir pour charger la mémoire Flash est décrite dans l’article TCO Web interactif avec des ESP32 et des ESP8266 (2).

La figure suivante montre l’arborescence à mettre en place :

Figure 2
Figure 2
L’arborescence des fichiers du croquis.

SketchTCOWeb4 est une appellation générique pour le répertoire et le croquis du module émetteur. Vous pouvez mettre le nom qui vous convient ou bien garder celui qui est prévu dans le ZIP fourni en fin de chapitre (pour mémoire TCObyUTPECA4a1 ).

MonTCO1.h et MonTCO1.js sont les deux fichiers décrivant le TCO MonTCO1 (appellation générique également). Le ZIP évoqué au paragraphe précédent est fourni avec trois TCO, à savoir Combrailles , Saint-Sernin et MonTCO_No04 .

Le sous-répertoire Data contient tous les fichiers devant être chargés dans la mémoire Flash de l’ESPxxx [2]. Outre MonTCO1.js déjà vu, il y a deux fichiers invariants (quel que soit le TCO) : TCOWeb4.css et TCOWeb4.js . Toutes les vignettes affichables sur un TCO se trouvent également dans ce sous-répertoire.

Le fichier HTML de description du TCO.

Le fichier index.html est défini comme ci-après.

<!DOCTYPE HTML>
<html lang="fr">
<head>
  <title>TCO par UTPECA</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <meta charset="utf-8">
  <link rel="icon" type="image/png" href="Icon1">
  <link rel="stylesheet" type="text/css" href="TCOWeb4.css"> 
</head>

<body background='Pierre' >
<div id="pagehaut" class="phaut">
   <table cellspacing='0' cellpadding='0'>
     <tr>
       <td><img width='110' src='Locoduino'></td>
       <td width='430'><h1><b>%NOM_DU_TCO%</b></h1></td>  
       <td><img width='425' src='Titre1'></td>  
     </tr>
   </table>
   <table><tr><td><img src='Ligne'></td></tr></table>

   <table>
      <tr>
         <td><p id='DESC'> &nbsp;Formez un itin&eacute;raire</p></td>
         <td><img width='60' height='40' src='Pierre60x40' id='pierre'></td>
         <td><canvas id='cnv1' width='60' height='40'>Votre navigateur ne supporte pas canvas</canvas></td>
         <td><h4>Alimenter l'itin&eacute;raire</h4>
            <label class="switch1"><input type="checkbox" onchange="AlimCantons()" id="B1" >
            <span  class="slider1"></span></label></td>
         <td><h4>Arr&ecirc;t d'urgence</h4>
            <label class="switch1"><input type="checkbox" onchange="ArretUrgence()" id="B2" >
            <span  class="slider2"></span></label></td>
      </tr>
   </table>
   <table><tr><td><img src='Ligne'></td></tr></table>
</div>

<div id='corps' class='corps'>

%CONSTRUIRE_TCO%

</table>
<table><tr><td><img src='Ligne'</td></tr></table>
</div>

<script defer src="%NOM_DU_TCO%"></script>
<script defer src="TCOWeb4.js"></script>
</body>
</html>

Il permet de faire référence à la feuille de style TCOWeb4.css , au fichier Javascript %NOM_DU_TCO%.js et au fichier Javascript TCOWeb4.js  ; leur description est donnée dans la suite de l’article.

De plus, à l’exécution du croquis, les variables %NOM_DU_TCO% et %CONSTRUIRE_TCO% seront remplacées par les chaînes de caractères fournies par la fonction processor (voir plus en avant du document).

La feuille de style TCOWeb4.css.

Sur une idée communiquée par un lecteur de Locoduino (trimarco232), une simplification de la rotation des vignettes a été apportée. Qu’il en soit remercié !

Avant, il fallait trois vignettes pour les rotations de 90°, 180° et 270° dans le sens trigonométrique, comme par exemple, pour l’aiguillage triple :

Figure 3
Figure 3
Exemples de vignettes utilisées pour la rotation.

Désormais, la rotation est prise en charge grâce à du code CSS (Cascading Style Sheets, en français : feuilles de style en cascade), qui permet d’effectuer la rotation lors de l’affichage de la vignette par le browser.

Pour de plus amples informations sur le CSS, la consultation du site CSS Tutorial (w3schools.com) s’impose. Le code CSS du fichier TCOWeb4.css est comme suit :

    html   {font-family: Arial; display: inline-block; text-align: left; color: rgb(0, 0, 66);}
    h1     {text-align: center; font-family: Arial Narrow; font-size: 50px; 
            margin-top: 0px;margin-bottom: 0px; padding-bottom: 0px; padding-top: 0px; }
    h2     {font-size: 3.0rem;}
    h4     {font-size: 12px; margin-left: 60px; }
    p      {font-size: 15px; font-weight: bold; width: 400px; margin-left: 30px; 
            margin-top: 0px; margin-bottom: 0px; padding-left: 10px;
            padding-bottom: 0px; padding-top: 0px; border: 2px solid #6B6B94}
    body   {margin: 0px auto; padding-bottom: 0px; padding-top: 0px; }
   .phaut   {width: 100%; overflow: hidden; display: inline; z-index: 0;
             margin-top: 0px;margin-bottom: 0px; padding-bottom: 0px; padding-top: 0px;}
   .corps   {width: 100%; overflow: auto; display: inline; z-index: 0;
             margin-top: 0px;margin-bottom: 0px; padding-bottom: 0px; padding-top: 0px;}
   .cnv1    {z-index: 1; }

   .ang000 {border: 0px; width: 64px; height: 64px;}
   .ang090 {border: 0px; width: 64px; height: 64px;
            -webkit-transform: rotate(-90deg);
            -moz-transform: rotate(-90deg);
            -ms-transform: rotate(-90deg);
            -o-transform: rotate(-90deg);
            transform: rotate(-90deg); }
   .ang180 {border: 0px; width: 64px; height: 64px;
            -webkit-transform: rotate(180deg);
            -moz-transform: rotate(180deg);
            -ms-transform: rotate(180deg);
            -o-transform: rotate(180deg);
            transform: rotate(180deg); }
   .ang270 {border: 0px; width: 64px; height: 64px;
            -webkit-transform: rotate(90deg);
            -moz-transform: rotate(90deg);
            -ms-transform: rotate(90deg);
            -o-transform: rotate(90deg);
            transform: rotate(90deg); }

   .switch1 {position: relative; display: inline-block; width: 120px; height: 40px;  margin-left: 60px;} 
   .switch1 input {display: none; }
   .slider1 {position: absolute; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; 
             border-radius: 6px; cursor:pointer; }
   .slider1:before {position: absolute; content: ""; height: 24px; width: 52px; left: 8px; bottom: 8px;
             background-color: #fff; -webkit-transition: .4s; transition: .4s; border-radius: 3px}
    input:checked+.slider1 {background-color: #039391}
    input:checked+.slider1:before {-webkit-transform: translateX(52px); 
                                   -ms-transform: translateX(52px); 
                                   transform: translateX(52px)}
   .slider2 {position: absolute; top: 0; left: 0; right: 0; bottom: 0; background-color: #B30000; 
             border-radius: 6px; cursor:pointer; }
   .slider2:before {position: absolute; content: ""; height: 24px; width: 52px; left: 8px; bottom: 8px;
             background-color: #fff; -webkit-transition: .4s; transition: .4s; border-radius: 3px}
    input:checked+.slider2 {background-color: #FFD65C}
    input:checked+.slider2:before {-webkit-transform: translateX(52px); 
                                   -ms-transform: translateX(52px); 
                                   transform: translateX(52px)}

Les rubriques .ang090, .ang180 et .ang270 permettent les rotations, dans le sens trigonométrique, d’angle de 90°, 180° ou 270°.

Les rubriques .slider1 et .slider2 correspondent respectivement au bouton « Alimenter l’itinéraire » et au bouton « Arrêt d’urgence ».

Le code Javascript %NOM_DU_TCO%.js.

Le fichier Javascript %NOM_DU_TCO%.js est définit comme ci-après. Il permet de décrire le TCO et il comprend :

  • La définition des variables représentant la position des appareils ;
  • La définition de la tables Elément de TCO, de la table descriptive du TCO, des tables Itinéraires et Cantons (conformément aux description fournies dans l’article TCO Web interactif avec des ESP32 et des ESP8266 (3)) ;

La définitions des variables :

// SCRIPT de description du TCO : Combrailles_v4
//*********************************************************************
 
var itiImg  = "_I6";
var almImg  = "_I5";
let ty_App  = "ATPXY";
let itinOk  = 0;
let msg001  = "&nbsp;Formez un itin&eacute;raire";
let msg002  = "&nbsp;Cantons aliment&eacute;s";
var posApp1 = "1";
var posApp2 = "1";
var posApp3 = "1";
var posApp4 = "1";
var posApp5 = "1";
var posApp6 = "1";
var posApp7 = "1";
var posApp8 = "1";
var imgWeb  = "";
var imgExt  = "";

La table des Eléments :

// Définition de la table ELEMENT (Vignettes)
//*********************************************************************
 
let TabElm = [
 { kod: "!Vide",    typ: "-", ori: "0", dst: "0" },
 { kod: "_Droit",   typ: "-", ori: "1", dst: "5" },
 { kod: "AigD1_E1", typ: "A", ori: "1", dst: "5" },
 { kod: "AigD1_E2", typ: "A", ori: "1", dst: "6" },
 { kod: "AigD2_E1", typ: "A", ori: "8", dst: "4" },
 { kod: "AigD2_E2", typ: "A", ori: "8", dst: "5" },
 { kod: "AigG1_E1", typ: "A", ori: "1", dst: "5" },
 { kod: "AigG1_E2", typ: "A", ori: "1", dst: "4" },
 { kod: "AigG2_E1", typ: "A", ori: "8", dst: "4" },
 { kod: "AigG2_E2", typ: "A", ori: "8", dst: "3" },
 …
 { kod: "VirD",     typ: "-", ori: "1", dst: "6" },
 { kod: "VirG",     typ: "-", ori: "1", dst: "4" }
];

La table descriptive du TCO :

// Définition de la table RESEAU (description du TCO)
//*********************************************************************
 
let TabRes = [
 { num: "X00Y01", kod: "Signal1",  ang: "0", typ: "D", img: "Sig1",      ori: "0", dst: "5", blk: "C001" },
 { num: "X01Y01", kod: "_Droit",   ang: "0", typ: "-", img: "H1",        ori: "1", dst: "5", blk: "C001" },
 { num: "X02Y01", kod: "AigD1_E1", ang: "0", typ: "A", img: "AigD1_E1",  ori: "1", dst: "5", blk: "" },
 { num: "X03Y01", kod: "_Droit",   ang: "0", typ: "-", img: "H1",        ori: "1", dst: "5", blk: "C005" },
 { num: "X04Y01", kod: "_Droit",   ang: "0", typ: "-", img: "H1",        ori: "1", dst: "5", blk: "C005" },
 { num: "X05Y01", kod: "_Droit",   ang: "0", typ: "-", img: "H1",        ori: "1", dst: "5", blk: "C005" },
 { num: "X06Y01", kod: "_Droit",   ang: "0", typ: "-", img: "H1",        ori: "1", dst: "5", blk: "C005" },
 { num: "X07Y01", kod: "_Droit",   ang: "0", typ: "-", img: "H1",        ori: "1", dst: "5", blk: "C005" },
 { num: "X08Y01", kod: "_Droit",   ang: "0", typ: "-", img: "H1",        ori: "1", dst: "5", blk: "C005" },
 { num: "X09Y01", kod: "AigG1_E1", ang: "2", typ: "A", img: "AigG1_E1",  ori: "5", dst: "1", blk: "" },
 …
 { num: "X08Y06", kod: "AigD1_E1", ang: "2", typ: "A", img: "AigD1_E1",  ori: "5", dst: "1", blk: "" },
 { num: "X09Y06", kod: "_Droit",   ang: "0", typ: "-", img: "H1",        ori: "1", dst: "5", blk: "C007" },
 { num: "X10Y06", kod: "_Droit",   ang: "0", typ: "-", img: "H1",        ori: "1", dst: "5", blk: "C007" },
 { num: "X11Y06", kod: "_Droit",   ang: "0", typ: "-", img: "H1",        ori: "1", dst: "5", blk: "C007" },
 { num: "X12Y06", kod: "_Droit",   ang: "0", typ: "-", img: "H1",        ori: "1", dst: "5", blk: "C007" },
 { num: "X13Y06", kod: "VirG",     ang: "0", typ: "-", img: "VirG1",     ori: "1", dst: "4", blk: "C007" }
];

La table Itinéraire et la table Cantons :

// Définition de la table ITINERAIRE
//*********************************************************************
let TabIti = [ ];


// Définition de la table CANTONS
//*********************************************************************
let TabBlk = [ ];

Ces tables sont vides au départ, elles sont renseignées par la fonction CalculeIti qui est abordée plus loin.

Le code Javascript TCOWeb4.js.

Ce fichier Javascript contient le code de toutes les fonctions utilisées pour la gestion des appareils ou le calcul des itinéraires. Il comprend :

  • Les fonctions de modification de la position des appareils de voie ChangeAig, ChangeAigS, ChangeAigT et ChangeTjd (qui sont réécrites pour tenir compte de la suppression de la variable « rotation » et pour la mise en place des itinéraires) ;
  • L’ajout de la nouvelle fonction ChangePlk pour gérer les changements d’état de la plaque tournante (à noter, le code permettant le fonctionnement physique de la plaque n’est pas fourni et reste à écrire) ;
  • Diverses fonctions pour gérer l’affichage du nom des appareils survolés par la souris (AfficheEtiq et EffaceEtiq) ;
  • Les fonctions nécessaires au calcul d’itinéraire :
  1. Les fonctions de prise en compte des appareils ChercheElm, RotateApp et ActionneApp ;
  2. La fonction la plus importante CalculeIti et des sous-fonctions (ChercheRes, ChercheNxt, VerifieNxt, …) ;
  3. Les deux fonctions actionnées par les boutons, à savoir AlimCantons et ArretUrgence.

La fonction ChangeAig (les autres fonctions pour les aiguilles symétriques ou triples et pour les TJD se déduisent de celle-ci) permet de :

  • si l’itinéraire est alimenté, on ne fait rien (itinOK > 1) ;
  • sinon on efface l’itinéraire avec la fonction EffaceIti ;
  • puis on change la position de l’aiguille (posAig), on affiche le libellé du nouvel état et la vignette correspondante ;
  • on recalcule la destination de l’appareil grâce à la fonction ActionneApp ;
  • on envoie au serveur le nouvel état de l’appareil.
function ChangeAig(posApp, kodApp, imgApp)
//*********************************************************************
{
   var posAig = posApp;
   var xhr = new XMLHttpRequest();
   if (itinOk > 1)    { return posApp;}
   EffaceIti();
   if ( posAig == "1" )
   {
      posAig = "2";
      document.getElementById("DESC").innerHTML = "&nbsp;"+kodApp+" : voie d&eacute;vi&eacute;e";
   }
   else
   {
      posAig = "1";
      document.getElementById("DESC").innerHTML = "&nbsp;"+kodApp+" : voie directe";
   }
   document.images['img'+kodApp].src=imgWeb+imgApp+'_E'+posAig+imgExt;
   ActionneApp(document.images['img'+kodApp].id, posAig);
   xhr.open("GET", "/update?appareil="+kodApp+":A&state="+posAig, true);
   xhr.send();
   return posAig;
}

La nouvelle fonction ChangePlk :

function ChangePlk(posApp, kodApp, imgApp)
//*********************************************************************
{
   var posPlk = posApp;
   var xhr = new XMLHttpRequest();
   if (itinOk > 1)    { return posApp;}
   EffaceIti();
   if ( posPlk == "1" )
   {  posPlk = "2"; }
   else if ( posPlk == "2" )
   {  posPlk = "3"; }
   else if ( posPlk == "3" )
   {  posPlk = "4"; }
   else
   {  posPlk = "1"; }
   document.getElementById("DESC").innerHTML = "&nbsp;"+kodApp+" : plaque en position "+posPlk;
   document.images['img'+kodApp].src=imgWeb+imgApp+'_E'+posPlk+imgExt;
   ActionneApp(document.images['img'+kodApp].id, posPlk);
   xhr.open("GET", "/update?appareil="+kodApp+":J&state="+posPlk, true);
   xhr.send();
   return posPlk;
}

Les fonctions Javascript permettant l’affichage du nom de l’appareil survolé par la souris :

function AfficheEtiq(nomApp, coordApp)
{
   let canvas    = document.getElementById('cnv1');
   let ctx       = canvas.getContext('2d'); 

   ctx.fillStyle   = '#48A';
   ctx.fillRect( 0, 0, 60, 40);
   ctx.fillStyle   = "yellow"; 
   ctx.font        = 'Bold 16px Sans-Serif';
   ctx.fillText(nomApp, 10, 25);
   ctx.strokeStyle = "#ff8000"; 
   ctx.lineWidth   = 4;
   ctx.strokeRect( 0, 0, 60, 40);
}

function EffaceEtiq(coordApp)
{
   let canvas    = document.getElementById('cnv1');
   let ctx       = canvas.getContext('2d');
   var img       = document.getElementById("pierre")
   ctx.drawImage(img, 0, 0);
}

La fonction principale du calcul de l’itinéraire conformément à l’algorithme présenté dans l’article TCO Web interactif avec des ESP32 et des ESP8266 (3). Cette fonction est exécutée dès qu’un élément de départ est cliqué :

function CalculeIti(coordIti, nomIti)
//*********************************************************************
{
   let posNxt = -1;
   var coordNxt = coordIti;
   let posElm = ChercheRes(coordIti);
   document.getElementById("DESC").innerHTML = "&nbsp;Calcul de l'itin&eacute;raire depuis le "+TabRes[posElm].kod+" "+nomIti;
   document.getElementById("B2").checked = false;
   EffaceIti(); 
   AfficheIti(coordIti, posElm);
   let iDst = TabRes[posElm].dst; 
   if (iDst == "0")
   { 
      TabRes[posElm].dst = TabRes[posElm].ori;
      TabRes[posElm].ori = iDst;
   }

   let bDone = false;
   while (!bDone)
   {
      posNxt = ChercheNxt(posElm);
      if (posNxt < 0) { bDone = true; break; }
      if (posNxt == posElm) { bDone = true; break; }
      if (!VerifieNxt(posElm, posNxt)) { bDone = true; break; }

      coordNxt = TabRes[posNxt].num;
      if (ty_App.indexOf(TabRes[posNxt].typ) < 0)
      { AfficheIti(coordNxt, posNxt); }
      posElm = posNxt;
   }
   itinOk = 1;
}

L’affichage de l’itinéraire constitué est pris en charge par la fonction suivante (à noter que cette fonction réalise également la mise à jour des tables Itinéraires et Cantons) :

function AfficheIti(coordElm, posElm)
//*********************************************************************
{
   document.getElementById(coordElm).src = imgWeb+TabRes[posElm].img+itiImg+imgExt;
   let iti = { num: TabRes[posElm].num, img: TabRes[posElm].img, blk: TabRes[posElm].blk };
   TabIti.push(iti);
   if (ChercheBlk(TabRes[posElm].blk) < 0)
   {
      let blk ={ nom: TabRes[posElm].blk, eta: "0" };
      TabBlk.push(blk);
   }
   return true; 
}

Quelques sous-fonctions, appelées par CalculeIti sont présentées ci-après.

function ChercheRes(coordRes)
//*********************************************************************
{
   var posElm = -1;
   for (let i = 0; i < TabRes.length; i++)
   {
      if (TabRes[i].num == coordRes) { posElm = i; break;}
   }
   return posElm; 
}

function ChercheNxt(posElm)
//*********************************************************************
{
   let posNxt = -1;
   let numElm = TabRes[posElm].num;
   let sT = numElm.slice(1, 3);
   let xC = Number(sT);
   sT = numElm.slice(4, 6);
   let yC = Number(sT);
   let dstElm = TabRes[posElm].dst;
   let dX = 0;
   let dY = 0;
   switch(dstElm)
   {
      case "1" : dX = -1; break;
      case "2" : dX = -1; dY = -1; break;
      case "3" : dY = -1; break;
      case "4" : dX =  1; dY = -1; break;
      case "5" : dX =  1; break;
      case "6" : dX =  1; dY =  1; break;
      case "7" : dY =  1; break;
      case "8" : dX = -1; dY =  1; break;
   }
   let xN = xC + dX;
   let yN = yC + dY;
   let numNxt = 'X'+(xN.toFixed()).padStart(2,"0")+'Y'+(yN.toFixed()).padStart(2,"0");
   posNxt = ChercheRes(numNxt);

   return posNxt;
}

function VerifieNxt(posElm, posNxt)
//*********************************************************************
{
   const k180 = "056781234";
   let b_Ok = true;
   let iOri = TabRes[posNxt].ori;
   let iDst = TabRes[posElm].dst;
   iDst = k180[Number(iDst)];
   if (iOri != iDst) 
   {
      // suivant : swap Orig et dest.
      iOri = TabRes[posNxt].dst;
      if (iOri == iDst) 
      {
         iDst               = TabRes[posNxt].dst;
         TabRes[posNxt].dst = TabRes[posNxt].ori;
         TabRes[posNxt].ori = iDst;
      }
      else
         b_Ok = false;  
   }
   return b_Ok;
}

Enfin, les deux fonctions appelées lors de l’activation des boutons :

function AlimCantons() 
//*********************************************************************
{
   if (itinOk == 1)
   {
      var xhr = new XMLHttpRequest();
      let i   = 0;
      let l_Blk = "";

      for (i = 0; i < TabIti.length; i++)
        {  
           document.getElementById(TabIti[i].num).src = imgWeb+TabIti[i].img+almImg+imgExt; 
        }

      for (i = 0; i < TabBlk.length; i++)
        { 
          l_Blk += TabBlk[i].nom;
          if (i < TabBlk.length-1) { l_Blk += ":"; } 
        }
      xhr.open("GET", "/update?section="+l_Blk+"&state="+itinOk.toString(), true);
      xhr.send();

      itinOk  = 2;
      document.getElementById("DESC").innerHTML = msg002;
   }
}

function ArretUrgence() 
//*********************************************************************
{
   if (itinOk == 2)
   {
      var xhr = new XMLHttpRequest();
      xhr.open("GET", "/update?section=RAZ_&state=0", true);
      xhr.send();

      EffaceIti();
      document.getElementById("DESC").innerHTML = msg001;
      document.getElementById("B1").checked = false;
   }
}

Le croquis à téléverser dans l’ESPxxx émetteur.

Le code du croquis de l’émetteur découle très largement du croquis de la version décrite dans l’article TCO Web interactif avec des ESP32 et des ESP8266 (2). Les seules différences concernent :

  • La définition de la table des récepteurs ("Aiguilles" et "Cantons" et du format du message envoyé aux récepteurs ;
  • La définition de la fonction OnDataSent (callback des données envoyées) ;
  • L’appairage avec les modules ESPxxx récepteurs grâce à leur code MAC ;
  • La fonction CommandeAppareil qui envoie la demande changement d’état aux récepteurs Aiguilles ;
  • La fourniture de la fonction AlimenteSection qui envoie la commande aux récepteurs Cantons.

Le code MAC [3] des récepteurs se récupère en jouant le croquis suivant sur chaque récepteur (c’est ce code qui devra alimenter le tableau des récepteurs – voir sa description plus loin) :

#ifdef ESP32
  #include <WiFi.h>
#else
  #include <ESP8266WiFi.h>
#endif
 
void setup()
{
  Serial.begin(115200);
  Serial.println();
  Serial.print("Addresse MAC : ");
  Serial.println(WiFi.macAddress());
}
 
void loop()
{
}
Les bibliothèques utilisées.

Il faut importer les bibliothèques prenant en charge un serveur Web asynchrone hébergé par un ESPxxx, la bibliothèque gérant le protocole ESP-NOW ainsi que les bibliothèques gérant l’accès à la mémoire Flash.

/*--------------------------------------------------------------------------
   Import required libraries
  --------------------------------------------------------------------------*/
#ifdef ESP32
#include <esp_now.h>
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <SPIFFS.h>
#else
#include <Arduino.h>
#include <espnow.h>
#include <ESP8266WiFi.h>
#include <Hash.h>
#include <ESPAsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include "FS.h"
#endif
Les constantes et variables.
/*--------------------------------------------------------------------------
   DONNEES DE LA TACHE TCO_WEB
  --------------------------------------------------------------------------*/
// Connection au routeur wifi (la box)
#define DEBUG
const char* ssid     = "... le nom de votre routeur ...";
const char* password = "... votre mot de passe ...";
      bool  b_Appr   = false;           // pour modifier appareil voie
      bool  b_Alim   = false;           // pour alimenter itinéraire

// communication entre le serveur et la page HTML (browser)
const char*  PARAM_INPUT_1 = "appareil"; // Nom de l'appareil
const char*  PARAM_INPUT_2 = "state";    // Etat de l'appareil
const char*  PARAM_INPUT_3 = "section";  // Nom de la section
      String inputMessage1;
      String inputMessage2;
      String inputMessage3;
La table des récepteurs.
/*--------------------------------------------------------------------------
   DEFINITION DE LA TABLE DES RECEPTEURS
  --------------------------------------------------------------------------*/
typedef struct
{
  bool    actif;
  char    tyRcp;                         // A = appareil, K = section
  uint8_t bcAdr[6];
} recepteur;

// Table des RECEIVER'S MAC ADDRESS
#define NBMAC 4
recepteur t_Mac[NBMAC] =
{
  // mettre ici les adresses MAC de vos microprocesseurs
  { false, 'A', {0x10, 0x97, 0xBD, 0xE4, 0xBF, 0x64}},      // appareils
  { false, 'A', {0xAC, 0x0B, 0xFB, 0xD9, 0x8A, 0xA0}},
  { false, 'K', {0x0C, 0xB8, 0x15, 0xF7, 0x85, 0xA0}},      // cantons
  { false, 'K', {0x8A, 0xA0, 0xAC, 0x0B, 0xFB, 0xD9}}       
};

Le nombre de récepteurs #define NBMAC est à adapter à votre besoin et les codes MAC doivent être ceux de vos ESPxxx.

Les données ESP-NOW.
/*--------------------------------------------------------------------------
   DONNEES PROTOCOLE ESP-NOW
  --------------------------------------------------------------------------*/
struct comm
{
  char   typRcp;       // type de récepteur (A = appareil, K = section)
  String nomKod;       // nom de l'appareil ou de la section

  union
  {
    struct             // Appareil de voie
    {
      byte    numApp;  // numéro appareil (de 1 à 8)
      char    typApp;  // type de l'appareil ('A', 'X', 'T', 'Y')
      char    etaApp;  // état de l'appareil ( de '1' à '4')
      byte    noMot1;  // No du 1er moteur (de 1 à 8)
      byte    noMot2;  // No du 2me moteur (de 1 à 8)
    };
    struct             // Section de voie
    {
      char    etaSek;  // état : 0->Off, 1->alimenté
      byte    no_Rel;  // No du relais (de 1 à 8)
      byte    id_Rcp;  // No récepteur (rang+1 des macAdr)
      byte    dummy1;
      byte    dummy2;
    };
  };
};

//Create a struct_message called myComm
struct comm myComm;

// gestion des appariements
#ifdef ESP32
esp_now_peer_info_t peerInfo;
#endif

// Create AsyncWebServer object on port 80
AsyncWebServer server(80);
Le choix du TCO (définition tables des appareils et fonction processor).

D’une manière générale, ce module de description du TCO est un fichier source chargé dans le croquis et contenant du code supplémentaire décrivant la table des appareils, la fonction processor ainsi que les vignettes à charger depuis la mémoire Flash. Cette fonction permet d’injecter dans le code HTML la description du TCO.

Le croquis fourni avec ce projet comprend trois définitions de TCO. Le choix de chaque TCO se fait en modifiant le #define qui va bien (sic) et en recompilant. Par exemple, modifier #define TCO1 par #define TCO2 pour obtenir le TCO nommé Saint-Sernin puis téléverser.

// Prise en compte du fichier source du TCO
/*********************************************************************/
#define TCO1
#ifdef TCO1
#include "Combrailles.h"
#endif
#ifdef TCO2
#include "Saint-Sernin.h"
#endif
#ifdef TCO4
#include "Mon_TCO_No04.h"
#endif
Figure 4
Figure 4
Autres TCO proposés dans cet article.
Les modules de description de TCO.

Combrailles.h sera pris pour exemple.

L’entête :

// CODE C (Arduino) de description du TCO : Combrailles_v4
/*********************************************************************/

// Définition du nom du TCO
/*********************************************************************/
String nomTCO = "Combrailles";

La description de la table des appareils :

/*********************************************************************/
// Définition de la table des Appareils
/*********************************************************************/
typedef struct
{
  char   etaApp;    // état de l'appareil ( de '1' à '4')
  char   typApp;    // type de l'appareil ('A', 'X', 'T', 'Y')
  byte   noMot1;    // No du 1er moteur (de 1 à 8)
  byte   noMot2;    // No du 2me moteur (de 1 à 8)
  byte   noRcpt;    // No du ESP32/ESP8266 récepteur
} appareil;

#define NBMOT 8
appareil t_App[NBMOT] =
{
  {'1', 'A', 1, 0, 1},
  {'1', 'A', 2, 0, 1},
  {'1', 'A', 3, 0, 1},
  {'1', 'A', 4, 0, 1},
  {'1', 'P', 5, 0, 1},
  {'1', 'A', 6, 0, 1},
  {'1', ' ', 0, 0, 0},
  {'1', ' ', 0, 0, 0}
};

La description de la table des sections de voie :

/*********************************************************************/
// Définition de la table des Sections de voie
/*********************************************************************/
typedef struct
{
  String nomSek;    // nom de la section
  char   etaSek;    // état de la section ( '1' alimenté, '0' sinon)
  byte   no_Rel;    // No du relais (de 1 à 8)
  byte   noRcpt;    // No du ESP32/ESP8266 récepteur
} section;

#define NBSEK 10
section t_Sek[NBSEK] =
{
  {"C001", '0', 1, 4},
  {"C002", '0', 2, 4},
  {"C003", '0', 3, 4},
  {"C004", '0', 4, 4},
  {"C005", '0', 5, 4},
  {"C006", '0', 6, 4},
  {"C007", '0', 7, 4},
  {"C008", '0', 8, 4},
  {"C009", '0', 1, 3},
  {"C010", '0', 2, 3}
};

La fonction processor :

// Remplacer %CONSTRUIRE_TCO% par la table HTML des vignettes
/*********************************************************************/
String processor(const String& var)
{
  if (var == "CONSTRUIRE_TCO")
  {
    String tco = "";
    tco += "<table border='0' cellspacing='0' cellpadding='0'><tr>\n";
    tco += "<tr>\n";
    tco += "<td> <img class='ang000' src='Sign' id='X00Y00'></td>\n"; 
    tco += "<td> <img class='ang000' src='vide' id='X01Y00'></td>\n"; 
    tco += "<td> <img class='ang000' src='vide' id='X02Y00'></td>\n"; 
    tco += "<td> <img class='ang000' src='vide' id='X03Y00'></td>\n"; 
    tco += "<td> <img class='ang000' src='vide' id='X04Y00'></td>\n"; 
    tco += "<td> <img class='ang000' src='vide' id='X05Y00'></td>\n"; 
    tco += "<td> <img class='ang000' src='vide' id='X06Y00'></td>\n"; 
    tco += "<td> <img class='ang000' src='vide' id='X07Y00'></td>\n"; 
    tco += "<td> <img class='ang000' src='vide' id='X08Y00'></td>\n"; 
...
    tco += "<td> <img class='ang000' src='vide' id='X10Y07'></td>\n"; 
    tco += "<td> <img class='ang000' src='vide' id='X11Y07'></td>\n"; 
    tco += "<td> <img class='ang000' src='vide' id='X12Y07'></td>\n"; 
    tco += "<td> <img class='ang000' src='vide' id='X13Y07'></td>\n"; 
    tco += "<td> <img class='ang000' src='vide' id='X14Y07'></td>\n"; 
    tco += "</tr>\n";
    tco += "</table>\n";
    return tco;
  }
  else if (var == "NOM_DU_TCO")  { return nomTCO; }
  else return String();
}

La fonction de chargement des vignettes (ChargerVignettes) ;

/*********************************************************************/
void ChargerVignettes()
/*********************************************************************/
// Route pour charger TCOWeb4.css file, TCOWeb4.js file et les vignettes
{
  server.on("/TCOWeb4.css", HTTP_GET, [](AsyncWebServerRequest * request)
    { request->send(SPIFFS, "/TCOWeb4.css", "text/css"); });
  server.on("/TCOWeb4.js", HTTP_GET, [](AsyncWebServerRequest *request)
    { request->send(SPIFFS, "/TCOWeb4.js", "text/javascript"); });
  server.on("/Combrailles", HTTP_GET, [](AsyncWebServerRequest *request)
    { request->send(SPIFFS, "/Combrailles.js", "text/javascript"); });

  server.on("/Pierre", HTTP_GET, [](AsyncWebServerRequest *request)
    { request->send(SPIFFS, "/Pierre.gif", "image/gif"); });
...
  server.on("/But5", HTTP_GET, [](AsyncWebServerRequest *request)
    { request->send(SPIFFS, "/But5.png", "image/png"); });
  server.on("/But5_I5", HTTP_GET, [](AsyncWebServerRequest *request)
    { request->send(SPIFFS, "/But5_I5.png", "image/png"); });
  server.on("/But5_I6", HTTP_GET, [](AsyncWebServerRequest *request)
    { request->send(SPIFFS, "/But5_I6.png", "image/png"); });
  server.on("/Plak1_E1", HTTP_GET, [](AsyncWebServerRequest *request)
    { request->send(SPIFFS, "/Plak1_E1.png", "image/png"); });
  server.on("/Plak1_E2", HTTP_GET, [](AsyncWebServerRequest *request)
    { request->send(SPIFFS, "/Plak1_E2.png", "image/png"); });
  server.on("/Plak1_E3", HTTP_GET, [](AsyncWebServerRequest *request)
    { request->send(SPIFFS, "/Plak1_E3.png", "image/png"); });
  server.on("/Plak1_E4", HTTP_GET, [](AsyncWebServerRequest *request)
    { request->send(SPIFFS, "/Plak1_E4.png", "image/png"); });
}

La fonction CommandeAppareil.

/*********************************************************************/
void CommandeAppareil(const String& nomTyp, const String& etaApp)
/*********************************************************************/
{
  int    colPos = nomTyp.indexOf(':');
  String nomApp = nomTyp.substring(0, colPos);   // nom de l'appareil
  String typApp = nomTyp.substring(colPos + 1);  // type de l'appareil (Aig, Tjd, ...
  String numApp = nomTyp.substring(3, colPos);   // numéro de l'appareil
  int    no_App = numApp.toInt() - 1;            // ...en indice de table
  t_App[no_App].etaApp = etaApp[0];              // maj état de l'appareil

  myComm.typRcp = 'A';                           // A = appareil de voie
  myComm.nomKod = nomApp;
  myComm.numApp = no_App;
  myComm.typApp = typApp[0];
  myComm.etaApp = etaApp[0];
  myComm.noMot1 = t_App[no_App].noMot1;
  myComm.noMot2 = t_App[no_App].noMot2;
  int    noMac  = int(t_App[no_App].noRcpt) - 1;

  // Debug : affichage des appareils et de leur état
  //===================================================================
  char macStr[18];
  Serial.print   ( nomApp );
  Serial.print   ( ", Etat" );
  Serial.print   ( etaApp );
  Serial.print   ( " ==> Mac[");
  snprintf(macStr, sizeof(macStr), "%02x:%02x:%02x:%02x:%02x:%02x",
           t_Mac[noMac].bcAdr[0], t_Mac[noMac].bcAdr[1], t_Mac[noMac].bcAdr[2],
           t_Mac[noMac].bcAdr[3], t_Mac[noMac].bcAdr[4], t_Mac[noMac].bcAdr[5]);
  Serial.print(macStr);
  Serial.print   ( "] : ");

  if ((noMac >= 0) && (t_Mac[noMac].actif))
  {
#ifdef ESP32
    esp_err_t result1 = esp_now_send(t_Mac[noMac].bcAdr,  (uint8_t *) &myComm, sizeof(comm));
    (result1 == ESP_OK) ? Serial.print("Envoi OK " ) : Serial.print ("Envoi KO ");
#else
    esp_now_send(t_Mac[noMac].bcAdr,  (uint8_t *) &myComm, sizeof(comm));
    Serial.print("Envoi OK " );
#endif
    delay(500);
  }
  else
  {
    Serial.println("Pas de récepteur");
  }
}

La fonction AlimenteSection.

/*********************************************************************/
int ChercheSection( String& nom)
/*********************************************************************/
{
  int j = -1;
  for (int i=0; i<NBSEK; i++)
  {
    if (t_Sek[i].nomSek == nom)
    {
      j = i;
      break;
    }
  }
  return(j);
}

/*********************************************************************/
void AlimenteSection( String& nom, const String& eta)
/*********************************************************************/
{
  int    colPos = 0;
  int    b_Fin  = 1;
  int    num    = -1;
  int    noMac  = 0;
  String strSek = nom;
  String nomSek = "";
  
  while (b_Fin > 0)
  {
    colPos        = strSek.indexOf(':');
    if (colPos < 0)
    {
      b_Fin  = 0;
      nomSek = strSek;                              // nom de la section
    }
    else
    {
      nomSek = strSek.substring(0, colPos);         // nom de la section
    }
    num           = ChercheSection(nomSek);         // rang de la section
    if (num < 0) { continue; }
    noMac         = int(t_Sek[num].noRcpt) - 1;
    t_Sek[num].etaSek = eta[0];                     // maj état de la section

    myComm.typRcp = 'K';                           // A = appareil de voie
    myComm.nomKod = nomSek;
    myComm.etaSek = eta[0];
    myComm.no_Rel = t_Sek[num].no_Rel;

    // Debug : affichage des sections et de leur état
    //===================================================================
    char macStr[18];
    Serial.print   ( t_Sek[num].nomSek );
    Serial.print   ( ", Etat" );
    Serial.print   ( t_Sek[num].etaSek );
    Serial.print   ( " ==> Mac[");
    snprintf(macStr, sizeof(macStr), "%02x:%02x:%02x:%02x:%02x:%02x",
             t_Mac[noMac].bcAdr[0], t_Mac[noMac].bcAdr[1], t_Mac[noMac].bcAdr[2],
             t_Mac[noMac].bcAdr[3], t_Mac[noMac].bcAdr[4], t_Mac[noMac].bcAdr[5]);
    Serial.print(macStr);
    Serial.print   ( "] : ");

    if ((noMac >= 0) && (t_Mac[noMac].actif))
    {
#ifdef ESP32
      esp_err_t result1 = esp_now_send(t_Mac[noMac].bcAdr,  (uint8_t *) &myComm, sizeof(comm));
      (result1 == ESP_OK) ? Serial.print("Envoi OK " ) : Serial.print ("Envoi KO ");
#else
      esp_now_send(t_Mac[noMac].bcAdr,  (uint8_t *) &myComm, sizeof(comm));
      Serial.print("Envoi OK " );
#endif
      delay(500);
    }
    else
    {
      Serial.println("Pas de récepteur");
    }
    if (colPos >= 0)
    { strSek = strSek.substring(colPos + 1); }  // le reste de la liste des cantons
  }
}

La fonction RAZSection.

/*********************************************************************/
void RAZSection( String& nom, const String& eta)
/*********************************************************************/
{
  for (int i = 0; i < NBMAC; i++)
  {
    if (t_Mac[i].tyRcp == 'K') 
    {
      myComm.typRcp = 'K';                           // A = appareil de voie
      myComm.nomKod = nom;
      myComm.etaSek = eta[0];
      myComm.no_Rel = 0;
      t_Sek[i].etaSek = eta[0];                  // maj état de la section

      // Debug : affichage des sections et de leur état
      //===================================================================
      char macStr[18];
      Serial.print   ( nom );
      Serial.print   ( ", Etat" );
      Serial.print   ( eta );
      Serial.print   ( " ==> Mac[");
      snprintf(macStr, sizeof(macStr), "%02x:%02x:%02x:%02x:%02x:%02x",
             t_Mac[i].bcAdr[0], t_Mac[i].bcAdr[1], t_Mac[i].bcAdr[2],
             t_Mac[i].bcAdr[3], t_Mac[i].bcAdr[4], t_Mac[i].bcAdr[5]);
      Serial.print(macStr);
      Serial.print   ( "] : ");

      if (t_Mac[i].actif)
      {
#ifdef ESP32
        esp_err_t result1 = esp_now_send(t_Mac[i].bcAdr,  (uint8_t *) &myComm, sizeof(comm));
        (result1 == ESP_OK) ? Serial.print("Envoi OK " ) : Serial.print ("Envoi KO ");
#else
        esp_now_send(t_Mac[i].bcAdr,  (uint8_t *) &myComm, sizeof(comm));
        Serial.print("Envoi OK " );
#endif
        delay(500);
      }
      else
      {
        Serial.println("Pas de récepteur");
      }
    }
  }
}

La fonction OnDataSent.

#ifdef ESP32
/*********************************************************************/
void OnDataSent(const uint8_t *mac_addr, esp_now_send_status_t status)
/*********************************************************************/
// callback when data is sent
{
  Serial.println(status == ESP_NOW_SEND_SUCCESS ? "==> Livraison acceptée" : "==> Livraison refusée");
}
#else
/*********************************************************************/
void OnDataSent(uint8_t *mac_addr, uint8_t status)
/*********************************************************************/
// callback when data is sent
{
  Serial.println(status == 0 ? "==> Livraison acceptée" : "==> Livraison refusée");
}
#endif
Les fonctions d’initialisation d’appareils et de sections de voie.
/*********************************************************************/
void InitialiseAppareils()
/*********************************************************************/
{
  //  String nomTyp = "";

  for (int i = 0; i < NBMOT; i++)
  {
    if (t_App[i].typApp != ' ')
    {
      String etaApp = String(t_App[i].etaApp);
      String nomTyp = "App" + String(i + 1) + ":" + String(t_App[i].typApp);
      if (t_App[i].noRcpt > 0)
      {
        CommandeAppareil(nomTyp, etaApp);
      }
    }
  }
}

/*********************************************************************/
void InitialiseSections()
/*********************************************************************/
{
  String allRaz = "RAZ_";
  RAZSection(allRaz, String('0'));
}

La fonction setup.

En premier lieu, mise en route du moniteur série, surtout à des fins de déboguage :

/*********************************************************************/
void setup()
/*********************************************************************/
{
  // Serial port for debugging purposes
  //------------------------------------------------------------------
  Serial.begin(115200);
  Serial.print("### TCO-Web/Emetteur (v4.00) sur ");
#ifdef ESP32
  Serial.println("ESP32 ###");
#else
  Serial.println("ESP8266 ###");
#endif
  Serial.print("### TCO installé : ");
  Serial.print(nomTCO);
  Serial.println(" ###");

Puis, il faut se connecter à ESP-NOW.

  // Connection à ESP-NOW
  //------------------------------------------------------------------
  WiFi.mode(WIFI_STA);
  WiFi.disconnect();
  Serial.print("Initialisation ESP-NOW : ");
#ifdef ESP32
  if (esp_now_init() != ESP_OK) {
    Serial.println("Erreur");
    return;
  }
  Serial.println("Ok");
  // fonction callback
  esp_now_register_send_cb(OnDataSent);
#else
  if (esp_now_init() != 0) {
    Serial.println("Erreur");
    return;
  }
  Serial.println("Ok");
  // fonction callback
  esp_now_set_self_role(ESP_NOW_ROLE_CONTROLLER);
  esp_now_register_send_cb(OnDataSent);
#endif

Ensuite, il faut appairer l’émetteur avec tous les récepteurs prévus.

  // Appairage des récepteurs
  //------------------------------------------------------------------
  char macStr[18];
  for (int noMac = 0; noMac < NBMAC; noMac++)
  {
    snprintf(macStr, sizeof(macStr), "%02x:%02x:%02x:%02x:%02x:%02x",
             t_Mac[noMac].bcAdr[0], t_Mac[noMac].bcAdr[1], t_Mac[noMac].bcAdr[2],
             t_Mac[noMac].bcAdr[3], t_Mac[noMac].bcAdr[4], t_Mac[noMac].bcAdr[5]);
    Serial.print("Appairage avec Mac[");
    Serial.print(macStr);
    Serial.print("] : ");

#ifdef ESP32
    peerInfo.channel = 0;
    peerInfo.encrypt = false;
    // register peer
    memcpy(peerInfo.peer_addr, t_Mac[noMac].bcAdr, 6);
    if (esp_now_add_peer(&peerInfo) != ESP_OK)
    {
      Serial.println("Echec");
      t_Mac[noMac].actif = false;
    }
    else
    {
      Serial.println("Ok");
      t_Mac[noMac].actif = true;
    }
#else
    if (esp_now_add_peer(t_Mac[noMac].bcAdr,  ESP_NOW_ROLE_SLAVE, 1, NULL, 0) != 0)
    {
      Serial.println("Echec");
      t_Mac[noMac].actif = false;
    }
    else
    {
      Serial.println("Ok");
      t_Mac[noMac].actif = true;
    }
#endif
  }

Ensuite, on initialise SPIFFS et on se connecte à WiFi.

  // Initialisation SPIFFS
  //------------------------------------------------------------------
  if (!SPIFFS.begin())
  {
    Serial.println("Initialisation SPIFFS incorrecte");
    return;
  }
  else
  {
    Serial.println("Initialisation SPIFFS : OK");
  }

  // Connection à Wi-Fi
  //------------------------------------------------------------------
  Serial.print   ( "Initialisation Wifi " );
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }

  // Print ESP Local Data
  Serial.println ( "" );
  Serial.print   ( "Maintenant connecté à " );
  Serial.println ( ssid );
  Serial.print   ( "Adresse IP : " );
  Serial.println ( WiFi.localIP() );
  Serial.print   ( "mac Adress : " );
  Serial.println ( WiFi.macAddress() );
  Serial.print   ( "WiFi channel : " );
  Serial.println ( WiFi.channel() );

Enfin, on démarre le serveur Web.

  // Démarrage serveur web
  //------------------------------------------------------------------
  // Route for root / web page
  server.on("/", HTTP_GET, [](AsyncWebServerRequest * request)
    { request->send(SPIFFS, "/index.html", String(), false, processor); });

  ChargerVignettes();

  // Send a GET request to <ESP_IP>/update?appareil=<inputMessage1>&state=<inputMessage2>
  server.on("/update", HTTP_GET, [] (AsyncWebServerRequest * request) 
  {
    // GET input1 value on <ESP_IP>/update?appareil=<inputMessage1>&state=<inputMessage2>
    if (request->hasParam(PARAM_INPUT_1) && request->hasParam(PARAM_INPUT_2))
    {
      inputMessage1 = request->getParam(PARAM_INPUT_1)->value();
      inputMessage2 = request->getParam(PARAM_INPUT_2)->value();
      // pour contrer le watchdog de Async_tcp :
      // on déporte le travail à faire ds fonction loop
      b_Appr        = true;    
    }
    else if (request->hasParam(PARAM_INPUT_3) && request->hasParam(PARAM_INPUT_2))
    {
      inputMessage3 = request->getParam(PARAM_INPUT_3)->value();
      inputMessage2 = request->getParam(PARAM_INPUT_2)->value();
      // pour contrer le watchdog de Async_tcp
      // on déporte le travail à faire ds fonction loop
      b_Alim        = true;    
    }
    else
    {
      inputMessage1 = "Message ";
      inputMessage2 = "KO";
      Serial.print(inputMessage1);
      Serial.println(inputMessage2);
    }

    request->send(200, "text/plain", "OK");
  });

  // Start server
  server.begin();

  // Initialisation Appareils
  //------------------------------------------------------------------
  Serial.println ( "Début initialisation appareils " );
  InitialiseAppareils();
  Serial.println ( "Initialisation appareils terminée " );

  // Initialisation Sections de voie
  //------------------------------------------------------------------
  Serial.println ( "Début initialisation sections de voie " );
  InitialiseSections();
  Serial.println ( "Initialisation sections de voie terminée " );

La fonction loop.

/*********************************************************************/
void loop()
/*********************************************************************/
{
  // pour contrer le watchdog de Async_tcp
  // le travail à faire est déporté ici
  if (b_Appr)
  {
    CommandeAppareil(inputMessage1, inputMessage2);
    b_Appr = false;
  }
  
  if (b_Alim)
  {
    if (inputMessage3.substring(0, 3) == "RAZ")
    { RAZSection(inputMessage3, inputMessage2); }
    else
    { AlimenteSection(inputMessage3, inputMessage2); }
    b_Alim = false;
  }
}
Et maintenant ? Conseils pour démarrer.

Les prérequis pour utiliser ce croquis sont les suivants :

Pour ces installations, vous pouvez également consulter TCO Web interactif avec des ESP32 et des ESP8266 (1) et TCO Web interactif avec des ESP32 et des ESP8266 (2).

Dès lors, il faut télécharger le ZIP du croquis ci-dessous, et le dézipper dans votre répertoire des croquis Arduino (généralement C :\Users\"nom_du_user"\Documents\Arduino\ ).

Croquis de l’émetteur et sous-répertoire Data.

Une fois le croquis installé, vous devez charger la mémoire Flash de votre microprocesseur en utilisant le plugin ESPxxx Sketch Data Upload. La manipulation est expliquée dans l’article TCO Web interactif avec des ESP32 et des ESP8266 (2).

Enfin, après avoir choisi le TCO ( Combrailles , Saint-Sernin ou Mon_TCO_No04 ), vous pouvez téléverser le croquis. Sur votre moniteur série, vous obtiendrez le résultat ci-après.

Figure 5
Figure 5
Contenu du moniteur série au démarrage de l’émetteur.

Le module récepteur "Aiguilles".

Le croquis pour les modules récepteurs Aiguilles a été décrit dans l’article TCO Web interactif avec des ESP32 et des ESP8266 (2). Les principales différences avec cette version sont la nouvelle structure de communication ESP-NOW permettant la prise en compte des itinéraires. Des améliorations cosmétiques ont également été apportées.

Croquis du récepteur "Aiguilles"

Lorsque vous aurez installé le croquis et que vous l’aurez téléversé, vous verrez sur le moniteur série les messages suivants :

Figure 6
Figure 6
Contenu du moniteur série au démarrage du récepteur "Aiguilles".

Le module récepteur "Cantons".

Le code du croquis.

Le code du croquis du module récepteur "Cantons" comprend, dans l’ordre :

  • Les includes nécessaires au programme : Esp-Now, et WiFi ;
  • Les constantes et variables générales du programme (initialisation Esp-Now, gestion des 74HC595, etc.) ;
  • La fonction getWifiChannel pour pouvoir utiliser le même channel que l’émetteur ;
  • La fonction AlimenteSection pour alimenter les cantons à la demande du module émetteur ;
  • La fonction OnDataRecv, appelée lorsque des données sont reçues par le récepteur ;
  • La fonction setup qui permet l’initialisation de la communication Esp-Now ;
  • La fonction loop qui détecte les demandes d’alimentation des cantons et les exécute.

Selon que le module récepteur est basé sur un ESP32 ou un ESP8266, la librairie gérant le WiFi n’est pas la même, ainsi que la librairie pour Esp-Now.

/*--------------------------------------------------------------------------
   Import required libraries
  --------------------------------------------------------------------------*/
#ifdef ESP32
  #include <esp_now.h>
  #include <esp_wifi.h>
  #include <WiFi.h>
#else
  #include <espnow.h>
  #include <ESP8266WiFi.h>
#endif

La structure de communication entre l’émetteur et les récepteurs est comme suit :

/*--------------------------------------------------------------------------
   DONNEES PROTOCOLE ESP-NOW
  --------------------------------------------------------------------------*/
// interface de commande des appareils et des sections de voie
struct comm
{
  char   typRcp;       // type de récepteur (A = appareil, K = section)
  String nomKod;       // nom de l'appareil ou de la section

  union
  {
    struct             // Appareil de voie
    {
      byte    numApp;  // numéro appareil (de 1 à 8)
      char    typApp;  // type de l'appareil ('A', 'J', 'T', 'S')
      char    etaApp;  // état de l'appareil ( de '1' à '4')
      byte    noMot1;  // No du 1er moteur (de 1 à 8)
      byte    noMot2;  // No du 2me moteur (de 1 à 8)
    };
    struct             // Section de voie
    {
      char    etaSek;  // état : 0->rien, 1->alimenté
      byte    no_Rel;  // No du relais (de 1 à 8)
      byte    id_Rcp;  // No récepteur (rang+1 des macAdr)
      byte    dummy1;
      byte    dummy2;
    };
  };
};
//Create a struct_message called myComm
struct comm myComm;
struct comm oldComm;

//Numéro du récepteur (rang+1 des adresses Mac chez l'émetteur
byte    no_Rcp  = 3;

Les données de commande du 74HC595, intermédiaire entre le récepteur et les relais d’alimentation des cantons sont comme ci-après :

/*--------------------------------------------------------------------------
 * GESTION DU 74HC595 
---------------------------------------------------------------------------*/
#ifdef ESP32
const    byte verrou     = 2;     // 74HC595 : ST_CP (latch)
const    byte donnee     = 4;     // 74HC595 : DS (data)
const    byte horloge    = 0;     // 74HC595 : SH_CP (clock) 
#else
const    byte verrou     = 13;    // 74HC595 : ST_CP (latch) D7
const    byte donnee     = 14;    // 74HC595 : DS (data)     D5
const    byte horloge    = 12;    // 74HC595 : SH_CP (clock) D6
#endif

         uint8_t  u_Alim = 0b00000000;             // Data pour le 74HC595
const    uint8_t  z_Msk  = 0b10000000;             // Masque

La fonction getWifiChannel pour récupérer le channel de l’émetteur :

/*--------------------------------------------------------------------------
 * RECUPERATION CHANNEL DE L'EMETTEUR
---------------------------------------------------------------------------*/
constexpr char WIFI_SSID[] = "... le nom de votre routeur ...";
int32_t getWiFiChannel(const char *ssid)
{
  if (int32_t n = WiFi.scanNetworks())
  {
    for (uint8_t i = 0; i < n; i++)
    {
      if (!strcmp(ssid, WiFi.SSID(i).c_str()))
      {
        return WiFi.channel(i);
      }
    }
  }
  return 0;
}

La fonction AlimenteSection permet de mettre sous ou hors tension tout ou partie des cantons gérés par le récepteur :

/*********************************************************************/
void AlimenteSection() 
/*********************************************************************/
{
  String   nomKod  = myComm.nomKod;
  char     etaSek  = myComm.etaSek;
  byte     no_App  = myComm.numApp;
  int      no_Rel  = myComm.no_Rel - 1;            // No relais (de 1 à 8)
  uint8_t  z_Msk1  = z_Msk >> no_Rel;
  uint8_t  w_Alim;
  if (nomKod.indexOf("RAZ") < 0)
  { 
    w_Alim = u_Alim | z_Msk1;
    no_Rcp = myComm.id_Rcp; 
  }
  else
  { 
    w_Alim = 0b00000000;
    no_Rcp = 0; 
  }

#ifdef DEBUG 
  delay(100);
  Serial.print   ( myComm.nomKod);
  Serial.print   ( " ==> Av = ");
  Serial.print   ( u_Alim, BIN);
  Serial.print   ( "  Ap = " );
  Serial.println ( w_Alim, BIN);  
#endif

  // Envoi données relais au 74HC595
  //===================================================================
  u_Alim = w_Alim;
  digitalWrite(verrou, LOW);                  // mettre le verrou
  shiftOut(donnee, horloge, LSBFIRST, u_Alim);
  digitalWrite(verrou, HIGH);                 // oter le verrou
}

La fonction OnDataRecv, appelée lorsque des données sont reçues par le récepteur :

#ifdef ESP32
/*********************************************************************/
void OnDataRecv(const uint8_t * mac, const uint8_t *incomingData, int len) 
/*********************************************************************/
//callback function that will be executed when data is received
{
  memcpy(&myComm, incomingData, sizeof(myComm));
);
  Serial.print   ( myComm.nomKod);
  Serial.print   ( " ==> ");
  Serial.print   ( myComm.etaSek == '0' ? "hors" : "sous");
  Serial.println ( " tension");
}
#else
/*********************************************************************/
void OnDataRecv(uint8_t * mac, uint8_t *incomingData, uint8_t len) 
/*********************************************************************/
{
  memcpy(&myComm, incomingData, sizeof(myComm));
);
  Serial.print   ( myComm.nomKod);
  Serial.print   ( " ==> ");
  Serial.print   ( myComm.etaSek == '0' ? "hors" : "sous");
  Serial.println ( " tension");
}
#endif 
 

La fonction setup permet l’initialisation de la communication Esp-Now :

 
/*********************************************************************/
void setup() 
/*********************************************************************/
{
  //Initialize Serial Monitor for debugging purposes
  //------------------------------------------------------------------
  Serial.begin(115200);
#ifdef ESP32
  Serial.print("### TCO-Web/ReceptSec (v4.50) sur ");
  Serial.println("ESP32 ###");
#else
  Serial.println("");
  Serial.print("### TCO-Web/ReceptSec (v4.50) sur ");
  Serial.println("ESP8266 ###");
#endif

  // Préparation du 74HC595 
  //------------------------------------------------------------------
  pinMode(horloge, OUTPUT);
  pinMode(verrou,  OUTPUT);
  pinMode(donnee,  OUTPUT);
  
  myComm.nomKod  = "RAZ_";
  myComm.etaSek  = '0';
  oldComm.nomKod = "RAZ_";
  oldComm.etaSek = '0';

  // Connection à ESP-NOW
  //------------------------------------------------------------------
  WiFi.mode(WIFI_STA);
  int32_t channel = getWiFiChannel(WIFI_SSID);
  Serial.print   ( "WiFi SSID =  " );
  Serial.println ( WIFI_SSID );
  Serial.print   ( "Old WiFi channel : " );
  Serial.println ( WiFi.channel() );
  Serial.print   ( "New WiFi channel : " );
  Serial.println ( channel );
  
#ifdef ESP32
  esp_wifi_set_promiscuous(true);
  esp_wifi_set_channel(channel, WIFI_SECOND_CHAN_NONE);
  esp_wifi_set_promiscuous(false);
#else
  wifi_promiscuous_enable(1);
  wifi_set_channel(channel);
  wifi_promiscuous_enable(0);
#endif


//WiFi.disconnect();
//Serial.println("");
  Serial.print("Initialisation ESP-NOW : ");
  
#ifdef ESP32
  if (esp_now_init() != ESP_OK) {
    Serial.println("Erreur");
    return;
  }
  Serial.println("Ok");
  // fonction callback
  esp_now_register_recv_cb(OnDataRecv);
#else
  if (esp_now_init() != 0) {
    Serial.println("Erreur");
    return;
  }
  Serial.println("Ok");
  // fonction callback
  esp_now_set_self_role(ESP_NOW_ROLE_CONTROLLER);
  esp_now_register_recv_cb(OnDataRecv);
#endif

  Serial.print ( "ReceptSec sur  Mac[" );
  Serial.print ( WiFi.macAddress() );
  Serial.println("] prêt à recevoir.");
}
 

La fonction loop détecte les demandes d’alimentation des cantons et les exécute :

/*********************************************************************/
void loop() 
/*********************************************************************/
{
  // test : entrée d'une commande de mise sous tension
  if (myComm.nomKod != oldComm.nomKod ||
      myComm.etaSek != oldComm.etaSek)
  {
     AlimenteSection();
     
     oldComm.nomKod = myComm.nomKod;
     oldComm.etaSek = myComm.etaSek;
     delay(100);
  }
}
 
Croquis du module récepteur "Cantons".

Lorsque vous aurez installé ce croquis et que vous l’aurez téléversé, vous verrez sur le moniteur série les messages suivants :

Figure 7
Figure 7
Contenu du moniteur au démarrage du croquis du récepteur "Cantons".

Pour conclure.

Vous pouvez dès maintenant définir votre propre TCO et le tester. Vous pouvez également introduire de nouvelles vignettes et pourquoi pas, les doter de nouvelles fonctions interactives.

Dans le prochain article, nous aborderons le circuit imprimé du récepteur "Cantons" ainsi que les préconisations pour alimenter votre réseau.

[1SPIFFS : Serial Peripheral Interface Flash File System.

[2ESPxxx : ESP32 ou ESP8266 de la société Espressif.

[3MAC : media access control – adresse physique du matériel.

Réagissez à « TCO Web interactif avec des ESP32 et des ESP8266 (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 »

Les derniers articles

Les articles les plus lus