12.picoでNTPサンプル動作 Arduino


12.picoでNTPサンプル動作 Arduino

pico + ESP8266のATコマンドで動作するシールド(Pico ESP8266 微雪電子)にてNTP(ntp.nict.jp)の時刻を受信します。
https://github.com/khoih-prog/ESP8266_AT_WebServer/blob/master/examples/UdpNTPClient/UdpNTPClient.ino
のスケッチを動作させます。今までのセットアップによってすぐに動作しました。協定世界時表示でしたので、日本時間に+9時間修正しました。

時刻

・NTP時刻 (Network Time Protocol)
 国立研究開発法人 情報通信研究機構 NICT
 NTPサーバ名: ntp.nict.jp
 サーバのパケットの送信時刻(UTC)
 https://jjy.nict.go.jp/tsp/PubNtp/index.html
 アクセス制限:1日480回(1H平均20回 3分に平均1回)

・UTC (協定世界時 Coordinated Universal Time)
 1900/1/1 00:00:00からの秒数

・GMT(グリニッジ標準時)
 UTCとほぼ同じ

・JST (日本標準時)
 JST = UTC+9
 UTCに9時間(=9x60x60=32400秒)加算(早い)
 例)UTC 15日13:56:00 = JST 15日22:56:00

・UNIX時間(コンピューターシステム上での時刻)
 1970/1/1 0:0:0からの秒数

暗号化

・WEP(Wired Equivalent Privacy)
 脆弱性がある旧方式(2002年中止)

・WPA(Wi-Fi Protected Access)
 Wi-Fi上で通信を暗号化して保護するための技術規格 2002/10発表規格

・WPA2
 WPAの後継。2004/9発表規格

・WPA3
 WPA2の後継。2018/6発表規格

NTPのパケットフォーマット

"06.Wio TerminalでWi-Fiから時刻取得"から再出

【図解】NTPのパケットフォーマットとパケットキャプチャ
https://milestone-of-se.nesuke.com/l7protocol/ntp/ntp-format/

RFC1305 Network Time Protocol (Ver3)の仕様、実装、および分析
https://datatracker.ietf.org/doc/html/rfc1305

RFC2030 IPv4、IPv6、およびOSI用のSimple Network Time Protocol (SNTP) Ver4
https://datatracker.ietf.org/doc/html/rfc2030

UDPセグメントのフォーマットは、
最初の16bit 送信元(Source)ポート=123
次の16bit 宛先(Destination)ポート=123
次の16bit セグメント長
次の16bit チェックサム
以降は、UDPデータです。

UDPデータ

* 行頭の数字はbyte(8bit)単位の数字です。●を選択しました。

1.1-2の2bit 閏秒指示子 (Leap Indicator)
その日の最後の1分が1秒追加(うるう秒=Leap Second)されるか、もしくは1秒削除されるかを事前に予告する。
00(0):予告なし(通常)
01(1):その日の最後の1分が61秒
10(2):その日の最後の1分が59秒
11(3):時刻同期無し ●

1.3-5の3bit バージョン番号 (Version Number)
100(4):現在のNTP及びSNTPバージョン ●

1.6-8の3bit Mode
アソシエーションのモード。NTP時刻情報の提供形態
000(0):予約
001(1):Symmetric Active
010(2):Symmetric Passive
011(3):Client ●
100(4):Server
101(5):Broadcast
110(6):NTP control message(制御クエリ)
111(7):プライベート利用に予約

2.の8bit 階層 (Stratum)
0:原子時計やGPSの時刻、NTPパケット上は取扱いません。●
1:原子時計やGPSに物理的に直結したNTPサーバ
2-15:それより1つ上のStratumのNTPサーバから時刻同期
16:時刻同期していない。
17-255:未使用

3.の8bit ポーリング間隔 (Poll)
次のNTPパケット送出までの最大間隔 例)デフォルト10は2^10=1024秒。

4.の8bit 精度 (Precision)
NTPを扱う機器のシステムクロックが扱える精度を相手に示します。Pollと同じく、例)2^(-18)=3.8uS

5-8.の32bit ルート遅延 (Root Delay)
32bit符号付固定小数点数 小数点がビット15と16の間
Strarum 1までの往復遅延(秒)。相対時間と周波数変位によって正負両方の値をとります。通常、-数mS~数百mS

9-12.の32bit ルート拡散 (Root Dispersion)
32bit符号無し固定小数点数で小数点がビット15と16の間
Strarum 1までの誤差(秒)。通常、0~数百mS

13-16.の32bit 参照識別子 (Reference ID)
どのNTPサーバを参照しているかを表しています。
Stratum 2~14のNTPサーバは、上位の NTPサーバのIPアドレスをセット。
Stratum 1のNTPサーバは、参照先を暗示する任意の1~4 文字のASCII文字列(GPS、WWVB等)

17-24.の64bit 参照時刻 (Reference Timestamp)
タイムスタンプフォーマット
最後に同期した時刻

25-32.の64bit 開始時刻 (Origin Timestamp)
タイムスタンプフォーマット
NTPサーバにリクエストを送信した時刻

33-40.の64bit 受信時刻 (Receive Timestamp)
タイムスタンプフォーマット
NTPサーバがリクエストを受信した時刻

41-48.の64bit 送信時刻 (Transmit Timestamp)
タイムスタンプフォーマット
NTPサーバのパケットの送信時刻。この時刻を受信して時刻表示します。

49.以下略

タイムスタンプフォーマット

"06.Wio TerminalでWi-Fiから時刻取得"から再出
64bit符号なし固定小数点。上位32bitが整数秒、下位32bitが小数点以下。
1900/1/1 0:0:0からの経過秒数。協定世界時(UTC:Universal Time, Coordinated)。日本時間はUTC+9H。スケッチでは上位32bitのみを使用します。

うるう年

"06.Wio TerminalでWi-Fiから時刻取得"から再出
2/29がある年です。西暦/4が割切れる年。しかし、100で割切れて、400で割切れない年は平年とします。よって、1900年は平年です。1900-1970年にうるう日は17日あります。+1秒します。
(1904,'08,'12,'16,'20,'24,'28,'32,'36,'40,'44,'48,'52,'56,'60,'64,'68)

ESP8266 Arduino Core の UDPクラス

https://arduino-esp8266.readthedocs.io/en/latest/esp8266wifi/udp-class.html?highlight=parsePacket#udp-class
を自動翻訳しました。

available

WiFiUDP.available(void);
バッファから読取りに使用できるバイト数(文字)を取得。これはすでに到着したデータです。WiFiUDP.parsePacket()の後でのみ正常に呼出すことができます。Streamユーティリティクラスを継承。
戻り値:現在のパケットで使用可能なバイト数,0=parsePacketがまだ呼び出されていない場合

begin

WiFiUDP.begin(port);
WiFiUDPライブラリとネットワーク設定を初期化。ローカルPORTでリッスンしているWiFiUDPソケットを開始。
port:リッスンするローカルポート(int)
戻り値:1=成功した場合, 0=使用できるソケットがない場合

beginPacket

WiFiUDP.beginPacket(hostName, port);
WiFiUDP.beginPacket(hostIp, port);
接続を開始して、UDPデータをリモート接続に書込みます。
hostName:リモートホストのアドレス。文字列またはIPアドレスを受け入れます
hostIp:リモート接続のIPアドレス(4バイト)
port:リモート接続のポート(int)
戻り値:1=成功した場合, 0:指定されたIPアドレスまたはポートに問題があった場合

endPacket

WiFiUDP.endPacket(void);
リモート接続にUDPデータを書込んだ後に呼出されます。パケットを終了して送信。
戻り値:1=パケットが正常に送信された場合, 0=エラーが発生した場合

flush

WiFiUDP.flush(void);
クライアントに書き込まれたがまだ読取られていないバイトはすべて破棄します。Streamユーティリティクラスから継承。
戻り値:なし

parsePacket

UDP.parsePacket(void);
次に利用可能な着信パケットの処理を開始し、UDPパケットの存在を確認し、サイズを報告。UDP.read()でバッファーを読取る前に、parsePacket()を呼び出す必要があります。
戻り値:バイト単位のパケットのサイズ, 0=使用可能なパケットがない場合

peek

WiFiUDP.peek(void);
次のバイトに進まずにファイルからバイトを読取ります。peek()を連続して呼出すと、次のread()の呼出しと同値が返されます。Streamクラスから継承。Streamクラスのメインページ参照。
戻り値:b=次のバイトまたは文字, -1=使用可能なものがない

read

WiFiUDP.read();
WiFiUDP.read(buffer, len);
指定バッファからUDPデータを読取ります。引数が指定されていない場合は、バッファ内の次の文字を返します。WiFiUDP.parsePacket()の後でのみ正常に呼出すことができます。
buffer:着信パケットを保持するためのバッファー(char *)
len:バッファの最大サイズ(int)
戻り値:b=バッファ内の文字(char)
size:バッファのサイズ
-1:使用可能なバッファがない場合

remoteIP

WiFiUDP.remoteIP(void);
リモート接続のIPアドレスを取得。WiFiUDP.parsePacket()の後に呼出す必要があります。
戻り値:4バイト 現在の着信パケットを送信したホストのIPアドレス

remotePort

UDP.remotePort(void);
リモートUDP接続のポートを取得。UDP.parsePacket()の後に呼出す必要があります。
戻り値:現在の着信パケットを送信したホストのポート

stop

WiFiUDP.stop(void);
サーバーから切断。UDPセッション中に使用されているリソースを解放。
戻り値:なし

write

WiFiUDP.write(byte);
WiFiUDP.write(buffer, size);
UDPデータをリモート接続に書込みます。beginPacket()とendPacket()の間にラップする必要があります。beginPacket()はデータのパケットを初期化し、endPacket()が呼び出されるまで送信されません。
byte:送信バイト
buffer:送信メッセージ
size:バッファのサイズ
戻り値:パケットへのシングルバイトバッファからパケットへのバイトサイズ

マルチキャストUDP

ESP8266に固有。ドキュメントはまだ。
beginMulticast (IPAddress interfaceAddr,IPAddress multicast,uint16_t port)
beginPacketMulticast(IPAddress multicastAddress,uint16_t port,IPAddress interfaceAddress,int ttl=1)
IPAddress destinationIP()
uint16_t localPort()
WiFiUDPクラスは、STAインターフェイスでのマルチキャストパケットの送受信をサポートしています。マルチキャストパケットを送信する場合は、udp.beginPacket(addr, port) を udp.beginPacketMulticast(addr, port, WiFi.localIP()) に置換えてください。マルチキャストパケットをリスニングする場合は、udp.begin(port)を udp.beginMulticast(WiFi.localIP(), multicast_ip_addr, port) に置換えます。udp.destinationIP() を使用すると、受信したパケットがマルチキャストアドレスに送信されたのか、ユニキャストアドレスに送信されたのかを知ることができます。

スケッチ

defines.h

"**********"は、自分のSSIDとそのパスワードを記入してください。
必要ない行は削除しました。

// defines.h
// ESP8266/ESP32 ATコマンド実行シールド用
// ESP8266_AT_WebServer は ESP8266/ESP32 AT-commandシールドがWebServerを動作させるためのライブラリ
// https://github.com/khoih-prog/ESP8266_AT_WebServer
// Esp8266WebServer.h

#ifndef defines_h                               // もし定義がされていなければ
#define defines_h                               // マクロ定義する
#define DEBUG_ESP8266_AT_WEBSERVER_PORT Serial  // 定義する
#define _ESP_AT_LOGLEVEL_  0                    // デバックレベル(0-4)
//#warning Using ESP8266-AT WiFi with ESP8266_AT_WebServer Library
#define SHIELD_TYPE  "ESP8266-AT & ESP8266_AT_WebServer Library" // 定義する

#ifdef CORE_TEENSY  // 違う
#elif (defined(ARDUINO_RASPBERRY_PI_PICO))  //
  //#warning RASPBERRY_PI_PICO board selected
  #define EspSerial  Serial1                // picoは1と2がある
#endif

#include <ESP8266_AT_WebServer.h>    // ESP8266のAT_WebServer
char ssid[] = "**********";          // 自分のSSID
char pass[] = "**********";          // そのパスワード
#endif                               // defines_h

UdpNTPClient.ino


// UdpNTPClient.ino 1日480回以上受信しない事
// ESP8266 ATコマンドシールド用簡易Arduinoウェブサーバサンプル
// ESP8266_AT_WebServerは ESP8266/ESP32 AT-commandシールド用のWebServerを実行するためのライブラリです
// https://github.com/khoih-prog/ESP8266_AT_WebServer/blob/master/examples/UdpNTPClient/UdpNTPClient.ino

// このシンプルで効果的な方法については、[Miguel Alexandre Wisintainer](https://github.com/tcpipchip)の
// クレジットを参照してください。
// いくつかのSTM32では、variant.hにSerialの定義しかなく、Serial/USBデバッグに使用されます。
// 例えば、Nucleo-144 F767ZIオリジナルのvariant.hでは、以下のようになります。
// #define SERIAL_PORT_MONITOR     Serial
// #define SERIAL_PORT_HARDWARE    Serial
//  ESP8266/ESP32-ATを使用するには、Serial1のような別のSerialが必要です。
//  これを行うには、まず、対応するvariant.hを以下のように修正します。
// #define SERIAL_PORT_HARDWARE    Serial1
//  とし、スケッチでD0=RX/D1=TXピンをHardware Serial1に割り当てます。
// #define EspSerial      SERIAL_PORT_HARDWARE    //Serial1
// HardwareSerial         Serial1(D0, D1);
//  ESPSerialをSerial1として使用したい各ボードのdefines.hにインクルードする必要があります。

#include "defines.h"           // 添付 定義ファイル ssid,BOARD_TYPEなど
#include "ESP8266_AT_Udp.h"    // ESP8266ATコマンド用UDP
int status = WL_IDLE_STATUS;   // WiFi.begin()が呼び出された時に割当てられる一時的ステータス
//char timeServer[]     = "time.nist.gov"; //NTP server
char timeServer[]         = "ntp.nict.jp"; // 情報通信研究機構のNTPサーバー
unsigned int localPort    = 2390;   // UDPパケットを受信するローカルポート(ESP8266側)
const int NTP_PACKET_SIZE = 48;     // NTPタイムスタンプは 最初の48バイトに含まれる
const int UDP_TIMEOUT     = 2000;   // UDPパケットの到着タイムアウト(mS)
byte packetBuffer[NTP_PACKET_SIZE]; // 入出力パケットを保持するバッファ
ESP8266_AT_UDP Udp;                 // UDPでパケットを送受信するためのUDPインスタンス

void sendNTPpacket(char *ntpSrv) {  // 指定タイムサーバーにNTPリクエストを送信する関数
  memset(packetBuffer, 0, NTP_PACKET_SIZE);//バッファの全byteを0 (メモリのポインタ,セット値,セットサイズ)
  //NTPリクエストの形成に必要な値を初期化(パケットの詳細は上記URL参照)
  packetBuffer[0] = 0b11100011; // LI=0b11(時刻同期無し),Ver=0b100(現在),Mode=0b011(Client)
  packetBuffer[1] = 0;      // 階層=0(取扱わず)
  packetBuffer[2] = 6;      // パケット送出最大間隔 2^6=64秒
  packetBuffer[3] = 0xEC;   // 精度 符号付EC(16)=-20(10) 2^-20=0.95uS
  // ルート遅延[4]-[7]とルート拡散[8]-[11]の計8byte(64bit)は0
  packetBuffer[12] = 49;    //参照識別子32bit [49,78,40,52]?
  packetBuffer[13] = 0x4E;  //
  packetBuffer[14] = 49;    //
  packetBuffer[15] = 52;    // これで、全NTPフィールドを設定完了
  Udp.beginPacket(ntpSrv, 123); // 接続開始し UDPデータをリモート接続に書込む。NTPのリクエストは123番ポート
  Udp.write(packetBuffer, NTP_PACKET_SIZE); // UDP送信データをリモート接続に書込む
  Udp.endPacket();                          // パケットを終了して送信
}

void setup() {
  Serial.begin(115200);               // シリアルモニタ初期化
  while (!Serial && millis() < 5000); // モニタを開いていなくて、ボード起動後5秒以内なら待つ
  Serial.print(F("\nUdpNTPClient.ino起動\nボード名= ")); // 表示
  Serial.println(BOARD_NAME);                     // "RASPBERRY_PI_PICO"と表示
  Serial.print(F("シールドtype= "));               // 表示
  Serial.println(SHIELD_TYPE); // "ESP8266-AT & ESP8266_AT_WebServer Library"と表示
  Serial.println(ESP8266_AT_WEBSERVER_VERSION);   // "ESP8266_AT_WebServer v1.5.4"と表示
  EspSerial.begin(115200);                        // ESPモジュールのシリアル初期化 
  WiFi.init(&EspSerial);  // ESPモジュールの初期化 "[ESP_AT] Use ES8266-AT Command"と表示 
  Serial.println(F("WiFiシールド初期化済"));        // 表示
  if (WiFi.status() == WL_NO_SHIELD) {            // wifi状態でシールドが無ければ
    Serial.println(F("WiFiシールドがありません"));   // 表示
    while (true);                                 // 永久ループ
  }
  while (status != WL_CONNECTED) {                // WiFiに接続して無ければ
    Serial.print(F("WPAで接続のSSID= "));          // 表示
    Serial.println(ssid);                         // ssid表示
    status = WiFi.begin(ssid, pass);              // 暗号化ネットワークに接続
  }
  Serial.print(F("接続済ESP8266 IP= "));           // 表示
  Serial.println(WiFi.localIP());                 // ESP8266のIP表示
  Udp.begin(localPort);                           // WiFiUDPライブラリとネット設定を初期化
}

void loop() {
  sendNTPpacket(timeServer); //タイムサーバーにNTPパケットを送信する関数へ ここのみ
  unsigned long startMs = millis();      // 現在時間をセット
  while (!Udp.available() && (millis() - startMs) < UDP_TIMEOUT) {}//データが未到着で 時間内なら待つ
  int packetSize = Udp.parsePacket();    // 次に利用可能な処理を開始しその着信パケットサイズ
  if (packetSize) {                      // データがあれば
    Serial.print(F("UDP受信のパケットサイズ= ")); // 表示
    Serial.println(packetSize);          // "48"と表示
    Serial.print(F("接続先IP= "));        // 表示
    IPAddress remoteIp = Udp.remoteIP(); // リモートIP読込み
    Serial.print(remoteIp);              // リモートIP表示
    Serial.print(F(", port= "));         // 表示
    Serial.println(Udp.remotePort());    // リモートポート表示
    Udp.read(packetBuffer, NTP_PACKET_SIZE); //パケットを受信したので、そのデータをバッファに読込む
  
    unsigned long highWord = word(packetBuffer[40], packetBuffer[41]); // 送信時刻上位16bit
    unsigned long lowWord = word(packetBuffer[42], packetBuffer[43]);  // 送信時刻下位16bit
    // [40]-[47]の64bitが送信時刻で、そのうち[40]-[43]が整数秒 [44]-[47]が小数点以下
    unsigned long secsSince1900 = highWord << 16 | lowWord;//2ワードを組合わせて長整数。NTP時間
    Serial.print(F("NTP時間 (1900/1/1からの秒数)= ")); // 表示
    Serial.println(secsSince1900);                   // NTP時間(秒)
    Serial.print(F("Unix時間(1970/1/1からの秒数)= ")); // 表示
    const unsigned long seventyYears = 2208988800UL; // 70年=(70x365+閏日17)x24x60x60秒
    unsigned long JSTtime = secsSince1900 - seventyYears + 32400; // -70年+日本時間9H
    Serial.println(JSTtime);                // JST(秒)を表示
    Serial.print(F("JST時刻= "));            // 表示
    Serial.print((JSTtime % 86400L) / 3600);// 1日以上を削除し時刻表示(86400s=60x60x24=1日,3600s=1H)
    Serial.print(F(":"));                   // 表示
    if (((JSTtime % 3600) / 60) < 10) {     // 1H以上を削除し 0-9分の時
      Serial.print(F("0"));                 // 分の10の位に"0"を表示
    }
    Serial.print((JSTtime % 3600) / 60);    // 1H以上を削除し 分を表示 (3600s=1H)
    Serial.print(F(":"));                   // 表示
    if ((JSTtime % 60) < 10) {              // 1分以上を削除し 秒が0-9の時
      Serial.print(F("0"));                 // 秒の10の位に"0"を表示
    }
    Serial.println(JSTtime % 60);           // 秒を表示
    Serial.println();                       // 改行
  }
  delay(60000);  // 1分待つ 注意)サーバーのアクセス制限有り1日480回(=1H20回=3分に1回)
}
フラッシュメモリ(1Mbyte)のうち、スケッチが5%使用。
RAM(262kbyte)のうち、グローバル変数が2%使用、ローカル変数で254kbyte使用可能。(1000byte=1kで計算)