2015/04/14

LinkIt ONE:向NTP伺服器取得時間

透過WiFi,傳送UDP封包,向NTP伺服器取得時間。

底下程式碼中,關於NTP的部份,主要來自Arduino官網的範例Arduino - UdpNTPClient,改用LinkIt ONE的API。原始碼可到GitHub下載。

先看看執行結果,送序列埠送出控制字元,操控WiFi模組,連線後,送出UDP封包向NTP伺服器取得時間。有哪些控制字元,請見程式碼。


程式碼:

#include <stdarg.h>
#include <LWiFi.h>
#include <LWiFiUdp.h>

#define BAUDRATE 115200

// 請修改成你的WiFi名稱與密碼
#define WIFI_AP "__WiFi_AP_NAME__"
#define WIFI_PASSWD __WiFi_PASSWORD__"

// NTP伺服器
#define NTP_SERVER "time.stdtime.gov.tw" // (118, 163, 81, 61)
IPAddress ntpServerIP;
#define ntpServerPort 123

#define LOCAL_PORT 2390

#define NTP_PACKET_SIZE 48

// UDP客戶端
LWiFiUDP udpcli;

// 類似於printf
void pf(const char *fmt, ... ){
    char tmp[128]; // limited to 128 chars
    va_list args;
    va_start(args, fmt);
    vsnprintf(tmp, 128, fmt, args);
    va_end(args);
    Serial.print(tmp);
}


// 根據WiFi模組狀態,回傳容易看懂的字串
const char * const wifi_status_str(LWifiStatus ws){
  switch(ws){
    case LWIFI_STATUS_DISABLED:
      return "WiFi module status: disabled";
    break;
    case LWIFI_STATUS_DISCONNECTED:
      return "WiFi module status: disconnected";
    break;
    case LWIFI_STATUS_CONNECTED:
      return "WiFi module status: connected";
    break;
  }
  return "WiFi status: error, no such status";
}

void setup(){
  Serial.begin(BAUDRATE);
 
  udpcli.begin(LOCAL_PORT); // 綁定某的通訊埠
}


// 送出詢問時間的UDP封包,詳情請看RFC 5905 unsigned long sendNTPpacket(IPAddress &address)
{
  byte buf[NTP_PACKET_SIZE];
  memset(buf, 0, NTP_PACKET_SIZE); // set all bytes in the buffer to 0
  // Initialize values needed to form NTP request
  buf[0] = 0b11100011;   // LI, Version, Mode
  buf[1] = 0;     // Stratum, or type of clock
  buf[2] = 6;     // Polling Interval
  buf[3] = 0xEC;  // Peer Clock Precision
  // 8 bytes of zero for Root Delay & Root Dispersion
  buf[12]  = 49;
  buf[13]  = 0x4E;
  buf[14]  = 49;
  buf[15]  = 52;

  // send a packet requesting a timestamp:
  udpcli.beginPacket(address, ntpServerPort);
  udpcli.write(buf, NTP_PACKET_SIZE);
  udpcli.endPacket();
}


// 拿到秒數(從1900年1月1日 0時0分0秒),印出
// 轉成Unix epoch time,印出
// 捨棄年月日,印出時分秒,HH:MM:SS void printTime(unsigned long secsSince1900){
  pf("Seconds since Jan 1 1900: %u\n", secsSince1900);
 
  unsigned long epoch = secsSince1900 - 2208988800UL; // seventy years
  pf("Unix time: %u\n", epoch); // 00:00:00 1 January 1970, UTC
 
  epoch %= 86400L;
  unsigned long hh = epoch / 3600;
  epoch %= 3600;
  unsigned mm = epoch / 60;
  epoch %= 60;
  unsigned ss = epoch;
  pf("The UTC time: %02u:%02u:%02u\n", hh, mm, ss);
}


// 主迴圈,從序列埠接收控制字元,做該做的事情 void loop(){
  if(Serial.available()){
    char d = Serial.read();
    switch(d){
      case 'l':{ // 搜尋WiFi網路
        int i;
        int n_ap = LWiFi.scanNetworks();
        pf("Number of WiFi AP found: %d\n", n_ap);
        for(i = 0; i < n_ap; i++){
            pf("%d, %s, RSSI: %d\n", i, LWiFi.SSID(i), LWiFi.RSSI(i));
        }
      }
      break;
      case 's':{ // 印出狀態
        LWifiStatus ws = LWiFi.status();
        Serial.println(wifi_status_str(ws));
       
        uint8_t ma[VM_WLAN_WNDRV_MAC_ADDRESS_LEN] = {0};
        LWiFi.macAddress(ma);
        Serial.print("MAC address: ");
        int i;
        for(i = 0; i < VM_WLAN_WNDRV_MAC_ADDRESS_LEN-1; i++){
            pf("%02X:", ma[i]);
        }
        pf("%02X\n", ma[i]);
       
        if(ws == LWIFI_STATUS_CONNECTED){
          IPAddress ipa = LWiFi.localIP();
          Serial.print("localIP: ");
          ipa.printTo(Serial);
          Serial.println("");
         
          Serial.print("subnetMask: ");
          ipa = LWiFi.subnetMask();
          ipa.printTo(Serial);
          Serial.println("");
         
          Serial.print("gatewayIP: ");
          ipa = LWiFi.gatewayIP();
          ipa.printTo(Serial);
          Serial.println("");
        }
      }
      break;
      case 'b':{ // 開啟WiFi模組
        Serial.println("LWiFi.begin() turn on WiFi module");
        LWiFi.begin();
      }
      break;
      case 'e':{ // 關閉WiFi模組
        Serial.println("LWiFi.end() turn off WiFi module");
        LWiFi.end();
      }
      break;
      case 'c':{ // 連接WiFi AP
        Serial.print("Connecting...");
        if(LWiFi.connect(WIFI_AP, LWiFiLoginInfo(LWIFI_WPA, WIFI_PASSWD)) > 0){
          Serial.println(" succeed");
        }
        else{
          Serial.println(" failed");
        }
      }
      break;
      case 'd':{ // 切斷與WiFi AP的連線
        LWiFi.disconnect();
        Serial.println("Disconnected");
      }
      break;
      case 't':{ // 向NTP伺服器詢問時間
        if(ntpServerIP == INADDR_NONE){
          if(LWiFi.hostByName(NTP_SERVER, ntpServerIP) != 1){
            pf("DNS lookup failed: %s", NTP_SERVER);
          }
        }
        if(ntpServerIP != INADDR_NONE){
          pf("Send request to NTP server %s\n", NTP_SERVER);
          sendNTPpacket(ntpServerIP);
        }
      }
      break;
    }
  }
 
  if(udpcli.parsePacket()){ // 收到NTP伺服器傳回來的時間
    byte packetBuffer[NTP_PACKET_SIZE];
    udpcli.read(packetBuffer, NTP_PACKET_SIZE);
    const unsigned long highWord = word(packetBuffer[40], packetBuffer[41]);
    const unsigned long lowWord = word(packetBuffer[42], packetBuffer[43]);
    const unsigned long secsSince1900 = highWord << 16 | lowWord;
    printTime(secsSince1900);
  }
}

5 comments:

  1. 葉大:
    您好,請問一下,我已從此範例成功向NTP取得時間,放在secsSince1900裡;我又從您另一篇文章看到"GPS設定時間",有一個不懂之處,就是在GPS那篇提到:
    datetimeInfo dt;
    LDateTime.setTime(&dt);
    LDateTime.setTime(&dt); // 此處只設定時間
    我要把secsSince1900的值放到哪裡才可以設定時間?
    謝謝您~

    ReplyDelete
  2. 葉大:
    您好,請問一下,我已從此範例成功向NTP取得時間,放在secsSince1900裡;我又從您另一篇文章看到"GPS設定時間",有一個不懂之處,就是在GPS那篇提到:
    datetimeInfo dt;
    LDateTime.setTime(&dt);
    LDateTime.setTime(&dt); // 此處只設定時間
    我要把secsSince1900的值放到哪裡才可以設定時間?
    謝謝您~

    ReplyDelete
    Replies
    1. 拿到secsSince1900後,算出年、月、日、時、分、秒,
      然後放進LinkIt ONE API定義的datetimeInfo結構,
      請參考
      http://labs.mediatek.com/site/global/developer_tools/mediatek_linkit/api_references/datetimeInfo.gsp

      譬如把年月日時分秒,放進底下變數t,
      datetimeInfo t;
      然後呼叫
      LDateTime.setTime(&t);
      把日期時間傳給LDateTime,便可設定。

      Delete
    2. 葉大:
      您好,看了您的解釋我似懂非懂!
      我有找到一個範例程式:
      datetimeInfo now;
      now.year = 2015;
      now.mon = 12;
      now.day = 21;
      LDateTime.setTime(&now);
      您的意思是不是把secsSince1900算出來的年月日時分秒取代2015、12、21?
      我現在遇到的問題是不知道要怎麼把secsSince1900變成年月日時分秒?
      我有看到另一個範例是:
      time_t t1 = 1471688234
      struct tm*nPtr = localtime (&t1)
      我如果把time_t t1 = secsSince1900,是不是tm_year的值就是year的值?

      Delete
    3. secsSince1900是自從1900年1月1日 0時0分0秒所經過的秒數,
      一年365天 一天24時 一時60分 一分60秒
      不斷進行除法 就可以得到 但要注意閏年
      可上網找找 應該已經有人寫好了
      我這篇文章 只有轉換出 時分秒

      datetimeInfo 是LinkIt ONE 定義的結構
      定義請見http://labs.mediatek.com/site/global/developer_tools/mediatek_linkit/api_references/datetimeInfo.gsp
      用來存放年月日時分秒

      放好後 再傳入給LDateTime.setTime
      便可設定日期與時間

      Delete