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.
TCO Web interactif
TCO Web interactif avec des ESP32 et des ESP8266 (4)
Les itinéraires (le logiciel)
.
Par :
DIFFICULTÉ :★★★
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.
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 :
- Utiliser la mémoire Flash par emploi de la bibliothèque SPIFFS [1] pour stocker les vignettes des éléments de TCO, le fichier HTML principal ( index.html ), la feuille de style et le code Javascript ;
- Mettre en service d’un serveur Web asynchrone (voir Random Nerd Tutorials : ESP32 Async Web Server – Control Outputs pour plus de précisions) ;
- Communiquer avec les divers récepteurs en utilisant le protocole ESP-NOW (voir sa description dans l’article TCO Web interactif avec des ESP32 et des ESP8266 (2)) ;
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 :
- en français : Notions de base en HTML - Apprendre le développement web ;
- en anglais : HTML Tutorial (w3schools.com) ;
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 :
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'> Formez un itiné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éraire</h4>
<label class="switch1"><input type="checkbox" onchange="AlimCantons()" id="B1" >
<span class="slider1"></span></label></td>
<td><h4>Arrê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 :
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 = " Formez un itinéraire";
let msg002 = " Cantons alimenté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
etChangeTjd
(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
etEffaceEtiq
) ; - Les fonctions nécessaires au calcul d’itinéraire :
- Les fonctions de prise en compte des appareils
ChercheElm
,RotateApp
etActionneApp
; - La fonction la plus importante
CalculeIti
et des sous-fonctions (ChercheRes
,ChercheNxt
,VerifieNxt
, …) ; - Les deux fonctions actionnées par les boutons, à savoir
AlimCantons
etArretUrgence
.
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 = " "+kodApp+" : voie déviée";
}
else
{
posAig = "1";
document.getElementById("DESC").innerHTML = " "+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 = " "+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 = " Calcul de l'itiné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
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 :
- avoir installé l’IDE Arduino (on suppose que cela est fait !) ;
- installer les bibliothèques pour les ESP32 et/ou les ESP8266 (voir Random Nerd Tutorials : Getting Started with the ESP32 Development Board) ;
- installer le ou les plugins pour utiliser la mémoire Flash (voir Random Nerd Tutorials : ESP32 Web Server using SPIFFS (SPI Flash File System)) ;
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\ ).
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.
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.
Lorsque vous aurez installé le croquis et que vous l’aurez téléversé, vous verrez sur le moniteur série les messages suivants :
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);
}
}
Lorsque vous aurez installé ce croquis et que vous l’aurez téléversé, vous verrez sur le moniteur série les messages suivants :
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.
[1] SPIFFS : Serial Peripheral Interface Flash File System.
[2] ESPxxx : ESP32 ou ESP8266 de la société Espressif.
[3] MAC : media access control – adresse physique du matériel.