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 :
- Utiliser la RTC du STM32L476RG pour horodater les enregistrements ;
- Recaler périodiquement l’horloge de la RTC grâce à un serveur de temps universel sur Internet ;
- 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).