2015年11月13日 星期五

ESP8266 函式庫 v2

雖然 ITEAD 的 WeeESP8266 函式庫寫得很棒, 也很好用, 但問題是有時我們的應用中只須用到其中幾個函數而已, 但卻要匯入整個函式庫, 似乎太浪費記憶體了. 這種封裝好的函式庫沒辦法輕易地分割出某部分要用到的函式, 所以我又回頭繼續精進之前寫的函式庫, 畢竟自己的東西比較好動手腳. 參考 :

# 撰寫 Arduino 的 ESP8266 WiFi 函式

這次我主要參考了下面這篇文章裡擷取 ESP8266 回應字元的函數 waitForResponse() 的寫法 :

Using an ESP8266 as a time source (Part 1)

函式名稱命名方式則是參考 WeeESP8266 函式庫稍加改變, 我喜歡用底線, 不喜歡Camel 命名法. 原本的 get_ESP8266_response() 我去掉了 ESP8266, 改為較簡單的 get_response() 如下 :

String get_response(int timeout, String term="OK\r\n") {  //取得 ESP8266 的回應字串
  String str="";  //儲存接收到的回應字串
  unsigned long t=millis() + timeout;  //計算 timeout 的時戳
  while (millis() < t) {  //還沒到達 timeout 時間
    if (sSerial.available()) { //若軟體序列埠接收緩衝器有資料
      str.concat((char)sSerial.read());  //串接回應字元    
      if (str.endsWith(term)) {break;}  //檢查是否已收到預期回應終結字串
      //if (str.lastIndexOf(term) != -1) {break;}  //檢查是否已收到預期回應終結字串
      }
    }
  str.trim();  //去除頭尾空白字元 (含跳行)
  return str;  //傳回回應字串
  }

此函數用一個 timeout 參數來控制等候 ESP8266 回應的最長時間, 然後利用第二個參數 term 來尋找預期的成功回應結束字串是否有出現, 預設是 OK 加跳行字元, 因為大部分的 ESP8266 AT 指令正常的回應都以 "OK\r\n" 結束. 等候時間可設長一些, 設為 10 秒應該很合理, 因為超過 10 秒沒有正常回應通常就是有問題了 (主機或網路).  如果成功的結束字串有出現就會立即傳回結果, 不會等到 10 秒後.

判斷是否已逾時, WeeESP8266 是使用另一種寫法, 似乎較好理解 :

start=millis();  //紀錄開始時戳
while (millis() - start < timeout) {...}   //若目前時戳減開始時戳還沒逾時

因為我對 C 語言字元陣列的用法還不熟, 改來改去總是出錯, 所以我沒有用字元陣列來接收回應字元, 而是直接用字串來處理, 主要是使用 concat() 函數來串接回應字元, 並以 endsWith() 來比對是否結束字串已出現, 當然, 用 lastIndexOf() 也是可以的.

將 get_response() 改好後, 就可以繼續改寫各函數了. 修改的重點條列如下 :

  1. 非 get_xxx() 類的函式傳回值都改為 boolean, 原先成功傳回 "OK", 失敗傳回 "NG" 雖然對於顯示結果較方便, 但要判斷執行結果時需要要進行字串比對, 比較耗時間, 我覺得還是像 WeeESP8266 那樣傳回布林值較合理. 
  2. 撤銷 set_mode() 函式, 改成較易理解的 set_ap(), set_station(), 與 set_ap_station(); 同樣地, set_mux() 也撤銷, 改為 set_single() 與 set_multiple(), 雖然這樣函式數目增加了, 但原先的 set_mode() 與 set_mux() 一旦久沒使用, 就會很快忘記 mode 與 mux 要傳入甚麼值. 
  3. 結束 IP 連線的 close_ip() 改成 release(), 這樣與 start_tcp(), start_udp() 比較搭, 因為 UDP 是 connectionless 的傳輸, 用 close 字眼感覺好像是關閉 UDP 連線似的. 
  4. 函式 start_tcp(), start_udp(), send_data(), release() 都有單一連線與多重連線的版本, 多重連線時要多指定一個通道參數 (0~5). 
  5. 成功回應的終止字串預設是 "OK\r\n", 比較特別的是 start_tcp() 與 send_data(). 前者當 TCP 連線成功時回應字串以 "Linked\r\n" 結尾, 而 start_udp() 則是以一般的 "OK\r\n" 結尾. 函式 send_data() 有兩段傳送, 一是先傳送字串長度, 其回應以 ">" 結尾; 然後第二段是傳送資料, 成功的話它會自動關閉連線, 回應字串以 "Unlink" 結尾, 回應字串中含有 "+IPD" 

修改後的函式庫與測試程式如下 :

#include <SoftwareSerial.h>
SoftwareSerial sSerial(10,11); //(RX,TX) 與 ESP8266 介接的軟體串列埠
String ssid="H30-L02-webbot";  //無線基地台識別
String pwd="blablabla";  //無線基地台密碼 

void setup() {
  sSerial.begin(9600);  //設定軟體序列埠速率 (to ESP8266)
  Serial.begin(9600);  //設定軟體序列埠速率 (to PC)
  Serial.println("*** SoftSerial connection to ESP8266 ***");
  Serial.println("Firmware version : " + get_version());  //取得韌體版本
  Serial.println("Baud rate : " + get_baud());  //取得傳送速率
  Serial.println("Get IP : " + get_ip());  //取得本地 IP
  //測試操作模式 working mode
  Serial.println("Get mode : " + get_mode());  //取得操作模式
  if (set_ap()) {Serial.println("Set AP mode ... OK");}  //設定為 AP 模式
  else {Serial.println("Set AP mode ... NG");}
  Serial.println("Get mode : " + get_mode());  //取得操作模式
  if (set_ap_station()) {Serial.println("Set AP+STATION mode ... OK");}
  else {Serial.println("Set AP+STATION mode ... NG");}  //設定為 AP+STATION 模式
  Serial.println("Get mode : " + get_mode());  //取得操作模式
  if (set_station()) {Serial.println("Set STATION mode ... OK");}
  else {Serial.println("Set STATION mode ... NG");}  //設定為 STATION 模式
  Serial.println("Get mode : " + get_mode());  //取得操作模式
  //測試 Mux (單一/多重連線能力)
  if (set_multiple()) {Serial.println("Set multiple connection ... OK");}
  else {Serial.println("Set multiple connection ... NG");}  //設定為多重連線
  Serial.println("Get mux : " + get_mux());  //取得 Mux 設定
  if (set_single()) {Serial.println("Set single connection ... OK");}
  else {Serial.println("Set single connection ... NG");}  //設定為單一連線
  Serial.println("Get mux : " + get_mux());  //取得 Mux 設定
  //測試 AP 連線
  Serial.println("Get AP : " + get_ap());  //取得 AP
  if (quit_ap()) {Serial.println("Quit AP ... OK");}  //離開 AP
  else {Serial.println("Quit AP ... NG");}  
  Serial.println("Get AP : " + get_ap());  //取得目前連線之 AP (NG)
  Serial.println("Get IP : " + get_ip());  //取得本地 IP (0.0.0.0)
  if (join_ap(ssid, pwd)) {Serial.println("Join AP ... OK");}  //加入 AP
  else {Serial.println("Join AP ... NG");}  
  Serial.println("Get AP : " + get_ap());  //取得 AP
  Serial.println("Get IP : " + get_ip());  //取得本地 IP (192.168.x.x)
  //測試多重連線下的 TCP/UDP 要求
  if (set_multiple()) {Serial.println("Set multiple connection ... OK");}
  else {Serial.println("Set multiple connection ... NG");}  //設定多重連線
  if (start_tcp(0,"www.google.com",80)) {  //連線 Google 首頁
    Serial.println("Connect Google ... OK");
    if (send_data(0,"GET /")) {Serial.println("Send GET/ ... OK");}
    else {Serial.println("Send GET/ ... NG");}  
    }
  else {Serial.println("Connect Google ... NG");}
  if (start_tcp(0,"184.106.153.149",80)) {  //連線 Thingspeak
    Serial.println("Connect Thingspeak ... OK");
    String str="GET /update?api_key=NO5N8C7T2KINFCQE&field1=24.00&field2=80.40&field3=85.00";
    if (send_data(0,str)) {Serial.println("Send GET/ ... OK");}
    else {Serial.println("Send GET/ ... NG");}  
    }
  else {Serial.println("Connect Thingspeak ... NG");}
  if (start_udp(0,"82.209.243.241",123)) {Serial.println("Start UDP ... OK");} //NTP
  else {Serial.println("Start UDP ... NG");}  //起始 UDP
  if (release(0)) {Serial.println("Release IP ... OK");}  //關閉 IP 連線
  else {Serial.println("Release IP ... NG");}
  //測試單一連線下的 TCP/UDP 要求    
  if (set_single()) {Serial.println("Set single connection ... OK");}
  else {Serial.println("Set single connection ... NG");}  //設定單一連線  
  if (start_tcp("www.google.com",80)) {  //連線 Google 首頁
    Serial.println("Connect Google ... OK");
    if (send_data("GET /")) {Serial.println("Send GET/ ... OK");}
    else {Serial.println("Send GET/ ... NG");}  
    }
  else {Serial.println("Connect Google ... NG");}
  if (start_tcp("184.106.153.149",80)) {  //連線 Thingspeak
    Serial.println("Connect Thingspeak ... OK");
    String str="GET /update?api_key=NO5N8C7T2KINFCQE&field1=24.00&field2=80.40&field3=85.00";
    if (send_data(str)) {Serial.println("Send GET/ ... OK");}
    else {Serial.println("Send GET/ ... NG");}  
    }
  else {Serial.println("Connect Thingspeak ... NG");}
  if (start_udp("82.209.243.241",123)) {Serial.println("Start UDP ... OK");} //要求NTP
  else {Serial.println("Start UDP ... NG");}  //起始 UDP
  if (release()) {Serial.println("Release IP ... OK");}  //關閉 IP 連線
  else {Serial.println("Release IP ... NG");}
  Serial.println("List APs : ");    //列出附近可用 AP
  Serial.println(list_ap());
  }

void loop() {
  if (sSerial.available()) {  //若軟體串列埠 RX 有收到來自 ESP8266 的回應字元
    Serial.write(sSerial.read());  //在串列埠監控視窗顯示 ESP8266 的回應字元
    }
  if (Serial.available()) {  //若串列埠 RX 有收到來自 PC 的 AT 指令字元 (USB TX)
    sSerial.write(Serial.read());  //將 PC 的傳來的字元傳給 ESP8266
    }
  }

String get_response(int timeout, String term="OK\r\n") {  //取得 ESP8266 的回應字串
  String str="";  //儲存接收到的回應字串
  unsigned long t=millis() + timeout;  //計算 timeout 的時戳
  while (millis() < t) {  //還沒到達 timeout 時間
    if (sSerial.available()) { //若軟體序列埠接收緩衝器有資料
      str.concat((char)sSerial.read());  //串接回應字元    
      if (str.endsWith(term)) {break;}  //檢查是否已收到預期回應終結字串
      //if (str.lastIndexOf(term) != -1) {break;}  //檢查是否已收到預期回應終結字串
      }
    }
  str.trim();  //去除頭尾空白字元 (含跳行)
  return str;  //傳回回應字串
  }

String get_version() {
  sSerial.println("AT+GMR");  //取得韌體版本
  sSerial.flush();  //等待序列埠傳送完畢
  String str=get_response(10000);  //取得 ESP8266 回應字串
  if (str.indexOf("OK") == -1) {return "NG";}
  else {return str.substring(0,str.indexOf("\r\n"));}
  }

String get_baud() {
  sSerial.println("AT+CIOBAUD?");  //取得傳送速率
  sSerial.flush();  //等待序列埠傳送完畢
  String str=get_response(10000);  //取得 ESP8266 回應字串
  if (str.indexOf("OK") != -1) {
    return str.substring(str.indexOf(":")+1,str.indexOf("\r\n"));
    }
  else {return "NG";}
  }

String get_ip() {
  sSerial.println("AT+CIFSR");  //取得 ESP8266 IP
  sSerial.flush();  //等待序列埠傳送完畢
  String str=get_response(10000);  //取得 ESP8266 回應字串
  if (str.indexOf("OK") != -1) {
    return str.substring(0,str.indexOf("\r\n"));
    }
  else {return "NG";}
  }

String get_mode() {
  sSerial.println("AT+CWMODE?");  //取得工作模式
  sSerial.flush();  //等待序列埠傳送完畢
  String str=get_response(10000);  //取得 ESP8266 回應字串
  if (str.indexOf("OK") != -1) {
    String mode=str.substring(str.indexOf(":")+1,str.indexOf("\r\n"));
    if (mode=="1") {return "STATION";}
    else if (mode=="2") {return "AP";}
    else if (mode=="3") {return "AP+STATION";}
    else {return "NG";}
    }
  else {return "NG";}
  }

boolean set_station() {
  sSerial.println("AT+CWMODE=1");  //設定 AP 工作模式
  sSerial.flush();  //等待序列埠傳送完畢
  String str=get_response(10000);  //取得 ESP8266 回應字串
  if (str.indexOf("OK") != -1 || str.indexOf("no change") != -1) {return true;}
  else {return false;}
  }

boolean set_ap() {
  sSerial.println("AT+CWMODE=2");  //設定 AP 工作模式
  sSerial.flush();  //等待序列埠傳送完畢
  String str=get_response(10000);  //取得 ESP8266 回應字串
  if (str.indexOf("OK") != -1 || str.indexOf("no change") != -1) {return true;}
  else {return false;}
  }

boolean set_ap_station() {
  sSerial.println("AT+CWMODE=3");  //設定 AP+Station 工作模式
  sSerial.flush();  //等待序列埠傳送完畢
  String str=get_response(10000);  //取得 ESP8266 回應字串
  if (str.indexOf("OK") != -1 || str.indexOf("no change") != -1) {return true;}
  else {return false;}
  }

String get_mux() {
  sSerial.println("AT+CIPMUX?");  //取得連線模式
  sSerial.flush();  //等待序列埠傳送完畢
  String str=get_response(10000);  //取得 ESP8266 回應字串
  if (str.indexOf("OK") != -1) {
    String mux=str.substring(str.indexOf(":") + 1, str.indexOf("\r\n"));
    if (mux=="0") {return "single";}
    else if (mux=="1") {return "multiple";}
    else {return "NG";}  
    }
  else {return "NG";}
  }

boolean set_single() {
  sSerial.println("AT+CIPMUX=0");  //設定單一連線
  sSerial.flush();  //等待序列埠傳送完畢
  String str=get_response(10000);  //取得 ESP8266 回應字串
  if (str.indexOf("OK") != -1) {return true;}
  else {return false;}
  }

boolean set_multiple() {
  sSerial.println("AT+CIPMUX=1");  //設定多重連線
  sSerial.flush();  //等待序列埠傳送完畢
  String str=get_response(10000);  //取得 ESP8266 回應字串
  if (str.indexOf("OK") != -1) {return true;}
  else {return false;}
  }

String list_ap() {
  sSerial.println("AT+CWLAP");  //取得連線之AP
  sSerial.flush();  //等待序列埠傳送完畢
  String str=get_response(10000);  //取得 ESP8266 回應字串
  if (str.indexOf("OK") != -1) {  //是否有OK
    str.replace("\r\n\r\nOK", "");  //去除結尾的兩個跳行與 OK
    return str;
    }
  else {return "NG";}
  }

boolean join_ap(String ssid, String pwd) {
  sSerial.println("AT+CWJAP=\"" + ssid + "\",\"" + pwd + "\"");  //連線
  sSerial.flush();  //等待序列埠傳送完畢
  String str=get_response(10000);  //取得 ESP8266 回應字串
  if (str.indexOf("OK") != -1) {return true;}
  else {return false;}
  }

String get_ap() {
  sSerial.println("AT+CWJAP?");  //取得連線之AP
  sSerial.flush();  //等待序列埠傳送完畢
  String str=get_response(10000);  //取得 ESP8266 回應字串
  if (str.indexOf("OK") != -1) {
    return str.substring(str.indexOf(":")+1,str.indexOf("\r\n"));
    }
  else {return "NG";}
  }

boolean quit_ap() {
  sSerial.println("AT+CWQAP");  //離線
  sSerial.flush();  //等待序列埠傳送完畢
  String str=get_response(10000);  //取得 ESP8266 回應字串
  if (str.indexOf("OK") != -1) {return true;}
  else {return false;}
  }

boolean start_tcp(String address, int port) {  //單一連線用
  sSerial.println("AT+CIPSTART=\"TCP\",\"" + address + "\"," + String(port));
  sSerial.flush();  //等待序列埠傳送完畢
  String str=get_response(10000, "Linked");  //取得 ESP8266 回應字串
  if (str.indexOf("Linked") != -1) {return true;}
  else {return false;}
  }

boolean start_tcp(byte id, String address, int port) {  //多重連線用
  sSerial.println("AT+CIPSTART=" + String(id) + ",\"TCP\",\"" + address + "\"," + String(port));
  sSerial.flush();  //等待序列埠傳送完畢
  String str=get_response(10000, "Linked");  //取得 ESP8266 回應字串
  if (str.indexOf("Linked") != -1) {return true;}
  else {return false;}
  }

boolean start_udp(String address, int port) {  //單一連線用
  sSerial.println("AT+CIPSTART=\"UDP\",\"" + address + "\"," + String(port));
  sSerial.flush();  //等待序列埠傳送完畢
  String str=get_response(10000);  //取得 ESP8266 回應字串
  if (str.indexOf("OK") != -1) {return true;} //UDP 非連線,是OK, 不是 Linked
  else {return false;}
  }

boolean start_udp(byte id, String address, int port) {  //多重連線用
  sSerial.println("AT+CIPSTART=" + String(id) + "\"UDP\",\"" + address + "\"," + String(port));
  sSerial.flush();  //等待序列埠傳送完畢
  String str=get_response(10000);  //取得 ESP8266 回應字串
  if (str.indexOf("OK") != -1) {return true;} //UDP 非連線,是OK, 不是 Linked
  else {return false;}
  }

boolean send_data(String s) {  //單一連線用
  String s1=s + "\r\n";  //務必加上跳行
  sSerial.println("AT+CIPSEND=" + String(s1.length()));
  sSerial.flush();  //等待序列埠傳送完畢
  String str=get_response(10000, ">");  //取得 ESP8266 回應字串
  if (str.indexOf(">") != -1) {  //收到 > 開始傳送資料
    //Serial.println("Sending ... \r\n" + s1);
    sSerial.println(s1); //傳送資料
    sSerial.flush();  //等待序列埠傳送完畢
    str=get_response(10000, "Unlink");  //取得 ESP8266 回應字串
    if (str.indexOf("+IPD") != -1) {  //傳送成功會自動拆線 Unlink
      //Serial.println(str);    
      return true;
      }
    else {  //傳送不成功須自行拆線
      release();  //關閉 IP 連線
      return false;
      }
    }
  else {  //傳送不成功須自行拆線
    release();  //關閉 IP 連線
    return false;
    }
  }

boolean send_data(byte id,String s) {  //多重連線用
  String s1=s + "\r\n";  //務必加上跳行
  sSerial.println("AT+CIPSEND=" + String(id) + "," + String(s1.length()));
  sSerial.flush();  //等待序列埠傳送完畢
  String str=get_response(10000, ">");  //取得 ESP8266 回應字串
  if (str.indexOf(">") != -1) {  //收到 > 開始傳送資料
    //Serial.println("Sending ... \r\n" + s1);
    sSerial.println(s1); //傳送資料
    sSerial.flush();  //等待序列埠傳送完畢
    str=get_response(10000, "Unlink");  //取得 ESP8266 回應字串
    if (str.indexOf("+IPD") != -1) {  //傳送成功會自動拆線 Unlink
      //Serial.println(str);
      return true;
      }
    else {  //傳送不成功須自行拆線
      release(id);  //關閉 IP 連線
      return false;
      }
    }
  else {  //傳送不成功須自行拆線
    release(id);  //關閉 IP 連線
    return false;
    }
  }

boolean release() { //單一連線
  sSerial.println("AT+CIPCLOSE");  //關閉 IP 連線
  sSerial.flush();  //等待序列埠傳送完畢
  String str=get_response(10000);  //取得 ESP8266 回應字串
  if (str.indexOf("OK") != -1) {return true;}
  else {return false;}
  }

boolean release(byte id) { //多重連線
  sSerial.println("AT+CIPCLOSE=" + String(id));  //關閉 IP 連線
  sSerial.flush();  //等待序列埠傳送完畢
  String str=get_response(10000);  //取得 ESP8266 回應字串
  if (str.indexOf("OK") != -1) {return true;}
  else {return false;}
  }

測試程式執行後輸出如下 :

*** SoftSerial connection to ESP8266 ***
Firmware version : 0018000902
Baud rate : 9600
Get IP : 192.168.43.40
Get mode : STATION
Set AP mode ... OK
Get mode : AP
Set AP+STATION mode ... OK
Get mode : AP+STATION
Set STATION mode ... OK
Get mode : STATION
Set multiple connection ... OK
Get mux : multiple
Set single connection ... OK
Get mux : single
Get AP : "H30-L02-webbot"
Quit AP ... OK
Get AP : NG
Get IP : 0.0.0.0
Join AP ... OK
Get AP : "H30-L02-webbot"
Get IP : 192.168.43.40
Set multiple connection ... OK
Connect Google ... OK
Send GET/ ... OK
Connect Thingspeak ... OK
Send GET/ ... OK
Start UDP ... OK
Release IP ... OK
Set single connection ... OK
Connect Google ... OK
Send GET/ ... OK
Connect Thingspeak ... OK
Send GET/ ... OK
Start UDP ... OK
Release IP ... OK
List APs :
NG

這裡最後呼叫 list_ap() 同樣失敗, 沒有傳回可用 AP. 但若將測試程式 list_ap() 以前的程式碼去除就會有輸出了, 所以我還是覺得可能是記憶體占用太多了, 上面程式占用 FLASH 52%, SRAM 71%.

另外, UDP 傳送資料需要使用字元陣列做傳送接收的緩衝器, 這部分我還不太熟, 等有空再來測試, 留給下一版 v3 再做, 反正 IoT 最主要是用到 TCP 而已.  


2015-11-13 補充 :

今天在檢視 Thingspeak 數據時發現有時 ESP8266 會卡住而沒有傳出資料, 這時需要重設 ESP8266, 但我似乎少寫了 AT+RST 的函式, 補充如下 :

以 0.9.2.2 AT 韌體而言, 下 AT+RST 指令後的回應為 :

OK
bB�鑭b禔S��"愃L�侒��餾�
[System Ready, Vendor:www.ai-thinker.com]
                                                                             
而 0.9.2.4 版的回應末尾有 ready (我跟 XLAN 買的第一顆 ESP8266 就是) :

AT+RST                                                                        
                                                                             
OK                                                                            
7!aS?'??!G?R??                                                          
[Vendor:www.ai-thinker.com Version:0.9.2.4]                                  
                                                                             
ready

參考 : ESP8266 WiFi 模組 AT command 測試

因此以右中括號 "]" 為共通之結尾字元, 然後判斷是否含有 "OK" :

boolean reset() {
  sSerial.println("AT+RST");  //重設 ESP8266
  sSerial.flush();  //等待序列埠傳送完畢
  String str=get_response(10000, "]\r\n");  //取得 ESP8266 回應字串
  if (str.indexOf("OK") != -1) {return true;}
  else {return false;}
  }

之前有看到一些應用範例, 在 loop() 迴圈中的第一件事就是重設 ESP8266.



沒有留言 :