2016年7月31日 星期日

★ 利用網頁控制 Arduino (二)

又到周末了, 昨天早上岳母突然打電話問我幾點回鄉下, 想要搭我便車回去, 我說是傍晚, 因為姊姊下午要去畫室, 傍晚要上數學, 時間比較趕, 我騎摩托車接她回來比較快. 姐姐聽我這樣說, 就說她可以搭捷運也很快, 所以早上便回鄉下了. 吃過中飯後在冰箱找到上週買的冰棒, 實在太棒了! 這夏日的午後悶熱難耐, 還是躲在書房吹電扇寫程式做實驗快活些.

今天繼續用 Arduino + ESP8266 來做  "Arduino Cookbook 錦囊妙計這本書 15.8 節的實驗, 它是在 15.7 節的基礎上擴展為依據要求不同的網址, 可顯示多個不同的網頁, 具體來說, 例如要求 192.168.2.114/digital/ 就顯示所有數位腳位的狀態; 而要求 192.168.2.114/analog/ 就顯示所有類比腳位的狀態. 事實上只要參考之前製作 IOT 模組時寫的 wifi 設定功能程式碼, 修改一下 15.7 節程式中的路徑處理部分就可以了, 參考 :

# 利用網頁控制 Arduino (一)
# 製作 Arduino Nano + ESP8266 物聯網模組 (三)

測試 1 :

#include <SoftwareSerial.h>
#define DEBUG true

SoftwareSerial esp8266(7,8); //(RX,TX)
String ssid="EDIMAX-tony";
String pwd="1234567890";
const int MAX_PAGE_NAME_LEN=8;  //buffer size
char buffer[MAX_PAGE_NAME_LEN + 1]; //store page_name

void setup() {
  Serial.begin(9600);
  esp8266.begin(9600);
  sendData("AT+RST\r\n",2000,DEBUG); // reset ESP8266
  sendData("AT+CWMODE=1\r\n",1000,DEBUG); //configure as station
  sendData("AT+CIPMUX=1\r\n",1000,DEBUG); //enable multiple connections
  sendData("AT+CIPSERVER=1,80\r\n",2000,DEBUG); //turn on server 80 port  
  while (!connectWifi(ssid, pwd)) {
    Serial.println("Connecting WiFi ... failed");
    delay(2000);
    }
  sendData("AT+GMR\r\n",1000,DEBUG);
  delay(3000); //wait for wifi connection to get local ip
  sendData("AT+CIFSR\r\n",1000,DEBUG); //get ip address
  }

void loop() {
  if (esp8266.available()) { // check if esp8266 is sending message
    if (esp8266.find("+IPD,")) {
      delay(1000); //waiting for response: ?pinD2=1&pinA2=1023
      int connectionId=esp8266.read()-48;  //turn ASCII to number
      if (esp8266.find("GET /")) { //retrieve page name (router)
        memset(buffer, 0, sizeof(buffer));  //clear buffer (all set to 0)
        String webpage="<html><body>";
        if (esp8266.readBytesUntil('/', buffer, sizeof(buffer))) {
           if (strcmp(buffer, "analog") == 0) { //show analog pins
            Serial.println("analog");
            webpage += "<h1>Analog Pins</h1>";
            for (byte i=0; i<6; i++) {
              webpage += "analog pin ";
              webpage += i;
              webpage += " is ";
              webpage += analogRead(i);
              webpage += "<br>";
              }
            }
          else if (strcmp(buffer, "digital") == 0) {
            Serial.println("digital");
            webpage += "<h1>Digital Pins</h1>";          
            for (byte i=2; i<14; i++) {
              webpage += "digital pin ";
              webpage += i;
              webpage += " is ";
              if (digitalRead(i) == 1) {webpage += "HIGH";}
              else {webpage += "LOW";}
              webpage += "<br>";
              }    
            }
          else {
            webpage += "<h1>Unknown Page</h1>";
            webpage += "Recognized Pages are : <br>";
            webpage += "192.168.x.x/analog/<br>";
            webpage += "192.168.x.x/digital/<br>";
            }
          }
        webpage += "</body></html>";
        String cipSend="AT+CIPSEND=";
        cipSend += connectionId;
        cipSend += ",";
        cipSend += webpage.length();
        cipSend += "\r\n";
        sendData(cipSend,1000,DEBUG);
        sendData(webpage,2000,DEBUG);
        sendData("AT+CIPCLOSE=" + (String)connectionId + "\r\n",3000,DEBUG);        
        }
      }
    }
  }

boolean connectWifi(String ssid, String pwd) {
  String res=sendData("AT+CWJAP=\"" + ssid + "\",\"" + pwd + "\"\r\n",8000,DEBUG);
  res.replace("\r\n",""); //remove all line terminator
  if (res.indexOf("OK") != -1) {return true;}
  else {return false;}
  }

String sendData(String command, const int timeout, boolean debug) {
  String res="";
  esp8266.print(command);
  long int time=millis();
  while ((time + timeout) > millis()) {
    while(esp8266.available()) {res.concat((char)esp8266.read());}
    }
  if (debug) {Serial.print(res);}
  return res;
  }

因為要擷取網址根目錄下的路徑並進行比對以決定輸出何種網頁, 需要一個 字元陣列 buffer 來儲存路徑, 因為 digital 有 7 個字元, 因此這裡宣告一個長度為 8 字元的緩衝區即足夠.

如果客戶端連線 192.168.x.x/digial/ 的話, ESP8266 會回應含有 GET /digital/ HTTP1.1 標頭的訊息到緩衝區裡, 因此可用 find() 函數找到連線要求的標記 "GET /", 然後以 readBytesUntil() 找尋下一個 "/", 將之間的路徑字元串 digital 存入 buffer 裡, 這樣便能用 strcmp() 函數來比對了.

此程式編譯後記憶體耗用情形如下 :

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

程式上傳後從序列埠監控視窗得知獲得 192.168.2.106 的 IP :

AT+RST


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

no change
AT+CIPMUX=1


OK
AT+CIPSERVER=1,80


OK
AT+CWJAP="EDIMAX-tony1966","1234567890"


OK
AT+GMR

0018000902

OK
AT+CIFSR

192.168.2.106

OK

以手機瀏覽器連線 ESP8266 伺服器, 分別輸入 192.168.2.106/digital/, 192.168.2.106/analog/, 以及 192.168.2.106/hello/, 結果如下 :




我們也可以把前一篇實驗裡更改 Arduino 輸出腳位狀態的功能整合進來, 增加一個 update 路徑如下 :

192.168.x.x/update/?pinD3=1&pinA2=1023

程式如下 :

測試 2 :

#include <SoftwareSerial.h>
#define DEBUG true

SoftwareSerial esp8266(7,8); //(RX,TX)
String ssid="EDIMAX-tony";
String pwd="1234567890";
const int MAX_PAGE_NAME_LEN=8;  //buffer size
char buffer[MAX_PAGE_NAME_LEN + 1]; //store page_name

void setup() {
  Serial.begin(9600);
  esp8266.begin(9600);
  sendData("AT+RST\r\n",2000,DEBUG); // reset ESP8266
  sendData("AT+CWMODE=1\r\n",1000,DEBUG); //configure as station
  sendData("AT+CIPMUX=1\r\n",1000,DEBUG); //enable multiple connections
  sendData("AT+CIPSERVER=1,80\r\n",2000,DEBUG); //turn on server 80 port  
  while (!connectWifi(ssid, pwd)) {
    Serial.println("Connecting WiFi ... failed");
    delay(2000);
    }
  sendData("AT+GMR\r\n",1000,DEBUG);
  delay(3000); //wait for wifi connection to get local ip
  sendData("AT+CIFSR\r\n",1000,DEBUG); //get ip address
  }

void loop() {
  if (esp8266.available()) { // check if esp8266 is sending message
    if (esp8266.find("+IPD,")) {
      delay(1000); //waiting for response: ?pinD2=1&pinA2=1023
      int connectionId=esp8266.read()-48;  //turn ASCII to number
      if (esp8266.find("GET /")) { //retrieve page name (router)
        memset(buffer, 0, sizeof(buffer));  //clear buffer (all set to 0)
        String webpage="<html><body>";
        if (esp8266.readBytesUntil('/', buffer, sizeof(buffer))) {
           if (strcmp(buffer, "analog") == 0) { //show analog pins
            Serial.println("analog");
            webpage += "<h1>Analog Pins</h1>";
            for (byte i=0; i<6; i++) {
              webpage += "analog pin ";
              webpage += i;
              webpage += " is ";
              webpage += analogRead(i);
              webpage += "<br>";
              }
            }
          else if (strcmp(buffer, "digital") == 0) {
            Serial.println("digital");
            webpage += "<h1>Digital Pins</h1>";          
            for (byte i=2; i<14; i++) {
              webpage += "digital pin ";
              webpage += i;
              webpage += " is ";
              if (digitalRead(i) == 1) {webpage += "HIGH";}
              else {webpage += "LOW";}
              webpage += "<br>";
              }    
            }
          else if (strcmp(buffer, "update") == 0) {
            Serial.println("update");
            while (esp8266.findUntil("pin", "\r\n")) {
              char type=(char)esp8266.read();
              int pin=esp8266.parseInt();
              int val=esp8266.parseInt();
              if (type=='D') {
                Serial.print("Digital pin ");
                webpage += "Digital pin ";
                webpage += pin;
                webpage += " is updated to";
                webpage += val;
                pinMode(pin, OUTPUT);
                digitalWrite(pin, val);
                }
              else if (type=='A') {
                Serial.print("Analog pin ");
                webpage += "Analog pin ";
                webpage += pin;
                webpage += " is updated to ";
                webpage += val;                
                analogWrite(pin, val);        
                }
              else {
                Serial.println("Unexpected type:" + type);
                }
              Serial.print(pin);
              Serial.print("=");
              Serial.println(val);  
              webpage += "<br>";       
              }   
            }            
          else {
            webpage += "<h1>Unknown Page</h1>";
            webpage += "Recognized Pages are : <br>";
            webpage += "192.168.x.x/analog/<br>";
            webpage += "192.168.x.x/digital/<br>";
            }
          }
        webpage += "</body></html>";
        String cipSend="AT+CIPSEND=";
        cipSend += connectionId;
        cipSend += ",";
        cipSend += webpage.length();
        cipSend += "\r\n";
        sendData(cipSend,1000,DEBUG);
        sendData(webpage,2000,DEBUG);
        sendData("AT+CIPCLOSE=" + (String)connectionId + "\r\n",3000,DEBUG);        
        }
      }
    }
  }

boolean connectWifi(String ssid, String pwd) {
  String res=sendData("AT+CWJAP=\"" + ssid + "\",\"" + pwd + "\"\r\n",8000,DEBUG);
  res.replace("\r\n",""); //remove all line terminator
  if (res.indexOf("OK") != -1) {return true;}
  else {return false;}
  }

String sendData(String command, const int timeout, boolean debug) {
  String res="";
  esp8266.print(command);
  long int time=millis();
  while ((time + timeout) > millis()) {
    while(esp8266.available()) {res.concat((char)esp8266.read());}
    }
  if (debug) {Serial.print(res);}
  return res;
  }

此程式編譯後記憶體耗用情形如下 :

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

執行結果如下 :


在上面的實驗中, 路徑 digital 與 analog 的腳位狀態也可以用 HTML 的表格來呈現, 這樣會比較整齊美觀, 這也是  "Arduino Cookbook 錦囊妙計這本書 15.9 節的實驗, 其實也不過就是在輸出網頁格式上加上 TABLE 的相關元素而已, 如下列測試 3 所示 :

測試 3 :

#include <SoftwareSerial.h>
#define DEBUG true

SoftwareSerial esp8266(7,8); //(RX,TX)
String ssid="EDIMAX-tony";
String pwd="1234567890";
const int MAX_PAGE_NAME_LEN=8;  //buffer size
char buffer[MAX_PAGE_NAME_LEN + 1]; //store page_name

void setup() {
  Serial.begin(9600);
  esp8266.begin(9600);
  sendData("AT+RST\r\n",2000,DEBUG); // reset ESP8266
  sendData("AT+CWMODE=1\r\n",1000,DEBUG); //configure as station
  sendData("AT+CIPMUX=1\r\n",1000,DEBUG); //enable multiple connections
  sendData("AT+CIPSERVER=1,80\r\n",2000,DEBUG); //turn on server 80 port  
  while (!connectWifi(ssid, pwd)) {
    Serial.println("Connecting WiFi ... failed");
    delay(2000);
    }
  sendData("AT+GMR\r\n",1000,DEBUG);
  delay(3000); //wait for wifi connection to get local ip
  sendData("AT+CIFSR\r\n",1000,DEBUG); //get ip address
  }

void loop() {
  if (esp8266.available()) { // check if esp8266 is sending message
    if (esp8266.find("+IPD,")) {
      delay(1000); //waiting for response: ?pinD2=1&pinA2=1023
      int connectionId=esp8266.read()-48;  //turn ASCII to number
      if (esp8266.find("GET /")) { //retrieve page name (router)
        memset(buffer, 0, sizeof(buffer));  //clear buffer (all set to 0)
        String webpage="<html><body>";
        if (esp8266.readBytesUntil('/', buffer, sizeof(buffer))) {
           if (strcmp(buffer, "analog") == 0) { //show analog pins
            Serial.println("analog");
            webpage += "<h1>Analog Pins</h1>";
            webpage += "<table border=1>";
            for (byte i=0; i<6; i++) {
              webpage += "<tr><td>analog pin ";
              webpage += i;
              webpage += "</td><td>";
              webpage += analogRead(i);
              webpage += "</td></tr>";
              }
            webpage += "</table>";
            }
          else if (strcmp(buffer, "digital") == 0) {
            Serial.println("digital");
            webpage += "<h1>Digital Pins</h1>";
            webpage += "<table border=1>";          
            for (byte i=2; i<=9; i++) {
              webpage += "<tr><td>digital pin ";
              webpage += i;
              webpage += "</td><td>";
              if (digitalRead(i) == 1) {webpage += "HIGH";}
              else {webpage += "LOW";}
              webpage += "</td></tr>";
              }
            webpage += "</table>";  
            }
          else if (strcmp(buffer, "update") == 0) {
            Serial.println("update");
            while (esp8266.findUntil("pin", "\r\n")) {
              char type=(char)esp8266.read();
              int pin=esp8266.parseInt();
              int val=esp8266.parseInt();
              if (type=='D') {
                Serial.print("Digital pin ");
                webpage += "Digital pin ";
                webpage += pin;
                webpage += " is updated to";
                webpage += val;
                pinMode(pin, OUTPUT);
                digitalWrite(pin, val);
                }
              else if (type=='A') {
                Serial.print("Analog pin ");
                webpage += "Analog pin ";
                webpage += pin;
                webpage += " is updated to ";
                webpage += val;              
                analogWrite(pin, val);      
                }
              else {
                Serial.println("Unexpected type:" + type);
                }
              Serial.print(pin);
              Serial.print("=");
              Serial.println(val);
              webpage += "<br>";    
              }
            }          
          else {
            webpage += "<h1>Unknown Page</h1>";
            webpage += "Recognized Pages are : <br>";
            webpage += "192.168.x.x/analog/<br>";
            webpage += "192.168.x.x/digital/<br>";
            }
          }
        webpage += "</body></html>";
        String cipSend="AT+CIPSEND=";
        cipSend += connectionId;
        cipSend += ",";
        cipSend += webpage.length();
        cipSend += "\r\n";
        sendData(cipSend,1000,DEBUG);
        sendData(webpage,5000,DEBUG);
        sendData("AT+CIPCLOSE=" + (String)connectionId + "\r\n",3000,DEBUG);        
        }
      }
    }
  }

boolean connectWifi(String ssid, String pwd) {
  String res=sendData("AT+CWJAP=\"" + ssid + "\",\"" + pwd + "\"\r\n",8000,DEBUG);
  res.replace("\r\n",""); //remove all line terminator
  if (res.indexOf("OK") != -1) {return true;}
  else {return false;}
  }

String sendData(String command, const int timeout, boolean debug) {
  String res="";
  esp8266.print(command);
  long int time=millis();
  while ((time + timeout) > millis()) {
    while(esp8266.available()) {res.concat((char)esp8266.read());}
    }
  if (debug) {Serial.print(res);}
  return res;
  }

此程式與上面兩個測試差別僅僅是在顯示數位與類比腳位部分添加 HTML 的表格元素而已 (藍色部分), 編譯後記憶體耗用情形如下 :

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

以手機連覽器連線 192.168.2.106/digital/  結果如下 :

連線 192.168.2.106/analog/ 結果如下 :


注意, 上面程式中只顯示了 D2~D9 這 8 個數位腳狀態, 而非如測試 1 中顯示 D2~D13, 原因是若顯示到 D10 以上, 瀏覽器會等很久都沒回應, 最後輸出 "無法顯示網頁", 這應該是 HTML 的表格元素佔用了太多 SRAM 記憶體所致.

因此我參考之前所用的 F() 函數, 將字串常數放在程式記憶體 (Flash) 以節省 SRAM 記憶體的技巧來解決此問題, 參考 :

# 使用 ITEAD WeeESP8266 函式庫改寫物聯網模組 wifi 設定程式
https://www.arduino.cc/en/Serial/Print

You can pass flash-memory based strings to Serial.print() by wrapping them with F(). For example :
Serial.print(F(“Hello World”))

修改後程式如下 :

測試 4 :

#include <SoftwareSerial.h>
#define DEBUG true

SoftwareSerial esp8266(7,8); //(RX,TX)
const String ssid="EDIMAX-tony";
const String pwd="1234567890";
const int MAX_PAGE_NAME_LEN=8;  //buffer size
char buffer[MAX_PAGE_NAME_LEN + 1]; //store page_name

void setup() {
  Serial.begin(9600);
  esp8266.begin(9600);
  sendData(F("AT+RST\r\n"),2000,DEBUG); // reset ESP8266
  sendData(F("AT+CWMODE=1\r\n"),1000,DEBUG); //configure as station
  sendData(F("AT+CIPMUX=1\r\n"),1000,DEBUG); //enable multiple connections
  sendData(F("AT+CIPSERVER=1,80\r\n"),2000,DEBUG); //turn on server 80 port  
  while (!connectWifi(ssid, pwd)) {
    Serial.println(F("Connecting WiFi ... failed"));
    delay(2000);
    }
  sendData(F("AT+GMR\r\n"),1000,DEBUG);
  delay(3000); //wait for wifi connection to get local ip
  sendData(F("AT+CIFSR\r\n"),1000,DEBUG); //get ip address
  }

void loop() {
  if (esp8266.available()) { // check if esp8266 is sending message
    if (esp8266.find("+IPD,")) {
      delay(1000); //waiting for response: ?pinD2=1&pinA2=1023
      int connectionId=esp8266.read()-48;  //turn ASCII to number
      if (esp8266.find("GET /")) { //retrieve page name (router)
        memset(buffer, 0, sizeof(buffer));  //clear buffer (all set to 0)
        String webpage=F("<html><body>");
        if (esp8266.readBytesUntil('/', buffer, sizeof(buffer))) {
           if (strcmp(buffer, "analog") == 0) { //show analog pins
            Serial.println(F("analog"));
            webpage += F("<h1>Analog Pins</h1>");
            webpage += F("<table border=1>");
            for (byte i=0; i<6; i++) {
              webpage += F("<tr><td>analog pin ");
              webpage += i;
              webpage += F("</td><td>");
              webpage += analogRead(i);
              webpage += F("</td></tr>");
              }
            webpage += F("</table>");
            }
          else if (strcmp(buffer, "digital") == 0) {
            Serial.println(F("digital"));
            webpage += F("<h1>Digital Pins</h1>");
            webpage += F("<table border=1>");          
            for (byte i=2; i<=13; i++) {
              webpage += F("<tr><td>digital pin ");
              webpage += i;
              webpage += F("</td><td>");
              if (digitalRead(i) == 1) {webpage += F("HIGH");}
              else {webpage += F("LOW");}
              webpage += F("</td></tr>");
              }
            webpage += F("</table>");  
            }
          else if (strcmp(buffer, "update") == 0) {
            Serial.println("update");
            while (esp8266.findUntil("pin", "\r\n")) {
              char type=(char)esp8266.read();
              int pin=esp8266.parseInt();
              int val=esp8266.parseInt();
              if (type=='D') {
                Serial.print(F("Digital pin "));
                webpage += F("Digital pin ");
                webpage += pin;
                webpage += F(" is updated to");
                webpage += val;
                pinMode(pin, OUTPUT);
                digitalWrite(pin, val);
                }
              else if (type=='A') {
                Serial.print(F("Analog pin "));
                webpage += F("Analog pin ");
                webpage += pin;
                webpage += F(" is updated to ");
                webpage += val;              
                analogWrite(pin, val);      
                }
              else {
                Serial.print(F("Unexpected type:"));
                Serial.println(type);
                }
              Serial.print(pin);
              Serial.print("=");
              Serial.println(val);
              webpage += F("<br>");    
              }
            }          
          else {
            webpage += F("<h1>Unknown Page</h1>");
            webpage += F("Recognized Pages are : <br>");
            webpage += F("192.168.x.x/analog/<br>");
            webpage += F("192.168.x.x/digital/<br>");
            }
          }
        webpage += F("</body></html>");
        String cipSend=F("AT+CIPSEND=");
        cipSend += connectionId;
        cipSend += F(",");
        cipSend += webpage.length();
        cipSend += F("\r\n");
        sendData(cipSend,1000,DEBUG);
        sendData(webpage,5000,DEBUG);
        sendData("AT+CIPCLOSE=" + (String)connectionId + F("\r\n"),3000,DEBUG);        
        }
      }
    }
  }

boolean connectWifi(String ssid, String pwd) {
  String res=sendData("AT+CWJAP=\"" + ssid + F("\",\"") + pwd + F("\"\r\n"),8000,DEBUG);
  res.replace("\r\n",""); //remove all line terminator
  if (res.indexOf("OK") != -1) {return true;}
  else {return false;}
  }

String sendData(String command, const int timeout, boolean debug) {
  String res=F("");
  esp8266.print(command);
  long int time=millis();
  while ((time + timeout) > millis()) {
    while(esp8266.available()) {res.concat((char)esp8266.read());}
    }
  if (debug) {Serial.print(res);}
  return res;
  }

此處我將所有可以使用 F() 函數的字串常數都用上了, 少部分沒有使用是因為編譯失敗的關係, 可能是資料型態轉換不合法, 就暫時不去研究了. 經過如此調整, 編譯後記憶體耗用如下 :

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

可見將字串常數放到程式記憶體去後, 動態記憶體耗用比已降至 21%, 以手機瀏覽器連線 192.168.2.106/digital/  結果已能正常輸出網頁矣 :


可見只有 2K SRAM 的 Arduino 真的必須對記憶體的使用錙銖必較啊! 

OK, 接下來要將上面測試 4 改用 WeeESP8266 函式褲來寫, 程式如下 :

測試 5 :

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

SoftwareSerial esp8266(7,8); //(D7:RX, D8:TX)
ESP8266 wifi(esp8266); //create wifi object
const String ssid="EDIMAX-tony";
const String pwd="1234567890";

void setup() {
  Serial.begin(9600);
  wifi.restart();  //reset ESP8266
  Serial.print(F("Firmware version ... "));
  Serial.println(wifi.getVersion()); //
  boolean ok=true; //default result ok
  ok &= wifi.setOprToStation(); //set esp8266 : mode 1
  ok &= wifi.enableMUX(); //enable multi-connection
  ok &= wifi.startTCPServer(80); //enable sep8266 web server
  if (ok) {Serial.println(F("Enable web server ... OK"));}
  while (!wifi.joinAP(ssid, pwd)) {
    Serial.println(F("Connecting WiFi ... failed"));
    delay(2000);
    }
  delay(3000); //wait for wifi connection to get local ip
  Serial.print(F("IP ... "));
  Serial.println(wifi.getLocalIP()); //show local ip
  }

void loop() {
  uint8_t buffer[128]={0}; //init receiving buffer to store esp response
  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, fetch requested path from HTTP
    String path=F(""); //init path
    int digitalRequests=0; //digital request counts
    int analogRequests=0; //analog request counts
    for (uint32_t i=0; i<len; i++) { //concat path string
      if (!path.endsWith(F(" HTTP/"))) {path.concat((char)buffer[i]);}
      else {break;}
      } //path : "+IPD,1,248:GET /digital/ HTTP/"
    path=path.substring(path.indexOf(F("/")) + 1, path.indexOf(F(" HTTP/")));
    Serial.println(path); //eg : digital, analog, update/?pinD2=1
    String webpage=F("<html><body>");
    if (path.startsWith("analog")) {
      Serial.println(F("analog"));
      webpage += F("<h1>Analog Pins</h1>");
      webpage += F("<table border=1>");
      for (byte i=0; i<6; i++) {
        webpage += F("<tr><td>analog pin ");
        webpage += i;
        webpage += F("</td><td>");
        webpage += analogRead(i);
        webpage += F("</td></tr>");
        }
      webpage += F("</table>");    
      }
    else if (path.startsWith("digital")) {
      Serial.println(F("digital"));
      webpage += F("<h1>Digital Pins</h1>");
      webpage += F("<table border=1>");          
      for (byte i=2; i<=10; i++) {
        webpage += F("<tr><td>digital pin ");
        webpage += i;
        webpage += F("</td><td>");
        if (digitalRead(i) == 1) {webpage += F("HIGH");}
        else {webpage += F("LOW");}
        webpage += F("</td></tr>");
        }
      webpage += F("</table>");        
      }
    else if (path.startsWith("update")) {
      path=path.substring(path.indexOf(F("?")) + 1); //pinD2=1&pinA3=1023
      while (path.indexOf(F("pin")) != -1) { //parsing not finished
        char type=path.charAt(3);
        int pin=path.substring(4, path.indexOf(F("="))).toInt();
        int val=path.substring(path.indexOf(F("=")) + 1).toInt();
        if (type=='D') {
          Serial.print(F("Digital pin "));
          webpage += F("Digital pin ");
          webpage += pin;
          webpage += F(" is updated to");
          webpage += val;
          pinMode(pin, OUTPUT);
          digitalWrite(pin, val);
          }
        else if  (type=='A') {
          Serial.print(F("Analog pin "));
          webpage += F("Analog pin ");
          webpage += pin;
          webpage += F(" is updated to ");
          webpage += val;              
          analogWrite(pin, val);  
          }
        else {
          Serial.print(F("Unexpected type:"));
          Serial.println(type);
          webpage += F("Unexpected type:");
          webpage += type;
          }
        Serial.print(pin);
        Serial.print(F("="));
        Serial.println(val);
        webpage += F("<br>");
        if (path.indexOf(F("&")) != -1) { //multiple parameters
          path=path.substring(path.indexOf(F("&")) + 1); //remove parameter    
          }
        else {path=F("");} //terminate loop
        }    
      }
    else {
      webpage += F("<h1>Unknown Page</h1>");
      webpage += F("Recognized Pages are : <br>");
      webpage += F("192.168.x.x/analog/<br>");
      webpage += F("192.168.x.x/digital/<br>");
      webpage += F("192.168.x.x/update/?pinD2=1&pinA3=1023<br>");          
      }
    webpage += F("</body></html>");
    wifi.send(mux_id, (const uint8_t*)webpage.c_str(), webpage.length());
    wifi.releaseTCP(mux_id);
    }
  }

有了測試 4 在記憶體耗用上的教訓, 在測試 5 中我已完全將能用 F() 函數來節省的字串常數都優化了, 甚至 ssid 與 pwd 兩個也加上 const 來減少 SRAM 的使用. 編譯後記憶體耗用情形如下 :

稿碼使用了 16,030 bytes (52%) 的程式存儲空間。最大值為 30,720 bytes。
全域變數使用了 725 bytes (35%) 的動態記憶體,剩餘 1,323 bytes 供局部變數。最大值為 2,048 bytes 。

這數字看起來還 OK, 用手機瀏覽器測試也能得到與上面測試 4 一樣的結果, 除了 digital 的部分只能顯示到 D10 外. 請注意上面程式的 digital 迴圈部分, 上限只到 10, 若改為 11 將遇到上面測試 3 網頁無反應問題. 看來看去也找不到哪裡還可以優化, 只好這樣了. 這也是使用函式庫的代價.

另外, 由於字串處理使用 startsWith() 的關係, analog 與 digital 後面的 / 不加也沒關係 :

192.168.2.106/analog
192.168.2.106/digital

這是跟上面的測試不同的地方.

參考 :

# Arduino Uart 互傳字串 與 字串處理 依特定符號切割
Arduino輸入字串切割成陣列
# [C/C++] 切割字串函數:strtok, Network mac address 分割
# How to convert string to char array in C++?
c_str() 用法 (C/C++)

2016年7月29日 星期五

利用網頁控制 Arduino (一)

最近雜務較多, 加上決定菁菁不上補習班後, 晚上都要照進度教她國二數學課程, 所以比較沒時間玩 Arduino, 只能撿零星時間拼湊. 說實在的, 只有學生時代才能有大把連續時間去學甚麼, 進社會工作之後, 在學習時間上, 就只能 "夾縫裡求生存", 別再肖想有一大串連續時間可以研究學習. 但弔詭的是 (應該說, 人很賤的地方是), 當你有很多時間的時候, 反而不會把握, 只想玩. 現在的大學生把珍貴的時間花在玩 Game 上 FB 或 Line, 等到要找工作了, 才發現自己啥都不懂. 不景氣的時代, 加上不爭氣的學子, 台灣的前途真是堪憂啊!

好了, 言歸正傳, 昨晚翻閱 "Arduino Cookbook 錦囊妙計" 第 15 章乙太網路和網路連結 15.7 節 "處理傳進來的網路需求", 書中範例是用 Wiznet 乙太網路擴充板讓 Arduino 連上區網, 並將乙太網板設定為網頁伺服器, 由 Arduino 當 HTTP 協定處理器, 負責回應前端的網頁要求. 我是打算將乙太網改成 ESP8266 ESP01 板來進行同樣的實驗, 使用我自行焊接的 Nano+ESP8266 IOT 模組, 參考 :

# 製作 Arduino Nano + ESP8266 物聯網模組 (四) : 完結篇

本實驗的目的, 是要讓使用者以瀏覽器連線 Arduino 的網頁伺服器, 透過不同的網址來設定 Arduino 個針腳的值, 亦即, 可透過網路遠端控制 Arduino 的輸出入. 其網址格式為 :

http://192.168.2.104/?pinD2=1  (將 Arduino D2 針腳設為 HIGH 輸出)
http://192.168.2.104/?pinA5=1023  (將 Arduino A5 針腳設為 1023 輸出)

對 Arduino Nano 而言, 其可程式化輸出入腳如下 :

  1. 數位腳位 :
    D0~D13 共 14 個, 其中 D0 (Rx) 與 D1 (Tx) 與 USB 共接; D3, D5,D6,D9~D11 六隻腳具有 PWM 類比模擬功能. 
  2. 類比腳位 :
    A0~A6 共 7 個, 具有 A/D 轉換功能, 作為輸入時可將外部 0~5V 類比輸入轉為 0~1023 的數值; 反之作為輸出時可將 0~1023 的數值轉成 0~5V 的電壓輸出. 
參考 :

# Arduino Nano規格簡介

本實驗就是要透過網頁來遠端控制 Arduino 的 IO 腳的輸出. 下面程式的寫法參考了我為 IOT 模組所寫的 Wifi 設定程式碼, 參考 :

# 以 Arduino+ESP8266 物聯網模組重作 DHT11 溫溼度實驗

完整程式如下 :

測試 1 :

#include <SoftwareSerial.h>
#define DEBUG true

SoftwareSerial esp8266(7,8); //(RX,TX)
String ssid="H30-L02-webbot";
String pwd="1234567890";

void setup() {
  Serial.begin(9600);
  esp8266.begin(9600);
  sendData("AT+RST\r\n",2000,DEBUG); // reset ESP8266
  sendData("AT+CWMODE=1\r\n",1000,DEBUG); //configure as station
  sendData("AT+CIPMUX=1\r\n",1000,DEBUG); //enable multiple connections
  sendData("AT+CIPSERVER=1,80\r\n",2000,DEBUG); //turn on server 80 port    
  while (!connectWifi(ssid, pwd)) {
    Serial.println("Connecting WiFi ... failed");
    delay(2000);
    }
  sendData("AT+GMR\r\n",1000,DEBUG);
  delay(3000); //wait for wifi connection to get local ip
  sendData("AT+CIFSR\r\n",1000,DEBUG); //get ip address
  }

void loop() {
  if (esp8266.available()) { // check if esp8266 is sending message
    int digitalRequests=0;
    int analogRequests=0;
    if (esp8266.find("+IPD,")) { //get client request
      delay(1000); //waiting for response: ?pinD2=1 or ?pinA4=1023
      int connectionId=esp8266.read()-48;  //turn ASCII to number
      if (esp8266.find("GET /?pin")) { //retrieve page name (router)
        char type=(char)esp8266.read(); //read char after ?pin
        int pin=esp8266.parseInt(); //get first int number from serial
        int val=esp8266.parseInt(); //get first int number from serial
        if (type=='D') {
          Serial.print("Digital pin ");
          pinMode(pin, OUTPUT); //set pin as ouput
          digitalWrite(pin, val);
          digitalRequests++;        
          }
        else if (type=='A') {
          Serial.print("Analog pin ");
          analogWrite(pin, val);
          analogRequests++;            
          }
        else {
          Serial.println("Unexpected type:" + type);
          }
        Serial.print(pin);
        Serial.print("=");
        Serial.println(val);
        String webpage="<html>";
        webpage += (String)digitalRequests + " digital pin(s) written<br>";
        webpage += (String)analogRequests + " analog pin(s) written<br><br>";
        for (byte i=0; i<6; i++) {
          webpage += "A" + (String)i + "=" + (String)analogRead(i) + "<br>";
          }
        webpage += "</html>";
        String cipSend="AT+CIPSEND=";
        cipSend += connectionId;
        cipSend += ",";
        cipSend += webpage.length();
        cipSend += "\r\n";
        sendData(cipSend,1000,DEBUG);
        sendData(webpage,2000,DEBUG);
        sendData("AT+CIPCLOSE=" + (String)connectionId + "\r\n",3000,DEBUG);          
        }
      }
    }
  }

boolean connectWifi(String ssid, String pwd) {
  String res=sendData("AT+CWJAP=\"" + ssid + "\",\"" + pwd + "\"\r\n",8000,DEBUG);
  res.replace("\r\n",""); //remove all line terminator
  if (res.indexOf("OK") != -1) {return true;}
  else {return false;}
  }

String sendData(String command, const int timeout, boolean debug) {
  String res="";
  esp8266.print(command);
  long int time=millis();
  while ((time + timeout) > millis()) {
    while(esp8266.available()) {res.concat((char)esp8266.read());}
    }
  if (debug) {Serial.print(res);}
  return res;
  }

此程式編譯後記憶體占用情形如下 :

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

在此程式的 setup() 函數中, 我先將 ESP8266 設定為 mode 1 (station), 並且啟動可多重連線之網頁伺服器, 然後呼叫 connectWifi 連線無線基地台, ESP8266 將從 AP 取得一個區網 IP, 此 IP 即為網頁伺服器之連線網址. 接著程式就進入 loop() 函數等待用戶端連線.

在 loop() 函數中, 呼叫序列埠的 available() 函數可偵測 Arduino 的軟體序列埠緩衝器是否有資料從 ESP8266 送過來, 有的話就在緩衝器中尋找 "+IPD," 字串, 此乃客戶端正在連線的標記, 其格式例如 :

+IPD,0,298:GET /?pinD2=1 HTTP/1.1

在 "+IPD," 後面的數字即客戶端的連線通道 ID, 因為 ESP8266 的多重連線僅有 0~4 這五個 ID, 因此只要用 read() 讀取即可 (一個字元), 從序列埠讀到的其實是 ASCII 編碼的字元, 是個整數, 因此必須減掉 48 才是 0~9 的數字, 因為 0~9 的數字在 ASCII 編碼中為 48~57.

讀取通道 ID 後緩衝區就剩下 ",298:GET /?pin......" 了, 因為 read() 函數在讀取緩衝區時, 會將該字元從緩衝區刪除. 接下來我們直接呼叫序列埠的 find() 函數在緩衝區中尋找 "GET /?pin" 字串, 與 read() 一樣, 找到後此函數會將包含 "GET /?pin" 在內的之前字元串都從緩衝區內刪除, 因此緩衝區這時只剩下例如 "D2=1" 或 "A3=1023" 的字元串了. 這時再呼叫序列埠的 read() 便能擷取出是數位腳位 'D' 還是類比腳位 'A' 了. 這樣緩衝區就剩下 "2=1" 或 "3=1023" 了, 最後, 利用序列埠的 parseInt() 函數便能依序取得 pin 腳編號以及其值 val 了, 此函數會跳過所有非數字字元, 傳回第一個找到的數字 (整數)就停止, 因此它找到 "=" 時便停止, 傳回 pin 編號, 再呼叫 parseInt() 則跳過 "=" 傳會 val 值.

參考 :

https://www.arduino.cc/en/Serial/Read
https://www.arduino.cc/en/Serial/Find
https://www.arduino.cc/en/Reference/ParseInt

擷取出客戶端要求網址中的 type, pin 與 value 後, 便可用 digitalWrite() 或 analogWrite() 去控制 Arduino 的輸出了, 然後向客戶端輸出網頁, 顯示有幾個腳位被變更, 以及類比輸入所量測到數值. 注意, 串接輸出網頁內容字串時若使用 += 的話, 若一次串接一個非字串型態的 int, char 等類別時會自動轉型, 不須強制轉型; 若用 + 串接的話則必須用 (String) 強制轉型, 否則編譯失敗.

將程式上傳後, 從序列埠監控視窗可看到 ESP8266 伺服器已經啟動, 而且從所連線的無線基地台 DHCP 獲得一個 IP : 192.168.43.111, 等待用戶端連線進來 :

AT+RST


OK
bB�鑭b禔S��#� B�侒��餾�
[System Ready, Vendor:www.ai-thinker.com]
AT+CWMODE=1

no change
AT+CIPMUX=1


OK
AT+CIPSERVER=1,80


OK
AT+CWJAP="H30-L02-webbot","1234567890"


OK
AT+GMR

0018000902

OK
AT+CIFSR

192.168.2.114

OK

我使用筆電連線 ESP8266 網頁伺服器 IP=192.168.2.114 可獲得伺服器回應之網頁 :


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

Analog pin 2=1023
HTTP/1.1
Host: 192.168.2.114
ConnectiAT+CIPSEND=2,132

> <html>0 digital pin(s) written<br>1 analog pin(s) written<br><b
SEND OK
AT+CIPCLOSE=2


+IPD,1,348:GET /favicon.ico HTTP/1.1
Host: 192.168.2.114
Connection: keep-alive
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36
Accept: */*
Referer: http://192.168.43.111/?pinA2=1023
Accept-Encoding: gzip, deflate, sdch
Accept-Language: zh-TW,zh;q=0.8,en-US;q=0.6,en;q=0.4


OK

OK

好了, 這個測試到此應該算完成了.

不過這實驗也可以用 WeeESP8266 函式庫來做, 對於網址路徑就可以單純用字串來處理, 而不是像上面範例那樣直接處理序列埠送收. 事實上我起先是採用此函式庫來做書上的實驗的. 但因為有點不順利, 還是乖乖地去直接處理序列埠送收, 就是上面測試 1 的程式. 關於 WeeESP8266 函式庫用法, 參考 :

ITEAD WeeESP8266 函式庫測試
ITEAD WeeESP8266 函式庫測試 (二) : 網頁伺服器
使用 WeeESP8266 函式庫重作 NTP 對時實驗
使用 ITEAD WeeESP8266 函式庫改寫物聯網模組 wifi 設定程式
深入淺出 Wifi 晶片 ESP8266 with Arduino

完整程式如下 :

測試 2 :

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

SoftwareSerial esp8266(7,8); //(D7:RX, D8:TX)
ESP8266 wifi(esp8266); //create wifi object
String ssid="EDIMAX-tony";
String pwd="1234567890";

void setup() {
  Serial.begin(9600);
  wifi.restart();  //reset ESP8266
  Serial.print(F("Firmware version ... "));
  Serial.println(wifi.getVersion()); //
  boolean ok=true; //default result ok
  ok &= wifi.setOprToStation(); //set esp8266 : mode 1
  ok &= wifi.enableMUX(); //enable multi-connection
  ok &= wifi.startTCPServer(80); //enable sep8266 web server
  if (ok) {Serial.println(F("Enable web server ... OK"));}
  while (!wifi.joinAP(ssid, pwd)) {
    Serial.println("Connecting WiFi ... failed");
    delay(2000);
    }
  delay(3000); //wait for wifi connection to get local ip
  Serial.print(F("IP ... "));
  Serial.println(wifi.getLocalIP()); //show local ip
  }

void loop() {
  uint8_t buffer[128]={0}; //init receiving buffer to store esp response
  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, fetch requested path from HTTP
    String path=""; //init path
    int digitalRequests=0; //digital request counts
    int analogRequests=0; //analog request counts
    for (uint32_t i=0; i<len; i++) { //concat path string
      if (!path.endsWith(" HTTP/")) {path.concat((char)buffer[i]);}
      else {break;}
      }
    path=path.substring(path.indexOf("/") + 1, path.indexOf(" HTTP/"));
    Serial.println(path); //eg: ?pinD2=1 or ?pinA4=1023
    char type=path.charAt(4);
    int pin=path.substring(5, path.indexOf("=")).toInt();  
    int val=path.substring(path.indexOf("=") + 1).toInt();
    if (type=='D') {
      Serial.print("Digital pin ");
      pinMode(pin, OUTPUT);
      digitalWrite(pin, val);
      digitalRequests++;
      }
    else if  (type=='A') {
      Serial.print("Analog pin ");
      analogWrite(pin, val);
      analogRequests++;    
      }
    else {
      Serial.print("Unexpected type:");
      Serial.println(type);
      }
    Serial.print(pin);
    Serial.print("=");
    Serial.println(val);  
    String webpage="<html>" + (String)digitalRequests + " digital pin(s) written<br>" +
                   (String)analogRequests + " analog pin(s) written<br><br>";
    for (byte i=0; i<6; i++) {
      webpage += "A" + (String)i + "=" + (String)analogRead(i) + "<br>";
      }    
    webpage += "</html>";      
    wifi.send(mux_id, (const uint8_t*)webpage.c_str(), webpage.length());
    wifi.releaseTCP(mux_id);
    }
  }

使用 WeeESP8266 函式庫需先建立一個名為 ESP8266 物件, 並傳入與 ESP8266 通訊的序列埠物件, 這裡是使用 D7 與 D8 作為 Rx 與 Dx 的軟體序列埠. 同樣在 setup() 函數中呼叫 SetOprToStation() 以設定 ESP8266 工作在 mode 1, 並呼叫 enableMUX() 與 startTCPServer() 啟動多重連線之網頁伺服器, 然後呼叫 joinAP() 連上無線基地台.

進入 loop() 後就開始偵測 ESP8266 是否有收到送客戶端的連線要求. 使用 WeeESP8266 送收資料須準備一個 uint_8 型態的緩衝器 buffer 與連線通道紀錄器 mux_id 當參數, 傳入 recv() 接收函數中, 它會將回應字元放入緩衝器內, 將連線通道紀錄在 mux_id 中, 並傳回緩衝器收到的資料長度 len. 當 len 不為 0 表示有收到客戶端要求, 這時可用一個迴圈去讀取緩衝區, 直到 "HTTP/" 為止, 放入路徑字串變數 path 中, 其內容例如 :

+IPD,1,248:GET /?pinD2=1 HTTP/

這樣便可以利用 Arduino 的字串函數來擷取要設定的腳位與值. 關於字串處理請參考 :

Arduino 基本語法筆記

透過下列指令即可取出介於第一個斜線與空白之間的 URL 資訊 :

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

這樣 path 就得到 ?pinD2=1 或 ?pinA3=1023 的字串了. 利用 char() 可得到數位腳 (D) 或類比腳 (A), 而透過 substring() 與 indexOf() 就可以抓出腳位編號 pin 與其設定值 val. 注意, 因為擷取出來的 pin 與 val 是數字字串, 因此必須用 toInt() 轉成整數, 以便傳入 digitalWrite() 與 analogWrite() 之中.

編譯後記憶體占用情形如下 :

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

可見使用函式庫較占記憶體喔! 大約多了 10% 左右.

程式上傳後以手機連線同一無線基地台, 輸入 ESP8266 伺服器網址, 這次改為設定數位腳位 D2, 得到預期結果 :

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

Firmware version ... 0018000902
Enable web server ... OK
Firmware version ... 0018000902
IP ... 192.168.2.114
?pinD2=0
Digital pin 2=0
favicon.ico
Unexpected type:c
0=0

比較上面兩個測試, 當然測試 1 直接處理序列埠較低階麻煩, 但好處是省記憶體, 而且比較能理解底層運作的細節; 測試 2 使用函式庫, 字串處理較簡單, 程式看來解結易懂, 但較吃記憶體. 不管哪種做法, 都還是要在瀏覽器上輸入網址來操控, 還是麻煩. 雖然已有 App 介面可以讓遠端控制 Arduino 的輸出更容易, 但書上這個實驗能讓我們更熟悉序列埠與字串的操作. 

2016-07-30 補充 :

今天早上重新檢視程式碼時覺得為啥要設 digitalRequests 與 analogRequests 這兩個變數呢? 不是一次設定一個腳位嗎? 仔細看作者在 15.7 節末尾的說明才發現, 原來書中的範例程式可以一次設定好幾個腳位, 每一組參數都用 & 連起來即可, 例如 :

192.168.2.114/?pinD2=1&pinA2=1023

上面兩個測試程式只能一次處理一個參數而已. 所以我將其修改為可一次設定多個參數的程式如下 :

測試 3 :

#include <SoftwareSerial.h>
#define DEBUG true

SoftwareSerial esp8266(7,8); //(RX,TX)
String ssid="EDIMAX-tony";
String pwd="1234567890";

void setup() {
  Serial.begin(9600);
  esp8266.begin(9600);
  sendData("AT+RST\r\n",2000,DEBUG); // reset ESP8266
  sendData("AT+CWMODE=1\r\n",1000,DEBUG); //configure as station
  sendData("AT+CIPMUX=1\r\n",1000,DEBUG); //enable multiple connections
  sendData("AT+CIPSERVER=1,80\r\n",2000,DEBUG); //turn on server 80 port    
  while (!connectWifi(ssid, pwd)) {
    Serial.println("Connecting WiFi ... failed");
    delay(2000);
    }
  sendData("AT+GMR\r\n",1000,DEBUG);
  delay(3000); //wait for wifi connection to get local ip
  sendData("AT+CIFSR\r\n",1000,DEBUG); //get ip address
  }

void loop() {
  if (esp8266.available()) { // check if esp8266 is sending message
    int digitalRequests=0;
    int analogRequests=0;
    if (esp8266.find("+IPD,")) {
      delay(1000); //waiting for response: ?pinD2=1&pinA2=1023
      int connectionId=esp8266.read()-48;  //turn ASCII to number
      if (esp8266.find("GET /")) { //retrieve page name (router)
        while (esp8266.findUntil("pin", "\r\n")) {
          char type=(char)esp8266.read();
          int pin=esp8266.parseInt();
          int val=esp8266.parseInt();
          if (type=='D') {
            Serial.print("Digital pin ");
            pinMode(pin, OUTPUT);
            digitalWrite(pin, val);
            digitalRequests++;        
            }
          else if (type=='A') {
            Serial.print("Analog pin ");
            analogWrite(pin, val);
            analogRequests++;            
            }
          else {
            Serial.println("Unexpected type:" + type);
            }
          Serial.print(pin);
          Serial.print("=");
          Serial.println(val);        
          }
        String webpage="<html>";
        webpage += (String)digitalRequests + " digital pin(s) written<br>";
        webpage += (String)analogRequests + " analog pin(s) written<br><br>";
        for (byte i=0; i<6; i++) {
          webpage += "A" + (String)i + "=" + (String)analogRead(i) + "<br>";
          }
        webpage += "</html>";
        String cipSend="AT+CIPSEND=";
        cipSend += connectionId;
        cipSend += ",";
        cipSend += webpage.length();
        cipSend += "\r\n";
        sendData(cipSend,1000,DEBUG);
        sendData(webpage,2000,DEBUG);
        sendData("AT+CIPCLOSE=" + (String)connectionId + "\r\n",3000,DEBUG);          
        }
      }
    }
  }

boolean connectWifi(String ssid, String pwd) {
  String res=sendData("AT+CWJAP=\"" + ssid + "\",\"" + pwd + "\"\r\n",8000,DEBUG);
  res.replace("\r\n",""); //remove all line terminator
  if (res.indexOf("OK") != -1) {return true;}
  else {return false;}
  }

String sendData(String command, const int timeout, boolean debug) {
  String res="";
  esp8266.print(command);
  long int time=millis();
  while ((time + timeout) > millis()) {
    while(esp8266.available()) {res.concat((char)esp8266.read());}
    }
  if (debug) {Serial.print(res);}
  return res;
  }

主要的改變是加入一個 while 迴圈用序列埠的 findUntil() 不斷去找 "pin". 編譯後記憶體耗用 :

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

瀏覽器連線結果正確 :


而使用 WeeESP8266 函式庫測試 2 則被改成下列測試 4 :

測試 4 :

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

SoftwareSerial esp8266(7,8); //(D7:RX, D8:TX)
ESP8266 wifi(esp8266); //create wifi object
String ssid="EDIMAX-tony";
String pwd="1234567890";

void setup() {
  Serial.begin(9600);
  wifi.restart();  //reset ESP8266
  Serial.print(F("Firmware version ... "));
  Serial.println(wifi.getVersion()); //
  boolean ok=true; //default result ok
  ok &= wifi.setOprToStation(); //set esp8266 : mode 1
  ok &= wifi.enableMUX(); //enable multi-connection
  ok &= wifi.startTCPServer(80); //enable sep8266 web server
  if (ok) {Serial.println(F("Enable web server ... OK"));}
  while (!wifi.joinAP(ssid, pwd)) {
    Serial.println("Connecting WiFi ... failed");
    delay(2000);
    }
  delay(3000); //wait for wifi connection to get local ip
  Serial.print(F("IP ... "));
  Serial.println(wifi.getLocalIP()); //show local ip
  }

void loop() {
  uint8_t buffer[128]={0}; //init receiving buffer to store esp response
  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, fetch requested path from HTTP
    String path=""; //init path
    int digitalRequests=0; //digital request counts
    int analogRequests=0; //analog request counts
    for (uint32_t i=0; i<len; i++) { //concat path string
      if (!path.endsWith(" HTTP/")) {path.concat((char)buffer[i]);}
      else {break;}
      } //path : "+IPD,1,248:GET /?pinD2=1&pinA4=1023 HTTP/"
    path=path.substring(path.indexOf("?") + 1, path.indexOf(" HTTP/"));
    Serial.println(path); //eg : pinD2=1 or pinD2=1&pinA4=1023
    while (path.indexOf("pin") != -1) { //parsing not finished
      char type=path.charAt(3);
      int pin=path.substring(4, path.indexOf("=")).toInt();  
      int val=path.substring(path.indexOf("=") + 1).toInt();
      if (type=='D') {
        Serial.print("Digital pin ");
        pinMode(pin, OUTPUT);
        digitalWrite(pin, val);
        digitalRequests++;
        }
      else if  (type=='A') {
        Serial.print("Analog pin ");
        analogWrite(pin, val);
        analogRequests++;    
        }
      else {
        Serial.print("Unexpected type:");
        Serial.println(type);
        }
      Serial.print(pin);
      Serial.print("=");
      Serial.println(val);
      if (path.indexOf("&") != -1) { //multiple parameters
        path=path.substring(path.indexOf("&") + 1); //remove parameter      
        }
      else {path="";} //terminate loop 
      }
    String webpage="<html>" + (String)digitalRequests + " digital pin(s) written<br>" +
                   (String)analogRequests + " analog pin(s) written<br><br>";
    for (byte i=0; i<6; i++) {
      webpage += "A" + (String)i + "=" + (String)analogRead(i) + "<br>";
      }    
    webpage += "</html>";      
    wifi.send(mux_id, (const uint8_t*)webpage.c_str(), webpage.length());
    wifi.releaseTCP(mux_id);
    }
  }

主要的變更也是多加了一個 while 迴圈來偵測是否還有 "pin" 字眼存在, 最重要的是迴圈末尾要自 path 字串中移除目前所處理的參數, 若已無 & 字元表示已處理完全部參數, 要把 path 設為空字串來終止迴圈. 編譯後記憶體耗用如下 :

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

以手機瀏覽器連線 192.168.2.114/?pinD2=1&pinD3=0&pinA2=1023 結果如下 :

可見迴圈會累計有幾個腳位被設定輸出值了.

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

Firmware version ... 0018000902
Enable web server ... OK
IP ... 192.168.2.114
pinD2=1&pinD3=0&pinA2=1023
Digital pin 2=1
Digital pin 3=0
Analog pin 2=1023
GET /favicon.ico


二哥的日本自助旅行

昨天早上請一個小時假, 載二哥與水某去小港機場, 他們要搭 11:30 的酷航前往大阪自助旅行, 將遊歷京都, 奈良, 神戶, 預計 8/2 返台. 去年就有計畫全家 (以及岳家) 赴日自助旅行, 因為今年暑假剛好二哥國中畢業, 終於有空旅行啦! 但是姊姊卻要準備明年初的學測, 加上我四月才帶爸去黑部立山, 所以計畫改為二哥與水某兩人.

我同事說現在去日本沒有楓葉, 又沒有知名祭典, 天氣也很熱, 何不秋天再去. 問題是下周二哥他們要新生訓練啦! 只有這個空檔能去旅行. 其實這次我也有點心動, 畢竟20年前在大阪出差工作前後也有半年, 也蠻想舊地重遊的.



2016年7月26日 星期二

2016 年第 28 周記事

本周因菁菁跟水某去台中玩, 所以只有我一個人回鄉下. 週六載爸去管醫師那裏取藥後順路參訪佛陀紀念館, 吃過午飯後才回鄉下. 回程無法再上國十 (因台 29 沒有國十的東向匝道, 只有西向), 因此只好走旗楠公路回家. 自從 1999 年 (姊姊出生那年) 年底國十開通後我就很少走這條公路了, 算來國十營運至今已快 16 年了, 帶給我們鄉親非常大的便利. 以前往返高雄單趟至少要花一個半小時, 走國十只要 40~50 分鐘.

兩周前颱風過後, 鄉下家裡來了一隻黑狗, 菁菁非常高興, 就把以前大咪吃剩下的貓食拿來餵牠. 餵過幾次後已經不那麼怕人了, 但是仍然摸不到. 狗狗有時在門口, 有時又跑去摩托車腳踏板上睡覺, 白天偶而會跑出去, 但傍晚又會回來. 貓食快吃完了, 上週日去全聯時, 菁菁就說要買一包狗食, 但是買回來還沒開呢, 本周牠就沒回來了. 我看到牠跟一隻棕色的狗沿著田埂往香蕉園走, 可能是找到真愛, 私奔去了.

本周只好將這包還沒開封的狗食拿回全聯換購, 但是店員輸入我的一卡通時竟然導致收銀機當機, 後面大排長龍, 店長說奇怪找不到我的貨品資料, 我說如果很麻煩, 那就算了, 或許狗狗還會再回來, 而且所謂狗來富, 搞不好這張 135 元的狗食發票就中了一千萬哩! (200 元都很少中的人就是容易做這種夢). 結果店長說收銀機已經 key 進去了, 這張發票也不能給我了, 必須人工退費給我, 只好在那邊誤了十幾分鐘. 耽誤時間事小, 一千萬元飛了才是大事 (唉, 夢還沒醒喔 ...).

至於大咪, 牠從過完年沿著田埂往北走, 跳上駁坎穿進高雄人農舍旁的小徑離家後就再也沒有回來, 如今已超過半年矣. 以前牠離家最長不會超過兩個月, 所以這次牠不會再回來了. 我研判牠可能 :
  1. 吃到中毒的老鼠 (機率 20%, 因牠很少吃老鼠)
  2. 被農田裡的網子絆住無法脫身 (機率 70%)
  3. 遭到野狗攻擊 (機率 80%, 因牠這兩年常帶傷回家)
總之, 大咪可能已經死了. 當初鄰居給我們的三隻貓至此全歸於無. 中咪是被每天傍晚運動會經過我家門口的那個人所帶的三隻大狗追逐咬傷後跌入水圳不知所蹤; 最可愛的小咪 (美國短毛貓) 疑似在外出時被偷走; 而當初最怕人, 畏首畏尾的大咪是陪伴我們最久的. 牠們三隻來我們家時小狐狸們都還小, 每天看三隻貓咪打打鬧鬧捉迷藏, 真是令人懷念的美麗時光.

本周還有一件麻煩事, 微軟 Win10 免費升版到 7/29 截止, 因此周末開始為家中三台 Win8.1 電腦升級. 鄉下的聯強電腦與我的應宏小筆電升級最順利, 聯強那台桌電因為才剛回復 Win8.1 不久 (為了清掉二哥下載的一堆遊戲), 所以就直接升版; 我的小筆電因為考量還要安裝開發工具與應用軟體比較麻煩, 而且也保持很乾淨, 所以也是清掉垃圾後直接升級.

倒是高雄這台聯想桌電比較麻煩, 因為也被二哥下載一堆遊戲必須回復, 問題是他曾自行回復一次 (用一鍵回復到 Win8 而已), 且之前過多遊戲軟體讓 C 碟差點爆掉, 導致以前我升級到 8.1 時所設的還原點被自動刪除, 而想利用映像檔還原卻不知何故無法抓到還原目錄, 結果只有重灌一途. 上週五晚上跑去建國路180號聯想店面 (236-0737 週一 ~ 週五 11:00~20:00) 押了 350 元借 6 片 H520 的 Win8 還原光碟回來, 但到昨晚才有時間重灌. 灌完 Win8 還要先升級成 Win8.1 才可以升到 Win10.

這回要管好這三台電腦的映像檔與管理員登入帳號才行, 避免電腦出問題時要花大把時間去務.


2016年7月25日 星期一

升級 Win10 並建立系統映像檔

隨著免費升級 Win10 倒數計時逼近, 我也開始思考升級是否為必要. 上週六決定還是升級好了, 因為 :
  1. 過了 7/29 截止期限後, 那個不斷提醒升級 Win10 的通知可能還是一直會出現.
  2. 可惡的微軟過一段時間就會宣布 Win8.1 停止更新
上週回鄉下時就將那台聯強的升級, 但這幾天下午都是雷雨, 週六 7/23 下午正要升級, 網路卻因為雷擊而不通, 所幸週日 7/24 下午就恢復了, 勉強在晚上回高雄前完成升版. 今天則是將我的應宏小筆電升級. 

升級前都先清除瀏覽器垃圾, 刪除或備份 C 碟下的檔案, 最後清空垃圾桶, 盡量讓 C 碟越乾淨越好. 雖然升級 Win10 時, 升級程式會將回復 Win8.1 的資訊存入 C:\Windows.old 資料夾中, 但為了以防萬一, 最好自行建立升版前的 Win8.1 映像檔再進行升版較保險. 建立 Win8.1 映像檔的入口是在控制台的檔案歷程記錄, 點選底下的系統映橡備份 :


選擇備份在外接硬碟 :


確認後按 "開始備份" 即可 :



備份完畢後, 到外接硬碟將備份資料夾 WindowsImageBackup 改為 WindowsImageBackup_升級win10前備份, 這樣做的目的是因為每次建立映像檔時都會蓋掉同名資料夾, 所以升級到 Win10 後再做一次映像檔時, 若沒改目錄名稱, 那 Win8.1 的映像檔就白做了.

OK, 這樣就可以到 Windows Update 去更新, 然後升級為 Win10 了.


升級後第一件事就是建立 Win10 的映像檔, 進入點跟 Win 8.1 不太一樣 (但後面一樣, 都是到檔案歷程記錄頁面). 首先點左下角 Win10 的開始鈕, 點選其中的 "設定" :


點選 "更新與安全性" :


點選 "備份" :


點選最底下的 "移至 [備份與還原] (Windows 7)" :


點選 "建立系統映像" :


選擇 "硬碟上" (我的外接隨身硬碟), 按下一步 :


按 "開始備份" 即可 :


完成後會問是否要建立修復光碟, 如果筆電有光碟機, 還是建立一片較好, 當系統開不了機時可用來開機, 以便從映像檔來回復系統.

建立映像檔後, 檢查存放映像檔的隨身硬碟, 發現容量少了 33.4G, 這就是映像檔的大小, 我是因為之前在 Win8.1 就安裝了 Office 以及一些程式開發工具的關係, 稍微大了一些.

前兩周參加同學會時聽阿峰說他升級 Win10 的慘況是關不了機, 我目前升級兩台都沒有遇到, 還算幸運. 初次使用 Win10 印象還不錯, 開關機都很快, 運作也很順暢, 只有開始選單有點花俏, 右邊還包含了磚塊系統, 有點不習慣.

參考 :

2016年7月23日 星期六

參訪佛陀紀念館

上周六本來要跟阿龍表弟騎腳踏車到佛陀紀念館, 卻因為舅媽建議改去阿公店水庫; 那本周六就自己騎去那裏吃個午飯再回來吧! 哪知聽說管醫師下周要出國, 骨科診所休診, 爸的退化性關節藥快吃完了, 那今天就必須去拿藥了. 我想不如乾脆回程順路載爸去那裏參訪吧!

從國十大樹交流道下去右轉大約十分鐘就到了, 原來紀念館就蓋在佛光山旁邊而已, 整個紀念館呈長型緩坡, 從門口慢慢逛到最後面約要一小時. 停好車已九點半了, 走到最後棟看 4D 的佛陀生平短片時已快 11 點. 今天非常熱, 還好館區有遮陽步道, 不然會中暑喔.

看完影片快 11 點半了, 走回第一棟 2F 的素食自助餐吃午飯, 每人 100 元吃到飽, 大約有九道菜, 有炒麵也有飯, 大概吃兩盤就飽了. 我覺得還不錯, 價格不會太貴.


雙蕃茄什錦炊飯

今天在 Yahoo 新聞看到這個料理的介紹, 哇! 好像很簡單嘛!

雙蕃茄什錦炊飯

所需材料 :

生米 2杯量米杯
洋蔥 1顆
蕃茄 2顆
鴻喜菇 1份
玉米 1支
肉片 150g
蔥 2支
水 1.5~2(量米杯)
鹽 2匙(鹽罐紅匙)

米洗好放底層, 其他材料放上層, 照一般煮飯方式讓電鍋去炊即可. 重點是鹽先不要放, 番茄整顆只劃四刀, 不要切開, 等跳了 20 分鐘後再放鹽並劃開番茄, 讓湯汁與飯融合在一起. 哪天來做做看.



2016年7月21日 星期四

滷苦瓜的方法

這陣子中午常到公司附近的國泰民安素食店吃午飯, 只要有滷苦瓜必點, 因為廚師將苦瓜滷得甘甜軟嫩, 吃後令人齒頰留香, 在這炎炎夏日最為退火. 有次我點完菜, 跟老闆誇讚其苦瓜滷得好, 順勢問她是怎麼滷的, 怎麼這麼好吃? 她只說是用豆薣加一些香料去滷, 原來是用豆薣啊! 但是甚麼香料咧? 或許是人家的秘密, 我就不方便再問了.

上週回鄉下滷苦瓜時就找出媽生前所製作的最後一罐豆薣, 舀了兩湯匙加進去, 另外加兩湯匙冰糖, 以及適量醬油, 水加至八分滿去滷, 結果吃起來有比較好吃, 苦味降低了, 但還不到素食店老闆的水準. 今天查了網路, 發現下列這個網站有非常多的苦瓜料理 :

# 苦瓜料理食譜作法

我挑了其中幾個還不錯的苦瓜食譜如下 :

# 油燜苦瓜香菇
# 豆鼓油悶苦瓜
# 苦瓜封
苦瓜燉蔭瓜雞湯
# 蔭瓜滷苦瓜
# 甘味苦瓜排骨
甘甜苦瓜排骨湯
# 百香果苦瓜泡菜
# 涼拌脆苦瓜
# 醬鳳梨釀苦瓜一鍋搞定
# 醬燒苦瓜

綜合上面幾個食譜, 我猜素食店老闆的滷苦瓜好吃的秘訣應該是豆薣加蔭瓜! 而她所說的香料有可能是八角. 上面食譜中也有人放香菇與蠔油下去, 因此這周回鄉下滷苦瓜時我打算放入下列八項去調味看看 :

八角, 蔭瓜, 豆薣, 香菇, 蠔油, 冰糖

另外食譜中有一些關於苦瓜的常識, 整理如下 :
  1. 苦瓜越白越不苦;偏白(脆)適合涼拌;偏黃(糯)適合紅燒。
  2. 苦瓜挑選時,表面凸起顆粒較大的,苦瓜越不苦。
  3. 百香果加砂糖,很容易在濾網中攪拌去籽,做成冰塊保存,打果汁或涼拌都好用。
  4. 苦瓜苦味來自果肉的地方並非白色薄膜。一般料理都會說要去除白色薄膜是因為口感不佳無脆度,白色海綿膜咬起來很有韌性口感不佳,相反的果肉是脆也是苦味的來源,所以涼拌時要以泡水方式讓果肉釋放苦澀味。
苦瓜是葫蘆科蔬菜, 性寒, 在食療中有清熱降火解毒功效, 其維生素 C 含量是瓜類中最高的, 故有抗氧化功能. 現代醫學研究發現苦瓜萃取物可增強免疫力, 對愛滋病, 癌症, 糖尿病等疾病可能有療效, 目前還尚未充分確認. 苦瓜熱量低, 含有豐富膳食纖維, 且其中的植化素苦瓜苷具有降血糖作用, 因此適合糖尿病人食用. 不過苦瓜苷活性在高溫烹煮下會減弱, 冷凍處理後反而升高. 值得注意的是, 苦瓜含鉀量較高, 糖尿病有腎病變者不宜多食.

參考 :

找便宜的好工具 : 品購網

昨天下午公司樓下會客室有廠商來擺攤, 賣的是金健康 6D 循環透氣床墊, 同事力邀我下去看看 (他想買, 人多好殺價), 單人 1680, 雙人 2600, 我同事買了兩單一雙, 殺到 1600 與 2500, 廠商說外面買至少要多 1000 塊. 我看了之後也有點心動, 但生意嘴吼溜溜, 你若全信的話肯定吃悶虧. 我說考慮一下, 下班前再看看. 其實是想上樓查一下網路價格比價一下.

搜尋 "金健康 6D 循環透氣床墊" 找到這個品購網, 原來這是一個比價網站, 它會搜尋各大購物網站, 按價格由低到高排列, 就可以找出目前最便宜的價位.

https://www.pingle.com.tw/

透過品購網找到 UDN 剛好昨天單人的有特價 88 折 1480 (1680*0.88), 便宜 200 元, 我就買了兩個, 另外一個給姊姊, 合計 2960, 加上 UDN 有定期贈送購物金, 昨天到期的有 600 元, 可使用 592 元, 因此只需付 2960-592=2368, 平均單價是 2368/2=1184 元, 比同事買的 1600 便宜了 416 元, 相當於原價打七折 (1184/1680). 果然驗證了生意嘴吼溜溜, 還說外面要貴上 1000 塊哩.

這告訴我們甚麼道理? 所謂的行銷術, 其實是一種騙術. 利用時空的限制 (你沒時間跑遠路貨比三家) 所造成的資訊不對稱, 靠一張嘴與誘人的話術, 讓消費者陷入自以為撿到便宜的陷阱, 產生強烈的購物衝動, 最終完成了經濟學上的供需滿足交易行為 (貨終於賣出去了 vs 今天買到 CP 值超高的東西).

對於業務員 (賣保險的, 賣童書的, 賣線上教學的 ... Any salespersons), 永遠要把他們當成潛在的騙子看. 我個人十幾年前幫小朋友買了一套線上學習教材, 業務員保證可無限期使用 (她會用員工帳號續約), 且公司會不定期派員訪視小朋友學習成果, 我竟然天真的相信了, 結果訪視是個屁, 三年後無法再使用, 打電話給當初的業務員, 她換手機了變空號, 打給公司, 公司說使用期限為三年 (契約書確實是寫三年), 無限期那是業務員個人行為亂保證. 我仔細審視了業務員的工作, 沒底薪, 只有業務獎金, 只要拉到生意, 這個月就有收入, 當然要竭盡所能+不擇手段做成一筆生意啦! 因此我得到一個結論 : 推銷員都是潛在的騙子, 但他們都是有交付貨品, 一切照合約走, 所以無法構成詐騙. 會得到這結論或許有點偏頗, 但我也是愛莫能助, 受到這種內傷, 不會偏頗才奇怪.

現在網路資訊發達, 消費者只要理性一點克制一下馬上消費的衝動, 回家查一下網路再決定也不遲. 現在智慧型手機更方便了, 用 App 也可以馬上查價比價, 例如這個品購網也可以下載手機 App, 逛街或有人兜售時可以隨時查核推銷員所言是否屬實 :

# Pingle省省吧 - 自動比價找便宜


2016年7月20日 星期三

關於 KOBOT 掃地機器人 RV337

今天上公司福利社網站, 赫然發現今年福利金是 10000 點, 早知道的話, 姊姊的手機用這來買就好啦! 這下不知該買甚麼, 就想起去年想買後來又打消念頭的掃地機器人, 但去年那款 LG 的已經沒在賣了, 現在只賣這款 KOBOT 的 RV337, 可掃地吸塵拖地, 還能返回充電站自動回充, 福利社定價 3990, 目前做活動加贈「TECHKO MAID聰明管家拖地機器人RM101」, 似乎蠻誘人的 :

# [ 開箱 ] 【美國 KOBOT】智慧多功能自動回充掃地機器人(銀色) [ 開箱 ]

製造商美國特科 Techko 公司是台灣人柯約瑟創辦, 參考 :

# 美特科集團正修結盟 研發機器人接軌世界

台灣總代理台擘在內湖, 電話:02-2799-0866 客服專線:02-2799-1969, 售後維修需寄回台北處理, 比較麻煩. 贈品 RM101 是拖地機器人, 沒有附電池, 須自己買 4 顆三號電池, 參考 :

# Techko Maid聰明管家拖地機器人RM101 吸塵器 $3000





打電話給代理商詢問功能, 摘要如下 :
  1. 此款掃地機無遙控器, 按面板上按鍵即開始掃地, 內有數種清掃模式會自動變換, 但無路徑記憶功能, 掃完會自動返回充電座.
  2. 贈品 RM101 拖地機器人使用 3 號電池四顆, 可連續使用約兩小時. 
看來此款只是一般低價位掃地機器人加上自動返航功能而已, 有路徑記憶功能的大都要上萬元. 而贈品似乎因為滯銷才搭配銷售. 總之, 有贈品促銷的物件要特別注意, 通常只是為了挑起不理性的購買衝動而已. 所以考量後決定暫時不買了. 有可能轉而購買新的汽車導航與行車紀錄器, 我那台 QRV 今年已滿十年, 原先安裝的照後鏡導航機已經太老舊, 且照後鏡塑膠柱已斷裂, 最近要找時間去附近店家問問看施工條件, 再來決定是否自購.