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 :
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.
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.