2016年6月2日 星期四

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

今晚把操作流程重新思考後, 反復修改程式與重作測試, 最終認為正確的做法是, 在做 wifi 設定時 ESP8266 應該設在模式 3, 而在工作模式是設在模式 1, 理由是 : 在模式 2 時下 "AT+CWJAP=ssid,pwd" 指令會失敗, 它的邏輯可能認為模式 2 是做 AP 用, 幹嘛要下這個連線其他 AP 的指令呢? 好像也蠻合理的. 測試結果發現這個指令只能在模式 1 與模式 3 時下達才會成功.

另外測試過程中發現, 在下 "AT+CWJAP=ssid,pwd" 指令後, 不要接著下 "AT+CWMODE=1" 修改為模式 1, 因為這會造成 SoftAP 馬上消失, 使得伺服器被關閉, 導致無法回應 "Wifi setup OK!" 給前端手機瀏覽器, 因此工作模式的切換應該放在 setup() 函數中才對.

為了要切換 wifi 設定模式與工作模式, 我使用了Arduino 的 D4 接腳與一個滑動開關來作為模式偵測之用, D4 先串接一個 10K 電阻接到滑動開關的中間接腳 (最好不要將 Arduino 的輸入接腳直接接地, 應該串個 10k 電阻), 而左方接腳則接 5V, 右方接腳接 GND, 如下圖所示 :


圖中滑動開關切在右邊, 使得 D4 經 10K 電阻接地, 這是在 Wifi 設定模式; 若切到左邊就是工作模式. 我們在程式中要偵測 D4 的位準, 若為 LOW 表示是在 Wifi 設定模式, 反之為工作模式.

在此實驗中我使用了之前製作的 ESP8266 轉接板, 直接從 Arduino 分別接 +5V 與 GND 到轉接板的左邊算來第四腳 VCC (紅線) 與第六腳 GND (黑線). 另外, D11 腳被定義為軟體序列埠的 TX, 它被接到轉接板的左邊算來第三腳 (綠線), 即 ESP8266 的 RX; 而 D10 腳被定義為軟體序列埠的 RX, 它被接到轉接板的左邊算來第一腳 (白線), 即 ESP8266 的 TX. 關於轉接板, 參考 :

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

如果沒有製作轉接板也是可以參考下面的電路圖自行接線 (使用 Upverter 繪製) :


這邊 ESP8266 需要一個 AMS1117 3.3V 穩壓晶片來提供電源, 因為 Nano 的 3.3V 最大輸出電流約 30mA 無法負荷 ESP8266 啟動時超過 200mA 的需求 (UNO 的 3.3V 或許可以推得動?). Arduino 數位接腳 D4 經 10K 限流電阻接到滑動開關的中間接腳, 兩邊接腳分別接到 +5V (working mode 工作模式) 與 GND (setup mode 設定模式). 關於 Upverter 參考 :

線上電子設計平台 Upverter
# Upverter 匯出與匯入專案

完整的的程式如下 :

#include <SoftwareSerial.h>
#define DEBUG true

SoftwareSerial esp8266(10,11); //(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)
void setup() {
  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
    sendData("AT+CIFSR\r\n",1000,DEBUG); //get ip address  
    }
  else {  //working mode
    sendData("AT+CWMODE=1\r\n",1000,DEBUG); // configure as station
    }
  }

void loop() {
  if (mode==LOW) {setupWifi();}
  else { //working mode : application codes are here
    sendData("AT+CIFSR\r\n",1000,DEBUG);
    delay(2000);
    }
  }

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

在 loop() 函數中, 首先判斷工作模式, 若在 wifi 設定模式 (mode==LOW), 就呼叫 setupWifi() 函數來處理; 否則就執行 else{} 中的工作模式 (mode==HIGH) 程式碼, 這裡只是簡單的用 AT+CIFSR 指令查詢 IP 而已.

在 wifi 設定模式中, 當擷取出使用者傳送的 ssid 與 pwd 後, 就呼叫 sendDat() 函數以 "AT+CWJAP=" 指令將其寫入 ESP8266 中, 然後從其回傳字串是否含有 "OK" 判斷是否設定成功, 成功的話就回應瀏覽器 "Wifi setup OK!".

程式上傳後, 先拔掉 Nano 的電源, 把滑動開關切到右邊的 wifi 設定模式, 再接上 Nano 的電源, 這時因為 D4 腳偵測到 LOW, 所以會進入設定模式, 在 setup() 函數中將 ESP8266 設在模式 3, 並開啟多重連線與伺服器之 80 埠.

這時打開手機的 wifi, 連線 "ESP_" 開頭的無線基地台, 這是 ESP8266 在模式 3 下開啟的 SoftAP.



然後啟動手機瀏覽器, 連線 192.168.4.1 (SoftAP 的固定網址), ESP8266 收到 Request 後會回應如下標頭訊息 :

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

Arduino 程式在偵測到 +IPD 連線回應後, 擷取 "GET /" 與 "HTTP" 間的網頁路徑, 發現不是 "update", 就利用 ESP8266 的伺服器功能向瀏覽器回應 wifi 設定表單網頁.

輸入家中聯外的無線基地台 ssid 與 pwd, 按 Connect, 這時 ESP8266 會回應如下標題訊息 :

+IPD,0,426:GET /update/?ssid=EDIMAX-tony&pwd=1234567890 HTTP/1.1

這時 Arduino 分析 "GET /" 與 "/"之間的網頁名稱為 update, 就繼續擷取後面的 ssid 與 pwd 參數, 再用 "AT+CWJAP=" 指令設定要連線的 AP. 成功的話就回應瀏覽器 "Wifi setup OK!" 即表示設定成功.

這時就可以拔掉電源, 將滑動開關切到左邊的工作模式, 再重新插電開機.

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

AT+RST


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

no change
AT+CIPMUX=1


OK
AT+CIPSERVER=1,80


OK
AT+CIFSR

192.168.4.1
0.0.0.0

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

Link

+IPD,1,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

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


OK
AT+CIPSEND=0,27

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


OK

設定成功後拔掉電源, 將滑動開關切到左邊的工作模式 (接 +5V), 重插電源後打開串列埠監視視窗, 就可看到 ESP8266 確實工作於模式 1, 在初始化後尚未連線到所設定之 AP 時, 預設 IP 為 0.0.0.0, 連線成功後就顯示從 DHCP 獲得指派之 IP 了 (此處為 192.168.2.114) :


AT+RST


OK
bB�鑭b禔S��"愃L�侂��餾�
[System Ready, Vendor:www.ai-thinker.com]
AT+CWMODE=1

no change
AT+CIFSR

0.0.0.0

OK
AT+CIFSR

0.0.0.0

OK
AT+CIFSR

192.168.2.114

OK
AT+CIFSR

192.168.2.114

OK
AT+CIFSR

192.168.2.114

........

上面程式的工作模式僅僅是向 ESP8266 每隔 2 秒送出 "AT+CIFSR", 因此它會不斷地回應所獲得的區網 IP, ESP8266 板子上的藍燈也會每 2 秒閃一次. 用在物聯網應用時 (例如溫溼度測候紀錄), 程式碼就寫在 loop() 函數的 else{} 區塊內即可.

此程式編譯後的訊息如下 :

草稿碼使用了 8,996 bytes (29%) 的程式存儲空間。最大值為 30,720 bytes。
全域變數使用了 722 bytes (35%) 的動態記憶體,剩餘 1,326 bytes 供局部變數。最大值為 2,048 bytes 。

很不錯, 只用掉了約 3 成的記憶體, 比之前用 wifi 函式庫還省, 這得感謝 AllAboutEE 程式碼的啟發, 這告訴我們 : 凡事要化繁為簡. 我們常為了完美追求形式化, 把整個系統越搞越大, 而效能卻越來越低.

好了, 終於在集中火力猛攻下, 完成了學習 ESP8266 以來始終想要搞定的事. 接下來我要把上面的電路製作成一個 Arduino+ESP8266 模組, 方便之後的應用實驗. 例如以前做過的 DHT11 物聯網實驗, 前陣子有網友問我為何其 DHT11 數值很奇怪, 那我就用新模組來重作看看唄!

# ESP8266 WiFi 模組與 Arduino 連線測試

已經好久沒拿烙鐵囉!

參考 :

Ameba Arduino: UART – 使用UART與電腦溝通
ESP8266 WiFi Control Relay
ESP8266 WiFi Control Device ( Relay ) 無線控制設備(繼電器)
Arduino WiFi Control with ESP8266 Module#
58 ESP8266 Sensor runs 17 days on a coin cell/transmits data to sparkfun.com and ubidots.com
Arduino/libraries/
WiFi Web Server
ESP8266 Temperature / Humidity Webserver
Arduino/libraries (ESP8266WiFi)
Webserver for Arduino ESP8266
https://github.com/itead/ITEADLIB_Arduino_WeeESP8266
ESP8266 WiFi 模組 AT command 測試
espressif/ESP8266_AT
認識Arduino與C語言的函式指標以及函式指標陣列
ESP8266 wiring with Arduino

13 則留言 :

Pungding 提到...

您好,我用Android手機透過WIFI連上SoftAP之後,開啟192.168.4.1可以看到輸入SSID&PASSWD的網頁,但是用iPhone7透過WIFI連上SoftAP之後,用Safari開啟192.168.4.1則一直是空白,沒有顯示任何東西,請問是否有遇過這樣的問題? 感謝分享!!

小狐狸事務所 提到...

因我無 Apple 產品, 都在 Android 上測試, 所以不知在 iPhone 有此問題. 但此為很單純網頁, 奇怪 iPhone 為何無法顯示, 有時間借手機來試試看.

小狐狸事務所 提到...

有可能是 Apple 的瀏覽器對 HTML 檢查較嚴, 因我的 input 元件沒有用 form 包起來.

小狐狸事務所 提到...

Sorry, 我看錯了, 有 form 哩! 看來可能還有其他格式問題也說不定. 手機瀏覽器似乎沒有偵錯工具.

Unknown 提到...

版主好~~看到版主您做伺服機測試,我就突發奇想想做客戶端測試而且要傳rfid的值,可是在見客戶端時卻卡關了,請版主解惑感恩~~,如果版主需要我的程式的話,請版主告知您的gmail~~感恩~~

小狐狸事務所 提到...

不太了解, 我沒做過 RFID 實驗. 您是說您的客戶端要讀取 RFID 然後傳給伺服器嗎?

Unknown 提到...

是的,而且也要把QR code值傳給伺服器

小狐狸事務所 提到...

客戶端是用 ESP8266 嗎? 我 mailbox : tony1966@ms5.hinet.net

Unknown 提到...

是的

Unknown 提到...

已寄給版主,不知版主的寫法如何,可以傳授一下嗎??謝謝~~

小狐狸事務所 提到...

太久沒用 Arduino, 生疏了. 請問你是用 ESP8266 Stand alone 還是 Arduino+ESP8266? 我手上沒有 RFID, 要等添購 RFID 模組才能測試.

Unknown 提到...

想請問如果想把上述的程式碼與Blynk整合
在Blynk.begin(auth,wifi,ssid,psw)的部分要如何取得儲存在ESP8266的SSID與密碼呢?

小狐狸事務所 提到...

Blynk 韌體與這不能共存, 應該是無法整合. 這需要 Blynk 提供 api 才行. 太久沒用 blynk 了, 不知是否有增加輸入基地台之功能.