ESP32でNTPClientライブラリを使って現在時刻を取得する方法

ESP32でNTPClientライブラリを使って現在時刻を取得する方法
ESP32でNTPClientライブラリを使って現在時刻を取得する方法

みなさん、マイコンボードでの現在時刻の取得ってどのようになさってますでしょうか? ▼ Arduino UNOなどのインターネットに繋がらないデバイスでしたら、リアルタイムクロック(水晶発振器内蔵)を使って時間管理します。

ESP32を使う場合ならWiFiでインターネット接続して、NTPサーバーで時刻合わせが便利でしょう。ここではNTPClientライブラリを使ってESP32で現在時刻を取得する方法をご紹介いたします。また、NTPClientライブラリではできない、UNIXエポックタイムから 2022-10-31T19:00:48Z のような時間フォーマットに変換するプログラムもご紹介いたします。

開発環境

この記事の執筆時では、次の開発環境でプログラムを実行テストしました。

項目
ArduinoESP32-DevKitC
IDEPlatform IO
ファームウェアarduino-esp32#2.0.2
NTPClient3.2.1

NTPClientライブラリのインストール

下記ページで公開されているNTPClientライブラリを使います。 GitHub - arduino-libraries/NTPClient: Connect to a NTP server ライブラリマネージャーなどからインストールします。私の場合は NTPClient.hNTPClient.cpp ファイルを直接プロジェクト配下に置きました。

ESP32でNTPClientライブラリを使って現在時刻を取得する

それではさっそく、NTPClientを使って現在時刻を取得してみましょう。次のソースコードは、ESP32でNTPClientライブラリを使って現在時刻を取得するプログラムです。

cpp
#include <WiFi.h>
#include <WiFiUdp.h>

#include "Arduino.h"
#include "NTPClient.h"

#ifndef WIFI_SSID
#define WIFI_SSID "xxxxx"  // WiFi SSID (2.4GHz only)
#endif

#ifndef WIFI_PASSWORD
#define WIFI_PASSWORD "xxxxx"  // WiFiパスワード
#endif

WiFiUDP udp;
NTPClient ntp(udp, "ntp.nict.jp", 32400,
              60000);  // udp, ServerName, timeOffset, updateInterval

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

    WiFi.mode(WIFI_STA);
    delay(500);
    WiFi.begin(WIFI_SSID, WIFI_PASSWORD);

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

void loop() {
    ntp.update();

    unsigned long epochTime = ntp.getEpochTime();
    String formattedTime = ntp.getFormattedTime();  // hh:mm:ss

    Serial.print(epochTime);
    Serial.print("  ");
    Serial.println(formattedTime);

    delay(1000);
}

端末で実行すると次のようにシリアルモニタに表示されるはずです。

1667244750  19:32:30
1667244751  19:32:31
1667244752  19:32:32
1667244753  19:32:33
1667244754  19:32:34
1667244755  19:32:35
NTPClient ntp(udp, "ntp.nict.jp", 32400, 60000); では、UDPクライアントを突っ込み、NTPサーバーを日本の ntp.nict.jp に指定、日本の時差32400秒(9時間 x 3600秒)でオフセット指定、最後にNTPサーバーへ接続して時間を修正する更新頻度(60秒に1回)を指定してます。メインループ loop() 内で必ず ntp.update() ハンドラを回します。

次の画像のように NTPClient.cpp ファイルに DEBUG_NTPClient のマクロ定義を追加すれば、NTPサーバーにアクセスされているかどうかデバッグできます。

NTPClient.cpp
NTPClient.cpp

NTPサーバーへ接続できれば、 getEpochTime() で正確なエポック秒(UNIX時間)を取得できます。エポック秒とはご存じの通り、1970年1月1日午前0時0分0秒を0秒とした経過秒数ですよね。C言語のtime_t型など32ビット定義だと、西暦2038年1月19日3時14分7秒(UTC)を過ぎると整数型が飽和して、正しい時刻が表示されなくなる問題が懸念されてます(2038年問題)。ESP32も32ビットなので、気になるところです。 ESP32 time_t型を追ってみると、どうもlongが使われているようです。大丈夫なのかな?^^;

time_t型
time_t型

NTPClientのエポック秒では unsigned long ですから、最大値が 4294967295 ということで2106年までは大丈夫そうです。

さて getFormattedTime() では「時:分:秒」を取得します。

現在時刻を「西暦-月-日 時:分:秒」のフォーマットで表現したい

さて、先ほどのプログラムでは年月日が表現されてませんでした。実はNTPClientライブラリ(3.2.1)には年月を取得できる関数が実装されていません。ですから関数を作って自力で実装する必要があります。 日は getDay() で取得できますね。なぜ年と月も取得できるようにしてくれないのでしょうね?手元にあるNTPClientライブラリの別バージョンでは getFormattedDate() という関数が実装されてまして、年月日も取得できるんですよね。ただそのライブラリ入手先が不明になってしまったので、先ほどのプログラムに関数を追加する形で「西暦-月-日 時:分:秒」のフォーマットで表現してみます。

次のソースコードは、エポック秒を元に現在時刻を「西暦-月-日 時:分:秒」のフォーマットで表現するプログラムです。

cpp
#include <WiFi.h>
#include <WiFiUdp.h>

#include "Arduino.h"
#include "NTPClient.h"

#ifndef WIFI_SSID
#define WIFI_SSID "xxxxx"  // WiFi SSID (2.4GHz only)
#endif

#ifndef WIFI_PASSWORD
#define WIFI_PASSWORD "xxxxx"  // WiFiパスワード
#endif

#define LEAP_YEAR(Y) ((Y > 0) && !(Y % 4) && ((Y % 100) || !(Y % 400)))

WiFiUDP udp;
NTPClient ntp(udp, "ntp.nict.jp", 32400,
              60000);  // udp, ServerName, timeOffset, updateInterval

String getFormattedTime(unsigned long secs) {
    unsigned long rawTime = secs;
    unsigned long hours = (rawTime % 86400L) / 3600;
    String hoursStr = hours < 10 ? "0" + String(hours) : String(hours);

    unsigned long minutes = (rawTime % 3600) / 60;
    String minuteStr = minutes < 10 ? "0" + String(minutes) : String(minutes);

    unsigned long seconds = rawTime % 60;
    String secondStr = seconds < 10 ? "0" + String(seconds) : String(seconds);

    return hoursStr + ":" + minuteStr + ":" + secondStr;
}

/**
 * @brief エポックタイムを元に現在時刻を整形して返す(ex: 2022-10-31T19:00:48Z)
 *
 */
String getFormattedDate(unsigned long secs) {
    unsigned long rawTime = secs / 86400L;  // in days
    unsigned long days = 0, year = 1970;
    uint8_t month;
    static const uint8_t monthDays[] = {31, 28, 31, 30, 31, 30,
                                        31, 31, 30, 31, 30, 31};

    while ((days += (LEAP_YEAR(year) ? 366 : 365)) <= rawTime) year++;
    rawTime -=
        days - (LEAP_YEAR(year)
                    ? 366
                    : 365);  // now it is days in this year, starting at 0
    days = 0;
    for (month = 0; month < 12; month++) {
        uint8_t monthLength;
        if (month == 1) {  // february
            monthLength = LEAP_YEAR(year) ? 29 : 28;
        } else {
            monthLength = monthDays[month];
        }
        if (rawTime < monthLength) break;
        rawTime -= monthLength;
    }
    String monthStr =
        ++month < 10 ? "0" + String(month) : String(month);  // jan is month 1
    String dayStr = ++rawTime < 10 ? "0" + String(rawTime)
                                   : String(rawTime);  // day of month
    return String(year) + "-" + monthStr + "-" + dayStr + "T" +
           getFormattedTime(secs) + "Z";
}

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

    WiFi.mode(WIFI_STA);
    delay(500);
    WiFi.begin(WIFI_SSID, WIFI_PASSWORD);

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

void loop() {
    ntp.update();

    unsigned long epochTime = ntp.getEpochTime();
    String formattedDate = getFormattedDate(epochTime);

    Serial.print(epochTime);
    Serial.print("  ");
    Serial.println(formattedDate);

    delay(1000);
}

コピペして流用した関数ですので関数の中身は追えてませんが、次のようにキレイに年月日も表現してくれます。他のフォーマットに変えるのもそれほど難しくはないでしょう。

Update from NTP Server
1667246120  2022-10-31T19:55:20Z
1667246121  2022-10-31T19:55:21Z
1667246122  2022-10-31T19:55:22Z
1667246123  2022-10-31T19:55:23Z
1667246124  2022-10-31T19:55:24Z
LEAP_YEAR はうるう年を考慮した日付を出すマクロ関数です。うるう年の計算式って意外とシンプルなんですねー。

NTPサーバーの仕組み

最後に、NTPサーバーの仕組みについて少し触れておきます。 NTPとは「Network Time Protocol」の略です。HTTPやTCPのように通信プロトコル、つまり約束事や取り決めのことですね。NTPサーバーでは原子時計を元に正確な時間を取得してます。端末からNTPサーバーに接続して現在時刻を取得するわけですが、ここで問題があります。ネットワーク接続による遅延の問題ですです。 ネットワーク環境によっては遅延が生じますので、いくらNTPサーバーが正確な時刻を刻んでいても、手元に届いた時は時間がずれている可能性があります。実は先ほどの「Network Time Protocol」と呼ばれる取り決めごとには、ネットワークの遅延も計測して補正しているんです。また、サーバー自身の処理時間も引き算したりと、よく考えられて実現されているんですね。ですから現在時刻をある程度正確に取得できるというわけですが、誤差がまったく無いわけではありません。そこはインターネット社会の宿命でしょうか。

関連記事

最後までご覧いただきありがとうございます!

▼ 記事に関するご質問やお仕事のご相談は以下よりお願いいたします。
お問い合わせフォーム

ESP32搭載ボード
ESP32の書籍
Arduinoで人気の周辺パーツ
M5Stackのオススメ参考書
M5Stack製品
M5StickCで使えるHat
ESP32ボード
関連記事