vendredi 3 avril 2020

ARDUINO : NTP sur Ethernet



ARDUINO : NTP sur Ethernet


Le problème se pose souvent lorsque l'on travaille avec un ARDUINO et une carte Ethernet W5100 ou W5500 : comment récupérer l'heure sans horloge RTC (Real Time Clock) ?

Cette petite librairie va être très utile. Elle permet de récupérer l'heure UTC (Coordinated Universal Time) ou locale à partir d'un serveur (pool.ntp.org par défaut).

Elle possède un mécanisme permettant d'appliquer une correction en fonction du fuseau horaire et des règles de changement d'heure été / hiver (Timezones).

Sur ESP8266 ou ESP32 on utilisera une autre solution. Voir en fin d'article.


1. Le besoin

Cette librairie a été conçue dans le but de pouvoir utiliser les 3 systèmes de gestion de l'heure les plus courants sur ARDUINO :
  • librairie standard C :
    • time()
    • gmtime()
    • localtime()
    • ctime()
    • strftime()
    • etc.
  • librairie TimeLib de Paul Stoffregen (optionnelle) :
    • year()
    • month()
    • day()
    • hour()
    • minute()
    • second()
    • etc.
  • librairie AdaFruit RtcLib (optionnelle)
La possibilité d'adopter une librairie connue permettra d'une part de satisfaire les besoins et habitudes spécifiques de chacun et d'autre part d'intégrer au besoin une horloge RTC DS3231.

On peut se poser la question de l'utilité d'une horloge RTC lorsque l'on dispose d'une connexion Ethernet.
Cette possibilité peut être utile lorsque cette connexion n'est pas permanente. Bien sûr dans ce cas on imagine assez bien que l'ARDUINO ne sera pas utilisé en tant que serveur WEB.
Pour cette raison j'ai choisi d'offrir la possibilité d'intégrer uniquement un DS3231, bien plus précis qu'un DS1307.

Un ordinateur du type PC dispose également d'une horloge RTC, et cette horloge est mise à jour périodiquement lorsque le PC est relié au réseau. J'ai repris le même principe ici.

    1.1. Heure UTC et locale

    L'heure UTC est l'heure au méridien de Greenwich (Greenwich Mean Time, abrégé en GMT). Elle correspond au fuseau horaire UTC+0.

    L'heure locale est l'heure correspondant à un fuseau horaire donné. Cette heure locale, suivant les pays, peut être corrigée en fonction d'horaires été / hiver.

    Afin de ne pas avoir à remettre à l'heure une horloge à chaque changement d'heure on utilise des règles de changement d'heure. En France elles sont les suivantes :
    • dernier dimanche de mars à 2:00 : GMT+2
    • dernier dimanche d'octobre à 3:00 : GMT+1
    Comme beaucoup de systèmes UNIX existants l'heure interne est stockée sous la forme UTC. Elle est récupérable directement à l'aide de la fonction time() de la Librairie standard C.
    L'heure est ensuite corrigée par la fonction localtime() en fonction de la zone et des changements d'heure été / hiver.

    1.2. Librairie standard C

    On peut récupérer l'heure UTC ou locale à l'aide des fonctions de la librairie standard C :

        time_t t;
        // heure UTC en nombre de secondes depuis le 01/01/1970
        time(&t);
     
        // heure UTC (année, mois, jour, heure, minutes, secondes, etc.)
        struct tm *current = gmtime(&t);
        const char *format = "%A %d/%m/%Y, %H:%M:%S";

        // formatage : jour jj/mm/aaaa, hh:mm:ss
        strftime(buf, sizeof(buf), format, current);

         // heure locale (année, mois, jour, heure, minutes, secondes, etc.) 
        current = localtime(&t); 
        // formatage : jour jj/mm/aaaa, hh:mm:ss 
        strftime(buf, sizeof(buf), format, current);

    La librairie standard C possède bien d'autres fonctions.

    1.3. Librairie optionnelles

    Les librairies TimeLib et RtcLib sont optionnelles. Il suffit de les activer par une option de compilation.
    Si ces librairies ne sont activée elles n'auront aucun besoin d'être installées.

    Il est possible d'ajouter une horloge RTC DS3231 comme source de temps supplémentaire. A chaque fois que le serveur NTP pourra être joint, la RTC sera mise à jour. Ensuite si le serveur NTP ne peut plus être joint, l'heure sera récupérée à partir de la RTC.

    Attention : la RTC est mise à jour à l'aide de l'heure UTC. Si l'on cherche à récupérer l'heure et la date à partir des méthodes de la RtcLib dans l'application, elle ne sera pas corrigée en fonction de l'heure été / hiver.

    Une méthode rtcNow() permet de récupérer l'heure de la RTC corrigée. Voir l'exemple plus bas.

    1.1. Stockage de l'heure

    Dans la librairie C une simple variable est utilisée pour le stockage de l'heure :

    volatile time_t __system_time;

    Sa valeur est égale au nombre de secondes écoulées depuis le 1er janvier 1970. C'est la norme en vigueur en langage C.

    La fonction time() retourne simplement la valeur de cette variable.

    Contrairement à une librairie C classique, dans la librairie C AVR la variable __system_time n'est pas incrémentée automatiquement, il convient de le faire toutes les secondes dans la fonction loop(). Classiquement on le fait comme ceci :

      static unsigned long lastTick;
      unsigned long tick = millis();
      if (tick - lastTick > 1000) {
        system_tick();
        lastTick = tick;
      }


    La librairie proposée ici met à disposition une méthode tick() permettant également la mise à jour de l'horloge RTC, si elle est utilisée. L'horloge RTC sera mise à jour avec une période exprimée en secondes, passée en paramètre à la méthode begin() :

    void EthernetNtp::begin(IPAddress &addr, time_t syncInterval);

    Nous appellerons donc cette méthode tick() en lieu et place de system_tick() :

      static unsigned long lastTick;
      unsigned long tick = millis();
      if (tick - lastTick > 1000) {
        EthernetNtp::getInstance()->tick();
        lastTick = tick;
      }


    Il est déconseillé d'utiliser la fonction delay() dans le sketch. Lorsque l'on développe un serveur WEB, c'est d'ailleurs peu recommandé.

    Si l'on est obligé d'utiliser delay() ou des fonctions de traitement longues il faudra plutôt faire ceci :

      static unsigned long lastTick;
      unsigned long tick = millis();
      while (
    tick - lastTick >= 1000) {
         EthernetNtp::getInstance()->tick();
         lastTick += 1000;
      }


    Le fonctionnement de la librairie TimeLib est différent. La variable interne sysTime est mise à jour à chaque appel de la méthode now() :
    La méthode now() est appelée par toutes les autres méthodes year(), month(), day(), hour(), minute(), second() de la librairie TimeLib.

    2. Mise en œuvre

    Dans la fonction setup() d'un sketch ARDUINO il suffit de peu de choses pour utiliser cette librairie. Voici un exemple typique pour une gestion de l'heure en TimeZone Europe/Paris. La majeure partie du code est associée au démarrage du serveur Ethernet et à la recherche du serveur NTP par DNS :

    void setup()
    {
      EthernetNtp *ntp = EthernetNtp::getInstance();
      Serial.begin(115200);
      Serial.println(F("Network Time Protocol Example"));
      Ethernet.begin(mac, ip, dnsIp);
      if (Ethernet.hardwareStatus() == EthernetNoHardware) {
        Serial.println(F("Ethernet hardware was not found.  Sorry :("));
        while (true) {
          delay(1);
        }
      }
      if (Ethernet.linkStatus() == LinkOFF) {
        Serial.println(F("Ethernet cable is not connected."));
      }
      // Europe/Paris : last sunday of october at 3:00 : 1H offset
      ntp->std("CET", Last, Sun, Oct, 3, 60);
      // Europe/Paris : last sunday of march at 2:00 : 2H offset
      ntp->dst("CEST", Last, Sun, Mar, 2, 120);
      dns.begin(dnsIp);
      // get NTP server using DNS
      Serial.print(F("DNS (")); Serial.print(ntpUrl); Serial.print(F("): "));
      Serial.println(dns.getHostByName(ntpUrl, ntpIp) == true ? "SUCCESS" : "FAILED");
      // update time every 60 seconds
      ntp->begin(ntpIp, 60);
    #ifdef USE_RTCLIB
      ntp->addDS3231();

    #endif
      Serial.print(F("server is at "));
      Serial.println(Ethernet.localIP());
    }


    Ensuite il suffira de récupérer l'heure et la date à l'aide des fonction habituelles de la librairie standard C ou TimeLib. Voici un bout de code affichant l'heure et la date UTC ou locale sur une page WEB :

        char buf[30];
        client.println(F("HTTP/1.1 200 OK"));
        client.println(F("Content-Type: text/html"));
        client.println(F("Connection: close"));
        client.println();
        client.println(F("<!DOCTYPE HTML>"));
        client.println(F("<html>"));
        client.print("Using standard C library:");
        client.println(F("<br />"));
        client.print("UTC time & date: ");
        time_t t;
        time(&t);
        struct tm *current = gmtime(&t);
        const char *format = "%A %d/%m/%Y, %H:%M:%S";
        strftime(buf, sizeof(buf), format, current);
     

        client.print(buf);
        client.println(F("<br />"));
        client.print("Local time & date: ");
        current = localtime(&t);
        strftime(buf, sizeof(buf), format, current);
     

        client.print(buf);
        client.println(F("<br /><br />"));
        client.print("Using formattedTime:");
        client.println(F("<br />"));
        EthernetNtp *ntp = EthernetNtp::getInstance();
        ntp->formattedTime(buf, sizeof(buf), format);
     

        client.print("Local time & date: ");
        client.print(buf);
    #ifdef USE_RTCLIB
        client.println(F("<br /><br />"));
        client.print("Using RTC:");
        client.println(F("<br />"));
        DateTime now(ntp->rtcNow());
        strcpy(buf, "DDD DD/MM/YYYY hh:mm:ss");
        now.toString(buf);
        client.print("Local time & date: ");
        client.print(buf);
    #endif
        client.println(F("<br />"));
        client.println(F("</html>"));


    Le fait de récupérer l'heure de la RTC avec la méthode rtcNow() offre peu d'intérêt, étant donné que l'heure système est forcément à jour par rapport au serveur NTP.
    Le seul intérêt réside dans l'utilisation de la classe DateTime qui offre certaines facilités.
    Personnellement je trouve que les fonctions standards de la librairie C sont plus nombreuses et bien plus puissantes.

    2.1. Tick

    Si l'on utilise les fonctions standards de la librairie C, ou la TimeLib, il ne faudra pas oublier de mettre à jour l'heure interne toutes les secondes dans la fonction loop() :

    void loop()
    {
      static char request[REQUEST_MAX + 1];
      static unsigned long lastTick;
      int reqIndex = 0;
      char c;
      EthernetClient client = server.available();

      unsigned long tick = millis();
      if (tick - lastTick > 1000) {
        EthernetNtp::getInstance()->tick();
        lastTick = tick;
      }


    2.2. Exemple

    L'exemple de la librairie utilise 3 méthodes de lecture et de formatage de l'heure :
    • librairie standard C : time(), gmtime(), localtime(), strftime()
    • méthode formattedTime() de la classe EthernetNtp
    • méthode rtcNow() de la classe EthernetNtp et méthode toString() de la classe DateTime (librairie RtcLib).
    L'URL suivante est utilisée : http://xxxx.xxx.xxx.xxx/time
    L'adresse IP est affichée sur la console au démarrage.

    Le résultat :

    Using standard C library:
    UTC time & date: Saturday 04/04/2020, 08:39:35
    Local time & date: Saturday 04/04/2020, 10:39:35

    Using formattedTime:
    Local time & date: Saturday 04/04/2020, 10:39:35

    Using RTC:
    Local time & date: Sat 04/04/2020 10:39:35


    Dans l'exemple l'heure NTP est demandée toutes les 60 secondes :

      ntp->begin(ntpIp, 60);

    On pourra augmenter cette période en fonction de la précision de l'oscillateur de la carte.

    2. La librairie

    La librairie est disponible ici :
    https://bitbucket.org/henri_bachetti/w5100-ntp.git

    On pourra activer les options TimeLib et RtcLib dans w5100-ntp.h comme ceci :

    // uncomment to use TimeLib
    #define USE_TIMELIB

    // uncomment to use RtcLib
    //#define USE_RTCLIB


    La librairie Ethernet :
    https://github.com/arduino-libraries/Ethernet

    On peut également installer les librairies TimeLib et RtcLib si l'on a choisi l'une ou l'autre de ces options, ou les deux :
    https://github.com/PaulStoffregen/Time
    https://github.com/adafruit/RTClib

    l'exemple complet :
    https://bitbucket.org/henri_bachetti/w5100-ntp/src/master/examples/ntp/ntp.ino

    3. ESP32 & ESP8266

    Avec un ESP32 ou un ESP8266 récupérer l'heure à partir d'un serveur NTP et configurer la TimeZone sont beaucoup plus simples :

    #include <Arduino.h>
    #ifdef ESP32
    #include <WiFi.h>
    #else
    #include <ESP8266WiFi.h>
    #endif

    #define MAX_SIZE 80

    const char *ssid = "sssssssssssss";
    const char *password = "pppppppppppppp";

    const char* ntpServer = "pool.ntp.org";

    void setup() {
      Serial.begin(115200);
      WiFi.begin(ssid, password);

      while (WiFi.status() != WL_CONNECTED) {
        Serial.print('.');
        delay (500);
      }
      configTzTime("CET-1CEST-2,M3.5.0/02:00:00,M10.5.0/03:00:00", ntpServer);
    }

    void loop() {
      time_t timestamp = time( NULL );
      char buffer[MAX_SIZE];
      struct tm *pTime = localtime(&timestamp );
      strftime(buffer, MAX_SIZE, "%d/%m/%Y %H:%M:%S", pTime);
      Serial.println(buffer);
      delay(1000);
    }

    Avec ce code l'heure est demandée au serveur toutes les heures par défaut :
    #define SNTP_UPDATE_DELAY 3600000
    Cette constante est dans les options (hardware/esp8266/3.0.0/tools/sdk/lwip2/include/lwip/apps/sntp_opts.h)

    La chaîne de caractères "CET-1CEST-2,M3.5.0/02:00:00,M10.5.0/03:00:00" est une "timezone string".

    Avec cette configuration le changement d'heure sera donc automatique le dernier dimanche de mars à 2:00 et octobre à 3:00.

    Les explications ici : https://www.di-mgt.com.au/wclock/tz.html

    Une documentation à lire : https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/system/system_time.html



    Cordialement
    Henri BACHETTI

    4. Mises à jour

    05/04/2020 : correction d'un bug de la mise à l'heure système.

    3 commentaires:

    1. Merci beaucoup. Par contre je n'ai pas été fichu de retrouver la librairie pour changer la valeur par défaut entre deux updates
      SNTP_UPDATE_DELAY 3600000. J'ai pris le sketch spécial ESP32 qui fonctionne très bien. Pour les noobs comme moi je pense qu'il serait bon de commenter un peu plus le code et aussi montrer comment on crée une variable globale alias de *buffer qui permet une utilisation réelle (par ex. affichage sur une page web). surtout que c'est un pointeur. Cordialement.

      RépondreSupprimer
      Réponses
      1. L'affichage sur une page WEB ne nécessite aucune variable globale. Simplement envoyer la chaîne buffer au client.
        Accessoirement, buffer n'est pas un pointeur, c'est une chaîne de caractères (c-string).

        Supprimer
      2. Si vous ne trouvez pas le fichier contenant l'option SNTP_UPDATE_DELAY, cherchez-le dans votre répertoire arduino : sntp_opts.h

        Supprimer