2016年7月4日 星期一

使用 ITEAD WeeESP8266 函式庫改寫物聯網模組 wifi 設定程式

上周菁菁與姐姐月考結束了, 晚上我終於有時間來將第二塊新版的 Arduino+ESP8266 物聯網模組焊好, 測試結果正常, 只要照之前畫好的正反面布線圖焊接, 烙鐵沒有氧化上錫順利的話, 大概一個小時就可以完成一個板子, 詳細製作程序參考 :

# 製作 Arduino Nano + ESP8266 物聯網模組 (三)

而舊版的 4.5cm*4.5cm 洞洞板則參考 :

# 製作 Arduino Nano + ESP8266 物聯網模組 (二)

連同三塊舊版的板子, 現在總共有五塊了 :


這樣應該足夠我目前實驗所需, 包括 : 風力發電機監控*1, 太陽能板監控*1, 門口生物移動偵測*2, 遠端自動澆水控制器*1.

接下來想搞定一件事 : 這個 IOT 模組的 Wifi 設定程式是否也可以用 ITEAD 所寫的 WeeESP8266 函式庫來實現呢? 會不會很吃記憶體?

去年 11 月曾經對 WeeESP8266 函式庫做過一些基本的測試, 結果非常滿意, 原始碼變得很簡潔, 可讀性也大幅提高. 但是在測試伺服器功能時卻卡關了, 我使用 Arduino 的 getBytes() 函數來將要回應給客戶端的網頁字串複製到傳送緩衝器, 再呼叫 send() 函數傳送回應訊息, 當資料少時沒問題, 但資料一多卻不知何故無法順利傳送回應訊息給客戶端 (緩衝器太少?). 納悶一陣子無解就擱下了.

上週找到下面這位 mlwmlw 的文章, 發現了 send() 函數用法的範例, 以其方式重新撰寫程式測試了 ESP8266 的伺服器功能, 結果順利地解決了問題, 參考 :

mlwmlw weeesp8277 web server example
# ITEAD WeeESP8266 函式庫測試 (二) : 網頁伺服器

當然, 接下來就是照此方法用 WeeESP8266 改寫 IOT 模組的 wifi 設定程式了, 經過一整天努力, 終於 ... 成功囉! 下面是初步測試的程式, 雖然可用, 但是有 bug (見下文) :

#include <SoftwareSerial.h>
#include "ESP8266.h"

SoftwareSerial esp8266(7,8); //(D7:RX, D8:TX)
ESP8266 wifi(esp8266); //create wifi obj
const int SW_PIN=4; //Pin to switch configuration or working mode
int mode; //store current mode(LOW=configuration, HIGH=working)

void setup() {
  //system use please do not edit
  Serial.begin(9600);
  wifi.restart();  //reset ESP8266
  Serial.println("Firmware version ... " + wifi.getVersion());  
  pinMode(SW_PIN, INPUT);
  mode=digitalRead(SW_PIN);
  boolean ok=true;
  if (mode==LOW) { //setup mode : for wifi configuration
    ok &= wifi.setOprToStationSoftAP();    
    ok &= wifi.enableMUX();
    ok &= wifi.startTCPServer(80);
    if (ok) {Serial.println("Setup mode ... OK");}
    }
  else { //working mode : for running application
    ok &= wifi.setOprToStation();
    if (ok) {Serial.println("Working mode ... OK");}
    }
  delay(5000);
  Serial.println("IP ... " + wifi.getLocalIP());
  }

void loop() {
  if (mode==LOW) {setupWifi();}
  else { //-----application codes listed bellow-----
    //if (esp8266.available()) {Serial.write(esp8266.read());}
    //if (Serial.available()) {esp8266.write(Serial.read());}
    }
  }

void setupWifi() {
  uint8_t buffer[256]={0}; //init buffer
  uint8_t mux_id; //TCP connection id (0~4)
  uint32_t len=wifi.recv(&mux_id, buffer, sizeof(buffer), 2000); //get response
  if (len > 0) { //data received
    String path="";
    for (uint32_t i=0; i<len; i++) {
      if (!path.endsWith(" HTTP/")) {path.concat((char)buffer[i]);}
      else {break;}
      }
    path=path.substring(path.indexOf("/") + 1, path.indexOf(" HTTP/"));
    Serial.println(path); //eg:update/?ssid=EDIMAX-tony&pwd=1234567890      
    if (path.startsWith("update")) { //update ssid & pwd
      String ssid=path.substring(path.indexOf("?") + 6, path.indexOf("&"));
      String pwd=path.substring(path.indexOf("&") + 5);
      //ssid.trim();
      //pwd.trim();
      Serial.println(ssid);
      Serial.println(pwd);
      if (wifi.joinAP(ssid, pwd)) {
        uint8_t data[]="<html>Wifi setup OK!</html>";
        wifi.send(mux_id, data, sizeof(data));
        }
      else {
        uint8_t data[]="<html>Wifi setup failed!</html>";
        wifi.send(mux_id, data, sizeof(data));
        }        
      }
    else { //show setup page
      uint8_t data[]="<html><form method=get action='/update/'>"
      "SSID <input name=ssid type=text><br>"
      "PWD <input name=pwd type=text> "
      "<input type=submit value=Connect></form></html>";
      wifi.send(mux_id, data, sizeof(data));    
      }
    Serial.println(wifi.releaseTCP(mux_id));
    }    
   Serial.println(wifi.getIPStatus().c_str());  
   }

序列埠監控視窗擷取訊息如下 (這是成功的訊息) : 

Firmware version ... 0018000902
Setup mode ... OK
IP ... 192.168.4.1
192.168.2.111
STATUS:2
STATUS:2
STATUS:2
STATUS:2
update/?ssid=EDIMAX-tony&pwd=1234567890
EDIMAX-tony
1234567890
1


STATUS:4
STATUS:4
STATUS:4

但有時候卻出現下列 STATUS:5 的奇怪訊息 :

update/?ssid=EDIMAX-tony&pwd=1234567890
EDIMAX-tony
1234567890
1
STATUS:5
+CIPSTATUS:1,"TCP","192.168.4.100",47783,1
STATUS:5
+CIPSTATUS:1,"TCP","192.168.4.100",47783,1
STATUS:5

怎麼會有 status 5 呢? ESP8266 文件上沒有啊! 後來在下列文章找到答案, 原來這是 Wifi 連線失敗之意, ESP8266 原始文件中並未列出, 新版文件會加上去 :

# return code ???

"Sorry that we missed it, status 5 means WiFi connection fail, we will add it in the next version of documentation."

連線 AP 明明有成功 (因為呼叫 getLocalIP() 除了看到 192.168.4.1 以外, 還有從 AP 獲得的 IP), 但手機瀏覽器卻顯示 "Wifi setup failed!", 真是奇怪! 後來我檢查了 WeeESP8266 的原始碼檔案 ESP8266.cpp, 發現 send() 只在回應中含有 "OK" 才會回傳 true, 如果 ssid 與 pwd 與先前設定雷同, ESP8266 會回應 "no change", 這其實也算成功, 但 send() 函式的寫法卻會回傳 false, 導致我們無法判斷到底有無成功 :

ESP8266.cpp 內容 :

bool ESP8266::sATCWJAP(String ssid, String pwd)
{
    String data;
    rx_empty();
    m_puart->print("AT+CWJAP=\"");
    m_puart->print(ssid);
    m_puart->print("\",\"");
    m_puart->print(pwd);
    m_puart->println("\"");
 
    data = recvString("OK", "FAIL", 10000);
    if (data.indexOf("OK") != -1) {
        return true;
    }
    return false;
}

怎辦? 也不想去修改 WeeESP8266 原始碼啊! 後來想到何不在呼叫 joinAP() 前先呼叫 leaveAP() 清除原先的設定呢? 這樣就不會出現 no change 回應了. 果真加上 leaveAP() 後就正常啦! 完整的程式如下 :

#include <SoftwareSerial.h>
#include "ESP8266.h"

SoftwareSerial esp8266(7,8); //(D7:RX, D8:TX)
ESP8266 wifi(esp8266); //create wifi obj
const int SW_PIN=4; //Pin to switch configuration or working mode
int mode; //store current mode(LOW=configuration, HIGH=working)

void setup() {
  //system use please do not edit
  Serial.begin(9600);
  wifi.restart();  //reset ESP8266
  Serial.println("Firmware version ... " + wifi.getVersion());  
  pinMode(SW_PIN, INPUT);
  mode=digitalRead(SW_PIN);
  boolean ok=true;
  if (mode==LOW) { //setup mode : for wifi configuration
    ok &= wifi.setOprToStationSoftAP();    
    ok &= wifi.enableMUX();
    ok &= wifi.startTCPServer(80);
    if (ok) {Serial.println("Setup mode ... OK");}
    }
  else { //working mode : for running application
    ok &= wifi.setOprToStation();
    if (ok) {Serial.println("Working mode ... OK");}
    }
  delay(5000);
  Serial.println("IP ... " + wifi.getLocalIP());
  }

void loop() {
  if (mode==LOW) {setupWifi();}
  else { //-----application codes listed bellow-----
    //if (esp8266.available()) {Serial.write(esp8266.read());}
    //if (Serial.available()) {esp8266.write(Serial.read());}
    }
  }

void setupWifi() {
  uint8_t buffer[128]={0}; //init buffer
  uint8_t mux_id; //TCP connection id (0~4)
  uint32_t len=wifi.recv(&mux_id, buffer, sizeof(buffer), 2000); //get response
  if (len > 0) { //data received
    String path="";
    for (uint32_t i=0; i<len; i++) {
      if (!path.endsWith(" HTTP/")) {path.concat((char)buffer[i]);}
      else {break;}
      }
    path=path.substring(path.indexOf("/") + 1, path.indexOf(" HTTP/"));
    Serial.println(path); //eg:update/?ssid=EDIMAX-tony&pwd=1234567890      
    if (path.startsWith("update")) { //update ssid & pwd
      String ssid=path.substring(path.indexOf("?") + 6, path.indexOf("&"));
      String pwd=path.substring(path.indexOf("&") + 5);
      Serial.println(ssid);
      Serial.println(pwd);
      wifi.leaveAP(); //it's a must, or joinAP will fail if no change
      if (wifi.joinAP(ssid, pwd)) {
        uint8_t data[]="<html>Wifi setup OK!</html>";
        wifi.send(mux_id, data, sizeof(data));
        }
      else {
        uint8_t data[]="<html>Wifi setup failed!</html>";
        wifi.send(mux_id, data, sizeof(data));
        }        
      }
    else { //show setup page
      uint8_t data[]="<html><form method=get action='/update/'>"
      "SSID <input name=ssid type=text><br>"
      "PWD <input name=pwd type=text> "
      "<input type=submit value=Connect></form></html>";
      wifi.send(mux_id, data, sizeof(data));    
      }
    Serial.println(wifi.releaseTCP(mux_id));
    }    
   Serial.println(wifi.getIPStatus().c_str());  
   }

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

Firmware version ... 0018000902
Setup mode ... OK
IP ... 192.168.4.1
192.168.2.111
STATUS:2
STATUS:2
STATUS:2
1
STATUS:3
+CIPSTATUS:1,"TCP","192.168.4.100",48637,1
STATUS:3
+CIPSTATUS:1,"TCP","192.168.4.100",48637,1
STATUS:3
+CIPSTATUS:1,"TCP","192.168.4.100",48637,1
STATUS:3
+CIPSTATUS:1,"TCP","192.168.4.100",48637,1
update/?ssid=EDIMAX-tony&pwd=1234567890
EDIMAX-tony
1234567890
1
STATUS:5
+CIPSTATUS:1,"TCP","192.168.4.100",48637,1
STATUS:5

此程式編譯後所耗記憶體如下 :

草稿碼使用了 14,404 bytes (46%) 的程式存儲空間。最大值為 30,720 bytes。
全域變數使用了 967 bytes (47%) 的動態記憶體,剩餘 1,081 bytes 供局部變數。最大值為 2,048 bytes 。

已經占掉快一半的記憶體了! 與之前使用 AllAboutEE 的作法分別多耗掉約 17% 與 12% :

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

參考 :

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

程式簡潔易讀的代價就是吃記憶體啊! 可見有一好沒兩好, 凡事皆須付出代價.

其他參考 :

mlwmlw WeeESP8266 thingspeak example
ESP8266 wiring with Arduino

2016-07-04 補充 :

經測試 0018000902 版的 AT+CWJAP 指令, 其實重複下此指令連線基地台, 若成功連線都回 OK, 不是 no change, 是我記錯了, 會回 no change 的是 AT+CIPSERVER 指令, 參考之前自行撰寫 ESP8266 函式庫時的測試紀錄 :

# 撰寫 Arduino 的 ESP8266 WiFi 函式

所以上面程式中的 leaveAP() 指令是不需要的. 至於為啥會有 STATUS:5 原因待查.

我把上面的程式稍微優化, 將 println() 的字串常數用 F() 函數存放到程式空間 (Flash 記憶體), 以便騰出 SRAM 空間給應用程式變數使用, 參考 :

# print()

#include <SoftwareSerial.h>
#include "ESP8266.h"
//-----application include & def listed HERE-----

SoftwareSerial esp8266(7,8); //(D7:RX, D8:TX)
ESP8266 wifi(esp8266); //create wifi obj
const int SW_PIN=4; //Pin to switch configuration or working mode
int mode; //store current mode(LOW=configuration, HIGH=working)

void setup() {
  //system use please do not edit
  Serial.begin(9600);
  wifi.restart();  //reset ESP8266
  Serial.print(F("Firmware version ... "));
  Serial.println(wifi.getVersion());  
  pinMode(SW_PIN, INPUT);
  mode=digitalRead(SW_PIN);
  boolean ok=true;
  if (mode==LOW) { //setup mode : for wifi configuration
    ok &= wifi.setOprToStationSoftAP();    
    ok &= wifi.enableMUX();
    ok &= wifi.startTCPServer(80);
    if (ok) {Serial.println(F("Setup mode ... OK"));}
    }
  else { //working mode : for running application
    ok &= wifi.setOprToStation();
    if (ok) {Serial.println(F("Working mode ... OK"));}
    }
  delay(5000);
  Serial.print(F("IP ... "));
  Serial.println(wifi.getLocalIP());
  //-----application setup codes listed HERE-----
  }

void loop() {
  if (mode==LOW) {setupWifi();} //system use please do not edit
  else {
    //-----application loop codes listed HERE-----
    if (esp8266.available()) {Serial.write(esp8266.read());}
    if (Serial.available()) {esp8266.write(Serial.read());}
    }
  }

void setupWifi() {
  uint8_t buffer[256]={0}; //init buffer
  uint8_t mux_id; //TCP connection id (0~4)
  uint32_t len=wifi.recv(&mux_id, buffer, sizeof(buffer), 2000); //get response
  if (len > 0) { //data received
    String path="";
    for (uint32_t i=0; i<len; i++) {
      if (!path.endsWith(" HTTP/")) {path.concat((char)buffer[i]);}
      else {break;}
      }
    path=path.substring(path.indexOf("/") + 1, path.indexOf(" HTTP/"));
    Serial.println(path); //eg:update/?ssid=EDIMAX-tony&pwd=1234567890      
    if (path.startsWith("update")) { //update ssid & pwd
      String ssid=path.substring(path.indexOf("?") + 6, path.indexOf("&"));
      String pwd=path.substring(path.indexOf("&") + 5);
      Serial.println(ssid);
      Serial.println(pwd);
      if (wifi.joinAP(ssid, pwd)) {
        uint8_t data[]="<html>Wifi setup OK!</html>";
        wifi.send(mux_id, data, sizeof(data));
        }
      else {
        uint8_t data[]="<html>Wifi setup failed!</html>";
        wifi.send(mux_id, data, sizeof(data));
        }        
      }
    else { //show setup page
      uint8_t data[]="<html><form method=get action='/update/'>"
      "SSID <input name=ssid type=text><br>"
      "PWD <input name=pwd type=text> "
      "<input type=submit value=Connect></form></html>";
      wifi.send(mux_id, data, sizeof(data));    
      }
    //Serial.println(wifi.releaseTCP(mux_id));
    }    
   Serial.println(wifi.getIPStatus().c_str());  
   }

這樣編譯後全域變數所占記憶體就降到 43% 了 : 

草稿碼使用了 13,854 bytes (45%) 的程式存儲空間。最大值為 30,720 bytes。
全域變數使用了 899 bytes (43%) 的動態記憶體,剩餘 1,149 bytes 供局部變數。最大值為 2,048 bytes 。

但是比起使用 AllAboutEE 函式的 29% 與 35%, 還是分別高出16% 與 8%.

2016-07-04 補充 :

多次測試上面程式發現, 在設定模式運作不太穩定, 有時 ok, 有時 NG, 原因不明, 實在可惱啊! 雖然切到工作模式正常, 但算了, 還是用 AllAboutEE 的函式比較可靠.


沒有留言 :