NTPサーバーから現在の時間を得る

Arduino IDE (1.8.19)のサンプルスケッチにNTPサーバーから現在の時間を取得するプログラムが有ります。今回はその説明。

サンプルスケッチの場所

サンプルスケッチは ファイルー>スケッチ例ー>ESP32ー>Timeー>SimpleTimeに有ります。

以下はそのコード。

SimpleTime.ino

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

const char *ssid = "YOUR_SSID";
const char *password = "YOUR_PASS";

const char *ntpServer1 = "pool.ntp.org";
const char *ntpServer2 = "time.nist.gov";
const long gmtOffset_sec = 3600;
const int daylightOffset_sec = 3600;

const char *time_zone = "CET-1CEST,M3.5.0,M10.5.0/3";  // TimeZone rule for Europe/Rome including daylight adjustment rules (optional)

void printLocalTime() {
  struct tm timeinfo;
  if (!getLocalTime(&timeinfo)) {
    Serial.println("No time available (yet)");
    return;
  }
  Serial.println(&timeinfo, "%A, %B %d %Y %H:%M:%S");
}

// Callback function (gets called when time adjusts via NTP)
void timeavailable(struct timeval *t) {
  Serial.println("Got time adjustment from NTP!");
  printLocalTime();
}

void setup() {
  Serial.begin(115200);

  // First step is to configure WiFi STA and connect in order to get the current time and date.
  Serial.printf("Connecting to %s ", ssid);
  WiFi.begin(ssid, password);

  /**
   * NTP server address could be acquired via DHCP,
   *
   * NOTE: This call should be made BEFORE esp32 acquires IP address via DHCP,
   * otherwise SNTP option 42 would be rejected by default.
   * NOTE: configTime() function call if made AFTER DHCP-client run
   * will OVERRIDE acquired NTP server address
   */
  esp_sntp_servermode_dhcp(1);  // (optional)

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

  // set notification call-back function
  sntp_set_time_sync_notification_cb(timeavailable);

  /**
   * This will set configured ntp servers and constant TimeZone/daylightOffset
   * should be OK if your time zone does not need to adjust daylightOffset twice a year,
   * in such a case time adjustment won't be handled automagically.
   */
  configTime(gmtOffset_sec, daylightOffset_sec, ntpServer1, ntpServer2);

  /**
   * A more convenient approach to handle TimeZones with daylightOffset
   * would be to specify a environment variable with TimeZone definition including daylight adjustmnet rules.
   * A list of rules for your zone could be obtained from https://github.com/esp8266/Arduino/blob/master/cores/esp8266/TZ.h
   */
  //configTzTime(time_zone, ntpServer1, ntpServer2);
}

void loop() {
  delay(5000);
  printLocalTime();  // it will take some time to sync time :)
}
  • 関数 configTime()(コードの61行) を使用してNTPサーバーから時間データを取得します。
  • void configTime(long gmtOffset_sec, int daylightOffset_sec, const char* server1, const char* server2, const char* server3); 
    • long gmtOffset_sec:    GMTとローカル時刻との差(単位は秒)
    • int daylightOffset_sec: 夏時間で進める時間(単位は秒)。
    • const char* server1    NTPサーバ。最低一つ設定する。
    • const char* server2
    • const char* server3
  • 日本の場合、各引数は以下の様になります。
    • gmtOffset_sec: 9 * 3600
    • daylightOffset_sec:0 (夏時間は無い)
    • NTPサーバ:最低でも1個、最大3個指定出来ます。今回は例題通り。
  • スケッチの流れ
    • NTPサーバにアクセスする為に先ずWiFiに接続
    • 61行: configTime(gmtOffset_sec, daylightOffset_sec, ntpServer1, ntpServer2);
      • NTPサーバに接続し時刻データを取得する関数。
      • void configTime(long gmtOffset_sec, int daylightOffset_sec, const char* server1, const char* server2, const char* server3);
        • long gmtOffset_sec:    GMTとローカル時刻との差(単位は秒)
        • int daylightOffset_sec: 夏時間で進める時間(単位は秒)
        • const char* server1    NTPサーバ。最低一つ設定する。
        • const char* server2
        • const char* server3
      • 各引数は以下の様に設定しました。
        • gmtOffset_sec: 9 * 3600 (日本の場合、GMTとの時間差は9時間)
        • daylightOffset_sec:0 (夏時間が無い場合)
        • NTPサーバ:最低でも1個、最大3個指定出来ます。今回は例題通り。
    • NTPサーバから時刻データを取得後、関数getLocalTime()を使って時間を表示。
      • このスケッチは5秒に1回、時間をシリアルモニタに表示します。
    • 51行: sntp_set_time_sync_notification_cb(timeavailable);
      • 関数configTime()は非同期関数で、JavaScriptのPROMISと同様、関数の処理完了を待たずに次へ進みます。
      • 処理完了したら実行する関数(CallBack関数)をこの関数で指定します。

5,6行のSSIDとPasswordを設定し、スケッチを実行すると以下の様にシリアルモニタに表示されます。

Monitor

Connecting to xxxxxxxx ..... CONNECTED
Got time adjustment from NTP!
Tuesday, May 13 2025 05:25:31
Tuesday, May 13 2025 05:25:33
Tuesday, May 13 2025 05:25:38
Tuesday, May 13 2025 05:25:43
Tuesday, May 13 2025 05:25:48
Tuesday, May 13 2025 05:25:53

  • 1行: Connecting to xxxxxxxx ….. CONNECTED ー> WiFiに接続完了
  • 2行: Got time adjustment from NTP! ー> CallBack関数の実行。configTime()の処理完了。
  • 表示間隔を5秒に設定しているのですが、1回目と2回目間隔が2秒となっています。これは
    • configTime()関数の完了を待たずにloop()へ進む。
    • loop()では先ずdelay(5000);が実行される。
    • このWaitの間にconfigTime()関数が完了しCallBack関数timeavailable()により1回目(3行)が表示。
    • その後、delay(5000);が終了してloop()関数のprintLocalTime();によって2回目の時刻(4行)が表示。
  • される為です。また、この2秒はネットの状況によって違う値になると思います。
  • その後はloop()関数のprintLocalTime();による表示となるので、5秒間隔となります。

configTime()とgetLocalTime()の関係

時刻データを取得出来る様になりましたが、configTime()とgetLocalTime()の関係が良く分かりません。そこでサンプルスケッチを以下の様に変更してみました。変更内容は以下の通り。

  • configTime()を実行する前にgetLocalTime()を実行。
  • configTime()処理完了後にWiFi接続を無効にする。
  • その後loop()関数でgetLocalTime()を実行。
SimpleTime01.ino

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

const char *ssid = "YOUR_SSID";
const char *password = "YOUR_PASS";

const char *ntpServer1 = "pool.ntp.org";
const char *ntpServer2 = "time.nist.gov";
const long gmtOffset_sec = 9 * 3600;
const int daylightOffset_sec = 0;

void printLocalTime() {
  struct tm timeinfo;
  if (!getLocalTime(&timeinfo)) {
    Serial.println("No time available (yet)");
    return;
  }
  Serial.println(&timeinfo, "%A, %B %d %Y %H:%M:%S");
}

// Callback function (gets called when time adjusts via NTP)
void timeavailable(struct timeval *t) {
  Serial.println("Got time adjustment from NTP!");

  WiFi.disconnect();
  Serial.println("Disconnect WiFi");
  
  printLocalTime();
}

void setup() {
  Serial.begin(115200);

  printLocalTime();

  // First step is to configure WiFi STA and connect in order to get the current time and date.
  Serial.printf("Connecting to %s ", ssid);
  WiFi.begin(ssid, password);

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

  // set notification call-back function
  sntp_set_time_sync_notification_cb(timeavailable);

  configTime(gmtOffset_sec, daylightOffset_sec, ntpServer1, ntpServer2);

}

void loop() {
  delay(5000);
  printLocalTime();  // it will take some time to sync time :)
}
  • 35行: configTime()実行前にgetLocalTime()関数を実行
  • 26行: configTime()完了後にWiFi接続を無効。

実行結果は以下

Monitor

entry 0x400805cc
No time available (yet)
Connecting to xxxxxxx..... CONNECTED
Got time adjustment from NTP!
Disconnect WiFi
Tuesday, May 13 2025 06:40:14
Tuesday, May 13 2025 06:40:14
Tuesday, May 13 2025 06:40:19
Tuesday, May 13 2025 06:40:24
Tuesday, May 13 2025 06:40:29
Tuesday, May 13 2025 06:40:34

  • 2行: No time available (yet) ー> WiFi接続前にgetLocalTime()を実行した結果。時刻の取得出来ず
  • 3行: ここでWiFiに接続。
  • 4行: configTime()を実行して処理完了。
  • 5行: 処理完了後、WiFiを切断
  • 6行以降: NTPサーバとの接続は有りませんが、現在の時間は表示されます。

configTime()実行前にgetLocalTime()を実行すると時刻データは取得出来ませんが、configTime()を実行した後はNTPサーバとの接続を切ってもgetLocalTime()で時刻データを取得出来る事が分かります。この事からconfigTime()の動作は以下の様になっていると思います。

  • NETを使ってNTPサーバに接続
  • NTPサーバに設定用パラメータを送り初期化
  • その後NTPサーバから時刻データを取得
  • データ取得後そのデータを元にESP32のRTCを設定
  • その後はgetLocalTime()関数が自身のRTCから時刻データを取得(これにネットは必要無い)

esp_sntp.hについて

ESP32に内蔵されているRTCの精度は悪く、例えば1日使用すると誤差は数秒となる様です。多分これを考慮してconfigTime()を実行するとESP32は定期的にNTPサーバと交信し自身のRTCをUpDateされる様です。

NTPサーバとの接続関係の関数はesp_sntp.hに定義されていて、https://github.com/espressif/esp-idf/blob/master/components/lwip/include/apps/esp_sntp.hにソースが、https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/system/system_time.html#functionsに説明が有ります。

定義されている関数に

  1.  sntp_sync_status_t sntp_get_sync_status(void):  交信状態を確認
  2.  void sntp_set_sync_interval(uint32_t interval_ms): SNTP operation時間の設定
  3.  uint32_t sntp_get_sync_interval(void):       SNTP operation時間の読み取り
  4.  bool sntp_restart(void):             SNTPの再スタート.
  5.  void esp_sntp_stop(void):            SNTP serviceの停止.

が有ります。 これを元にスケッチを以下の様に変更しました。

SimpleTime02.ino

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

const char *ssid = "xxxxxx";
const char *password = "yyyyyy";

const char *ntpServer1 = "pool.ntp.org";
const char *ntpServer2 = "time.nist.gov";
const long gmtOffset_sec = 9 * 3600;
const int daylightOffset_sec = 0;

#include <ESP32Time.h>
ESP32Time rtc(gmtOffset_sec);

void printLocalTime() {
  struct tm timeinfo;
  if (!getLocalTime(&timeinfo)) {
    Serial.println("No time available (yet)");
    return;
  }
  Serial.println(&timeinfo, "%A, %B %d %Y %H:%M:%S");
}

void setup() {
  Serial.begin(115200);

  rtc.setTime(0, 0, 0, 1, 1, 2025);             // 1st Jan 2025 00:00:00
  
  Serial.print("Before WiFi Connect: ");
  printLocalTime();
  
  Serial.printf("Connecting to %s ", ssid);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println(" CONNECTED");

  configTime(gmtOffset_sec, daylightOffset_sec, ntpServer1, ntpServer2);
  while (sntp_get_sync_status() != SNTP_SYNC_STATUS_COMPLETED) ;
  
  Serial.print("After WiFi Connect: ");
  printLocalTime();
}

void loop() {
  int a;

    Serial.println("Default interval: " + String(sntp_get_sync_interval()));
    sntp_set_sync_interval(20 * 1000);
    sntp_restart();
    Serial.println("Set interval: " + String(sntp_get_sync_interval()));

    a = 0;
    Serial.println("Start"); 
    while(1){
      Serial.printf("a=%d, %d sec: ",a, a * 4); 
      printLocalTime();  // it will take some time to sync time :)
      delay(4000);
      a ++;
      if(a == 3) 
        rtc.setTime(0, 0, 0, 1, 1, 2025);             // 1st Jan 2025 00:00:00

      if(a == 9){
        esp_sntp_stop();
        rtc.setTime(0, 0, 0, 1, 1, 2025);             // 1st Jan 2025 00:00:00
      }
    }
}
  • 13,14,28行: 
    • そもそもgetLocalTime()とconfigTime()の関係は無くかつWiFiと関係は無く動作する事が分かりました。
    • 前回WiFi接続前にエラーが出たのはRTCの初期化を行っていなかった為です。
    • そこで今回はRTCを2025年1月1日00:00:00と初期化しました。
  • 31行: printLocalTime(); ー> ここでWiFi接続前にgetLocalTime()を実行。
  • 41行: configTime()を実行しNTPサーバに時刻データを要求
  • 42行: while (sntp_get_sync_status() != SNTP_SYNC_STATUS_COMPLETED) ;
    • sntp_get_sync_status()でNTPサーバの状態をモニタ。この関数の戻り値は以下の3つ
      • enumerator SNTP_SYNC_STATUS_RESET
      • enumerator SNTP_SYNC_STATUS_COMPLETED
      • enumerator SNTP_SYNC_STATUS_IN_PROGRESS
    • while (sntp_get_sync_status() != SNTP_SYNC_STATUS_COMPLETED) ;で処理の完了を待ちとなります
  • 45行: configTime()実行後、時刻データの表示
  • Loop関数では
    • 51行: sntp_get_sync_interval() → デフォルトの更新時間を取得。戻り値は更新時間で単位はmsec
    • 52行: 更新時間の設定。 2秒x1000(msec)
    • 53行: sntp_set_sync_interval()実行後はこの関数の実行が必要な様です。
    • 以降は4秒毎に時刻データを表示しますが、途中下記を行っています
      • 63行: 開始12秒後(a = 3)にRTCを2025年1月1日00:00:00に設定
      • 66行: 開始36秒後(a = 9)にNTPサーバの同期を止め(esp_sntp_stop())
             再びRTCを2025年1月1日00:00:00に設定

下記は実行結果。

Monitor

entry 0x400805cc
Before WiFi Connect: Wednesday, January 01 2025 00:00:00
Connecting to xxxxxx .. CONNECTED
After WiFi Connect: Wednesday, May 14 2025 13:49:32
Default interval: 10800000
Set interval: 20000
Start
a=0, 0 sec: Wednesday, May 14 2025 13:49:32
a=1, 4 sec: Wednesday, May 14 2025 13:49:36
a=2, 8 sec: Wednesday, May 14 2025 13:49:40
a=3, 12 sec: Wednesday, January 01 2025 00:00:00
a=4, 16 sec: Wednesday, January 01 2025 00:00:04
a=5, 20 sec: Wednesday, January 01 2025 00:00:08
a=6, 24 sec: Wednesday, May 14 2025 13:49:56
a=7, 28 sec: Wednesday, May 14 2025 13:50:00
a=8, 32 sec: Wednesday, May 14 2025 13:50:04
a=9, 36 sec: Wednesday, January 01 2025 00:00:00
a=10, 40 sec: Wednesday, January 01 2025 00:00:03
a=11, 44 sec: Wednesday, January 01 2025 00:00:07
a=12, 48 sec: Wednesday, January 01 2025 00:00:11
a=13, 52 sec: Wednesday, January 01 2025 00:00:15
a=14, 56 sec: Wednesday, January 01 2025 00:00:19

  • 2行: WiFi接続前のgetLocalTime()実行結果。今回はエラーにならず、設定時間を表示しています。
  • 4行: NTPサーバ接続後のgetLocalTime()実行結果。時刻が現行の時間になっています。
  • 5行: デフォルトの更新時間。10800000 / 60 /60 /1000 = 3。更新時間は3時間
  • 6行: 設定後の更新時間。HPに最短の更新時間は15秒と有りました。15秒より長い時間を指定します。
  • 8〜10行: 現行の時刻を表示。
  • 11〜13行: a = 3でRTCを2025年1月1日00:00:00に設定。それ以降は変更された時刻を表示
  • 14〜16行: 20秒間隔でRTCが更新される設定なので、ここで1回目の更新が行われる。
            時刻が現行に戻っています。
  • 17行: a = 9で再びRTCを2025年1月1日00:00:00に設定し今度は同期を停止(esp_sntp_stop();)
         していいます。
  • 以降: 同期がオンなら40秒後(18行)の表示が現行の時間になるのですが、今回は更新されていません。
        WiFiの接続を切らなくてもesp_sntp_stop()を実行するれば、同期が切れる様です。

最後に

configTime()関数を使うと時刻データが簡単に取得出来ると喜んでいたのですが意外と奥が深い事が分かりました。長時間稼働しかつ時間を管理するプログラムの場合、RTCの精度は重要です。その様な場合、自動でRTCを更新する機能はとても便利です。