Gérer la date, l’heure et le fuseau horaire

Le sketch « time_management.ino » ajoute des fonctions de gestion du temps à «wifi_connection.ino ». Plus précisément, il montre comment :

  1. Utiliser la RTC du STM32L476RG pour horodater les enregistrements ;
  2. Recaler périodiquement l’horloge de la RTC grâce à un serveur de temps universel sur Internet ;
  3. Ajuster l’horloge de la RTC en tenant compte de l’heure saisonnière appliquée en France .

Dans les déclarations globales nous avons apporté les modifications suivantes :

  • Ajout d’un champs char datetime[18], un tableau de 18 caractères, dans Measure :
035 struct Measure {
036   char datetime[18];
037   float temperature;
038   uint16_t pressure;
039   uint8_t humidity;
040   uint16_t co2;
041 };
  • Ajout des déclarations pour la RTC :
110 // Déclarations pour la RTC
111 #include <STM32RTC.h>
112 STM32RTC& rtc = STM32RTC::getInstance();
113 
114 #define SET_RTC_MOD (300) // Sur-période de recalage de la RTC
115 uint32_t rtc_mod;
  • Ajout des déclarations pour le service UDP et le protocole NTP :
117 // Déclarations pour le service UDP
118 #include <WiFiEspUdp.h>
119 #define UDP_PORT (2390)
120 WiFiEspUDP Udp;
121
122 // Adresses IP de deux serveurs NTP
123 #define NTP_SERVER_1  "216.239.35.4"
124 #define NTP_SERVER_2  "time.nist.gov"
125
126 char TimeServer1[] = NTP_SERVER_1;
127 char TimeServer2[] = NTP_SERVER_2;


137 struct DateTime {
138   byte Year;
139   byte Month;
140   byte Day;
141   byte Hours;
142   byte Minutes;
143   byte Seconds;
144   byte WeekDay;
145 };
  • Ajout des déclarations pour le fuseau horaire et l’ajustement d’heure saisonnier :
129 // Déclarations pour le fuseau horaire et l’ajustement d’heure saisonnier 
130 #define SUMMER_TIME_MONTH (3) // Mois du passage à l'heure d'été
131 #define WINTER_TIME_MONTH (10) // Mois du passage à l'heure d'hivers
132 #define DST_WEEKDAY (7) // Jour du changement d'heure saisonnier
133 #define SECS_PER_DAY (86400) // Dans une journée on a 86400 secondes
134 #define GMT_OFFSET (+1) // Ajustement d'heure dû au fuseau horaire
135 byte dst_offset; // Ajustement d'heure saisonnier

Dans setup nous avons ajouté le code pour démarrer la RTC et gérer sa fréquence de recalage :

164   // Démarre la RTC
165   rtc.setClockSource(STM32RTC::LSE_CLOCK); // Choix de la source d'horloge
166   rtc.begin();
167 
168   // Modulo pour les recalages de la RTC
169   rtc_mod = SET_RTC_MOD;

La ligne 165 précise que la RTC devra prendre comme source l’oscillateur à quartz externe installé sur le module ST-LINK sécable de la carte NUCLEO.

Les modifications de loop (lignes 212 à 225 et 239-240) figurent ci-après :

210 void loop() {
211 
212   if (rtc_mod == SET_RTC_MOD) { // Toutes les SET_RTC_MOD itérations...
213 
214     rtc_mod = 0;
215 
216     // Re-connexion du module Wi-Fi au réseau spécifié
217     Connect_WiFi(&Serial);
218 
219     // Acquisition NTP de la date et de l'heure de la mesure, recalage de la RTC
220     Set_RTC_Time(&Serial);
221 
222     // Ré-arme l'IDWG
223     IWatchdog.reload();
224 
225   } // Clôture de if (rtc_mod == SET_RTC_MOD)
226 
227   rtc_mod++;
228 
229   if (rec_mod == SET_REC_MOD) { // Toutes les SET_REC_MOD itérations...
230 
231     rec_mod = 0;
232 
233     // Allume la LED utilisateur
234     digitalWrite(LED_BUILTIN, HIGH);


239     // Construit la trame de mesures Record_Payload
240     Build_Payload(&Serial, Record_Payload);


245     // Eteint la LED utilisateur
246     digitalWrite(LED_BUILTIN, LOW);
247 
248   } // Clôture de if (rec_mod == SET_REC_MOD)
249 


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

La reconnexion éventuelle au réseau Wi-Fi et le recalage de la RTC n’ont pas besoin d’être réalisés aussi fréquemment que les mesures, c’est pourquoi on les place dans une autre temporisation (ligne 212) : ils ont lieu toutes les SET_RTC_MOD = 300 itérations.

La fonction Set_RTC_Time est plus élaborée que la plupart de celles que nous avons étudiées jusqu’ici. Voici son code complet, commenté aussi clairement que possible :

595 void Set_RTC_Time(HardwareSerial *serial) {
596 
597   serial->println("NTP call and RTC setting");
598 
599   uint8_t n = 0;
600   bool try_alternative_ntp_server = false;
601 
602   while (true) {
603 
604     uint32_t UTC_NOW;
605 
606     // Récupère l'heure et la date universelles de l'instant
607     if (!try_alternative_ntp_server) {
608       UTC_NOW = getUTC(TimeServer1); // Interroge le serveur NTP n°1
609     }
610     else { // Si le serveur NTP n°1 n'a pas répondu...
611       UTC_NOW = getUTC(TimeServer2); //... interroge le serveur NTP n°2
612     }
613 
614     // Si la requête au serveur NTP est un succès
615     if (UTC_NOW) {
616 
617       // Règle le calendrier et l'horloge de la RTC avec le temps universel...
618       rtc.setEpoch(UTC_NOW);
619 
620       //... pour en extraire l'année courante
621       byte Current_Year = rtc.getYear();
622 
623       // Obtient la date du changement d'heure en été pour l'année en cours
624       uint32_t UTC_SUMMER = Get_DST_Date(Current_Year, SUMMER_TIME_MONTH, DST_WEEKDAY);
625 
626       // Obtient la date du changement d'heure en hivers pour l'année en cours
627       uint32_t UTC_WINTER = Get_DST_Date(Current_Year, WINTER_TIME_MONTH, DST_WEEKDAY);
628 
629       // Si on n'est pas encore en été ou si on est en hivers...
630       if (UTC_NOW < UTC_SUMMER || UTC_NOW > UTC_WINTER) {
631         dst_offset = 0; //... pas d'ajustement
632       }
633       // Si on est en été ou en automne
634       else if (UTC_NOW > UTC_SUMMER && UTC_NOW < UTC_WINTER) {
635         dst_offset = 1;  //... rajoute 1 heure
636       }
637       serial->println("DST shift set to +" + String(dst_offset) + " hour(s)");
638 
639       // Le calcul de l'ajustement d'heure a utilisé et de ce fait déréglé la RTC.
640       // Nous devons à nouveau interroger le serveur NTP pour la remettre à l'heure.
641       // Nous imposons une pause de DELAY_5S avant d'interroger à nouveau le NTP pour 
642       // que celui-ci ne rejette pas notre requête.
643 
644       delay(DELAY_5S);
645 
646       if (!try_alternative_ntp_server) {
647         UTC_NOW = getUTC(TimeServer1);
648       }
649       else {
650         UTC_NOW = getUTC(TimeServer2);
651       }
652 
653       rtc.setEpoch(UTC_NOW);
654       break;
655     }
656     else { // Si la requête au serveur NTP a échoué
657 
658       serial->println("NTP failure : could not get UTC");
659       try_alternative_ntp_server = true;
660       n++;
661 
662       // Ré-initialisation du compte à rebours de l'IDWG
663       IWatchdog.reload();
664 
665       // Si c'est le deuxième échec consécutif, software reset
666       if (n > RESET_RETRY) HAL_NVIC_SystemReset();
667       delay(DELAY_5S);
668     }
669   }
670 }

Set_RTC_Time récupère le temps universel non coordonné (UTC) avec le le Network Time Prtotocol (NTP). Elle gère également l’heure d’hivers / d’été en France (changements les derniers dimanches des mois de mars et octobre).

Pour mettre à jour l’heure avec le protocole NTP on utilise la fonction getUTC aux lignes 608, 611, 647 et 650. Son code, directement copié des exemples de la bibliothèque WiFiEsp est détaillé ci-après :

740 uint32_t getUTC(char *ntpSrv) {
741 
742   const uint32_t NTP_PACKET_SIZE = 48;
743   const uint32_t UDP_TIMEOUT = 2000;
744   byte packetBuffer[NTP_PACKET_SIZE];
745 
746   uint32_t UTC = 0;
747   memset(packetBuffer, 0, NTP_PACKET_SIZE);
748 
749   packetBuffer[0] = 0b11100011;
750   packetBuffer[1] = 0;
751   packetBuffer[2] = 6;
752   packetBuffer[3] = 0xEC;
753 
754   packetBuffer[12]  = 49;
755   packetBuffer[13]  = 0x4E;
756   packetBuffer[14]  = 49;
757   packetBuffer[15]  = 52;
758 
759   Udp.begin(UDP_PORT);
760   Udp.beginPacket(ntpSrv, 123); //NTP requests are to port 123
761   Udp.write(packetBuffer, NTP_PACKET_SIZE);
762   Udp.endPacket();
763 
764   uint32_t startMs = millis();
765   while (!Udp.available() && (millis() - startMs) < UDP_TIMEOUT);
766 
767   if (Udp.parsePacket()) {
768 
769     Udp.read(packetBuffer, NTP_PACKET_SIZE);
770     uint32_t highWord = word(packetBuffer[40], packetBuffer[41]);
771     uint32_t lowWord = word(packetBuffer[42], packetBuffer[43]);
772     uint32_t secsSince1900 = highWord << 16 | lowWord;
773     const uint32_t seventyYears = 2208988800UL;
774 
775     UTC = secsSince1900 - seventyYears;
776     Udp.stop();
777   }
778   return UTC;
779 }

La fonction Get_DST_Date, appelée aux lignes 624 et 627, détermine les dates des changements d’heure en été et en hivers. Elle est codée comme suit :

699 uint32_t Get_DST_Date(byte Year, byte Month, byte WeekDay) {
700 
701   const byte day_one = 1;
702 
703   // L'heure avant le changement est toujours 3h00
704   const byte dsts = 0;
705   const byte dstm = 0;
706   const byte dsth = 3;
707   rtc.setTime(dsth, dstm, dsts);
708 
709   // On initialise la RTC au premier jour du mois du changement d'heure
710   rtc.setDay(day_one);
711   rtc.setMonth(Month);
712   rtc.setYear(Year);
713 
714   /* On récupère le temps universel et on re-paramètre l'horloge avec setEpoch()  */
715   /* afin que la fonction getWeekDay() renvoie par la suite des valeurs correctes */
716 
717   // Temps universel du premier jour du mois du changement d'heure
718   uint32_t utc = rtc.getEpoch();
719   rtc.setEpoch(utc);
720 
721   // Calcul itératif de la date exacte du changement d'heure en France :
722   // Renvoie le DERNIER WeekDay du mois en question.
723 
724   byte WeekDay_ = rtc.getWeekDay();
725   byte Month_ = rtc.getMonth();
726   uint32_t utc_ = 0;
727 
728   // Aussi longtemps que l'on reste dans le mois sélectionné
729   while (Month_ == Month) {
730 
731     // Si le jour est susceptible d'être celui du changement d'heure
732     if (WeekDay_ == WeekDay) {
733       utc_ = utc;  // alors sauvegarde le temps universel de ce jour
734     }
735     
736     // Passe au jour suivant
737     utc = utc + SECS_PER_DAY; 
738     
739     // Avance la RTC d'un jour
740     rtc.setEpoch(utc); 
741     
742     WeekDay_ = rtc.getWeekDay();
743     Month_   = rtc.getMonth();
744   }
745   
746   // A la sortie de la boucle, _utc contient bien le DERNIER WeekDay du mois
747   // du changement d'heure !
748   
749   return utc_;
750 }

Enfin, la fonction Build_Payload (ligne 240) va rajouter aux mesures une « étiquette » d’horodatage à l’aide de la fonction Make_Local_TimeStamp (ligne 789) qui appelle à son tour la fonction Make_TimeStamp (ligne 822). Voici leurs codes :

784 void Build_Payload(HardwareSerial *serial, char* Payload) {
785 
786   char bufEpoch[10];
787   uint32_t UTC_time = rtc.getEpoch();
788 
789   Make_Local_TimeStamp(measure.datetime, UTC_time, serial);
790 
791   // Construit un objet "String" nommé "payload" à partir des mesures
792   String payload =  String(measure.datetime) + ";"
793                     + String(measure.temperature) + ";"
794                     + String(measure.pressure) + ";"
795                     + String(measure.humidity);
796 
797   // Envoie le contenu de payload dans le tableau de caractères Record_Payload
798   payload.toCharArray( Record_Payload, RECORD_PAYLOAD_SIZE );
799 
800   // Affichage des mesures sur le port série passé en argument
801   serial->println("Payload : " + payload);
802 }
809 void Make_Local_TimeStamp(char* timestamp, uint32_t UTC_time, HardwareSerial *serial) {
810 
811   DateTime date_time;
812 
813   date_time.Year = rtc.getYear();
814   date_time.Month = rtc.getMonth();
815   date_time.Day = rtc.getDay();
816   // Le modulo sur 24 heures (%24) ci-dessous permet de conserver une heure
817   // entre 00:00 et 23:59 après les corrections saisonnières et méridiennes
818   date_time.Hours = ((rtc.getHours() + (byte)GMT_OFFSET + dst_offset) % 24);
819   date_time.Minutes = rtc.getMinutes();
820   date_time.Seconds = rtc.getSeconds();
821 
822   Make_TimeStamp(timestamp, &date_time);
823 }

Make_Local_TimeStamp range les relevés de la RTC dans la variable date_time de type DateTime (structure définie aux lignes 137 à 145) puis la passe comme argument à Make_TimeStamp. Remarquez le modulo sur 24 heures à la ligne 818 pour que l’heure reste entre 0 et 23 après l’ajout des corrections méridienne et saisonnière.

829 void Make_TimeStamp(char* timestamp, DateTime* data) {
830 
831   char two_bytes[3];
832 
833   snprintf(two_bytes, 3, "%02d", data->Year);
834   timestamp[0] = two_bytes[0];
835   timestamp[1] = two_bytes[1];
836   timestamp[2] = '/';
837 
838   snprintf(two_bytes, 3, "%02d", data->Month);
839   timestamp[3] = two_bytes[0];
840   timestamp[4] = two_bytes[1];
841   timestamp[5] = '/';
842 
843   snprintf(two_bytes, 3, "%02d", data->Day);
844   timestamp[6] = two_bytes[0];
845   timestamp[7] = two_bytes[1];
846   timestamp[8] = ' ';
847 
848   snprintf(two_bytes, 3, "%02d", data->Hours);
849   timestamp[9] = two_bytes[0];
850   timestamp[10] = two_bytes[1];
851   timestamp[11] = ':';
852 
853   snprintf(two_bytes, 3, "%02d", data->Minutes);
854   timestamp[12] = two_bytes[0];
855   timestamp[13] = two_bytes[1];
856   timestamp[14] = ':';
857 
858   snprintf(two_bytes, 3, "%02d", data->Seconds);
859   timestamp[15] = two_bytes[0];
860   timestamp[16] = two_bytes[1];
861   timestamp[17] = '\0';
862 }

Make_TimeStamp convertit et agrège les champs Year, Month, Day … de date_time dans le tableau de caractères timestamp. Pour convertir les valeurs numériques des champs de date_time dans les chaines de caractères qui les représentent, on a recours à la fonction snprintf (lignes 833, 838, 843, 848, 853, 858).
Le tableau timestamp est terminé par le caractère \0 (ligne 861) ce qui, conformément aux exigences du langage C, permettra au compilateur de le traiter comme une chaîne de 18 caractères (indexés de 0 à 17).