Après les principes fondateurs de la carte Satellite V1 et la description du matériel, nous abordons maintenant les aspects logiciels externes au Satellite, juste avant de décrire le logiciel du Satellite lui-même. S’agissant plus d’un middleware que d’une API, nous avons décidé de nommer cette couche logicielle : le Satellite Abstraction Middleware qui doit être appelé par les applications.
... SAM pour les intimes !
Cet article comporte 3 parties :
Les formats des messages CAN pour le projet Locoduinodrome
2 capteurs de position « tout ou rien » (IR, ILS, capteurs à effet Hall,...)
1 détecteur de présence par consommation (intégré à la carte)
9 DEL
Le mode de communication choisi consiste à échanger des messages périodiquement entre le SAM et les satellites, chaque message émis (sortant) par un satellite regroupant l’ensemble des états des capteurs sous son contrôle et chaque message reçu (entrant) par un satellite contenant l’ensemble des commandes : la position souhaitée de l’aiguille et l’état des LED qu’il pilote. Ceci évite de multiplier les messages qu’échangent les satellites avec le système de gestion et diminue le trafic sur le réseau CAN.
Les Satellites V1 émettent donc un seul message CAN de type standard (identifiant de 11 bits) pour communiquer les états du détecteur de présence et des 2 capteurs de position. L’identifiant de ce message est égal à 0x10 + le numéro de Satellite émetteur. Il vaut donc 0x10 pour le Satellite 0, 0x11 pour le 1, et ainsi de suite jusqu’à 0x1F pour le Satellite 15 (voir figure 1).
Figure 1 : Identifiant des messages CAN émis par les satellites.
Cet identifiant est montré ici en binaire. La partie grisée, sur 7 bits, est fixe et qualifie le type du message. La partie blanche, sur 4 bits, reçoit le numéro du satellite de 0 à 15 dans cette version.
Vient ensuite un seul octet de données dont 3 bits sont significatifs : 1 bit pour l’état de la zone (occupée/libre), 1 bit pour le détecteur ponctuel 0 (faisceaux IR obstrué ou libre, capteur à effet Hall déclenché ou pas, etc) et 1 bit pour le détecteur ponctuel 1 comme montré à la figure 2.
Figure 2 : Unique octet de donnée des messages émis par le satellite
Seuls 3 bits de poids faible sont utilisés. Lorsqu’un bit est à ’-’, cela signifie que sa valeur est indifférente.
Ce message est envoyé automatiquement par le Satellite dès qu’un changement se produit afin de minimiser la latence entre une détection et la notification du changement au système de gestion. Il est également envoyé à chaque fois qu’un message d’état est reçu du système de gestion. Ceci permet, d’une part, de synchroniser ces messages périodiques [1] sur le système de gestion pour avoir une charge réseau homogène et prédictible et, d’autre part, de savoir si un Satellite est vivant et répond normalement.
Exemple : Le Satellite 6 détecte un véhicule dans la zone dont il est responsable mais les deux détecteurs ponctuels ne détectent rien. Le message qu’il envoie a le format montré à la figure 3.
Figure 3 : exemple de message envoyé par le Satellite 6
Le numéro du Satellite émetteur est dans les 4 bits de poids faible de l’identifiant. 0110 en binaire correspond à la valeur 6 en décimal. Les 5 bits de poids fort de l’octet 0 étant indifférents, ils sont mis à 0. Le bit 2 à 1 signifie que la zone est occupée. Les bits 1 et 0 à 0 indiquent qu’aucun des capteurs ponctuels ne détecte un véhicule.
Soit en binaire : 00000100
Le format des messages reçus par un satellite
Les Satellites V1 reçoivent un seul message spécifiant les états des actionneurs que la carte contrôle. L’identifiant de ce message standard est égal à 0x20 + le numéro du satellite destinataire. Il vaut donc 0x20 pour le Satellite 0, 0x21 pour le 1, et ainsi de suite jusqu’à 0x2F pour le Satellite 15 (voir figure 4).
Figure 4 : Identifiant des messages CAN reçus par les satellites.
Egalement en binaire, l’identifiant est formé d’une partie fixe grisée qui qualifie le type de message et d’une partie variable en blanc qui donne le numéro du Satellite destinataire.
La consigne des actionneurs comprend 3 octets formés de 9 fois 2 bits d’état des DEL plus 1 bit de position pour le servo. L’état de chaque DEL est décrit par 2 bits pour permettre les 3 états : éteint, allumé, clignotant. L’état du servo est décrit par un seul bit : position horaire ou trigonométrique qui correspondent à des positions maximales (à régler pour chaque satellite) quand le sens de déplacement est sens horaire ou sens trigonométrique. Le détail est présenté à la figure 5. Comme la position de l’aiguille dépend du montage mécanique du servo, savoir quelle position du servo correspond à quelle position de l’aiguille relève du système de gestion.
Figure 5 : Format des messages de consigne reçus par un Satellite
L’octet 0 donne l’état des LEDs 0 à 3, l’octet 1 l’état des LEDs 4 à 7 et l’octet 3 l’état de la LED 8 et du servo.
Exemple : la consigne pour le Satellite 7 est de mettre le servomoteur à la position horaire et d’avoir toutes les DEL éteintes sauf la 0 qui clignote et la 6 qui est allumée. Le message de consigne correspondant est présenté figure 6.
Figure 6 : message de consigne envoyé au Satellite 7
Dans ses 4 bits de poids faible, l’identifiant contient le numéro du Satellite (0111 en binaire représente 7 en décimal). Les 3 octets de données spécifient les états des LEDs et du servo. Les bits indifférents sont ici mis à 0.
Le Satellite Abstraction Middleware (SAM)
Le SAM est l’interface entre les satellites et un unique gestionnaire.
Ce qu’on appelle « gestionnaire » ici, est le système de gestion des circulations du réseau, le cerveau du réseau en quelque sorte. Il peut prendre différentes formes dont celle d’une carte Arduino avec interface CAN (ce qui est notre but ici) :
Figure 7 : Le gestionnaire sur Arduino
L’Arduino contient à la fois le gestionnaire, le middleware SAM et l’interface CAN
Il peut aussi prendre la forme d’un logiciel gestionnaire sur ordinateur (tel que JMRI par exemple), pourvu qu’il puisse émettre et recevoir les messages CAN par l’intermédiaire d’une passerelle (gateway) comme une passerelle CAN/WiFI-Ethernet ou CAN/Bluetooth :
Figure 8 : JMRI comme système de gestion des circulations
Le middleware SAM en C++ ne peut pas être intégré à JMRI en Java. Une passerelle est nécessaire.
Une telle passerelle n’est pas décrite dans cet article.
Ce gestionnaire pourra également prendre la forme d’un TCO pourvu qu’il n’interfère pas avec un autre gestionnaire car les doubles commandes des appareils de voie et des signaux ne sont pas envisageables.
Ce gestionnaire est donc un système généralement centralisé qui, à partir des informations sur les états occupés/libres des cantons, sur la position des appareils de voie et sur une représentation informatique de la structure du réseau, va déterminer l’état des signaux, les positions des aiguilles, donc recevoir les états des satellites et leur envoyer des commandes.
Il pourra évidemment faire bien plus, comme le suivi des trains, le bloc système, les itinéraires, etc.. en envoyant des commandes aux trains, mais c’est un autre domaine qui sort de cet article.
Le SAM est donc du code qui s’interface à votre gestionnaire pour lui donner accès aux satellites de façon simple.
On l’a vu au premier article de la série (La carte Satellite V1 (1)), comme les satellites ne sont pas spécialisés, ni sur le plan matériel, ni sur le plan logiciel, mais que leurs fonctions sont hétérogènes, leur spécialisation est faite par le gestionnaire, via le bus CAN. Car ces satellites sont des entrées/sorties déportées.
La correspondance entre les entrées-sorties des satellites et les équipements de voie est totalement libre : c’est à dire, par exemple, qu’un feu à 9 ampoules pourrait être géré par 2 Satellites, l’un pour 4 ampoules, l’autre pour 5. C’est une hypothèse d’école, bien sûr, mais ça peut arriver sur un réseau réel pour éviter d’ajouter un satellite quand il reste de la place dans un autre à proximité. Le fait de le voir comme une entité unique est de nature logicielle dans SAM, mais non matérielle. Il n’y a pas de configuration dans les satellites, ce qui élimine une grande partie de la complexité.
Pour gérer cette architecture matérielle extensible à volonté, le SAM définit un ensemble d’objets. Ces objets représentent les objets physiques du réseau (zones, aiguillages, feux, comme des proxy). La façon dont ces objets communiquent avec les nœuds sur le réseau CAN n’est pas du ressort du gestionnaire qui ne voit que le SAM.
SAM contient donc des classes d’interfaçage pour chaque type d’objet :
Une classe de base PointWrapper dont une instance est affectée à chaque commande d’aiguille.
Une classe de base SignalWrapper pour chaque signal.
Comme il existe sur notre réseau plusieurs types de signaux n’ayant pas tous le même nombre de DEL et le même comportement (par exemple dans le cas d’un œilleton), SAM définit un jeu de classes dérivées dont une instance est affectée à chaque signal. nous en avons défini 4 pour les besoins du Locoduinodrome :
Une classe SemaphoreSignalWrapper
Une classe CarreSignalWrapper
Une classe SemaphoreRalentissementSignalWrapper
Une classe CarreRappelRalentissementSignalWrapper
Nous allons regarder de plus près chacune des classes.
La commande d’aiguillage
SAM définit la classe PointWrapper qui possède une méthode begin() avec comme arguments le numéro d’aiguille du gestionnaire et le numéro de satellite (puisqu’on a une seule aiguille par satellite). Une table statique indexée par le numéro d’aiguille du gestionnaire permet de retrouver l’objet PointWrapper correspondant et ce dernier connaît le message qu’il doit envoyer.
Ensuite la méthode statique setPointPosition utilise le numéro d’aiguille du gestionnaire pour indexer la table, récupérer l’objet et appeler le setPosition de l’objet qui écrira dans le message.
Exemple d’utilisation pour une commande d’aiguillage :
setPointPosition(pointId, pointPos);
où pointId est le numéro d’aiguillage vu du gestionnaire et pointPos la position demandée (droit ou dévié).
Voici la déclaration de la classe PointWrapper que l’on trouve dans le fichier SatelliteWrapper.h :
class PointWrapper
{
private:
static PointWrapper *sPointList;
static PointWrapper **sPointTable;
static int16_t sHigherPointNumber;
int16_t mPointNumber; /* numéro de l'aiguillage dans le gestionnaire */
uint8_t mSatelliteId; /* indentifiant du satellite */
uint8_t mSatelliteIndex; /* index du satellite dans la table */
PointWrapper *mNext;
void setPosition(const bool inPosition);
void lookupMessage();
public:
/*
* begin construit la table indexée par le numéro d'aiguillage à partir de
* la liste. La liste est construite par le constructeur.
* begin demande également à chaque aiguillage de chercher leur
* message CAN en fonction de l'identifiant du satellite.
* À appeler dans setup.
*/
static void begin();
/*
* setPointPosition positionne l'aiguillage inPointNumber à la position
* inPosition
*/
static void setPointPosition(const int16_t inPointNumber, const bool inPosition);
/*
* constructeur
*/
PointWrapper(const uint16_t inPointNumber, const uint8_t inSatelliteId);
};
Et voici le code de la classe PointWrapper que l’on trouve dans le fichier SatelliteWrapper.cpp. Les explications sont dans les commentaires :
/*-------------------------------------------------------------
* PointWrapper class
*-------------------------------------------------------------
*/
PointWrapper *PointWrapper::sPointList = NULL;
PointWrapper **PointWrapper::sPointTable = NULL;
int16_t PointWrapper::sHigherPointNumber = -1;
/*
* Constructeur.
*
* Chaque objet PointWrapper établit une correspondance entre l'identifiant
* de l'aiguillage dans le gestionnaire et le numéro du satellite qui
* contrôle cet aiguillage. Le constructeur constitue également une liste de
* ces objets qui est ensuite exploité dans begin.
*/
PointWrapper::PointWrapper(
const uint16_t inPointNumber,
const uint8_t inSatelliteId
) :
mPointNumber(inPointNumber),
mSatelliteId(inSatelliteId),
mSatelliteIndex(NO_SATELLITE_INDEX)
{
/* Ajoute l'aiguillage dans la liste */
mNext = sPointList;
sPointList = this;
/* Note le max des identifiants d'aiguillage dans le gestionnaire */
if (mPointNumber > sHigherPointNumber) sHigherPointNumber = mPointNumber;
}
/*
* begin construit la table indexée par l'identifiant d'aiguillage dans le
* gestionnaire et l'objet PointWrapper qui établit la correspondance
* entre l'identifiant d'aiguillage et l'identifiant du satellite qui le
* commande. Il n'est pas nécessaire que les identifiants soient de un en un
* mais la consommation mémoire dépend du max des identifiants.
*/
void PointWrapper::begin()
{
if (sHigherPointNumber != -1) {
sPointTable = new PointWrapper*[sHigherPointNumber + 1];
if (sPointTable != NULL) {
for (int16_t i = 0; i <= sHigherPointNumber; i++) {
sPointTable[i] = NULL;
}
PointWrapper *currentPoint = sPointList;
while (currentPoint != NULL) {
sPointTable[currentPoint->mPointNumber] = currentPoint;
/* inscrit l'aiguillage dans la messagerie CAN */
currentPoint->lookupMessage();
currentPoint = currentPoint->mNext;
}
}
}
}
void PointWrapper::setPointPosition(
const int16_t inPointNumber,
const bool inPosition
)
{
if (inPointNumber >= 0 && inPointNumber <= sHigherPointNumber) {
if (sPointTable[inPointNumber] != NULL) {
sPointTable[inPointNumber]->setPosition(inPosition);
}
}
}
void PointWrapper::setPosition(const bool inPosition)
{
if (mSatelliteIndex != NO_MESSAGE_INDEX) {
outSatellitesMessages[mSatelliteIndex].setPointPosition(inPosition);
}
}
void PointWrapper::lookupMessage()
{
mSatelliteIndex = lookupMessageForId(mSatelliteId);
if (mSatelliteIndex != NO_MESSAGE_INDEX) {
outSatellitesMessages[mSatelliteIndex].reserve(mSatelliteId);
}
}
Les commandes de signaux
SAM définit la classe SignalWrapper qui possède une méthode begin() avec comme arguments le numéro de signal du gestionnaire, le numéro de satellite et une série de DEL (3, 6 ou 9) affectées à ce signal qui correspond au connecteur réel sur la carte Satellite comme représenté sur figure 9.
Figure 9 : Les connecteurs pour les signaux
3 connecteurs pour les signaux sont présents sur la carte. La broche la plus à droite peut être à 5V ou GND selon la position d’un strap pour choisir des LED à anode commune ou à cathode commune. On peut connecter 3 signaux à 3 feux ou bien un signal à 3 feux sur S1 et un à 6 sur S2 ou encore un signal à 9 feux sur S1.
Une table statique indexée par le numéro de signal du gestionnaire permet de retrouver l’objet SignalWrapper correspondant et ce dernier connaît le message où il doit écrire.
Exemple d’utilisation pour une commande de signal :
setSignalState(signalId, signalState);
où signalId est le numéro de feu et signalState son état dans le gestionnaire.
Voici la déclaration de la classe SignalWrapper que l’on trouve dans le fichier SatelliteWrapper.h :
/*
* La classe SignalWrapper est une classe abtraite représentant
* la correspondance entre un signal du gestionnaire et le signal matériel
* sur le satellite.
*/
/* cet enum definit le connecteur */
enum { SIGNAL_0 = 0, SIGNAL_1 = 3, SIGNAL_2 = 6 };
class SignalWrapper
{
private:
static SignalWrapper *sSignalList;
static SignalWrapper **sSignalTable;
static int16_t sHigherSignalNumber;
int16_t mSignalNumber; /* Numéro du signal dans le gestionnaire */
uint8_t mSatelliteId; /* Identifiant du satellite sur lequel le signal est implanté */
SignalWrapper *mNext;
virtual void setState(const uint16_t inState) = 0;
void lookupMessage();
protected:
uint8_t mSatelliteIndex; /* Index du satellite dans la table */
uint8_t mSlot; /* Slot / connecteur sur lequel le signal est connecté sur le satellite */
public:
static void begin();
static void setSignalState(const int16_t inSignalNumber, const uint16_t inState);
SignalWrapper(const uint16_t inSignalNumber, const uint8_t inSatelliteId, const uint8_t inSlot);
};
Et voici le code de la classe SignalWrapper que l’on trouve dans le fichier SatelliteWrapper.cpp :
/*------------------------------------------------------------
* SignalWrapper class
*-------------------------------------------------------------
*/
SignalWrapper *SignalWrapper::sSignalList = NULL;
SignalWrapper **SignalWrapper::sSignalTable = NULL;
int16_t SignalWrapper::sHigherSignalNumber = -1;
void SignalWrapper::lookupMessage()
{
mSatelliteIndex = lookupMessageForId(mSatelliteId);
if (mSatelliteIndex != NO_MESSAGE_INDEX) {
outSatellitesMessages[mSatelliteIndex].reserve(mSatelliteId);
}
}
void SignalWrapper::setSignalState(
const int16_t inSignalNumber,
const uint16_t inState
)
{
if (inSignalNumber >= 0 && inSignalNumber <= sHigherSignalNumber) {
if (sSignalTable[inSignalNumber] != NULL) {
sSignalTable[inSignalNumber]->setState(inState);
}
}
}
void SignalWrapper::begin()
{
if (sHigherSignalNumber != -1) {
sSignalTable = new SignalWrapper*[sHigherSignalNumber + 1];
if (sSignalTable != NULL) {
for (int16_t i = 0; i <= sHigherSignalNumber; i++) {
sSignalTable[i] = NULL;
}
SignalWrapper *currentSignal = sSignalList;
while (currentSignal != NULL) {
sSignalTable[currentSignal->mSignalNumber] = currentSignal;
/* inscrit l'aiguillage dans la messagerie CAN */
currentSignal->lookupMessage();
currentSignal = currentSignal->mNext;
}
}
}
}
SignalWrapper::SignalWrapper(
const uint16_t inSignalNumber,
const uint8_t inSatelliteId,
const uint8_t inSlot
) :
mSignalNumber(inSignalNumber),
mSatelliteId(inSatelliteId),
mSlot(inSlot)
{
/* Ajoute l'aiguillage dans la liste */
mNext = sSignalList;
sSignalList = this;
/* Note le max des identifiants d'aiguillage dans le gestionnaire */
if (mSignalNumber > sHigherSignalNumber) sHigherSignalNumber = mSignalNumber;
}
Ensuite, il faut définir les classes dérivées de la classe de base SignalWrapper pour les signaux réels utilisés sur notre réseau Locoduinodrome : Sémaphore, Carré ; Sémaphore ralentissement ; Carré rappel de ralentissement.
Au préalable, les 15 types de signaux possibles sont d’abord définis dans un enum dans le fichier Feux.h :
// les feux possibles
enum {
E = 0, // eteint
Vl = bit(0), // 1 ou 0b0000000000000001
A = bit(1), // 2 ou 0b0000000000000010
S = bit(2), // 4 ou 0b0000000000000100
C = bit(3), // 8 ou 0b0000000000001000
R = bit(4), // 16 ou 0b0000000000010000
RR = bit(5), // 32 ou 0b0000000000100000
M = bit(6), // 64 ou 0b0000000001000000
Cv = bit(7), // 128 ou 0b0000000010000000
Vlc = bit(8), // 256 ou 0b0000000100000000
Ac = bit(9), // 512 ou 0b0000001000000000
Sc = bit(10), // 1024 ou 0b0000010000000000
Rc = bit(11), // 2048 ou 0b0000100000000000
RRc = bit(12), // 4096 ou 0b0001000000000000
Mc = bit(13), // 8192 ou 0b0010000000000000
D = bit(14), // 16384 ou 0b0100000000000000
X = bit(15) // 32768 ou 0b1000000000000000
};
typedef uint16_t typeFeux; // 16 cas possibles
La signification des valeurs de feux est indiquée dans la figure suivante, l’indice "c" signifie "clignotant" et le valeur X est réservée pour un usage futur :
Figure 10 : Les différents types de signaux dans l’enum
L’indice "c" signifie "clignotant"
Les classes dérivées suivantes sont codées dans le fichier SatelliteWrapper.cpp.
Classe dérivée pour les sémaphores :
/*
* Wrapper pour les sémaphores : 3 feux, jaune, rouge, vert
*/
class SemaphoreSignalWrapper : public SignalWrapper
{
private:
virtual void setState(const uint16_t inState);
public:
SemaphoreSignalWrapper(const uint16_t inSignalNumber, const uint8_t inSatelliteId, const uint8_t inSlot) :
SignalWrapper(inSignalNumber, inSatelliteId, inSlot) {}
};
/*-----------------------------------------------------------------
* Wrapper pour les sémaphores : 3 feux, jaune, rouge, vert
*
* L'ordre des LED sur le satellite est :
* - jaune
* - rouge
* - vert
*/
void SemaphoreSignalWrapper::setState(const uint16_t inState)
{
if (mSatelliteIndex != NO_MESSAGE_INDEX) {
AbstractCANOutSatelliteMessage &message = outSatellitesMessages[mSatelliteIndex];
message.setLED(mSlot, inState & A ? LED_ON : LED_OFF); /* jaune */
message.setLED(mSlot + 1, inState & S ? LED_ON : LED_OFF); /* rouge */
message.setLED(mSlot + 2, inState & Vl ? LED_ON : LED_OFF); /* vert */
}
}
Classe dérivée pour les carrés :
/*-----------------------------------------------------------------
* Wrapper pour les carrés
*/
class CarreSignalWrapper : public SignalWrapper
{
private:
virtual void setState(const uint16_t inState);
public:
CarreSignalWrapper(const uint16_t inSignalNumber, const uint8_t inSatelliteId, const uint8_t inSlot) :
SignalWrapper(inSignalNumber, inSatelliteId, inSlot) {}
};
/*-----------------------------------------------------------------
* Wrapper pour les carrés
*
* L'ordre des LED sur le satellite est :
* - jaune
* - rouge
* - vert
* - rouge2
* - oeilleton (blanc)
*/
void CarreSignalWrapper::setState(const uint16_t inState)
{
if (mSatelliteIndex != NO_MESSAGE_INDEX) {
AbstractCANOutSatelliteMessage &message = outSatellitesMessages[mSatelliteIndex];
message.setLED(mSlot, inState & A ? LED_ON : LED_OFF); /* jaune */
message.setLED(mSlot + 1, inState & S ? LED_ON : inState & C ? LED_ON : LED_OFF); /* rouge */
message.setLED(mSlot + 2, inState & Vl ? LED_ON : LED_OFF); /* vert */
message.setLED(mSlot + 3, inState & C ? LED_ON : LED_OFF); /* rouge2 */
message.setLED(mSlot + 4, inState & C ? LED_OFF : inState ? LED_ON : LED_OFF); /* oeilleton */
}
}
Classe dérivée pour les sémaphores avec ralentissement :
/*-----------------------------------------------------------------
* Wrapper pour les semaphores avec ralentissement
*/
class SemaphoreRalentissementSignalWrapper : public SignalWrapper
{
private:
virtual void setState(const uint16_t inState);
public:
SemaphoreRalentissementSignalWrapper(const uint16_t inSignalNumber, const uint8_t inSatelliteId, const uint8_t inSlot) :
SignalWrapper(inSignalNumber, inSatelliteId, inSlot) {}
};
/*-----------------------------------------------------------------
* Wrapper pour les semaphores avec ralentissement
*
* L'ordre des LED sur le satellite est :
* - jaune
* - rouge
* - vert
* - jaune2
* - jaune3
*/
void SemaphoreRalentissementSignalWrapper::setState(const uint16_t inState)
{
if (mSatelliteIndex != NO_MESSAGE_INDEX) {
AbstractCANOutSatelliteMessage &message = outSatellitesMessages[mSatelliteIndex];
message.setLED(mSlot, inState & A ? LED_ON : LED_OFF); /* jaune */
message.setLED(mSlot + 1, inState & S ? LED_ON : LED_OFF); /* rouge */
message.setLED(mSlot + 2, inState & Vl ? LED_ON : LED_OFF); /* vert */
message.setLED(mSlot + 3, inState & R ? LED_ON : inState & Rc ? LED_BLINK : LED_OFF); /* jaune2 */
message.setLED(mSlot + 4, inState & R ? LED_ON : inState & Rc ? LED_BLINK : LED_OFF); /* jaune3 */
}
}
Classe dérivée pour les carrés avec rappel de ralentissement :
/*-----------------------------------------------------------------
* Wrapper pour les carres avec rappel ralentissement
*/
class CarreRappelRalentissementSignalWrapper : public SignalWrapper
{
private:
virtual void setState(const uint16_t inState);
public:
CarreRappelRalentissementSignalWrapper(const uint16_t inSignalNumber, const uint8_t inSatelliteId, const uint8_t inSlot) :
SignalWrapper(inSignalNumber, inSatelliteId, inSlot) {}
};
/*-----------------------------------------------------------------
* Wrapper pour les carrés avec rappel ralentissement
*
* L'ordre des LED sur le satellite est :
* - jaune
* - rouge
* - vert
* - rouge2
* - jaune2
* - jaune3
* - oeilleton
*/
void CarreRappelRalentissementSignalWrapper::setState(const uint16_t inState)
{
if (mSatelliteIndex != NO_MESSAGE_INDEX) {
AbstractCANOutSatelliteMessage &message = outSatellitesMessages[mSatelliteIndex];
message.setLED(mSlot, inState & A ? LED_ON : LED_OFF); /* jaune */
message.setLED(mSlot + 1, inState & C ? LED_ON : LED_OFF); /* rouge */
message.setLED(mSlot + 2, inState & Vl ? LED_ON : LED_OFF); /* vert */
message.setLED(mSlot + 3, inState & C ? LED_ON : LED_OFF); /* rouge2 */
message.setLED(mSlot + 4, inState & RR ? LED_ON : inState & RRc ? LED_BLINK : LED_OFF); /* jaune2 */
message.setLED(mSlot + 5, inState & RR ? LED_ON : inState & RRc ? LED_BLINK : LED_OFF); /* jaune3 */
message.setLED(mSlot + 6, inState & C ? LED_OFF : inState ? LED_ON : LED_OFF); /* oeilleton */
}
}
Deux fichiers SatelliteConfig.h et Messaging.h contiennent les constantes de configuration utilisées dans ces classes pointWrapper et signalWrapper, ainsi que dans la classe des messages CAN qui suit :
/*
* Lookup the message table for a satellite Id
*/
static uint8_t lookupMessageForId(const uint8_t inSatelliteId)
{
for (uint8_t messIdx = 0; messIdx < NUMBER_OF_SATELLITES; messIdx++) {
if (outSatellitesMessages[messIdx].satelliteId() == inSatelliteId ||
outSatellitesMessages[messIdx].satelliteId() == NO_SATELLITE_ID)
{
return messIdx;
}
}
return NO_MESSAGE_INDEX; /* overflow */
}
extern MCP_CAN canController;
Le constructeur :
AbstractCANOutSatelliteMessage::AbstractCANOutSatelliteMessage() :
mSatelliteId(NO_SATELLITE_ID)
{
/* Valeurs par défaut pour les commandes */
setPointPosition(false);
for (uint8_t led = 0; led < NUMBER_OF_LED; led++) {
setLED(led, LED_OFF);
}
}
void AbstractCANOutSatelliteMessage::setPointPosition(const bool inPosition)
{
mData[2] &= 0x7F; /* reset the point position */
mData[2] |= (inPosition ? 0x80 : 0x00); /* set the point position in the frame */
}
L’intégration dans votre programme de gestion de réseau
Le dossier "SAM" est téléchargeable sur le Git Locoduino, ici. Il se trouve à l’intérieur du dossier Locoduinodrome.
Ce dossier SAM contient tout ce qu’il faut ajouter à votre programme de gestion de réseau :
le SAM
l’interface CAN qui nécessite la présence de la bibliothèque CAN_BUS_SHIELD téléchargeable également sur le Git Locoduino ici :
les fonctions setup() et loop() déjà prêtes à recevoir votre propre logiciel.
Il vous appartiendra évidemment d’adapter la configuration, qui est ici adaptée à notre Locoduinodrome, à votre propre réseau.
Il convient de laisser ensemble tous les fichiers de ce dossier. Le mieux est d’ajouter toutes les déclarations, fonctions et variables de votre propre gestionnaire à l’intérieur du fichier SAM.ino de ce dossier.
Les autres fichiers sont nécessaires (sauf README.md) à la compilation.
Dans le programme SAM ( le ".ino"), nous aurons successivement :
L’appel de la bibliothèque mcp_can ;
La création de l’objet canController ;
La routine d’interruption sur réception de messages CAN.
#include <mcp_can.h>
#include <mcp_can_dfs.h>
/*
* Interface CAN
*/
const uint8_t spiCS = 53; // 9 sur Nano ou Uno, 53 sur un Mega
MCP_CAN canController(spiCS);
// CAN interrupt routine
volatile bool FlagReceive = false; // can interrupt flag
void MCP2515_ISR() {FlagReceive = true;}
Puis l’appel de l’API des satellites (SAM) qui n’est pas une bibliothèque mais qui est sous forme de fichiers annexes du programme SAM.ino, donc placés dans le même dossier que lui :
/*
* Les satellites
*/
#include "SatelliteWrapper.h"
#include "Feux.h"
/*
* Les aiguillages
*/
PointWrapper pointWrapper0(0, 2); /* aiguillage 0 du gestionnaire sur satellite 2 */
PointWrapper pointWrapper1(1, 6); /* aiguillage 1 du gestionnaire sur satellite 6 */
/*
* Les signaux
*/
SemaphoreSignalWrapper S1wrapper(0, 4, SIGNAL_0); /* signal 0 du gestionnaire sur satellite 4, slot 0 */
SemaphoreSignalWrapper S2wrapper(1, 3, SIGNAL_0); /* signal 1 du gestionnaire sur satellite 3, slot 0 */
CarreSignalWrapper C3wrapper(4, 0, SIGNAL_0); /* signal 4 du gestionnaire sur satellite 0, slot 0 */
CarreSignalWrapper C4wrapper(5, 1, SIGNAL_0); /* signal 5 du gestionnaire sur satellite 1, slot 0 */
CarreSignalWrapper C5wrapper(6, 6, SIGNAL_0); /* signal 6 du gestionnaire sur satellite 6, slot 0 */
CarreSignalWrapper C6wrapper(7, 2, SIGNAL_0); /* signal 7 du gestionnaire sur satellite 2, slot 0 */
CarreRappelRalentissementSignalWrapper C1wrapper(2, 7, SIGNAL_0); /* signal 2 du gestionnaire sur satellite 7, slot 0 */
CarreRappelRalentissementSignalWrapper C2wrapper(3, 5, SIGNAL_0); /* signal 3 du gestionnaire sur satellite 5, slot 0 */
SemaphoreRalentissementSignalWrapper S3wrapper(8, 3, SIGNAL_1); /* signal 8 du gestionnaire sur satellite 3, slot 1 */
SemaphoreRalentissementSignalWrapper S4wrapper(9, 4, SIGNAL_1); /* signal 9 du gestionnaire sur satellite 4, slot 1 */
Le setup()
C’est un exemple bien-sûr que nous vous proposons avec tout ce qu’il faut pour réussir :
L’initialisation complète du bus CAN qui commence par la methode start(), puis la déclaration des filtres que vous devrez adapter à votre réseau, puis la méthode begin() ;
le démarrage de SAM ;
une sortie sur le moniteur de l’IDE de l’état initial des satellites.
void setup()
{
Serial.begin(115200);
// init CAN
canController.start();
/*
* set mask & filters
*/
canController.init_Mask(0, 0, 0x3F0); // there are 2 mask in mcp2515, you need to set both of them
canController.init_Mask(1, 0, 0x3F0); // precisement : Id 0x10 à 0x4F
// filtres du buffer 0
canController.init_Filt(0, 0, 0x10); // Reception possible : Id 10 & 1F (hex) : Satellites
canController.init_Filt(1, 0, 0x40); // Reception possible : Id 4x (hex)
// filtres du buffer 1
canController.init_Filt(2, 0, 0x10); // Reception possible : Id 1x (hex)
canController.init_Filt(3, 0, 0x40); // Reception possible : Id 4x (hex) 40 conduite via centrale DCC
canController.init_Filt(4, 0, 0x43); // Reception possible : Id 4x (hex) 43 keepalive
canController.init_Filt(5, 0, 0x48); // Reception possible : Id 4x (hex) 48 etat DCC
while (CAN_OK != canController.begin(CAN_500KBPS)) // init can bus : baudrate = 500k (carte NiRem a 16 Mhz)
{
Serial.println("CAN BUS Shield init fail");
delay(100);
}
Serial.println("CAN BUS Shield init ok!");
attachInterrupt(0, MCP2515_ISR, FALLING); // start interrupt
PointWrapper::begin();
SignalWrapper::begin();
Serial.println("Initial");
printOutBuffers();
tempo = millis();
}
Nous allons maintenant regarder en détail ce qui se passe dans le loop().
La loop()
Nous avons d’abord le traitement des messages CAN reçus. Ici ils sont simplement affichés sur la console.
unsigned char len = 0;
unsigned char buf[8];
if(FlagReceive) // test si message
{
FlagReceive=0;
if (CAN_MSGAVAIL == canController.checkReceive()) // check if data coming
{
canController.readMsgBuf(&len, buf); // read data, len: data length, buf: data buf
unsigned int canId = canController.getCanId();
Serial.print(" ID: 0x");
Serial.print(canId, HEX);
Serial.print(" data:");
for(int i = 0; i<len; i++) // print the data
{
Serial.print(" 0x");
Serial.print(buf[i], HEX);
}
Serial.println();
}
}
Dans une application de SAM, les messages CAN seront décodés (à l’aide de l’instruction switch par exemple) et provoqueront un appel de fonction qui dépendra du bit d’occupation lu dans le premier octet du message et de l’identifiant du satellite lu dans l’identifiant du message.
Enfin nous avons la tâche périodique d’interrogation des satellites :
sendSatelliteMessage();
Pour la mise au point, il peut être commode d’afficher la liste des bits d’état des satellites, avec la fonction printOutBuffers() qui se trouve dans le fichier SatelliteWrapper.cpp :
printOutBuffers();
Ce qui se traduira par un tableau comme ci-dessous où les lettres correspondant aux DEL A (jaune), S (rouge), Vl (vert), C (rouge), RR (Jaune) seront figurées par :
Ce tableau correspond évidemment à la configuration des satellites décrite juste avant le setup().
A titre d’exemple d’application, si on veut tester, sous forme d’animation, tous les cas possibles de signaux sur tous les satellites, on définit quelques variables globales :
unsigned long tempo;
int nsig = 0;
int sig = 0;
unsigned int feu = 1;
puis une fonction périodique de ce genre qui peut être ajoutée dans la loop() :
if (millis() - tempo > 500)
{
tempo = millis();
Serial.print("signal ");Serial.print(sig);Serial.print(" feu ");Serial.println(feu);
SignalWrapper::setSignalState(sig, feu);
sig++;
if (sig > 9) {
sig = 0;
feu = feu << 1;
if (feu == 0) feu = 1;
}
}
Configuration des satellites
Nous sommes partis du principe que tous les logiciels de satellites sont identiques. Une seule exception dans la version V1 : l’identifiant propre de chaque satellite est gravé en même temps que son code. Mais il pourrait être configurable via le bus CAN.
Par contre, après l’installation des satellites sur le réseau, il y a quelques réglages à faire :
Les valeurs des butées du servomoteur d’aiguille
La vitesse du servo
Les intensités lumineuses des DEL pour un meilleur rendu lumineux car nous savons que les DEL blanches éclairent plus que les DEL rouges qui, elles-mêmes, éclairent plus que les jaunes et les vertes.
Les temps d’allumage et d’extinction lors des changements d’état progressifs pour simuler l’inertie des lampes à incandescence.
Le temps allumé et la période dans le cas des DEL clignotantes.
SAM ne comprend pas d’interface pour assurer ces opérations de configuration qui n’ont lieu qu’une seule fois en principe. Il n’était pas nécessaire d’alourdir SAM de ces fonctions.
Nous avons donc défini un message CAN spécial pour régler individuellement chaque paramètre de chaque satellite. Lorsque le satellite concerné reçoit ce type de message, il agit sur un de ses paramètres et sauvegarde le résultat dans son EEPROM.
Figure 11 : Trame de réglage
La trame de réglage permet de fixer les positions min et max du servo, sa vitesse et les caractéristiques de chaque LED : intensité et, pour le clignotement, la période, le temps où la LED est allumée et le temps nécessaires pour passer d’éteint à allumé et d’allumé à éteint, ceci dans le but de simuler l’inertie thermique des filaments.
Il est caractérisé par :
Un identifiant de 29 bits dont les bits de poids faibles contiennent le numéro de satellite
Un premier octet de donnée dont le poids fort signifie ’0’ = réglage temporaire (sans enregistrement en EEPROM dans le satellite et ’1’ = réglage permanent donc avec enregistrement en EEPROM. Dans les bits de poids faible, on peut choisir ’0’ = un réglage de servo ou ’1’ = un réglage de luminosité de DEL.
Un deuxième octet dont les bits de poids fort définissent le numéro d’objet (servo ou DEL) à configurer et les bits de poids faible le type de valeur à configurer (voir tableau)
de 1 à 4 octets de données pour passer la valeur de configuration dont le type détermine le nombre d’octets.
A titre d’exemple, on pourra réaliser des messages de configuration avec les variables et les fonctions suivantes :
int sMin; // servo min
int sMax; // servo max
union sVitType {float f; unsigned char b[sizeof(float)];} ;
sVitType sVit; // servo vitesse
uint8_t sLum = 255; // intensité
uint8_t sAlt = 255; // temps d'allumage
uint8_t sFad = 255; // temps d'extinction
uint8_t sOnt = 255; // temps allumé
uint8_t sPer = 255; // periode
unsigned char bufS[8]; // les 8 octets du message CAN à envoyer
Fonctions d’envoi d’un message de configuration :
Ces fonctions s’adressent directement à la bibliothèque CAN.
void sendConfig(byte cId, byte cLg)
{
unsigned long longId = 0x1FFFFF20 + cId; // cId est l'identifiant du satellite
canController.sendMsgBuf(longId, 1, cLg, bufS);
}
-1- Réglage des butées de servo, par exemple le coté Max contenu dans une variable sMax codée sur un entier int qu’il faut convertir en 2 octets :
-2- Réglage de la vitesse d’un servo contenue dans une variable sVit codée sur un long qu’il faut convertir en 4 octets à partir d’une déclaration de type Union :
bufS[0]= 0x80; // reglage servo
bufS[1]=2; // a0 vitesse
bufS[2]=sVit.b[3]; // octet 3 de la conversion du long en byte
bufS[3]=sVit.b[2]; // octet 2 de la conversion du long en byte
bufS[4]=sVit.b[1]; // octet 1de la conversion du long en byte
bufS[5]=sVit.b[0]; // octet 0 de la conversion du long en byte
sendConfig(satConfig, 6);
-3- Réglage de la luminosité d’une DEL contenue dans une variable sLum codée sur 1 octet :
bufS[0]= 0x81; // reglage led
bufS[1]= (ledConfig << 3) + 0; // numero de led + intensité
bufS[2]=sLum; // valeur de la luminosité
sendConfig(satConfig, 3);
-4- Réglage de la période de clignotement d’un DEL :
bufS[0]= 0x81; // reglage led
bufS[1]= (ledConfig << 3) + 4; // numero de led + intensité
bufS[2]=sPer; // valeur de la periode
sendConfig(satConfig, 3);
Ceci conclut cet article consacré à la communication avec les Satellites. Le prochain article présentera le logiciel du Satellite V1.