2016年5月23日 星期一

AllAboutEE 的 ESP8266 伺服器測試 (一)

上週六日國中會考 (5/14~15), 我去雄工給二哥陪考, 看完兩本 Arduino 的書, 第一天看完剛從明儀買到的 "Arduino 物聯網專案實作 (博碩, 江良志譯)", 第二天看完跟市圖借的 "實戰數位家庭自動化-使用 Arduino", 兩天下來看得興味盎然, 又重新燃起 Arduino + ESP8266 的熱情. 奇怪, 記得去年底 Arduino 玩得不亦樂乎, 是為啥停下腳步呢?

我去翻查去年的網誌, 原來去年底要把剩下的休假休掉, 在家連休五天時, 看到自己前年底寫的如何在 GAE 部署 jQuery EasyUI 的文章, 發現這題目我只起了個頭, 做完初步測試證明可行後就切換到別的事情上了 (應該是 PHP/Java). 既然一時興起就繼續把 jQuery EasyUI on GAE 一直做下去, 弄到三月初終於搞定了, 把 PHP 架站機幾乎整個全部改寫為 Python 版並移植到 GAE 上頭去了. 然後三月底, 因為跟市圖借了一本 "Raspberry Pi 最佳入門與實戰應用", 又把塵封已久的樹莓派拿出來玩, 一直到現在. 因為樹莓派的關係, Arduino 也重獲我之芳心, 希望這次能像 EasyUI on GAE 一樣能集中火力攻關.

今天在網路上找到下面這個網站 :

ESP8266 Arduino Webserver Tutorial Code


這是 AllAboutEE 所製作的教育影片, 其網址在 :

How To Use the ESP8266 and Arduino as a Webserver
# Send Data From Webpage to ESP8266 (Toggle Arduino Pins From Webpage)

這讓我想起上一次玩 Arduino+ESP8266 時所寫的函式庫, 當時似乎遇到記憶體運用的瓶頸, 為了改進函式庫, 我還借了好幾本 C 語言的書來鑽研哩! 參考 :

ESP8266 函式庫 v2

不過今天看了這個 AllAboutEE 的做法, 覺得其寫法也很不錯, 其實原理都差不多, 但是他沒有寫一堆函式, 只寫了 sendData() 這個主要函式而已, 其他的就依據需要, 直接將 AT 指令當參數用 sendData() 傳送即可, 好處是不須匯入整個函式庫, 讓那些用不到的函式占用記憶體位置; 而且直接接觸 AT 指令可增強熟悉度.

此次測試我使用去年底所製作的 ESP8266 轉接板, 基本原理就是利用 Arduino 的內建函式庫 SoftwareSerial 指定兩個 Pin 與 ESP8266 做 UART 通訊, 因為 Arduino 的硬體串列埠 TX 與 RX 只有一對, 已經被用來接 PC 的 USB 以便上傳程式與 Debug 用. 必須透過軟體序列埠來向 ESP8266 下 AT 指令與接收回應.



參考下面兩篇文章, 裡面有紀錄轉接板製作方式, 沒有轉接板也可以參考所附電路圖在麵包板上直接接線 :

# 製作 ESP8266 轉接板
# 撰寫 Arduino 的 ESP8266 WiFi 函式


ESP8266 的 Vcc 可直接取用 Nano 的 3.3V 輸出即可, 但如果發現 ESP8266 不穩定, 通常是電源不夠力, 因為 PC 的 USB 輸出約 500mA, 而 Arduino 的 3.3V 輸出最多只有 150~200mA 而已, 參考 :

How much current can I draw from the Arduino's pins?
# How much current can be drawn from an Arduino Uno's 3.3V rail?

這時就需要獨立的 3.3V 電源供應, 可用兩個 1.5V 乾電池串聯起來供電, 但切記 ESP8266 的 GND 必須跟 Arduino 的 GND 共接地以形成迴路, 很多人會忽略這個, 當然怎麼測都不會 Work. 建議使用一個麵包板電源模組, 這種板子很便宜, 可直接插在麵包板的上下電源槽, 板上使用 AMS1117 穩壓晶片提供 3.3V 800mA 的驅動力.

 
本測試需要對 ESP8266 的 AT 指令操作有基本的了解, 參考 :

ESP8266 WiFi 模組 AT command 測試

之前曾對 ESP8266 做過伺服器功能測試, 不過當時是將 ESP8266 設在模式 1 (Station), 亦即只能連上指定的無線基地台, 本身不當 AP, 這做法跟 AllAboutEE 是一樣的. 當連線無線基地台成功時會從無線 AP 的 DHCP 取得一個區網 IP. 在模式 1 下用 AT+CIPMUX=1 與 AT+CIPSERVER=1,80 開啟多重連線與 80 埠的網頁伺服器, 這樣連線到 ESP8266 的區網 IP 時就會連線到該網頁伺服器. 參考 :

# ESP8266 網頁伺服器 AT 指令測試

我修改了 AllAboutEE 網頁中的程式, 不同之處是將工作模式改為 3 (STA+AP), 這是我參考 Webduino 的做法, 希望可以用手機或筆電來連線 ESP8266 的 SoftAP 以便設定 wifi 的 ssid 與 password, 讓 ESP8266 連上無線基地台. 因為一個 Arduino + ESP8266 物聯網專案必須透過 wifi 連上 Internet, 在測試時我們是將無線基地台的 ssid 與密碼寫在程式中, 但實務上可不能要使用者自己用 Arduino IDE 去改源碼, 編譯後上傳 Arduino 吧?

我覺得 Webduino 的做法很棒, 就是讓 ESP8266 工作在模式 3, 同樣開啟多重連線與網頁伺服器, 這時所建立的 SoftAP 無線基地台 (網址固定是 192.168.4.1) 可讓使用者用手機或筆電連線, 跟連線區網 IP 一樣會連到此網頁伺服器. 然後用瀏覽器來設定 Station 部分要連線之聯外無線基地台的 SSID 與密碼. 這個做法的好處是 SoftAP 的網址固定就是 192.168.4.1, 如果是設為模式 1, 因為我們無法得知 DHCP 到底會指派甚麼 IP 給 ESP8266, 當然就無法連線此網頁伺服器了 (當然可以去聯外無線基地台的管理頁面依據 MAC 來查詢, 但我們不能要求使用者這麼做, 因為不是每個人都了解網路設定).

下列程式碼只是初步讓 Arduino 透過 ESP8266 建立一個網頁伺服器, 當客戶端連線 ESP8266 的 SoftAP 網址 192.168.4.1 時, 回應客戶端 (即手機瀏覽器) 一個無線基地台的連線設定頁面而已.

#include <SoftwareSerial.h>
#define DEBUG true

SoftwareSerial esp8266(10,11); //(RX,TX)

void setup() {
  Serial.begin(9600); //start hardware serial port
  esp8266.begin(9600); //start soft serial port
  sendData("AT+RST\r\n",2000,DEBUG); // reset module
  sendData("AT+CWMODE=3\r\n",1000,DEBUG); // configure as STA+AP
  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
    /*
    while(esp8266.available())
    {
      // The esp has data so display its output to the serial window
      char c = esp8266.read(); // read the next character.
      Serial.write(c);
    } */
 
    if (esp8266.find("+IPD,")) {  //收到客戶端的連線要求, 進行回應
      delay(1000);
      // +IPD, 後的字元是連線 ID (ASCII碼), 用 read() 讀取後減 48 為數字
      int connectionId = esp8266.read()-48;
      //subtract 48 because the read() function returns
      //the ASCII decimal value and 0 (the first decimal number) starts at 48
      String webpage="<html><form method=get>SSID <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=password 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;
  }

此程式在 setup() 部分有三個最重要的 AT 指令 :
  1. AT+CWMODE=3 : 設定 ESP8266 工作於 STA+AP 模式
  2. AT+CIPMUX=1 :  開啟多重連線 (作為伺服器之必須)
  3. AT+CIPSERVER=1,80 : 開啟伺服器之 80 埠做網頁服務
原作者只寫了一個 sendData() 函數來傳送 AT 指令, 此分成兩部分, 先傳送包含連線 ID 與網頁資料長度的 CIPSEND, 然後才是傳送網頁本身. 雖然分成兩步驟, 但這樣將 CIPSEND 分離出來的作法讓 sendData() 可以用來傳送任何 AT 指令, 共用效果最大.

注意, 程式中使用序列埠的 find() 函數尋找回應字串 "+IPD,", 這是 ESP8266 接收到來自客戶端連線後的回應字串開頭,  後面跟著的是連線通道 id, 是 0~4 的數字, 可用 read() 函數讀取, 但讀到的是以 ASCII 編碼的通道號碼, 其值為 48~52 的數值 (48 是 "0" 的 ASCII 碼), 因此必須減 48 才會得到正確的通道號碼. 關於 ESP8266 連線回應字串 +IPD 參考 :

# ESP8266 網頁伺服器 AT 指令測試

程式上傳 Arduino 後, 打開手機的 wifi, 應該可以偵測到 SSID 是 "ESP_" 開頭的無線基地台 (例如 ESP_9CBD07), 這就是 ESP8266 模式 2 或 3 下所建立的 SoftAP, 打開手機 wifi 連線此無線基地台 (密碼可事先用 AT+CWSAP 指令設定, ESP8266 預設無密碼) :



連線此 SoftAP 成功後, 打開手機瀏覽器, 連線 ESP8266 伺服器網址 192.168.4.1, 上面的程式碼中, Arduino 就會透過軟體序列埠將網頁碼輸出給 ESP8266, 而 ESP8266 的伺服器就將收到的網頁碼回應給客戶端, 即手機瀏覽器 :



輸入 SSID 與 PWD 後, 按 Connect 鈕就會將此兩參數傳遞給 ESP8266, 但是因為上面的程式沒有進一步處理收到的參數, 因此還是回應同樣的網頁. 因手機瀏覽器沒有開發工具, 無法觀察 HTTP 訊息中的參數, 因此我改用筆電來連線 ESP8266 之 SoftAP :


連線成功後, 同樣打開瀏覽器, 輸入網址 192.168.4.1, 同時按下 F12 觀察 HTTP 訊息 :


可見按下 Connect 後, 此網頁確實向後端 (即 ESP8266) 傳遞了 ssid 與 password 這兩個參數, ESP8266 的網頁伺服器會對此 Request 做出回應 (即 +IPD 字串), 參考這篇 :

ESP8266 網頁伺服器 AT 指令測試

Arduino 透過軟體序列埠可以接收到這個回應字串, 我們接下來必須剖析這個回應字串中的位址訊息, 即 HTTP 後面的網址, 擷取出 GET 方法傳送的兩個參數 ssid 與 password, 然後由 Arduino 向 ESP8266 下達 AT+CWJAP 指令, 即可讓 ESP8266 的 Station 部分連上指定的無線基地台.

下面是序列埠監視視窗擷取之訊息 :

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
0.0.0.0

OK
AT+CIPMUX=1


OK
AT+CIPSERVER=1,80


OK
,362:GET / HTTP/1.1
Host: 192.168.4.1
User-Agent: Mozilla/5.A
> <html><form method=get>SSID <input name=ssid type=text><br>
SEND OK
AT+CIPSEND=0,36

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

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


OK
Unlink
,358:GET /?ssid=ttt&password=aaa HTTP/1.1
Host: 192.168.4.1
A
> <html><form method=get>SSID <input name=ssid type=text><br>
SEND OK
AT+CIPSEND=0,36

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

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


OK
Unlink

注意喲, 當使用手機瀏覽器連線 SoftAP 網址 192.168.4.1 成功時, ESP8266 會回應 Link 以及隨後的 +IPD 回應字串, 但因為 Serial 物件的 find() 函數在讀取串列埠緩衝器時會刪除已讀的字元, 所以當其讀到 "+IPD," 時也把這五個字元同時從緩衝器刪除 (每讀一個字元刪除一個), 接著用 read() 函數讀取連線通道字元時, 又把連線通道 '0' 刪除, 所以在 sendData() 函式中最後第二行輸出回應字串時就少了這六個字元了, 實際上完整的第一行如下 :

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

但最後輸出時第一行變成 :

,362:GET / HTTP/1.1

OK, 到此已完成伺服器測試的第一步, 接下來要修改上面的程式, 進一步剖析 HTTP 表頭資訊, 擷取其中之網址與所傳遞之參數, 以便能透過 AT+CWJAP 指令來設定想要連線之無線基地台.

關於剖析表頭字串, 需要使用到串列埠之 find(), readBytesUntil() 等函數, 可參考下列文章 :

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


2016-05-28 補充 :

我在 0.9.5 韌體的 ESP8266 發現, 其 SoftAP 不是以 "ESP_" 開頭, 而是 "AI-THINKER_".

其他參考 :

# ESP8266 - AT Command Reference
ESP8266 WiFi Module Quick Start Guide
# wifiwebserver
http://www.ebook777.com/make-sensors-projects-experiments-measure-world-arduino-raspberry-pi/

2016-06-03 補充 :

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

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


17 則留言 :

Unknown 提到...

版主 你好
我現在讓ESP8266在 STA+AP 模式,但用瀏覽器連線到192.168.4.1後,序列埠監視視窗沒有抓到
,362:GET / HTTP/1.1
Host: 192.168.4.1
User-Agent: Mozilla/5.A


請問是否瀏覽器連線到192.168.4.1後就會收到以上訊息?
不知版主是否知道若是沒有收到,可能會是甚麼原因呢?

小狐狸事務所 提到...

您的電腦或手機須先連線到 ESP8266 的 AP (SSID 為 ESPxxxx 的) 後再用瀏覽器喔!

明月伴清風 提到...

這位大哥: 請教您一個問題,我在esp8266架了一個server可以在網頁上顯示類比腳讀到的數值,網址是wifi的靜態IP
192.168.0.13 服務商是新竹的北視寬頻,用安卓手機在家中,可以連線到此server,顯示也正常,但是用電腦桌機卻連不到192.168.0.13,然後把手機拿到20公尺外,就是家中上網的wifi連不到時,手機改用遠傳電訊4G上網的服務,也連不進此server192.168.0.13,所以應該是wifi的192.168.0.13沒有連到外網,請問要如何解決,
用arduino和ethernet的網路線架server時都可以連到外網,因為是固定IP,我現在用wemos d1 的esp8266晶片 架web server 就是電腦連不進來,只有手機經過家中的北視寬頻wifi才連得到此web server.
時常拜讀您的網路文章 受益良多, 在此 說聲 謝謝!

希望您有空時, 能幫小弟解決這個困難, 再次感謝. 祝 黃大哥 家庭美滿 事事如意.
新竹 張先生 留

Unknown 提到...

協助回答一下。
在區域網路內,網段要一樣才能做"內網"連線,簡單來說,ESP 8266連到的SSID要與使用者端連線的SSID相同,才能互通。如要遠端(無距離限制)連線就需吧此AP對外連線(開通),或者用第三方平台連線,如Firebase,thingSpeak等。希望有幫助到您,因小弟對網管不是太專業。也借此希望能夠互相分享資源與知識。

小狐狸事務所 提到...

劉兄感謝您, 我這兩天忙還沒時間看.

小狐狸事務所 提到...

是的, 如劉兄所言, 要用瀏覽器控制 ESP8266, 第一種方式是 ESP 連線家中的無線基地台取得一個 IP, 如您的 192.168.0.13, 然後手機或電腦也連上同一基地台, 這樣兩者便在同一網段可互通了. 第二種方式是不管 ESP 有無連上基地台, 打開它自己的 AP 模式 (IP 固定為 192.168.4.1), 然後讓手機或電腦改為連線 ESP 的自有 AP (SSID 是 ESP_開頭的), 這樣也可以連線到我們寫的 ESP Server. 如果要在外網控制 ESP, 則要用 ThingSpeak 或 Blynk 等物聯網雲端服務.

阿麥 提到...

小狐狸你好:
我把上面的程式碼upload後
開啟序列視窗顯示
AT+RST

OK
+STA_DISCONNECTED:"cc:9f:7a:62:22:a9"
bB׆PR⸮⸮⸮ȤRN⸮ȤRN⸮H⸮⸮NO=⸮⸮
ready
AT+CWMODE=3

OK
AT+CIFSR
+CIFSR:APIP,"192.168.4.1"
+CIFSR:APMAC,"3e:71:bf:32:0c:aa"
+CIFSR:STAIP,"0.0.0.0"
+CIFSR:STAMAC,"3c:71:bf:32:0c:aa"

OK
AT+CIPMUX=1

OK
AT+CIPSERVER=1,80

OK
然後手機開啟WIFI,也有找到ESP開頭的熱點

但連接上後,手機開啟瀏覽器輸入網址192.168.4.1
網頁都顯示
無法連上這個網站

無法連上http://192.168.4.1/

ERR_ADDRESS_UNREACHABLE

請問該怎麼解決這個問題,謝謝。

小狐狸事務所 提到...

Hi, 你手機 WiFi 要打開, 搜尋 ESP_ 開頭的基地台, 連線該基地台 (預設密碼 micropythoN) 後才能使用 192.168.4.1, 參考 :

http://yhhuang1966.blogspot.com/2017/05/micropython-on-esp8266-wifi_16.html

阿麥 提到...

小狐狸您好:
有測試出來了,是可以顯示的,原因出在我自己的手機。
我的手機連上ESP8266的熱點時,都會跳出一個提示訊息"這個網路沒有網際網路連線。要繼續保持連線嗎?"
先前我都直接忽略滑掉這個訊息,以為WIFI列表顯示已連線,就是真的連線了。稍早把這個提示訊息點開確認保持連線後,就可以正常顯示網頁內容了,烏龍一場。

小狐狸事務所 提到...

了解, ESP8266 的內建基地台沒有連接 Internet 所以才會有那個訊息, 但不是每支手機會詢問, 您使用 iPhone 嗎?

阿麥 提到...

小狐狸您好:
不是,我是使用安卓系統的手機,手機是NOKIA 6

匿名 提到...

版主我想問下關於文中提到的esp8266要共地形成迴路,請問是只要把GPIO0腳位接地就可以嗎?因為之前的文章好像有提過GPIO0腳位要接地才能成功燒錄程式上去。

小狐狸事務所 提到...

Hi, 所謂共接地是指, Arduino 與 ESP8266 若分別由兩個獨立電源供電時, 他們 GND 要接在一起, 如果不這麼做, 兩個迴路是獨立的, 無法形成 TX/RX 互接時的電位參考點. 而 GPIO0 在燒錄時接地是指參考 ESP8266 本身的 GND 而言, 與 Arduino 的共接地這件事無關.

匿名 提到...

了解了,感謝回復。另外我想再問一些問題,我目前是用uno r3板來做文章中的範例,可是在上傳完後無法透過wifi搜尋到esp開頭的熱點來做後續連線動作,我想知道是哪裏出了問題,謝謝
序列視窗顯示如下
ctx: cont
sp: 3ffef7c0 end: 3ffefa00 offset: 01b0

>>>stack>>>
3ffef970: 00000001 3ffee878 3ffef9c0 40203071
3ffef980: 3ffefc14 000001a5 3ffef9c0 402020cc
3ffef990: 000003e8 00000000 3ffef9cc 40202bb6
3ffef9a0: 3ffe8874 00002580 3ffee878 3ffee9cc
3ffef9b0: 3fffdad0 00000000 00002580 40202174
3ffef9c0: 3fff0e54 0000000f 00000000 3fff0e3c
3ffef9d0: 0000000f 0000000a feefeffe feefeffe
3ffef9e0: feefeffe feefeffe 3ffee9c4 40202e18
3ffef9f0: feefeffe feefeffe 3ffee9e0 40100710
<<<stack<<<
@f⸮⸮4⸮t⸮⸮H*⸮⸮

小狐狸事務所 提到...

這表示 Arduino 透過 UART 與 ESP8266 溝通沒成功, ESP8266 沒有被設定工作於 STA+AP 模式, 無法建立 AP 基地台, 所以掃不到 SSID. 事實上我已放棄 Arduino+ESP8266 模式, 而是直接用 ESP8266, 他比 Arduino 強多了 (16 bit vs 8 bit), ESP8266 可直接用 Arduino IDE 寫程式, 參考 :
http://yhhuang1966.blogspot.com/2017/09/arduino-ide-esp8266-led.html

小狐狸事務所 提到...

更正 : ESP3266 是 32 位元微控器

匿名 提到...

知道了,我會參考看看的,謝謝