2016年8月3日 星期三

★ 利用網頁控制 Arduino (三) : 使用超連結與按鈕

周末的這兩天按照  "Arduino Cookbook 錦囊妙計" 這本書的 15.7~15.9 節的內容測試了透過瀏覽器網址列去控制 Arduino 輸出腳位狀態的實驗, 雖然是很基礎的測試, 但因為書本上關於利用網路遙控 Arduino 的描述大多是針對 Ethernet 乙太網或 Wiznet Wifi 擴充板, 沒有關於 ESP8266 的範例, 所以就盲修瞎練一番, 還真的能用哩! 參考 :

利用網頁控制 Arduino (一)
# 利用網頁控制 Arduino (二)

試過使用 WeeESP8266 函式庫與直接操控序列埠兩種方式, 總結來說還是直接操作序列埠比較好, 因為它在記憶體耗用上較不嚴重. 接下來要測試直接在網頁上按按鈕或超連結來控制 Arduino, 因為在網址列輸入網址實在太不文明了.

下面範例 1 是參考 "Arduino Cookbook 錦囊妙計" 這本書的 15.10 節的範例改編而成, 原範例是以提交按鈕使用 POST 方法來傳送表單資料, 以便控制 D8 腳位的 HIGH/LOW 狀態. 但因為我的 IOT 模組使用 D7, D8 當作 Arduino 與 ESP8266 之間的軟體序列埠連接, 所以改為設定 D2 腳位的狀態. 其次, 我想稍安勿躁, 先用超連結以 GET 方法來傳送 HTTP 要求, 後續再來研究如何用 POST 方法來傳送. 使用 GET 方法當然可以用表單, 但最簡單的方式是採用超連結, 格式如下 :

<a href='/?pinD2=0'>OFF</a>
<a href='/?pinD2=1'>ON</a>

按超連結時會向此網站根目錄要求 /?pinD2=0 或 /?pinD2=1 的資源, 相當於在網址列輸入 192.168.x.x/?pinD2=x 的效果.

完整程式如下 :

測試 1 :

#include <SoftwareSerial.h>
#define DEBUG true

SoftwareSerial esp8266(7,8); //(RX,TX)
const String ssid="H30-L02-webbot";
const String pwd="1234567890";
const int MAX_PAGE_NAME_LEN=8;  //buffer size
char buffer[MAX_PAGE_NAME_LEN + 1]; //store page_name

void setup() {
  Serial.begin(9600);
  esp8266.begin(9600);
  sendData(F("AT+RST\r\n"),2000,DEBUG); // reset ESP8266
  sendData(F("AT+CWMODE=1\r\n"),1000,DEBUG); //configure as station
  sendData(F("AT+CIPMUX=1\r\n"),1000,DEBUG); //enable multiple connections
  sendData(F("AT+CIPSERVER=1,80\r\n"),2000,DEBUG); //turn on server 80 port
  while (!connectWifi(ssid, pwd)) {
    Serial.println(F("Connecting WiFi ... failed"));
    delay(2000);
    }
  sendData(F("AT+GMR\r\n"),1000,DEBUG);
  delay(3000); //wait for wifi connection to get local ip
  sendData(F("AT+CIFSR\r\n"),1000,DEBUG); //get ip address
  }

void loop() {
  if (esp8266.available()) { // check if esp8266 is sending message
    if (esp8266.find("+IPD,")) {
      delay(1000); //waiting for response: 0,234:GET /?pinD2=1
      int connectionId=esp8266.read()-48;  //turn ASCII to number
      bool on=false;
      if (esp8266.find("GET /?pin")) { //retrieve page name (router)
        char type=(char)esp8266.read(); //read char after ?pin
        int pin=esp8266.parseInt(); //get first int number from serial
        int val=esp8266.parseInt(); //get first int number from serial
        if (val==1) {on=true;}
        if (type=='D') { //update output status
          Serial.print(F("Digital pin "));
          Serial.print(pin);
          Serial.print(F(" is changed to "));
          Serial.println(val);            
          pinMode(pin, OUTPUT); //set pin as ouput
          digitalWrite(pin, val);
          }
        }
      String webpage=F("<html>\r\n");
      webpage += F("<body>\r\n");
      webpage += F("<h3>Click links to turn D2 pin ON or OFF</h3>\r\n");    
      webpage += F("<a href='/?pinD2=1'>ON</a>\r\n");
      webpage += F("<a href='/?pinD2=0'>OFF</a>\r\n");
      webpage += F("State : ");
      if (digitalRead(2) == 1) {webpage += "ON\r\n";}
      else {webpage += "OFF\r\n";}        
      webpage += F("</body>\r\n");
      webpage += F("</html>");
      String cipSend=F("AT+CIPSEND=");
      cipSend += connectionId;
      cipSend += ",";
      cipSend += webpage.length();
      cipSend += F("\r\n");
      sendData(cipSend,1000,DEBUG);
      sendData(webpage,3000,DEBUG);
      delay(1); //for client to receive
      sendData("AT+CIPCLOSE=" + (String)connectionId + "\r\n",3000,DEBUG);        
      }
    }
  }

boolean connectWifi(String ssid, String pwd) {
  String res=sendData("AT+CWJAP=\"" + ssid + F("\",\"") + pwd + F("\"\r\n"),8000,DEBUG);
  res.replace("\r\n",""); //remove all line terminator
  if (res.indexOf("OK") != -1) {return true;}
  else {return false;}
  }

String sendData(String command, const int timeout, boolean debug) {
  String res=F("");
  esp8266.print(command);
  long int time=millis();
  while ((time + timeout) > millis()) {
    while(esp8266.available()) {res.concat((char)esp8266.read());}
    }
  if (debug) {Serial.print(res);}
  return res;
  }

此程式會從軟體序列埠讀取 HTTP 要求標頭裡的資源路徑, 因此我們只要從序列埠緩衝器裡搜尋 "GET /?pin" 即可找到後面的腳位資料 D2=0 或 1, 用 read() 讀取腳位類型 (type), 再用 parsInt() 讀取腳位編號 pin 與設定值 val. 因為超連結固定送出 D2, 所以這裡不需要去判別是否 pin=2.

使用筆電的 Firefox 瀏覽器透過同一個無線基地台連線 Arduino+ESP8266 伺服器, 同時按下 F12 進入開發模式, 首先只輸入 IP 的話, 因為沒有腳位設定資料,  所以就直接顯示網頁如下 :

按下 ON 超連結時會將 D2 設為 HIGH :

按下 OFF 超連結則將 D2 設為 LOW :

如果有按下 F12, 切到 "網路" 會在底下會看到 GET 方法的 Request :


在右下角的 "檔頭" 可看到 HTTP Request 的標頭訊息, 由於我們的程式中沒有回應 200 OK, 所以不會顯示回應狀態 :


切到 "回應" 會顯示伺服器回應之 HTML 碼內容 :


如果要回應較完整的 HTTP 訊息 (例如回應狀態) 與標準 HTML5 碼 (例如顯示標題, 以及中文等), 則需要傳送較多的資料, 在趙英傑寫的 "超圖解 Arduino 互動設計入門 2" 這本書的第 15, 16 兩章有關於 HTTP 協定與 HTML 語法的扼要介紹. 不過這本書使用以 Wiznet 乙太網晶片為對象的 Webduino 函式庫, 沒辦法用在 ESP8266. 雖然如此, 這本書對於網頁前後端互動解說頗詳盡, 值得參考.

我修改測試 1 裡的回應網頁, 送出標準的 HTTP 標頭, 並加上 charset=utf-8 的 meta 元素以便能正常顯示繁體中文, 完整程式如下 :

測試 2 : 

#include <SoftwareSerial.h>
#define DEBUG true

SoftwareSerial esp8266(7,8); //(RX,TX)
const String ssid="H30-L02-webbot";
const String pwd="1234567890";
const int MAX_PAGE_NAME_LEN=8;  //buffer size
char buffer[MAX_PAGE_NAME_LEN + 1]; //store page_name

void setup() {
  Serial.begin(9600);
  esp8266.begin(9600);
  sendData(F("AT+RST\r\n"),2000,DEBUG); // reset ESP8266
  sendData(F("AT+CWMODE=1\r\n"),1000,DEBUG); //configure as station
  sendData(F("AT+CIPMUX=1\r\n"),1000,DEBUG); //enable multiple connections
  sendData(F("AT+CIPSERVER=1,80\r\n"),2000,DEBUG); //turn on server 80 port
  while (!connectWifi(ssid, pwd)) {
    Serial.println(F("Connecting WiFi ... failed"));
    delay(2000);
    }
  sendData(F("AT+GMR\r\n"),1000,DEBUG);
  delay(3000); //wait for wifi connection to get local ip
  sendData(F("AT+CIFSR\r\n"),1000,DEBUG); //get ip address
  }

void loop() {
  if (esp8266.available()) { // check if esp8266 is sending message
    if (esp8266.find("+IPD,")) {
      delay(1000); //waiting for response: 0,234:GET /?pinD2=1
      int connectionId=esp8266.read()-48;  //turn ASCII to number
      bool on=false;
      if (esp8266.find("GET /?pin")) { //retrieve page name (router)
        char type=(char)esp8266.read(); //read char after ?pin
        int pin=esp8266.parseInt(); //get first int number from serial
        int val=esp8266.parseInt(); //get first int number from serial
        if (val==1) {on=true;}
        if (type=='D') { //update output status
          Serial.print(F("Digital pin "));
          Serial.print(pin);
          Serial.print(F(" is changed to "));
          Serial.println(val);            
          pinMode(pin, OUTPUT); //set pin as ouput
          digitalWrite(pin, val);
          }
        }
      String webpage=F("HTTP/1.1 200 OK\r\n");
      webpage += F("Content-Type: text/html\r\n\r\n");
      webpage += F("\r\n");  //this new line is a must
      webpage += F("<!doctype html>\r\n");
      webpage += F("<html>\r\n");
      webpage += F("<head>\r\n");
      webpage += F("<meta charset='utf-8'>\r\n");
      webpage += F("<title>Arduino+ESP8266 網頁控制</title>\r\n");    
      webpage += F("</head>\r\n");
      webpage += F("<body>\r\n");
      webpage += F("<h3>按超連結開啟或關閉 D2 腳位之輸出</h3>\r\n");
      webpage += F("<a href='/?pinD2=1'>開啟</a>\r\n");
      webpage += F("<a href='/?pinD2=0'>關閉</a>\r\n");
      webpage += F("狀態 : ");
      if (digitalRead(2) == 1) {webpage += "開啟\r\n";}
      else {webpage += "關閉\r\n";}    
      webpage += F("</body>\r\n");
      webpage += F("</html>");
      String cipSend=F("AT+CIPSEND=");
      cipSend += connectionId;
      cipSend += ",";
      cipSend += webpage.length();
      cipSend += F("\r\n");
      sendData(cipSend,1000,DEBUG);
      sendData(webpage,3000,DEBUG);
      delay(1); //for client to receive
      sendData("AT+CIPCLOSE=" + (String)connectionId + "\r\n",3000,DEBUG);        
      }
    }
  }

boolean connectWifi(String ssid, String pwd) {
  String res=sendData("AT+CWJAP=\"" + ssid + F("\",\"") + pwd + F("\"\r\n"),8000,DEBUG);
  res.replace("\r\n",""); //remove all line terminator
  if (res.indexOf("OK") != -1) {return true;}
  else {return false;}
  }

String sendData(String command, const int timeout, boolean debug) {
  String res=F("");
  esp8266.print(command);
  long int time=millis();
  while ((time + timeout) > millis()) {
    while(esp8266.available()) {res.concat((char)esp8266.read());}
    }
  if (debug) {Serial.print(res);}
  return res;
  }

注意. 在 HTTP 標頭與網頁內容之間要有一個空行 ("\r\n"), 這是 HTTP 協定的格式. 程式上傳後用筆電瀏覽器連線 Arduino 伺服器, 果真不一樣了, 變成中文介面, 不僅頁面 :


由於伺服器回應訊息最前面我們添加了 "HTTP/1.1 200 OK", 這時用 Firefox 的 F12 開發模式可以看到 HTTP 標頭出現回應狀態 200 OK :


回應的 HTML5 網頁原始碼如下 :


以上是使用超連結向遠端伺服器請求資源, 超連結用的是 HTTP 的 GET 方法. 也可以使用表單元素來提出 HTTP 請求, 理論上可以使用 GET 或 POST 方法, 例如  "Arduino Cookbook 錦囊妙計" 這本書的 15.10 節就是使用 POST 方法.

但我測試的結果是, 最好使用 GET 方法, 因為所請求的資源路徑與參數就放在 HTTP 標頭的第一行, 一定可以裝進緩衝區裡面, 所以程式可以讀取到資源路徑以及傳遞之參數以便判斷要做何處理. 反之, 若使用 POST 方法的話, 參數是放在 HTTP 後面的 Body 而非標頭部分, 如果標頭很長 (電腦瀏覽器通常是這樣), 那麼可能就會裝不進緩衝器內, 導致程式讀不到所需之參數, 這樣就沒辦法控制輸出埠了.

下面測試 3 便是將測試 2 改成用表單方式以 GET 方法發出請求 :

測試 3 : 

#include <SoftwareSerial.h>
#define DEBUG true

SoftwareSerial esp8266(7,8); //(RX,TX)
const String ssid="H30-L02-webbot";
const String pwd="1234567890";
const int MAX_PAGE_NAME_LEN=8;  //buffer size
char buffer[MAX_PAGE_NAME_LEN + 1]; //store page_name

void setup() {
  Serial.begin(9600);
  esp8266.begin(9600);
  sendData(F("AT+RST\r\n"),2000,DEBUG); // reset ESP8266
  sendData(F("AT+CWMODE=1\r\n"),1000,DEBUG); //configure as station
  sendData(F("AT+CIPMUX=1\r\n"),1000,DEBUG); //enable multiple connections
  sendData(F("AT+CIPSERVER=1,80\r\n"),2000,DEBUG); //turn on server 80 port
  while (!connectWifi(ssid, pwd)) {
    Serial.println(F("Connecting WiFi ... failed"));
    delay(2000);
    }
  sendData(F("AT+GMR\r\n"),1000,DEBUG);
  delay(3000); //wait for wifi connection to get local ip
  sendData(F("AT+CIFSR\r\n"),1000,DEBUG); //get ip address
  }

void loop() {
  if (esp8266.available()) { // check if esp8266 is sending message
    if (esp8266.find("+IPD,")) {
      delay(1000); //waiting for response: 0,234:GET /?pinD2=1
      int connectionId=esp8266.read()-48;  //turn ASCII to number
      bool on=false; //default : off
      if (esp8266.find("GET /?pin")) { //retrieve page name (router)
        char type=(char)esp8266.read(); //read char after ?pin
        int pin=esp8266.parseInt(); //get first int number from serial
        int val=esp8266.parseInt(); //get first int number from serial
        if (val==1) {on=true;}
        if (type=='D') { //update output status
          Serial.print(F("Digital pin "));
          Serial.print(pin);
          Serial.print(F(" is changed to "));
          Serial.println(val);          
          pinMode(pin, OUTPUT); //set pin as ouput
          digitalWrite(pin, val);
          }
        }
      String webpage=F("HTTP/1.1 200 OK\r\n");
      webpage += F("Content-Type: text/html\r\n\r\n");
      webpage += F("\r\n");
      webpage += F("<!doctype html>\r\n");
      webpage += F("<html>\r\n");
      webpage += F("<head>\r\n");
      webpage += F("<meta charset='utf-8'>\r\n");
      webpage += F("<title>Arduino+ESP8266 網頁控制</title>\r\n");    
      webpage += F("</head>\r\n");
      webpage += F("<body>\r\n");
      webpage += F("<h3>按按鈕開啟或關閉 D2 腳位之輸出</h3>\r\n");
      webpage += F("<form action='/' method='GET'>\r\n");
      webpage += F("<input type='hidden' name='pinD2' value='1'>\r\n");         
      webpage += F("<input type='submit' value='開啟'>\r\n");
      webpage += F("</form>\r\n");
      webpage += F("<form action='/' method='GET'>\r\n");
      webpage += F("<input type='hidden' name='pinD2' value='0'>\r\n");         
      webpage += F("<input type='submit' value='關閉'>\r\n");
      webpage += F("</form>\r\n"); 
      webpage += F("狀態 : ");
      if (digitalRead(2) == 1) {webpage += "開啟\r\n";}
      else {webpage += "關閉\r\n";}    
      webpage += F("</body>\r\n");
      webpage += F("</html>");
      String cipSend=F("AT+CIPSEND=");
      cipSend += connectionId;
      cipSend += ",";
      cipSend += webpage.length();
      cipSend += F("\r\n");
      sendData(cipSend,1000,DEBUG);
      sendData(webpage,5000,DEBUG);
      delay(1); //for client to receive
      sendData("AT+CIPCLOSE=" + (String)connectionId + "\r\n",3000,DEBUG);        
      }
    }
  }

boolean connectWifi(String ssid, String pwd) {
  String res=sendData("AT+CWJAP=\"" + ssid + F("\",\"") + pwd + F("\"\r\n"),8000,DEBUG);
  res.replace("\r\n",""); //remove all line terminator
  if (res.indexOf("OK") != -1) {return true;}
  else {return false;}
  }

String sendData(String command, const int timeout, boolean debug) {
  String res=F("");
  esp8266.print(command);
  long int time=millis();
  while ((time + timeout) > millis()) {
    while(esp8266.available()) {res.concat((char)esp8266.read());}
    }
  if (debug) {Serial.print(res);}
  return res;
  }

此程式只是將測試 2 的超連結改成 form 表單而已, 超連結變成提交 submit 按鈕, 而要傳遞的參數則是放在隱藏元素裡, 其名稱 name 即為腳位名稱 pinD2, 而 value 屬性就是其值, 0 或 1. 下面是執行結果  (我改換了另外一個 IOT 模組, 所以 IP 變成 192.168.2.151), 按下關閉鈕狀態變成關閉 :

按下開啟鈕狀態變成開啟 : 

傳出 pinD2 參數, 其值為 1 :


回應的網頁原始碼 :


我們可以修改上面測試 3 的程式來觀察 GET 方法傳送的 HTTP 訊息內容, 將 loop() 的內容改換為如下測試 4  程式 :

測試 4 :

#include <SoftwareSerial.h>
#define DEBUG true

SoftwareSerial esp8266(7,8); //(RX,TX)
const String ssid="H30-L02-webbot";
const String pwd="1234567890";
const int MAX_PAGE_NAME_LEN=8;  //buffer size
char buffer[MAX_PAGE_NAME_LEN + 1]; //store page_name

void setup() {
  Serial.begin(9600);
  esp8266.begin(9600);
  sendData(F("AT+RST\r\n"),2000,DEBUG); // reset ESP8266
  sendData(F("AT+CWMODE=1\r\n"),1000,DEBUG); //configure as station
  sendData(F("AT+CIPMUX=1\r\n"),1000,DEBUG); //enable multiple connections
  sendData(F("AT+CIPSERVER=1,80\r\n"),2000,DEBUG); //turn on server 80 port
  while (!connectWifi(ssid, pwd)) {
    Serial.println(F("Connecting WiFi ... failed"));
    delay(2000);
    }
  sendData(F("AT+GMR\r\n"),1000,DEBUG);
  delay(3000); //wait for wifi connection to get local ip
  sendData(F("AT+CIFSR\r\n"),1000,DEBUG); //get ip address
  }

void loop() {
  if (esp8266.available()) { // check if esp8266 is sending message
    Serial.print((char)esp8266.read());
    }
  }

boolean connectWifi(String ssid, String pwd) {
  String res=sendData("AT+CWJAP=\"" + ssid + F("\",\"") + pwd + F("\"\r\n"),8000,DEBUG);
  res.replace("\r\n",""); //remove all line terminator
  if (res.indexOf("OK") != -1) {return true;}
  else {return false;}
  }

String sendData(String command, const int timeout, boolean debug) {
  String res=F("");
  esp8266.print(command);
  long int time=millis();
  while ((time + timeout) > millis()) {
    while(esp8266.available()) {res.concat((char)esp8266.read());}
    }
  if (debug) {Serial.print(res);}
  return res;
  }

這裡很簡單就是印出序列埠全部收到的字元, 而電腦的瀏覽器仍然停留在測試 3 的網頁, 這時按下關閉或開啟鈕, 就會在序列埠監控視窗看到瀏覽器所傳送的 HTTP 訊息全貌 :

Link


+IPD,1,359:GET /?pinD2=1 HTTP/1.1
Host: 192.168.43.151
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:46.0) Gecko/20100101 Firefox/46.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-TW,zh;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Referer: http://192.168.43.151/?pinD2=1
Connection: keep-alive


OK
Unlink

可見我的筆電 Chrome 瀏覽器傳送的 HTTP 標頭非常長, 但不論用表單 GET 或超連結, 傳送給 Arduino 伺服器的 HTTP 訊息中, 參數都是在標頭第一行的 "GET /" 後面, 所以要擷取處理都很方便. 如果改用 POST 方法提出請求, 那麼標頭就會以 POST 開頭, 變成 "POST / HTTP/1.1", 而參數會放在標頭結束後的空行後面. 若將上面測試 3 中的表單改成以 POST 提出請求, 那麼由於標頭太長的緣故, 我們將難以取出放在最後面的參數, 如下列測試 5 所示 :

測試 5 :

#include <SoftwareSerial.h>
#define DEBUG true

SoftwareSerial esp8266(7,8); //(RX,TX)
const String ssid="H30-L02-webbot";
const String pwd="1234567890";
const int MAX_PAGE_NAME_LEN=8;  //buffer size
char buffer[MAX_PAGE_NAME_LEN + 1]; //store page_name

void setup() {
  Serial.begin(9600);
  esp8266.begin(9600);
  sendData(F("AT+RST\r\n"),2000,DEBUG); // reset ESP8266
  sendData(F("AT+CWMODE=1\r\n"),1000,DEBUG); //configure as station
  sendData(F("AT+CIPMUX=1\r\n"),1000,DEBUG); //enable multiple connections
  sendData(F("AT+CIPSERVER=1,80\r\n"),2000,DEBUG); //turn on server 80 port
  while (!connectWifi(ssid, pwd)) {
    Serial.println(F("Connecting WiFi ... failed"));
    delay(2000);
    }
  sendData(F("AT+GMR\r\n"),1000,DEBUG);
  delay(3000); //wait for wifi connection to get local ip
  sendData(F("AT+CIFSR\r\n"),1000,DEBUG); //get ip address
  }

void loop() {
  if (esp8266.available()) { // check if esp8266 is sending message
    if (esp8266.find("+IPD,")) {
      delay(1000); //waiting for response: 0,234:GET /?pinD2=1
      int connectionId=esp8266.read()-48;  //turn ASCII to number
      bool on=false; //default : off
      if (esp8266.findUntil("pinD", "\r\n")) {
        char type=(char)esp8266.read(); //read char after ?pin
        int pin=esp8266.parseInt(); //get first int number from serial
        int val=esp8266.parseInt(); //get first int number from serial
        if (val==1) {on=true;}
        if (type=='D') { //update output status
          Serial.print(F("Digital pin "));
          Serial.print(pin);
          Serial.print(F(" is changed to "));
          Serial.println(val);          
          pinMode(pin, OUTPUT); //set pin as ouput
          digitalWrite(pin, val);
          }
        }
      String webpage=F("HTTP/1.1 200 OK\r\n");
      webpage += F("Content-Type: text/html\r\n\r\n");
      webpage += F("\r\n");
      webpage += F("<!doctype html>\r\n");
      webpage += F("<html>\r\n");
      webpage += F("<head>\r\n");
      webpage += F("<meta charset='utf-8'>\r\n");
      webpage += F("<title>Arduino+ESP8266 網頁控制</title>\r\n");    
      webpage += F("</head>\r\n");
      webpage += F("<body>\r\n");
      webpage += F("<h3>按按鈕開啟或關閉 D2 腳位之輸出</h3>\r\n");
      webpage += F("<form action='/' method='POST'>\r\n");
      webpage += F("<input type='hidden' name='pinD2' value='1'>\r\n");         
      webpage += F("<input type='submit' value='開啟'>\r\n");
      webpage += F("</form>\r\n");
      webpage += F("<form action='/' method='POST'>\r\n");
      webpage += F("<input type='hidden' name='pinD2' value='0'>\r\n");         
      webpage += F("<input type='submit' value='關閉'>\r\n");
      webpage += F("</form>\r\n"); 
      webpage += F("狀態 : ");
      if (digitalRead(2) == 1) {webpage += "開啟\r\n";}
      else {webpage += "關閉\r\n";}    
      webpage += F("</body>\r\n");
      webpage += F("</html>");
      String cipSend=F("AT+CIPSEND=");
      cipSend += connectionId;
      cipSend += ",";
      cipSend += webpage.length();
      cipSend += F("\r\n");
      sendData(cipSend,1000,DEBUG);
      sendData(webpage,5000,DEBUG);
      delay(1); //for client to receive
      sendData("AT+CIPCLOSE=" + (String)connectionId + "\r\n",3000,DEBUG);        
      }
    }
  }

boolean connectWifi(String ssid, String pwd) {
  String res=sendData("AT+CWJAP=\"" + ssid + F("\",\"") + pwd + F("\"\r\n"),8000,DEBUG);
  res.replace("\r\n",""); //remove all line terminator
  if (res.indexOf("OK") != -1) {return true;}
  else {return false;}
  }

String sendData(String command, const int timeout, boolean debug) {
  String res=F("");
  esp8266.print(command);
  long int time=millis();
  while ((time + timeout) > millis()) {
    while(esp8266.available()) {res.concat((char)esp8266.read());}
    }
  if (debug) {Serial.print(res);}
  return res;
  }

注意, 此處兩個表單的 method 為 POST, 而 HTTP 解析則用 findUtil() 去尋找 "pinD" 直到該行行尾, 但是實際上用瀏覽器測試發現, 上面的程式無法正常運作, 不論按開啟或關閉都無法改變 D2 腳位的狀態, 可見參數 pinD 並沒有被找到. 可能是 Arduino 這個 8 位元的小腦袋沒辦法處理這麼長的標頭? 同樣瀏覽器保留測試 5 程式的網頁, 改上傳上面測試 4 的程式, 然後按下開啟或關閉按鈕, 就可以在序列埠監控視窗看到 POST 方法完整的 HTTP 訊息 :

Link

+IPD,0,419:POST / HTTP/1.1
Host: 192.168.43.151
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:46.0) Gecko/20100101 Firefox/46.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-TW,zh;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Referer: http://192.168.43.151/
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded
Content-Length: 7

pinD2=1
OK

這裡可以清楚看出 HTTP 協定格式, 在長長的標頭結束後必須空一行, 然後才是 Body 部分, 也就是所傳送的參數. 把 +IPD 到 pinD2=1 複製到 WORD 中統計字元數, 總共是 408 個字元 :


而軟體序列埠預設為 64 Bytes. 到 Arduino IDE 的安裝目錄下找到軟體序列埠函式庫檔案 SoftwareSerial.h, 路徑如下 :

D:\arduino-1.6.6\hardware\arduino\avr\libraries\SoftwareSerial

將讀取緩衝器常數 _SS_MAX_RX_BUFF 從預設的 64 改為 512 :

#define _SS_MAX_RX_BUFF 512 // RX buffer size

重開 Arduino IDE, 重新上傳測試 5 的程式後, 以筆電連線 Arduino 伺服器, 結果這回瀏覽器反而一直轉, 最後顯示連線失敗, 可見伺服器沒有回應 (連 200 OK 也沒有), 更慘.

序列埠監控視窗擷取訊息如下 :

AT+GMR

0018000902

OK
AT+CIFSR

192.168.43.151

OK
Host: 192.168.43.151
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:46.0) Gecko/20100101 Firefox/46.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-TW,zh;q=0.8,en-US;q=0.5,en;q=0.AT+CIPSEND=0,504

> AT+CIPCLOSE=0

看起來 Arduino 有將 504 字元的網頁傳送給 ESP8266, 但還沒傳送完就送 AT+CIPCLOSE 了. 我有將 timeout 延長為 10 秒還是沒有用, 可見雖然緩衝區調大了, 但是 Arduino 動態記憶體只有 2KB 而已, 加大緩衝區反而使記憶體耗用大增, 可能因為這樣使效能變低, 導致程式運作不正常.

以上測試說明, Arduino 只是一個 8 位元微控器, 用來讀取感測器控制周邊非常適合, 但拿它來當網頁伺服器就太為難它了, 就算要用, 網頁也要盡量簡單就好. 所以接下來的測試我要回歸上面測試 1 的最簡 HTML 模式, 就是不送回應標頭, 網頁也是純英文, 讓 Arduino 需要傳送的字元數降到最低 (我連讓網頁原始碼較容易看得跳行 \r\n 都省了). 同時軟體序列埠也調回預設的 64 Bytes.

上面的測試都只針對 D2, 接下來我想擴大為可在網頁上控制 Arduino 的全部數位腳的輸出, 如測試 6 所示 :

測試 6 :

#include <SoftwareSerial.h>
#define DEBUG true

SoftwareSerial esp8266(7,8); //(RX,TX)
const String ssid="H30-L02-webbot";
const String pwd="1234567890";
const int MAX_PAGE_NAME_LEN=8;  //buffer size
char buffer[MAX_PAGE_NAME_LEN + 1]; //store page_name

void setup() {
  Serial.begin(9600);
  esp8266.begin(9600);
  sendData(F("AT+RST\r\n"),2000,DEBUG); // reset ESP8266
  sendData(F("AT+CWMODE=1\r\n"),1000,DEBUG); //configure as station
  sendData(F("AT+CIPMUX=1\r\n"),1000,DEBUG); //enable multiple connections
  sendData(F("AT+CIPSERVER=1,80\r\n"),2000,DEBUG); //turn on server 80 port
  while (!connectWifi(ssid, pwd)) {
    Serial.println(F("Connecting WiFi ... failed"));
    delay(2000);
    }
  sendData(F("AT+GMR\r\n"),1000,DEBUG);
  delay(3000); //wait for wifi connection to get local ip
  sendData(F("AT+CIFSR\r\n"),1000,DEBUG); //get ip address
  }

void loop() {
  if (esp8266.available()) { // check if esp8266 is sending message
    if (esp8266.find("+IPD,")) {
      delay(1000); //waiting for response: 0,234:GET /?pinD2=1
      int connectionId=esp8266.read()-48;  //turn ASCII to number
      bool on=false;
      if (esp8266.find("GET /?pin")) { //retrieve page name (router)
        char type=(char)esp8266.read(); //read char after ?pin
        int pin=esp8266.parseInt(); //get first int number from serial
        int val=esp8266.parseInt(); //get first int number from serial
        if (val==1) {on=true;}
        if (type=='D') { //update output status
          Serial.print(F("Digital pin "));
          Serial.print(pin);
          Serial.print(F(" is changed to "));
          Serial.println(val);            
          pinMode(pin, OUTPUT); //set pin as ouput
          digitalWrite(pin, val);
          }
        }
      String webpage=F("<html><body>");    
      for (byte i=2; i<=6; i++) {
        webpage += F("D");
        webpage += i;
        webpage += F(" <a href='/?pinD");
        webpage += i;
        webpage += F("=1'>ON</a>");
        webpage += F(" <a href='/?pinD");
        webpage += i;
        webpage += F("=0'>OFF</a>");
        if (digitalRead(i) == HIGH) {webpage += F(" ON");}
        else if (digitalRead(i) == LOW) {webpage += F(" OFF");}
        webpage += F("<br>");
        }
      webpage += F("</body></html>");
      String cipSend=F("AT+CIPSEND=");
      cipSend += connectionId;
      cipSend += ",";
      cipSend += webpage.length();
      cipSend += F("\r\n");
      sendData(cipSend,1000,DEBUG);
      sendData(webpage,3000,DEBUG);
      delay(1);
      sendData("AT+CIPCLOSE=" + (String)connectionId + "\r\n",3000,DEBUG);        
      }
    }
  }

boolean connectWifi(String ssid, String pwd) {
  String res=sendData("AT+CWJAP=\"" + ssid + F("\",\"") + pwd + F("\"\r\n"),8000,DEBUG);
  res.replace("\r\n",""); //remove all line terminator
  if (res.indexOf("OK") != -1) {return true;}
  else {return false;}
  }

String sendData(String command, const int timeout, boolean debug) {
  String res=F("");
  esp8266.print(command);
  long int time=millis();
  while ((time + timeout) > millis()) {
    while(esp8266.available()) {res.concat((char)esp8266.read());}
    }
  if (debug) {Serial.print(res);}
  return res;
  }

此程式中使用迴圈來掃描數位針腳的狀態, 並製作 ON/OFF 的超連結來改變其輸出狀態. 用瀏覽器連線伺服器結果如下 :

這裡只顯示 D2~D6, 原因是若再增加數位腳, 輸出網頁內容增多會可能會使伺服器超載, 導致網頁出不來. 這在在說明 Arduino 當伺服器實在是太操了, 它還是較適合當終端控制器. 總之, 這一系列測試下來, 結論是 :
  1. 用 GET 提出 HTTP 要求, 不要用 POST
  2. Arduino 在 ESP8266 幫忙下當 Server 是可以的, 但效能僅僅是堪用而已. 它比較適合當 Client.


4 則留言 :

梁淑媛 提到...

謝謝分享!已經成功抓到NTP的時間。
Sending request to NTP server ...
AT+CIPSTART="UDP","91.226.136.136",123

CONNECT

OK
AT+CIPSEND=48


OK
>
'+IPD,48:' found, NTP server answered :

24 02 06 ED 00 00 00 34 00 00
06 AB 75 36 C1 B9 DB 50 03 4E
94 58 6C 8C 00 00 00 00 00 00
00 00 DB 50 04 BA DD B4 E1 C8
DB 50 04 BA DD BB 70 9E !!!!!!!!!!!!!!!AT+CIPCLOSE

CLOSED

OK


NTP time stamp packets (byte 32~35)=DB 50 4 BA
NTP time stamp (seconds since 1900-01-01)=3679454394
Unix time stamp (seconds since 1970-01-01)=1470465594
2016-08-06 14:39:54
2016-08-06 14:39:55
2016-08-06 14:39:56
2016-08-06 14:39:57
2016-08-06 14:39:58
2016-08-06 14:39:59
2016-08-06 14:40:00
2016-08-06 14:40:01
2016-08-06 14:40:02
2016-08-06 14:40:03
2016-08-06 14:40:04

小狐狸事務所 提到...

OK!

梁淑媛 提到...

Dear Tony,

Thanks for your sharing information.
大部分時間是正常,可是有時候還是沒有收到資料,就會顯示,下一次更新就會恢復正常。

NTP time stamp packets (byte 32~35)=0 0 0 0
NTP time stamp (seconds since 1900-01-01)=0
Unix time stamp (seconds since 1970-01-01)=2085978496
2036-02-07 14:28:16 Thu
2036-02-07 14:28:17 Thu
2036-02-07 14:28:18 Thu
2036-02-07 14:28:19 Thu
2036-02-07 14:28:20 Thu

2036-02-07 14:29:13 Thu
2036-02-07 14:29:14 Thu
2036-02-07 14:29:15 Thu
AT+CIPSTART="UDP","91.226.136.136",123

CONNECT

OK
AT+CIPSEND=48


OK
>
'+IPD,48:' found, NTP server answered :

24 02 06 ED 00 00 00 28 00 00
08 44 FA 3D D8 B4 DB 51 76 AE
95 F4 37 2F 00 00 00 00 00 00
00 00 DB 51 7C 4B 1C F9 7E EB
DB 51 7C 4B 1D 01 D9 46 !!!!!!!!!!!!!!!AT+CIPCLOSE

CLOSED

OK


NTP time stamp packets (byte 32~35)=DB 51 7C 4B
NTP time stamp (seconds since 1900-01-01)=3679550539
Unix time stamp (seconds since 1970-01-01)=1470561739
2016-08-07 17:22:19 Sun
2016-08-07 17:22:20 Sun
2016-08-07 17:22:21 Sun
2016-08-07 17:22:22 Sun
2016-08-07 17:22:23 Sun

小狐狸事務所 提到...

是的沒錯, 這不一定是 NTP 沒回應, 畢竟 UDP 傳輸不保證到達, 掉封包也不會重傳. 可以這麼做, 如果收到時戳是 00000000, 那麼就不要去更新 Arduino 系統時鐘即可.