2016年5月31日 星期二

AllAboutEE 的 ESP8266 伺服器測試 (二)

久攻不下的 ESP8266 伺服器測試終於在昨晚搞定了! 弄到 12 點多確定成功了就大笑幾聲, 滿意的進入夢鄉. 晚上回到家又重做一遍, 確定沒問題就可以來寫測試紀錄啦! 硬體接線此處不再重貼, 參考本篇測試紀錄的前篇暖身測試 :

# AllAboutEE 的 ESP8266 伺服器測試 (一)

此測試的目的, 是要演示是否可從手機的瀏覽器 (或許以後可以設計一個 App 更好), 設定 Arduino+ESP8266 模組的 Wifi 上網環境. 會有這個想法, 來自於過去一段時間玩 ESP8266 這個 CP 值超高的 Wifi 模組時, 發現要在 Arduino 程式中設定 ESP8266 連上家中的無線基地台, 讓 Arduino 連上物聯網服務並不難, 就是在 setup() 函數中用 AT+CWJAP 這個 AT 指令來設定即可.

問題是, 以我的情況而言, 我每周要來往都市的工寮與鄉下的老家, 兩個地方都有 Wifi, 但這兩台無線基地台的 SSID 與密碼都不同, 基於不可救藥的念舊, 我都沒想要將他們改為一致的衝動. 結果是, 當我把 Arduino+ESP8266 模組帶回去鄉下做實驗時, 就要修改程式, 換成連線鄉下AP 的 SSID. 同樣地, 回到高雄時又要改, 真的很煩.

對會寫程式的人而言就只是煩而已, 但對使用者而言就頭大了, 我們不可能要求使用者自己下載 Arduino IDE 自己改程式吧! 但是, 難道要裝個 1602 顯示模組跟小鍵盤來讓使用者輸入嗎? SSID 是英文怎麼辦? 後來接觸到 Webduino 這個以 Web 技術來開發 Arduino 創意的產品, 受其採用手機為輸出入介面的做法感到振奮, 沒錯, 手機就是一應具全的超完美輸出入介面呀! 只要能與手機連上線, 就可以利用手機的虛擬鍵盤與螢幕來跟 Arduino+ESP8266 溝通啦!

我買了 "實戰 Webduino" 這本書來看 (也買了 Webduino 的馬克 1 號實驗板, 但是到現在都還沒空把玩), 我從書裡面揣摩它應該是將 ESP8266 設在模式 3 (AP+STA), 並啟動 80 埠網頁伺服器功能, 這樣可讓我們在 ESP8266 還沒連上家中 Wifi 之前, 可以使用手機透過其 SoftAP 的固定網址 192.168.4.1 連線 ESP8266 伺服器. 但伺服器的回應事實上是利用 Arduino 來控制的, ESP8266 只是提供 Web 伺服器功能而已. 參考 :

關於 Webduino 開發板

兩周前二哥考完會考後, 我就開始思考如何實作這種機制, 因為一想到之前寫的 ESP8266 函式庫就頭大, 因為光載入這函式庫我看起碼佔掉近 3 成記憶體. 正在煩惱怎麼做比較好時找到 AllAboutEE 所寫的 ESP8266 函式, 我覺得非常精簡好用, 於是決定重起爐灶以此為基礎來測試囉! 參考 :

How To Use the ESP8266 and Arduino as a Webserver
ESP8266 函式庫 v2

OK, 前情提要交代完, 接下來要來處理 HTTP 標頭中的路徑 (path 或 router), 來控制 wifi 設定的網頁切換. 我參考了 Oreilly 出版的 "Arduino Cookbook 錦囊妙計 第二版" 第 15-8 節 "處理特定網頁需求" (p540) 之作法, 主要利用 Serial 物件的 find() 與 readBytesUntil(), 以及 C 語言的字串處理函數 strcmp() 來擷取與剖析 HTTP 標頭中的路徑字串.

三民

此外在字串處理部分, 我也參考了下列文章 :

https://www.arduino.cc/en/Reference/Serial
Arduino: Sending and Receiving Multi-Digit Integers
SO, HOW DOES SERIAL.READBYTESUNTIL() WORK?

我的構想是以下列的網址來更新 ESP8266 的 Wifi 連線設定 :

http://192.168.4.1/update/?ssid=myssid&pwd=mypwd

而任何其他路徑就顯示 Wifi 設定畫面, 例如 :

http://192.168.4.1

前篇所述, 當使用者連線 192.168.4.1 時, ES8266 會回應如下 HTTP 標頭 (GET 以後的, 前面的 +IPD 是 ESP8266 的回應標頭, 跟著的 0 是連線通道, 362 是回應字元數) :

+IPD,0,362:GET / HTTP/1.1

這裡顯示前端瀏覽器是以 GET 方法提出網頁要求, 其路徑是第 1 個斜線 "/" 表示存取根目錄. 如果是送出 Wifi 設定要求, 那麼網址會變成 /update/?ssid=myssid&pwd=mypwd, 而 ESP8266 會回應如下 HTTP 標頭 :

+IPD,0,426:GET /update/?ssid=myssid&pwd=mypwd HTTP/1.1

我們必須從這個回應字串中取得其中第一個斜線後面的路徑字串, 如果是 update 就進入設定區塊, 然後繼續擷取問號後面的參數 ssid 與 pwd, 以便填入 AT+CWJAP 後面來設定要連線之 AP. 如果路徑不是 update, 那就顯示設定表單網頁. 我把前篇的程式修改為如下 :

#include <SoftwareSerial.h>
#define DEBUG true

SoftwareSerial esp8266(10,11); //(RX,TX)
const int MAX_PAGE_NAME_LEN=48;
char buffer[MAX_PAGE_NAME_LEN + 1];

void setup() {
  Serial.begin(9600);
  esp8266.begin(9600);
  sendData("AT+RST\r\n",2000,DEBUG); // reset module
  sendData("AT+CWMODE=3\r\n",1000,DEBUG); // configure as access point
  sendData("AT+CIFSR\r\n",1000,DEBUG); // get ip address
  sendData("AT+CIPMUX=1\r\n",1000,DEBUG); // configure for multiple connections
  sendData("AT+CIPSERVER=1,80\r\n",1000,DEBUG); // turn on server on port 80
  }

void loop() {
  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 router from remaining response
        memset(buffer, 0, sizeof(buffer));  //clear buffer (all set to 0)
        if (esp8266.find("/")) { //find page router start char
          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;
              //set joint AP
              sendData("AT+CWJAP=\"" + ssid + "\",\"" + pwd + "\"\r\n",6000,DEBUG);
              sendData("AT+CIFSR\r\n",1000,DEBUG);
              //show result
              String webpage="<html>Wifi setup OK!</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 <input name=ssid type=text><br>";
              String cipSend = "AT+CIPSEND=";
              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;
  }

在這程式中, 我指定了一個 48 個字元的暫存區 buffer, 用來儲存擷取到的網頁路徑以及參數, 48 個 Bytes 應該綽綽有餘了. 這裡要注意的是, 我為了精簡路徑長度, 將密碼欄位的 name 由前篇中的 password 改為 pwd. 而且表單內也加入 action="/update/" 屬性, 這樣提交表單時便會向 192.168.4.1 的 port 80 要求取得 /update/?ssid=myssid&pwd=mypwd 網頁了 (GET 方法會把要傳遞的參數以 ? 黏在 action 路徑後面傳送, 每個參數以 & 串接).

當我們從 ESP8266 回應的 +IPD 後面擷取出通道號碼後, 接著就用 find() 函數從剩下的回應字串中尋找 "GET " 字串 (注意 T 後面有一個空格), 這是 HTTP 標頭的開始. 找到後再往下搜尋斜線字元 "/", 找到的話表示後面接著的便是路徑了, 依續將回應字串讀取到 buffer 陣列內, 直到第二個斜線出現為止. 然後用 strcmp() 函數比對 buffer 內所儲存的是否為 "update", 是的話就進入設定區塊, 繼續往下擷取 ssid 與 pwd 參數, 否則就進入 wifi 設定表單頁面.

程式上傳 Arduino 後, 打開序列埠監控視窗, 將手機的 Wifi 功能打開, 連線 ESP_ 開頭的基地台, 這是 ESP8266 在模式 3 下所建立的基地台 :


然後打開瀏覽器, 輸入 SoftAP 的網址 192.168.4.1, Arduino 會透過 ESP8266 的伺服器回應 Wifi 設定網頁 :

輸入我家無線基地台的 ssid 與密碼後, 按 Connect 即顯示設定成功訊息 :


從下面序列埠監控視窗擷取的輸出訊息可知, 設定 Wifi 連線成功後, AT+CIFSR 指令顯示 ESP8266 的 STA 從家中無線基地台的 DHCP 獲得 192.168.2.102 這個區網 IP, 顯示此設定確實已成功地讓 ESP8266 連上家中的區網, 當然就可連到互聯網了 :

AT+RST


OK
bB�鑭b禔S��"丮B�侒��餾�
[System Ready, Vendor:www.ai-thinker.com]
AT+CWMODE=3

no change
AT+CIFSR

192.168.4.1  (SoftAP 網址)
0.0.0.0          (STA 尚未連上 wifi 基地台, 預設網址 0.0.0.0)

OK
AT+CIPMUX=1


OK
AT+CIPSERVER=1,80


OK
1.1
Host: 192.168.4.1
Connection: keep-aliveAT+CIPSEND=0,77

> <html><form method=get action='/update/'>SSID <input name=ssid
SEND OK
AT+CIPSEND=0,31

> PWD <input name=pwd type=text>
SEND OK
AT+CIPSEND=0,47

> <input type=submit value=Connect></form></html>
SEND OK
AT+CIPCLOSE=0


OK
Unlink
Link

+IPD,0,355:GET /favicon.ico HTTP/1.1
Host: 192.168.4.1
Connection: keep-alive
Accept: */*
User-Agent: Mozilla/5.0 (Linux; Android 4.4.2; H30-L02 Build/HonorH30-L02) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/30.0.0.0 Mobile Safari/537.36
Accept-Encoding: gzip,deflate
Accept-Language: zh-TW,en-US;q=0.8
X-Requested-With: com.android.browser


OK
HTTP/1.1
AT+CWJAP="EDIMAX-tony","1234567890"


OK
AT+CIFSR

192.168.4.1       (SoftAP 網址)
192.168.2.102   (連上 wifi 基地台後 DHCP 所指派的區域網址)

OK
AT+CIPSEND=1,27

> <html>Wifi setup OK!</html>
SEND OK
AT+CIPCLOSE=1


OK

感謝 AllAboutEE 的程式碼, 上面整個編譯後只佔了 27% 的程式儲存空間, 還有 70% 以上可用來開發應用 :


以上便是本次測試紀錄, 終於把 Arduino+ESP8266 模組連網的最後一道障礙拆除啦! 好了, 寫完就要睡覺去囉!

馬上補充 :

其實上面的程式可以稍微再精簡, 就是 find("GET ") 與 find("/") 可以合而為一, 這樣 if 就少一層啦!

      if (esp8266.find("GET /")) { //retrieve page router from remaining response
        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;
            //set joint AP
            sendData("AT+CWJAP=\"" + ssid + "\",\"" + pwd + "\"\r\n",6000,DEBUG);
            sendData("AT+CIFSR\r\n",1000,DEBUG);
            //Serial.print("AT+CWJAP=\"" + ssid + "\",\"" + pwd + "\"\r\n");
            //show result
            String webpage="<html>Wifi setup OK!</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 <input name=ssid type=text><br>";
            String cipSend = "AT+CIPSEND=";
            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);
            }
          }
        }
      }
    }
  }


2016-06-01 補充 :

今早重作此實驗卻發現出現 "busy ...", Reset 後重新做也一樣 :

HTTP/1.1
HostAT+CWJAP="EDIMAX-tony","1234567890"

AT+CIFSR

busy p...
AT+CIPSEND=0,27

busy p...
<html>Wifi setup OK!</html>AT+CIPCLOSE=0

busy p...


不知原因為何, 難道如下列網頁所言是韌體版本的關係?

參考 :

Page impossible to be refreshed / AT+CIPCLOSE= causes "busy p..." message
Page impossible to be refreshed / AT+CIPCLOSE= busy p... 
ESP8266 Wi-Fi + Arduino upload to Xively and ThingsSpeak

2016-06-03 補充 :

注意, 以上只是一個測試專案的中間過程記錄, 不是最終結果. 參看 :

AllAboutEE 的 ESP8266 伺服器測試 (四) : 完結篇


沒有留言 :