2015年11月4日 星期三

關於用 Arduino+ESP8266 進行網路對時的問題

我在 Oreilly 出版的 "Arduino Cookbook" (Arduino 錦囊妙計第二版) 15-14 節看到利用 NTP (Network Time Protocol) 協定從同步時鐘伺服器網站取得 UTC 時間 (Coordinated Universal Time, 世界標準時間) 的範例, 我覺得這在物聯網應用應該很常用, 例如為 RTC 模組更新時間, 以便與世界標準時間保持同步, 或者在記錄感測器數據到 SD 卡時, 如果沒有 RTC 模組可用, 也可以透過 WiFi 網路取得事件發生時間. 這本書的範例程式可從 Oreilly 網站下載 :

# ArduinoCookbook2E.zip

解開後在 ch15/ch15r14 子目錄下就可以找到範例程式檔. 我將此兩範例檔壓縮在下列 NTP_Ethernet.zip 中 :

# https://www.dropbox.com/s/7qz2v39ph9tv06u/NTP_Ethernet.zip?dl=0

不過, 這個範例使用的是乙太網函式庫 Ethernet.h, 主要是支援以 WZ5100 晶片為主的 Ethernet 網路擴充板, 無法用在 ESP8266 的 WiFi 網路.

在 TCP/IP 模型中, NTP 協定屬於應用層協定, 使用 port 123, 是目前仍在使用中的古老協定之一, 其傳輸層使用 UDP, 這是一個不可靠的非連接性協定, 適用於需要反應較即時的應用. 關於 NTP 可參閱維基與鳥哥的大作 :

# 鳥哥的 Linux 私房菜 : 第十五章、時間伺服器: NTP 伺服器
# Wiki : 網路時間協定
# Wiki : Network Time Protocol


大致摘要一下 NTP 用到的知識 :
  1. GMT (格林威治時間) 與 UTC (國際標準時間) 雖然都是以格林威治時區為參考, 但準度不同, GMT 是太陽通過格林威治天文臺為基準, 使用於 1880 年以前; 而 UTC 則以原子鐘的銫原子震盪週期來計算, 兩者約有 16 分鐘的誤差.
  2. 台灣位於東經 120 度北緯 25 度, 時區為 CCT (中原標準時區), GMT+8.
  3. NTP 伺服器傳回的是 1900/1/1 以來的總秒數, 若要換算成 UNIX 自 1970/1/1 運作以來的秒數, 必須減掉 2208988800 秒.
而 NTP 協定的結構可參考 :

# NTP Data Packet Structure of theNTP Data packet

在 ESP8266 的 AT 韌體中, UDP 訊息可以用 AT 指令 CIPSTART 來起始, 但這並不是建立連線, 而僅僅是註冊要傳送 IP 封包而已, 傳送資料要用 CIPSEND, 例如 :

AT+CIPSTART="UDP","129.6.15.28",123

AT+CIPSEND=48

但接下來要傳送甚麼資料給 NTP 伺服器呢? 下列這篇文章的範例似乎是要傳送 48 BYTES 的資料給 ESP8266 :

# Arduino ESP8266 and NTP

但我照著此文所述, 在 < 出現後傳送那幾個怪碼 (ãì 1N14) 給 ESP8266, 卻沒收到任何回應, 而作者的卻是回應如下 :

SEND OK

+IPD,48:$ ãACTSØ7×0Æêä

OK

難道這是編碼的問題嗎? 這篇文章也沒下文. 最後, 我在下面這篇找到可資參考範例, 是有回應了, 但還未成功 :

# NTP from ESP8266 via AT commands

此作者是參考 Arduino 論壇另一篇回應文章改寫的 :

Arduino ESP8266 and NTP

從這兩篇可以進一步了解, 傳送給 NTP 伺服器的資料有 48 個位元組, 其中 Byte 0~3 以及 12~15 須填入特定的值. 而傳回的時間戳記 (time stamp, 1900/1/1 以來之秒數) 是放在回應字元的 Byte 40 (MSB)~43 (LSB).

作者使用 Arduino Mega 當主控, 以 Serial1 與 ESP8266 介接, 但我是用軟體序列埠, 故我把這個程式中的 Serial1 全部改成 sSerial, 並添加 SoftwareSerial sSerial(10,11) 指令, 以便能移植到我的 Nano + ESP8266 實驗麵包板上 :

/*
ESP8266 connected to sSerial
*/
#include <SoftwareSerial.h>
SoftwareSerial sSerial(10,11); //(RX,TX) 與 ESP8266 介接的軟體串列埠

void setup() {
  // initialize both serial ports:
  Serial.begin(9600);
  sSerial.begin(9600);
  Serial.println("...");
  }

void loop() {
  delay(3000);
  Serial.println();
  Serial.println("Getting time..");
  int Epoch = GetTime();
  Serial.print("Epoch is: ");
  Serial.println(Epoch);
  delay(5000);
  }

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

unsigned long GetTime() {
  String cmd = "AT+CIPSTART=\"UDP\",\"130.102.128.23\",123"; // NTP server
  sSerial.println(cmd);
  delay(2000);
  if(sSerial.find("Error")) {
    Serial.print("RECEIVED: Error");
    return 0;
    }
  int counta = 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
  packetBuffer[12]=49;     //參考 ID (4 bytes)
  packetBuffer[13]=0x4E;
  packetBuffer[14]=49;
  packetBuffer[15]=52;

  sSerial.print("AT+CIPSEND=");
  sSerial.println(NTP_PACKET_SIZE);
  if (sSerial.find(">")) {
    for (byte i = 0; i < NTP_PACKET_SIZE; i++) {
      sSerial.write(packetBuffer[i]);
      delay(5);
      }
    }
  else{
    sSerial.println("AT+CIPCLOSE");
    return 0;
    }
  //sSerial.find("+IPD,48:");
  int acksize = NTP_PACKET_SIZE + 1 + 2 + 8; // ESP8266 adds a space, a CRLF and starts with "+IPD,48:"

  Serial.println("ESP2866 ACK : ");
  for (byte i = 0; i < acksize; i++) {
    while (sSerial.available() == 0) { // you may have to wait for some bytes
      counta += 1;
      Serial.print(".");
      delay(100);
      if (counta == 15) {return 0;}
      }
    byte ch = sSerial.read();
    if (ch < 0x10) {Serial.print('0');}
    Serial.print(ch,HEX);
    Serial.print(' ');
    if ( (((i+1) % 15) == 0) ) { Serial.println(); }
    }
  Serial.println();
  Serial.println();
  memset(packetBuffer, 0, NTP_PACKET_SIZE);
  Serial.println("Server answer : ");
  int i = 0;
  while (sSerial.available() > 0) {
    byte ch = sSerial.read();
    if (i <= NTP_PACKET_SIZE) {
      packetBuffer[i] = ch;
      }
    if (ch < 0x10) {Serial.print('0');}
    Serial.print(ch,HEX);
    Serial.print(' ');
    if ((((i+1) % 15) == 0)) {Serial.println();}
    delay(5);
    i++;
    if ((i < NTP_PACKET_SIZE) && (sSerial.available() == 0)) {
      while (sSerial.available() == 0) { // you may have to wait for some bytes
        counta += 1;
        Serial.print("!");
        delay(100);
        if (counta == 15) {return 0;}
        }
      }
    }
  Serial.println();
  Serial.println();
  Serial.print(i+1);
  Serial.println(" bytes received"); // will be more than 48
  Serial.print(packetBuffer[40],HEX);
  Serial.print(" ");
  Serial.print(packetBuffer[41],HEX);
  Serial.print(" ");
  Serial.print(packetBuffer[42],HEX);
  Serial.print(" ");
  Serial.print(packetBuffer[43],HEX);
  Serial.print(" = ");
  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(secsSince1900,DEC);
  // 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;
  unsigned long DST = 60*60*2; // adjust to your GMT+DST
  unsigned long timestamp = epoch + DST;
  Serial.println();
  Serial.print("Epoch : ");
  Serial.println(epoch,DEC);
  return epoch;
  }

此程式執行後, 序列埠監視視窗的輸出如下 :

...

Getting time..
ESP2866 ACK :
C6 00 06 EC 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 .0D 0A 53 45 4E 44 20 4F 4B 0D 0A

Server answer :


1 bytes received
0 0 0 0 = 0
Epoch : 2085978496
Epoch is: -32384

最後一列中小數點後面的資料之 ASCII 解碼如下 :

0D (CR)
0A (LF)
53 (S)
45 (E)
4E (N)
44 (D)
20 (SP)
4F (O)
4B (K)
0D (CR)
0A (LF)

但是從其輸出可知, 似乎沒有從 NTP 伺服器收到回應,  這是 NTP 伺服器的問題嗎? 我更改 IP 為 192.43.244.18 與 192.5.41.40 後有改善, 雖然大部分要求都沒有回應, 但偶而還是會收到伺服器回應, 例如 :


Getting time..
ESP2866 ACK :
C6 00 06 EC 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 ..0D 0A 53 45 4E 44 20 4F 4B 0D 0A

Server answer :
0D 0A 2B 49 50 44 2C 34 38 3A 24 01 06 E3 00
00 00 00 00 00 00 00 41 43 54 53 D9 E4 6E 2D
30 F4 36 DA 00 00 00 00 00 00 00 00 D9 E4 6E
51 80 26 3B F1 D9 E4 6E 51 80 27 8C 0A 4B

60 bytes received
0 0 D9 E4 = 55780
Epoch : 2086034276
Epoch is: 23396

但新的問題是, 此程式就算成功地收到 NTP 伺服器的回應, 其 Epoch 時間值都一樣不變, Why?

參考下列網址 :

http://www.stdtime.gov.tw/chinese/bulletin/NTP%20promo.txt
台灣合用的ntp server


可用的 NTP 伺服器網址收集如下, 但不是每一個都有回應 :

192.43.244.18    time.nist.gov (ACTS)
132.163.135.130  time-A.timefreq.bldrdoc.gov (ACTS)
192.5.41.40      tick.usno.navy.mil


tick.stdtime.gov.tw
tock.stdtime.gov.tw
time.stdtime.gov.tw
clock.stdtime.gov.tw
watch.stdtime.gov.tw

另外, 有一位德國人把支援 NTP AT 指令的 0.9.4 韌體的函式寫成函式庫 :

# Arduino Zeitsynchronisation mit ESP8266 und NTP

其函式庫含範例可從 Git 下載 :

https://drive.google.com/file/d/0B0j0q4jsSHdjMnVyajZZMXhvNTQ/view

而支援 NTP AT 指令的 0.9.4 版韌體可從下列網址下載 :

https://drive.google.com/file/d/0B0j0q4jsSHdjMnVyajZZMXhvNTQ/view

如果 0.9.2 版的韌體在 NTP 網路對時上一直無法成功的話, 考慮找一顆 ESP8266 來燒錄 0.9.4 版韌體試試看, 此版本有為了 NTP 新增 AT 指令.

其他參考資料 :


# How do es work?
# 有無人玩開ESP8266 (超平WIFI SOC MODULE)呢?
teos0009/ESP8266 Arduino Mega (有範例程式)
Tutorial: IoT Datalogger with ESP8266 WiFi Module and FRDM-KL25Z
# A Guide To Using ESP8266 With TEENSY 3
鳥哥的 Linux 私房菜 : 第十五章、時間伺服器: NTP 伺服器
NTP Request Packet
# Addition of NTP support for 0020000903/0020000904 (0.9.3 版韌體)
# Using an ESP8266 as a time source (Part 1) (0.9.3 版韌體)
# Using an ESP8266 as a time source (Part 2) (使用 NodeMCU)
# Display Internet based Time (NTP)
# UDP NTP CLIENT
# NTP DRIVER FOR ESP8266
Richard's stuff (ESP8266 剖析)
# A SIMPLE NTP CLIENT FOR ESP8266
# REAL TIME CLOCK (DS1307/DS3231) FOR THE ESP8266
# Low-memory footprint, scheduler-friendly NTP client
Arduino ESP8266 and NTP (Read 6501 times)
# Network Time Protocol (NTP) Client
# Arduino教學-使用ESP8266 wifi模組+DHT 溫溼度感測器上傳thingspeak
# 使用 Arduino IDE 開發 ESP8266 物聯網應用 - ThingSpeak, HTTP GET / POST 資料上傳方法
# Arduino 結合ESP8266將空氣資料送到Synology NAS(取代Thingspeak)
家庭環境自動監測管理器-Arduino nano與esp8266實作(韌體篇)
# UDP SERVER EXAMPLE
# A UDP example of ESP8266
# at_example_0020000903
IoT, Internet Of Thing, ESP8266 開始玩了
BROWSER CONTROLLED RASPBERRY PI CAMERA CAR
http://lindr.org/IoT/   (ESP8266 整合開發板 Uninus)
# Uninus 無線Wifi 手機控制 繼電器模組 $410
# [第3代] Uninus 無線Wifi 手機控制 繼電器模組 支援Web/REST/MQTT IoT 智慧開關 小K $420
# 第3代 Uninus ESP8266 NodeMCU ESP-12E/07 最小系統 測試版 $230
# AT+CIPSTART bug when using UDP and DNS lookup
help with esp8266
Arduino ESP8266 and NTP (Read 6723 times)
# Help to Decipher Arduino Code
https://forum.arduino.cc/index.php?topic=285188.0
# Arduino新款EEPROM函式庫小觀察
# Arduino 學習筆記

2016-06-08 補充 :

我已經在下列文章解決此問題 :

使用 ITEAD 的 WeeESP8266 函式庫進行網路對時

2016-06-12 補充 :

直接使用 AT 指令查詢 NTP 的問題已經解決了, 詳見 :

以 Arduino + ESP8266 物聯網模組重作 NTP 實驗 (二)


4 則留言 :

Unknown 提到...

小狐狸您好
我才剛剛玩arduino
我有個設想要利用arduino 做一個按照順序開燈可以打開電磁鎖的機關
目前我想做4-6個開關a b c d e f
這四個開關一開始都是關掉的
當按照順序從a b c d e f 依序打開燈 電磁鎖也可以打開
想請問這種按照順序的要如何寫出來了

謝謝您的答覆

小狐狸事務所 提到...

Sorry, 今天收信才看到您的詢問, 我沒做過這種實驗, 不過按照順序判斷的需要使用有限狀態機, 可參考葉難這篇 :
http://yehnan.blogspot.tw/2012/02/arduinosimon-says.html

我是小心肝 提到...

AT+RST


OK
WIFI DISCONNECT
bBֆQR⸮⸮⸮ȤSN⸮ȤRN⸮H⸮⸮O⸮⸮b(D⸮⸮aH⸮⸮⸮Z⸮H⸮HH⸮⸮⸮⸮5
AT+GMR

AT version:1.1.0.0(May 11 2016 18:09:56)
SDK version:1.5.4(baaeaebb)
compile time:Feb 24 2017 10:13:27
OK
WIFI CONNECTED
AT+CWMODE=3


OK
AT+CIPMUX=1


OK
AT+CIPSERVER=1,80


OK
WIFI GOT IP
AT+CIFSR

+CIFSR:APIP,"192.168.4.1"
+CIFSR:APMAC,"a2:20:a6:21:b7:c8"
+CIFSR:STAIP,"192.168.1.243"
+CIFSR:STAMAC,"a0:20:a6:21:b7:c8"

OK
請問我是這樣就停住了是發生什麼事情?

小狐狸事務所 提到...

可能是資料太多記憶體洩漏嘞