NTP für den ESP8266 mit Sommerzeit

Anmerkung: diese Art der NTP Abfrage ist per Stand 2019 veraltet und ab ESP Core 2.6.0 nicht mehr notwendig. Der Artikel wurde überarbeitet. Die neue Version findest du hier:

Die NTP Abfrage für den ESP8266 / NodeMCU mit Sommerzeit

Ein paar Worte zum NTP am ESP8266 mit Sommerzeit Umstellung

Ursprünglich war ich im Internet auf auf der Suche nach einem Beispiel für eine NTP Abfrage am ESP8266 ohne große Libraries, delay-freier NTP Abfrage und Sommerzeit Umschaltung. Nach einigen Tagen stellte sich dann heraus, dass die TimeLib wirklich alles mitbring was es für eine Systemuhr ohne RTC braucht und auch nicht wirkich all zu groß ist. Daher habe ich nur mehr das mitgelieferte Beispiel TimeNTP_ESP8266WiFi abgeändert.

Was am Beispiel geändert wurde:

Auffällig am mitgelieferten Beispiel der Library ist die große Wartezeit von 1500ms nach der NTP Abfrage. Ist der NTP Server nicht erreichbar, verweilt der Microprozessor 1,5 Sekunden in einer While-Schleife bis es zum Abbruch kommt. Noch länger dauert es, wenn die Internetverbindung ganz wegfällt. Scheitert bereits die DNS Auflösung, dauerte bei mir über 6 Sekunden. Dann noch die vorhin erwähnten 1,5 Sekunden - und so lange ist der Microprozessor mit "warten" beschäftigt und macht nichts anderes.

Mir war das zuviel und daher gab es im wesentlichen zwei Optimierungen.

  • Der DNS Lookup kann ab ESP Core 2.4.0 mit einem dritten Parameter mit einem Timeout versehen werden. Scheitert der DNS Lookup wird die Funktion komplett verlassen.
  • Eine erfolgreiche NTP Abfrage definiert den Timeout für die nächste Abfrage. Damit "lernt" der ESP wie lange ein NTP Abfrage üblicherweise dauert und gibt einen neuen Grenzwert für die nächste NTP Abfrage vor.

Für die europäische Sommerzeitermitlung gibt es im Internet einige Beispiele, das habe ich weitgehend unkontrolliert übernommen.

Wichtig zum Verständnis ist, dass mein ESP als "Systemzeit" die Zeit gem. UTC führt, und die Anpassung an Zeitzone und Sommerzeit erst bei der Ausgabe geschieht. Hintergrund ist, dass ich ein Datumssortiertes Log in eine SPIFFS Datei schreiben will. Damit habe ich keine Zeitsprünge in der Logdatei.

/*
This sketch is based on the TimeNTP_ESP8266WiFi.ino 
Modified example showing time sync to NTP time source and daylight saving for European Union
Requires ESP Core 2.4.0

Modifications
- Definied timeout for DNS Lookup of NTP timeout (requires ESP Core 2.4.0) 
  gets rid of the 10 seconds blocking of NTP lookup on failure
- Program upodates threashold for NTP timeout dynamically to minimal but optimistic timeouts
- system clock runs in UTC - not in local time zone (!)
- daylight saving model according regulations in Europe.
- Print date time function

Memory Usage:
Original Sketch with ESP Core 2.3.0: Flash 229781 Bytes (21%) - RAM 32260 Bytes (39%) - Just as note 
Original Sketch with ESP Core 2.4.0: Flash 255579 Bytes (24%) - RAM 33540 Bytes (40%) - Reference
This sktech minimized: Flash 255759 Bytes (24%) - RAM 33392 Bytes (40%) - doing the same like the Reference
This sketch with daylight saving: Flash 255979 Bytes (24%) - Ram 33412 Bytes (40%) - final version

*/

#include <TimeLib.h>
#include <ESP8266WiFi.h>
#include <WiFiUdp.h>

const char* ssid = "set your SSID";
const char* password = "set your wifi passord";

// NTP Servers:
static const char ntpServerName[] = "at.pool.ntp.org"; // use the nearest NTP pool to your location
//static const char ntpServerName[] = "time.nist.gov";
//static const char ntpServerName[] = "time-a.timefreq.bldrdoc.gov";
//static const char ntpServerName[] = "time-b.timefreq.bldrdoc.gov";
//static const char ntpServerName[] = "time-c.timefreq.bldrdoc.gov";

const int timeZone = 1; // Central European Time // if you change to any timeZone out of EU, adopt daylightSaving function!
//const int timeZone = -5; // Eastern Standard Time (USA)
//const int timeZone = -4; // Eastern Daylight Time (USA)
//const int timeZone = -8; // Pacific Standard Time (USA)
//const int timeZone = -7; // Pacific Daylight Time (USA)

const uint32_t syncIntervalMax = 300; // could be the same value like the syncInterval in time.cpp | 300
const uint32_t syncIntervalAfterFail = 60; // if sync with NTP fails, retry earlier | 60
const uint32_t ntpWaitMax = 900; // maximum time in ms to wait for an answer of NTP Server,
                                 //most used value 1500 I prefere below one second: 900
uint32_t ntpWaitActual = ntpWaitMax; // optimized/reduced wait time, start with maximum.

WiFiUDP Udp;
unsigned int localPort = 8888; // local port to listen for UDP packets

time_t getNtpTime();
void digitalClockDisplay();
void printDigits(int digits);
void sendNTPpacket(IPAddress &address);

void setup()
{
Serial.begin(115200);
while (!Serial) ; // Needed for Leonardo only
delay(250);
Serial.println(F("TimeNTP Example\n"
"Connecting to "));
Serial.println(ssid);
WiFi.begin(ssid, password);

while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(F("."));
}

Serial.print(F("IP number assigned by DHCP is "));
Serial.println(WiFi.localIP());
Serial.println(F("Starting UDP"));
Udp.begin(localPort);
Serial.print(F("Local port: "));
Serial.println(Udp.localPort());
Serial.println(F("waiting for sync"));
setSyncProvider(getNtpTime);
setSyncInterval(syncIntervalMax);
}

time_t prevDisplay = 0; // when the digital clock was displayed

void loop()
{
if (timeStatus() != timeNotSet) {
if (now() != prevDisplay) { //update the display only if time has changed
prevDisplay = now();
digitalClockDisplay(); // use digitalClockDisplay_minor() if you need the same output like the reference sketch
}
}

// do something different

}


void digitalClockDisplay() // print local date time to serial
{
char readable[20]; // declare a char buffer large enough for your output
uint32_t local_t = nowLocal();
sprintf(readable, "%04d-%02d-%02d %02d:%02d:%02d", year(local_t), month(local_t), day(local_t), hour(local_t), minute(local_t), second(local_t)); // prefered version
Serial.println(readable);
}

//output function printing out the same format like the example sketch
void digitalClockDisplay_minor()
{
char readable[20]; // declare a char buffer large enough for your output
//sprintf(readable, "%04d-%02d-%02d %02d:%02d:%02d", year(), month(), day(), hour(), minute(), second()); // prefered version
sprintf(readable, "%d:%02d:%02d %d.%d.%d", hour(), minute(), second(), day(), month(), year()); // according example
Serial.println(readable);
}


/*-------- NTP code ----------*/

const int NTP_PACKET_SIZE = 48; // NTP time is in the first 48 bytes of message
byte packetBuffer[NTP_PACKET_SIZE]; // buffer to hold incoming & outgoing packets

time_t getNtpTime()
{
IPAddress ntpServerIP; // NTP server's ip address

while (Udp.parsePacket() > 0) ; // discard any previously received packets
Serial.println(F("Transmit NTP Request"));
// get a random server from the pool
// Serial.println(millis()); // Debug only
if (WiFi.hostByName(ntpServerName, ntpServerIP, ntpWaitActual) == 1) // third parameter introduced with ESP Core 2.4.0 
{
// Serial.println(millis()); // Debug only

Serial.print(ntpServerName);
Serial.print(F(": "));
Serial.println(ntpServerIP);
sendNTPpacket(ntpServerIP);

// Serial.print(F("ntpWaitActual=")); Serial.println(ntpWaitActual); // Debug only

uint32_t beginWait = millis();
uint32_t endWait = 0;
while (millis() - beginWait < ntpWaitActual) {
int size = Udp.parsePacket();
if (size >= NTP_PACKET_SIZE) {
endWait = millis();

// Serial.println(beginWait); // Debug only
// Serial.println(endWait); // Debug only

if ((endWait - beginWait) + 200 < ntpWaitMax) ntpWaitActual = (endWait - beginWait) + 200; // reduce wait time if possible.
Serial.println(F("Receive NTP Response"));
Udp.read(packetBuffer, NTP_PACKET_SIZE); // read packet into the buffer
unsigned long secsSince1900;
// convert four bytes starting at location 40 to a long integer
secsSince1900 = (unsigned long)packetBuffer[40] << 24;
secsSince1900 |= (unsigned long)packetBuffer[41] << 16;
secsSince1900 |= (unsigned long)packetBuffer[42] << 8;
secsSince1900 |= (unsigned long)packetBuffer[43];

setSyncInterval(syncIntervalMax);
//return secsSince1900 - 2208988800UL + timeZone * SECS_PER_HOUR; // plus timeZone ist böse
return secsSince1900 - 2208988800UL ; // Important: Systemtime = UTC!!!
}
}
Serial.println(F("No NTP response :-("));
}
else
{
// Serial.println(millis()); // Debug only
// Serial.println(F("No DNS lookup :-("));
}
if (ntpWaitActual + 200 < ntpWaitMax) ntpWaitActual += 200; // expand wait time if necessary - I'm pretty aware of that this value will hardly reach ntpWaitMax with this simple condition.
setSyncInterval(syncIntervalAfterFail); // in both cases: no NTP response or no DNS lookup
return 0; // return 0 if unable to get the time
}

// send an NTP request to the time server at the given address
void sendNTPpacket(IPAddress &address)
{
// set all bytes in the buffer to 0
memset(packetBuffer, 0, NTP_PACKET_SIZE);
// Initialize values needed to form NTP request
// (see URL above for details on the packets)
packetBuffer[0] = 0b11100011; // LI, Version, Mode
packetBuffer[1] = 0; // Stratum, or type of clock
packetBuffer[2] = 6; // Polling Interval
packetBuffer[3] = 0xEC; // Peer Clock Precision
// 8 bytes of zero for Root Delay & Root Dispersion
packetBuffer[12] = 49;
packetBuffer[13] = 0x4E;
packetBuffer[14] = 49;
packetBuffer[15] = 52;
// all NTP fields have been given values, now
// you can send a packet requesting a timestamp:
Udp.beginPacket(address, 123); //NTP requests are to port 123
Udp.write(packetBuffer, NTP_PACKET_SIZE);
Udp.endPacket();
}


//--------------
// returns the current date/time as UNIX timestamp, incl. timezone, including daylightsaving
uint32_t nowLocal()
{
uint32_t local_t = now();
if (isDayLightSaving(local_t))
local_t += 3600 + timeZone * SECS_PER_HOUR;
else
local_t += timeZone * SECS_PER_HOUR;
return local_t;
}

// calculates the daylight saving time for middle Europe. Input: Unixtime in UTC (!)
boolean isDayLightSaving(uint32_t local_t)
{
if (month(local_t) < 3 || month(local_t) > 10) return false; // no DSL in Jan, Feb, Nov, Dez
if (month(local_t) > 3 && month(local_t) < 10) return true; // DSL in Apr, May, Jun, Jul, Aug, Sep
// if (month == 3 && (hour + 24 * day) >= (1 + tzHours + 24 * (31 - (5 * year / 4 + 4) % 7)) || month == 10 && (hour + 24 * day) < (1 + tzHours + 24 * (31 - (5 * year / 4 + 1) % 7)));
if (month(local_t) == 3 && (hour(local_t) + 24 * day(local_t)) >= (1 + 24 * (31 - (5 * year(local_t) / 4 + 4) % 7)) || month(local_t) == 10 && (hour(local_t) + 24 * day(local_t)) < (1 + 24 * (31 - (5 * year(local_t) / 4 + 1) % 7)))
return true;
else
return false;
}

Zusammenfassung der Änderungen

Die Änderungen gegenüber dem Original-Beispiel sind:

  • Ab dem ESP Core 2.4.0 hat die DNS Abfrage einen dritten optionalen Parameter für einen Timeout. Damit wartet die NTP Ermittlung nicht mehr 10 Sekunden sollte keine Internetverbindung bestehen.
  • Zur Laufzeit werden die Timeouts "dynamisch" angepasst. Damit bricht die NTP im Fehlerfall schneller ab. Jeder Fehlversuch verlängert den nächsten Timeout.
  • Der Intervall für die nächste NTP Abfrage nach einem Fehlversuch kann parametriert werden.
  • Die Systemzeit läuft in UTC - nicht in lokaler Zeit (siehe weiter unten)
  • Sommerzeitumstellung für Europa/EU
  • Die Printausgaben wurden etwas modifiziert, sodass der Sketch trotz der zusätzlichen Funktionalität nicht wesentlich mehr Speicher als das mitgelieferte Beispiel benötigt.

Testen

Zum testen empfiehlt es sich, die Timeout kürzer zu wählen:

const uint32_t syncIntervalMax = 60; // could be the same value like the syncInterval in time.cpp | 300
const uint32_t syncIntervalAfterFail = 30; // if sync with NTP fails, retry earlier | 60

Und dann noch in eigener Sache

  • Der Sketch ist so wie er ist. Ich bin kein Programmierer und so sieht der Code auch aus.
  • Kommentare gibt es dann, wenn ich glaube dass sie notwendig sind. Andere machen sicher mehr.
  • MISSING in den Kommentaren deuten auf Unzulänglichkeiten im Code, notwendige Klärungen oder Todo's.
  • Meine Entwicklung ist für einen NodeMCU V1.0 entstanden.

Anmerkung: diese Art der NTP Abfrage ist per Stand 2019 veraltet und ab ESP Core 2.6.0 nicht mehr notwendig. Der Artikel wurde überarbeitet. Die neue Version findest du hier:

Die NTP Abfrage für den ESP8266 / NodeMCU mit Sommerzeit

Links

Die mit Sternchen (*) gekennzeichneten Verweise sind sogenannte Affiliate/Provision-Links. Wenn du auf so einen Verweis klickst und über diesen Link einkaufst, bekomme ich von deinem Einkauf eine (kleine) Provision. Für dich verändert sich der Preis dadurch nicht. Ich empfehle nur Produkte die ich selber besitze und wenn ich überzeugt bin, dass sie für andere interesssant sind.

Protokoll

erstellt: 2018-02-03 | Stand: 2024-03-22