Le programme :
Je rappelle que Marklin® utilise principalement le protocole CAN pour la communication entre ses équipements (hormis malheureusement la rétro signalisation qui est en S88) mais aussi le TCP. C’est sous ce mode que l’on va par exemple relier avantageusement une CS2 ou CS3 à Rocrail®. Mais la MS2, pour les questions de positionnement dans la gamme déjà évoquées, n’est pas équipée de cette liaison.
Tout le protocole de communication CAN de Marklin® est public. Vous pouvez le consulter ici : cs2CAN-Protokoll-2_0 Voir également, au sujet de ce document, mon commentaire en bas de l’article (*).
Les ordinateurs « domestiques » en règle générale communiquent en WiFi, en Ethernet et en Série (USB). Mais pas en CAN malheureusement.
Le programme est avant tout une passerelle qui route les messages entrants en TCP vers le bus CAN et inversement.
Il est téléchargeable sur le Github de Locoduino
Dans le programme, vous devrez renseigner le nom de votre réseau WiFi et son mot de passe. Nous verrons cela un peu plus loin.
Si vous utilisez PlateformIO, vous pourrez téléverser alors le programme sans autre modification.
Sur l’IDE Arduino, vous devrez tout d’abord, renommer le fichier main.cpp avec un nom explicite pour vous, « marklin_cs2_can-gateway.ino » par exemple, en pensant bien à ajouter l’extension « .ino » à la place de ".cpp>.
Et si ce n’est pas déjà fait, installez la bibliothèque CAN pour ESP32 de Pierre Molinaro disponible sur Github
La bibliothèque WiFi @ 2.0.0 est disponible d’origine sur les deux IDE PlateformIO et Arduino et n’a donc pas besoin d’installation.
Quelques considérations générales sur le code :
Précision préliminaire importante :
Il est de règle sur Locoduino que l’on publie nos programmes (open source) et qu’on les commente pour que d’autres puisse s’en inspirer et/ou les modifier.
Mais vous pouvez tout à fait utiliser ce montage sans avoir compris ni même lu ce qui va suivre concernant le code !
Il faut comprendre que le système proposé n’est ni plus ni moins qu’une passerelle entre un réseau utilisant le protocole TCP et un bus CAN.
Il faut saluer l’efficacité de Marklin® dans la simplicité. Les messages sont tous de 13 bytes en entrée et de 13 bytes en sortie, quel que soit le sens de circulation. L’ordre des bits, (13 x 8 = 104 bits par messages) est le même en entrée et en sortie, c’est juste leur segmentation qui diffère entre une trame TCP et une trame CAN
Le travail consiste à lire les messages entrants, organiser leur segmentation et de le renvoyer sur l’autre réseau.
L’enjeu est de ne perdre aucun message !!! D’où la nécessité d’un programme qui travail vite et qui puisse traiter les tâches de réception et d’envoi en parallèle et non de manière séquentielle.
J’ai utilisé pour cela la programmation en tâches (task) disponible avec FreeRTOS, le système d’exploitation de l’ESP32. Nous avons quatre tâches principales qui s’exécutent concurrament :
CANReceiveTask
: Réception des messages en provenance du bus CAN
TCPSendTask
: Envoi des messages en TCP vers Rocrail®
TCPReceiveTask
: Réception des messages TCP
CANSendTask
: Envoi des messages sur le bus CAN
Les tâches s’échangent les messages au travers de deux "queues", files d’attente, dont l’objectif est de stocker les messages avant traitement dans le but de n’en perdre aucun. Ces files sont : canToTcpQueue
et tcpToCanQueue
Le debug s’exécute également dans une tâche dédiée debugFrameTask
avec une queue qui lui est propre debugQueue
Quelques zooms sur le code :
/*
rocrail_CAN_TCP_GW
Programme permettant le pilotage de locomotives à partir de Rocrail©
et la mise a jour de la liste des locomotives en MFX en utilisant une liaison TCP (WiFi ou Ethernet).
*/
#define PROJECT "rocrail_can_tcp_gateway"
#define VERSION "1.0"
#define AUTHOR "Christophe BOBILLE - www.locoduino.org"
//----------------------------------------------------------------------------------------
// Board Check
//----------------------------------------------------------------------------------------
#ifndef ARDUINO_ARCH_ESP32
#error "Select an ESP32 board"
#endif
//----------------------------------------------------------------------------------------
// Include files
//----------------------------------------------------------------------------------------
#include <ACAN_ESP32.h> // https://github.com/pierremolinaro/acan-esp32.git
#include <Arduino.h>
#include <WiFi.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <freertos/queue.h>
//----------------------------------------------------------------------------------------
// Debug serial
//----------------------------------------------------------------------------------------
#define debug Serial // (only for TCP connection)
Classiquement, nous trouvons en début de programmes les fichiers inclus, les déclarations et initialisations de variables globales, l’affectations des broches.
On remarquera l’inclusion des fichiers freertos
#define debug Serial
signifie que l’on écrit dans le code debug.print("Hello")
par exemple mais que la pré-compilation va substituer Serial en lieu et place de debug.
Personnellement, pour l’affichage de message série comme ceux du debug, j’utilise l’excellent utilitaire coolTerm
- Figure 9 - Exemple d’informations fournies par le debug dans une fenêtre de l’utilitaire CoolTerm
//----------------------------------------------------------------------------------------
// CAN Desired Bit Rate
//----------------------------------------------------------------------------------------
static const uint32_t DESIRED_BIT_RATE = 250UL * 1000UL; // Marklin CAN baudrate = 250Kbit/s
La vitesse de communication sur le bus CAN est réglée à 250Kbp/s qui est le standard Marklin®
//----------------------------------------------------------------------------------------
// Buffers : Rocrail always send 13 bytes
//----------------------------------------------------------------------------------------
byte cBuffer[13]; // CAN buffer
byte sBuffer[13]; // Serial buffer
Nous déclarons deux tableaux de type byte pour Les buffers de réception, un pour les réceptions tcp, un pour le can.
//----------------------------------------------------------------------------------------
// Marklin hash
//----------------------------------------------------------------------------------------
uint16_t rrHash; // for Rocrail hash
Nous déclarons également une variable sur 16 bits qui va contenir l’identifiant unique (ID) du logiciel Rocrail® en tant qu’équipement présent sur le bus CAN.
//----------------------------------------------------------------------------------------
// TCP/WIFI-ETHERNET
//----------------------------------------------------------------------------------------
#include <WiFi.h>
const char *ssid = "*************";
const char *password = "*************";
const uint port = 15731;
const char *ip = "192.168.1.13"; // tcpbin.com's ip
Nous devons inclure la bibliothèque WiFi, renseigner le SSID de notre réseau WiFi domestique ainsi que le mot de passe d’accès à ce réseau.
Ici, j’ai donné une adresse ip « arbitraire » (192.168.1.13) mais cohérente par rapport à la table d’adresse de ma box. Cette adresse doit être la même que celle renseignée dans Rocrail®.
- Figure 10 - Paramètres TCP dans Rocrail®
Le port d’écoute du serveur sur l’ESP32 est 15731, port par défaut des CS2/CS3. Il peut être modifié si vous avez déjà des équipements qui l’utilisent.
WiFiServer server(port);
WiFiClient client;
Pour finir avec cette partie instances/déclarations, nous instancions un objet server
de la classe WiFiServer pour que notre ESP32 se transforme en un serveur tcp et un objet client
de la classe WiFiClient
pour la connexion de Rocrail® à l’ESP32. Notez qu’il n’y a qu’une instance possible, n’autorisant ainsi qu’une seule connexion ce qui est à mon sens préférable pour cette utilisation précise.
//----------------------------------------------------------------------------------------
// Queues
//----------------------------------------------------------------------------------------
QueueHandle_t canToTcpQueue;
QueueHandle_t tcpToCanQueue;
QueueHandle_t debugQueue; // Queue for debug messages
//----------------------------------------------------------------------------------------
// Tasks
//----------------------------------------------------------------------------------------
void CANReceiveTask(void *pvParameters);
void TCPSendTask(void *pvParameters);
void TCPReceiveTask(void *pvParameters);
void CANSendTask(void *pvParameters);
void debugFrameTask(void *pvParameters); // Debug task
Et ici, nous avons les déclarations correspondant aux cinq tâches et aux queues évoquées plus haut.
Dans le setup()
, nous trouvons tout d’abord le begin()
du ports série, que nous avons nommé debug
pour une meilleure lisibilité
//----------------------------------------------------------------------------------------
// SETUP
//----------------------------------------------------------------------------------------
void setup()
{
//--- Start serial
debug.begin(115200); // For debug
while (!debug)
{
debug.print(".");
delay(200);
}
delay(100);
debug.println("Start setup");
Nous avons ensuite dans le programme, la configuration du CAN,
ACAN_ESP32_Settings settings(DESIRED_BIT_RATE);
tout d’abord en renseignant le débit souhaité
settings.mRxPin = GPIO_NUM_22; // Optional, default Rx pin is GPIO_NUM_5
settings.mTxPin = GPIO_NUM_23; // Optional, default Tx pin is GPIO_NUM_4
... puis les broches utilisées. Attention, ma carte « universelle » utilise les broches 22 et 23 alors que traditionnellement, ce sont les broches 4 et 5 qui sont réservées pour la communication CAN.
const uint32_t errorCode = ACAN_ESP32::can.begin(settings);
et enfin on démarre une connexion.
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED)
{
delay(500);
debug.print(".");
}
debug.println("");
debug.println("WiFi connected.");
debug.println("IP address: ");
debug.println(WiFi.localIP());
server.begin();
Viennent ensuite l’activation du WiFi et le lancement du serveur...
while (!client) // listen for incoming clients
client = server.available();
// extract the Rocrail hash
debug.printf("New Client Rocrail : 0x");
if (client.connected()) { // loop while the client's connected
int16_t rb = 0; //!\ Do not change type int16_t
while (rb != 13)
{
if (client.available()) // if there's bytes to read from the client,
rb = client.readBytes(cBuffer, 13);
}
RrHash = ((cBuffer[2] << 8) | cBuffer[3]);
debug.println(RrHash, HEX);
Nous profitons du setup pour lancer une action qui ne sera exécutée qu’une seule fois, à savoir le décodage du hash, identifiant unique de Rocrail® sur le bus CAN.
Le message de reconnaissance est le premier message envoyé par Rocrail® à l’occasion de son démarrage. Aussi, veillez-bien à ce que l’ESP32 soit déjà opérationnel avant d’ouvrir Rocrail®
Dans le protocole de communication CAN de Marklin®, l’ID de l’équipement est « contenu » dans les 16 bits de poids faible de l’identifiant long du message qui compte 29 bits. On obtient sa valeur par la lecture de deux octets avec un décalage de 8 bits pour le premier octet. RrHash = ((cBuffer[2] << 8) | cBuffer[3]);
while (!client) // listen for incoming clients
client = server.available();
// extract the Rocrail hash
debug.printf("New Client Rocrail : 0x");
if (client.connected())
{ // loop while the client's connected
int16_t rb = 0; //!\ Do not change type int16_t See https://www.arduino.cc/reference/en/language/functions/communication/stream/streamreadbytes/
while (rb != 13)
{
if (client.available()) // if there's bytes to read from the client,
rb = client.readBytes(cBuffer, 13);
}
rrHash = ((cBuffer[2] << 8) | cBuffer[3]);
debug.println(rrHash, HEX);
Ensuite, nous nous chargeons d’envoyer sans plus attendre ce message sur le bus CAN afin que tous les équipements raccordés aient connaissance de la présence de Rocrail®. C’est en effet l’une des spécificités du bus CAN particulièrement utile sur nos réseaux miniatures : TOUS les équipements CAN peuvent lire TOUS les messages circulants sur le bus et aucun message ne doit avoir de destinataire(s) « attitré(s) ». Mais on peut choisir, sur n’importe quel équipement, de « filtrer » tel(s) ou tel(s) expéditeur(s) ou telle ou telle commande (ou plage de commande).
// --- register Rocrail on the CAN bus
CANMessage frame;
frame.id = (cBuffer[0] << 24) | (cBuffer[1] << 16) | rrHash;
frame.ext = true;
frame.len = cBuffer[4];
for (byte i = 0; i < frame.len; i++)
frame.data[i] = cBuffer[i + 5];
const uint32_t ok = ACAN_ESP32::can.tryToSend(frame);
debugFrame(&frame);
Puis à la fin du setup()
, nous lançons l’exécution des tâches avec leurs paramètres spécifiques ainsi que pour les queues :
// Create queues
canToTcpQueue = xQueueCreate(50, sizeof(CANMessage));
tcpToCanQueue = xQueueCreate(50, 13 * sizeof(byte));
debugQueue = xQueueCreate(50, sizeof(CANMessage));
// Create tasks
xTaskCreatePinnedToCore(CANReceiveTask, "CANReceiveTask", 2 * 1024, NULL, 3, NULL, 0); // priority 3 on core 0
xTaskCreatePinnedToCore(TCPSendTask, "TCPSendTask", 2 * 1024, NULL, 5, NULL, 1); // priority 5 on core 1
xTaskCreatePinnedToCore(TCPReceiveTask, "TCPReceiveTask", 2 * 1024, NULL, 3, NULL, 1); // priority 3 on core 1
xTaskCreatePinnedToCore(CANSendTask, "CANSendTask", 2 * 1024, NULL, 5, NULL, 0); // priority 5 on core 0
xTaskCreatePinnedToCore(debugFrameTask, "debugFrameTask", 2 * 1024, NULL, 1, NULL, 1); // debug task with priority 1 on core 1
Avec xTaskCreatePinnedToCore
, je demande au système d’exécuter une tâche sur l’un ou l’autre cœur de l’ESP32. Je répartis ainsi le travail ce qui a pour conséquence d’améliorer significativement les performances globales. C’est la dernière valeur passée en paramètre de la fonction.
L’avant avant dernier paramètre est la priorité donnée à la tâche par rapport aux autres. Les tâches de réceptions ont une priorité "3" plus élevés que les tâches d’envoi "5". La tâche de debug est moins prioritaire avec un niveau 1.
Il n’y a rien à exécuter dans le loop()
"traditionnel" puisque les tâches s’exécutant simultanément, ce sont chacune des loops ! Mais dans le framework Arduino, la présence de void loop()
est obligatoire, même s’il n’y a rien à exécuter dedans.
Vous avez bien compris la répartition des tâches et leurs exécutions (quasi) simultanées. Détaillons la première et la plus simple : CANReceiveTask
//----------------------------------------------------------------------------------------
// CANReceiveTask
//----------------------------------------------------------------------------------------
void CANReceiveTask(void *pvParameters)
{
CANMessage frameIn;
while (true)
{
if (ACAN_ESP32::can.receive(frameIn))
{
xQueueSend(canToTcpQueue, &frameIn, portMAX_DELAY);
xQueueSend(debugQueue, &frameIn, 10); // send to debug queue
}
vTaskDelay(10 / portTICK_PERIOD_MS); // Avoid busy-waiting
}
}
frameIn
est une instance (objet créé) de la classe CANMessage
. Cette instance est créée dans une partie de code qui n’est exécutée qu’une seule fois.
En effet, le loop
de la fonction est exécuté à l’intérieur de la boucle while(true)
. En programmation, c’est un des moyens pour créer une boucle répétée sans fin, la condition true est par essence toujours vraie.
if (ACAN_ESP32::can.receive(frameIn))
nous indique que, si un message CAN est reçu, celui-ci sera placé dans la queue canToTcpQueue
par la fonction xQueueSend()
La fonction TCPReceiveTask réalise la même opération mais cette fois pour les messages envoyés par le client (Rocrail®) à destination du serveur TCP de l’ESP32 ;
//----------------------------------------------------------------------------------------
// TCPReceiveTask
//----------------------------------------------------------------------------------------
void TCPReceiveTask(void *pvParameters)
{
while (true)
{
if (client.connected() && client.available())
{
if (client.readBytes(cBuffer, 13) == 13)
xQueueSend(tcpToCanQueue, cBuffer, 10);
}
vTaskDelay(10 / portTICK_PERIOD_MS); // Avoid busy-waiting
}
}
Concernant maintenant les tâches d’envoi, nous examinerons CANSendTask
byte buffer[13];
Nous déclarons tout d’abord une fois pour toute un buffer "local" à 13 éléments. Souvenez vous, les trames TCP sont toujours de 13 bytes, une case de tableau donc pour chacun des octets.
Ensuite, au travers de la boucle sans fin, nous examinons s’il y a un (des) message(s) dans la queue. Dans l’affirmative, nous positionnons chacun des octets reçus à sa place dans l’identifiant du messages CAN mais aussi dans les datas par un jeu de ce que l’on appelle en informatique, la manipulation de bits !
En fin, vous l’avez compris, on expédie le message CAN sur le bus : ACAN_ESP32::can.tryToSend(frameOut);
//----------------------------------------------------------------------------------------
// CANSendTask
//----------------------------------------------------------------------------------------
void CANSendTask(void *pvParameters)
{
byte buffer[13];
while (true)
{
if (xQueueReceive(tcpToCanQueue, buffer, portMAX_DELAY))
{
CANMessage frameOut;
frameOut.id = (buffer[0] << 24) | (buffer[1] << 16) | rrHash;
frameOut.ext = true;
frameOut.len = buffer[4];
for (byte i = 0; i < frameOut.len; i++)
frameOut.data[i] = buffer[i + 5];
const bool ok = ACAN_ESP32::can.tryToSend(frameOut);
xQueueSend(debugQueue, &frameOut, 10); // send to debug queue
}
}
}
Pour TCPSendTask, vous vous en doutez, les choses sont très similaires :
//----------------------------------------------------------------------------------------
// TCPSendTask
//----------------------------------------------------------------------------------------
void TCPSendTask(void *pvParameters)
{
CANMessage frameIn;
while (true)
{
if (xQueueReceive(canToTcpQueue, &frameIn, portMAX_DELAY))
{
// clear sBuffer
memset(sBuffer, 0, sizeof(sBuffer));
// debug.printf("CAN -> TCP\n\n");
xQueueSend(debugQueue, &frameIn, 10); // send to debug queue
sBuffer[0] = (frameIn.id & 0xFF000000) >> 24;
sBuffer[1] = (frameIn.id & 0xFF0000) >> 16;
sBuffer[2] = (frameIn.id & 0xFF00) >> 8; // hash
sBuffer[3] = (frameIn.id & 0x00FF); // hash
sBuffer[4] = frameIn.len;
for (byte i = 0; i < frameIn.len; i++)
sBuffer[i + 5] = frameIn.data[i];
client.write(sBuffer, 13);
}
}
}
Pour completer cette présentation, il nous faut regarder côté Rocrail® les réglages à opérer :
- Figure 11 - Ecran des propriétés de Rocrail® sous l’onglet "centrale"
1° - On crée une nouvelle centrale dans le menu « propriétés de Rocrail® » onglet « centrale » à l’aide du bouton « ajouter » en bas à gauche en ayant pris soin tout d’abord de sélectionner « mbus » dans le menu déroulant à gauche du bouton ajouter.
- Figure 12 - Les paramètres pour l’IP du serveur et pour le n0 de port dans Rocrail®
2° - Vous double-cliquez sur la ligne ainsi crée et vous choisissez TCP (si vous voulez communiquer sous ce mode) ou « CC-Schnitte » en communication série.
- Figure 14 - Le mode "Maître" ne doit pas être activé.
Notez que Rocrail® ne doit pas être paramétré en "Maître" mais en "Esclave".
- Figure 13 : Ne pas oublier d’affecter une centrale à la locomotive. Ici ESP_32_WIFI
Pour la reconnaissance MFX des locomotives sur les rails et l’incrémentation dans Rocrail®, il faut supprimer la locomotive dans la MS2 et poser la locomotive sur les rails. Quand la MS2 aura terminer sa reconnaissance, elle va diffuser par le bus CAN l’ensemble des données et Rocrail® va ainsi pouvoir l’enregistrer à son tour. Veillez bien à affecter une centrale à cet "objet locomotive" dans Rocrail® sinon tout sera perdu à la fermeture de Rocrail® et tout sera à recommencer.