2016年7月2日 星期六

ITEAD WeeESP8266 函式庫測試 (二) : 網頁伺服器

下面內容是繼續去年 2015 年 11 月中旬左右所做的測試改寫的, 那時在完成 ITEAD 的 WeeESP8266 函式庫大部分函式的測試後, 緊接著測試其伺服器用法, 但遇到 send() 無法順利傳送回應訊息的挫折, 只好暫停, 這一停就是半年. 這幾天因為製作 Arduino+ESP8266 IOT 實驗模組的工作告一段落, 所以就找出這個測試紀錄來, 打算繼續完成此實驗, 以竟全功.

我覺得 WeeESP8266 函式庫係上乘之作, 除了耗用記憶體稍多外, 基本上我對此函式庫效能非常滿意, 真的, 專家寫的東東確實不一樣. 參考 :

ITEAD WeeESP8266 函式庫測試
# ESP8266 函式庫記憶體耗損比較

ITEAD 的 WeeESP8266 函式庫 Git 資源與其 API 文件參考 :

https://github.com/itead/ITEADLIB_Arduino_WeeESP8266
http://docs.iteadstudio.com/ITEADLIB_Arduino_WeeESP8266/index.html (API)

WeeESP8266 函式庫的下載安裝方式我記錄在下面這篇 :

# 使用 ITEAD 的 WeeESP8266 函式庫進行網路對時

這裡我大略敘述一下 :

從 WeeESP8266 的 Git 網站按右側欄底下的 "Download zip" 下載函式庫, 解壓縮後將整個目錄複製到 Arduino 安裝目錄的 libraries 下, 或者媒體庫的文件的 Arduino/libraries 下亦可.

如果你是使用 Arduino Mega 這種擁有兩組 UART 埠的板子, 那麼 WeeESP8266 就可以直接使用; 但如果使用 UNO, NANO, Pro mini 等僅有一組硬體 UART 串列埠的話, 你必須使用軟體序列埠跟 ESP8266 通訊, 以便保留硬體串列埠來跟接 PC 的 USB 埠以便上傳程式與監控輸出訊息. 這種情況下必須修改 WeeESP8266 目錄下的 ESP8266.h 檔, 將第 27 行首的註解 // 拿掉, 變成如下 :

#define ESP8266_USE_SOFTWARE_SERIAL

這樣 WeeESP8266 才能透過軟體序列埠與 ESP8266 通訊.

首先我想看看匯入函式庫會佔多少記憶體. 一個如下的 Arduino 程式架構會佔掉 450 bytes 的程式空間 (Flash) 與 9 bytes 的 SRAM :

void setup() {
  // put your setup code here, to run once:
  }
void loop() {
  // put your main code here, to run repeatedly:
  }

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

匯入軟體序列埠函式庫則會多了 266 bytes 程式空間與 68 bytes 的 SRAM, 各耗掉 2% 與 3% 記憶體 :

#include <SoftwareSerial.h>
void setup() {
  // put your setup code here, to run once:
  }
void loop() {
  // put your main code here, to run repeatedly:
  }

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

如果再匯入 WeeESP8266 函式庫, 又再多用掉 6 bytes 程式空間與 SRAM, 共耗掉 2% 與 4% 記憶體 :

#include <SoftwareSerial.h>
#include "ESP8266.h"
void setup() {
  // put your setup code here, to run once:
  }
void loop() {
  // put your main code here, to run repeatedly:
  }

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

所以只是匯入函式庫其實還好, 要呼叫函式的時候才會真正耗用大量記憶體. OK, 接下來要測試如何使用此函式庫將 ESP8266 打造成一個網頁伺服器. 首先拿此函式庫所附的範例來測試, 我已將範例檔 TCPServer.ino 做了一些修改, 把大部分的 print() 改為 println(), 以及加上程式註解如下 :

測試 1 :

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

SoftwareSerial esp8266(7, 8); //建立軟體序列埠物件 (RX:D7, TX:D8)
ESP8266 wifi(esp8266); //建立 ESP8266 物件

void setup(void) {
  //起始硬體串列埠
  Serial.begin(9600);
  //顯示 ESP8266 AT 韌體版本
  Serial.print("FW version : ");
  Serial.println(wifi.getVersion().c_str());
  //設定 ESP8266 為模式 3 (Station + AP)
  if (wifi.setOprToStationSoftAP()) {Serial.println("Set STA+AP mode (3) ... OK");}
  else {Serial.println("Set STA+AP mode (3) ... NG");}
  //開啟 ESP8266 多重連線
  if (wifi.enableMUX()) {Serial.println("Set multiple connection ... OK");}
  else {Serial.println("Set multiple connection ... NG");}
  //啟動 ESP8266 伺服器
  if (wifi.startTCPServer(80)) {Serial.println("Start TCP server ... OK");}
  else {Serial.println("Start TCP server ... NG");}
  //設定 ESP8266 TCP 連線回應逾時計時器 (10 秒)
  if (wifi.setTCPServerTimeout(10)) {Serial.println("Set TCP server timout ... OK");}
  else {Serial.println("Set TCP server timout ... NG");}
  }

void loop(void) {
  //設定送收緩衝器 buffer 與 TCP 連線通道 mux_id (0~4)
  uint8_t buffer[256]={0}; 
  uint8_t mux_id;
  //讀取 TCP 連線通道存至 mux_id, 回應資料存至緩衝器 buffer (計時 1 秒)
  uint32_t len=wifi.recv(&mux_id, buffer, sizeof(buffer), 1000); //傳回資料長度
  if (len > 0) { //有收到 ESP8266 回應資料
    //顯示 TCP 連線狀態
    Serial.print("Status:[");
    Serial.print(wifi.getIPStatus().c_str());
    Serial.println("]");
    //顯示 TCP 連線通道
    Serial.print("Received from :");
    Serial.print(mux_id);
    Serial.print("[");
    //顯示所收到的 ESP8266 回應資料 (TCP Payload), 將整數轉成可讀字元
    for (uint32_t i=0; i<len; i++) {Serial.print((char)buffer[i]);}
    Serial.println("]");
    //將所收到的 TCP Payload (回應資料) 原封不動傳回給 Client 端
    if (wifi.send(mux_id, buffer, len)) {Serial.println("Send back ... OK");}
    else {Serial.println("Send back ... NG");}
    //釋放 TCP 連線
    Serial.print("Release TCP connection ");  
    Serial.print(mux_id);
    if (wifi.releaseTCP(mux_id)) {Serial.println(" ... OK");}
    else {Serial.println(" ... NG");}
    //顯示連線狀態
    Serial.print("Status:[");
    Serial.print(wifi.getIPStatus().c_str());
    Serial.println("]");
    }
  }

要把 ESP8266 當作網頁伺服器, 其工作模式可以是 STA (mode 1), AP (mode 2), STA+AP (mode 3) 三個模式的任何一個都可以, 只要讓 ESP8266 能取得 IP 即可. 模式 1 必須連線到一個無線基地台 (AP), 由其指派一個區網 IP 來作為網頁伺服器的 IP. 模式 2 則固定會產生一個 192.168.4.1 的IP 作為 SoftAP 的網址, 這也可以作為網頁伺服器的網址. 而模式 3 則會有 192.168.4.1 與預設 0.0.0.0 的區網 IP, 如果用 joinAP() 連上其他基地台的話, 這個 0.0.0.0 就會被 DHCP 指派的 IP 取代. 上面程式我們使用模式 3, 因為不需要連線到基地台就有一個現成的 IP 192.168.4.1 可用, 測試起來比較簡單.

要讓 ESP8266 變成網頁伺服器, 必須呼叫下面這兩個指令才行 :
  1. enableMUX() : 開啟多重連線 (讓多個 client 連線進來)
  2. startTCPServer(80) : 啟動 TCP 網頁伺服器 (80 port)

這裡雖說多個連線, 實際上 ESP8266 最多只容許同時 5 個連線 (Cannel 0~4). 在 setup() 中做好這些設定之後, ESP8266 就變成一個網頁伺服器了, 但在 Arduino + ESP8266 的組合裡, ESP8266 不負責邏輯判斷, 只是利用軟體序列埠轉送客戶端要求給 Arduino, 由 Arduino 來決定該回應甚麼給客戶端, 類似 PHP 的網頁引擎功能.

所以 Arduino 接下來便進入 loop() 迴圈中不斷地偵測是否有收到 ESP8266 轉來的客戶端要求 (request), 我們利用 WeeESP8266 函式庫的 recv() 函式來讀取軟體序列埠緩衝區, 它會傳回接收緩衝區已收到資料的長度, 若長度不為 0 表示有收到客戶端要求, 須進行要求訊息的解讀, 擷取 客戶端要求的網址 URL (帶在 HTTP 訊息中), 以決定該回應甚麼訊息. 函式 recv() 的 API 如下 :

uint32_t recv (uint8_t mux_id, uint8_t *buffer, uint32_t buffer_size, uint32_t timeout=1000)

呼叫函式 recv() 前必須先定義一個連線通道變數 mux_id 與一個接收緩衝區 buffer, 作為第一與第二參數傳進去, 而第三參數則是此緩衝區的大小, 可用 sizeof(buffer) 取得. 第四參數可有可為的回應計時器, 預設是 1 秒.

當接收到 ESP8266 傳來的客戶端要求後, 我們使用迴圈來 dump 接收緩衝器就可以看到這些 HTTP 要求訊息, 注意, 因為 buffer 是 uint32_t 類型的整數, 因此必須用 char 來強制轉型為可讀的 ASCII 字元才能了解. 上面的程式中, 我們這個 Arduino 網頁引擎只是簡單地將收到的客戶端要求原封不動地用 send() 函式傳回給客戶端而已 (這算啥伺服器啊, echo server 嗎?).

Send() 的 API 如下 :

bool send (uint8_t mux_id, const uint8_t *buffer, uint32_t len)

同樣地須傳入三個參數 : 連線通道, 回應資料, 以及資料長度. 上面程式就是把接收緩衝區直接放進去, 傳回客戶端而已. 注意, 接收函式 recv() 要傳入通道編號 mux_id 的位址 (傳址呼叫), 這樣才能在函式內取得通道編號後改變外面變數之值; 而傳送函式 send() 則是傳入通道編號 mux_id 的值, 因為其值不需要更改.

接下來打開手機的 Wifi 功能, 連線 ESP8266 在 mode2/3 模式下所開啟的基地台 (韌體 0.9.2 下是 ESP_ 開頭的熱點) :



連線成功後打開手機瀏覽器, 輸入伺服器網址 192.168.4.1 即可看到 Arduino 回應的 HTTP request 訊息 :


上面第一排的 GET / HTTP/1.1 便是 HTTP 標頭中與客戶端所要求之 URI 有關之訊息, 要求的 URI 是根目錄 /. 

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

FW version : 0018000902
Set STA+AP mode (3) ... OK
Set multiple connection ... OK
Start TCP server ... OK
Set TCP server timout ... OK
Status:[android.browser
]
Received from :0[GET / HTTP/1.1
Host: 192.168.4.1
Connection: keep-alive
Cache-Control: max-age=0
x-wap-profile: http://wap1.huawei.com/uaprof/HONOR_H30-L02_Global_UAProfile.xml
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
User-A]
Send back ... OK
Release TCP connection 0 ... OK
Status:[STATUS:3
+CIPSTATUS:1,"TCP","192.168.4.100",60034,1]
Status:[android.browser
]
Received from :0[GET / HTTP/1.1
Host: 192.168.4.1
Connection: keep-alive
Cache-Control: max-age=0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
x-wap-profile: http://wap1.huawei.com/uaprof/HONOR_H30-L02_Global_UAProfile.xml
User-A]
Send back ... OK
Release TCP connection 0 ... OK
Status:[STATUS:3
+CIPSTATUS:1,"TCP","192.168.4.102",59558,1]

上面測試 1 的程式編譯後所佔記憶體如下 : 

草稿碼使用了 12,584 bytes (40%) 的程式存儲空間。最大值為 30,720 bytes。
全域變數使用了 987 bytes (48%) 的動態記憶體,剩餘 1,061 bytes 供局部變數。最大值為 2,048 bytes 。

哇咧, 已經快用掉一半啦! 

如果要回送自訂內容給客戶端, 就要在送收緩衝器 buffer 的內容上動手腳, 例如下列測試 2 是回送 Hello! 加上開機後的毫秒數 :

測試 2 :

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

SoftwareSerial esp8266(7, 8); //建立軟體序列埠物件 (RX:D7, TX:D8)
ESP8266 wifi(esp8266); //建立 ESP8266 物件

void setup(void) {
  //起始硬體串列埠
  Serial.begin(9600);
  //顯示 ESP8266 AT 韌體版本
  Serial.print("FW version : ");
  Serial.println(wifi.getVersion().c_str());
  //設定 ESP8266 為模式 3 (Station + AP)
  if (wifi.setOprToStationSoftAP()) {Serial.println("Set STA+AP mode (3) ... OK");}
  else {Serial.println("Set STA+AP mode (3) ... NG");}
  //開啟 ESP8266 多重連線
  if (wifi.enableMUX()) {Serial.println("Set multiple connection ... OK");}
  else {Serial.println("Set multiple connection ... NG");}
  //啟動 ESP8266 伺服器
  if (wifi.startTCPServer(80)) {Serial.println("Start TCP server ... OK");}
  else {Serial.println("Start TCP server ... NG");}
  //設定 ESP8266 TCP 連線回應逾時計時器
  if (wifi.setTCPServerTimeout(10)) {Serial.println("Set TCP server timout ... OK");}
  else {Serial.println("Set TCP server timout ... NG");}
  }

void loop(void) {
  //設定送收緩衝器 buffer 與 TCP 連線通道 mux_id (0~4)
  uint8_t buffer[256]={0};
  uint8_t mux_id;
  //讀取 TCP 連線通道存至 mux_id, 回應資料存至緩衝器 buffer (計時 1 秒)
  uint32_t len=wifi.recv(&mux_id, buffer, sizeof(buffer), 1000); //傳回資料長度
  if (len > 0) { //有收到 ESP8266 回應資料
    //顯示 TCP 連線狀態
    Serial.print("Status:[");
    Serial.print(wifi.getIPStatus().c_str());
    Serial.println("]");
    //顯示 TCP 連線通道
    Serial.print("Received from :");
    Serial.print(mux_id);
    Serial.print("[");
    //顯示所收到的 ESP8266 回應資料 (TCP Payload), 將整數轉成可讀字元
    for (uint32_t i=0; i<len; i++) {Serial.print((char)buffer[i]);}
    Serial.println("]");
    //回應 Client 端 :
    memset(buffer, 0, sizeof(buffer)); //清除緩衝區
    String data="<b>Hello!</b> " + String(millis()) + "\r\n";
    Serial.println(data);
    //將要傳送的字串資料複製到送收緩衝器 buffer
    data.getBytes(buffer, data.length());    
    if (wifi.send(mux_id, buffer, data.length())) {Serial.println("Send back ... OK");}
    else {Serial.println("Send back ... NG");}
    //釋放 TCP 連線
    Serial.print("Release TCP connection ");  
    Serial.print(mux_id);
    if (wifi.releaseTCP(mux_id)) {Serial.println(" ... OK");}
    else {Serial.println(" ... NG");}
    //顯示連線狀態
    Serial.print("Status:[");
    Serial.print(wifi.getIPStatus().c_str());
    Serial.println("]");
    }
  }

上面藍色部分就是測試 2 與測試 1 不同的部分, 這裡我們先呼叫 C 語言的 memset() 函數清除緩衝區 buffer 裡面所儲存之接收資料, 然後定義一個要傳送的字串 data, 由於緩衝區是整數型態, 因此要使用 getBytes() 函式將要傳送給客戶端的字串一個 byte 一個 byte 複製到送收緩衝器 buffer 裡. 注意, 這裡要傳送的字串必須以跳行字元 "\r\n" 結尾, 否則毫秒數的最後一位會被吃掉. 其次 send() 函數的第三參數必須取 data 的長度, 亦即須與實際傳送之資料相符, 否則會出現亂碼.

這樣從手機瀏覽器連線至 ESP8266 伺服器 192.168.4.1 時, 就會看到如下輸出 :

每次瀏覽 192.168.4.1 就會看到毫秒數不斷增加.

注意, 因為傳送內容裡含有 HTML 標籤, 雖然沒有完整的 HTML 格式, 但瀏覽器仍以 HTML 檔解讀, 因此會顯示正確內容. 如果將 data 中的 <b> 與 </b> 去除的話, 瀏覽器會認為這是一個非 HTML 文件, 會彈出視窗詢問是否要下載存檔喔. 如果資料前後用 <html> 與 </html> 包起來是比較不會有問題, 當然最保險還是傳送 HTTP 標頭啦!

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

FW version : 0018000902
Set STA+AP mode (3) ... OK
Set multiple connection ... OK
Start TCP server ... OK
Set TCP server timout ... OK
Status:[]
Received from :0[GET / HTTP/1.1
Host: 192.168.4.1
Connection: keep-alive
Cache-Control: max-age=0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
x-wap-profile: http://wap1.huawei.com/uaprof/HONOR_H30-L02_Global_UAProfile.xml,http://]
Hello! 6438

Send back ... OK
Release TCP connection 0 ... OK
Status:[STATUS:3
+CIPSTATUS:1,"TCP","192.168.4.102",33112,1]

最近我在 mlwmlw 的 Github 上找到關於 WeeESP8266 的伺服器範例, 發現要傳送的資料是字串常數的話  (即長度固定), 使用 send() 傳送回應資料有更簡單的方法, 那就是直接使用 uint_8 型態來定義要傳送的回應資為一字串陣列即可, 參考 :

mlwmlw weeesp8277 web server example
mlwmlw WeeESP8266 thingspeak example

但是上面測試 2 要回應的訊息含有無法預測長度之毫秒數, 所以沒辦法這麼做, 必須改為固定傳回 "Hello World!" 才能使用此方法, 如下列測試 3 : 

測試 3 :

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

SoftwareSerial esp8266(7, 8); //建立軟體序列埠物件 (RX:D7, TX:D8)
ESP8266 wifi(esp8266); //建立 ESP8266 物件

void setup(void) {
  //起始硬體串列埠
  Serial.begin(9600);
  //顯示 ESP8266 AT 韌體版本
  Serial.print("FW version : ");
  Serial.println(wifi.getVersion().c_str());
  //設定 ESP8266 為模式 3 (Station + AP)
  if (wifi.setOprToStationSoftAP()) {Serial.println("Set STA+AP mode (3) ... OK");}
  else {Serial.println("Set STA+AP mode (3) ... NG");}
  //開啟 ESP8266 多重連線
  if (wifi.enableMUX()) {Serial.println("Set multiple connection ... OK");}
  else {Serial.println("Set multiple connection ... NG");}
  //啟動 ESP8266 伺服器
  if (wifi.startTCPServer(80)) {Serial.println("Start TCP server ... OK");}
  else {Serial.println("Start TCP server ... NG");}
  //設定 ESP8266 TCP 連線回應逾時計時器
  if (wifi.setTCPServerTimeout(10)) {Serial.println("Set TCP server timout ... OK");}
  else {Serial.println("Set TCP server timout ... NG");}
  }

void loop(void) {
  //設定送收緩衝器 buffer 與 TCP 連線通道 mux_id (0~4)
  uint8_t buffer[256]={0};
  uint8_t mux_id;
  //讀取 TCP 連線通道存至 mux_id, 回應資料存至緩衝器 buffer (計時 1 秒)
  uint32_t len=wifi.recv(&mux_id, buffer, sizeof(buffer), 1000); //傳回資料長度
  if (len > 0) { //有收到 ESP8266 回應資料
    //顯示 TCP 連線狀態
    Serial.print("Status:[");
    Serial.print(wifi.getIPStatus().c_str());
    Serial.println("]");
    //顯示 TCP 連線通道
    Serial.print("Received from :");
    Serial.print(mux_id);
    Serial.print("[");
    //顯示所收到的 ESP8266 回應資料 (TCP Payload), 將整數轉成可讀字元
    for (uint32_t i=0; i<len; i++) {Serial.print((char)buffer[i]);}
    Serial.println("]");
    //回應 Client 端
    uint8_t data[]="<b>Hello! "
    "World!</b>";
    if (wifi.send(mux_id, data, sizeof(data))) {Serial.println("Send response ... OK");}
    else {Serial.println("Send response ... NG");}
    //釋放 TCP 連線
    Serial.print("Release TCP connection ");  
    Serial.print(mux_id);
    if (wifi.releaseTCP(mux_id)) {Serial.println(" ... OK");}
    else {Serial.println(" ... NG");}
    //顯示連線狀態
    Serial.print("Status:[");
    Serial.print(wifi.getIPStatus().c_str());
    Serial.println("]");
    }
  }


注意, 上面範例只是要表達 : 一長串要回應的字串常數可以分成多段, 在適當長度時用雙引號括起來直接跳行即可. 像上面的 Hello! World! 很短其實只要寫成一行即可 :

uint8_t data[]="Hello! World!";

其次, 採用這種做法時, 回應字串最後面不用加 "\r\n".

接下來我們要處理客戶端要求不同的 URL 位址時的情況, 例如要求 192.168.4.1/hello 回應 "Hello! World!"; 而要求 192.168.4.1/bye 時就回應 "Bye bye!", 都不是則回應 "Sorry!", 如下面測試 4 :

測試 4 :

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

SoftwareSerial esp8266(7, 8); //建立軟體序列埠物件 (RX:D7, TX:D8)
ESP8266 wifi(esp8266); //建立 ESP8266 物件

void setup(void) {
  //起始硬體串列埠
  Serial.begin(9600);
  //顯示 ESP8266 AT 韌體版本
  Serial.print("FW version : ");
  Serial.println(wifi.getVersion().c_str());
  //設定 ESP8266 為模式 3 (Station + AP)
  if (wifi.setOprToStationSoftAP()) {Serial.println("Set STA+AP mode (3) ... OK");}
  else {Serial.println("Set STA+AP mode (3) ... NG");}
  //開啟 ESP8266 多重連線
  if (wifi.enableMUX()) {Serial.println("Set multiple connection ... OK");}
  else {Serial.println("Set multiple connection ... NG");}
  //啟動 ESP8266 伺服器
  if (wifi.startTCPServer(80)) {Serial.println("Start TCP server ... OK");}
  else {Serial.println("Start TCP server ... NG");}
  //設定 ESP8266 TCP 連線回應逾時計時器
  if (wifi.setTCPServerTimeout(10)) {Serial.println("Set TCP server timout ... OK");}
  else {Serial.println("Set TCP server timout ... NG");}
  }

void loop(void) {
  //設定送收緩衝器 buffer 與 TCP 連線通道 mux_id (0~4)
  uint8_t buffer[256]={0};
  uint8_t mux_id;
  //讀取 TCP 連線通道存至 mux_id, 回應資料存至緩衝器 buffer (計時 1 秒)
  uint32_t len=wifi.recv(&mux_id, buffer, sizeof(buffer), 1000); //傳回資料長度
  if (len > 0) { //有收到 ESP8266 回應資料
    //顯示 TCP 連線狀態
    Serial.print("Status:[");
    Serial.print(wifi.getIPStatus().c_str());
    Serial.println("]");
    //顯示 TCP 連線通道
    Serial.print("Received from :");
    Serial.print(mux_id);
    Serial.print("[");
    //顯示所收到的 ESP8266 回應資料 (TCP Payload), 將整數轉成可讀字元
    for (uint32_t i=0; i<len; i++) {Serial.print((char)buffer[i]);}
    Serial.println("]");
    //解析 HTTP 標頭的 URI, 擷取客戶端要求之網頁路徑 (router)
    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:hello/?para1=a&para2=b
    //回應 Client 端
    if (path.startsWith("hello")) { //show Hello! World!  
      uint8_t data[]="<b>Hello! World!</b>";
      if (wifi.send(mux_id, data, sizeof(data))) {Serial.println("Send response ... OK");}
      else {Serial.println("Send response ... NG");}    
      }
    else if (path.startsWith("bye")) { //show Bye bye!
      uint8_t data[]="<b>Bye bye!</b>";
      if (wifi.send(mux_id, data, sizeof(data))) {Serial.println("Send response ... OK");}
      else {Serial.println("Send response ... NG");}  
      }
    else {
      uint8_t data[]="<b>Sorry!</b>";
      if (wifi.send(mux_id, data, sizeof(data))) {Serial.println("Send response ... OK");}
      else {Serial.println("Send response ... NG");}
      }
    //釋放 TCP 連線
    Serial.print("Release TCP connection ");  
    Serial.print(mux_id);
    if (wifi.releaseTCP(mux_id)) {Serial.println(" ... OK");}
    else {Serial.println(" ... NG");}
    //顯示連線狀態
    Serial.print("Status:[");
    Serial.print(wifi.getIPStatus().c_str());
    Serial.println("]");
    }
  }

首先宣告一個 path 字串來儲存 HTTP 標頭中的 URI 路徑, 然後掃描緩衝區, 一個 byte 一個 byte 讀進來, 用 (char) 將其強制轉型為字元後再用 concat() 串成字串, 直到出現 " HTTP/" 即可停止.

    for (uint32_t i=0; i<len; i++) {
      if (!path.endsWith(" HTTP/")) {path.concat((char)buffer[i]);}
      else {break;}
      }

如此得到的 path 字串應該如下 :

GET / HTTP/  (這是網址列輸入 192.168.4.1 的結果)
GET /hello HTTP/  (這是網址列輸入 192.168.4.1/hello 的結果)
GET /bye HTTP/  (這是網址列輸入 192.168.4.1/bye 的結果)

然後使用 substring() 把根目錄後面的路徑抓出來 :

   path=path.substring(path.indexOf("/") + 1, path.indexOf(" HTTP/"));

這樣上面三種標頭會分別得到個別的 path 字串 : "",  "hello", 以及 "bye". 到此就可以用 startsWith() 函式來判別要回應不同的訊息給客戶端了. 以手機瀏覽器輸入不同網址, 果真得到預期的回應如下 :




今天一整天都在研究這個, 總算有滿意的結果了. 雖然說使用函式庫比較吃記憶體, 但是整個程式變得簡短易讀, 而且感覺運作起來蠻穩定的耶! 從去年底開始玩 WeeESP8266 函式庫到現在才把最後的伺服器部分搞定, 已過了半年了, 那時是因為對 send() 函數的使用方式不熟, 試了好幾次都失敗, 以為是記憶體耗損太多, 自己對 C 不熟, 只好束之高閣, 轉而研究 C 語言, 但隨後又被別的事物打斷, 真是好事多磨.

參考 :

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語言的函式指標以及函式指標陣列


沒有留言 :