DOME's diary

雑多なブログです。主に理系の話を書きます。

ESP32 (Arduino IDE)でNTPサーバーから情報を取得する

サークル”オタク語り”アドベントカレンダー2023 12/8
※コードは適当なので参考までに

こんにちは。
趣味の電子工作でESP32を用いてNTPと通信し時刻合わせをする時計を製作したので、NTPサーバーとの通信に関して覚え書き的にブログを書こうと思います。
ESP32 (Arduino IDE)においては、configTime()関数*1がありますが、今回RTCへの書き込み(秒が変わるピッタリのタイミングで書き込みする必要がある)や、うるう秒情報の取得を行いたかったので、パケット情報を全て取得することに挑戦しました。

NTPについて

NTPについての詳細な解説は他の詳しい人に任せて、ざっくりと説明します。
NTPは機器の時刻を合わせるためのプロトコルです。NTPサーバーにアクセスすると、今の正確な時刻を教えてくれます。実際には通信に時間が少しかかりますので、機器からサーバーの通信とサーバーから機器の通信にかかる時間が同じとして補正を行います。

NTPのパケットについて

NTPでは、サーバーにパケット(データ)を送信することでリクエストを行い、同様のパケットを受信します。
送受されるパケットはRFC5905*2で下記のように定義されています。

NTPパケットの内容

NTPのパケットフォーマット
LI:うるう秒表示 2bit整数
 0:警告なし
 1:次の9:00(日本時間)に+1秒のうるう秒
 2:次の9:00(日本時間)に-1秒のうるう秒
 3:不明
VN:バージョン 3bit整数
 今はver. 4
Mode:モード 3bit整数
 0:予約
 1:対称アクティブ
 2:対称パッシブ
 3:クライアント
 4:サーバー
 5:ブロードキャスト
 6:NTPコントロールメッセージ
 7:私的利用のため予約
Stratum:Stratum 8bit整数
 数字が小さいほど階層が高く(基準の時計に近く)なります。
Poll:ポーリング間隔 8bit符号付き整数、log2(x)
 NTPリクエストする間隔。底を2とした対数が取られています。
Precision:精度 8bit符号付き整数、log2(x)
 時刻の精度を表します。底を2とした対数が取られています。
Root Delay:ルート遅延 32bit固定小数点
 基準クロックからの通信に要する合計時間
Root Dispersion:ルート分散 32bit固定小数点
 基準クロックからの通信時間の合計分散
Reference Timestamp:参照時間 64bit固定小数点
 サーバーの時刻を最後に同期した時刻
Origin Timestamp:起点時間 64bit固定小数点
 クライアントがパケットを送信した時間
Receive Timestamp:受信時間 64bit固定小数点
 サーバーがパケットを受信した時間
Transmit Timestamp:送信時間 64bit固定小数点
 サーバーがパケットを送信した時間

実際に通信してみた

今回の仕様

今回はNICTが管理するNTPサーバー(Stratum1)のntp.nict.jpにリクエストを送信することにします。
またNTPのモードに関して、NTPサーバーは互いを参照しあい時刻を同期するといった機能があったりしますが、今回は時刻を取得するのみで、ESP32から同期用時刻情報を送信することはないので、Server / Clientモードを使います。 つまり、Mode3の空パケットをリクエストとして送信します。 NTPサーバーのUDPにおけるポート番号は基本123ですので、そのように設定します。

コード(Arduino IDEで書き込み)

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


//WiFi
char WiFissid[32] = "My_SSID";  // enter your WiFi SSID
char WiFipass[32] = "My_pass";  // enter your WiFi password

WiFiUDP Udp;                                           // A UDP instance to let us send and receive packets over UDP
byte mac[6] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };  //write default MAC address
unsigned int localPort = 8888;                         // local port to listen for UDP packets
const char NTPServer[] = "ntp.nict.jp";                // NTP server
const int NTP_PACKET_SIZE = 48;                        // NTP time stamp is in the first 48 bytes of the message
byte packetBuffer[NTP_PACKET_SIZE];                    //buffer to hold incoming and outgoing packets
void connectWiFi() {
  Serial.println("WiFi Connecting...");
  WiFi.mode(WIFI_STA);
  WiFi.begin(WiFissid, WiFipass); //WiFi begin try
  unsigned int startTime = millis();
  while (1) {
    if (WiFi.status() == WL_CONNECTED) {
      Serial.println("WiFi Connected");
      WiFi.macAddress(mac);
      break;
    }
    if (millis() - startTime >= 5000) {
      Serial.println("WiFi connection timed out");
      break;
    }
    delay(1);
  }
}

//NTP
bool NTP() {
  // Initialize values needed to form NTP request
  memset(packetBuffer, 0, NTP_PACKET_SIZE);  // set all bytes in the buffer to 0
  packetBuffer[0] = 0b00100011;              // LI, Version, Mode


  Udp.begin(localPort);
  Udp.beginPacket(NTPServer, 123);  // NTP requests are to port 123
  unsigned long time_send = millis();
  Udp.write(packetBuffer, NTP_PACKET_SIZE); //send request
  Udp.endPacket();
  while (Udp.parsePacket() == 0) {
    if (millis() - time_send >= 5000) {
      return 0;
    }
  }
  Udp.read(packetBuffer, NTP_PACKET_SIZE);  // read the packet into the buffer
  // the timestamp starts at byte 40 of the received packet
  // this is NTP time (seconds since Jan 1 1900):
  Serial.print("LI:");
  Serial.println(packetBuffer[0] >> 6);
  Serial.print("VN:");
  Serial.println((packetBuffer[0] >> 3) & 0x07);
  Serial.print("Mode:");
  Serial.println(packetBuffer[0] & 0x07);
  Serial.print("Stratum:");
  Serial.println(packetBuffer[1]);
  Serial.print("Poll:");
  Serial.println(pow(2, (int8_t)packetBuffer[2]));
  Serial.print("Precision:");
  Serial.println(pow(2, (int8_t)packetBuffer[3]));
  Serial.print("Root Delay:");
  Serial.println(FixedPoint32toFloat(&packetBuffer[4]));
  Serial.print("Root Dispersion:");
  Serial.println(FixedPoint32toFloat(&packetBuffer[8]));


  Serial.print("Reference Timestamp:");
  Serial.println(FixedPoint64toFloat(&packetBuffer[16]));
  Serial.print("Origin Timestamp:");
  Serial.println(FixedPoint64toFloat(&packetBuffer[24]));
  Serial.print("Receive Timestamp:");
  Serial.println(FixedPoint64toFloat(&packetBuffer[32]));
  Serial.print("Transmit Timestamp:");
  Serial.println(FixedPoint64toFloat(&packetBuffer[40]));
  return 1;
}

//type conversion
double FixedPoint64toFloat(byte *b0) {
  // combine the four bytes (two words) into a long integer
  unsigned long integer = word(b0[0], b0[1]) << 16 | word(b0[2], b0[3]);
  unsigned long decimal = word(b0[4], b0[5]) << 16 | word(b0[6], b0[7]);
  double retval = integer + (double)decimal / (pow(2, 32));
  return retval;
}
double FixedPoint32toFloat(byte *b0) {
  // combine the four bytes (two words) into a long integer
  unsigned long integer = word(b0[0], b0[1]);
  unsigned long decimal = word(b0[2], b0[3]);
  double retval = integer + (double)decimal / (pow(2, 16));
  return retval;
}


void setup() {
  Serial.begin(115200);
  connectWiFi();
}
void loop() {
  NTP();
  delay(1000*60*5);
}

実行結果(Serial Monitor)

LI:0
VN:4
Mode:4
Stratum:1
Poll:1.00
Precision:0.00
Root Delay:0.00
Root Dispersion:0.00
Reference Timestamp:3910875529.00
Origin Timestamp:0.00
Receive Timestamp:3910875529.16
Transmit Timestamp:3910875529.16

上記のように、時刻を取得することができました。(時刻を変換すれば、僕がいつこのブログを書いたか分かります)

おわりに

今回はESP32でNTPサーバーから時刻を取得する方法について、共有させていただきました。プログラミングに関しては素人なので、指摘があればいただけるとありがたかったりします。
ところで、記事をほったらかしにしている間に、なんとうるう秒の廃止が決定してしまいました。 www.gizmodo.jp
今回NTPパケット取得から始めたモチベーションが時計へのうるう秒の実装なので、なんとも言えない気分です。 まぁ、サーバーとかを直で叩く方法とかについてだいぶ詳しくなれたので良しとしましょう。