1

Librairie Universelle Pour la Téléinformation

Pourquoi ?

Travaillant sur différents projets utilisant la téléinformation dans des environnements assez variés (Arduino, Raspberry, Spark Core, ESP8266, …) j’en avais un peu marre de devoir faire des copier/coller de mon code original écrit il y a quelques années pour mon programme teleinfo. De plus chaque fois j’ai eu besoin de m’adapter aux environnements et faire des modifications. je me suis donc retrouvé avec trop de versions. Thibault m’en a remis une couche récemment avec sa propre version pour le programmateur à fils pilotes WIFI que nous utilisons 😉

Fonctions nécessaires

J’ai donc décidé de me lancer dans l’adaptation d’une libraire qui me servira partout et qui se doit d’être un tant soit peu “intelligente” et surtout fonctionnant avec tous les type de contrats EdF sans devoir faire des modifications sauvages d’étiquettes à la volée ou autres.

Cette librairie doit être optimisée et complètement dynamique c’est à dire qu’à aucun moment les valeurs des étiquettes reçues ne sont codées “en dur”. Au fur et à mesure que les données arrivent elles sont mémorisées et un espace mémoire qui leur est alloué dynamiquement. Ensuite les valeurs sont mises à jour au fur et à mesure de la réception. Ceci permet de rendre cette librairie directement compatible avec tous les types de contrats (Heures pleines/creuses, BBR, Tempo, ….)

Elle doit aussi fournir un système de callback afin de vous avertir quand il se passe un événement d’intéressant.

Développement

Format

La librairie est réalisé dans le format classique de librairies de type Arduino, elle fonctionnera donc directement depuis l’IDE de cet environnement pour les Arduino ainsi que pour le super nouveau chip Wifi très en vogue l’ESP8266 grâce à son inclusion dans l’IDE Arduino. Pour les autres cibles (Spark Core, Photon, Raspberry, …) il faudra peut être copier les fichiers ailleurs afin de les intégrer dans chaque environnement. Je reviendrais sur ce point quand j’aurai avancé sur le sujet (intégration dans remora par exemple)

Son code doit être simple est concis afin de pouvoir tourner sur des cible réduites en taille de code et en mémoire.

Mémorisation

Les données de téléinformation reçues sont vérifiées et ensuite stockées dans une liste chaînée. Je ne vais pas faire un cours la dessus, mais c’est un peu comme un tableau mais totalement dynamique. Je vous conseille d’aller voir ce cours dédié si vous êtes intéressés. En tous cas au niveau optimisation y’a pas mieux.

Le format d’une entrée de la liste pour cette librairie est le suivant :

  • Un pointeur sur l’objet suivant (ou NULL si c’est le dernier objet de la liste)
  • la valeur de la checksum de l’étiquette
  • des flags indiquant par exemple si cette étiquette est “nouvelle” (juste reçue et donc créée) ou vient d’être “mise à jour” dans la dernière trame reçue .
  • le nom de l’étiquette (via un pointeur dans l’objet)
  • la valeur de l’étiquette (via un pointeur dans l’objet)

Cette structure est déclarée dans le fichier include LibTeleinfo.h

// Linked list structure containing all values received
typedef struct _ValueList ValueList;
struct _ValueList 
{
  ValueList *next; // next element
  uint8_t checksum;// checksum
  uint8_t flags;   // specific flags
  char  * name;    // LABEL of value name
  char  * value;   // value 
};

Vous avez donc compris que la taille d’un élément dépendra de la longueur de l’étiquette ainsi que de sa valeur. Mais ne vous inquiétez pas, vous n’aurez pas à vous soucier de la gestion cette liste de manière générale, la librairie se charge de tout, ce qui vous intéresse c’est de pouvoir naviguer dans ses éléments.

Fonctions

La puissance de cette librairie provient de la possibilité de lui définir des callback, c’est à dire d’appeler vos propres fonctions à réception d’événements. J’ai défini les 4 événements suivants:

  • Réception d’un signal ADPS : dépassement de consommation de contrat
  • Réception d’une ligne d’étiquette : une étiquette+valeur+checksum ok vient d’être reçue (ex PAPP=250)
  • Trame modifiée :  une trame complète a été reçue (EOT) mais des données de cette trame ont été modifiées depuis la dernière trame reçue.
  • Trame identique : une trame complète a été reçue(EOT)  mais elle est identique à la dernière  trame reçue (généralement on ne fait rien).

Lors de l’appel de ces callback (qui sont les vôtres je le rappelle) par la librairie vous recevrez en paramètre les informations nécessaires, par exemple pour ADPS ce sera le numéro de la phase comme suit :

  • 0 : Phase unique (Monophasé)
  • 1 : Phase 1 si triphasé
  • 2 : Phase 2 si triphasé
  • 3 : Phase 2 si triphasé

Les déclarations des callback sont faites dans le fichier include LibTeleinfo.h

  public:
    void        attachADPS(void (*_fn_ADPS)(uint8_t phase));  
    void        attachData(void (*_fn_data)(ValueList * valueslist, uint8_t flags));  
    void        attachNewFrame(void (*_fn_new_frame)(ValueList * valueslist));  
    void        attachUpdatedFrame(void (*_fn_updated_frame)(ValueList * valueslist));  

  private:
    void      (*_fn_ADPS)(uint8_t phase);
    void      (*_fn_data)(ValueList * valueslist, uint8_t flags);
    void      (*_fn_new_frame)(ValueList * valueslist);
    void      (*_fn_updated_frame)(ValueList * valueslist);

Utilisation

Connexions

Dans les explications suivantes, je vais utiliser un Arduino. Dans mon cas (ou si votre device ne possède qu’un seul port série) j’utilise une instance softserial afin de ne pas utiliser la vraie liaison série (TX/RX) que je garde pour le debug à 115200. D’où la connexion sur D3 (mais vous pouvez la changer). La téléinfo est donc reçue sur cette patte D3 qui recevra le signal RDX du schéma suivant :

Montage de base

RXD sur la patte D3 de l’arduino

Pour des informations détaillées et des explications complètes sur ce schéma vous pouvez vous référer à cet article dédié

Attention : La librairie softserial fonctionne parfaitement, mais est très gourmande en ressources d’interruption et elle perturbe très fortement les fonctions système telles que millis(), donc ne vous attendez pas à un fonctionnement normal de millis() mais plutôt à un fonctionnement retardé. J’avais mis un ticker toutes les secondes, et, parfois çà me mettait bien 1s, et d’autres, je devais attendre parfois jusqu’a 4s avant de voir mon ticker incrémenté !!!

Ce n’est pas super gênant, mais gardez le en mémoire si besoin, çà vous  évitera de vous prendre la tête quand çà ne fonctionnera pas comme vous le pensez.

Exemple

Pour utiliser la libraire dans votre programme c’est assez simple il faut inclure les fichiers de la libraire comme n’importe quelle autre libraire, l’instancier, l’initialiser et enfin l’appeler dans votre main loop.

Donc un programme minimal pourrait ressembler à cela :

#include <SoftwareSerial.h>
#include <LibTeleinfo.h>

SoftwareSerial Serial1(3,4);
TInfo tinfo; 

void setup()
{
  // Configure Teleinfo Soft serial 
  Serial1.begin(1200);

  // Init teleinfo
  tinfo.init();
}

void loop()
{
  if ( Serial1.available() )
    tinfo.process(Serial1.read());
}

On déclare les fichiers d’inclusion, on instancie la librairie, on configure softserial de nom Serial1 sur D3/RX (D4 TX ne sera pas utilisé). Dans l’init on initialise la librairie et enfin le main loop.

Le main loop regarde si un caractère est présent sur la liaison série préalablement déclarée et l’envoi à la libraire téléinfo pour son traitement. Je n’ai volontairement pas inclus l’objet série dans la librairie pour un portage plus facile. En effet, sur un Raspberry Pi tournant sous linux je suis pas certain qu’on puisse avoir un objet “Serial”. Donc pour se prémunir de tout problème, on passe juste les caractères reçus 1 à 1 à la librairie, et elle se débrouille toute seule. Ce n’est même pas obligé d’être synchrone.

Mais que fait alors ce programme ? et bien pour vous rien, il s’occupe juste de la gestion de réception/contrôle des données puis stocke et met à jour la liste chaînée. Voilà un squelette auquel tout programme utilisant cette librairie doit ressembler.

Affichage à réception d’étiquette

L’exemple précédent était juste une mise en bouche, on passe la vitesse supérieure, et nous allons utiliser une callback événementielle pour afficher les valeurs reçues. Cette callback est appelé à chaque réception une ligne d’étiquette valide (checksum OK), soit par exemple :

PAPP=00140 &

Pour réaliser cela, c’est très simple il faut commencer par créer une fonction qui va réaliser le traitement souhaité (ici juste l’affichage), cette fonction reçoit de la librairie un pointeur sur l’objet de la liste chaînée correspondant ainsi que des flags indiquant ce qu’il s’est passé :

void DataCallback(ValueList * me, uint8_t  flags)
{
  printUptime();

  if (flags & TINFO_FLAGS_ADDED) 
    Serial.print(F("NEW -> "));

  if (flags & TINFO_FLAGS_UPDATED)
    Serial.print(F("MAJ -> "));

  // Display values
  Serial.print(me->name);
  Serial.print("=");
  Serial.println(me->value);
}

Cette fonction affiche un pseudo compteur de seconde. Puis si la donnée reçue est nouvelle ou mise à jour grâce au flag fourni en paramètre . Et enfin elle affiche l’étiquette ainsi que sa valeur.

Bien entendu il faut dire à la librairie d’affecter notre callback à la fonction précédemment créée, ce qui rajoute à notre exemple précédent une ligne de code dans le setup()   tinfo.attachData(DataCallback);

Ce qui donne :

#include <SoftwareSerial.h>
#include <LibTeleinfo.h>

SoftwareSerial Serial1(3,4);
TInfo tinfo; 

void DataCallback(ValueList * me, uint8_t  flags)
{
  // compteur de secondes basique
  Serial.print(millis()/1000);
  Serial.print(F("\t"));

  if (flags & TINFO_FLAGS_ADDED) 
    Serial.print(F("NEW -> "));

  if (flags & TINFO_FLAGS_UPDATED)
    Serial.print(F("MAJ -> "));

  // Display values
  Serial.print(me->name);
  Serial.print("=");
  Serial.println(me->value);
}

void setup()
{
  // Configure Teleinfo Soft serial 
  Serial1.begin(1200);

  // Init teleinfo
  tinfo.init();

  // Attacher la callback dont nous avons besoin
  tinfo.attachData(DataCallback);
}

void loop()
{
  if ( Serial1.available() )
    tinfo.process(Serial1.read());
}

La sortie finale sur la serial nous donne :

========================================
Arduino_Softserial_Etiquette.ino
Jul 19 2015 17:26:24
 
Teleinfo started
0	NEW -> ADCO=031428067147
0	NEW -> OPTARIF=HC..
0	NEW -> ISOUSC=15
0	NEW -> HCHC=000246679
0	NEW -> HCHP=000000000
0	NEW -> PTEC=HC..
0	NEW -> IINST=001
0	NEW -> IMAX=001
0	NEW -> PAPP=00140
0	NEW -> HHPHC=A
0	NEW -> MOTDETAT=000000
3	MAJ -> HCHC=000246680
11	MAJ -> PAPP=00170
12	MAJ -> PAPP=00160
12	MAJ -> PAPP=00170
13	MAJ -> HCHC=000246681
13	MAJ -> PAPP=00140
17	MAJ -> PAPP=00150
18	MAJ -> PAPP=00140
23	MAJ -> HCHC=000246682
23	MAJ -> PAPP=00150
24	MAJ -> PAPP=00140

Clignotement à réception de trame

L’exemple précédent montrait l’utilisation à réception des lignes d’étiquettes, ce n’est pas ce que je recommande, je préconise plus un traitement à réception d’une trame complète. Voici comment utiliser les callback adéquates. Dans cet exemple nous reprendrons juste les fonctions de l’exemple précédent sur lequel nous allons :

  • ajouter un clignotement court sur réception d’une trame identique à la précédente
  • ajouter un clignotement long pour une trame dont les données on été modifiées
  • afficher le signal ADPS si présent

on ajoute donc nos 3 callbacks :

/* ======================================================================
Function: ADPSCallback 
Purpose : called by library when we detected a ADPS on any phased
Input   : phase number 
            0 for ADPS (monophase)
            1 for ADIR1 triphase
            2 for ADIR2 triphase
            3 for ADIR3 triphase
Output  : - 
Comments: should have been initialised in the main sketch with a
          tinfo.attachADPSCallback(ADPSCallback())
====================================================================== */
void ADPSCallback(uint8_t phase)
{
  printUptime();

  // Monophasé
  if (phase == 0 ) {
    Serial.println(F("ADPS"));
  }
  else {
    Serial.print(F("ADPS PHASE #"));
    Serial.println('0' + phase);
  }
}

/* ======================================================================
Function: NewFrame 
Purpose : callback when we received a complete teleinfo frame
Input   : linked list pointer on the concerned data
Output  : - 
Comments: -
====================================================================== */
void NewFrame(ValueList * me)
{
  // Start short led blink
  digitalWrite(LEDPIN, HIGH);
  blinkLed = millis();
  blinkDelay = 50; // 50ms

  // Show our not accurate second counter
  printUptime();
  Serial.println(F("FRAME -> SAME AS PREVIOUS"));
}

/* ======================================================================
Function: NewFrame 
Purpose : callback when we received a complete teleinfo frame
Input   : linked list pointer on the concerned data
Output  : - 
Comments: it's called only if one data in the frame is different than
          the previous frame
====================================================================== */
void UpdatedFrame(ValueList * me)
{
  // Start long led blink
  digitalWrite(LEDPIN, HIGH);
  blinkLed = millis();
  blinkDelay = 100; // 100ms

  // Show our not accurate second counter
  printUptime();
  Serial.println(F("FRAME -> UPDATED"));
}

Puis on les déclare dans le setup pour les “attacher”

  // Attacher les callback dont nous avons besoin
  // pour cette demo, toutes
  tinfo.attachADPS(ADPSCallback);
  tinfo.attachData(DataCallback);
  tinfo.attachNewFrame(NewFrame);
  tinfo.attachUpdatedFrame(UpdatedFrame);

Et enfin J’ai modifié le main avec un compteur de millis() pour le clignotement de la led

// Pour clignotement LED asynchrone
unsigned long blinkLed  = 0;
uint8_t       blinkDelay= 0;

/* ======================================================================
Function: loop
Purpose : infinite loop main code
Input   : -
Output  : - 
Comments: -
====================================================================== */
void loop()
{
  // On a reçu un caractère ?
  if ( Serial1.available() )
    tinfo.process(Serial1.read());

  // Verifier si le clignotement LED doit s'arreter 
  if (blinkLed && ((millis()-blinkLed) >= blinkDelay))
  {
    digitalWrite(LEDPIN, LOW);
    blinkLed = 0;
  }
}

Et la sortie Serie nous donne maintenant :

========================================
Arduino_Softserial_Blink.ino
Jul 19 2015 19:29:08

0s	Teleinfo started
0s	NEW -> ADCO=031428067147
0s	NEW -> OPTARIF=HC..
0s	NEW -> ISOUSC=15
0s	NEW -> HCHC=000246924
0s	NEW -> HCHP=000000000
0s	NEW -> PTEC=HC..
0s	NEW -> IINST=001
0s	NEW -> IMAX=001
1s	NEW -> PAPP=00140
1s	NEW -> HHPHC=A
1s	NEW -> MOTDETAT=000000
1s	FRAME -> UPDATED
1s	FRAME -> SAME AS PREVIOUS
2s	FRAME -> SAME AS PREVIOUS
2s	FRAME -> SAME AS PREVIOUS
2s	FRAME -> SAME AS PREVIOUS
3s	FRAME -> SAME AS PREVIOUS
3s	FRAME -> SAME AS PREVIOUS
4s	FRAME -> SAME AS PREVIOUS
4s	FRAME -> SAME AS PREVIOUS
5s	FRAME -> SAME AS PREVIOUS
5s	MAJ -> PAPP=00150
5s	FRAME -> UPDATED
6s	MAJ -> PAPP=00170
6s	FRAME -> UPDATED
6s	FRAME -> SAME AS PREVIOUS
6s	MAJ -> HCHC=000246925
6s	MAJ -> PAPP=00140
7s	FRAME -> UPDATED
7s	FRAME -> SAME AS PREVIOUS
7s	FRAME -> SAME AS PREVIOUS
8s	FRAME -> SAME AS PREVIOUS
8s	FRAME -> SAME AS PREVIOUS
9s	FRAME -> SAME AS PREVIOUS

Nous avons vu comment fonctionnent les callback, maintenant je vais vous montrer comment naviguer dans la liste chaînée afin de récupérer les données en réception de trame.

Envoi des données modifiés au format JSON

Pour ce faire, nous allons envoyer sur la serial les données au format JSON (oui oui, une lubie totalement arbitraire, fruit du hasard, et ce n’est absolument pas pour les faire digérer par node-red comme par exemple ici).

je ne vais pas détailler ce qui a déjà été vu mais me focaliser sur ce qui est nouveau :

  • Ajout d’un compteur de secondes avec le timer1 pour pallier au shift indroduit par sofwareserial sur millis();
  • A chaque trame modifiée, envoi sur la Serial en JSON uniquement les valeurs modifiées et non pas toute la trame
  • Transformer les données de type numérique en numérique JSON (enlever les 0 inutiles et les guillemets dans le format JSON pour indiquer à la cible que ce sont des valeurs numériques)
  • toute les minutes, envoi d’une trame complète (toutes les valeurs) avec en plus une donnée _UPTIME pour le fun.

Je préfixe les données “maison” par _ pour un traitement ultérieur afin de les différencier des véritables données reçues par la Téléinfo.

Les callback de l’exemple précédent on été modifiées pour appeler une fonction qui va se charger de la transformation en JSON des données de la liste chaînée (sauf sur ADPS ou on le fait en direct). Une variable globale fulldata  sera positionnée par le main loop toutes les 60 secondes pour indiquer que le prochain envoi sera de type “trame complète”.

/* ======================================================================
Function: ADPSCallback 
Purpose : called by library when we detected a ADPS on any phased
Input   : phase number 
            0 for ADPS (monophase)
            1 for ADIR1 triphase
            2 for ADIR2 triphase
            3 for ADIR3 triphase
Output  : - 
Comments: should have been initialised in the main sketch with a
          tinfo.attachADPSCallback(ADPSCallback())
====================================================================== */
void ADPSCallback(uint8_t phase)
{
  // Envoyer JSON { "ADPS"; n}
  // n = numero de la phase 1 à 3
  if (phase == 0)
    phase = 1;
  Serial.print(F("{\"ADPS\":"));
  Serial.print('0' + phase);
  Serial.println(F("}"));
}

/* ======================================================================
Function: NewFrame 
Purpose : callback when we received a complete teleinfo frame
Input   : linked list pointer on the concerned data
Output  : - 
Comments: -
====================================================================== */
void NewFrame(ValueList * me)
{
  // Start short led blink
  digitalWrite(LEDPIN, HIGH);
  blinkLed = millis();
  blinkDelay = 50; // 50ms

  // Envoyer les valeurs uniquement si demandé
  if (fulldata) 
    sendJSON(me, true);

  fulldata = false;
}

/* ======================================================================
Function: UpdatedFrame 
Purpose : callback when we received a complete teleinfo frame
Input   : linked list pointer on the concerned data
Output  : - 
Comments: it's called only if one data in the frame is different than
          the previous frame
====================================================================== */
void UpdatedFrame(ValueList * me)
{
  bool firstdata = true;

  // Start long led blink
  digitalWrite(LEDPIN, HIGH);
  blinkLed = millis();
  blinkDelay = 50; // 50ms

  // Envoyer les valeurs 
  sendJSON(me, fulldata);
  fulldata = false;
}

Rien de bien sorcier donc, voyons maintenant la fonction sendJSON qui navigue dans la liste chaînée.  Comme nous recevons un pointeur sur notre liste chaînée, nous allons naviguer dans tous les éléments, et seuls ceux étant indiqués comme “modifiés” seront envoyés.

Si le paramètre d’entré all  vaut true , alors TOUS les élément seront envoyés.

/* ======================================================================
Function: sendJSON 
Purpose : dump teleinfo values on serial
Input   : linked list pointer on the concerned data
          true to dump all values, false for only modified ones
Output  : - 
Comments: -
====================================================================== */
void sendJSON(ValueList * me, boolean all)
{
  bool firstdata = true;

  // Got at least one ?
  if (me) {
    // Json start
    Serial.print(F("{"));

    if (all) {
      Serial.print(F("\"_UPTIME\":"));
      Serial.print(uptime, DEC);
      firstdata = false;
    }

    // Loop thru the node
    while (me->next) {
      // go to next node
      me = me->next;

      // uniquement sur les nouvelles valeurs ou celles modifiées 
      // sauf si explicitement demandé toutes
      if ( all || ( me->flags & (TINFO_FLAGS_UPDATED | TINFO_FLAGS_ADDED) ) )
      {
        // First elemement, no comma
        if (firstdata)
          firstdata = false;
        else
          Serial.print(F(", ")) ;

        Serial.print(F("\"")) ;
        Serial.print(me->name) ;
        Serial.print(F("\":")) ;

        // we have at least something ?
        if (me->value && strlen(me->value))
        {
          boolean isNumber = true;
          uint8_t c;
          char * p = me->value;

          // check if value is number
          while (*p && isNumber) {
            if ( *p < '0' || *p > '9' )
              isNumber = false;
            p++;
          }
  
          // this will add "" on not number values
          if (!isNumber) {
            Serial.print(F("\"")) ;
            Serial.print(me->value) ;
            Serial.print(F("\"")) ;
          }
          // this will remove leading zero on numbers
          else
            Serial.print(atol(me->value));
        }
      }
    }
   // Json end
   Serial.println(F("}")) ;
  }
}

Et enfin ce que donne la sortie série, que du JSON avec bien les _UPTIME toutes les 60s et la trame complète.

{"_UPTIME":3, "ADCO":1363296075, "OPTARIF":"HC..", "ISOUSC":15, "HCHC":247013, "HCHP":0, "PTEC":"HC..", "IINST":1, "IMAX":1, "PAPP":140, "HHPHC":"A", "MOTDETAT":0}
{"PAPP":150}
{"PAPP":140}
{"PAPP":150}
{"PAPP":140}
{"PAPP":150}
{"PAPP":140}
{"HCHC":247014}
{"PAPP":150}
{"PAPP":140}
{"_UPTIME":60, "ADCO":1363296075, "OPTARIF":"HC..", "ISOUSC":15, "HCHC":247014, "HCHP":0, "PTEC":"HC..", "IINST":1, "IMAX":1, "PAPP":140, "HHPHC":"A", "MOTDETAT":0}
{"HCHC":247015, "PAPP":160}
{"PAPP":150}
{"PAPP":160}
{"PAPP":150}
{"PAPP":160}
{"PAPP":150}
{"PAPP":140}
{"PAPP":160}
{"PAPP":150}
{"PAPP":140}
{"HCHC":247016, "PAPP":150}
{"PAPP":140}
{"PAPP":160}
{"PAPP":150}
{"PAPP":140}
{"_UPTIME":121, "ADCO":1363296075, "OPTARIF":"HC..", "ISOUSC":15, "HCHC":247016, "HCHP":0, "PTEC":"HC..", "IINST":1, "IMAX":1, "PAPP":140, "HHPHC":"A", "MOTDETAT":0}
{"PAPP":150}
{"PAPP":160}
{"PAPP":150}
{"PAPP":160}
{"HCHC":247017, "PAPP":150}
{"PAPP":160}
{"PAPP":150}

Conclusion

Voilà pour une présentation rapide de la librairie, je vous invite à aller voir sur le repo github dédié, j’ai posté pas mal d’exemples bien documentés donc vous devriez y trouver toutes les informations nécessaires.

J’ai même fait un exemple tournant sur Raspberry Pi avec un dongle Micro Teleinfo ou une carte ArduiPi.

Ensuite ?

Ensuite, et bien c’est assez simple, je travaille actuellement sur un module téléinfo Wifi à base d’ESP8266, tout le hard est validé sur breadboard et j’ai bien avancé au niveau code. Il reste pas mal de boulot, genre Bootstrap, AJAX/JQuery. D’ailleurs si une personne est motivée et maîtrise parfaitement ces technologies, je suis preneur car je suis un peu charrette en ce moment.

Je vais lancer la 1ere série de PCB pour avoir çà d’ici la fin de l’été (et de mes vacances), mais comme vous êtes arrivé en fin d’article pour vous récompenser de votre lecture assidue, voici comment se présente l’interface vue depuis le navigateur Internet de cette carte nommé Wifinfo.

Wifinfo Information Systeme

Wifinfo Information Systeme

Wifinfo Données de Téléinformation

Wifinfo Données de Téléinformation

Je vais publier les schémas de cette carte très rapidement 😉

Réferences

  • le repo github contenant la librairie ainsi que les exemples
  • la spécification ERDF concernant la téléinformation
  • tous les articles connexes de mon blog
  • J’ai créé une catégorie dédiée à cette librairie sur la communauté téléinfo pour toute question relative à son support ou son utilisation.

Charles

You can discuss about this article or other project using the community forum