Les fichiers du programme :
Voyons maintenant les fichiers du programme à proprement parler.
Le fichier Config.h regroupe les informations de configuration statiques (contrairement à settings.json pour les données dynamiques).
Vous n’avez rien à modifier dans ce fichier (à moins que vous sachiez ce que vous faites).
/*
Config.h
Sur les cartes qui utilisent un module ESP32-WROVER pour avoir plus de RAM,
les pins GPIO16 et GPIO17 ne sont pas disponibles car ils sont utilisés en interne par la PSRAM.
ESP32 datasheet : https://www.espressif.com/sites/default/files/documentation/esp32-wrover-e_esp32-wrover-ie_datasheet_en.pdf
Pin mapping pour cette application : https://www.locoduino.org/IMG/png/pin_mapping_v7.png
*/
#ifndef __CONFIG__
#define __CONFIG__
#include <Arduino.h>
enum : uint8_t // Index des satellites périphériques
{
p00,
p01,
p10,
p11,
m00,
m01,
m10,
m11
};
/* ----- Options -------------------*/
#define SAUV_BY_MAIN // Sauvegardes des paramètres commandées par la carte Main
#define CHIP_INFO
#define RAILCOM
//#define RFID
/* ----- Debug -------------------*/
#define DEBUG
#ifdef DEBUG
#define debug Serial
#endif
//#define TEST_MEMORY_TASK
/* ---------------------------------*/
#define NO_ID 255
#define NO_PIN 255
/* ----- CAN ----------------------*/
#define CAN_RX GPIO_NUM_22
#define CAN_TX GPIO_NUM_23
#define CAN_BITRATE 1000UL * 1000UL // 1 Mb/s
/* ----- Node ----------------------*/
const uint8_t nodePsize = 8;
const uint8_t aigSize = 6;
const uint8_t sensorSize = 2;
const uint8_t signalSize = 2;
/* ----- Railcom -------------------*/
#define NB_ADDRESS_TO_COMPARE 10 // Nombre de valeurs à comparer pour obtenir l'adresse de la loco
#ifdef RAILCOM
#define RAILCOM_RX GPIO_NUM_0
#define RAILCOM_TX GPIO_NUM_17
#endif
/* ----- Sensors ------------------*/
#define CAPT_PONCT_ANTIHOR_PIN GPIO_NUM_12
#define CAPT_PONCT_HORAIRE_PIN GPIO_NUM_15
#define CAPT_PONCT_TEMPO 10UL
/* ----- Mesure de courant ---------*/
#define MESURE_COURANT GPIO_NUM_33 // ADC1 canal 5
/* ----- Registres a decalage ------*/
#define SHREG_PIN_VERROU GPIO_NUM_4
#define SHREG_PIN_HORLOGE GPIO_NUM_5
#define SHREG_PIN_DATA GPIO_NUM_18
/* ----- Découverte ---------------*/
#define INTER_DEV_1 GPIO_NUM_34 // Broche du dip switch pour inter1 dévié
#define INTER_DEV_2 GPIO_NUM_39 // Broche du dip switch pour inter2 dévié
#define BTN_SAT_PLUS GPIO_NUM_36 // Bouton de validation
#define BTN_SAT_MOINS GPIO_NUM_35 // Bouton de validation
#define LED_PIN_DISCOV GPIO_NUM_32 // Led
/* ----- Aiguilles -----------------*/
#define AIG_PIN_SIGNAL_0 GPIO_NUM_19
#define AIG_PIN_SIGNAL_1 GPIO_NUM_21
#define AIG_PIN_SIGNAL_2 GPIO_NUM_2
#define AIG_PIN_SIGNAL_3 GPIO_NUM_13
#define AIG_SPEED 6000.0000
#endif
Le fichier main.cpp désigne en c++ le fichier que l’application exécute en premier. C’est ce nom qui est utilisé avec l’environnement de développement PlateformIO.
Si vous utilisez l’IDE Arduino, ce fichier devra être renommé par exemple satAutonomeClient.ino, le même nom que le dossier de niveau supérieur dans le répertoire. Ne pas oublier l’extension .ino avec cet environnement de développement en remplacement de .cpp.
On trouve tout d’abord tout un ensemble de fichier que le compilateur a besoin de connaitre pour relier tous les fichiers entre eux ainsi que les bibliothèques utilisées.
//--- Fichiers inclus
#include <Arduino.h>
#include "CanMsg.h"
#include "CanConfig.h"
#include "Config.h"
#ifdef CHIP_INFO
#include "ChipInfo.h"
#endif
#include "Discovery.h"
#include "GestionReseau.h"
#include "Node.h"
#include "Railcom.h"
#include "Settings.h"
#include "SignauxCmd.h"
#include "WebHandler.h"
#include "Wifi_fl.h"
#include "freertos/queue.h"
Sous PlateformIO, il est possible de renseigner les bibliothèques nécessaire à l’aide du fichier plateformeio.ini dans la variable containeur lib_deps.
lib_deps =
me-no-dev/AsyncTCP @ ^1.1.1
https://github.com/me-no-dev/ESPAsyncWebServer.git
pierremolinaro/ACAN_ESP32@=1.1.2
bblanchon/ArduinoJson@^6.20.0
locoduino/RingBuffer@^1.0.5
roboticsbrno/ServoESP32@=1.0.3 ; ->bug avec la v1.1.1
Contrairement à l’IDE Arduino qui partage ses bibliothèques entre les différents projets, il possible avec PlateformIO d’associer à un projet les bibliothèques utilisées. Elles sont purement et simplement copiées dans un dossier caché du projet évitant ainsi des conflits. Ces bibliothèques sont régulièrement mises à jour par PlateformIO si vous lui demandez par un subtil jeu de code à la fin de chaque ligne comme : @^1.0.3
C’est vraiment très pratique car cela permet aussi d’empêcher la mise à jour avec une nouvelle version quand il y a une incompatibilité avec votre programme. C’est le cas par exemple avec cette bibliothèque dont j’interdit la mise jour au-delà de la version 1.0.3 : roboticsbrno/ServoESP32@=1.0.3 ; ->bug avec la v1.1.1
De même concernant la bibliothèque ACAN_ESP32 pour laquelle j’ai constaté des incompatibilités avec certains ESP32 "anciens". J’ai préféré ne pas prendre de risque en "bridant" une version qui fonctionne bien : pierremolinaro/ACAN_ESP32@=1.1.2
Revenons au fichier main.cpp. Nous réalisons ensuite un certain nombre d’instances de classes. Respectivement de la classe Node, Railcom, éventuellement RFID, puis des classes Fl_Wifi et WebHandler.
// Instances
Node *node = new Node();
Railcom railcom(RAILCOM_RX, RAILCOM_TX);
Fl_Wifi wifi;
WebHandler webHandler;
Les objets crées au travers de ces instances seront utilisés tout au long de l’exécution du programme.
La fonction setup()
est assez classique, je ne m’étendrai pas dessus sauf pour préciser que c’est ici que sont démarrées un certain nombre de tâches qui vont ensuite se dérouler sans plus s’arrêter. Nous en reparlerons au fur et à mesure. Ici non plus vous n’avez à faire de modifications de programme.
/*-------------------------------------------------------------
setup
--------------------------------------------------------------*/
void setup()
{
Serial.begin(115200);
while (!Serial)
;
delay(100);
//--- Infos ESP32 (desactivable)
#ifdef CHIP_INFO
ChipInfo::print();
#endif
Serial.printf("\nProject : %s", PROJECT);
Serial.printf("\nVersion : %s", VERSION);
Serial.printf("\nAuteur : %s", AUTHOR);
Serial.printf("\nFichier : %s", __FILE__);
Serial.printf("\nCompiled : %s", __DATE__);
Serial.printf(" - %s\n\n", __TIME__);
Serial.printf("-----------------------------------\n\n");
Settings::setup(node);
vTaskDelay(pdMS_TO_TICKS(100));
//--- Configure ESP32 CAN
CanConfig::setup();
vTaskDelay(pdMS_TO_TICKS(100));
CanMsg::setup(node);
vTaskDelay(pdMS_TO_TICKS(100));
bool err = 0;
if (err == Settings::begin())
{
Serial.printf("-----------------------------------\n");
Serial.printf("ID Node : %d\n", node->ID());
Serial.printf("-----------------------------------\n\n");
}
else
{
Serial.printf("[Settings] : Echec de la configuration\n");
return;
}
Serial.printf(Settings::discoveryOn() ? "[Discovery] : on\n\n" : "[Discovery] : off\n\n");
Serial.printf("-----------------------------------\n\n");
if (Settings::discoveryOn()) // Si option validee, lancement de la méthode pour le procecuss de decouverte
{
//--- Wifi et web serveur
if (Settings::wifiOn()) // Si option validee
{
wifi.start();
webHandler.init(node, 80);
}
Discovery::begin(node);
}
else
{
for (byte i = 0; i < signalSize; i++)
{
if (node->signal[i] == nullptr)
node->signal[i] = new Signal;
node->signal[i]->setup();
}
railcom.begin();
SignauxCmd::setup();
GestionReseau::setup(node);
Settings::wifiOn(false);
Serial.end(); // Desactivation de Serial en exploitation
}
Serial.printf(Settings::wifiOn() ? "[Wifi] : on\n" : "Wifi : off\n");
Serial.printf("[Main %d] : End setup\n\n", __LINE__);
#ifndef debug
Serial.end(); // Desactivation de Serial
#endif
} // ->End setup
Durant cette phase de setup(), un certain nombre d’informations seront retournées sur le moniteur série qui nous renseignent sur la bonne exécution du programme au cours de cette première phase jusqu’à l’apparition de End setup qui nous informe que tout c’est bien déroulé.
![PNG - 390.9 kio](local/cache-vignettes/L610xH887/setupprint-b0898.png?1708292386)
Avec Infos ESP32, vous vous assurerez que votre ESP32 possède bien les propriétés requises : 240 MHz pour les CPU, 2 cœurs, au moins 4 MB de mémoire flash.
Comme nous l’avons déjà vu, c’est ici également que sera renseigné l’adresse IP du satellite.
Pour la fonction loop()
, nous vérifions si le paramètre wifiOn
du fichier Settings est vrai ou faux
. Dans ce dernier cas, on n’exécute pas les méthodes liées au serveur web qui ne sera de toutes façons pas opérationnel. On économise ainsi de la puissance de calcul autrement utilisée par le serveur web et le wifi.
En exploitation, la fonction loop() ne servira qu’à la mise à jour de l’adresse de la locomotive au travers de Railcom©.
/*-------------------------------------------------------------
loop
--------------------------------------------------------------*/
void loop()
{
//******************** Ecouteur page web **********************************
if (Settings::wifiOn()) // Si option validée
webHandler.loop(); // ecoute des ports web 80 et 81
if (!Settings::discoveryOn()) // Si option non validée
{
//************************* Railcom ****************************************
if (railcom.address())
{
node->busy(true);
node->loco.address(railcom.address());
#ifdef debug
debug.printf("[Main %d ] Railcom - Numero de loco : %d\n", __LINE__, node->loco.address());
debug.printf("[main %d ] Railcom - this node busy : %d\n", __LINE__, node->busy());
#endif
}
else
{
node->loco.address(0);
#ifdef debug
debug.printf("[Main %d ] Railcom - Pas de loco.\n", __LINE__);
#endif
}
//**************************************************************************
}
vTaskDelay(pdMS_TO_TICKS(100));
} // ->End loop
Intéressons nous maintenant au fichier Settings et en particulier à la fonction Settings::setup(node)
qui sera exécutée en tout premier dans le setup(). C’est une méthode static
qui s’exécute sans qu’il y ait besoin d’instancier un objet. On lui passe en paramètre l’instance node
de la classe Node
. Ou plus exactement un pointeur sur l’adresse de l’objet en mémoire puisque qu’il a été instancié avec new : Node *node = new Node();
L’instance, l’objet node
représente virtuellement le satellite physique avec ses extensions, capteurs, signaux, aiguilles...
La fonction Settings::setup()
est la première exécutée qui cherche à se connecter à la mémoire flash pour lire les fichiers qui y sont stockés.
/*-------------------------------------------------------------
setup
--------------------------------------------------------------*/
void Settings::setup(Node *nd)
{
node = nd;
if (!SPIFFS.begin(true))
{
debug.printf("[Settings %d] : An Error has occurred while mounting SPIFFS\n\n", __LINE__);
return;
}
else
debug.printf("[Settings %d] : SPIFFS ok mounting\n", __LINE__);
readFile();
}
C’est ensuite la fonction readFile()
qui a en charge d’ouvrir le fichier settings.json et de copier chacune des valeurs qui y sont contenues dans les attributs de l’objet node. Le satellite est automatiquement re configuré avec les paramètres enregistrés lors de la dernière sauvegarde.
/*-------------------------------------------------------------
readFile
--------------------------------------------------------------*/
void Settings::readFile()
{
File file = SPIFFS.open("/settings.json", "r");
if (!file)
{
debug.printf("[Settings %d] : Failed to open settings.json\n\n", __LINE__);
return;
}
else
{
debug.printf("\nInformations du fichier \"settings.json\" : \n\n");
DynamicJsonDocument doc(4 * 1024);
DeserializationError error = deserializeJson(doc, file);
vTaskDelay(pdMS_TO_TICKS(100));
if (error)
debug.printf("[Settings %d] Failed to read file, using default configuration\n\n", __LINE__);
else
{
// ---
node->ID(doc["idNode"] | NO_ID);
debug.printf("- ID node : %d\n", node->ID());
Discovery::comptAig(doc["comptAig"]);
node->masqueAig(doc["masqueAig"]);
WIFI_ON = doc["wifi_on"];
DISCOVERY_ON = doc["discovery_on"];
ssid_str = doc["ssid"].as<String>();
password_str = doc["password"].as<String>();
strcpy(ssid, ssid_str.c_str());
strcpy(password, password_str.c_str());
node->maxSpeed(doc["maxSpeed"]);
node->sensMarche(doc["sensMarche"]);
// Nœuds
const char *index[] = {"p00", "p01", "p10", "p11", "m00", "m01", "m10", "m11"};
for (byte i = 0; i < nodePsize; i++)
{
if (doc[index[i]] != "null")
{
if (node->nodeP[i] == nullptr)
node->nodeP[i] = new NodePeriph;
node->nodeP[i]->ID(doc[index[i]]);
debug.printf("- node->nodeP[%s]->id : %d\n", index[i], node->nodeP[i]->ID());
}
else
debug.printf("- node->nodeP[%s]->id : NULL\n", index[i]);
}
debug.printf("---------------------------------\n");
// Aiguilles
for (byte i = 0; i < aigSize; i++)
{
// debug.printf("valeur de aig %d : %s%c\n", i, doc["aig" + String(i)]);
if (doc["aig" + String(i)] != "null")
{
if (node->aig[i] == nullptr)
node->aig[i] = new Aig;
node->aig[i]->ID(doc["aig" + String(i) + "id"]);
node->aig[i]->posDroit(doc["aig" + String(i) + "posDroit"]);
node->aig[i]->posDevie(doc["aig" + String(i) + "posDevie"]);
node->aig[i]->speed(doc["aig" + String(i) + "speed"]);
node->aig[i]->pin(doc["aig" + String(i) + "pin"]);
node->aig[i]->setup();
debug.printf("- Creation de l'aiguille %d\n", i);
}
}
debug.printf("---------------------------------\n");
// Signaux
for (byte i = 0; i < signalSize; i++)
{
if (doc["sign" + String(i)] != "null")
{
if (node->signal[i] == nullptr)
node->signal[i] = new Signal;
node->signal[i]->type(doc["sign" + String(i) + "type"]);
node->signal[i]->position(doc["sign" + String(i) + "position"]);
debug.printf("- Creation du signal %d\n", i);
}
}
debug.printf("---------------------------------\n");
}
file.close();
}
} //--- End readFile
Cela nous permet de renseigner les identifiants des satellites périphériques, les aiguilles présentes avec tous leurs attributs et d’autres informations comme le mot de passe wifi.
En tout premier lieu, cette fonction lit la valeur du l’identifiant de la carte qui est enregistré dans le fichier settings.json. Si l’ID est 255, cela veut dire que la carte n’est pas initialisée.
Dans ce cas, la fonction Settings::begin()
va interroger la carte Main qui lui retourne un identifiant que la carte va maintenant conserver de façon définitive.
Notez que le programme attend une réponse de la carte Main avant de poursuivre le programme.
/*-------------------------------------------------------------
begin
--------------------------------------------------------------*/
bool Settings::begin()
{
//--- Test de la présence de la carte Main
while (!isMainReady)
{
CanMsg::sendMsg(0, 0xB2, node->ID(), 254, 0);
if (!isMainReady)
debug.printf("[Settings %d] : Attente de reponse en provenance de la carte Main.\n", __LINE__);
vTaskDelay(pdMS_TO_TICKS(100));
}
//--- Identifiant du Node
while (node->ID() == NO_ID) // L'identifiant n'est pas en mémoire
{
//--- Requete identifiant
debug.printf("[Settings %d] : Le satellite ne possede pas d'identifiant.\n", __LINE__);
CanMsg::sendMsg(0, 0xB4, node->ID(), 254, 0);
vTaskDelay(pdMS_TO_TICKS(100));
}
writeFile();
debug.printf("[Settings %d] : End settings\n", __LINE__);
debug.printf("-----------------------------------\n\n");
return 0;
} //--- End begin
Une fois l’identifiant obtenu, il est aussitôt sauvegardé dans le fichier settings.json au travers de la fonction writeFile();
. On peut dire de la fonction writeFile()
qu’elle fait un peu en sens inverse ce que fait readFile()
! Elle permet la sauvegarde en mémoire flash des paramètres du satellite.
/*-------------------------------------------------------------
writeFile
--------------------------------------------------------------*/
void Settings::writeFile()
{
File file = SPIFFS.open("/settings.json", "w");
if (!file)
{
#ifdef DEBUG
debug.println("Failed to open settings.json\n\n");
#endif
return;
}
else
{
DynamicJsonDocument doc(4 * 1024);
doc["idNode"] = node->ID();
doc["comptAig"] = Discovery::comptAig();
doc["masqueAig"] = node->masqueAig();
doc["wifi_on"] = WIFI_ON;
doc["discovery_on"] = DISCOVERY_ON;
doc["ssid"] = ssid;
doc["password"] = password;
doc["maxSpeed"] = node->maxSpeed();
doc["sensMarche"] = node->sensMarche();
// Nœuds
const String index[] = {"p00", "p01", "p10", "p11", "m00", "m01", "m10", "m11"};
for (byte i = 0; i < nodePsize; i++)
{
if (node->nodeP[i] == nullptr)
doc[index[i]] = "null";
else
doc[index[i]] = node->nodeP[i]->ID();
}
// Aiguilles
for (byte i = 0; i < aigSize; i++)
{
if (node->aig[i] == nullptr)
doc["aig" + String(i)] = "null";
else
{
doc["aig" + String(i) + "id"] = node->aig[i]->ID();
doc["aig" + String(i) + "posDroit"] = node->aig[i]->posDroit();
doc["aig" + String(i) + "posDevie"] = node->aig[i]->posDevie();
doc["aig" + String(i) + "speed"] = node->aig[i]->speed();
doc["aig" + String(i) + "pin"] = node->aig[i]->pin();
}
}
// Signaux
for (byte i = 0; i < signalSize; i++)
{
if (node->signal[i] == nullptr)
doc["sign" + String(i)] = "null";
else
{
doc["sign" + String(i) + "type"] = node->signal[i]->type();
doc["sign" + String(i) + "position"] = node->signal[i]->position();
}
}
String output;
serializeJson(doc, output);
file.print(output);
file.close();
debug.print("Sauvegarde des datas en FLASH\n");
}
} //--- End writeFile
C’est tout pour les fonctions contenues dans ce fichier Settings.cpp
.
Concernant le fonctionnement de Railcom, fichiers Railcom.h et Railcom.cpp, je ne l’aborderai pas ici car il a déjà fait l’objet d’un article très détaillé sur le site : Détection RailCom© avec ESP32
Pas plus que CanConfig.h
et CanConfig.cpp
qui sont des configurations classiques. On notera cependant que je n’utilise aucun filtre. Chaque satellite reçoit donc l’ensemble des messages échangés sur le bus CAN et, par programmation rejette ou retient et traite les messages qui l’intéresse.
Nous poursuivrons dans les prochains articles la présentation du code pour ces satellites autonomes ainsi que pour les cartes Main et Watchdog.
(*1) – FreeRTOS est également le système d’exploitation de l’Arduino
(*2) - Certains constatent que PlateformIO est souvent privilégié à contrarion de l’IDE Arduino et s’intérrogent