Le sketch final

Le sketch le plus avancé développé pour notre station météo connectée est meteo_station.ino ; il est affiché ci-après. Il est disponible en téléchargement ici, dans un fichier zip qui contient l’ensemble de ce tutoriel.

Il rajoute quelques fonctions supplémentaires à record_parameters.ino pour changer à la demande, après démarrage, et enregistrer en mémoire flash les coordonnées géographiques de la station, la clef pour l’API OpenWeather et le jeton MQTT.

Il propose une amélioration de la gestion des erreurs dans la fonction Read_Sensors. Désormais, elle horodate les erreurs de mesure des capteurs et les écrit dans un fichier de log ERRLOG.CSV sur la carte SD avant de forcer un reset de la station.

Il n’utilise aucune technique nouvelle et nous vous laissons le soin de l’étudier pour comprendre les ajouts qu’il contient. Nous atteignons la fin de ce tutoriel et vous devriez à présent être assez expérimenté pour mener à bien cet exercice. 

meteo_station.ino

/*------------------------------------------------------------------------------------------------------------
  - Mesure de température, humidité, pression et concentration en CO2
  - Le port série du ST-LINK affiche les messages de diagnostic et/ou debug.
  - Gestion des temporisations de la boucle principale avec le mode Low Power.
  - Implémentation de l'independant watchdog.
  - Ajout de la connectivité Wi-Fi avec le module ESP01.
  - Les mesures sont horodatées grâce à la RTC du microcontrôleur STM32L4 et de requêtes NTP récurrentes.
  - Les mesures sont envoyées à Thingsboard sur Internet avec le protocole MQTT puis publiées
    par celui-ci (donc consultables) à une URL donnée.
  - Lorsque la station ne parvient pas à joindre le serveur MQTT de ThingsBoard pour lui adresser une trame, cette
    dernière est enregistrée sur une carte SD pour être renvoyée plus tard.
  - Le bouton utilisateur est géré avec une interruption activée à la fin de la fonction setup.
    Cette interruption liste les trames MQTT éventuellement écrites sur la carte SD.
  - La station se connecte au site Open Weather pour obtenir des prévisions météos à court terme
    (décodées mais inutilisées) et ajuster la RTC avec le fuseau horaire et l'heure d'été ou d'hivers.
  - On utilise un module Bluetooth HC-06 (code PIN : 1234) série et ses interruptions pour envoyer des commandes
    à la station après son démarrage.
  - Si le bouton utilisateur est appuyé pendant l'exécution de setup, on entre dans la procédure
    de saisie et mémorisation des informations de connexion au Wi-Fi dans la Flash. Cette opération se fait
	par la connexion Bluetooth réalisées avec le module HC-06.
  - Ajout des commandes Bluetooth pour enregistrer en Flash les coordonnées géographiques, le jeton Thingsboard
    et la clef pour l'API OpenWeather.
  ------------------------ --------------------------------------------------------------------------------------
  Matériel & brochage
  - Carte Nucleo L476RG
  - Shield X-Nucleo IKS01A2 sur I2C1 / Conn. Arduino : SCL = CN5 10, SDA = CN5 09, GND = CN6 07, VCC = CN6 04
  - Module Grove SCD30 sur I2C1 / CN10 : GND = 20, VCC = 08, SCL = 03, SDA = 05
  - Module SD card sur SPI1 / CN10 : CS = 19, MOSI = 15, SCK = 31, MISO = 13, GND = 09, VCC = 07
  - Module Wi-Fi ESP-01 sur UART4 / CN7 : TX = 28, RX = 30, GND = 22, VCC = 16
  - Module Bluetooth HC-06 sur UART5 / CN7 : GND = 20, VCC = 18, RX = 04, TX = 03
  --------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------*/
/* Variables globales, includes, defines                                       */
/*----------------------------------------------------------------------------*/
#define MAIN_LOOP_DELAY (10000) // Temporisation de la boucle principale (10s)
#define SET_REC_MOD (30) // Sur-période de mesures et enregistrements
uint32_t rec_mod;
uint32_t main_loop_delay;
#define DELAY_5S (5000) // Temps d'attente 5s pour différents usages
#define RESET_RETRY (2) // Nombre de tentatives avant de forcer un software reset
#define STLK_BDRATE (230400) // Débit du port série du ST-LINK
// Nombre maximum de caractères dans les chaînes envoyées aux tableaux tampons
#define MAX_LEN (80)
bool setup_completed = false;

// Structure pour les données mesurées
struct Measure {
  char datetime[18];
  float temperature;
  uint16_t pressure;
  uint8_t humidity;
  uint16_t co2;
};

struct Measure measure; // Variable de type Measure

#include <Wire.h> // Référence pour le bus I2C1

// Références pour les capteurs du X-Nucleo IKS01A2
#include <HTS221Sensor.h>
#include <LPS22HBSensor.h>

// Pointeurs vers les classes des capteurs
HTS221Sensor *HT;
LPS22HBSensor *PT;

// Offsets pour les capteurs (constantes)
#define TEMP_OFFSET (0.0)   // en Celsius
#define PRES_OFFSET (-0.5)  // en hPa
#define HUMI_OFFSET (-3)    // en % 

#include "SCD30.h" // Bibliothèque du capteur du SCD30

// Offset et paramètre pour le capteur (constantes)
#define AUTO_CALIBRATION false
#define CO2_OFFSET (0)

SCD30 co2Sensor; // Pointeurs vers classes des capteurs

// Déclarations pour le module carte SD
#include <SPI.h> // Pilote du contrôleur de bus SPI
#include <SD.h> // Pilote du module carte SD
#define SDCARD_SSEL (PC7) // Chip select pin

File datalogfile; // Fichier pour enregistrer les mesures
char datalogfilename[] = "DATALOG.CSV"; // Nom du fichier

#include <STM32LowPower.h> // Bibliothèque STM32 Low Power

#include <IWatchdog.h> // Bibliothèque de l'independant watchdog (IDWG)

// Nécessaire pour tenir le décompte des software resets
uint32_t swrst_cnt __attribute__((__section__(".noinit")));
uint32_t first_run __attribute__((__section__(".noinit")));

#include <WiFiEsp.h> // Bibliothèque pour le WiFi
HardwareSerial WiFi_Serial(PA1, PA0); // Port série de l'ESP01 (PA1=RX, PA0=TX)
#define WIFI_BDRATE (115200) // Débit du port série de l'ESP01

char ssid[MAX_LEN] = {0}; // Tableau tampon pour le SSID
char pass[MAX_LEN] = {0}; // Tableau tampon pour le mot de passe

#include <STM32RTC.h> // Bibliothèque de la RTC
STM32RTC& rtc = STM32RTC::getInstance();

#define SET_RTC_MOD (300) // Sur-période de recalage de la RTC
uint32_t rtc_mod;

#include <WiFiEspUdp.h> // Bibliothèque pour le service UDP
#define UDP_PORT (2390)
WiFiEspUDP Udp; // Pointeur vers la classe serveur UDP

// Adresses IP de deux serveurs NTP
#define NTP_SERVER_1  "216.239.35.4"
#define NTP_SERVER_2  "time.nist.gov"
char TimeServer1[] = NTP_SERVER_1;
char TimeServer2[] = NTP_SERVER_2;

// Déclarations pour le fuseau horaire et l’ajustement d’heure saisonnier
#define SUMMER_TIME_MONTH (3) // Mois du passage à l'heure d'été
#define WINTER_TIME_MONTH (10) // Mois du passage à l'heure d'hivers
#define DST_WEEKDAY (7) // Jour du changement d'heure saisonnier
#define SECS_PER_DAY (86400) // Dans une journée on a 86400 secondes
#define GMT_OFFSET (+1) // Ajustement d'heure dû au fuseau horaire
byte dst_offset; // Ajustement d'heure saisonnier
byte gmt_offset; // Ajustement d'heure méridien

struct DateTime {
  byte Year;
  byte Month;
  byte Day;
  byte Hours;
  byte Minutes;
  byte Seconds;
  byte WeekDay;
} ;

// Déclarations pour le protocole MQTT
#include <WiFiEspClient.h>
#include <PubSubClient.h>

WiFiEspClient espClient;
PubSubClient MQTT_Client(espClient); // Pointeur vers la classe PubSubClient

#define MQTT_PAYLOAD_SIZE (165)
#define MQTT_TOKEN_SIZE (21)
#define MQTT_KEEP_ALIVE (30)
#define MQTT_SOCK_TOUT (30)
#define MQTT_RECONNECT_TOUT (10)
#define MQTT_DEVICE "Station_Meteo"
#define MQTT_PUB_TOPIC "v1/devices/me/telemetry"
#define MQTT_PORT (1883)
#define MQTT_PAYLOAD_SIZE (165)
#define TB_SERVER "demo.thingsboard.io" // URL du serveur Thingsboard
#define MQTT_TOKEN "my_access_token"

char MQTTtoken[] = MQTT_TOKEN;
char TBServer[] = TB_SERVER;
char MQTT_Payload[MQTT_PAYLOAD_SIZE] = {0};

// Macro : est-ce que l'année est bissextile ?
#define LPYR(year) ((((year) % 4 == 0) && ((year) % 100 != 0)) || ((year) % 400 == 0))

// Nb de jours par mois
static uint8_t RTC_Months[2][12] = {
  {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}, // Année non-bissextile
  {31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}  // Année bissextile
};

// Structure pour ranger les informations géographiques
struct Location {
  float Latitude;
  float Longitude;
  float Altitude;
} ;

struct Location MyLocation;

// Déclarations pour les requêtes à OpenWeather

#include <ArduinoJson.h> // Bibliothèque pour le parsing des chaînes JSON
#define OWM_KEY "myOWMkey" // Clef fournie par le site Open Weather
#define OWM_SERVER "api.openweathermap.org" // Serveur OpenWeather
#define OWM_SERVER_PORT (80)

char OWMKey[] = OWM_KEY;
char owmServer[] = OWM_SERVER;
uint32_t timezone = -1; // Décalage horaire complet (saisonnier + méridien) en s
const uint8_t owmFcst = 6; // Rapporte 6 prévisions météos espacées de 3 heures

struct Forecast {
  char date_time[19];
  char description[40];
};

// Déclarations du port série du HC-06
HardwareSerial BT_Serial(PD2, PC12);
// Débit du port série du HC-06 (9600 bds recommandé)
#define BT_BDRATE (9600)
// Buffer d'émission et réception pour BT_Serial
char BT_Buffer[MAX_LEN] = {0};

// Switchs de gestion des commandes par interruptions
volatile bool sd_card_erasing = false;
volatile bool sd_card_listing = false;
volatile bool measure_now = false;
volatile bool reset_now = false;
volatile bool save_coordinates = false;
volatile bool save_owm_key = false;
volatile bool save_mqtt_token = false;

// Bibliothèque pour l'émulation d'une EEPROM dans la mémoire Flash
#include <EEPROM.h>

// Log des erreurs de mesure
File errlogfile; // Fichier pour horodater les erreurs de mesures
char errlogfilename[] = "ERRLOG.CSV"; // Nom du fichier
#define ERR_MESS_SIZE (100)
char ERR_MESS[ERR_MESS_SIZE] = {0};

/*----------------------------------------------------------------------------*/
/* Paramètres de démarrage                                                    */
/*----------------------------------------------------------------------------*/
void setup() {

  // Initialise le port série du ST-LINK
  Serial.begin(STLK_BDRATE);
  while (!Serial) delay(100);
  Serial.println("\nST-LINK serial baud rate set to " + String(STLK_BDRATE));

  // Initialise la LED utilisateur
  pinMode(LED_BUILTIN, OUTPUT);

  // Initialise le bus I2C1 (par défaut sous Arduino)
  Wire.begin();

  // Démarre la RTC
  rtc.setClockSource(STM32RTC::LSE_CLOCK); // Choix de la source d'horloge
  rtc.begin();

  // Modulo pour les recalages de la RTC
  rtc_mod = SET_RTC_MOD;

  // Initialise les capteurs environnementaux
  Initialize_Sensors(&Serial);

  // Modulo pour les mesures des capteurs environnementaux
  rec_mod = SET_REC_MOD;

  // Initialise le bouton utilisateur
  pinMode(USER_BTN, INPUT_PULLUP);

  // Initialise le module carte SD
  Initialize_SD_card(&Serial);

  // Création de nouveaux fichier de logs sur la carte SD
  LogFile_CreateOpen(datalogfilename, &Serial);
  LogFile_CreateOpen(errlogfilename, &Serial);

  // Initialise le port série du module HC-06
  BT_Serial.begin(BT_BDRATE);
  while (!BT_Serial) delay(100);
  Serial.println("Bluetooth serial baud rate set to " + String(STLK_BDRATE));

  // Autorise le module Bluetooth à interrompre le mode veille
  LowPower.enableWakeupFrom(&BT_Serial, BT_Serial_ISR);
  Serial.println("Bluetooth module ready to process commands");
  BT_Serial.println("Bluetooth module ready to process commands");

  // Initialise le module Wi-Fi
  Initialize_WiFi(&Serial);

  /*  Mise à jour des informations de connexion WiFi via blueTooth.
    Pour entrer les informations de connexion au WiFi il faut :
      1 - Maintenir le bouton user de la carte Nucleo enfoncé au démarrage.
      2 - Se connecter au port série du module HC-06 avec Termite pour renseigner
      le SSID et le mot de passe du réseau WiFi sélectionné.*/
  Update_Wifi_Credentials(&Serial);

  /* Charge les données de coordonnées, altitude et les clefs MQTT et OWM depuis l'EEPROM
     et calcule le décalage de fuseau horaire */
  Load_User_Parameters(&Serial);

  // Initialise le service MQTT
  Initialize_MQTT(&Serial);

  // Initialise le watchdog
  main_loop_delay = Initialize_Watchdog(&Serial);

  // On attache une interruption au bouton USER pour gérer son appui
  attachInterrupt(digitalPinToInterrupt(USER_BTN), Button_Down_ISR, LOW );

  // Démarre le mode basse consommation
  LowPower.begin();
  Serial.println("Low power mode activated");

  // Signale que la fonction setup s'est déroulée jusqu'au bout
  setup_completed = true;
}

/*----------------------------------------------------------------------------*/
/* Boucle principale                                                          */
/*----------------------------------------------------------------------------*/
void loop() {

  if (rtc_mod == SET_RTC_MOD) { // Toutes les SET_RTC_MOD itérations ...

    rtc_mod = 0;

    // Re-connexion du module Wi-Fi au réseau spécifié
    Connect_WiFi(&Serial);

    // Récupère les informations météo et le temps local avec Open Weather
    timezone = getWeatherForecast(&Serial);

    // Acquisition NTP de la date et de l'heure de la mesure, recalage de la RTC
    Set_RTC_Time(&Serial);

    // Poste à nouveau les trames MQTT éventuellement sauvegardées sur la carte SD
    Post_SD_Records(datalogfilename, &Serial, MQTT_Payload);

    // Ré-arme l'IDWG
    IWatchdog.reload();

  } // Clôture de if (rtc_mod == SET_RTC_MOD)

  rtc_mod++;

  if (rec_mod == SET_REC_MOD) { // Toutes les SET_REC_MOD itérations ...

    rec_mod = 0;

    // Allume la LED utilisateur
    digitalWrite(LED_BUILTIN, HIGH);

    // Lecture des capteurs environnementaux
    Read_Sensors(&Serial);

    // Construit la trame MQTT (JSON)
    Build_Payload(&Serial, MQTT_Payload);

    // Tente de publier la trame MQTT. Si la publication échoue, écrit la trame sur la carte SD
    // pour sa republication ultérieure.
    if (!Post_Payload(&Serial, MQTT_Payload)) {
      Serial.println("MQTT server did not respond : payload stored in SD card");
      SD_Write_Data(datalogfilename, MQTT_Payload);
    }

    // Eteint la LED utilisateur
    digitalWrite(LED_BUILTIN, LOW);

  } // Clôture de if (rec_mod == SET_REC_MOD)

  rec_mod++;

  // Gestion des commandes envoyées par Bluetooth

  // Liste des trames MQTT enregistrées sur la carte SD
  if (sd_card_listing) {
    LogFile_List(datalogfilename, &Serial);
    sd_card_listing = false;
  }
  // Force la station à effectuer une mesure
  else if (measure_now) {
    Serial.println("User triggered measurement");
    rec_mod = SET_REC_MOD;
    measure_now = false;
  }
  // Effacement du fichier datalogfile de la carte SD
  else if (sd_card_erasing) {
    sd_card_erasing = false;
    LogFile_Delete(datalogfilename, &Serial);
  }
  // Force la station à redémarrer (RESET)
  else if (reset_now) {
    reset_now = false;
    HAL_NVIC_SystemReset();
  }
  // Enregistre les coordonnées géographiques dans l'EEPROM
  else if (save_coordinates) {
    save_coordinates = false;
    Save_Coordinates();
  }
  // Enregistre la clef OWM dans l'EEPROM
  else if (save_owm_key) {
    save_owm_key = false;
    Save_OWM_Key();
  }
  // Enregistre la clée ThingsBoard dans l'EEPROM
  else if (save_mqtt_token) {
    save_mqtt_token = false;
    Save_MQTT_Key();
  }

  // Entretient la connexion MQTT (keepalive messages au broker) et gère d'éventuels callbacks.
  MQTT_Client.loop();

  // Ré-arme l'IDWG
  IWatchdog.reload();

  // Mise en veille (mode "STOP") avant l'itération suivante
  delay(5);
  LowPower.deepSleep(main_loop_delay);
}

/*----------------------------------------------------------------------------*/
/* Initialisation des capteurs de température, pression, humidité, CO2        */
/*----------------------------------------------------------------------------*/
void Initialize_Sensors(HardwareSerial *serial) {

  HT = new HTS221Sensor (&Wire); // Création d'une instance de la classe "HTS221Sensor"
  HT->Enable(); // Active le capteur HTS221
  serial->println("HTS221 temperature sensor started.");

  PT = new LPS22HBSensor(&Wire); // Création d'une instance de la classe "LPS22HBSensor"
  PT->Enable(); // Active le capteur LPS22HB
  serial->println("LPS22HB temperature sensor started.");

  // Démarrage du capteur de CO2, calibration de celui-ci si requis
  if (!co2Sensor.begin(Wire, AUTO_CALIBRATION)) { // Gestion d'erreur ...
    serial->println("CO2 sensor not detected. Please check wiring. Freezing...");
    while (true); // Si le capteur est muet, bloque l'exécution du firmware ici
  }

  // Paramétrage du capteur de CO2
  co2Sensor.setMeasurementInterval(main_loop_delay * SET_REC_MOD / 1000);
  float offset = co2Sensor.getTemperatureOffset();

  co2Sensor.setTemperatureOffset(offset);
  serial->println("SCD30 CO2 sensor started.");
}

/*----------------------------------------------------------------------------*/
/* Lecture des capteurs environnementaux                                      */
/*----------------------------------------------------------------------------*/
void Read_Sensors(HardwareSerial *serial) {

  float temperature, pressure, humidity;
  HTS221StatusTypeDef errHTS221;
  LPS22HBStatusTypeDef errLPS22HB;
  String ERR_MESS_STRING;
  bool Error_Occured = false;

  errHTS221 = HT->GetTemperature(&temperature); // Lecture de la température
  if (errHTS221 != HTS221_STATUS_OK || !temperature) { // Gestion d'erreur ...
    serial->println("HTS221 temperature sensor failure. Freezing...");
    // En cas d'erreur de lecture, écris la dans le fichier de logs
    Fill_DateTime(&Serial); // Construit l'étiquette d'horodatage
    ERR_MESS_STRING = String(measure.datetime) + ";" + "HTS221 temperature sensor failure.";
    ERR_MESS_STRING.toCharArray(ERR_MESS, ERR_MESS_SIZE);
    SD_Write_Data(errlogfilename, ERR_MESS);
    Error_Occured = true;
  }

  errLPS22HB = PT->GetPressure(&pressure); // Lecture de la pression
  if (errLPS22HB != LPS22HB_STATUS_OK || !pressure) { // Gestion d'erreur ...
    serial->println("LPS22HB pressure sensor failure. Freezing...");
    // En cas d'erreur de lecture, écris la dans le fichier de logs
    Fill_DateTime(&Serial); // Construit l'étiquette d'horodatage
    ERR_MESS_STRING = String(measure.datetime) + ";" + "LPS22HB pressure sensor failure.";
    ERR_MESS_STRING.toCharArray(ERR_MESS, ERR_MESS_SIZE);
    SD_Write_Data(errlogfilename, ERR_MESS);
    Error_Occured = true;
  }

  errHTS221 = HT->GetHumidity(&humidity); // Lecture de l'humidité
  if (errHTS221 != HTS221_STATUS_OK || !humidity) { // Gestion d'erreur ...
    serial->println("HTS221 humidity sensor failure. Freezing...");
    // En cas d'erreur de lecture, écris la dans le fichier de logs
    Fill_DateTime(&Serial); // Construit l'étiquette d'horodatage
    ERR_MESS_STRING = String(measure.datetime) + ";" + "HTS221 humidity sensor failure.";
    ERR_MESS_STRING.toCharArray(ERR_MESS, ERR_MESS_SIZE);
    SD_Write_Data(errlogfilename, ERR_MESS);
    Error_Occured = true;
  }

  // Mémorise les mesures dans les membres de la structure "measure"
  measure.temperature = temperature + (float)TEMP_OFFSET;
  measure.pressure = (uint16_t)(Corr_Pres(pressure + (float)PRES_OFFSET, MyLocation.Altitude));
  measure.humidity = (uint8_t)(humidity + (float)HUMI_OFFSET);

  // Paramétrage du capteur de CO2 (correction de pression absolue)
  co2Sensor.setAmbientPressure((uint16_t)(pressure + (float)PRES_OFFSET));

  while (!co2Sensor.dataAvailable()); // Attend que le capteur puisse renvoyer une valeur

  uint16_t co2 = co2Sensor.getCO2(); // Lecture de la concentration de CO2
  if (!co2) { // Gestion d'erreur : si la concentration lue de CO2 égale 0 ppm
    serial->println("CO2 sensor failure. Freezing ...");
    // En cas d'erreur de lecture, écris la dans le fichier de logs
    Fill_DateTime(&Serial); // Construit l'étiquette d'horodatage
    ERR_MESS_STRING = String(measure.datetime) + ";" + "CO2 sensor failure.";
    ERR_MESS_STRING.toCharArray(ERR_MESS, ERR_MESS_SIZE);
    SD_Write_Data(errlogfilename, ERR_MESS);
    Error_Occured = true;
  }

  // Mémorise la concentration de CO2 dans le membre "co2" de la structure "measure"
  measure.co2 = co2 + (uint16_t)CO2_OFFSET;

  // En cas d'erreur de lecture de l'un des capteurs, redémarre la station
  if (Error_Occured) HAL_NVIC_SystemReset();

}

/*----------------------------------------------------------------------------*/
/* Pression rapportée au niveau de la mer                                     */
/*----------------------------------------------------------------------------*/
float Corr_Pres(float pressure, float altitude ) {
  return pressure * pow(1.0 - (altitude * 2.255808707E-5), -5.255);
}

/*----------------------------------------------------------------------------*/
/* Initialisation du module carte SD                                          */
/*----------------------------------------------------------------------------*/
void Initialize_SD_card(HardwareSerial * serial) {

  // Change la broche assignée à la ligne SPI CLK (horloge)
  SPI.setSCLK(PB3); // Nb : par défaut la broche est PA5

  /* Démarre le bus SPI et initialise le module SD sur la broche SDCARD_SSEL
    pour le chip select */
  if (!SD.begin(SDCARD_SSEL)) {
    serial->println("SD card failed, or not present");
    while (true);
  }
  serial->println("SD card module initialized");
}

/*--------------------------------------------------------------------------------*/
/* Ouverture du fichier datalogfilename sur la carte SD                           */
/*--------------------------------------------------------------------------------*/
void LogFile_CreateOpen(char* datalogfilename, HardwareSerial * serial) {
  // Ouvre le fichier datalogfile en écriture
  datalogfile = SD.open(datalogfilename, FILE_WRITE);
  serial->println("File " + (String)datalogfilename + " available for data recording");
}

/*----------------------------------------------------------------------------------*/
/* Enregistrement de Data sur la carte SD, dans le fichier logfilename              */
/*----------------------------------------------------------------------------------*/
void SD_Write_Data(char* logfilename, char* Data) {
  datalogfile.println(Data); // Ecrit sur une ligne
  datalogfile.flush(); // Force l'écriture physique sur la carte SD
}

/*----------------------------------------------------------------------------*/
/* Affichage ligne par ligne du contenu de datalogfilename sur un port série      */
/*----------------------------------------------------------------------------*/
void LogFile_List(char* datalogfilename, HardwareSerial * serial) {

  if (datalogfile.size()) { // si le fichier n’est pas vide

    serial->println((String)datalogfilename + " content :");

    String buffer;

    // Ferme le fichier datalogfile initialement ouvert en écriture
    datalogfile.close();

    // Ouvre le fichier datalogfilename en lecture
    datalogfile = SD.open(datalogfilename, FILE_READ);

    // Lis le fichier ligne par ligne et les imprime sur serial
    if (datalogfile) {
      while (datalogfile.available()) {
        buffer = datalogfile.readStringUntil('\n');
        serial->println(buffer);
        delay(5);
      }
    } else {
      serial->println("error opening " + (String)datalogfilename);
      while (true);
    }

    // Ferme le ficher datalogfile, ouvert en lecture
    datalogfile.close();

    // Ouvre le fichier datalogfile, en écriture
    datalogfile = SD.open(datalogfilename, FILE_WRITE);
  }
  else {
    serial->println((String)datalogfilename + " is empty");
  }
}

/*----------------------------------------------------------------------------*/
/* Effacement du fichier datalogfile sur la carte SD puis reset                   */
/*----------------------------------------------------------------------------*/
void LogFile_Delete(char* datalogfilename, HardwareSerial *serial) {
  if (datalogfile) datalogfile.close();
  if (SD.exists(datalogfilename)) SD.remove(datalogfilename);
  HAL_NVIC_SystemReset();
}

/*----------------------------------------------------------------------------*/
/* Gestion de l'interruption du douton : force une mesure                     */
/*----------------------------------------------------------------------------*/
void Button_Down_ISR(void) {
  measure_now = true;
}

/*----------------------------------------------------------------------------*/
/* Initialisation du watchdog et des bases de temps                           */
/*----------------------------------------------------------------------------*/
uint32_t Initialize_Watchdog(HardwareSerial *serial) {

  // IWDG_TIMEOUT_MAX est la durée maximum du compte à rebours de l'IDWG (en µs)
  serial->print("Watchdog timeout set to : ");
  serial->print(IWDG_TIMEOUT_MAX / 1000000);
  serial->println("s");

  uint32_t MainLoopDelay;

  // Si nécessaire, raccourcis la temporisation de la boucle principale pour
  // qu'elle reste inférieure à la durée maximum du compte à rebours de l'IDWG
  if ( 1000 * MAIN_LOOP_DELAY > IWDG_TIMEOUT_MAX) {
    MainLoopDelay = (IWDG_TIMEOUT_MAX - 1000000) / 1000;
  }
  else {
    MainLoopDelay = MAIN_LOOP_DELAY;
  }

  serial->print("Application time base set to : ");
  serial->print(MainLoopDelay / 1000);
  serial->println("s");
  serial->print("Measurements period set to : ");
  serial->print(MainLoopDelay * SET_REC_MOD / 1000);
  serial->println("s");

  // Initialise la variable swrst_cnt à la mise sous tension ou au
  // premier démarrage après rechargement du firmware
  if (first_run != 0xDEAD0000) {
    swrst_cnt = 0;
    Serial.println("First run after power-up");
    first_run = 0xDEAD0000;
  }

  // Démarre l'IDWG
  IWatchdog.begin(IWDG_TIMEOUT_MAX);
  if (!IWatchdog.isEnabled()) {
    serial->println("Watchdog enabled !");
  }

  // Si on vient juste de démarrer après un reset dû à l'IDWG
  if (IWatchdog.isReset(true)) {
    ++swrst_cnt; // on incrémente swrst_cnt (comptage des software resets)
    serial->println("\nSystem rebooting from watchdog timeout.");
  }

  // Si swrst_cnt n'est pas égale à zéro
  if (swrst_cnt) {
    serial->println("Total software resets since power-on : " + String(swrst_cnt));
  }

  // Retourne la valeur potentiellement réduite de la temporisation de la boucle
  // principale
  return MainLoopDelay;
}

/*----------------------------------------------------------------------------*/
/* Initialisation du module WiFi                                             */
/*----------------------------------------------------------------------------*/
void Initialize_WiFi(HardwareSerial *serial) {

  // Initialise le port série du WiFi
  WiFi_Serial.begin(WIFI_BDRATE);
  while (!WiFi_Serial) delay(100);

  serial->println("WiFi serial baud rate set to " + String(WIFI_BDRATE));

  // Initialise le module ESP01
  WiFi.init(&WiFi_Serial);

  if (WiFi.status() == WL_NO_SHIELD) {
    serial->println("WiFi module not present");
    while (true);
  }

  serial->println("WiFi module available");
}

/*----------------------------------------------------------------------------*/
/* Connexion au WiFi                                                          */
/*----------------------------------------------------------------------------*/
void Connect_WiFi(HardwareSerial *serial) {

  uint8_t status = WL_IDLE_STATUS;
  uint8_t n = 0;

  while ( status != WL_CONNECTED) {

    serial->println("Connecting to WPA SSID: " + (String)ssid);
    status = WiFi.begin(ssid, pass);
    n++;

    // Rechargement du watchdog
    IWatchdog.reload();

    // Si trop de tentatives de reconnexion infructueuses, RESET !
    if (n > RESET_RETRY) HAL_NVIC_SystemReset();
    delay(DELAY_5S);
  }
}

/*----------------------------------------------------------------------------*/
/* Renvoie la liste de réseaux WiFi présents sur le port série spécifié       */
/*----------------------------------------------------------------------------*/
void listWiFiNetworks(HardwareSerial *serial) {

  // Cherche les réseaux Wifi à proximité
  int numSsid = WiFi.scanNetworks();

  if (numSsid == -1) {
    serial->println("Couldn't get a wifi connection");
    while (true);
  }

  serial->println("Available WiFi networks :");

  // Renvoie la liste des réseaux trouvés sur le port série serial
  for (int thisNet = 0; thisNet < numSsid; thisNet++) {

    if (strlen(WiFi.SSID(thisNet)) != 0) {
      serial->print(thisNet);
      serial->print(") ");
      serial->print(WiFi.SSID(thisNet));
      serial->print("\tSignal: ");
      serial->print(WiFi.RSSI(thisNet));
      serial->print(" dBm");
      serial->print("\tEncryption: ");

      int thisType = WiFi.encryptionType(thisNet);

      switch (thisType) {
        case ENC_TYPE_WEP:
          serial->println("WEP");
          break;
        case ENC_TYPE_WPA_PSK:
          serial->println("WPA_PSK");
          break;
        case ENC_TYPE_WPA2_PSK:
          serial->println("WPA2_PSK");
          break;
        case ENC_TYPE_WPA_WPA2_PSK:
          serial->println("WPA_WPA2_PSK");
          break;
        case ENC_TYPE_NONE:
          serial->println("None");
          break;
      }
    }
  }
}

/*----------------------------------------------------------------------------*/
/* Récupération du temps universel et mise à l'heure de la RTC.               */
/* Gère également l'heure d'hivers / d'été en France (changements les         */
/* derniers dimanches des mois de mars et octobre)                            */
/*----------------------------------------------------------------------------*/
void Set_RTC_Time(HardwareSerial *serial) {

  serial->println("NTP call and RTC setting");

  uint8_t n = 0;
  bool try_alternative_ntp_server = false;

  while (true) {

    uint32_t UTC_NOW;

    // Récupère l'heure et la date universelles de l'instant
    if (!try_alternative_ntp_server) {
      UTC_NOW = getUTC(TimeServer1); // Interroge le serveur NTP n°1
    }
    else { // Si le serveur NTP n°1 n'a pas répondu ...
      UTC_NOW = getUTC(TimeServer2); // ... interroge le serveur NTP n°2
    }

    if (UTC_NOW) { // Si la requête au serveur NTP est un succès

      // Règle le calendrier et l'horloge de la RTC avec le temps universel ...
      rtc.setEpoch(UTC_NOW);

      // Si on n'a pas déjà obtenu les correction saisonnières et méridiennes par OpenWeather
      // on procède au calcul de l'ajustement d'heure saisonnière avec la RTC
      if (timezone == -1) {

        // Extrait l'année courante
        byte Current_Year = rtc.getYear();

        // Obtient la date du changement d'heure en été pour l'année en cours
        uint32_t UTC_SUMMER = Get_DST_Date(Current_Year, SUMMER_TIME_MONTH, DST_WEEKDAY);

        // Obtient la date du changement d'heure en hivers pour l'année en cours
        uint32_t UTC_WINTER = Get_DST_Date(Current_Year, WINTER_TIME_MONTH, DST_WEEKDAY);

        // Si on n'est pas encore en été ou si on est en hivers ...
        if (UTC_NOW < UTC_SUMMER || UTC_NOW > UTC_WINTER) {
          dst_offset = 0; // ... pas d'ajustement
        }
        // Si on est en été ou en automne
        else if (UTC_NOW > UTC_SUMMER && UTC_NOW < UTC_WINTER) {
          dst_offset = 1;  // ... rajoute 1 heure
        }
        serial->println("Daylight saving time shift set to +" + String(dst_offset) + " hour(s)");

        // Le calcul de l'ajustement d'heure a utilisé et de ce fait déréglé la RTC.
        // Nous devons à nouveau interroger le serveur NTP pour remettre la RTC à l'heure courante.
        // Nous imposons une pause de DELAY_5S avant d'interroger à nouveau le NTP pour que celui-ci
        // ne rejette pas notre requête.

        delay(DELAY_5S);

        if (!try_alternative_ntp_server) {
          UTC_NOW = getUTC(TimeServer1);
        }
        else {
          UTC_NOW = getUTC(TimeServer2);
        }

        rtc.setEpoch(UTC_NOW);
      }
      break;
    }
    else { // Si la requête au serveur NTP a échoué

      serial->println("NTP failure : could not get UTC");
      try_alternative_ntp_server = true;
      n++;

      // Ré-initialisation du compte à rebours de l'IDWG
      IWatchdog.reload();

      // Si c'est le deuxième échec consécutif, software reset
      if (n > RESET_RETRY) HAL_NVIC_SystemReset();
      delay(DELAY_5S);
    }
  }
}

/*----------------------------------------------------------------------------*/
/* Fournit la date de l'heure d'été/d'hivers                                  */
/* Entrée :                                                                   */
/*    Year : année concernée                                                  */
/*    Month : mois concerné                                                   */
/*    WeekDay : jour concerné (lundi = 1 ... dimanche = 7)                    */
/* Sortie :                                                                   */
/*    valeur UTC de la date du changement d'heure en entrée                   */
/* ATTENTION : Gère l'heure d'hivers / d'été selon la règle appliquée en      */
/* France (changements les DERNIERS dimanches des mois de mars et octobre)    */
/*----------------------------------------------------------------------------*/
uint32_t Get_DST_Date(byte Year, byte Month, byte WeekDay) {

  const byte day_one = 1;

  // L'heure avant le changement est toujours 3h00
  const byte dsts = 0;
  const byte dstm = 0;
  const byte dsth = 3;
  rtc.setTime(dsth, dstm, dsts);

  // On initialise la RTC au premier jour du mois du changement d'heure
  rtc.setDay(day_one);
  rtc.setMonth(Month);
  rtc.setYear(Year);

  /* On récupère le temps universel et on re-paramètre l'horloge avec setEpoch()  */
  /* afin que la fonction getWeekDay() renvoie par la suite des valeurs correctes */

  // Temps universel du premier jour du mois du changement d'heure
  uint32_t utc = rtc.getEpoch();
  rtc.setEpoch(utc);

  // Calcul itératif de la date exacte du changement d'heure en France :
  // Renvoie le DERNIER WeekDay du mois en question.

  byte WeekDay_ = rtc.getWeekDay();
  byte Month_ = rtc.getMonth();
  uint32_t utc_ = 0;

  // Aussi longtemps que l'on reste dans le mois sélectionné
  while (Month_ == Month) {

    // Si le jour est susceptible d'être celui du changement d'heure
    if (WeekDay_ == WeekDay) {
      utc_ = utc;  // alors sauvegarde le temps universel de ce jour
    }

    // Passe au jour suivant
    utc = utc + SECS_PER_DAY;

    // Avance la RTC d'un jour
    rtc.setEpoch(utc);

    WeekDay_ = rtc.getWeekDay();
    Month_   = rtc.getMonth();
  }

  // A la sortie de la boucle, _utc contient bien le DERNIER WeekDay du mois
  // du changement d'heure !

  return utc_;
}

/*----------------------------------------------------------------------------*/
/* Requête au serveur de temps internet pour récupérer le temps universel     */
/* coordonné (UTC).                                                           */
/*----------------------------------------------------------------------------*/
uint32_t getUTC(char *ntpSrv) {

  const uint32_t NTP_PACKET_SIZE = 48;
  const uint32_t UDP_TIMEOUT = 2000;
  byte packetBuffer[NTP_PACKET_SIZE];

  uint32_t UTC = 0;
  memset(packetBuffer, 0, NTP_PACKET_SIZE);

  packetBuffer[0] = 0b11100011;
  packetBuffer[1] = 0;
  packetBuffer[2] = 6;
  packetBuffer[3] = 0xEC;

  packetBuffer[12]  = 49;
  packetBuffer[13]  = 0x4E;
  packetBuffer[14]  = 49;
  packetBuffer[15]  = 52;

  Udp.begin(UDP_PORT);
  Udp.beginPacket(ntpSrv, 123); //NTP requests are to port 123
  Udp.write(packetBuffer, NTP_PACKET_SIZE);
  Udp.endPacket();

  uint32_t startMs = millis();
  while (!Udp.available() && (millis() - startMs) < UDP_TIMEOUT);

  if (Udp.parsePacket()) {

    Udp.read(packetBuffer, NTP_PACKET_SIZE);
    uint32_t highWord = word(packetBuffer[40], packetBuffer[41]);
    uint32_t lowWord = word(packetBuffer[42], packetBuffer[43]);
    uint32_t secsSince1900 = highWord << 16 | lowWord;
    const uint32_t seventyYears = 2208988800UL;

    UTC = secsSince1900 - seventyYears;
    Udp.stop();
  }
  return UTC;
}

/*-----------------------------------------------------------------------------*/
/* Ecris l'UTC au format "20/12/30 10:34:09" dans measure.datetime.                                   */
/*-----------------------------------------------------------------------------*/

void Fill_DateTime(HardwareSerial *serial) {
  
  // Date et heure universelles au format UNIX (Epoch)
  uint32_t UTC_time = rtc.getEpoch();
  char bufEpoch[10];

  // Ecris l'UTC au format UNIX dans bufEpoch
  ltoa(UTC_time, bufEpoch, 10);

  // Ecris l'UTC au format "20/12/30 10:34:09" dans measure.datetime
  Make_Local_TimeStamp(measure.datetime, UTC_time, serial);
}

/*-----------------------------------------------------------------------------*/
/* Remplis un tableau de caractères avec la date et l'heure à partir           */
/* des données exprimées en temps universel.                                   */
/* Intègre les corrections de méridien et les corrections d'heure d'été/hivers.*/
/*-----------------------------------------------------------------------------*/
void Make_Local_TimeStamp(char* timestamp, uint32_t UTC_time, HardwareSerial *serial) {

  struct DateTime date_time;

  if (timezone == -1) {  // Si on n'a pas obtenu les infos de OpenWeather
    date_time.Year = rtc.getYear();
    date_time.Month = rtc.getMonth();
    date_time.Day = rtc.getDay();
    date_time.Hours = ((rtc.getHours() + (byte)GMT_OFFSET + dst_offset) % 24);
    date_time.Minutes = rtc.getMinutes();
    date_time.Seconds = rtc.getSeconds();
  }
  else {  // Si on a obtenu les infos de OpenWeather

    uint32_t Local_time = UTC_time + timezone;
    GetDateTimeFromUnix(&date_time, Local_time);
  }

  Make_TimeStamp(timestamp, &date_time);
}

/*----------------------------------------------------------------------------*/
/* Remplis un tableau de caractères avec la date et l'heure à partir          */
/* des données exprimées en bytes                                             */
/*----------------------------------------------------------------------------*/
void Make_TimeStamp(char* timestamp, struct DateTime* data) {

  char two_bytes[3];

  snprintf(two_bytes, 3, "%02d", data->Year);
  timestamp[0] = two_bytes[0];
  timestamp[1] = two_bytes[1];
  timestamp[2] = '/';

  snprintf(two_bytes, 3, "%02d", data->Month);
  timestamp[3] = two_bytes[0];
  timestamp[4] = two_bytes[1];
  timestamp[5] = '/';

  snprintf(two_bytes, 3, "%02d", data->Day);
  timestamp[6] = two_bytes[0];
  timestamp[7] = two_bytes[1];
  timestamp[8] = ' ';

  snprintf(two_bytes, 3, "%02d", data->Hours);
  timestamp[9] = two_bytes[0];
  timestamp[10] = two_bytes[1];
  timestamp[11] = ':';

  snprintf(two_bytes, 3, "%02d", data->Minutes);
  timestamp[12] = two_bytes[0];
  timestamp[13] = two_bytes[1];
  timestamp[14] = ':';

  snprintf(two_bytes, 3, "%02d", data->Seconds);
  timestamp[15] = two_bytes[0];
  timestamp[16] = two_bytes[1];
  timestamp[17] = '\0';
}

/*----------------------------------------------------------------------------*/
/* Initialisation du service MQTT                                             */
/*----------------------------------------------------------------------------*/
void Initialize_MQTT(HardwareSerial *serial) {

  // Définition du serveur et du port
  MQTT_Client.setServer(TBServer, MQTT_PORT);

  // Autres paramétrages (d'après les exemples de la bibliothèque PubSubClient)
  MQTT_Client.setKeepAlive(MQTT_KEEP_ALIVE);
  MQTT_Client.setSocketTimeout(MQTT_SOCK_TOUT);

  serial->println("MQTT server set to " + (String)TBServer + ":" + String(MQTT_PORT)) ;
  serial->println("MQTT publishing on topic " + (String)MQTT_PUB_TOPIC);
}

/*----------------------------------------------------------------------------*/
/* Construction de la trame MQTT (format JSON)                                */
/*----------------------------------------------------------------------------*/
void Build_Payload(HardwareSerial *serial, char* MQTT_Payload) {

  // Ecris la température dans la chaîne de caractères bufTemp avec une décimale
  char bufTemp[6];
  dtostrf(measure.temperature, 0, 1, bufTemp);

 // Date et heure universelles au format UNIX (Epoch)
  uint32_t UTC_time = rtc.getEpoch();
  char bufEpoch[10];

  // Ecris l'UTC au format UNIX dans bufEpoch
  ltoa(UTC_time, bufEpoch, 10);

  // Ecris l'UTC au format "20/12/30 10:34:09" dans measure.datetime
  Make_Local_TimeStamp(measure.datetime, UTC_time, serial);

  // Construit une trame MQTT conforme à l'API Thingsboard
  String payload = "{";
  payload += "\"ts\":";
  payload += bufEpoch;
  payload += "000";
  payload += ", ";
  payload += "\"values\":";
  payload += "{";
  payload += "\"Time\":\"";
  payload += measure.datetime;
  payload += "\", ";
  payload += "\"Temperature\":";
  payload += bufTemp;
  payload += ", ";
  payload += "\"Pressure\":";
  payload += measure.pressure;
  payload += ", ";
  payload += "\"Humidity\":";
  payload += measure.humidity;
  payload += ", ";
  payload += "\"CO2\":";
  payload += measure.co2;
  payload += "}";
  payload += "}";

  payload.toCharArray( MQTT_Payload, MQTT_PAYLOAD_SIZE );

  serial->println("MQTT post : " + payload);
}

/*----------------------------------------------------------------------------*/
/* Publication des mesures sur Thingsboard                                    */
/*----------------------------------------------------------------------------*/
bool Post_Payload(HardwareSerial *serial, char* MQTT_Payload) {

  bool ret;
  if (MQTT_Reconnect(serial)) {

    if (MQTT_Client.publish(MQTT_PUB_TOPIC, MQTT_Payload)) {
      ret = true;
    }
    else {
      ret = false;
    }
  }
  else {
    ret = false;
  }

  return ret;
}

/*----------------------------------------------------------------------------*/
/* Connexion / reconnexion au serveur MQTT                                    */
/*----------------------------------------------------------------------------*/
bool MQTT_Reconnect(HardwareSerial *serial) {

  bool ret;
  uint8_t n = 0;

  while (!MQTT_Client.connected()) {

    if (!MQTT_Client.connect(MQTT_DEVICE, MQTTtoken, NULL)) {

      IWatchdog.reload();

      if (n == MQTT_RECONNECT_TOUT + 1) {
        ret = false;
        break;
      }
      n++;
      delay(DELAY_5S);
    }
    else {
      ret = true;
    }
  }
  return ret;
}

/*----------------------------------------------------------------------------*/
/* Parcours le fichier datalogfile, poste ses enregistrements sur Thingsboard.    */
/* Efface le fichier une fois l'opération terminée.                           */
/*----------------------------------------------------------------------------*/
void Post_SD_Records(char* datalogfilename, HardwareSerial *serial, char* MQTT_Payload) {

  if (datalogfile.size() != 0) {

    // Ferme datalogfile
    if (datalogfile) datalogfile.close();

    // Ouvre datalogfile en lecture seule
    datalogfile = SD.open(datalogfilename, FILE_READ);

    if (datalogfile) {

      serial->println("Resending MQTT frames stored in SD card");

      uint32_t n = 0;
      String buffer;

      // Parcours datalogfile ligne par ligne (trames MQTT)
      while (datalogfile.available()) {

        // Charge la trame dans buffer
        buffer = datalogfile.readStringUntil('\n');

        // Place le contenu du buffer dans MQTT_Payload
        buffer.toCharArray(MQTT_Payload, MQTT_PAYLOAD_SIZE);

        // Envoi du contenu de MQTT_Payload à Thingsboard
        if (!Post_Payload(serial, MQTT_Payload)) {
          serial->println("\nMQTT post ERROR : " + buffer);
        }
        else {
          serial->println("\nMQTT post : " + buffer);
        }
        // Re-chargement de l'IDWG
        IWatchdog.reload();

        n++;
      }

      if (n) serial->println("Number of MQTT frames sent : " + String(n));

      // Ferme datalogfile et l'efface
      datalogfile.close();
      SD.remove(datalogfilename);

      // Crée un nouveau fichier datalogfile (ouverture obligatoire)
      datalogfile = SD.open(datalogfilename, FILE_WRITE);

      // Ferme immédiatement le nouveau fichier
      datalogfile.close();

      // Software reset, pour ne pas segmenter la mémoire à force d'ouvertures et
      // fermetures de fichiers
      HAL_NVIC_SystemReset();

    } else {
      serial->println("error opening " + (String)datalogfilename);
      while (true);
    }
  }
}

/*----------------------------------------------------------------------------*/
/* Récupère les informations météo avec Open Weather Map                        */
/*----------------------------------------------------------------------------*/
uint32_t getWeatherForecast(HardwareSerial *serial) {

  uint32_t tzone = 0;

  serial->println("\nConnecting to " + (String)owmServer);

  // Connexion au serveur OpenWeather
  if (espClient.connect(owmServer, OWM_SERVER_PORT)) {

    // Requête HTTP au serveur OpenWeather
    espClient.print("GET /data/2.5/forecast?");
    espClient.print("lat=" + String(MyLocation.Latitude));
    espClient.print("&lon=" + String(MyLocation.Longitude));
    espClient.print("&appid=" + (String)OWMKey);
    espClient.print("&cnt=" + String(owmFcst));
    espClient.println("&units=metric HTTP/1.1");
    espClient.println("Host: " + (String)owmServer);
    espClient.println("Connection: close");
    espClient.println();

    // Attend la réponse du serveur
    uint32_t _startMillis = millis();
    while (!espClient.available() && (millis() - _startMillis < DELAY_5S));

    // Lecture de la réponse du serveur dans l'objet String owmServerResponse
    String owmServerResponse = "";
    while (espClient.available()) {
      owmServerResponse = espClient.readStringUntil('\n');
    }

    // Si la réponse du serveur n'est pas nulle ...
    if (owmServerResponse.length()) {

      // serial->println("Raw meteo data (JSON) : " + owmServerResponse);

      // Crée un buffer pour accueillir des données indexées au format JSON
      StaticJsonDocument<5000> JsonDoc;

      // Convertit le contenu de owmServerResponse en JSON
      DeserializationError error = deserializeJson(JsonDoc, owmServerResponse);

      // Si conversion OK, extrait de JsonDoc les champs qui nous intéressent
      if (!error) {

        // Pression au niveau de la mer
        String sealvl = JsonDoc["list"][0]["main"]["sea_level"];
        // Agglomération la plus proche
        String place = JsonDoc["city"]["name"];
        // Pays
        String country = JsonDoc["city"]["country"];
        // Heure lever de soleil, au format EPOCH
        String sunrise = JsonDoc["city"]["sunrise"];
        // Heure coucher de soleil, au format EPOCH
        String sunset = JsonDoc["city"]["sunset"];

        // time_zone contiendra le nombre de secondes à ajouter ou sosutraire
        // au temps UTC pour obtenir l'heure locale correcte incluant
        // les corrections méridienne et saisonnière.
        String time_zone = JsonDoc["city"]["timezone"];
        tzone = time_zone.toInt();

        uint32_t Sunrise_time = sunrise.toInt();
        uint32_t Sunset_time = sunset.toInt();

        // Convertit les dates & heures au format UNIX UTC (EPOCH)
        //en versions lisibles

        struct DateTime sunrise_date_time;
        GetDateTimeFromUnix(&sunrise_date_time, Sunrise_time);
        char srdt[18];
        Make_TimeStamp(srdt, &sunrise_date_time);

        struct DateTime sunset_date_time;
        GetDateTimeFromUnix(&sunset_date_time, Sunset_time);
        char ssdt[18];
        Make_TimeStamp(ssdt, &sunset_date_time);

        // Affiche les données météo sur le port série
        serial->println("\nClosest town : " + place + " (" + country + ")" );
        serial->println("Time zone shift, including daylight saving time : "
                        + time_zone + "s");
        serial->println("Sunrise : " + (String)srdt);
        serial->println("Sunset : " + (String)ssdt);
        serial->println("Pressure : " + sealvl + " hPa");
        serial->println("\nForecasted weather conditions :");

        // Tableau de structures Forecast pour charger les owmFcst prévisions
        struct Forecast weather_forecast[owmFcst];

        struct DateTime forecast_date_time;

        for (uint8_t i = 0; i < owmFcst; i++) {

          String nextWeatherTime = JsonDoc["list"][i]["dt"];
          String nextWeatherDescription =
            JsonDoc["list"][i]["weather"][0]["description"];

          uint32_t nextWeather_time = nextWeatherTime.toInt();
          GetDateTimeFromUnix(&forecast_date_time, nextWeather_time);
          char sdt[18];
          Make_TimeStamp(sdt, &forecast_date_time);
          strcpy(weather_forecast[i].date_time, sdt);

          nextWeatherDescription.toCharArray(weather_forecast[i].description, 39);

          serial->println(" On " + (String)weather_forecast[i].date_time
                          + " : " + nextWeatherDescription);
        }
      }
      else {
        serial->println("deserializeJson() failed");
      }
    }
    else {
      serial->println("No response from server");
    }
  } else {
    serial->println("Unable to connect to server");
  }
  return tzone;
}

/*------------------------------------------------------------------------------------------*/
/* Conversion valeur epoch UNIX en date - heure                                             */
/*------------------------------------------------------------------------------------------*/
int GetDateTimeFromUnix(DateTime* data, uint32_t unix) {

  uint16_t year;

  /* Get seconds from unix */
  data->Seconds = unix % 60;
  /* Go to minutes */
  unix /= 60;
  /* Get minutes */
  data->Minutes = unix % 60;
  /* Go to hours */
  unix /= 60;
  /* Get hours */
  data->Hours = unix % 24;
  /* Go to days */
  unix /= 24;

  /* Get week day */
  /* Monday is day one */
  data->WeekDay = (unix + 3) % 7 + 1;

  /* Get year */
  year = 1970;
  while (true) {
    if (LPYR(year)) {
      if (unix >= 366) {
        unix -= 366;
      } else {
        break;
      }
    } else if (unix >= 365) {
      unix -= 365;
    } else {
      break;
    }
    year++;
  }
  /* Get year in xx format */
  data->Year = (uint8_t) (year - 2000);
  /* Get month */
  for (data->Month = 0; data->Month < 12; data->Month++) {
    if (LPYR(year)) {
      if (unix >= (uint32_t)RTC_Months[1][data->Month]) {
        unix -= RTC_Months[1][data->Month];
      } else {
        break;
      }
    } else if (unix >= (uint32_t)RTC_Months[0][data->Month]) {
      unix -= RTC_Months[0][data->Month];
    } else {
      break;
    }
  }
  /* Get month */
  /* Month starts with 1 */
  data->Month++;
  /* Get date */
  /* Date starts with 1 */
  data->Day = unix + 1;

  /* Return OK */
  return 1;
}

/*----------------------------------------------------------------------------*/
/* Lecture des entrées sur un port série.                                     */
/* La séquence CR-LF ("\r\n") valide la saisie                                */
/* Retourne le nombre de caractères lus                                       */
/*----------------------------------------------------------------------------*/
uint16_t processSerial(HardwareSerial *serial, char* serial_buffer) {

  // Nombre de caractères reçus
  uint16_t strLen = 0;

  // Le premier caractère est '\0' (terminaison de chaîne)
  serial_buffer[0] = '\0';

  // Boucle sans clause de sortie pour pour suspendre l'exécution
  // jusqu'à la saisie complète lorsque processSerial est appelée
  // depuis Update_Wifi_Credentials
  while (true) {

    // Si on reçois des caractères
    if (serial->available()) {

      // Lis un caractère
      char inChar = (char)serial->read();

      // Si ce caractère est LF ('\n') ...
      if (inChar == '\n') {
        // ... ajoute un caractère '\0' à la fin du buffer
        serial_buffer[strLen - 1] = '\0';
        break; // ... quitte la boucle while (fin de réception)
      }

      // Autrement, aussi longtemps qu'on ne dépasse pas la taille
      // du buffer
      else if ((strLen < (MAX_LEN - 1))) {
        // ajoute au buffer le dernier caractère reçu
        serial_buffer[strLen++] = inChar;
      }
    }
    else if (setup_completed) {
      break;
    }
  }

  return strLen; // Renvoie le nombre de caractères lus.
}

/*----------------------------------------------------------------------------*/
/* Met à jour les informations de login WiFi et le enregistre en EEPROM       */
/*----------------------------------------------------------------------------*/
void Update_Wifi_Credentials(HardwareSerial *serial) {

  // Si le bouton utilisateur est enfoncé à ce moment, met à '\0' le premier
  // caractère dans l'EEPROM simulée
  if (!digitalRead(USER_BTN)) EEPROM.write(0, '\0');

  uint8_t ret, i;

  // Si ce caractère est zéro '\0', lance la demande de saisie du SSID et du PWD
  if (!EEPROM.read(0)) {

    BT_Serial.println("WiFi configuration mode set");
    serial->println("WiFi configuration mode set");

    // Liste des réseaux Wi-Fi disponibles
    listWiFiNetworks(serial);

    // Demande de saisie de l'identifiant du réseau Wi-Fi
    BT_Serial.println("Enter Wi-Fi SSID (" + String(MAX_LEN) + " char. max.) : ");

    // Acquisition du SSID sur le port série du module HC-06, dans BT_Buffer
    if (!processSerial(&BT_Serial, BT_Buffer)) {
      serial->println("Error while processing serial : null length SSID");
      while (true);
    }

    BT_Serial.println("Saved SSID : " + String(BT_Buffer));

    // Copie du buffer du port série dans le tableau ssid et, en cas d'erreur ...
    ret = copy_arrays(BT_Buffer, ssid);
    if (ret) {
      serial->print("Error while copying SSID : " + String(ret));
      while (true);
    }

    serial->println("Saved SSID : " + String(ssid));

    // Demande de saisie du mot de passe du réseau Wi-Fi
    BT_Serial.println("Enter Wi-Fi password (" + String(MAX_LEN) + " char. max.) : ");

    // Acquisition du mot de passe sur le port série du module HC-06, dans BT_Buffer
    if (!processSerial(&BT_Serial, BT_Buffer)) {
      serial->println("Error while processing serial : null length password");
      while (true);
    }

    BT_Serial.println("Saved password : " + String(BT_Buffer));

    // Copie du buffer du port série dans le tableau ssid et, en cas d'erreur ...
    ret = copy_arrays(BT_Buffer, pass);
    if (ret) {
      serial->print("Error while copying password : " + String(ret));
      while (true);
    }

    serial->println("Saved password : " + String(pass));

    // Ecrit le SSID l'EEPROM du STM32L476
    for (uint16_t i = 0; i < strlen(ssid); i++) EEPROM.write(i, (byte)ssid[i]);

    // Termine par le caractère zéro pour clôturer la chaîne
    EEPROM.write(strlen(ssid), '\0');

    // Ecrit le PWD l'EEPROM du STM32L476, MAX_LEN + 1 bytes plus loin
    for (uint16_t i = 0 ; i < strlen(pass); i++)
      EEPROM.write(i + MAX_LEN + 1, (byte)pass[i]);

    // Termine par le caractère zéro pour clôturer la chaîne
    EEPROM.write(strlen(pass) + MAX_LEN + 1, '\0');

    serial->println("WiFi credentials successfully saved inside EEPROM");
  }

  // Si le SSID et le PWD sont déjà écrits en EEPROM alors lis les et
  // charge les en mémoire
  else {

    serial->println("Reading WiFi credentials from EEPROM");

    // Lis jusqu'au prochain caractère zéro de terminaison de chaîne
    for (uint16_t i = 0; i < MAX_LEN; i++) {
      BT_Buffer[i] = (char)EEPROM.read(i);
      if (!BT_Buffer[i]) break;
    }

    // Copie le SSID dans le tableau ssid et, en cas d'erreur, ...
    ret = copy_arrays(BT_Buffer, ssid);
    if (ret) {
      serial->println("SSID copy error code : " + String(ret));
      EEPROM.write(0, '\0');
      serial->println("Resetting WiFi credentials");
      delay(5000);
      HAL_NVIC_SystemReset();
    }

    serial->println("Loaded SSID : " + String(ssid));

    // Lis jusqu'au prochain caractère zéro de terminaison de chaîne
    for (uint16_t i = 0; i < MAX_LEN; i++) {
      BT_Buffer[i] =  (char)EEPROM.read(i + MAX_LEN + 1);
      if (!BT_Buffer[i]) break;
    }

    // Copie le PWD dans le tableau pass et, en cas d'erreur, ...
    ret = copy_arrays(BT_Buffer, pass);
    if (ret) {
      serial->println("PWD copy error code : " + String(ret));
      EEPROM.write(0, '\0');
      serial->println("Resetting WiFi credentials");
      delay(5000);
      HAL_NVIC_SystemReset();
    }

    serial->println("Loaded password : " + String(pass));
  }
}

/*----------------------------------------------------------------------------*/
/* Copie un tableau de caractères dans un autre                              */
/*----------------------------------------------------------------------------*/
uint8_t copy_arrays(char* input, char* output) {

  int retval = 0;
  int input_len = strlen(input);
  int output_len = MAX_LEN;

  // Si on est certain que le tableau d'entrée tiendra dans celui
  // de sortie
  if (input_len < output_len + 1 ) {
    // Fais la copie
    for (uint8_t i = 0; i < input_len; i++) {
      output[i] = input[i];
    }
  }
  // Si l'un des deux tableaux au moins est de longueur nulle ....
  else if (!input_len || !output_len) {
    retval = 1; // retourne le code d'erreur "1"
  }
  // Si le tableau d'entrée est trop long pour être copié dans celui
  // de sortie
  else if (input_len > output_len) {
    retval = 2; // retourne le code d'erreur "2"
  }
  return retval;
}

/*----------------------------------------------------------------------------*/
/* Gestion des interruption du module Bluetooth / UART4                       */
/*----------------------------------------------------------------------------*/
void BT_Serial_ISR() {

  if (processSerial(&BT_Serial, BT_Buffer)) {

    Serial.print("\nUser command is ");
    Serial.println(BT_Buffer);

    if (!strcmp(BT_Buffer, "SD_ERASE")) {
      sd_card_erasing = true;
    }
    else if (!strcmp(BT_Buffer, "SD_LIST")) {
      sd_card_listing = true;
    }
    else if (!strcmp(BT_Buffer, "MEASURE")) {
      measure_now = true;
    }
    else if (!strcmp(BT_Buffer, "RESET")) {
      reset_now = true;
    }
    else {

      // Treatment of parameter-enriched commands

      char command[MAX_LEN];
      char value[MAX_LEN - 14];

      for (uint16_t i = 0; i < 13; i++) {
        command[i] = BT_Buffer[i];
      }
      command[13] = '\0';

      uint16_t strLen = 0;

      for (uint16_t i = 13; i < MAX_LEN; i++) {
        value[strLen++] = BT_Buffer[i];
        if (!BT_Buffer[i]) break;
      }

      if (!strcmp(command, "SET_LOC_ALTI:")) {
        MyLocation.Altitude = atof(value);
        save_coordinates = true;
      }
      else if (!strcmp(command, "SET_LOC_LONG:") == 0) {
        MyLocation.Longitude = atof(value);
        save_coordinates = true;
      }
      else if (!strcmp(command, "SET_LOC_LATI:")) {
        MyLocation.Latitude = atof(value);
        save_coordinates = true;
      }
      else if (!strcmp(command, "SET_MQTT_TKN:")) {
        strcpy(MQTTtoken, value);
        save_mqtt_token = true;
      }
      else if (!strcmp(command, "SET_WMAP_KEY:")) {
        strcpy(OWMKey, value);
        save_owm_key = true;
      }
      else {
        BT_Commands_Listing(&BT_Serial);
      }
    }
  }
}

/*----------------------------------------------------------------------------*/
/* Sauvegarde de la position géographique dans l'EEPROM                       */
/*----------------------------------------------------------------------------*/
void Save_Coordinates(void) {
  uint16_t offset = 2 * MAX_LEN + 1;
  EEPROM.put(offset, MyLocation);
}

/*----------------------------------------------------------------------------*/
/* Sauvegarde de la clef Open Weather Maps dans l'EEPROM                      */
/*----------------------------------------------------------------------------*/
void Save_OWM_Key(void) {
  uint16_t offset = 2 * MAX_LEN + 2 + sizeof(MyLocation);
  EEPROM.put(offset, OWMKey);
}

/*----------------------------------------------------------------------------*/
/* Sauvegarde du token (clef) ThingsBoard dans l'EEPROM                       */
/*----------------------------------------------------------------------------*/
void Save_MQTT_Key(void) {
  uint16_t offset = 2 * MAX_LEN + 3 + sizeof(MyLocation) + sizeof(OWMKey);
  EEPROM.put(offset, MQTTtoken);
}

/*----------------------------------------------------------------------------* /
  /* Listing des commandes Bluetooth sur le port série passé en argument        */
/*----------------------------------------------------------------------------*/
void BT_Commands_Listing(HardwareSerial *serial) {

  serial->println("\nAllowed commands : ");
  serial->println(" MEASURE : force a measurement right now");
  serial->println("    Input example : \"MEASURE\"");
  serial->println(" SD_ERASE : erase SD card log file");
  serial->println("    Input example : \"SD_ERASE\"");
  serial->println(" SD_LIST : list SD card log file");
  serial->println("    Input example : \"SD_LIST\"");
  serial->println(" RESET : reset station");
  serial->println("    Input example : \"RESET\"");
  serial->println(" SET_LOC_ALTI : set altitude in meters");
  serial->println("    Input example : \"SET_LOC_ALTI:485\"");
  serial->println(" SET_LOC_LATI : set latitude in decimal degrees");
  serial->println("    Input example : \"SET_LOC_LATI:43.75\"");
  serial->println(" SET_LOC_LONG : set longitude in decimal degrees");
  serial->println("    Input example : \"SET_LOC_LONG:5.45\"");
  serial->println(" SET_MQTT_TKN : set MQTT token (20 alphanumercis chars)");
  serial->println("    Input example : \"SET_MQTT_TKN:ZbHxwsU0rK8RZH5T0tpW\"");
  serial->println(" SET_WMAP_KEY : set Open Weather Maps key (32 alphanumerics chars)");
  serial->println("    Input example : \"SET_WMAP_KEY:5049cbe22ad0eb847b71ca1df47657aa\"");
}

/*----------------------------------------------------------------------------*/
/* Chargement des paramètres utilisateur depuis l'EEPROM :                    */
/* Altitude (m) : float                                                       */
/* Latitude (decimal degrees) : float                                         */
/* Longitude (decimal degrees) : float                                        */
/* Clef MQTT pour ThingsBoard 20 chars (ex: "ZbHdwsX0rK8RZH5y0tpU")           */
/* Clef Open Weather Maps 32 chars (ex : "5049ceb22ad0be847b71ac1df47657aa")  */
/*----------------------------------------------------------------------------*/
void Load_User_Parameters(HardwareSerial *serial) {

  uint16_t offset = 2 * MAX_LEN + 1;
  serial->println("Loading geographic location");

  EEPROM.get(offset, MyLocation);

  gmt_offset = (byte)ceil(MyLocation.Longitude / 15);

  serial->println("- Latitude : " + (String)MyLocation.Latitude);
  serial->println("- Longitude : " + (String)MyLocation.Longitude + " ; GMT offset is " + String(3600 * gmt_offset) + "s");
  serial->println("- Altitude : " + (String)MyLocation.Altitude);

  offset += sizeof(MyLocation) + 1;
  EEPROM.get(offset, OWMKey);

  offset += sizeof(OWMKey) + 1;
  EEPROM.get(offset, MQTTtoken);

  serial->println("Open Weather Maps key : " + (String)OWMKey);
  serial->println("MQTT token : " + (String)MQTTtoken);
}