SPRESENSE Wi-Fi Add-onボード iS110BでNTP時刻取得する

Qiita Advent calendar2021の記事です。

:::note
Spresenseの関係会社に勤務していますが、業務外の個人活動の紹介です。
当記事は会社と関係ありません。
:::

:::note alert
誤った基板上のピン接続により故障する可能性があります。
自己責任でお願いします。
:::

Wi-Fi Add-onボード iS110BでUDP通信するサンプルを自作できたので、NPT時刻取得を試してみました。

###事前準備
いきなり外のNTPサーバにつなげるのは怖かったので、自分のPCでNTPサーバの機能を動くようにしました。
こちらを参考にしました。

http://hnet.jp/ntp/

###スケッチの作成
12/15に公開したUDP通信用のサンプルをさらに改造しました。

https://qiita.com/nakmura/items/c522e76a5f95225867b2

ここでNTPの時刻取得のためのパケットはこちらを参考にしました。
http://arms22.blog91.fc2.com/blog-entry-445.html

<details><summary> AppFunc.cppの変更箇所だけ抜粋 </summary><div>

``` 
/*-------------------------------------------------------------------------*
   Constants:
  -------------------------------------------------------------------------*/
// RTC.hを追加
#include <RTC.h>
...
/*---------------------------------------------------------------------------*
   App_UDPClient_Test
  ---------------------------------------------------------------------------*/
// 部分的に追加・修正
void App_UDPClient_Test(void)
{
  ATCMD_RESP_E resp;
//  char server_cid = 0;
  uint8_t server_cid = 0;
  bool served = false;
//  ATCMD_NetworkStatus networkStatus;
  uint32_t timer = 0;

  AtCmd_Init();
    
  App_InitModule();
  App_ConnectAP();

  while (1) 
  {
    if (!served) 
    {
      ATCMD_NetworkStatus networkStatus;
      resp = ATCMD_RESP_UNMATCH;

      ConsoleLog( "Start UDP Client");
      resp = AtCmd_NCUDP( (char *)UDPSRVR_IP, (char *)UDPSRVR_PORT, (char *)LocalPort, &server_cid);   // Create UDP Client; AT+NCUDP=<Dest-Address>,<Port>[<,Src-Port>]
      ConsolePrintf( "server_cid: %d \r\n", server_cid);
      
      AtCmd_CID();
      
      if (resp != ATCMD_RESP_OK) 
      {
        ConsoleLog( "No Connect!" );
        delay(2000);
        continue;
      }
      if (server_cid == ATCMD_INVALID_CID) 
      {
        ConsoleLog( "No CID!" );
        delay(2000);
        continue;
      }

      do 
      {
        resp = AtCmd_NSTAT(&networkStatus);         // AT+NSTAT=?
      } while (ATCMD_RESP_OK != resp);

      ConsoleLog( "Connected" );
      ConsolePrintf("IP: %d.%d.%d.%d\r\n\r\n",
                    networkStatus.addr.ipv4[0], networkStatus.addr.ipv4[1], networkStatus.addr.ipv4[2], networkStatus.addr.ipv4[3]);

      AtCmd_Time();

      served = true;
    }
    else 
    {
      ConsoleLog( "Start to send UDP Data");
      // Prepare for the next chunck of incoming data
      WiFi_InitESCBuffer();

      // Start the infinite loop to send the data

      // http://arms22.blog91.fc2.com/blog-entry-445.html
      // set all bytes in the buffer to 0
      memset(UDP_Data, 0x00, NTP_PACKET_SIZE);
      // Initialize values needed to form NTP request
      // (see URL above for details on the packets)
      UDP_Data[0] = B11100011; //0x1b; // ; // 00(no announce) + 100(version4) + 011(client) // 0b11100011 LI, Version, Mode
      UDP_Data[1] = 0; //0x05; // not sync // Stratum, or type of clock  // 0
      UDP_Data[2] = 6; // 2^6 = 64 sec // 6 Polling Interval 2^6 = 64sec
      UDP_Data[3] = 0xEC; // 0xE9;       // 0xEC Peer Clock Precision 256 - EC = -20 -> 2^-20 ~ 0.9us
      // 8 bytes of zero for Root Delay & Root Dispersion CP_Data[4] ~ UDP_Data[11] = 0x00
      //
      UDP_Data[12]  = 49;       // Reference ID 49 4E 49 52
      UDP_Data[13]  = 0x4E;
      UDP_Data[14]  = 49;
      UDP_Data[15]  = 52;
      
//      for (int i = 0; i < NTP_PACKET_SIZE; i++)
//      {   
//        ConsolePrintf("%02x ", UDP_Data[i]);                   
//      }
      ConsolePrintf("\r\n");
              
      while ( 1 ) 
      {
//        ConsolePrintf( "send at cid: %d \r\n", server_cid);
        
        AtCmd_SendBulkData(server_cid, UDP_Data, NTP_PACKET_SIZE);

//        ConsolePrintf( "before GetGPIO37\r\n");

        resp = AtCmd_RecvResponse();  // Description: Wait for a response after sending a command. Keep parsing the data until a response is found.

//        ConsolePrintf( "response after sending a command: %d vs ref: %d\r\n", resp, ATCMD_RESP_BULK_DATA_RX);
        
//        ConsolePrintf( "after GetGPIO37\r\n");

        if ( ATCMD_RESP_BULK_DATA_RX == resp ) 
        {
//          ConsolePrintf( "Command sent successfully. \r\n\n");
          
          if ( Check_CID( server_cid ) ) 
          {
//            ConsolePrintf( "CID check succeeded\r\nReceive %d byte: \r\n", ESCBufferCnt - 1);

//            ConsolePrintf("Transmitted Data: ");         
//            for (int i = 0; i < NTP_PACKET_SIZE; i++)
//            {
//               ConsolePrintf("%02x ", UDP_Data[i]);        
//            }       
//            Serial.println("");
//            ConsolePrintf("Receive Data:     ");  
//            Receive Dataは1バイト目から開始　ESCBufferは0にサイズが入っている つまり1つずれている
//            for (int i = 1; i < ESCBufferCnt; i++)
//            {
//               ConsolePrintf("%02x ", ESCBuffer[i]);        
//            }       
//            Serial.println("");
               
            // 時刻情報は40, 41, 42, 43バイト目のデータ
            // ESCBufferは1つずれている　
            unsigned long highWord = word(ESCBuffer[40+1], ESCBuffer[41+1]);
            unsigned long lowWord = word(ESCBuffer[42+1], ESCBuffer[43+1]);

//            ConsolePrintf("time data: %02x %02x %02x %02x \r\n\n", ESCBuffer[40], ESCBuffer[41], ESCBuffer[42], ESCBuffer[43]);

            // NTPタイムスタンプ 小数部は切り捨て
            unsigned long secsSince1900 = highWord << 16 | lowWord;
//            Serial.print("Seconds since Jan 1 1900 = " );
//            Serial.println(secsSince1900);

            // NTPタイムスタンプをUNIXタイムに変換
            const unsigned long seventyYears = 2208988800UL;
            unsigned long epoch = secsSince1900 - seventyYears + 3600 * 9;
//            Serial.print("Unix time = ");
//            Serial.println(epoch);

            // 雰囲気で代入した         
            RtcTime now1 = epoch;
            
            Serial.print("JST is ");
            Serial.print(now1.year());
            Serial.print('/');
            Serial.print(now1.month());
            Serial.print('/');
            Serial.print(now1.day());
            Serial.print(' ');
            Serial.print(now1.hour());
            Serial.print(':');
            Serial.print(now1.minute());
            Serial.print(':');
            Serial.println(now1.second());
            Serial.println("parsed");
            Serial.println("");
          }
          WiFi_InitESCBuffer();
          
          delay(1000);
        }
...

``` 

</details></div>

送信バッファのUDP_Dataと異なり、受信バッファのESCBufferは先頭1バイトが受信データの長さを表すので、変換するときに注意が必要でした。

###実行時の様子
https://youtu.be/xOePFX-oOBs

Khabane lameさんのように言うとESP32だと1行で済むよねという内容ですが、NTPの勉強になってよかったです。
https://wak-tech.com/archives/833

受信した時刻を送信するパケットに反映させたり、iS110Bに時刻の情報をセットしたりすると面白そうです。