2016年6月27日 星期一

從 HTTP 網頁伺服器獲取時間訊息的方法

Arduino 除了可以利用 UDP 協定從 NTP 伺服器取得時戳外, 也可以透過網頁服務 (TCP 協定) 來取得時間訊息. 我在下面這篇論壇文章中除了獲取 UDP 的相關 資料外, 也從 Mark Brawell 所發表的回應中得知他建立了幾個跟時間有關的網頁服務, 參考 :

Topic: Arduino ESP8266 and NTP (Read 13317 times)
http://www.foxhollow.ca/nodemgr/?info

只要對這些網址發出 HTTP 要求, 就能取得包括日期, 時間, 星期等訊息, 常用的連結如下 :

http://www.foxhollow.ca/nodemgr/?date (日期 2016-Jun-20)
# http://www.foxhollow.ca/nodemgr/?time (12小時制時間 08:44:08pm)
http://www.foxhollow.ca/nodemgr/?time24 (24小時制時間 20:44:41)
http://www.foxhollow.ca/nodemgr/?Day (星期 Mon)
http://www.foxhollow.ca/nodemgr/?Year (年 2016)
http://www.foxhollow.ca/nodemgr/?month (月 06)
http://www.foxhollow.ca/nodemgr/?day (日 20)
http://www.foxhollow.ca/nodemgr/?Hour (24小時制時)

注意, 由於 Mark Brawell 的伺服器位於加拿大東部 (魁北克省), 因此回傳的時間訊息為美東時間, 換算成台北時間必須加 12 小時, 日期則需加 1 天. 星期幾比較簡單, 取得英文簡寫後用陣列轉換即可. 但時間與日期部分在跨年或跨月時就比較麻煩, 因為不只要換算時, 連日, 月, 年都要同步換算. 不如直接取得自 1970/1/1 以來的 Unix 時戳 (GMT 時間), 再加上 28800 秒 (即 8 小時) 後得到台北時間的時戳來換算比較簡單 :

http://www.foxhollow.ca/nodemgr/?unixtime (Unix 時戳)

下面就使用上週製作好的 Arduino Nano + ESP8266 物聯網模組來測試一下以 TCP 協定從 Mark Bradwell 的網頁伺服器擷取時間. 關於使用 TCP/IP 抓取網頁的方法, 參考 :

# 用 ESP8266 的 TCP/IP 連線抓取網頁

關於從 NTP 伺服器取得時間的方法, 參考 :

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

本次實驗的程式如下 :

#include <SoftwareSerial.h> 
#define DEBUG true

//system use please do not edit 
SoftwareSerial esp8266(7,8); //(RX,TX) 
const int SW_PIN=4; //Pin to switch configuration or working mode
const int MAX_PAGE_NAME_LEN=48;  //buffer size
char buffer[MAX_PAGE_NAME_LEN + 1]; //store page_name/ssid/pwd
int mode; //store current mode(LOW=configuration, HIGH=working)
//-----application codes listed bellow-----

void setup() {
  //system use please do not edit
  Serial.begin(9600);
  esp8266.begin(9600);
  sendData("AT+RST\r\n",2000,DEBUG); // reset ESP8266 
  pinMode(SW_PIN, INPUT);
  mode=digitalRead(SW_PIN);
  sendData("AT+GMR\r\n",1000,DEBUG); 
  if (mode==LOW) { //setup mode : for wifi configuration
    sendData("AT+CWMODE=3\r\n",1000,DEBUG); //configure as access point
    sendData("AT+CIPMUX=1\r\n",1000,DEBUG); //enable multiple connections
    sendData("AT+CIPSERVER=1,80\r\n",2000,DEBUG); //turn on server 80 port         
    }
  else {  //working mode : for running application
    sendData("AT+CWMODE=1\r\n",1000,DEBUG); //configure as a station
    delay(3000); //wait for wifi connection to get local ip
    }
  sendData("AT+CIFSR\r\n",1000,DEBUG); //get ip address
  //-----application codes listed bellow-----
  }
void loop() {
  if (mode==LOW) {setupWifi();} 
  else { //-----application codes listed bellow-----
    Serial.println("Sending HTTP request to www.foxhollow.ca web server ...");
    Serial.println(getCST());
    }
  }

String getCST() {
  sendData("AT+CIPSTART=\"TCP\",\"www.foxhollow.ca\",80\r\n",1000,DEBUG);
  sendData("AT+CIPSEND=24\r\n",1000,DEBUG); //24 bytes include \r\n
  String res=sendData("GET /nodemgr/?unixtime\r\n",3000,DEBUG);
  res.replace("\r\n",""); //remove all line terminator for getting time stamp
  //retrieve time stamp (between ":" and "OK")
  //+IPD,10:1467029520
  //OK
  String t=res.substring(res.indexOf(":") + 1,res.lastIndexOf("OK"));
  //Serial.println(t);
  char buf[10]; //buffer to store unixtime string (for strtol)
  //copy char in string to char array
  for (byte i=0; i<t.length(); i++) {buf[i]=t.charAt(i);}
  char *eptr; //end pointer used in strtol()
  unsigned long epoch=strtol(buf, &eptr, 10); //convert string to long
  Serial.println("GMT time stamp=" + epoch);
  unsigned long c=epoch + 28800; //convert to CST time stamp (GMT+8hr)
  //Convert CST (Taipei time) time stamp to hour:minute:second
  String cst=""; 
  byte hour=(c % 86400L) / 3600; //hour (86400 secs per day)
  if (hour < 10) {cst += "0";} //prefix with "0" if single digit
  cst.concat(hour); 
  cst.concat(":");
  byte  min=(c % 3600) / 60;   //minute (3600 secs per minute)
  if (min < 10) {cst.concat("0");} //prefix with "0" if single digit
  cst.concat(min); 
  cst.concat(":");
  byte sec=c % 60; //second
  if (sec < 10) {cst.concat("0");} //prefix with "0" if single digit
  cst.concat(sec); 
  res=sendData("AT+CIPCLOSE\r\n",5000,DEBUG); //close session 
  if (res.indexOf("OK") != -1) {return cst;}
  else {return "00:00:00";}  
  }

void setupWifi() {
  if (esp8266.available()) { // check if the esp is sending a message   
    if (esp8266.find("+IPD,")) {
      delay(1000);
      //esp8266 link response : +IPD,0,498:GET / HTTP/1.1
      //retrieve connection ID from response (0~4, after "+IPD,") 
      int connectionId=esp8266.read()-48;  //from ASCII to number
      //subtract 48 because read() returns ASCII decimal value
      //and in ASCII, "0" (the first decimal number) starts at 48
      if (esp8266.find("GET /")) { //retrieve page name (router)
        memset(buffer, 0, sizeof(buffer));  //clear buffer (all set to 0)
        if (esp8266.readBytesUntil('/', buffer, sizeof(buffer))) {
          if (strcmp(buffer, "update") == 0) { //update wifi
            //"?ssid=aaa&pwd=bbb HTTP/1.1"             
            esp8266.find("?ssid="); //skip ssid token
            memset(buffer, 0, sizeof(buffer));  //clear buffer (all set to 0)
            esp8266.readBytesUntil('&', buffer, sizeof(buffer)); //retrieve ssid
            String ssid=buffer;
            esp8266.find("pwd="); //skip pwd token
            memset(buffer, 0, sizeof(buffer));  //clear buffer (all set to 0)
            esp8266.readBytesUntil(' ', buffer, sizeof(buffer)); //retrieve pwd
            String pwd=buffer;
            //configure as a station
            String res=sendData("AT+CWJAP=\"" + ssid + "\",\"" + pwd + "\"\r\n",6000,DEBUG);
                      
            //show setup result
            String webpage="<html>Wifi setup ";
            if (res.indexOf("OK") != -1) {webpage += "OK!</html>";}
            else {webpage += "Failed!</html>";}
            String cipSend="AT+CIPSEND=";
            cipSend += connectionId;
            cipSend += ",";
            cipSend +=webpage.length();
            cipSend +="\r\n";
            sendData(cipSend,1000,DEBUG);
            sendData(webpage,2000,DEBUG);
            
            String closeCommand = "AT+CIPCLOSE=";
            closeCommand+=connectionId; // append connection id
            closeCommand+="\r\n";   
            sendData(closeCommand,3000,DEBUG);                 
            }
          else { //show setup page
            String webpage="<html><form method=get action='/update/'>SSID ";
            webpage += "<input name=ssid type=text><br>";
            String cipSend = "AT+CIPSEND=";
            cipSend += connectionId;
            cipSend += ",";
            cipSend +=webpage.length();
            cipSend +="\r\n";
            sendData(cipSend,1000,DEBUG);
            sendData(webpage,2000,DEBUG);

            webpage="PWD <input name=pwd type=text> ";
            cipSend = "AT+CIPSEND=";
            cipSend += connectionId;
            cipSend += ",";
            cipSend +=webpage.length();
            cipSend +="\r\n";
            sendData(cipSend,1000,DEBUG);
            sendData(webpage,2000,DEBUG);

            webpage="<input type=submit value=Connect></form></html>";
            cipSend = "AT+CIPSEND=";
            cipSend += connectionId;
            cipSend += ",";
            cipSend +=webpage.length();
            cipSend +="\r\n";
            sendData(cipSend,1000,DEBUG);
            sendData(webpage,2000,DEBUG);

            String closeCommand = "AT+CIPCLOSE=";
            closeCommand+=connectionId; // append connection id
            closeCommand+="\r\n";   
            sendData(closeCommand,3000,DEBUG);
            }
          }
        }
      }
    } 
  }

String sendData(String command, const int timeout, boolean debug) {
  String response="";
  esp8266.print(command); // send the read character to the esp8266
  long int time=millis();
  while ((time+timeout) > millis()) {
    while(esp8266.available()) {
      // The esp has data so display its output to the serial window 
      char c=esp8266.read(); // read the next character.
      response += c;
      }  
    }
  if (debug) {Serial.print(response);}
  return response;
  }

在此程式中, 首先我們對 ESP8266 下達如下 AT 指令以建立一條 TCP 連線 :

AT+CIPSTART="TCP", "www.foxhollow.ca", 80

然後再用下列 AT 指令告訴 ESP8266, 我們要傳送的資料長度含尾端跳行共 24 bytes :

AT+CIPSEND=24

接著傳送 HTTP 內容, 也就是 GET 方法與要存取之資源路徑 :

GET /nodemgr/?unixtime

內容只有 22 個字元, 但要加上尾巴的 \r\n 兩個跳行字元, 所以上面 CIPSEND 要設為 24. 然後利用字串的 substring() 函數擷取 Unix 時戳數據, 先將回應訊息中的跳行全數去除後, 抓取 ":" 與 "OK" 中間的字串即得. 再用迴圈將時戳字串複製到字元陣列中, 以便利用 strtol() 函數將字元陣列轉換成 long 型態整數, 這樣才能進一步換算成時間. 

最後必須用下列指令釋放 TCP/IP 連線 :

AT+CIPCLOSE

上面程式編譯後記憶體耗用情形如下 :

草稿碼使用了 12,080 bytes (39%) 的程式存儲空間。最大值為 30,720 bytes。
全域變數使用了 924 bytes (45%) 的動態記憶體,剩餘 1,124 bytes 供局部變數。最大值為 2,048 bytes 。

可見比之前用 NTP 要節省 (分別是 43% 與 58%), 全域變數省最多.

下面是序列埠監控視窗擷取的輸出訊息 :

AT+RST


OK
bB�鑭b禔S��"兀L�侒��餾�
[System Ready, Vendor:www.ai-thinker.com]
AT+GMR

0018000902

OK
AT+CWMODE=1

no change
AT+CIFSR

192.168.2.103

OK
Sending HTTP request to www.foxhollow.ca web server ...
AT+CIPSTART="TCP","www.foxhollow.ca",80


OK
Linked
AT+CIPSEND=24

> GET /nodemgr/?unixtime

SEND OK

+IPD,10:1467029879
OK

AT+CIPCLOSE


OK
Unlink
20:17:59
Sending HTTP request to www.foxhollow.ca web server ...
AT+CIPSTART="TCP","www.foxhollow.ca",80


OK
Linked
AT+CIPSEND=24

> GET /nodemgr/?unixtime

SEND OK

+IPD,10:1467029889
OK
� �
AT+CIPCLOSE


OK
Unlink
20:18:09
Sending HTTP request to www.foxhollow.ca web server ...
AT+CIPSTART="TCP","www.foxhollow.ca",80


OK
Linked
AT+CIPSEND=24

> GET /nodemgr/?unixtime

SEND OK

+IPD,10:1467029899
OK
? U
n
`
nan
AT+CIPCLOSE


OK
Unlink
20:18:19

除了使用古老的 NTP 協定外, 看起來用 TCP 協定抓時間比較簡單, 只要做字串處理即可.

參考資料 :

# C Language: strtoll function
# C library function - strcpy()
# Convert string to long long C?
How to convert Unix TimeStamp into day:month:date:year format in C?
# C library function - strftime()
# [Arduino] Timestamp function
# Converting UNIX timestamp to seconds, minutes, hours, days, weeks, month, years, decades ago
# Arduino Time library

2016-06-28 補充 :

長時間觀察結果, 發現此種做法效果不佳, 該伺服器似乎不太能穩定回應要求, 還是用 NTP 為宜.


沒有留言 :