Recurring Updates to ESP32 Time on LED Clock Using NTP Server

For several months, I have been trying to get my Arduino sketch to connect to wifi, obtain time from an ntp server, update the ESP32 internal time, display the time on an LED clock, and disconnect from Wifi. I use Wifi manager initially to setup wifi credentials and then obtain the time as outlined previously. That has been working fine...time displayed and updated on clock. However, since internal timekeeping of ESP32 drifts a fair amount, I want to autoconnect to the internet when the day changes (i.e., midnight) and follow the previous steps to update the internal ESP32 to keep the clock more accurate. I thought this wouldn't be too difficult, but time displayed on my LED clock still drifts. I think the wifi connecting and disconnecting functions are working fine. I also think reaching the NTP server is working. The problem seems to exist with clearing/updating the internal ESP32 timekeeping mechanism. This is my first forum post, so please be gentle :smile: Thanks!

/* Use this sketch with Paul's LED Clock. Select the ESP32 Dev Module board with partition scheme for "Huge APP".
   It uses the WifiManager library to enter wifi credentials over an internet connected device instead of 
   hard-coded credentials. The sketch also accesses the Internet daily to adjust the time. */

#include <WiFi.h>
#include "time.h"

#include <MD_Parola.h>
#include <MD_MAX72xx.h>
#include <SPI.h>

#include <WiFiManager.h>  // https://github.com/tzapu/WiFiManager

#define MAXTIMEFAILCOUNT 5  // Maximum number of times the wireless time setting process can fail before giving up
#define WIFIFAILCOUNT 5     // Maximum number of tries to connect to the wireless network

#define HARDWARE_TYPE MD_MAX72XX::FC16_HW
#define MAX_DEVICES 8
#define CLK_PIN 14
#define DATA_PIN 13
#define CS_PIN 15

#define BUF_SIZE 45
#define SPEED_BASE 300
char curMessage[BUF_SIZE] = { "" };
char newMessage[BUF_SIZE] = { "" };
bool newMessageAvailable = true;
byte intensityDisplay;
byte speedIndex = 3;

bool dailyTimeCheck = false;

WiFiManager wm;

struct tm timeinfo;

const long checkinterval = 2000;  // time to try again to obtain ntp time

const char* ntpServer = "pool.ntp.org";
const long gmtOffset_sec = -18000;
const int daylightOffset_sec = 3600;

int yr = 0;
int mt = 0;
int dy = 0;
int hr = 0;
int mi = 0;
int se = 0;

int prevMinute = -1;  // // Capture the minute to check if it's time to update the ntp time
int prevHour = -1;    // Capture the hour to check if it's time to update the ntp time
int prevDay = -1;     // Capture the day to check if it's time to update the ntp time

MD_Parola P = MD_Parola(HARDWARE_TYPE, DATA_PIN, CLK_PIN, CS_PIN, MAX_DEVICES);

void setup() {

  Serial.begin(115200);

  WiFi.mode(WIFI_STA);  // explicitly set mode, esp defaults to STA+AP
                        // it is a good practice to make sure your code sets wifi mode how you want it.

  //WiFiManager, Local intialization. Once its business is done, there is no need to keep it around
  //  WiFiManager wm;

  /* Reset settings - For testing purposes, wipes eeprom credentials
     stored by the ESP library.
     Uncomment line below for testing wireless setup. 
     User must re-enter wireless credentials each time device is powered up.
  */

  //wm.resetSettings();

  // Automatically connect using saved credentials,
  // if connection fails, it starts an access point with the specified name ( "AutoConnectAP"),
  // if empty will auto generate SSID, if password is blank it will be anonymous AP (wm.autoConnect())
  // then goes into a blocking loop awaiting configuration and will return success result

  bool res;
  // res = wm.autoConnect(); // auto generated AP name from chipid
  res = wm.autoConnect("LEDClockConnectAP");  // anonymous ap
  // res = wm.autoConnect("AutoConnectAP","password"); // password protected ap

  if (!res) {
    Serial.println("Failed to connect.");
    //  ESP.restart();
  } else {
    //if you get here you have connected to the WiFi
    Serial.println("Success...connected to WiFi.");
  }

  //wifiConnect(); Don't need to connect to wifi, already connected

  P.begin();
  P.displayClear();
  P.displayReset();
  intensityDisplay = 2;
  P.setIntensity(intensityDisplay);

  fetchNTPTime();
  strcpy(curMessage, newMessage);
  newMessageAvailable = false;
  P.displayText(curMessage, PA_CENTER, (SPEED_BASE / speedIndex), 0, PA_PRINT, PA_NO_EFFECT);

  wifiDisconnect();
}

void loop() {
  if (P.displayAnimate() && newMessageAvailable) {
    strcpy(curMessage, newMessage);
    newMessageAvailable = false;
    P.displayText(curMessage, PA_CENTER, (SPEED_BASE / speedIndex), 0, PA_PRINT, PA_NO_EFFECT);
  }
  timeUpdate();  // Check every second to update time on the display
}

// This file contains the wifi and time code

/*
  %A  Full weekday name
  %B  Full month name
  %d  Day of the month
  %Y  Year
  %H  Hour in 24h format
  %I  Hour in 12h format
  %M  Minute
  %S  Second
*/

void timeUpdate() {
  // struct tm timeinfo;
  if (isNewDay()) {
    dailyTimeCheck = true;
    wifiConnect();
    fetchNTPTime();  // Use a function to get NTP time
    wifiDisconnect();
    dailyTimeCheck = false;
  } else {
    if (getLocalTime(&timeinfo) && !dailyTimeCheck) {
      int currentMinute = timeinfo.tm_min;
      if (currentMinute != prevMinute) {
        // Update the time on the display
        strftime(newMessage, sizeof(newMessage), "%H : %M", &timeinfo);
        newMessageAvailable = true;
        // Update the previous minute
        prevMinute = currentMinute;
      }
    }
  }
}

// bool isNewHour() {
//   struct tm timeinfo;
//   if (getLocalTime(&timeinfo)) {
//     int currentHour = timeinfo.tm_hour;
//     if (currentHour != prevHour) {
//       // Update the previous day
//       prevHour = currentHour;
//       return true;
//     }
//   }
//   return false;
// }

bool isNewDay() {
  // struct tm timeinfo;
  if (getLocalTime(&timeinfo)) {
    int currentDay = timeinfo.tm_mday;
    if (currentDay != prevDay) {
      // Update the previous day
      prevDay = currentDay;
      return true;
    }
  }
  return false;
}

void fetchNTPTime() {
    // Clear existing time on ESP32
    struct timeval tv_reset;
    tv_reset.tv_sec = 0;
    tv_reset.tv_usec = 0;
    settimeofday(&tv_reset, nullptr);

    // Obtain time from NTP server
    configTime(gmtOffset_sec, daylightOffset_sec, ntpServer); // Config time synchronization parameters
    Serial.println("Waiting for time from NTP...");
    time_t now = time(nullptr);
    int retry = 0;
    while (now < 24 * 3600) { // Time set to 1970, waiting for NTP sync
        delay(500);
        Serial.print(".");
        now = time(nullptr);
        retry++;
        if (retry > 20) {
            Serial.println("Failed to obtain time from NTP server.");
            return;
        }
    }

    // Update internal clock of ESP32
    struct tm timeinfo;
    gmtime_r(&now, &timeinfo);
    struct timeval tv;
    tv.tv_sec = now;
    tv.tv_usec = 0;
    settimeofday(&tv, nullptr);

    yr = timeinfo.tm_year + 1900;
    mt = timeinfo.tm_mon + 1;
    dy = timeinfo.tm_mday;
    hr = timeinfo.tm_hour;
    mi = timeinfo.tm_min;
    se = timeinfo.tm_sec;

    Serial.println("Time obtained from NTP server:");
    Serial.println(&timeinfo, "%A, %B %d %Y %H:%M:%S");

    // Immediately update the LED clock display
    strftime(newMessage, sizeof(newMessage), "%H : %M", &timeinfo);
    newMessageAvailable = true;
    P.displayText(newMessage, PA_CENTER, (SPEED_BASE / speedIndex), 0, PA_PRINT, PA_NO_EFFECT);
}

void wifiConnect() {
  // Connect to WiFi
  static int wifiFail = 0;  // Count the number of failed attempts to connect to wifi
  static unsigned long lastTimeCheck = 0;
  unsigned long timeNow = millis();
  Serial.println("Connecting to wifi.");
  wm.setConfigPortalBlocking(false);  // Per example, this should result in non-blocking code.
  wm.setConfigPortalTimeout(120);
  wm.setTimeout(20);
  wm.autoConnect();
  if (WiFi.status() != WL_CONNECTED) {        
    wifiFail++;
    if (wifiFail < WIFIFAILCOUNT) {
      Serial.println("Trying again to connect to WiFi.");
      if ((timeNow - lastTimeCheck) >= checkinterval) {
        wifiConnect();
        lastTimeCheck = timeNow;
      }
    } else {
      Serial.println("Unable to connect to WiFi after several attempts.");
    }
  } else {
    Serial.println("Connected to WiFi.");
  }  
}

void wifiDisconnect() {
  // Disconnect from Wifi  
  Serial.printf("Disconnecting from WiFi.");
  Serial.println();
  WiFi.disconnect(true);
  WiFi.mode(WIFI_OFF); // This command should actually turn off wifi
}

the default NTP interval is 1h.
Why is that not working for you?

There's no reason to do anything like that. ESP32 can be configured to automatically and periodically update its internal time via NTP. The update period is configurable, but as noted by @noiasca, every hour is more than sufficient. You can also configure it so that updates smoothly and monotonically adjust the internal time.

Thanks @noiasca and @gfvalvo for the quick replies! I'm definitely not a skilled programmer. Could you please assist with updating my code? I'd rather just connect to the NTP once a day to obtain the current time...probably not necessary to retrieve it every hour. Other than getting the NTP time, I really don't require wifi for anything else in this project. ChatGPT gave me some of the recommended code for the fetchNTPTime function, but I noticed that the time displayed on the clock with still drifting. If you could help me streamline my code, I would really appreciate it.

what is the issue - why aren't you connected to internet 24h/7?

if you see a drift in 24h already, why would you use an 24h interval? Do you see this drift already after 12h? after 6h?

The clock crystal on an ESP runs tight - othewise the WiFi wouldn't work.

I guess it's alright to be connected to the internet 24/7, but I wanted to reduce power consumption. I have some other motor and LED things going on separately from the clock functions. I think the ESP32 internal clock drifts about 2 seconds per day. I'm not worried about the two seconds specifically, but after a month it's obviously around a minute. I thought just a quick daily update of the time via NTP would suffice.

Hmmm...that's interesting. Is a different mechanism being using for keeping this clock time?

So somewhere near midnight (or 2AM seems 'golden') go get the NTP time and plug that in.

Or start looking for NTP midnight in anticipation and zero it out.

That was my thought...just update the clock time when the day changes. I can just keep the clock off the network except for that very short update period.

some months ago I made a POC with an ESP and deepsleep:

How can I force an ESP32 that awakes from deepsleep to RE-sync with a NTP-server? - Using Arduino / Programming Questions - Arduino Forum

I assume this will also work if you aren't connected to wifi 24/7.
So disconnect wifi (to reduce power? *)
connect it at midnight,
wait till you get the information of a valid NTP sync (from the callback)
disconnect Wifi.

*) Powersaving by switching of the WIFI and a LED Matrix sounds a bit weired to me ...

Unless it's battery powered. Otherwise it's solving a non-existent problem.

Thanks! I'll check out that link to the related topic. I may have seen it already after coming across your replies to other posts during my web search for help. When I saw your reply to me, I thought...I recognize the username noiasca! :smile:

1 Like

Good Morning, I updated my sketch to include coding from the topic linked by noiasca. Instead of synchronizing the ESP32 time with the NTP server once each day, I decided to sync it every hour. I've been comparing the time displayed on the LED clock with the internet time. I think it works :smile: However, I've been fooled many times over the last few months when it wasn't actually working. I'm going to test it a couple of more days. Although the updated code seems to work, I'm still not sure if I'm using the callback function properly. If you don't mind, please review my updated code and provide fixes/recommendations for improvement. Thank you again!

/* Use this sketch with Paul's LED Clock. Select the ESP32 Dev Module board with partition scheme for "Huge APP".
   It uses the WifiManager library to enter wifi credentials over an internet connected device instead of 
   hard-coded credentials. The sketch also accesses the Internet daily to adjust the time. 
   Many thanks to noiasca and gfvalvo on the Arduino Forum for their assistance. */

#include <WiFi.h> // we need wifi to get internet access
#include "time.h" // for time() ctime()

#include <MD_Parola.h>
#include <MD_MAX72xx.h>
#include <SPI.h>

#include <WiFiManager.h>  // https://github.com/tzapu/WiFiManager

#define WIFIFAILCOUNT 5     // Maximum number of tries to connect to the wireless network

#define HARDWARE_TYPE MD_MAX72XX::FC16_HW
#define MAX_DEVICES 8
#define CLK_PIN 14
#define DATA_PIN 13
#define CS_PIN 15

#define BUF_SIZE 45
#define SPEED_BASE 300
char curMessage[BUF_SIZE] = { "" };
char newMessage[BUF_SIZE] = { "" };
bool newMessageAvailable = true;
byte intensityDisplay;
byte speedIndex = 3;

bool dailyTimeCheck = false;

WiFiManager wm;

time_t now;                          // this are the seconds since Epoch (1970) - UTC
struct tm timeinfo;

const long checkinterval = 2000;  // time to try again to obtain ntp time

/* Configuration of NTP */
// choose the best fitting NTP server pool for your country
#define MY_NTP_SERVER "pool.ntp.org"

// choose your time zone from this list
// https://github.com/nayarsystems/posix_tz_db/blob/master/zones.csv
#define MY_TZ "EST5EDT,M3.2.0,M11.1.0"

int yr = 0;
int mt = 0;
int dy = 0;
int hr = 0;
int mi = 0;
int se = 0;

int prevMinute = -1;  // // Capture the minute to check if it's time to update the ntp time
int prevHour = -1;    // Capture the hour to check if it's time to update the ntp time
int prevDay = -1;     // Capture the day to check if it's time to update the ntp time

MD_Parola P = MD_Parola(HARDWARE_TYPE, DATA_PIN, CLK_PIN, CS_PIN, MAX_DEVICES);

// callback to check if NTP was called
#include "esp_sntp.h"
void cbSyncTime(struct timeval *tv) { // callback function to show when NTP was synchronized
  Serial.println("NTP time synched");  
}

void setup() {

  Serial.begin(115200);

  WiFi.mode(WIFI_STA);  // explicitly set mode, esp defaults to STA+AP
                        // it is a good practice to make sure your code sets wifi mode how you want it.

  //WiFiManager, Local intialization. Once its business is done, there is no need to keep it around
  //  WiFiManager wm;

  /* Reset settings - For testing purposes, wipes eeprom credentials
     stored by the ESP library.
     Uncomment line below for testing wireless setup. 
     User must re-enter wireless credentials each time device is powered up.
  */

  //wm.resetSettings();

  // Automatically connect using saved credentials,
  // if connection fails, it starts an access point with the specified name ( "AutoConnectAP"),
  // if empty will auto generate SSID, if password is blank it will be anonymous AP (wm.autoConnect())
  // then goes into a blocking loop awaiting configuration and will return success result

  bool res;
  // res = wm.autoConnect(); // auto generated AP name from chipid
  res = wm.autoConnect("LEDClockConnectAP");  // anonymous ap
  // res = wm.autoConnect("AutoConnectAP","password"); // password protected ap

  if (!res) {
    Serial.println("Failed to connect.");
    //  ESP.restart();
  } else {
    //if you get here you have connected to the WiFi
    Serial.println("Success...connected to WiFi.");
  }

  //wifiConnect(); Don't need to connect to wifi, already connected

  P.begin();
  P.displayClear();
  P.displayReset();
  intensityDisplay = 2;
  P.setIntensity(intensityDisplay);

  sntp_set_time_sync_notification_cb(cbSyncTime);  // set a Callback function for time synchronization notification
  configTime(0, 0, MY_NTP_SERVER);  // 0, 0 because we will use TZ in the next line
  setenv("TZ", MY_TZ, 1);            // Set environment variable with your time zone
  tzset();

  fetchNTPTime();
  strcpy(curMessage, newMessage);
  newMessageAvailable = false;
  P.displayText(curMessage, PA_CENTER, (SPEED_BASE / speedIndex), 0, PA_PRINT, PA_NO_EFFECT);

  wifiDisconnect();
}

void loop() {
  if (P.displayAnimate() && newMessageAvailable) {
    strcpy(curMessage, newMessage);
    newMessageAvailable = false;
    P.displayText(curMessage, PA_CENTER, (SPEED_BASE / speedIndex), 0, PA_PRINT, PA_NO_EFFECT);
  }
  timeUpdate();  // Check every second to update time on the display
}

// This file contains the wifi and time code

/*
  %A  Full weekday name
  %B  Full month name
  %d  Day of the month
  %Y  Year
  %H  Hour in 24h format
  %I  Hour in 12h format
  %M  Minute
  %S  Second
*/

void timeUpdate() {  
  if (isNewHour()) {
    dailyTimeCheck = true;
    wifiConnect();
    fetchNTPTime();  // Use a function to get NTP time
    wifiDisconnect();
    dailyTimeCheck = false;
  } else {
    if (getLocalTime(&timeinfo) && !dailyTimeCheck) {
      int currentMinute = timeinfo.tm_min;
      if (currentMinute != prevMinute) {
        // Update the time on the display
        strftime(newMessage, sizeof(newMessage), "%H : %M", &timeinfo);
        newMessageAvailable = true;
        // Update the previous minute
        prevMinute = currentMinute;
      }
    }
  }
}

bool isNewHour() {
  if (getLocalTime(&timeinfo)) {
    int currentHour = timeinfo.tm_hour;
    if (currentHour != prevHour) {
      // Update the previous hour
      prevHour = currentHour;
      return true;
    }
  }
  return false;
}

// bool isNewDay() {
//   if (getLocalTime(&timeinfo)) {
//     int currentDay = timeinfo.tm_mday;
//     if (currentDay != prevDay) {
//       // Update the previous day
//       prevDay = currentDay;
//       return true;
//     }
//   }
//   return false;
// }

void fetchNTPTime() {
    
    // Update internal clock of ESP32
    struct tm local;
    sntp_set_time_sync_notification_cb(cbSyncTime);  // set a Callback function for time synchronization notification
    configTzTime(MY_TZ, MY_NTP_SERVER);    
    getLocalTime(&local, 10000);           // try 10 secs to sync

    yr = timeinfo.tm_year + 1900;
    mt = timeinfo.tm_mon + 1;
    dy = timeinfo.tm_mday;
    hr = timeinfo.tm_hour;
    mi = timeinfo.tm_min;
    se = timeinfo.tm_sec;

    Serial.println("Time obtained from NTP server:");
    showTime();

    // Immediately update the LED clock display
    strftime(newMessage, sizeof(newMessage), "%H : %M", &timeinfo);
    newMessageAvailable = true;
    P.displayText(newMessage, PA_CENTER, (SPEED_BASE / speedIndex), 0, PA_PRINT, PA_NO_EFFECT);
}

void showTime() {
  time(&now); // read the current time
  localtime_r(&now, &timeinfo);             // update the structure tm with the current time
  Serial.print("year:");
  Serial.print(timeinfo.tm_year + 1900);    // years since 1900
  Serial.print("\tmonth:");
  Serial.print(timeinfo.tm_mon + 1);        // January = 0 (!)
  Serial.print("\tday:");
  Serial.print(timeinfo.tm_mday);           // day of month
  Serial.print("\thour:");
  Serial.print(timeinfo.tm_hour);           // hours since midnight 0-23
  Serial.print("\tmin:");
  Serial.print(timeinfo.tm_min);            // minutes after the hour 0-59
  Serial.print("\tsec:");
  Serial.print(timeinfo.tm_sec);            // seconds after the minute 0-61*
  Serial.print("\twday");
  Serial.print(timeinfo.tm_wday);           // days since Sunday 0-6
  if (timeinfo.tm_isdst == 1)               // Daylight Saving Time flag
    Serial.print("\tDST");  
  else
    Serial.print("\tstandard");
  Serial.println();
}

void wifiConnect() {
  // Connect to WiFi
  static int wifiFail = 0;  // Count the number of failed attempts to connect to wifi
  static unsigned long lastTimeCheck = 0;
  unsigned long timeNow = millis();
  Serial.println("Connecting to wifi.");
  wm.setConfigPortalBlocking(false);  // Per example, this should result in non-blocking code.
  wm.setConfigPortalTimeout(120);
  wm.setTimeout(20);
  wm.autoConnect();
  if (WiFi.status() != WL_CONNECTED) {        
    wifiFail++;
    if (wifiFail < WIFIFAILCOUNT) {
      Serial.println("Trying again to connect to WiFi.");
      if ((timeNow - lastTimeCheck) >= checkinterval) {
        wifiConnect();
        lastTimeCheck = timeNow;
      }
    } else {
      Serial.println("Unable to connect to WiFi after several attempts.");
    }
  } else {
    Serial.println("Connected to WiFi.");
  }  
}

void wifiDisconnect() {
  // Disconnect from Wifi  
  Serial.printf("Disconnecting from WiFi.");
  Serial.println();
  WiFi.disconnect(true);
  WiFi.mode(WIFI_OFF); // This command should actually turn off wifi
}

Is it your intent to call wifiConnect() recursively?

This is my minimalistic version of an ESP8266 clock. Maybe parts are useful.
I update time every 10 hours (more than enough), and switch off WiFi after an update.
Add your WiFi credentials, set your location (link is in the sketch), and local pool.
Leo..

#include <ESP8266WiFi.h>
unsigned long prevMillis, prevTime, interval = 36e6; // 10 hours
bool ntp, sleep;
time_t now;
tm tm;

void time_is_set() { // valid time string?
  Serial.println("time was updated");
  ntp = true;
}

void setup() {
  Serial.begin(74880); // native baud rate
  Serial.println("\nESP8266 Clock");
  WiFi.mode(WIFI_STA);
  WiFi.begin("My_SSID", "My_PW"); // WiFi credentials
  // https://github.com/nayarsystems/posix_tz_db/blob/master/zones.csv
  configTime("NZST-12NZDT,M9.5.0,M4.1.0/3", "nz.pool.ntp.org"); // location, local pool
  settimeofday_cb(time_is_set); // enable callback
  while (WiFi.status() != WL_CONNECTED) delay(200); // connect
  while (!ntp); // wait here until a valid time string is received
}

void loop() {
  time(&now); // epoch
  localtime_r(&now, &tm); // add local offset
  if (now != prevTime) { // only print if time has changed
    prevTime = now; // remember
    printf("%02u:%02u:%02u\n", tm.tm_hour, tm.tm_min, tm.tm_sec);
    if (ntp) {
      ntp = false;
      sleep = true;
      prevMillis = millis();
      Serial.println("Sleep");
      WiFi.forceSleepBegin();
    }
    if (sleep && millis() - prevMillis > interval) {
      sleep = false;
      Serial.println("Wake");
      WiFi.forceSleepWake();
    }
  }
}

The old way, not used anymore.

NTP is built into the latest ESP libraries.
Use a location string. That also takes care of daylight savings.
Leo..

Yes. My thought was to connect once a day (when the day changes) to sync the ESP32 time with NTP, and then disconnect. It shouldn't be a problem to sync it every hour, but I thought that was a bit overkill. As noiasca pointed out, I could just leave the ESP32 connected to wifi all the time. I just figured it was unnecessary to stay connected, and I could keep the ESP32 off the network. Not that bandwidth is an issue for this process :laughing: As you pointed out, saving battery life would be a good reason to reduce wifi connectivity. This clock will be plugged in, but reducing wifi time may be very useful for future battery powered projects.

Thanks Wawa!!!

It all started here (click).

The ESP32 is a bit different.
There is a link for that at the bottom of that page.
Leo..