# 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=
# Page impossible to be refreshed / AT+CIPCLOSE=
# ESP8266 Wi-Fi + Arduino upload to Xively and ThingsSpeak
注意, 以上只是一個測試專案的中間過程記錄, 不是最終結果. 參看 :
# AllAboutEE 的 ESP8266 伺服器測試 (四) : 完結篇