[TUTORIEL] Récupération de l'heure sur internet

Un shield Ethernet à souder soit-même - A DIY Ethernet Shield

[TUTORIEL] Récupération de l'heure sur internet

Message non lude Laetitia » Lun 22 Fév 2016 18:53

Bonjour,

Aujourd'hui nous allons apprendre à synchroniser notre Arduino avec les serveurs NTP !

- MATÉRIEL -

- Arduino/Genuino Uno
- Shield Gate

Librairie EtherCard

Pas de schéma, il suffit d'empiler le shield Gate sur le Genuino et d'y brancher un câble Ethernet !

- EXPLICATIONS -

Le protocole NTP, qu'est-ce que c'est ?

Wikipedia a écrit:Le Protocole d'Heure Réseau (Network Time Protocol ou NTP) est un protocole qui permet de synchroniser, via un réseau informatique, l'horloge locale d'ordinateurs sur une référence d'heure.

En d'autres termes, il s'agit d'un outil pratique lorsque vous voulez horodater des données ou déclencher des événements à des horaires précis. Il est en outre bien plus fiable qu'une RTC, qui dérive plus ou moins au cours du temps : vous pouvez en revanche vous servir des serveurs NTP pour remettre à l'heure votre RTC. Vous pourrez ainsi synchroniser vos projets avec une horloge universelle !

    Fonctionnement des serveurs NTP
Les serveurs sont organisés selon un système de strates, numérotées à partir de 0 pour la couche la plus haute, jusqu'à 15 :

NTP Architecture.png
NTP Architecture.png (24.06 Kio) Vu 1452 fois

  • Strate 0 : Elle regroupe les horloges de haute précision, comme les horloges atomiques (au Césium, au Rubidium), les horloges GPS et autres horloges radio. Celles-ci génèrent une pulsation par seconde extrêmement précise qui déclenche une interruption et met à jour la date sur l'ordinateur qui leur est relié. On appelle les horloges de la strate 0 des horloges de référence.
  • Strate 1 : Synchronisée directement avec la strate 0, avec un délai de quelques microsecondes uniquement. Les serveurs de la strate 1 peuvent communiquer entre eux pour vérifier qu'ils sont bien en phase, ou pour faire une sauvegarde. On les appelle également les serveurs de temps primaires.
  • Strate 2 : Elle contient les ordinateurs synchronisés avec les serveurs de la strate 1. Souvent les ordinateurs de la strate 2 communiquent avec plusieurs serveurs de la strate 1. Ils peuvent également communiquer entre eux, pour fournir une horloge plus stable et robuste pour tous les appareils du groupe.
  • Strate 3 : Elle regroupe les ordinateurs synchronisés aux serveurs de la strate 2. Elle utilise exactement les même algorithmes pour récupérer les données que la strate 2, et peut elle-même faire office de serveur pour les ordinateurs de la strate 4, et ainsi de suite.

    Récupération des données
Pour récupérer les données sur le serveur, il faut aller l'interroger ! Plus facile à dire qu'à faire : à qui est-ce qu'on s'adresse et comment ?

Plutôt que d'aller embêter sans cesse le même serveur, nous allons interroger le pool NTP. Celui-ci nous met à disposition 4 adresses web qui pointent vers un ensemble aléatoire de serveurs changeant toutes les heures :

Code: Tout sélectionner
0.pool.ntp.org
1.pool.ntp.org
2.pool.ntp.org
3.pool.ntp.org

Comme les serveurs interrogés sont situés partout dans le monde, on peut perdre en précision (le temps que les données arrivent à destination, elles sont en quelque sorte "périmées"), il est possible d'utiliser une zone continentale :

Code: Tout sélectionner
0.africa.pool.ntp.org // ainsi que 1, 2, 3.africa.pool.ntp.org
0.asia.pool.ntp.org   // etc.
0.europe.pool.ntp.org
0.north-america.pool.ntp.org
0.oceania.pool.ntp.org
0.south-america.pool.ntp.org

Pour encore plus de précision, on peut également interroger la zone du pays où l'on se trouve :

Code: Tout sélectionner
es.pool.ntp.org
fr.pool.ntp.org  // Il s'agit du serveur que nous utiliserons dans ce tutoriel
uk.pool.ntp.org
// ...et ainsi de suite

Maintenant que nous savons auprès de qui faire nos requêtes, il faut encore savoir les formuler !

Bon, soyons honnêtes, le protocole est assez velu (pour les curieux ou les amateurs de migraine, les détails sont ici), mais la librairie EtherCard a tout ce qu'il faut pour s'en sortir !

Nous avons à disposition deux fonctions utiles :

Code: Tout sélectionner
ether.ntpRequest(unsigned int *ntpip, unsigned int srcport)

Le premier argument pris par cette fonction est un pointeur vers l'adresse IP du serveur NTP, que l'on obtiendra grâce à ether.hisip ; le second est le port utilisé par le serveur NTP, en l'occurrence le n°123 (voir liste des ports TCP/UDP).

Cette première étape permet d'envoyer une requête au serveur NTP distant.

Code: Tout sélectionner
ether.ntpProcessAnswer(unsigned long *time, unsigned int dstport_l)

Le premier argument pris par cette fonction est un pointeur vers un entier long non signé qui servira à stocker les informations reçues ; le second est le port utilisé, toujours le même que précédemment.

Cette seconde étape sert à décoder les informations reçues avec les algorithmes appropriés.

    Détermination de la date et de l'heure
Revenons sur le format des données NTP :

NTP Data Format.png
NTP Data Format.png (5.57 Kio) Vu 1450 fois

Le Datestamp correspond à une version très précise et complète de l'horloge, mais qui sert uniquement de référence interne : 127 bits juste pour donner l'heure, ça commence à faire long...

Le Timestamp en revanche est celui qui sera échangé lors des discussions client-serveur. Celui-ci, complet, est stocké sur 64 bits : les 32 premiers comptent le nombre de secondes passées depuis le 1er Janvier 1900 (début de l'horloge NTP), les 32 suivants les fractions de secondes écoulées.

Comme nous n'avons pas besoin d'une précision atomique pour nos projets Arduino, la librairie retourne simplement les secondes écoulées indiquées par le serveur NTP : sur 32 bits non signés on peut compter jusqu'à 4 294 967 295 secondes, soit un peu plus de 136 ans !

Maintenant, il ne manque plus qu'un peu de mathématiques pour déduire le reste :

On sait que l'horloge UNIX commence le 1er janvier 1970 à 00h00.
On compte le nombre de secondes écoulées depuis le lancement de l'horloge.

On sait qu'il y a 60 secondes * 60 minutes * 24 heures = 86400 secondes en une journée.

Si l'on divise le nombre de secondes écoulées par 86400, le quotient représente le nombre de jours écoulés depuis le 1er janvier 1970 (les données de date, séparées du reste), et le reste de la division (modulo) correspond au nombre de secondes écoulées dans la journée en cours (les données d'heure).

Traitement de l'heure :

  • Données d'heure / 3600 = heures hh
  • (Données d'heure % 3600) / 60 = minutes mm
  • Données d'heure % 60 = secondes ss
Traitement de la date :

  • (Données de date + 4) % 7 = jour de la semaine (de dimanche à lundi, on compte les jours de la semaine à partir du jour 0, le 1er Janvier 1970, qui était un jeudi) jr
  • Données de date - nombre de jours dans l'année => une année à rajouter à 1970 AA, à répéter jusqu'à ce qu'il reste moins de 365 ou 366 jours, en tenant compte des années bissextiles (sinon on sera embêtés pour calculer la suite)
  • Dernier résultat obtenu - nombre de jours dans le mois => un mois à compter à partir de Janvier MM, à répéter jusqu'à ce qu'il reste moins de ~30 jours (en fonction des mois, vous me suivez toujours ?)
  • Dernier résultat obtenu = numéro du jour dans le mois JJ
Synthèse : En 7 étapes nous avons récupéré les informations nécessaires à un horodatage complet : jr JJ.MM.AA hh:mm:ss

Note : l'horloge NTP donne l'heure pour le fuseau horaire UTC, sans gestion de l'heure d'été, il faut donc penser à ajuster les résultats obtenus en fonction de là où vous vous trouvez ! Dans le sketch de ce tutoriel j'ai rajouté une heure à celle calculée puisque Toulouse se situe sur le fuseau UTC+1.

Une fois que cette partie-là tient la route, vous pouvez toujours embellir le code avec des tableaux pour stocker les noms des jours de la semaine, ceux des mois, pour faire un affichage un peu plus sympa, mais vous avez déjà l'essentiel.

Ouf, ça commençait à faire long... Passons donc à la pratique !

- CODE -

Code: Tout sélectionner
/*
 * TUTORIEL : Récupération de l'heure sur un serveur NTP
 *
 * Matériel : Arduino/Genuino Uno http://snootlab.com/arduino-genuino-fr/956-genuino-uno-fr.html
 *            Shield Gate http://snootlab.com/shields-snootlab/85-gate-fr.html
 *           
 * Librairie EtherCard https://github.com/Snootlab/EtherCard
 */

#include <avr/pgmspace.h> // cf. https://www.arduino.cc/en/Reference/PROGMEM
#include <EtherCard.h>

// Adresse MAC, doit être unique sur le réseau
static byte mymac[6] = {0x4C,0x61,0x65,0x74,0x75,0x65};

// Liste des serveurs dispo sur http://support.ntp.org/bin/view/Servers/StratumTwoTimeServers
// Attention aux restrictions d'accès aux serveurs
const char ntp[] PROGMEM = "fr.pool.ntp.org"; // Serveur utilisé

// Le buffer doit être assez grand pour contenir l'intégralité du paquet
byte Ethernet::buffer[500];

// Conversion des données NTP en quelque chose de lisible par l'homme
// Adapté de : https://github.com/thiseldo/EtherCardExamples/blob/master/EtherCard_ntp/EtherCard_ntp.ino

// Nombre de secondes entre 1-Jan-1900 et 1-Jan-1970,
// L'horloge NTP commence en 1900 et l'horloge UNIX en 1970
#define OFFSET 2208988800

#define DEBUT_HORLOGE_UNIX  1970
#define SECONDES_PAR_JOUR  86400 // 60s * 60min * 24h
#define BISSEXTILE(annee)  (!((annee) % 4) && (((annee) % 100) || !((annee) % 400))) // retourne 1 si l'année est bissextile,  sinon 0
#define LONGUEUR_ANNEE(annee)  (BISSEXTILE(annee) ? 366 : 365) // retourne 366 si l'année est bissextile, sinon 365

static const char abbrev_jr[] PROGMEM = "DimLunMarMerJeuVenSam";
// bissextile = 0-1
// mois = 0-11
// valeur de retour : combien de jours comporte le mois

byte long_mois(byte bissextile,byte mois)
{
  const byte l_mois[2][12] =
  {
    { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }
    ,
    { 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }
  };
  return(l_mois[bissextile][mois]);
}

// gmtime - Convertit le temps UNIX (secondes écoulées depuis 1970) et
// l'enregistre sous la forme Mar 23.02.2016 dans date et 16:05:55 dans horloge
// La valeur retournée par la fonction correspond aux minutes.

byte gmtime(const unsigned long donnees,char *d, char *h)
{
  char jour[4];
  byte i;
  unsigned long infoheure;
  unsigned int infodate;
  unsigned int tm_annee = DEBUT_HORLOGE_UNIX;
  byte tm_sec, tm_min, tm_heure, tm_jsem, tm_mois;

  infodate = donnees / SECONDES_PAR_JOUR;    // calcul de la date
  infoheure = donnees % SECONDES_PAR_JOUR; // calcul de l'heure

  tm_sec = infoheure % 60;           // calcul des secondes
  tm_min = (infoheure % 3600) / 60;  // calcul des minutes
  tm_heure = infoheure / 3600;       // calcul des heures
  tm_jsem = (infodate + 4) % 7;  // calcul du jour de la semaine, le jour 0 était un jeudi

  // calcul de l'année
  while (infodate >= LONGUEUR_ANNEE(tm_annee))
  {
    infodate -= LONGUEUR_ANNEE(tm_annee);
    tm_annee++;
  }

  // calcul du mois
  tm_mois = 0;
  while (infodate >= long_mois(BISSEXTILE(tm_annee),tm_mois))
  {
    infodate -= long_mois(BISSEXTILE(tm_annee),tm_mois);
    tm_mois++;
  }

  // récupération du nom du jour de la semaine
  i=0;
  while (i<3)
  {
    jour[i]= pgm_read_byte(&(abbrev_jr[tm_jsem*3 + i]));
    i++;
  }
  jour[3] = '\0'; // ajout du caractère de fin de chaîne

  // Concaténation des données
  sprintf_P(d, PSTR("%s %02u.%02u.%u"), jour, infodate+1, tm_mois+1, tm_annee);
  sprintf_P(h, PSTR("%02u:%02u:%02u"), tm_heure+1, tm_min, tm_sec); // tm_heure+1 <=> UTC+1
  return(tm_min);
}

unsigned long derniereMaj = 0; // date de la dernière requête NTP
unsigned long resultat = 0;    // stockage des données NTP

void setup()
{
  Serial.begin(9600);
  Serial.println( F("Demo client NTP") );
 
  if (!ether.begin(sizeof Ethernet::buffer, mymac))
    Serial.println( F("Impossible d'acceder au controleur Ethernet") );
 
  if (!ether.dhcpSetup())
    Serial.println( F("Echec DHCP"));
}

void loop()
{
  unsigned int dat;
  char date[14];
  char horloge[9];
  int len = 0;
 
  // Mise à jour toutes les 10 secondes
  if(derniereMaj + 10000 < millis())
  {
    derniereMaj = millis();
    Serial.print( F("\nServeur NTP : ") );
    char buffer[20]; // tableau pour stocker le nom du serveur
    strcpy_P(buffer, ntp);
    Serial.println(buffer);

    if (!ether.dnsLookup(ntp))
      Serial.println( F("Echec DNS"));

    else
    {
      ether.printIp("IP serveur : ", ether.hisip);
      Serial.println( F("Envoi de la requete NTP..."));
      ether.ntpRequest(ether.hisip, 123);
    }
  }
 
  // Gestion du ping et attente d'un paquet TCP
  len = ether.packetReceive();
  dat = ether.packetLoop(len);
 
  if (len > 0) // Réponse non traitée : c'est parti !
  {   
    if (ether.ntpProcessAnswer(&resultat,123))
    {
      Serial.print( F("Secondes depuis 1900 : "));
      Serial.println(resultat);
     
      if (resultat)
      {
        resultat -= OFFSET; // conversion temps NTP > temps UNIX
        gmtime(resultat, date, horloge); // extraction de la date & de l'heure
       
        Serial.print(date);
        Serial.print(" - ");
        Serial.println(horloge);
      }
    }
  }
}

- COMMENTAIRES -

Pour les plus curieux :
  • La page Wikipedia anglaise est plus complète,
  • Une description détaillée du protocole NTP et de son fonctionnement est disponible ici,
  • Et si vous voulez vraiment explorer les possibilités du NTP, en dehors de l'Arduino, visitez sa page officielle !
C'est tout pour cette fois ! J'espère que ce tutoriel vous aura plu, et bonne bidouille en attendant le prochain !
"If it's itchy, scratch it !" - "DIY or die"

RTFM (À lire avant de poster) - ANDb (Arduino Noob Database)
Avatar de l’utilisateur
Laetitia
 
Messages: 296
Inscription: Mar 7 Aoû 2012 15:07
Localisation: Toulouse

Retourner vers Gate

Qui est en ligne

Utilisateurs parcourant ce forum: Aucun utilisateur enregistré et 1 invité

cron