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ライブラリを使います。 GitHub - arduino-libraries/NTPClient: Connect to a NTP server ライブラリマネージャーなどからインストールします。私の場合は NTPClient.h と NTPClient.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.cpp ファイルに DEBUG_NTPClient のマクロ定義を追加すれば、NTPサーバーにアクセスされているかどうかデバッグできます。
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が使われているようです。大丈夫なのかな?^^;
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
NTPサーバーの仕組み
最後に、NTPサーバーの仕組みについて少し触れておきます。 NTPとは「Network Time Protocol」の略です。HTTPやTCPのように通信プロトコル、つまり約束事や取り決めのことですね。NTPサーバーでは原子時計を元に正確な時間を取得してます。端末からNTPサーバーに接続して現在時刻を取得するわけですが、ここで問題があります。ネットワーク接続による遅延の問題ですです。 ネットワーク環境によっては遅延が生じますので、いくらNTPサーバーが正確な時刻を刻んでいても、手元に届いた時は時間がずれている可能性があります。実は先ほどの「Network Time Protocol」と呼ばれる取り決めごとには、ネットワークの遅延も計測して補正しているんです。また、サーバー自身の処理時間も引き算したりと、よく考えられて実現されているんですね。ですから現在時刻をある程度正確に取得できるというわけですが、誤差がまったく無いわけではありません。そこはインターネット社会の宿命でしょうか。