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

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

みなさん、マイコンボードでの現在時刻の取得ってどのようになさってますでしょうか?

▼ Arduino UNOなどのインターネットに繋がらないデバイスでしたら、リアルタイムクロック(水晶発振器内蔵)を使った時間管理を行うと思います。

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

開発環境

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

項目
Arduino ESP32-DevKitC
IDE Platform IO
ファームウェア arduino-esp32#2.0.2
NTPClient 3.2.1

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

下記ページで公開されているNTPClientライブラリを使います。

ライブラリマネージャーなどからインストールしておきます。私の場合は NTPClient.hNTPClient.cpp ファイルを直接プロジェクト配下に置きました。

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

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

#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() という関数が実装されてまして、年月日も取得できるんですよね。ただそのライブラリ入手先が不明になってしまったので、先ほどのプログラムに関数を追加する形で「西暦-月-日 時:分:秒」のフォーマットで表現できるようにしてみたいと思います。

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

#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」と呼ばれる取り決めごとには、ネットワークの遅延も計測して補正しているんです。また、サーバー自身の処理時間も引き算したりと、よく考えられて実現されているんですね。ですから現在時刻をある程度正確に取得できるというわけですが、誤差がまったく無いわけではありません。そこはインターネット社会の宿命でしょうか。

「キッチンノート.fun」という料理サイトを立ち上げました!このサイトで紹介していた料理記事は、そちらへ移動しました。
記事に関するご質問などがあれば、
Twitter または お問い合わせ までご連絡ください。
関連記事