2017年9月26日 星期二

Arduino C on ESP8266 學習筆記 (三) : 從 NTP 伺服器取得網路時間

雖然我不是很喜歡用 Arduino IDE 來寫 ESP8266 程式 (我比較喜歡用 MicroPython), 但既然已經起了頭, 就至少把常用的功能測試一下, 讓 512K Flash 的 ESP-01 能物盡其用 (例如製作物聯網插座). ESP8266 最重要的功能就是透過 WiFi 連網, 而 ESP-01 只接出 GPIO 0, GPIO 2 兩隻腳, 因此若要取得時鐘訊號透過網路從 NTP 取得最方便, 不需要占用 GPIO 外接 RTC, 因此本篇要測試如何從 NTP 伺服器取得網路時間.

我主要是參考下面這篇範例來改寫 :

Arduino/NTPClient.ino at master · esp8266/Arduino

關於 ESP8266WiFi.h 函式庫文件參考 :

Arduino/doc/esp8266wifi/

本系列之前的測試紀錄參考 :

使用 Arduino IDE 開發 ESP8266 應用 (一) : 環境設定與韌體上傳
使用 Arduino IDE 開發 ESP8266 應用 (二) : 在網頁上控制 LED


測試 1 :  從 NTP 伺服器取得 UTC 網路時間

#include <ESP8266WiFi.h>
#include <WiFiUdp.h>

char* ssid="H30-L02-webbot";               //WiFi SSID
char* password="1234567890";                //WiFi password

unsigned int localPort=2390;   //local port to listen for UDP packets
IPAddress timeServerIP;    //time.nist.gov NTP server address
const char* ntpServerName="time.nist.gov"; //NTP Server host name
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
WiFiUDP udp;   //UDP instance to let us send and receive packets over UDP

void setup() {
    Serial.begin(115200);
    Serial.println();
    Serial.println();
    //Connecting to a WiFi network
    Serial.print("Connecting to ");
    Serial.println(ssid);
    WiFi.begin(ssid, password);
    while (WiFi.status() != WL_CONNECTED) {
        delay(500);
        Serial.print(".");
        }
    Serial.println("");
    Serial.println("WiFi connected");
    Serial.print("IP address: ");
    Serial.println(WiFi.localIP());
    //Start UDP
    Serial.println("Starting UDP");
    udp.begin(localPort);
    Serial.print("Local port: ");
    Serial.println(udp.localPort());
    }

void loop() {
    //get a random server from the pool (get an IP from Server Name)
    WiFi.hostByName(ntpServerName, timeServerIP);
    sendNTPpacket(timeServerIP);    //send an NTP packet to a time server
    delay(1000);  // wait to see if a reply is available

    int cb=udp.parsePacket();  //return bytes received
    if (!cb) {Serial.println("no packet yet");}
    else {  //received a packet, read the data from the buffer
        Serial.print("packet received, length=");
        Serial.println(cb);   //=48
        udp.read(packetBuffer, NTP_PACKET_SIZE);    //read the packet into the buffer

        //the timestamp starts at byte 40 of the received packet and is four bytes,
        //or two words, long. First, esxtract the two words:
        unsigned long highWord=word(packetBuffer[40], packetBuffer[41]);
        unsigned long lowWord=word(packetBuffer[42], packetBuffer[43]);
        //combine the four bytes (two words) into a long integer
        //this is NTP time (seconds since Jan 1 1900):
        unsigned long secsSince1900=highWord << 16 | lowWord;
        Serial.print("Seconds since Jan 1 1900 = " );
        Serial.println(secsSince1900);

        //now convert NTP time into everyday time:
        Serial.print("Unix time = ");
        // Unix time starts on Jan 1 1970. In seconds, that's 2208988800:
        const unsigned long seventyYears=2208988800UL;
        // subtract seventy years:
        unsigned long epoch=secsSince1900 - seventyYears;
        // print Unix time:
        Serial.println(epoch);
        // print the hour, minute and second:
        Serial.print("The UTC time is ");       //UTC=Greenwich Meridian (GMT)
        Serial.print((epoch  % 86400L) / 3600); //print the hour (86400 secs/day)
        Serial.print(':');
        //In the first 10 minutes of each hour, we'll want a leading '0'
        if ( ((epoch % 3600) / 60) < 10 ) {Serial.print('0');}
        Serial.print((epoch  % 3600) / 60); // print the minute (3600 secs/minute)
        Serial.print(':');
        // In the first 10 seconds of each minute, we'll want a leading '0'
        if ( (epoch % 60) < 10 ) {Serial.print('0');}
        Serial.println(epoch % 60); // print the second
        }
    delay(10000);
    }

unsigned long sendNTPpacket(IPAddress& address) {
    Serial.println("sending NTP packet...");
    // set all bytes in the buffer to 0
    memset(packetBuffer, 0, NTP_PACKET_SIZE);  //clear the buffer
    //Initialize values needed to form NTP request
    //(see URL above for details on the packets)
    packetBuffer[0]=0b11100011;   // LI, Version, Mode
    packetBuffer[1]=0;     // Stratum, or type of clock
    packetBuffer[2]=6;     // Polling Interval
    packetBuffer[3]=0xEC;  // Peer Clock Precision
    //8 bytes of zero for Root Delay & Root Dispersion
    packetBuffer[12]=49;
    packetBuffer[13]=0x4E;
    packetBuffer[14]=49;
    packetBuffer[15]=52;
    // all NTP fields have been given values, now
    // you can send a packet requesting a timestamp:
    udp.beginPacket(address, 123); //NTP requests are to port 123
    udp.write(packetBuffer, NTP_PACKET_SIZE); //send UDP request to NTP server
    udp.endPacket();
    }

NTP 伺服器使用的是 UDP 協定, 傳送與接收的封包數都是 48 個 Bytes, 參考以前在 Arduino+ESP8266 所做的測試紀錄 :

再探 NTP 協定
利用 NTP 伺服器來同步 Arduino 系統時鐘 (三)


注意, 使用 UDP 協定功能必須匯入 WiFiUdp.h 函式庫, 此函式庫在開發板設定時即已下載, 不須另外安裝. 上面的程式編譯上傳後重開機執行, 序列埠監控視窗輸出如下 :

Connecting to H30-L02-webbot
..
WiFi connected
IP address: 192.168.43.163
Starting UDP
Local port: 2390
sending NTP packet...
packet received, length=48
Seconds since Jan 1 1900 = 3715429678
Unix time = 1506440878
The UTC time is 15:47:58
sending NTP packet...
packet received, length=48
Seconds since Jan 1 1900 = 3715429684
Unix time = 1506440884
The UTC time is 15:48:04
sending NTP packet...
packet received, length=48
Seconds since Jan 1 1900 = 3715429691
Unix time = 1506440891
The UTC time is 15:48:11
sending NTP packet...
packet received, length=48
Seconds since Jan 1 1900 = 3715429697
Unix time = 1506440897
The UTC time is 15:48:17
sending NTP packet...
packet received, length=48
Seconds since Jan 1 1900 = 3715429704
Unix time = 1506440904
The UTC time is 15:48:24

注意, 上面顯示的是 UTC 時間, 轉成台灣時間需 +8 小時 (28800 秒).

參考 "利用 NTP 伺服器來同步 Arduino 系統時鐘 (三)" 的做法, 把 UTC 時間加上 28800 秒後轉成台灣的時戳, 然後用 Arduino 的 setTime() 函數將此時戳設定到 ESP8266 的內部時鐘, 這樣就可以呼叫 Time.h 與 TimeAlarms.h 函式庫的 year(), month(), hour() 等函數了, 注意, Time.h 與 TimeAlarms.h 都要匯入.


 函式 說明
 hour() 傳回現在的時 (24 小時制)
 hourFormat12() 傳回現在的時 (12小時制)
 minute() 傳回現在的分
 second() 傳回現在的秒
 year() 傳回現在的年
 month() 傳回現在的月
 day() 傳回現在的日
 weekday() 傳回現在的星期 (星期日為 1)


測試 2 :  從 NTP 伺服器取得 UTC 網路時間更新內件 RTC (使用計時器但無效)

#include <ESP8266WiFi.h>
#include <WiFiUdp.h>
#include <Time.h>
#include <TimeAlarms.h>

char* ssid="H30-L02-webbot";    //WiFi SSID
char* password="1234567890";      //WiFi password

unsigned int localPort=2390;   //local port to listen for UDP packets
IPAddress timeServerIP;    //time.nist.gov NTP server address
const char* ntpServerName="time.nist.gov"; //NTP Server host name
const int NTP_PACKET_SIZE=48;  //NTP timestamp resides in the first 48 bytes of packets
byte packetBuffer[ NTP_PACKET_SIZE];  //buffer to hold incoming and outgoing packets
WiFiUDP udp;  //UDP instance to let us send and receive packets over UDP

void setup() {
    Serial.begin(115200);
    Serial.println();
    Serial.println();
    //Connecting to a WiFi network
    Serial.print("Connecting to ");
    Serial.println(ssid);
    WiFi.begin(ssid, password);
    while (WiFi.status() != WL_CONNECTED) {
        delay(500);
        Serial.print(".");
        }
    Serial.println("");
    Serial.println("WiFi connected");
    Serial.print("IP address: ");
    Serial.println(WiFi.localIP());
    //Start UDP
    Serial.println("Starting UDP");
    udp.begin(localPort);
    Serial.print("Local port: ");
    Serial.println(udp.localPort());
    sync_clock();
    Alarm.timerRepeat(60, sync_clock); //timer task every 60 seconds
    }

void loop() {
    String d=getDate();
    String t=getTime();
    String w=getWeek();
    Serial.println(d + " " + t + " " + w);  
    delay(10000);
    }
 
void sync_clock() {
  setTime(getUnixTime() + 28800L);
  }

String getDate() {
  String d=(String)year() + "-";
  byte M=month();
  if (M < 10) {d.concat('0');}
  d.concat(M);
  d.concat('-');
  byte D=day();
  if (D < 10) {d.concat('0');}
  d.concat(D);
  return d;
  }

String getTime() {
  String t="";
  byte h=hour();
  if (h < 10) {t.concat('0');}
  t.concat(h);
  t.concat(':');
  byte m=minute();
  if (m < 10) {t.concat('0');}
  t.concat(m);
  t.concat(':');
  byte s=second();
  if (s < 10) {t.concat('0');}
  t.concat(s);
  return t;
  }

String getWeek() {
  String w[]={"Sun","Mon","Tue","Wed","Thu","Fri","Sat"};
  return w[weekday()-1];
  }

unsigned long getUnixTime() {
    WiFi.hostByName(ntpServerName, timeServerIP);  //get a random server from the pool
    sendNTPpacket(timeServerIP);                   //send an NTP packet to a time server
    delay(1000);                                   // wait to see if a reply is available

    int cb=udp.parsePacket();                      //return bytes received
    unsigned long unix_time=0;
    if (!cb) {Serial.println("no packet yet");}
    else {  //received a packet, read the data from the buffer
        Serial.print("packet received, length=");
        Serial.println(cb);                        //=48
        udp.read(packetBuffer, NTP_PACKET_SIZE);  //read the packet into the buffer

        //the timestamp starts at byte 40 of the received packet and is four bytes,
        //or two words, long. First, esxtract the two words:
        unsigned long highWord=word(packetBuffer[40], packetBuffer[41]);
        unsigned long lowWord=word(packetBuffer[42], packetBuffer[43]);
        //combine the four bytes (two words) into a long integer
        //this is NTP time (seconds since Jan 1 1900):
        unsigned long secsSince1900=highWord << 16 | lowWord;
        Serial.print("Seconds since Jan 1 1900=" );
        Serial.println(secsSince1900);
        Serial.print("Unix time=");
        //Unix time starts on Jan 1 1970. In seconds, that's 2208988800:
        unix_time=secsSince1900 - 2208988800UL;
        Serial.print(F("Unix time stamp (seconds since 1970-01-01)="));
        Serial.println(unix_time); //print Unix time
        }  
    return unix_time; //return seconds since 1970-01-01
    }

unsigned long sendNTPpacket(IPAddress& address) {
    Serial.println("sending NTP packet...");
    // set all bytes in the buffer to 0
    memset(packetBuffer, 0, NTP_PACKET_SIZE);
    //Initialize values needed to form NTP request
    //(see URL above for details on the packets)
    packetBuffer[0]=0b11100011;   // LI, Version, Mode
    packetBuffer[1]=0;     // Stratum, or type of clock
    packetBuffer[2]=6;     // Polling Interval
    packetBuffer[3]=0xEC;  // Peer Clock Precision
    //8 bytes of zero for Root Delay & Root Dispersion
    packetBuffer[12]=49;
    packetBuffer[13]=0x4E;
    packetBuffer[14]=49;
    packetBuffer[15]=52;
    // all NTP fields have been given values, now
    // you can send a packet requesting a timestamp:
    udp.beginPacket(address, 123); //NTP requests are to port 123
    udp.write(packetBuffer, NTP_PACKET_SIZE);
    udp.endPacket();
    }

序列埠監控視窗輸出如下 :

Connecting to H30-L02-webbot
..
WiFi connected
IP address: 192.168.43.163
Starting UDP
Local port: 2390
sending NTP packet...
packet received, length=48
Seconds since Jan 1 1900=3715472215
Unix time=Unix time stamp (seconds since 1970-01-01)=1506483415
2017-09-27 11:36:55 Wed
2017-09-27 11:37:05 Wed
2017-09-27 11:37:15 Wed
2017-09-27 11:37:25 Wed
2017-09-27 11:37:35 Wed
2017-09-27 11:37:45 Wed
2017-09-27 11:37:55 Wed
2017-09-27 11:38:05 Wed
2017-09-27 11:38:15 Wed
2017-09-27 11:38:25 Wed
2017-09-27 11:38:35 Wed
2017-09-27 11:38:45 Wed
2017-09-27 11:38:55 Wed
2017-09-27 11:39:05 Wed
2017-09-27 11:39:15 Wed
2017-09-27 11:39:25 Wed

看起來 Arduino 內建的 setTime() 可以正常設定 ESP8266 的 RTC 內部時鐘, 但 Alarm.timerRepeat() 似乎沒有運作! 照理應該每 60 秒呼叫一次 sync_clock() 才對, 但除了在 setup() 中呼叫過一次 sync_clock() 外, 之後就沒有再呼叫了, 奇怪, 不知哪裡出問題.

我在下列葉難的文章中看到 Timer 函式庫 :

Arduino一個好用的計時器程式庫  

改用 Timer.h 函式庫裡的 Timer.every() 也是一樣無作用, 我猜有可能這些函式庫是針對 Arduino 板子, 可能對 ESP8266 無效? 參考下面這篇 :

ESP8266 Timer

看起來在 Arduino IDE 中要搞定 ESP8266 的 Timer 似乎很麻煩哩! 但我要的只是想固定一段時間 (例如 40 秒) 就從 NTP 伺服器取得最新時間來更新內部時鐘而已, 免得內部時鐘誤差越來越大. 其實若 TimerAlarms.h 不能用, 也可以用計數器, 例如下面範例每秒顯示一次內部時鐘, 但每 40 次迴圈 (即大約每 40 秒) 就讀取 NTP 時間來同步 :


測試 3 :  從 NTP 伺服器取得 UTC 網路時間更新內建 RTC (使用計數器)

#include <ESP8266WiFi.h>
#include <WiFiUdp.h>
#include <Time.h>
#include <TimeAlarms.h>

char* ssid="H30-L02-webbot";    //WiFi SSID
char* password="1234567890";      //WiFi password

unsigned int localPort=2390;   //local port to listen for UDP packets
IPAddress timeServerIP;    //time.nist.gov NTP server address
const char* ntpServerName="time.nist.gov"; //NTP Server host name
const int NTP_PACKET_SIZE=48;  //NTP timestamp resides in the first 48 bytes of packets
byte packetBuffer[ NTP_PACKET_SIZE];  //buffer to hold incoming and outgoing packets
WiFiUDP udp;  //UDP instance to let us send and receive packets over UDP
int count=0;

void setup() {
    Serial.begin(115200);
    Serial.println();
    Serial.println();
    //Connecting to a WiFi network
    Serial.print("Connecting to ");
    Serial.println(ssid);
    WiFi.begin(ssid, password);
    while (WiFi.status() != WL_CONNECTED) {
        delay(500);
        Serial.print(".");
        }
    Serial.println("");
    Serial.println("WiFi connected");
    Serial.print("IP address: ");
    Serial.println(WiFi.localIP());
    //Start UDP
    Serial.println("Starting UDP");
    udp.begin(localPort);
    Serial.print("Local port: ");
    Serial.println(udp.localPort());
    sync_clock();   //以 NTP 時間初始設定內部時鐘
    }

void loop() {
    String d=getDate();
    String t=getTime();
    String w=getWeek();
    int Hm=hour()*100 + minute();   //時:分整數, 0~2359
    Serial.print(d + " " + t + " " + w + " ");
    Serial.println(Hm);  
    ++count;
    if (count >= 40) {  //每 40 次迴圈與 NTP 同步一次
        sync_clock();
        count=0;
        }
    delay(1000);
    }
 
void sync_clock() {
  unsigned long GMT=getUnixTime();
  if (GMT != 0) {   //有得到 NTP 回應才更新 ESP8266 內建 RTC
    setTime(GMT + 28800L);  //以台灣時間更新內部時鐘
    }
  }

String getDate() {
  String d=(String)year() + "-";
  byte M=month();
  if (M < 10) {d.concat('0');}
  d.concat(M);
  d.concat('-');
  byte D=day();
  if (D < 10) {d.concat('0');}
  d.concat(D);
  return d;
  }

String getTime() {
  String t="";
  byte h=hour();
  if (h < 10) {t.concat('0');}
  t.concat(h);
  t.concat(':');
  byte m=minute();
  if (m < 10) {t.concat('0');}
  t.concat(m);
  t.concat(':');
  byte s=second();
  if (s < 10) {t.concat('0');}
  t.concat(s);
  return t;
  }

String getWeek() {
  String w[]={"Sun","Mon","Tue","Wed","Thu","Fri","Sat"};
  return w[weekday()-1];
  }

String getDateTime() {  //傳回日期時間
  String dt=(String)year() + "-";
  byte M=month();
  if (M < 10) {dt.concat('0');}
  dt.concat(M);
  dt.concat('-');
  byte d=day();
  if (d < 10) {dt.concat('0');}
  dt.concat(d);
  dt.concat(' ');
  byte h=hour();
  if (h < 10) {dt.concat('0');}
  dt.concat(h);
  dt.concat(':');
  byte m=minute();
  if (m < 10) {dt.concat('0');}
  dt.concat(m);
  dt.concat(':');
  byte s=second();
  if (s < 10) {dt.concat('0');}
  dt.concat(s);
  return dt;  //傳回格式如 2016-07-16 16:09:23 的日期時間字串
  }

unsigned long getUnixTime() {
    WiFi.hostByName(ntpServerName, timeServerIP);  //get a random server from the pool
    sendNTPpacket(timeServerIP);   //send an NTP packet to a time server
    delay(1000);   // wait to see if a reply is available

    int cb=udp.parsePacket();     //return bytes received
    unsigned long unix_time=0;    //預設傳回 0, 表示未收到 NTP 回應
    if (!cb) {Serial.println("no packet yet");}
    else {  //received a packet, read the data from the buffer
        Serial.print("packet received, length=");
        Serial.println(cb);    //=48
        udp.read(packetBuffer, NTP_PACKET_SIZE);  //read the packet into the buffer

        //the timestamp starts at byte 40 of the received packet and is four bytes,
        //or two words, long. First, esxtract the two words:
        unsigned long highWord=word(packetBuffer[40], packetBuffer[41]);
        unsigned long lowWord=word(packetBuffer[42], packetBuffer[43]);
        //combine the four bytes (two words) into a long integer
        //this is NTP time (seconds since Jan 1 1900):
        unsigned long secsSince1900=highWord << 16 | lowWord;
        Serial.print("Seconds since Jan 1 1900=" );
        Serial.println(secsSince1900);
        Serial.print("Unix time=");
        //Unix time starts on Jan 1 1970. In seconds, that's 2208988800:
        unix_time=secsSince1900 - 2208988800UL;    //更新 unix_time
        Serial.print(F("Unix time stamp (seconds since 1970-01-01)="));
        Serial.println(unix_time); //print Unix time
        }  
    return unix_time; //return seconds since 1970-01-01
    }

unsigned long sendNTPpacket(IPAddress& address) {
    Serial.println("sending NTP packet...");
    // set all bytes in the buffer to 0
    memset(packetBuffer, 0, NTP_PACKET_SIZE);
    //Initialize values needed to form NTP request
    //(see URL above for details on the packets)
    packetBuffer[0]=0b11100011;   // LI, Version, Mode
    packetBuffer[1]=0;     // Stratum, or type of clock
    packetBuffer[2]=6;     // Polling Interval
    packetBuffer[3]=0xEC;  // Peer Clock Precision
    //8 bytes of zero for Root Delay & Root Dispersion
    packetBuffer[12]=49;
    packetBuffer[13]=0x4E;
    packetBuffer[14]=49;
    packetBuffer[15]=52;
    // all NTP fields have been given values, now
    // you can send a packet requesting a timestamp:
    udp.beginPacket(address, 123); //NTP requests are to port 123
    udp.write(packetBuffer, NTP_PACKET_SIZE);
    udp.endPacket();
    }

注意, 在 loop() 中的 Hm 變數是由 Hour 乘以 100 後加上 Minute, 這樣會組成 0~2359 的 "時分 整數", 可應用在物聯網插頭或自動澆水器等應用的時間判斷上, 例如如果要在早上六點與傍晚六點各澆水十分鐘, 則啟動馬達的 "時分整數" 位於 600~610 與 1800~1810 兩個區間內, 可用如下程式碼判斷 :

if ((Hm >= 600 && Hm <=610) || (Hm >= 1800 && Hm <=1810)) {
    digitalWrite(motorPin, HIGH);  //motor on
    }
 else {
    digitalWrite(motorPin, LOW);  //motor off
    }

當然如果需要也可以精細到秒, 例如 :

HmS=int Hm=hour()*10000 + minute()*100 + second();

這個 HmS 的值區間為 0~235959. 要注意的是, 不管是 Hm 還是 HmS, 部分值域是無意義的, 例如 Hm 的 178 就是無意義的值, 因為 3 位數最大只到 159 (1 點 59 分), 接著就跳到 200  (2 點) 了.

序列埠監控視窗輸出訊息 :

Connecting to H30-L02-webbot
.................
WiFi connected
IP address: 192.168.43.163
Starting UDP
Local port: 2390
sending NTP packet...
packet received, length=48
Seconds since Jan 1 1900=3715498311
Unix time=Unix time stamp (seconds since 1970-01-01)=1506509511
2017-09-27 18:51:51 Wed 1851
2017-09-27 18:51:52 Wed 1851
2017-09-27 18:51:53 Wed 1851
2017-09-27 18:51:54 Wed 1851
2017-09-27 18:51:55 Wed 1851
2017-09-27 18:51:56 Wed 1851
2017-09-27 18:51:57 Wed 1851
2017-09-27 18:51:58 Wed 1851
2017-09-27 18:51:59 Wed 1851
2017-09-27 18:52:00 Wed 1852
2017-09-27 18:52:01 Wed 1852
2017-09-27 18:52:02 Wed 1852
2017-09-27 18:52:03 Wed 1852
2017-09-27 18:52:04 Wed 1852
2017-09-27 18:52:05 Wed 1852
2017-09-27 18:52:06 Wed 1852
2017-09-27 18:52:07 Wed 1852
2017-09-27 18:52:08 Wed 1852
2017-09-27 18:52:09 Wed 1852
2017-09-27 18:52:10 Wed 1852
2017-09-27 18:52:11 Wed 1852
2017-09-27 18:52:12 Wed 1852
2017-09-27 18:52:13 Wed 1852
2017-09-27 18:52:14 Wed 1852
2017-09-27 18:52:15 Wed 1852
2017-09-27 18:52:16 Wed 1852
2017-09-27 18:52:17 Wed 1852
2017-09-27 18:52:18 Wed 1852
2017-09-27 18:52:19 Wed 1852
2017-09-27 18:52:20 Wed 1852
2017-09-27 18:52:21 Wed 1852
2017-09-27 18:52:22 Wed 1852
2017-09-27 18:52:23 Wed 1852
2017-09-27 18:52:24 Wed 1852
2017-09-27 18:52:25 Wed 1852
2017-09-27 18:52:26 Wed 1852
2017-09-27 18:52:27 Wed 1852
2017-09-27 18:52:28 Wed 1852
2017-09-27 18:52:29 Wed 1852
2017-09-27 18:52:30 Wed 1852
sending NTP packet...
packet received, length=48
Seconds since Jan 1 1900=3715498352
Unix time=Unix time stamp (seconds since 1970-01-01)=1506509552
2017-09-27 18:52:33 Wed 1852
2017-09-27 18:52:34 Wed 1852
2017-09-27 18:52:35 Wed 1852
2017-09-27 18:52:36 Wed 1852
2017-09-27 18:52:37 Wed 1852
2017-09-27 18:52:38 Wed 1852
2017-09-27 18:52:39 Wed 1852

可見確實每 40 次迴圈就會與 NTP 伺服器同步一次, 事實上頻率不用這麼高, 跑了數百次到數千次再同步也不會有顯著誤差. 但是要注意, 下面這篇文件提到, ESP8266 內建 RTC 每 7 個小時 45 分會溢位, 因此 7 個小時內一定要再跟 NTP 伺服器同步一次, 參考 :

http://docs.micropython.org/en/v1.8.7/esp8266/esp8266/general.html#real-time-clock

如果只是要取得 "時分" 或 "時分秒" 整數以判別目前時間, 其實也不需要匯入 Time.h 與 TimeAlarms.h, 直接將時戳做餘數與加法運算即可, 如下列兩個函數 :

int getHm(unsigned long T) {
  return ((T  % 86400L) / 3600)*100 + ((T  % 3600) / 60);
  }

int getHmS(unsigned long T) {
  return ((T  % 86400L) / 3600)*10000 + ((T  % 3600) / 60)*100 + (T % 60);
  }

使用時只要傳入時戳 T 呼叫 Hm() 或 HmS() 即可 :

unsigned long T=getUnixTime() + 28800L;  //計算 GMT+8 時戳
if (Hm(T) >= 600 && Hm(T) <= 610) {
    digitalWrite(motorPin, HIGH);  //motor on
    }
 else {
    digitalWrite(motorPin, LOW);  //motor off
    }

例如下列範例 :

測試 4 :  從 NTP 伺服器取得網路時間模擬控制馬達開關 (不使用時間函式庫)

#include <ESP8266WiFi.h>
#include <WiFiUdp.h>

char* ssid="H30-L02-webbot";    //WiFi SSID
char* password="a5572056";      //WiFi password

unsigned int localPort=2390;   //local port to listen for UDP packets
IPAddress timeServerIP;    //time.nist.gov NTP server address
const char* ntpServerName="time.nist.gov"; //NTP Server host name
const int NTP_PACKET_SIZE=48;  //NTP timestamp resides in the first 48 bytes of packets
byte packetBuffer[ NTP_PACKET_SIZE];  //buffer to hold incoming and outgoing packets
WiFiUDP udp;  //UDP instance to let us send and receive packets over UDP

const int motorPin=2;      // GPIO2

void setup() {
    pinMode(motorPin, OUTPUT);
    digitalWrite(motorPin, LOW);
    Serial.begin(115200);
    Serial.println();
    Serial.println();
    //Connecting to a WiFi network
    Serial.print("Connecting to ");
    Serial.println(ssid);
    WiFi.begin(ssid, password);
    while (WiFi.status() != WL_CONNECTED) {
        delay(500);
        Serial.print(".");
        }
    Serial.println("");
    Serial.println("WiFi connected");
    Serial.print("IP address: ");
    Serial.println(WiFi.localIP());
    //Start UDP
    Serial.println("Starting UDP");
    udp.begin(localPort);
    Serial.print("Local port: ");
    Serial.println(udp.localPort());
    }

void loop() {
    int Hm=getHm(getUnixTime() + 28800L);   //取得台灣時間的 "時分" 整數
    Serial.println(Hm);
    if (Hm >= 1500 && Hm <= 1510) {  //在 15:00 ~15:10 間啟動馬達
        digitalWrite(motorPin, HIGH);  //motor on
        Serial.println("Motor is ON");
        }
    else {
        digitalWrite(motorPin, LOW);  //motor off
        Serial.println("Motor is OFF");
        }  
    delay(60000);
    }

int getHm(unsigned long T) {  //return 0~2359
  return ((T  % 86400L) / 3600)*100 + ((T  % 3600) / 60);  
  }

int getHmS(unsigned long T) {  //return 0~235959
  return ((T  % 86400L) / 3600)*10000 + ((T  % 3600) / 60)*100 + (T % 60);  
  }

unsigned long getUnixTime() { //get GMT epoch
    WiFi.hostByName(ntpServerName, timeServerIP);  //get a random server from the pool
    sendNTPpacket(timeServerIP);                   //send an NTP packet to a time server
    delay(1000);                                   // wait to see if a reply is available

    int cb=udp.parsePacket();                      //return bytes received
    unsigned long unix_time=0;
    if (!cb) {Serial.println("no packet yet");}
    else {  //received a packet, read the data from the buffer
        Serial.print("packet received, length=");
        Serial.println(cb);                        //=48
        udp.read(packetBuffer, NTP_PACKET_SIZE);  //read the packet into the buffer

        //the timestamp starts at byte 40 of the received packet and is four bytes,
        //or two words, long. First, esxtract the two words:
        unsigned long highWord=word(packetBuffer[40], packetBuffer[41]);
        unsigned long lowWord=word(packetBuffer[42], packetBuffer[43]);
        //combine the four bytes (two words) into a long integer
        //this is NTP time (seconds since Jan 1 1900):
        unsigned long secsSince1900=highWord << 16 | lowWord;
        Serial.print("Seconds since Jan 1 1900=" );
        Serial.println(secsSince1900);
        Serial.print("Unix time=");
        //Unix time starts on Jan 1 1970. In seconds, that's 2208988800:
        unix_time=secsSince1900 - 2208988800UL;
        Serial.print(F("Unix time stamp (seconds since 1970-01-01)="));
        Serial.println(unix_time); //print Unix time
        }  
    return unix_time; //return seconds since 1970-01-01
    }

unsigned long sendNTPpacket(IPAddress& address) {
    Serial.println("sending NTP packet...");
    // set all bytes in the buffer to 0
    memset(packetBuffer, 0, NTP_PACKET_SIZE);
    //Initialize values needed to form NTP request
    //(see URL above for details on the packets)
    packetBuffer[0]=0b11100011;   // LI, Version, Mode
    packetBuffer[1]=0;     // Stratum, or type of clock
    packetBuffer[2]=6;     // Polling Interval
    packetBuffer[3]=0xEC;  // Peer Clock Precision
    //8 bytes of zero for Root Delay & Root Dispersion
    packetBuffer[12]=49;
    packetBuffer[13]=0x4E;
    packetBuffer[14]=49;
    packetBuffer[15]=52;
    // all NTP fields have been given values, now
    // you can send a packet requesting a timestamp:
    udp.beginPacket(address, 123); //NTP requests are to port 123
    udp.write(packetBuffer, NTP_PACKET_SIZE);
    udp.endPacket();
    }

不過上面這個程式有風險, 因為它完全依靠 UDP 查詢到的時間來決定是否啟閉馬達, 而 UDP 協定是不保證傳輸品質的協定, 有時候收不到回應, 這時 getUnixTime() 會傳回 0. 比較保險的做法還是像測試 3 那樣依靠 ESP8266 內部的 RTC, 使用 Time.h 與 TimeAlarms.h 的 hour(), minute() 一定可以取得資料製作 "時分" 整數, 只要定期與 UDP 同步即可.

測試結果 :

Connecting to H30-L02-webbot
..
WiFi connected
IP address: 192.168.43.163
Starting UDP
Local port: 2390
sending NTP packet...
packet received, length=48
Seconds since Jan 1 1900=3715570098
Unix time=Unix time stamp (seconds since 1970-01-01)=1506581298
1448
Motor is OFF
....
....
sending NTP packet...
packet received, length=48
Seconds since Jan 1 1900=3715570680
Unix time=Unix time stamp (seconds since 1970-01-01)=1506581880
1458
Motor is OFF
sending NTP packet...
packet received, length=48
Seconds since Jan 1 1900=3715570741
Unix time=Unix time stamp (seconds since 1970-01-01)=1506581941
1459
Motor is OFF
sending NTP packet...
packet received, length=48
Seconds since Jan 1 1900=3715570803
Unix time=Unix time stamp (seconds since 1970-01-01)=1506582003
1500
Motor is ON
sending NTP packet...
packet received, length=48
Seconds since Jan 1 1900=3715570865
Unix time=Unix time stamp (seconds since 1970-01-01)=1506582065
1501
Motor is ON
....
....
Motor is ON
sending NTP packet...
packet received, length=48
Seconds since Jan 1 1900=3715571050
Unix time=Unix time stamp (seconds since 1970-01-01)=1506582250
1504
Motor is ON
sending NTP packet...
no packet yet
800
Motor is OFF     (未取得 1505 回應, 導致馬達關閉)
sending NTP packet...
packet received, length=48
Seconds since Jan 1 1900=3715571173
Unix time=Unix time stamp (seconds since 1970-01-01)=1506582373
1506
Motor is ON
sending NTP packet...
packet received, length=48
Seconds since Jan 1 1900=3715571235
Unix time=Unix time stamp (seconds since 1970-01-01)=1506582435
1507
Motor is ON
sending NTP packet...
no packet yet
800
Motor is OFF   (未取得 1508 回應, 導致馬達關閉)
sending NTP packet...
packet received, length=48
Seconds since Jan 1 1900=3715571358
Unix time=Unix time stamp (seconds since 1970-01-01)=1506582558
1509
Motor is ON
sending NTP packet...
packet received, length=48
Seconds since Jan 1 1900=3715571420
Unix time=Unix time stamp (seconds since 1970-01-01)=1506582620
1510
Motor is ON
sending NTP packet...
packet received, length=48
Seconds since Jan 1 1900=3715571481
Unix time=Unix time stamp (seconds since 1970-01-01)=1506582681
1511
Motor is OFF

可見在 15:05 與 15:08 時, 因未取得 UDP 回應, 使得 Hm 計算結果為 800 (即 GMT+8 的 8), 導致馬達被關閉. 可以將 loop() 內程式碼加上判斷 getUnixTime() 是否為 0 的機制, 若未取得 NTP 回應就不做啟閉動作, 維持原狀態 :

    unsigned long T=getUnixTime();
    if (T != 0) {
      int Hm=getHm(T + 28800L);   //取得台灣時間的 "時分" 整數
      Serial.println(Hm);
      if (Hm >= 1500 && Hm <= 1510) {  //在 15:00 ~15:10 間啟動馬達
          digitalWrite(motorPin, HIGH);  //motor on
          Serial.println("Motor is ON");
          }
      else {
          digitalWrite(motorPin, LOW);  //motor off
          Serial.println("Motor is OFF");
          }
      }

參考 :

https://github.com/esp8266/Arduino/blob/master/libraries/ESP8266WiFi/src/WiFiUdp.h
ESP8266 Timer0 and ISR
http://twincati.blogspot.tw/2016/12/esp8266-timer.html
# 86Duino 程式語法參考

2017-09-28 補充 :

這幾天停下 C 語言來做 Arduino on ESP8266 實驗要暫停一下, 這些實驗是專為 512K Flash 的 ESP-01 而做的, 我想將其應用在小型終端控制器, 例如物聯網開關與自動灑水系統. 有空回來時要繼續做的項目如下 :
  1. HTTP 客戶端 (MySQL 資料庫, ThingSpeak, Twitter 控制 ...)
  2. WiFi 基地台連線設定
  3. Blynk on ESP8266 Standalone

1 則留言 :

Brian 提到...

void loop() {
String d=getDate();
String t=getTime();
String w=getWeek();
Serial.println(d + " " + t + " " + w);
Alarm.delay(10000);
}


你只要把ALARM 的內部計時器有做工作就會正常工作。