ESP32 SNTP C++ ESP-IDF

Automatically set the ESP32 time using an NTP server in C++ using the ESP-IDF

SNTP or Simple Network Time Protocol is a protocol used to get the current time and date.

In this tutorial, we will create an SNTP class to fetch the time from an NTP server to set the time for the ESP32 device.

ESP32 SNTP C++ library

Open CPPWIFI

Open VS Code and open the CPPWIFI folder that we created here. If you have not done that tutorial and do not want to do so now, you can download the project from GitHub here. The project inherits from the Wifi class that we created to check if the wifi is connected before it will attempt to fetch the time and date from an NTP server.

Download CPPWIFI from GitHub

To use the project from GitHub, follow the following steps:

  • Download the CPPWIFI project from GitHub and save the folder on your computer.
  • Create a new project in VS Code following these instructions.
  • Copy the .vscode folder that was created in the new sample project into the CPPWIFI folder
  • Open the CPPWIFI folder in VS Code, you should now be able to build the project and flash it to your ESP32 device.

Create the header and source files for the SNTP class

Create and new file in the include/CPPWIFI folder and name it sntp.h.

Now create a new file in src/CPPWIFI and name this file sntp.cpp.

Update CMake

Open CMakeLists.txt in the src folder and add the newly created sntp.cpp file to the sources variable. Remember that sntp.cpp is in the CPPWIFI folder, so we must add the full path CPPWIFI/sntp.cpp.

The contents of CMakeLists.txt should now look like this:

set(SOURCES main.cpp
            CPPWIFI/wifi.cpp
            CPPWIFI/sntp.cpp)
            
idf_component_register(SRCS ${SOURCES}
                    INCLUDE_DIRS .  ../include/CPPWIFI
                    REQUIRES esp_wifi nvs_flash esp_event esp_netif lwip)

We do not need to add sntp.h because the /include/CPPWIFI folder is already added to INCLUDE_DIRS

Define the Sntp class in the header

Open sntp.h in the /include/CPPWIFI folder and add the following code:

#pragma once

#include <ctime>
#include <chrono>
#include <iomanip>
#include <string>

#include "esp_sntp.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

#include "wifi.h"

namespace SNTP
{
    class Sntp : private WIFI::Wifi // Inherits from Wifi to access the wifi state
    {       
        static void callbackOnNtpUpdate(timeval *tv);

    public:
        Sntp(void) = default;
        ~Sntp(void) { sntp_stop(); };

        static esp_err_t init(void);

        static bool setUpdateInterval(uint32_t ms, bool immediate = false);

        [[nodiscard]] bool sntpState(void) {return _running;}

        [[nodiscard]] static const auto timePointNow(void);

        [[nodiscard]] static const auto timeSinceLastUpdate(void);

        [[nodiscard]] static const char* timeNowAscii(void);

        [[nodiscard]] static std::chrono::seconds epochSeconds(void);

    private:
        static std::chrono::_V2::system_clock::time_point _lastUpdate;
        static bool _running;

    }; // Class Sntp
} // namespace SNTP

Notice that nearly all methods and variables are static. We will only ever have one instance of this class.

Let’s take a closer look at the parts:

#pragma once

#include <ctime>
#include <chrono>
#include <iomanip>
#include <string>

#include "esp_sntp.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

#include "wifi.h"

This is the include guard and some include that the sntp class will need. At this point, I’d like to mention that it’s not best practice to add all the incudes in the header file. It can increase compilation time if headers are included that are not needed.

static void callbackOnNtpUpdate(timeval *tv);

This is the callback method that will be called every time the time and dates are updated from the NTP server.

    public:
        Sntp(void) = default;
        ~Sntp(void) { sntp_stop(); };

        static esp_err_t init(void);

        static bool setUpdateInterval(uint32_t ms, bool immediate = false);

        [[nodiscard]] bool sntpState(void) {return _running;}

        [[nodiscard]] static const auto timePointNow(void);

        [[nodiscard]] static const auto timeSinceLastUpdate(void);

        [[nodiscard]] static const char* timeNowAscii(void);

        [[nodiscard]] static std::chrono::seconds epochSeconds(void);

The public methods of this class.

[[nodiscard]] is added to encourage the compiler to issue warnings if the return value is not used.

Let’s see what each does:

  • Sntp(void) is the constructor. Nothing special happens when the SNTP object is created.
  • ~Sntp(void) is the deconstructor. When the SNTP object gets destroyed sntp_stop() will be called to stop the sntp service.
  • init(void) initialize the sntp service to some hard-coded settings.
  • setUpdateInterval sets how often the ESP32 device polls the sntp server for the current time and date.
  • sntpState(void) returns whether the sntp service has been started.
  • timePointNow(void) returns the current time and date.
  • timeSinceLastUpdate(void) returns the amount of time that passed since the last update from the NTP server.
  • timeNowAscii(void) returns the current time in user-readable ASCII format.
  • epochSeconds(void) returns the number of seconds passed since the time epoch.
    private:
        static std::chrono::_V2::system_clock::time_point _lastUpdate;
        static bool _running;

We have only 2 private variables and no private methods:

  • _lastUpdate holds the time of the last update from an NTP server.
  • _running indicates whether the NTP service is started.

Implement the class methods

Open sntp.cpp in the src/CPPWIFI folder and add the following code:

#include "sntp.h"
#include <iostream>

namespace SNTP
{
    //Statics
    std::chrono::_V2::system_clock::time_point Sntp::_lastUpdate{};
    bool Sntp::_running{false};

    void Sntp::callbackOnNtpUpdate(timeval *tv)
    {
        std::cout << "NTP updated, current time is: " << timeNowAscii() << '\n';
        _lastUpdate = std::chrono::system_clock::now();
    }

    esp_err_t Sntp::init(void)
    {
        if (!_running)
        {
            if(state_e::CONNECTED != Wifi::GetState())
            {
                std::cout << "Failed to initialise SNTP, Wifi not connected\n";
                return ESP_FAIL;
            }

            setenv("TZ", "GMT-2", 1); // South Africa Time (GMT+2)
            tzset();

            sntp_setoperatingmode(SNTP_OPMODE_POLL);
            sntp_setservername(0, "time.google.com");
            sntp_setservername(1, "pool.ntp.com");

            sntp_set_time_sync_notification_cb(&callbackOnNtpUpdate);
            sntp_set_sync_interval(60 * 60 * 1000); // Update time every hour

            sntp_init();

            std::cout << "SNTP Initialised\n";

            _running = true;
        }

        if (_running)
        {
            return ESP_OK;
        }
        return ESP_FAIL;
    }

    bool Sntp::setUpdateInterval(uint32_t ms, const bool immediate)
    {
        if (_running)
        {
            sntp_set_sync_interval(ms);
            if (immediate)
            {
                sntp_restart();
            }
            return true;
        }
        return false;
    }

    [[nodiscard]] const auto Sntp::timePointNow(void)
    {
        return std::chrono::system_clock::now();
    }

    [[nodiscard]] const auto Sntp::timeSinceLastUpdate(void)
    {
        return timePointNow() - _lastUpdate;
    }

    [[nodiscard]] std::chrono::seconds Sntp::epochSeconds(void)
    {
        return std::chrono::duration_cast<std::chrono::seconds>(timePointNow().time_since_epoch());
    }

    [[nodiscard]] const char *Sntp::timeNowAscii(void)
    {
        const std::time_t timeNow{std::chrono::system_clock::to_time_t(timePointNow())};

        return std::asctime(std::localtime(&timeNow));
    }
}// namespace SNTP

Let’s look at each method.

    void Sntp::callbackOnNtpUpdate(timeval *tv)
    {
        std::cout << "NTP updated, current time is: " << timeNowAscii() << '\n';
        _lastUpdate = std::chrono::system_clock::now();
    }

This is a callback method that will be called every time that the time gets updated from the NTP server.

In this example, we write the current time and date to the serial port and set the _lastUpdate to the time when the callback method is called. This is useful if you want to find out how much the clock drift is for your device which in turn can be used to decide how often you want to update the time from the NTP server.

    esp_err_t Sntp::init(void)
    {
        if (!_running)
        {
            if(state_e::CONNECTED != Wifi::GetState())
            {
                std::cout << "Failed to initialise SNTP, Wifi not connected\n";
                return ESP_FAIL;
            }

            setenv("TZ", "GMT-2", 1); // South Africa Time (GMT+2)
            tzset();

            sntp_setoperatingmode(SNTP_OPMODE_POLL);
            sntp_setservername(0, "time.google.com");
            sntp_setservername(1, "pool.ntp.com");

            sntp_set_time_sync_notification_cb(&callbackOnNtpUpdate);
            sntp_set_sync_interval(60 * 60 * 1000); // Update time every hour

            sntp_init();

            std::cout << "SNTP Initialised\n";

            _running = true;
        }

        if (_running)
        {
            return ESP_OK;
        }
        return ESP_FAIL;
    }

This is where we initialize the sntp service.

First, we check if the service is already running, if it is then we do nothing and return ESP_OK. If the service is not running, then we check if the wifi is connected. If the Wifi is not connected then we write a message to the serial port and return ESP_FAIL. Note that in the future we will use logging to write tagged messages to the serial port.

Let’s now look closer at the individual parts to initialize the sntp service:

            setenv("TZ", "GMT-2", 1); // (GMT+2)
            tzset();

These two methods are not ESP32 or ESP-IDF specific, these are GNU POSIC system methods to change environment variables.

The specific environment variable that we want to change is TZ or the Time Zone environment variable. The area that I live in is in the timezone “GMT+2” meaning my time is 2 hours ahead of Greenwich Mean Time. For my ESP32 device to display the correct time, I need to set the TZ environment variable to a string value of “GMT-2“.

We then call tzset() to update the C library runtime data for the new time zone.

            sntp_setoperatingmode(SNTP_OPMODE_POLL);
            sntp_setservername(0, "time.google.com");
            sntp_setservername(1, "pool.ntp.com");

In these set of function calls, we set the sntp operating mode to poll the sntp server and then we add 2 sntp servers to use. If one sntp server becomes unavailable then the ESP32 device will attempt to use the other.

            sntp_set_time_sync_notification_cb(&callbackOnNtpUpdate);
            sntp_set_sync_interval(60 * 60 * 1000); // Update time every hour

Here we set the callback function to be called whenever the system time has been updated from an NTP server and we set the update interval.

You’ll notice that I wrote the argument for sntp_set_sync_interval() as 60 * 60 * 1000 rather than 3 600 000. Doing it this way makes it significantly easier to see what the time period is, and easy to edit to any length of time. For example, if I want the interval to be two hours, then I can simply change it to 2 * 60 * 60 * 1000.

sntp_init();

With the setup done, we can now call this function to start the sntp service.

    bool Sntp::setUpdateInterval(uint32_t ms, const bool immediate)
    {
        if (_running)
        {
            sntp_set_sync_interval(ms);
            if (immediate)
            {
                sntp_restart();
            }
            return true;
        }
        return false;
    }

This method can be used to change the update interval. When calling the method we can add an optional boolean value that will tell the method to restart the sntp service and apply the new interval immediately. If the boolean is set for false and excluded, then the new interval will only come into effect after the next update.

    [[nodiscard]] const auto Sntp::timePointNow(void)
    {
        return std::chrono::system_clock::now();
    }

    [[nodiscard]] const auto Sntp::timeSinceLastUpdate(void)
    {
        return timePointNow() - _lastUpdate;
    }

    [[nodiscard]] std::chrono::seconds Sntp::epochSeconds(void)
    {
        return std::chrono::duration_cast<std::chrono::seconds>(timePointNow().time_since_epoch());
    }

    [[nodiscard]] const char *Sntp::timeNowAscii(void)
    {
        const std::time_t timeNow{std::chrono::system_clock::to_time_t(timePointNow())};

        return std::asctime(std::localtime(&timeNow));
    }

The last few methods only do what the method names imply.

Get the time from an NTP server

main.h

Open main.h in the src folder and add the following line of code:

SNTP::Sntp Sntp;

This creates an object of the Sntp class named Sntp.

The complete main.h file should now look like this:

#include "wifi.h"
#include "sntp.h"

class Main final
{
private:
public:
    void run(void);
    void setup(void);

    WIFI::Wifi::state_e wifiState { WIFI::Wifi::state_e::NOT_INITIALIZED };
    WIFI::Wifi Wifi;
    SNTP::Sntp Sntp;
};

main.cpp

Open main.cpp in the src folder. We are only going to add 5 (3) lines of code to get Sntp functionality.

bool sntpInitialised = Sntp.sntpState();
if(!sntpInitialised)
   {
        Sntp.init();
   }

In short, we check if the sntp service is running. If it’s not running then we call Sntp.init() to initialize and start the service.

The location of the new lines of code does matter, main.cpp should look like the following:

#include <string>
#include <iostream>

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "nvs_flash.h"
#include "main.h"

Main App;

void Main::run(void)
{
    wifiState = Wifi.GetState();
    bool sntpInitialised = Sntp.sntpState();

    switch (wifiState)
    {
    case WIFI::Wifi::state_e::READY_TO_CONNECT:
        std::cout << "Wifi Status: READY_TO_CONNECT\n";
        Wifi.Begin();
        break;
    case WIFI::Wifi::state_e::DISCONNECTED:
        std::cout << "Wifi Status: DISCONNECTED\n";
        Wifi.Begin();
        break;
    case WIFI::Wifi::state_e::CONNECTING:
        std::cout << "Wifi Status: CONNECTING\n";
        break;
    case WIFI::Wifi::state_e::WAITING_FOR_IP:
        std::cout << "Wifi Status: WAITING_FOR_IP\n";
        break;
    case WIFI::Wifi::state_e::ERROR:
        std::cout << "Wifi Status: ERROR\n";
        break;
    case WIFI::Wifi::state_e::CONNECTED:
        std::cout << "Wifi Status: CONNECTED\n";
        if(!sntpInitialised)
        {
            Sntp.init();
        }
        break;
    case WIFI::Wifi::state_e::NOT_INITIALIZED:
        std::cout << "Wifi Status: NOT_INITIALIZED\n";
        break;
    case WIFI::Wifi::state_e::INITIALIZED:
        std::cout << "Wifi Status: INITIALIZED\n";
        break;
    }
}

void Main::setup(void)
{
    esp_event_loop_create_default();
    nvs_flash_init();

    Wifi.SetCredentials("TestGuest", "00000000");
    Wifi.Init();
}

extern "C" void app_main(void)
{
    App.setup();
    while (true)
    {
        vTaskDelay(pdMS_TO_TICKS(1000));
        App.run();
    }
}

Notice that we are only calling Sntp.init() after the wifi is connected. The service will fail to start otherwise.

Note that this is not the best way to implement it in the main method, it’s the easiest way to demonstrate it for tutorial purposes.

Run the program

You can now compile the project and upload the program to your ESP32 device.

The current date and time should output to the terminal every time the system time is updated by the sntp server.

ESP32 SNTP time and date

Conclusion

With this class, you can set up your ESP32 device to get the current date and time automatically without having to manually set the date or time.

This will become even more useful when we cover logging in a future tutorial. When the SNTP service is running then the log adds a timestamp to the log output to the serial port.

To download this project from Github, click here.

Thanks for reading.

Similar Posts