2016年6月10日 星期五

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

完成 Arduino + ESP8266 物聯網模組後, 我就想用此模組來重做 NTP 實驗. 上回我是使用 WeeESP8266 函式庫成功地完成了 NTP 實驗, 參考 :

關於用 Arduino+ESP8266 進行網路對時的問題
使用 ITEAD 的 WeeESP8266 函式庫進行網路對時

雖然 WeeESP8266 函式庫完整好用, 但是載入整個函式庫也耗掉不少記憶體與可用的變數空間, 應用程式碼稍大些可能導致編譯警告或執行不正常. 這次我要使用從 AllAboutEE 修改而來的單一函式, 雖然不像 WeeESP8266 那麼方便, 必須接觸 AT 指令, 但程式編譯後的程式碼精簡較不占位置. 

我參考下列這篇文章中 adminarduinocc 所貼的 Working codes 程式 :

Topic: Arduino ESP8266 and NTP (Read 13278 times)

我將其修改為 getTime() 函數並加入之前完結篇裡 Arduino + ESP8266 物聯網所使用的程式架構內, 在 loop() 中呼叫它以便取得 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-----
//Request for NTP time stamp (in the first 48 bytes)
byte packetBuffer[48]; //buffer : send & recv data to/from NTP

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);
  if (mode==LOW) { //wifi configuration mode :
    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",1000,DEBUG); //turn on server 80 port      
    }
  else {
    sendData("AT+CWMODE?\r\n",1000,DEBUG);
    sendData("AT+CIPMUX?\r\n",1000,DEBUG);
    delay(3000);
    }
  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("Getting time..");
    int Epoch=GetTime();
    Serial.print("Epoch is: ");
    Serial.println(Epoch);
    delay(5000);
    }
  }

unsigned long GetTime() {
  //Start UDP Rrequest to NTP Server 91.226.136.136, 82.209.243.241,192.5.41.40
  sendData("AT+CIPSTART=\"UDP\",\"192.5.41.40\",123\r\n",5000,DEBUG);
  memset(packetBuffer,0,48); //clear buffer
  //Initialize buffer to form NTP request packet
  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;          //reference ID (4 bytes)
  packetBuffer[13]=0x4E;
  packetBuffer[14]=49;
  packetBuffer[15]=52;
  //send request to NTP Server
  sendData("AT+CIPSEND=48\r\n",1000,DEBUG); //send data length
  if (esp8266.find(">")) { //sending data to NTP if available
    for (byte i=0; i < 48; i++) {
      esp8266.write(packetBuffer[i]);
      delay(5);
      }
    Serial.println("Send request ... OK");  
    }
  else {
    sendData("AT+CIPCLOSE\r\n",1000,DEBUG);
    return 0;
    }
  delay(3000); //waiting for NTP Server response
  int counta=0; //wainting counter
  memset(packetBuffer,0,48); //clear buffer for receiving
  if (Serial.find("+IPD,48:")) { //if NTP Server responds
    Serial.println("Server answer : ");
    int i=0; //response packet byte counter
    while (esp8266.available() > 0) { //read response into packet buffer
      byte ch=esp8266.read();
      if (i < 48) {packetBuffer[i]=ch;} //read into buffer
      i++; //increment packet counter
      if ((i < 48) && (esp8266.available() == 0)) {
        //Packet not complete but no response : wait 1.5s for response
        while (esp8266.available() == 0) { //
          counta += 1; //increment waiting counter
          Serial.print("!"); //show ! means waiting for response packet
          delay(100);
          if (counta == 15) {exit;} //waiting timeout
          }
        }
      }
    }
  //handling response packet : timestamp starts at byte 40 of response packet
  //(4 bytes long), extract them into words :
  unsigned long highWord=word(packetBuffer[40],packetBuffer[41]);
  unsigned long lowWord=word(packetBuffer[42],packetBuffer[43]);
  //combine the two words into a long integer time stamp
  //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);
  //convert NTP time into everyday time:
  Serial.print("Unix time=");
  //Unix time starts on Jan 1 1970=2208988800 seconds since 1900-01-01
  const unsigned long seventyYears=2208988800UL;
  //subtract seventy years to get Unix time stamp
  unsigned long epoch=secsSince1900 - seventyYears;
  Serial.println(epoch); //print Unix time
  //Covert epoch to UTC/GMT (Greenwich Meridian) hour:minute:second
  Serial.print("The UTC time is "); // UTC is the time at
  Serial.print((epoch % 86400L) / 3600); //hour (86400 secs per day)
  Serial.print(':');
  if (((epoch % 3600) / 60) < 10) {Serial.print('0');}
  Serial.print((epoch % 3600) / 60); //minute (3600 secs per minute)
  Serial.print(':');
  if ((epoch % 60) < 10 ) {Serial.print('0');}
  Serial.println(epoch % 60); //second
  sendData("AT+CIPCLOSE\r\n",1000,DEBUG); //close session
  }

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;
  }

但是編譯上傳後卻沒有如預期取得 NTP 的回應, 序列埠監控視窗擷取的訊息如下 : 

AT+RST


OK
bB�鑭b禔S��"��岓��S��
[Vendor:www.ai-thinker.com Version:0.9.2.4]

ready
AT+CWMODE?

+CWMODE:1

OK
AT+CIPMUX?

+CIPMUX:0

OK
AT+CIFSR

192.168.2.110

OK
Getting time..
AT+CIPSTART="UDP","82.209.243.241",123


OK
AT+CIPSEND=48

> AT+CIPCLOSE
Epoch is: 0
Getting time..
AT+CIPSTART="UDP","82.209.243.241",123

wrong syntax

ERROR

SEND OK
AT+CIPSEND=48

> AT+CIPCLOSE
Epoch is: 0
Getting time..
AT+CIPSTART="UDP","82.209.243.241",123

wrong syntax

ERROR

SEND OK
AT+CIPSEND=48

> AT+CIPCLOSE
Epoch is: 0

看起來似乎並非 NTP 伺服器有問題, 而是發出 Request 沒有成功, 導致收不到 NTP 的回應所致. Why? 到底哪裡出錯?

我看了下面這篇文章, 裡面提到 NTP 的回應會大於 84 bytes, 超過序列埠緩衝器的預設值 (64 bytes), 建議放寬為 128 bytes :

Updated comment for UdpNTPClient

"NOTE: The serial buffer size must be larger than 36 + packet size
In this example we use an UDP packet of 48 bytes so the buffer must be
at least 36+48=84 bytes that exceeds the default buffer size (64).
You must modify the serial buffer size to 128
For HardwareSerial modify _SS_MAX_RX_BUFF in
Arduino\hardware\arduino\avr\cores\arduino\SoftwareSerial.h
For SoftwareSerial modify _SS_MAX_RX_BUFF in
Arduino\hardware\arduino\avr\libraries\SoftwareSerial\SoftwareSerial.h
*/"

我是使用軟體序列埠與 ESP8266 溝通, 因此要修改 SoftSerial 的預設緩衝器, 我找到下列檔案中的 _SS_MAX_RX_BUFF 常數, 並將其從 64 改為 128 :

E:\arduino-1.6.1\hardware\arduino\avr\libraries\SoftwareSerial\SoftwareSerial.h

#define _SS_MAX_RX_BUFF 128 // RX buffer size
#ifndef GCC_VERSION

但改了還是無效. Why?


參考 :


2016-06-12 補充 :

這個問題已經解決了, 詳見 :

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

2016-08-05 補充 :

關於 NTP 回應訊息處理已有新方法, 參見 :



沒有留言 :