Projet station météo ESP8266 (Partie 4). ArduinoJson, charger, enregistrer des fichiers (SPIFFS) • Domotique et objets connectés à faire soi-même

Dans cette 4ème partie du projet de station météo connectée, nous allons aborder le stockage des fichiers dans le système de fichier SPIFFS. Les mesures (température, humidité et pression atmosphérique) seront enregistrées au format JSON à l’aide de la librairie ArduinoJson dans un fichier stocké dans la zone de fichier SPIFFS de l’ESP8266.

Les données ainsi enregistrées serviront à alimenter un historique. Les mesures seront recharger au démarrage de l’ESP8266 pour poursuivre l’acquisition de données.

Petits rappels sur le format JSON avant de commencer

JSON est un format qui permet de structurer les données pour le stockage et le transfert. Il remplace très avantageusement le format XML dans le langage Javascript (entre autre) ou il est supporté nativement. Il est également très simple de créer une structure JSON dans le code Arduino.

Pour tout savoir sur le format JSON, je vous conseille de commencer par lire ce tutoriel détaillé

Sans trop rentrer dans les détails, voici comment ça fonctionne :

  • Les données sont représentées par des paires clé:valeur
  • Le signe deux-points  :  sépare la clé de la valeur
  • Le signe guillemet  ”  permet d’encadrer une chaîne de caractères
  • les paires clé:valeur sont séparées par des virgules  ,  (sauf la dernière ligne)
  • Les accolades  {}  contiennent des objets. On peut y stocker ce que l’on souhaite à condition de respecter le formaliste JSON ci-dessus
  • Les crochets  []  contiennent des tableaux

Ce qui donne par exemple

{
  "cle1":"valeur1",
  "cle2":"[1,2,3,4]",
  "cle3":{
    "clea":"valeura",
    "cleb":"['a','b','c']"
  }
}

Ou un tableau

[{
    "temperature": "18",
    "humidite": "52"
}, {
    "temperature": "19",
    "humidite": "52"
}]

Dernière remarque, le format JSON n’est rien d’autre qu’une chaîne de caractère, ce qui donne

[{"temperature":"18","humidite":"52"},{"temperature":"19","humidite":"52" }]

Super pratique pour envoyer des données ou des commandes à l’aide de messages MQTT ou Node-RED via le port série ! D’ailleurs, si le sujet vous intéresse, voici un projet complet

Pour tester vos JSON, je vous conseille jsonlint disponible en ligne et largement suffisant.

Installer la librairie ArduinoJson

Il est possible de créer une structure pour gérer les données mais le format JSON (déjà expliqué dans le tutoriel précédent) offre de nombreux avantages. La librairie ArduinoJson développée par Benoît Blanchon est un modèle du genre. Très bien documentée, elle est très simple de mise en oeuvre.

Seul bémol, manipuler des objets JSON n’est pas sans conséquences sur le programme Arduino. En effet, l’objet JSON est stocké en mémoire, ce qui limite grandement nos ambitions. Si vous avez besoin de créez un système d’acquisition de données, cette librairie n’est pas la mieux adaptée. Dans le cadre de ce projet, elle est très intéressante car il est très facile de manipuler les données et les envoyer vers l’interface Web (juste en 2 lignes de code !).

Pour tout savoir sur la version 6 de la librairie ArduinoJSON, je vous conseille de commencer par lire ce tutoriel détaillé

Attention, le code a été développé avec la version 5.6.2 de la librairie ArduinoJson. Il n’est pas compatible avec la version 6.

Pour installer la librairie ArduinoJson, allez tout simplement dans le gestionnaire de bibliothèque.

Préparer un buffer et l’objet JSON

Ensuite, on ajoute une déclaration en début de programme

#include 

La librairie ArduinoJson est très bien documentée (le wiki sur trouve sur GitHub). Je vous propose un exemple d’utilisation typique dans lequel nous allons réaliser les manipulations suivantes :

  • Création d’un objet JSON
  • Ajout de clés : timestamp, t (température), h (humidité), pa (pression atmosphérique), bart et barh (données classifiées pour construire un histogramme en barre)
  • Exporter et enregistrer le JSON dans la zone SPIFFS
  • Recharger le fichier de données depuis la zone SPIFFS et recréer l’objet JSON correspondant

On commence donc par créer un Buffer qui va contenir l’objet JSON. Il est possible de laisser la librairie changer dynamiquement la taille du buffer, mais les performances en seront affectée. Pour déterminer la taille du buffer, reportez vous à cette page (section Memory Usage). Estimons la taille pour ce projet. On va enregistrer 12 mesures durant 7 heures, soit 84 points par grandeur physique.

  • 4 tableaux de 84 éléments (temps, température, pression atmosphérique) = 4 x ( 8 + 12 x 84 ) = 4064 bytes
  • 2 tableaux de 7 éléments (pour histogramme en barre) = 2 x ( 8 + 12 x 7 ) = 184 bytes

Soit un total de 4248 bytes. Par sécurité, on va arrondir à 4400 bytes. S’il manque un peu de place, tous les points de mesure ne pourront pas être enregistrés.

StaticJsonBuffer jsonBuffer;

Maintenant, on créé l’objet JSON root  dans ce buffer

JsonObject& root = jsonBuffer.createObject();

Puis on créé les clés qui vont contenir les tableaux de données.On a le choix entre ajouter un tableau (createNestedArray) ou un objet (createNestedObject). Ici, on ajoute 5 tableaux. La taille des tableaux n’est pas fixée d’avance, à nous à calculer l’espace nécessaire au moment de la création du buffer. S’il n’y a plus de place dans le buffer, il ne sera plus possible d’ajouter des données supplémentaires.

JsonArray& timestamp = root.createNestedArray("timestamp");
JsonArray& hist_t = root.createNestedArray("t");
JsonArray& hist_h = root.createNestedArray("h");
JsonArray& hist_pa = root.createNestedArray("pa");
JsonArray& bart = root.createNestedArray("bart");  
JsonArray& barh = root.createNestedArray("barh");

On en profite également pour créer un buffer qui servira aux exportations. Si on doit exporter les données (ce qui est généralement le cas), il faudra donc diviser la mémoire disponible en deux, la première partie pour le JSON, la seconde pour le tampon d’exportation.

char json[44000];

Manipuler un JSON dans du code Arduino. Ajout, modification, suppression de données

Maintenant que l’objet JSON est prêt, on va pouvoir ajouter des points régulièrement. Ici, on va stocker dans le JSON un point toutes les 5 minutes. On dispose de plusieurs fonctions pour manipuler les données dans le JSON :

  • add(data) : ajoute une nouvelle données à la suite. On pousse une nouvelle valeur dans un tableau par exemple.
  • removeAt(index) : on supprime la données à l’index indiqué.
  • set(index,data) : on actualise la données à l’index indiqué.

La fonction addPtToHist suivante permet d’ajouter un point de mesure pour chaque grandeur à intervalle régulier (ici intervalHist = 5000 ms). Le temps est récupéré sur le serveur NTP à l’aide de la méthode NTP.getTime() présentée dans le tutoriel précédent. On limite le nombre de chiffres significatifs pour les nombres décimaux à l’aide de la méthode double_with_n_digits(variable, nombre_chiffre_significatif).

Enfin, comme on est obligé de limiter le nombre de mesures enregistrées (ici 84 par variable), on doit supprimer la plus ancienne. Pour cela il suffit de tester la taille du tableau, si elle est supérieure à la taille définie, on supprime le premier élément de chaque tableau à l’aide de la méthode removeAt(index). Il est ainsi très facile de mettre en place un buffer tournant.

void addPtToHist(){
  unsigned long currentMillis = millis();
  if ( currentMillis - previousMillis > intervalHist ) {
    previousMillis = currentMillis;
    timestamp.add(NTP.getTime());
    hist_t.add(double_with_n_digits(t, 1));
    hist_h.add(double_with_n_digits(h, 1));
    hist_pa.add(double_with_n_digits(pa, 1));
    if ( hist_t.size() > sizeHist ) {
      //Serial.println("efface anciennes mesures");
      timestamp.removeAt(0);
      hist_t.removeAt(0);
      hist_h.removeAt(0);
      hist_pa.removeAt(0);
    } 
    saveHistory();
  }
}

D’autres fonctions utiles :

  • size() Retourne la taille d’un tableau (tableau.size())
  • success() Retourne vrai si un tableau a été correctement parsé (converti d’une chaine en objet JSON) ou alloué

Comment exporter l’objet JSON et l’enregistrer dans la zone SPIFFS de l’ESP8266

Vous avez du remarquer l’appel à la fonction saveHistory() dans la fonction précédente. Voyons comment faire pour exporter un objet JSON et l’enregistrer dans un simple fichier texte dans l’espace SPIFFS.

Petits rappels :

  • On appel la librairie FS.h en début de programme
  • Pour pouvoir accéder aux fichiers stockés dans la zone SPIFFS, il faut initialiser la librairie en appelant la fonction SPIFFS.begin() dans le setup().

Maintenant, on peut accéder au système de fichier SPIFFS.  Exporter le JSON dans un fichier texte, c’est trois lignes de code avec les librairies ArduinoJson et FS !.

  • On ouvre le fichier avec la méthode SPIFFS.open(nom_fichier, option). Ici on va accéder au fichier en écriture w. Si le fichier n’existe pas, il sera créé.
  • La méthode printTo permet de sérialiser(serialize), c’est à dire de transformer l’objet JSON en une chaine de caractère. On indique la sortie de la fonction, ici le fichier ouvert (historyFile).
  • On referme le fichier (close).
  • C’est terminé.
void saveHistory(){          
  File historyFile = SPIFFS.open(HISTORY_FILE, "w");
  root.printTo(historyFile); // Exporte et enregistre le JSON dans la zone SPIFFS - Export and save JSON object to SPIFFS area
  historyFile.close();  
}

La méthode printTo est très pratique, elle permet donc de sérialiser le JSON et de l’envoyer aussi bien vers un fichier que vers le port série pour la mise au point du programme. Il existe également la variante prettyPrintTo qui permet de rendre plus lisible (pour un humain !) l’objet JSON comme le fait http://jsonlint.com/.

Importer et parser un fichier texte en JSON (historique de mesure)

On va recharger l’historique sauvegardé précédemment au démarrage de l’ESP8266 et poursuivre l’acquisition de données. Pour parser une chaine de caractères en objet JSON, on doit disposer d’un buffer dont la taille correspond à l’objet JSON final. Comme on va recharger l’historique courant, on dispose déjà du buffer root. Inutile d’en créer un nouveau. On ouvre le fichier avec la méthode SPIFFS.open mais cette fois en lecture uniquement (paramètre r). Avant de faire la conversion, il est préférable de tester si le fichier n’est pas vide (file.size()).

On charge le contenu du fichier dans un buffer (source) temporaire (buf). On appel ensuite la méthode jsonBuffer.paseObject en lui passant en paramètre le contenu du buffer (buf.get()).

La méthode success permet de vérifier que l’objet JSON est correct.

void loadHistory(){
  File file = SPIFFS.open(HISTORY_FILE, "r");
  if (!file){
    Serial.println("Aucun historique existe - No History Exist");
  } else {
    size_t size = file.size();
    if ( size == 0 ) {
      Serial.println("Fichier historique vide - History file empty !");
    } else {
      std::unique_ptr buf (new char[size]);
      file.readBytes(buf.get(), size);
      JsonObject& root = jsonBuffer.parseObject(buf.get());
      if (!root.success()) {
        Serial.println("Impossible de lire le JSON - Impossible to read JSON file");
      } else {
        Serial.println("Historique charge - History loaded");
        root.printTo(Serial);  
      }
    }
    file.close();
  }
}

Dans le prochain tutoriel nous verrons comment préparer une réponse à partir d’éléments extraits du JSON avant de l’envoyer à l’interface WEB.

Exemples utilisés pour l’écriture de l’article

JSON :  https://github.com/bblanchon/ArduinoJson/wiki
SPIFFS : https://github.com/esp8266/Arduino/issues/1685
SPIFFS : https://github.com/esp8266/Arduino/blob/master/libraries/esp8266/examples/ConfigFile/ConfigFile.ino

Accéder rapidement aux autres parties du projet

Voici les liens pour accéder aux autres parties du projet

Vous êtes ici

Avez-vous aimé cet article ?