enezNTP

Fournir l'heure au système enez.


Problématique

Pour un système qui enregistre des données horodatées, ce pose alors le problème de la précision de l'horodatage. Quand le système est relié à l'Internet, les serveurs se mettent à l'heure automatiquement grâce aux différents services NTP accessibles sur Internet. Dans le cas d'une installation isolée, cela n'est pas possible. La mise à l'heure manuelle est bien entendu possible, mais les petites machines n'ont pas d'horloge assez précise pour conserver l'heure correctement. Celle-ci dérive petit à petit et un décalage d'une heure peut arriver au bout de quelques jours. Pour remèdier à cela, on va utiliser un module ESP8266 avec un récepteur GPS comme serveur NTP.


Fonctionnement

Le module est autonome. ce sont les autres appareils qui l'interrogent. Le module publie sur le service MQTT certains éléments de fonctionnement. Ces éléments sont publiés avec un format json de type {"info": "xxx", "UTC": "yyy", "IP": "zzz", "port": nnn}, avec :

  • info : nature du message
  • UTC : heure TU du module, fournie par le GPS
  • IP : Adresse IP de l'appareil demandant l'heure, si cela s'applique
  • port : port utilisé par le demandeur, si cela s'applique

Les différents info utilisées sont :

  • WiFi & MQTT setup => Lors du démarrage ou de la sortie de veille
  • Init done => L'initialisation est terminée
  • GPS fixed => Le GPS fournie une heure correcte
  • Process NTP => Une demande de synchronisation de l'heure a été faite
  • First minute => Pour vérifier le bon fonctionnement, message envoyé toutes les secondes minutes d'une heure

Configuration des serveurs enez


Cablage du module


Installation

Le code est disponible ci-dessous. C'est un code Arduino. Il est nécessaire de le personnaliser en fonction du réseau et de l'environnement informatique de l'appareil. Il se télécharge sur le NodeMCU avec l'IDE Arduino. Il n'a pas été testé avec l'Arduino Web Editor.


Code

Il est nécessaire de personnaliser certaines informations selon l'environnement de l'installation :

  • Réseau WiFi utilisé => "wifiSSID" & "wifiPassword"
  • Adresses réseau => "ip", "gateway" & "subnet"
  • Serveur MQTT => "mqttServer", "mqttPort", "deviceID" & "payloadTopic
/*
|   --- Arduino Software ---
|   
|   ESP8266 - NTP Server
|    
|   Copyright finizi 2019 - Created on 2019-05-01 with Arduino software 1.8.21
|   my.inizisoft.net/grav/enez
|
|     This program is free software: you can redistribute it and/or modify
|      it under the terms of the Creative Commons Attribution-ShareAlike 4.0 International 
|      (CC BY-SA 4.0) https://creativecommons.org/licenses/by-sa/4.0/.
|     You are free to:
|        Share : copy and redistribute the material in any medium or format
|        Adapt : remix, transform, and build upon the material
|      for any purpose, even commercially.
|     This license is acceptable for Free Cultural Works.
|     The licensor cannot revoke these freedoms as long as you follow the license terms.
|
|     Code sous licence (CC BY-SA 4.0) :
|      Creative Commons Attribution - Partage dans les Mêmes Conditions 4.0 International
|
|  
|     Inspired from various examples and sources.
|     => https://www.maeder.xyz/wp/building-a-cheap-stratum-1-gps-ntp-timeserver/
|     -> http://git.cleverdomain.org/arduino/
|     -> http://www.osengr.org/Projects/NTP-Server/NTP-Server.shtml
|     -> https://forum.arduino.cc/index.php?topic=197870.0
|     -> http://www.dre.vanderbilt.edu/~schmidt/DOC_ROOT/TAO/orbsvcs/orbsvcs/AV/ntp-time.h
|     -> https://github.com/arodland/Arduino-GPS-NTP-Server
|  
*/

/***************************************************************************************************

Usage :
  + Reading the NMEA sentences from a GPS
  + Responding to UDP NTP queries
  + Sending metrics to an MQTT server
  + For power saving reasons, NTP server is up for 30 minutes and down for 70 minutes

  + Following informations must be set (see parts "Network relatives" & "MQTT relatives") : 
    * Printing debug & rrunning informations => "DEBUG_PRINT"
    * WiFi network used => "wifiSSID" & "wifiPassword"
    * IP adresses => "ip", "gateway" & "subnet"
    * MQTT server => "mqttServer", "mqttPort", "deviceID" & "payloadTopic"

Equipment required :
  + 1 x ESP8266 nodeMCU
  + 1 x GPS shield

Pins used :
  + D5 : TX 
  + D6 : RX 
  + D7 : PPS 

***************************************************************************************************/

/*  
******************************************************************************************************************************************************
**
**  Structure du code
**   . Defines
**   . Includes
**   . Variables globales
**   . Objets
**   . setup()
**   . loop()
**   . fonctions diverses
**
******************************************************************************************************************************************************
*/

/***************************************************************************************************
** Some macros for debug
*/
//#define DEBUG_PRINT   //If you comment this line, the DPRINT & DPRINTLN lines are defined as blank.
#ifdef DEBUG_PRINT    //Macros are usually in all capital letters.
  #define DPRINT(...)    Serial.print(__VA_ARGS__)     //DPRINT is a macro, debug print
  #define DPRINTLN(...)  Serial.println(__VA_ARGS__)   //DPRINTLN is a macro, debug print with new line
#else
  #define DPRINT(...)     //now defines a blank line
  #define DPRINTLN(...)   //now defines a blank line
#endif

/***************************************************************************************************
** The libraries used 
*/
#include <TimeLib.h>
#include <sys/time.h>
#include <ESP8266WiFi.h>
#include <WiFiUdp.h>
#include <SoftwareSerial.h>
#include <TinyGPS++.h>
#include <Adafruit_MQTT.h>
#include <Adafruit_MQTT_Client.h>

/***************************************************************************************************
** Declaration of constants & variables
*/

// Used pins ESP8266
//const int pinVcc =   D0;
const int pinGpsRx = D5;
const int pinGpsTx = D6;
const int pinPps =   D7;

// Network relatives
const char* wifiSSID = "##-wifiSSID-##";
const char* wifiPassword = "##-password-##";
const IPAddress ip(192,168,xx,xx);
const IPAddress gateway(192,168,xx,xx);
const IPAddress subnet(255,255,255,0);
const IPAddress noip(0,0,0,0);

// MQTT relatives
const char* mqttServer = "172.24.1.1";
const int   mqttPort = 8324;
const char* deviceID = "##-deviceID-##";
const char* payloadTopic = "my/topic";
char msg[200];                       // Message payload
long lastM1;                         // Last time MQTT message minute Nb 1

// debug relatives
const int debugSerialSpeed = 115200;

// GPS relatives
const int gpsSerialSpeed = 9600L;
boolean bfixed = false;
boolean firstFix = false;
volatile uint64_t pps = 0;
uint64_t lastpps = 0;
time_t lasttime = 0;               // time last pps
uint32_t timestamp, tempval;
struct timeval tvalPaket, tvalNow;

// NTP relatives
static const int ntpPacketSize = 48;
const int ntpPort = 123;           // Time Server Port
byte packetBuffer[ntpPacketSize];  // buffers for receiving and sending data
const unsigned long seventyYears = 2208988800UL;
static const char ntpPacketTemplate[48] = {
                4 /* Mode: Server reply */ | 3 << 3 /* Version: NTPv3 */,
                1 /* Stratum */, 9 /* Poll: 512sec */, -21 /* Precision: 0.5 usec */,
                0, 0, 0, 0 /* Root Delay */,
                0, 0, 0, 10 /* Root Dispersion */,
                'G', 'P', 'S', 0 /* Reference ID */,
                0, 0, 0, 0, 0, 0, 0, 0 /* Reference Timestamp */,
                0, 0, 0, 0, 0, 0, 0, 0 /* Origin Timestamp */,
                0, 0, 0, 0, 0, 0, 0, 0 /* Receive Timestamp */,
                0, 0, 0, 0, 0, 0, 0, 0 /* Transmit Timestamp */
};

// Energy saving
const long workingMinutes = 30;            // Working 30 minutes
const long sleepingMinutes = 70;           // Go sleeping for 1 hour & 10 minutes (near the maximum possible)

/***************************************************************************************************
** Objects
*/
// Create software serial port to communicate with the GPS device 
SoftwareSerial gpsSerial(pinGpsTx, pinGpsRx);
// Create the gps class to handle infos from GPS device
TinyGPSPlus gps;
// Create udp class
WiFiUDP udp;
// Create an ESP8266 WiFiClient class to connect to the MQTT server.
WiFiClient espClient;
// Setup the MQTT client class by passing in the WiFi client and MQTT server and login details.
Adafruit_MQTT_Client mqtt(&espClient, mqttServer, mqttPort); 
// Setup the feed for publishing.
Adafruit_MQTT_Publish payload = Adafruit_MQTT_Publish(&mqtt, payloadTopic);

/***************************************************************************************************
** Interrupt Service Routine
*/
void ICACHE_RAM_ATTR isr() { 
  pps = micros64(); // Flag the 1pps input signal
}

/***************************************************************************************************
** Setup
*/
void setup(void) {
  #ifdef DEBUG_PRINT
    // Initialise serial communication
    Serial.begin(debugSerialSpeed);
    while (!Serial) { }
    delay(10);
    Serial.println("");
  #endif

  // Say who am I
  DPRINTLN("");
  DPRINTLN(F("NTP server from GPS time."));
  DPRINTLN(F("Parameters are :"));
  DPRINT(F(" - wifiSSID : "));
  DPRINTLN(wifiSSID);
  DPRINT(F(" - MQTT host : "));
  DPRINT(mqttServer);
  DPRINT(F(":"));
  DPRINTLN(mqttPort);
  DPRINT(F(" - Topic : "));
  DPRINTLN(payloadTopic);
  DPRINT(F(" - DeviceID : "));
  DPRINTLN(deviceID);

  DPRINTLN("Startup GPS");
  //pinMode(pinVcc, OUTPUT);        // Initialize the pin to powering GPS
  //digitalWrite(pinVcc, HIGH);     // Turn the GPS on (Note that LOW is the voltage level
  delay(50);
  gpsSerial.begin(gpsSerialSpeed);
  processGPS();

  wifiSetup();

  // MQTT setup
  mqttPublishMessage("WiFi & MQTT setup", WiFi.localIP(), 0);
  lastM1 = millis();
  DPRINTLN(F("MQTT initialized !"));

  DPRINTLN("Setting up PPS pin");
  pinMode(pinPps, INPUT);
  attachInterrupt(pinPps, isr, RISING);

  DPRINTLN("Starting udp");
  udp.begin(ntpPort);
  DPRINTLN("Waiting for fix ...");
  mqttPublishMessage("Init done", noip, 0);
}

/***************************************************************************************************
** Loop
*/
void loop(void) {
  //DPRINT("~");
  //DPRINT(gps.time.second());
  //DPRINT("~");
  //DPRINTLN(gps.location.lat(), 6);  // Latitude in degrees (double)

  unsigned long mMax = (workingMinutes + 1L) * 60L * 1000L;
  if (millis() > mMax) {
    // Not possible, so basically die and restart
    DPRINTLN("Up for too long time");
    mqttPublishMessage("Out Dated", noip, 0);
    //ESP.restart();
    // wait for WDT to reset me
    while (1);
  }

  processGPS();
  // not fixed if not synchronized for more than a second
  if ((micros64() - lastpps) > 1010000) {
    bfixed = false;
  }

  if (bfixed) {
    if (!firstFix) {
      DPRINTLN("GPS fixed !"); //@@@
      mqttPublishMessage("GPS fixed", noip, 0);
      firstFix = true;
    }
    processNTP();
  }

  if ( (gps.time.minute() == 1) && (millis() - lastM1 > 60L * 1000L) ) {
    DPRINTLN("First minute");
    mqttPublishMessage("First minute", noip, 0);
    lastM1 = millis();
  }

/* */ 
  if (millis() > 1000L * 60L * workingMinutes) {
    unsigned long tSleep = 1000L * 60L * sleepingMinutes * 1000L;
    DPRINT("At ");
    DPRINT(millis());
    DPRINT(F(" ms, Go to sleep for "));
    DPRINT(tSleep);
    DPRINT(" microseconds");
    mqttPublishMessage("Go to sleep...", noip, 0);
    //digitalWrite(pinVcc, LOW);      // Turn the GPS off
    delay(500);
    ESP.deepSleep(tSleep);   // in microseconds
  }
/* */  
}

/***************************************************************************************************
** WiFi functions
*/
void wifiSetup(void) {
  delay(10);
  DPRINTLN();
  DPRINT("Connecting to ");
  DPRINTLN(wifiSSID);
  WiFi.mode(WIFI_STA);
  if (WiFi.status() != WL_CONNECTED) { // FIX FOR USING 2.3.0 CORE (only .begin if not connected)
    WiFi.begin(wifiSSID, wifiPassword); // connect to the network
  }
  WiFi.config(ip, gateway, subnet);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    DPRINT(".");
    processGPS();
  }
  DPRINTLN("");
  DPRINTLN("WiFi connected !");
  DPRINT("IP address : ");
  DPRINTLN(WiFi.localIP());
  DPRINT("Mac address : ");
  DPRINTLN(WiFi.macAddress());
}

/***************************************************************************************************
** MQTT functions
*/
// Function to connect and reconnect as necessary to the MQTT server.
// Should be called in the loop function and it will take care if connecting.
void mqttConnect(void) {
  int8_t ret;

  // Stop if already connected.
  if (mqtt.connected()) {
    return;
  }

  DPRINT("Connecting to MQTT... ");
  mqtt.disconnect();
  delay(100);
  uint8_t retries = 3;
  while ((ret = mqtt.connect()) != 0) {    // connect will return 0 for connected
    String errStr = mqtt.connectErrorString(ret);
    DPRINTLN(errStr);
    DPRINTLN("Retrying MQTT connection in 5 seconds...");
    mqtt.disconnect();
    delay(5000);  // wait 5 seconds
    retries--;
    if (retries == 0) {
      // basically die and restart
      DPRINTLN("basically die and restart");
      mqttPublishMessage("Die", noip, 0);
      delay(500);
      //ESP.restart();
      // wait for WDT to reset me
      while (1);
    }
  }
  DPRINTLN("MQTT Connected!");
}

// Function to publish a message
void mqttPublishMessage(String info, IPAddress ip, int port) {
  String lts = tsFromGps();
  DPRINTLN(lts);

  mqttConnect();           // make the connection

  // Building the message payload
  if ( (ip != noip) && (port != 0) ) {
    snprintf(msg, 200, "{\"info\": \"%s\", \"UTC\": \"%s\", \"IP\": \"%s\", \"port\": %.d }", info.c_str(), lts.c_str(), ip.toString().c_str(), port);
  }
  else {
    snprintf(msg, 200, "{\"info\": \"%s\", \"UTC\": \"%s\" }", info.c_str(), lts.c_str());
  }  
  DPRINT(msg);

  // Publish the message
  if (! payload.publish(msg)) {
    DPRINTLN(F(" : Failed"));
  } else {
    DPRINTLN(F(" : OK!"));
  }
  // Disconnect immediately
  delay(50);
  mqtt.disconnect();

}

/***************************************************************************************************
** GPS functions
*/
void processPPS(void) {
  if (pps > 0) {
    lastpps = pps;
    pps = 0;
    if (gps.date.isValid() && gps.time.isValid()) {
      lasttime = tmConvert(gps.date.year(), gps.date.month(), gps.date.day(), gps.time.hour(), gps.time.minute(), gps.time.second());
      // PPS is after the timestamp -> add one second
      lasttime++;
      timeval valnow;
      bfixed = true;
    }
    else {
      lastpps = 0;
      lasttime = 0;
    }
  }
}

void processGPS(void) {
  while (gpsSerial.available()) {
    processPPS();
    gps.encode(gpsSerial.read());
  }
}

time_t tmConvert(int YYYY, byte MM, byte DD, byte hh, byte mm, byte ss) {
  tmElements_t tmSet;
  tmSet.Year = YYYY - 1970;
  tmSet.Month = MM;
  tmSet.Day = DD;
  tmSet.Hour = hh;
  tmSet.Minute = mm;
  tmSet.Second = ss;
  return makeTime(tmSet);   //convert to time_t
}

String tsFromGps(void) {
  char tts[30];                       // time string
  snprintf(tts, 30, "%04d-%02d-%02dT%02d:%02d:%02d.0", gps.date.year(), gps.date.month(), gps.date.day(), gps.time.hour(), gps.time.minute(), gps.time.second());
  return(String(tts));
}

/***************************************************************************************************
** NTP functions
*/
int NTPgettimeofday(struct timeval *tv, struct timezone *tz) {
  if (bfixed == false) {
    return -1;
  }
  uint64_t delta = micros64() - lastpps;

  tv->tv_sec = lasttime;
  while (delta>=1000000) {
    delta = delta - 1000000;
    tv->tv_sec++;
  }
  tv->tv_usec = delta;
  return 1;
}

void processNTP() {
  // if there's data available, read a packet
  int packetSize = udp.parsePacket();

  if (packetSize) {
    NTPgettimeofday (&tvalPaket, NULL);
    udp.read(packetBuffer, ntpPacketSize);
    IPAddress Remote = udp.remoteIP();
    int PortNum = udp.remotePort();

    unsigned char reply[48];
    uint32 tx_ts_upper, tx_ts_lower;
    memcpy(reply, ntpPacketTemplate, 48);
    // XXX set Leap Indicator 
    // Copy client transmit timestamp into origin timestamp 
    memcpy(reply + 24, packetBuffer + 40, 8);

    // reference timestamp
    // fixme: remember cooect last recieve
    NTPgettimeofday (&tvalNow, NULL);

    tempval = seventyYears + tvalNow.tv_sec - 1; // one second before, usually get gps time once a second -> FIXME

    reply[16] = (tempval >> 24) & 0XFF;
    reply[17] = (tempval >> 16) & 0xFF;
    reply[18] = (tempval >> 8) & 0xFF;
    reply[19] = (tempval) & 0xFF;

    // synced at full seconds with pps signal
    reply[20] = 0;
    reply[21] = 0;
    reply[22] = 0;
    reply[23] = 0;

    //receive timestamp
    tempval = seventyYears + tvalPaket.tv_sec;
    reply[32] = (tempval >> 24) & 0XFF;
    reply[33] = (tempval >> 16) & 0xFF;
    reply[34] = (tempval >> 8) & 0xFF;
    reply[35] = (tempval) & 0xFF;

    tempval = usec2ntp(tvalPaket.tv_usec);

    reply[36] = (tempval >> 24) & 0XFF;
    reply[37] = (tempval >> 16) & 0XFF;
    reply[38] = (tempval >> 8) & 0XFF;
    reply[39] = (tempval) & 0XFF;

    //transmitt timestamp
    NTPgettimeofday (&tvalNow, NULL);

    tempval = seventyYears + tvalNow.tv_sec;
    reply[40] = (tempval >> 24) & 0XFF;
    reply[41] = (tempval >> 16) & 0xFF;
    reply[42] = (tempval >> 8) & 0xFF;
    reply[43] = (tempval) & 0xFF;

    tempval = usec2ntp(tvalNow.tv_usec);
    reply[44] = (tempval >> 24) & 0XFF;
    reply[45] = (tempval >> 16) & 0XFF;
    reply[46] = (tempval >> 8) & 0XFF;
    reply[47] = (tempval) & 0XFF;

    // Reply to the IP address and port that sent the NTP request
    udp.beginPacket(Remote, PortNum);
    udp.write(reply, ntpPacketSize);
    udp.endPacket();

    // inform the world
    delay(50);
    mqttPublishMessage("Process NTP", Remote, PortNum);

  }
}

/*
** convert microseconds to fraction of second * 2^32 (i.e., the lsw of
** a 64-bit ntp timestamp).  This routine uses the factorization
** 2^32/10^6 = 4096 + 256 - 1825/32 which results in a max conversion
** error of 3 * 10^-7 and an average error of half that.
*/
unsigned int usec2ntp(unsigned int usec) {
  unsigned int t = (usec * 1825) >> 5;
  return ((usec << 12) + (usec << 8) - t);
}

/* END !
*/