2015年11月11日 星期三

Arduino IDE 1.6.6 上架囉

今天上 Arduino 官網時發現, Arduino IDE 已推出 1.6.6 新版, 於是馬上下載嘗新. 我都是下載免安裝的 ZIP 檔, 這樣比較方便. 啟動後發現起始畫面已經換新, 傳統的灰綠色背景不見了, 改成白色方格 :

1.6.6

在起始畫面右邊出現 "Genuino", 我以為是新推出的板子, 到官網一查才知, Genuino 是 Arduino 的姊妹品牌, 所有銷售到美國以外的 Arduino 原廠產品都會印上 Genuino 字樣; 而銷售於美國國內者為 Arduino 品牌.  參考 :

https://www.arduino.cc/en/Main/ArduinoBoardUno

之前 1.6.5 版的起始畫面長這樣 :

1.6.5

而更早一些的 1.6.1 版起始畫面如下 :

1.6.1

這三種版本主要的差異在 "工具" 選單的功能選項, 1.6.1 版的 IDE 在啟動後必須自行設定板子與處理器型號, 以及序列埠的埠號, 這得去控制台的裝置管理員查詢, 如果忘記了這項設定動作, 上傳程式時就會出現錯誤訊息, 蠻麻煩的 :

1.6.1

但是 1.6.5 版就會自動辨識板子與處理器型號, 以及目前接在哪一個序列埠, 完全不用再設定, 可以立刻編譯上傳. 當然, 如果 PC 連接了兩種不同的 Arduino 板子, 那麼在上傳前還是必須手動設定這三項參數.

1.6.5

而目前最新的 1.6.6 版則多了一個 "Serial Plotter" 功能 :

1.6.6

這個 Serial Plotter 可以讓我們從 Arduino 透過序列埠 (也就是 USB 連線) 將資料送到 PC 繪製成隨時間變化的圖形, 而不再只是一列列輸出的數據而已. 以前想要在 PC 上同步顯示數據的時變圖 (即數據圖形化 Data Visualization), 必須在 PC 安裝 Processing 軟體, 撰寫程式接收序列埠數據後再繪圖; 或者透過網路將資料丟到物聯網伺服器平台上也可以. 現在有了 Serial Plotter, 馬上就可以檢視圖形化的數據了, 參考 :

# New Arduino Serial Plotter

我用這篇文章中的程式測試了 Serial Plotter 功能, 真的太棒了 !

https://youtu.be/uQabv7htLrM


但是, Serial Plotter 與 Serial Monitor 一次只能開一個, 不能既要魚, 又要熊掌.

1.6.6 版底層的中介平台 Java 虛擬機 (JVM/JRE) 已更換為 Java 8, 這是自 1.5.5 版提升到 Java 7 以來最主要的變動. Arduino IDE 的 JRE 都是自帶的, 每一個版本的 IDE 都是執行各自的 JRE, 不管是安裝或免安裝版都不用另外安裝 JRE 或 JDK.

另外, 在程式編輯器中按 CTRL+L, 或在功能列選取 "編輯/跳到行 ... " 會跳出一個視窗詢問要跳到第幾行. 另一個好用功能是, 在程式編輯器中連續點滑鼠左鍵會選取整列, 1.6.5 版以前是要按住左鍵拉到列尾, 控制不好就會選取好幾行, 挺麻煩的.

當然新版也修正了許多 bug, 其他功能異動參閱 Release Notes :

https://www.arduino.cc/en/Main/ReleaseNotes

最後, 值得一提的是, 在 1.6.1 版時, 每次重新上傳程式到 Arduino, 已經開啟的序列埠監視視窗總是會被自動關閉, 上傳完畢程式重新執行後, 還必須手動再次開啟序列埠監視視窗, 真的很麻煩. 但 1.6.5 版之後就不會了, 只是清空視窗內容而已.  


2015年11月10日 星期二

MP3 檔案在手機無法播放問題

最近將常聽的音樂檔傳到手機, 發現其中有一個 mp3 檔案無法播放, 會跳到其他歌曲. 奇怪, 不是已經是 mp3 格式嗎? 在電腦中播放都沒有問題呀! 難道是 codec 問題?

搜尋 Google 均無所獲, 只好想辦法重新編碼. 找到下面這款 Media Converter 終於搞定此問題 :

# 音樂轉檔精靈 2.22.0.1 免安裝中文版 - 轉換任何音樂格式


這個免安裝版軟體我用 VirusTotal 掃過沒問題, 使用上也很簡單, 先按 "新增媒體" 加入要轉換的檔案或目錄, 再按 "開始編碼" 即可. 我使用預設參數邊碼後, 檔案大小少一半, 音質變化聽不出來有差別.


2015年11月9日 星期一

ITEAD WeeESP8266 函式庫測試

在利用 WeeESP8266 函式庫完成與 NTP 伺服器的網路對時實驗後, 繼續對此函式庫的主要函式進行功能測試. 我原先是將所有測試寫在一個程式檔案裡, 但編譯後發現程式超過15KB (約 49%), 動態記憶體使用率超過 75%, 出現如下警語:

"Low memory available, stability problems may occur."

程式執行出現錯誤, 但下 AT 指令又 OK, 這是所謂的 Memory leak 現象嗎? 將程式分成兩半, 執行起來就正常了, 難道此函式庫耗記憶體?

關於 WeeESP8266 函式庫, 參考 :

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

下面測試 1 用來測試基本函數與工作模式 :

測試 1 : WeeESP8266_test_1.ino

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

SoftwareSerial sSerial(10, 11);  //(RX:D10, TX:D11)
ESP8266 wifi(sSerial);  //建立名為 wifi 的 ESP8266 物件

void setup() {
  Serial.begin(9600);  //啟始硬體序列埠 (debug 用)  
  Serial.println("*** WeeESP8266 WiFi Library Function Test ***");
  if (wifi.kick()) {Serial.println("Reset ESP8266 ... OK");}  //重啟 ESP8266
  else {Serial.println("Reset ESP8266 ... Error");} 
  Serial.println("Firmware Version ... " + wifi.getVersion()); //傳回 AT 韌體版本  
  Serial.println("Local IP ... " + wifi.getLocalIP()); //傳回本地 IP
  //測試工作模式
  if (wifi.setOprToSoftAP()) {Serial.println("Set Operation Mode=AP ... OK");}
  else {Serial.println("Set Operation Mode=AP ... Error");}  //設為 AP 模式 (2)
  delay(3000);
  Serial.println("Local IP ... " + wifi.getLocalIP()); //傳回本地 IP 
  if (wifi.setOprToStationSoftAP()) {Serial.println("Set Operation Mode=Station+AP ... OK");} 
  else {Serial.println("Set Operation Mode=Station+AP ... Error");}  //設為 STA+AP 模式 (3)
  delay(5000); //等待 DHCP 指派 IP
  Serial.println("Local IP ... " + wifi.getLocalIP()); //傳回本地 IP 
  if (wifi.setOprToStation()) {Serial.println("Set Operation Mode=Station ... OK");} 
  else {Serial.println("Set Operation Mode=Station ... Error");}  //設為 STA 模式 (1)
  delay(3000); //等待 DHCP 指派 IP
  Serial.println("Local IP ... " + wifi.getLocalIP()); //傳回本地 IP
  Serial.print("List available APs ... "); 
  Serial.println(wifi.getAPList().c_str()); //傳回附近可用的 AP   
  if (wifi.restart()) {Serial.println("Restart ESP8266 ... OK");}  //重啟 ESP8266
  else {Serial.println("Restart ESP8266 ... Error");} 
  }
 
void loop() {
  if (sSerial.available()) {   //若軟體序列埠 Rx 收到資料 (來自 ESP8266)
    Serial.write(sSerial.read());  //讀取後寫入硬體序列埠 Tx (PC)
    }
  if (Serial.available()) {  //若硬體序列埠 Rx 收到資料 (來自 PC)
    sSerial.write(Serial.read());  //讀取後寫入軟體序列埠 Tx (ESP8266)
    }  
  }

此程式編譯後佔 34% Flash 記憶體, 52% SRAM 記憶體. 其輸出如下 :

*** WeeESP8266 WiFi Library Function Test ***
Reset ESP8266 ... OK
Firmware Version ... 0018000902
Local IP ... 192.168.2.106             (模式 1 由 DHCP 指派之 IP)
Set Operation Mode=AP ... OK
Local IP ... 192.168.4.1                 (模式 2 基地台 IP)
Set Operation Mode=Station+AP ... OK
Local IP ... 192.168.4.1                 (模式 3 有兩個 IP)
192.168.2.106
Set Operation Mode=Station ... OK
Local IP ... 192.168.2.106             (模式 1 由 DHCP 指派之 IP)
List available APs ...
Restart ESP8266 ... OK

從輸出可知, getAPList() 函數不知何故竟然沒有傳回可用 AP 字串, 我檢查原始碼, 其 AT 指令沒問題, 可能是接收回應的函數有 bug.

下面的程式是在單一連線模式下測試 UDP 之註冊/登出, 以及 TCP 連線之建立與拆線.

測試 2 : WeeESP8266_test_2.ino 

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

SoftwareSerial sSerial(10, 11);  //(RX:D10, TX:D11)
ESP8266 wifi(sSerial);  //建立名為 wifi 的 ESP8266 物件

void setup() {
  Serial.begin(9600);  //啟始硬體序列埠 (debug 用) 
  Serial.println("*** WeeESP8266 WiFi Library Function Test ***");
  //設定為單一連線模式
  if (wifi.disableMUX()) {Serial.println("disable Mux (single connection) ... OK");}
  else {Serial.println("disable Mux (single connection) ... Error");} 
  //測試 UDP 註冊與登出
  if (wifi.registerUDP("82.209.243.241", 123)) { //測試 NTP 伺服器
    Serial.println("Register UDP ... OK");
    Serial.println("IP Status ... " + wifi.getIPStatus()); //傳回目前 IP 連線狀態
    if (wifi.unregisterUDP()) {  //登出
      Serial.println("Unregister UDP ... OK");
      Serial.println("IP Status ... " + wifi.getIPStatus()); //傳回目前 IP 連線狀態
      }
    else {
      Serial.println("Unregister UDP ... Error");
      }
    }
  else {Serial.println("Register UDP ... Error");}   
  //測試 TCP 連線
  uint8_t buf[128]={0};  //儲存回應字元的緩衝器
  if (wifi.createTCP("www.google.com", 80)) {
    Serial.println("Create TCP connection (www.google.com) ... OK");
    Serial.println("IP Status ... " + wifi.getIPStatus()); //傳回目前 IP 連線狀態
    char *data = "GET /\r\n";  //字元陣列指標, 後面一定要加跳行 \r\n
    wifi.send((const uint8_t*)data, strlen(data));  //傳送資料
    uint32_t len=wifi.recv(buf, sizeof(buf), 10000);  //接收回應字元 (10 秒逾時)
    if (len > 0) {  //有收到回應
      Serial.print("Received:[");
      for (uint32_t i = 0; i < len; i++) {  //輸出回應字元
        Serial.print((char)buf[i]);
        }
      Serial.println("]");
      }
    if (wifi.releaseTCP()) {Serial.println("Release TCP ... OK");}  //關閉連線
    else {Serial.println("Release TCP ... Error");}     
    }
  else {Serial.println("Create TCP connection (www.google.com) ... Error");}
  }
 
void loop() {}

注意, 傳送字串必須放在字元陣列指標所指之位址, 而且必須在末尾加上跳行字元 "\r\n". 此程式編譯後佔 Flash 記憶體 37%, 變數佔 SRAM 52%. 其輸出為 :

*** WeeESP8266 WiFi Library Function Test ***
disable Mux (single connection) ... OK   (單一連線)
Register UDP ... OK          (註冊 UDP)
IP Status ... STATUS:4     (Disconnected)
+CIPSTATUS:0,"UDP","82.209.243.241",123,0
Unregister UDP ... OK       (解除註冊 UDP)
IP Status ... STATUS:4      (Disconnected)
Create TCP connection (www.google.com) ... OK   (建立 TCP 連線)
IP Status ... STATUS:3      (Connected)
+CIPSTATUS:0,"TCP","74.125.204.106",80,0    (連線 Google 首頁)
Received:[HTTP/1.0 302 Found    
Cache-Control: private
Content-Type: text/html; charset=UTF-8
Location: http://www.google.com.tw/?gfe_rd=c
]
Release TCP ... OK     (拆除 TCP 連線)

可見在單一連線時, UDP 註冊與 TCP 連線均正常完成.

若設為多重連線模式, 則用 UDP 註冊與 TCP 連線均會失敗 (這是因為沒有指定通道的緣故, 詳後述), 如測試 3 所示 :

測試 3 : WeeESP8266_test_3.ino 

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

SoftwareSerial sSerial(10, 11);  //(RX:D10, TX:D11)
ESP8266 wifi(sSerial);  //建立名為 wifi 的 ESP8266 物件

void setup() {
  Serial.begin(9600);  //啟始硬體序列埠 (debug 用)  
  Serial.println("*** WeeESP8266 WiFi Library Function Test ***");
  //設定為多重連線模式
  if (wifi.enableMUX()) {Serial.println("Enable Mux (multi connection) ... OK");} 
  else {Serial.println("Enable Mux (multi connection) ... Error");}
  
  //測試 UDP 註冊與登出
  if (wifi.registerUDP("82.209.243.241", 123)) { //測試 NTP 伺服器
    Serial.println("Register UDP ... OK");
    Serial.println("IP Status ... " + wifi.getIPStatus()); //傳回目前 IP 連線狀態
    if (wifi.unregisterUDP()) {
      Serial.println("Unregister UDP ... OK");
      Serial.println("IP Status ... " + wifi.getIPStatus()); //傳回目前 IP 連線狀態
      }
    else {
      Serial.println("Unregister UDP ... Error");
      }
    }
  else {Serial.println("Register UDP ... Error");}    
  //測試 TCP 連線
  uint8_t buf[128]={0};  //儲存回應字元的緩衝器
  if (wifi.createTCP("www.google.com", 80)) {
    Serial.println("Create TCP connection (www.google.com) ... OK");
    Serial.println("IP Status ... " + wifi.getIPStatus()); //傳回目前 IP 連線狀態
    char *data = "GET /\r\n";  //後面一定要加跳行 \r\n
    wifi.send((const uint8_t*)data, strlen(data));  //傳送資料
    uint32_t len=wifi.recv(buf, sizeof(buf), 10000);  //接收回應字元
    if (len > 0) {  //有收到回應
      Serial.print("Received:[");
      for (uint32_t i = 0; i < len; i++) {  //輸出回應字元
        Serial.print((char)buf[i]);
        }
      Serial.println("]");
      }
    if (wifi.releaseTCP()) {Serial.println("Release TCP ... OK");}  //關閉連線
    else {Serial.println("Release TCP ... Error");}      
    } 
  else {Serial.println("Create TCP connection (www.google.com) ... Error");}
  }

其輸出如下 :

*** WeeESP8266 WiFi Library Function Test ***
Enable Mux (multi connection) ... OK
Register UDP ... Error
Create TCP connection (www.google.com) ... Error

可見與我之前的測試結果一樣, 在多重連線時, 起始 UDP/TCP 通訊會失敗. 參見 :

ESP8266 0.9.2.2 版韌體多重連線時 TCP 連線失敗問題

但 ... 仔細看一下函數列表下方, registerUDP(), unregisterUDP(), createTCP(), 以及 releaseTCP() 這四個函數都有單一連線與多重連線兩種多型, 突然發現之前的測試有盲點, 沒注意到 UDP 與 TCP 的傳送, 其 AT 指令在多重連線時必須多指定一個參數 : 通道 id (0~4). 下列測試 4 中, 加了一個通道參數後就成功了 :

測試 4 : WeeESP8266_test_4.ino

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

SoftwareSerial sSerial(10, 11);  //(RX:D10, TX:D11)
ESP8266 wifi(sSerial);  //建立名為 wifi 的 ESP8266 物件

void setup() {
  Serial.begin(9600);  //啟始硬體序列埠 (debug 用)  
  Serial.println("*** WeeESP8266 WiFi Library Function Test ***");
  //設定為多重連線模式
  if (wifi.enableMUX()) {Serial.println("Enable Mux (multi connection) ... OK");} 
  else {Serial.println("Enable Mux (multi connection) ... Error");}  
  //測試 UDP 註冊與登出
  if (wifi.registerUDP(0,"82.209.243.241", 123)) { //測試 NTP 伺服器
    Serial.println("Register UDP ... OK");
    Serial.println("IP Status ... " + wifi.getIPStatus()); //傳回目前 IP 連線狀態
    if (wifi.unregisterUDP(0)) {
      Serial.println("Unregister UDP ... OK");
      Serial.println("IP Status ... " + wifi.getIPStatus()); //傳回目前 IP 連線狀態
      }
    else {
      Serial.println("Unregister UDP ... Error");
      }
    }
  else {Serial.println("Register UDP ... Error");}    
  //測試 TCP 連線
  uint8_t buf[128]={0};  //儲存回應字元的緩衝器
  if (wifi.createTCP(0,"www.google.com", 80)) {
    Serial.println("Create TCP connection (www.google.com) ... OK");
    Serial.println("IP Status ... " + wifi.getIPStatus()); //傳回目前 IP 連線狀態
    char *data = "GET /\r\n";  //後面一定要加跳行 \r\n
    wifi.send((const uint8_t*)data, strlen(data));  //傳送資料
    uint32_t len=wifi.recv(buf, sizeof(buf), 10000);  //接收回應字元
    if (len > 0) {  //有收到回應
      Serial.print("Received:[");
      for (uint32_t i = 0; i < len; i++) {  //輸出回應字元
        Serial.print((char)buf[i]);
        }
      Serial.println("]");
      }
    if (wifi.releaseTCP(0)) {Serial.println("Release TCP ... OK");}  //關閉連線
    else {Serial.println("Release TCP ... Error");}      
    } 
  else {Serial.println("Create TCP connection (www.google.com) ... Error");}
  }
 
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
    } 
  }

此處我指定了多重連線的通道 0, ESP8266 的連線通道最多 5 個, 可任意指定 0 ~ 4, 但關閉時也必須指定通道, 否則會失敗. 關於多重連線的通道, 參考 CIPSTATUS 與 CIPSTART 指令 :

# https://github.com/espressif/esp8266_at/wiki/CIPSTATUS
# https://github.com/espressif/ESP8266_AT/wiki/CIPSTART 

輸出如下 :

*** WeeESP8266 WiFi Library Function Test ***
Enable Mux (multi connection) ... Error  (已經是多重連線, 再設定就會 Error)
Register UDP ... OK
IP Status ... STATUS:3
+CIPSTATUS:0,"UDP","82.209.243.241",123,0
+CIPSTATUS:1,"TCP","64.233.188.104",80,0
Unregister UDP ... OK
IP Status ... STATUS:3
+CIPSTATUS:1,"TCP","64.233.188.104",80,0
Create TCP connection (www.google.com) ... OK
IP Status ... STATUS:3
+CIPSTATUS:0,"TCP","173.194.72.105",80,0
+CIPSTATUS:1,"TCP","64.233.188.104",80,0
Release TCP ... OK

終於搞定了! 多年來寫程式的經驗告訴我, 電腦絕對不會錯, 錯的通常是人腦啦. 我同時也修改了自己寫的函式庫.

2015-11-09 補充 :

今天把測試 1 的程式刪減到只呼叫 3 個函數, 結果 getAPList() 就有回應了, 程式如下 :

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

SoftwareSerial sSerial(10, 11);  //(RX:D10, TX:D11)
ESP8266 wifi(sSerial);  //建立名為 wifi 的 ESP8266 物件

void setup() {
  Serial.begin(9600);  //啟始硬體序列埠 (debug 用) 
  Serial.println("*** WeeESP8266 WiFi Library Function Test ***");
  Serial.println("Firmware Version ... " + wifi.getVersion()); //傳回 AT 韌體版本 
  Serial.println("Local IP ... " + wifi.getLocalIP()); //傳回本地 IP
  Serial.println("List available APs ... ");
  Serial.println(wifi.getAPList().c_str()); //傳回附近可用的 AP  
  }

void loop() {
  if (sSerial.available()) {   //若軟體序列埠 Rx 收到資料 (來自 ESP8266)
    Serial.write(sSerial.read());  //讀取後寫入硬體序列埠 Tx (PC)
    }
  if (Serial.available()) {  //若硬體序列埠 Rx 收到資料 (來自 PC)
    sSerial.write(Serial.read());  //讀取後寫入軟體序列埠 Tx (ESP8266)
    } 
  }


其輸出為 :

*** WeeESP8266 WiFi Library Function Test ***
Firmware Version ... 0018000902
Local IP ... 192.168.2.106
List available APs ...
+CWLAP:(3,"leeyuyun",-91,"b8:a3:86:94:f8:e8",1)
+CWLAP:(2,"edimax.setup",-69,"74:da:38:15:16:00",1)
+CWLAP:(3,"Home",-76,"c4:e9:84:66:33:c1",2)
+CWLAP:(4,"EDIMAX-tony",-52,"80:1f:02:2d:5a:9e",11)
+CWLAP:(3,"MOTOROLA-5N6F",-84,"f8:35:dd:74:d2:b6",11)
+CWLAP:(4,"alex",-92,"fc:75:16:01:26:0c",6)
+CWLAP:(3,"TP-LINK_601A04",-71,"e8:de:27:60:1a:04",10)

可見 getAPList() 函數是沒問題的, 我懷疑在上面測試 1 程式是不是耗用比較多記憶體, 導致 memory leak 呢? 此程式耗掉 25% FLASH, 37% SRAM, 相較於測試 1 的 34% FLASH 與 52% SRAM 確實少很多, 不過看起來還夠呀! 奇怪?

當附近可用 AP 少時, 上面這個程式執行 OK, 但若 AP 一多時就會秀逗無回應, 怎樣才是多是個臨界值問題, 需要時間去測試與檢查記憶體使用情形. 其實, 這個函數在 Arduino 的應用中並不是很重要, 我們通常固定連線某個 AP, 知道附近有哪些 AP 並沒有多大意義.


2015年11月8日 星期日

第 45 周記事

今天是立冬, 天氣卻異常炎熱, 難道今年又是個暖冬嗎? 我喜歡的冬天現在是越來越短了, 工業化造成的暖化, 坦白說, 我覺得已經回不去了, 無論環保團體如何努力, 就算那些不想簽京都議定書的大國與強國, 現在肯幡然醒悟作彌補, 一切都可能已經太晚了. 這個物質世界甚麼都有一個臨界點, 超過了臨界點就是永遠再見. 地球的天氣就是這樣, 可能已經過了臨界點了, 溫度每年上升, 光看北極不斷融化的冰棚就知道了.

本周菁菁要跟同學去龍華市場拍相片, 說綜合課要用, 所以只有我跟姊姊回鄉下了. 早上從市集回來經過種子行, 又買了五株玉女小番茄與五株大番茄回來種, 連同上週種下去的橙蜜, 總共是 16 株了 :


第一批的十棵玉米已經抽穗了, 好快 !


大咪今天一整天都在家睡懶覺, 爸說其實大咪最近幾乎每天都會回來, 不像以前一出去就一兩個禮拜, 有時還真懷疑他到底是不是我家的貓. 今天沒爬上樑去睡, 而是趴在機車踏板上 :


有時覺得當隻貓好像也不錯.

週四接到簡訊通知, 二哥英檢初級聽讀通過了, 聽力 93, 閱讀 96. 一月份要報名複試.


2015年11月7日 星期六

Nissan 愛手作

今天去一心路 Nissan 參加他們辦的 "愛手作" 活動, 邀請 Nissan 車主到維修廠的教室, 由專業首飾製作老師教手作銀飾. 報上名後繳交 100 元活動費, 材料由 Nissan 提供, 據說價值 2500 元呢. 我是看菁菁喜歡玩串珠珠, 自己打造手環, 所以就幫她報名, 原先她還不怎麼有興趣, 結果活動結束後大叫物超所值, 因為結尾抽獎時, 她抽中首獎.

從來沒去過一心路的維修廠, 因為離家裡太遠了. 早上到那裏一看, 哇, 比大中路要寬敞, 應該是旗艦店吧. 出乎我意料, 來參加的竟然是大人居多, 我以為應該都是幫孩子們報名的哩. 十點開始後, 我跟水某就在休息區喝飲料, 吃點心, 看報紙, 偶而進去教室拍些照片, 好久沒這麼優閒過周末了. 11 點中場休息, Nissan 還在營業廳另外準備甜點飲料招待, 真不錯.

菁菁因為不知道銀黏土打開後會很快乾掉, 沒有持續加些水分, 結果只做了一個小珠珠, 隔壁一位媽媽就作了一個方形項鍊, 還蠻好看的. 做完後要拿去烘乾, 中場休息完後繼續用刷子磨光, 嘿, 還真神奇, 原本土色的物件, 磨過後就亮晶晶咧!


最後是抽獎活動, 我都有舉手大聲答題, 但主持人就是沒點到我, 眼看汽車模型與熊寶寶布偶都被抽完了, 我只好低頭安靜地看我 iPad 裡的今周刊. 沒想到這時主持人說最後還有一個大獎, 要請他們總經理抽. 我心想, 前面都沒抽中, 那首獎就給我吧! 嘿, 還真靈, 真的就報出我的名字 (菁說是她的功勞, 因為她在心裡狂念我的名字), 上台跟他們總經理合照領獎回到休息區打開一看, 是 Tiffany 的純銀項鍊, 愛美的菁菁愛不釋手, 說以後給她當嫁妝.


活動結束後, 沿著一心路開, 到林森路左轉, 直走就到成功路的高雄展覽館, 因為前幾天同事給我三張食品展的入場券, 想說既然來到這區域, 就順路去看看. 站在展覽館門口, 不遠處就是 85 大樓 :


已經中午了, 館內參觀 (試吃) 的人非常多 :


光是逛海產區試吃, 這一點那一點就很飽了, 看到冠軍牛肉麵攤位都不想進去了. 另外一區是食品機具攤位, 我對一款李英愛代言的韓製慢速壓榨調理機很中意, 不僅可製麵, 也可製冰淇淋, 榨果汁, 作麻糬, ... 優惠價 $15800, 攤位人員很熱誠解說, 水某也很心動 ... 我說我們再去逛一圈冷靜一下, 回來再考慮看看 ... 不過終究是 ... 沒買. 我擔心只有五分鐘熱度, 一個禮拜後就跟麵包機, 豆漿機一樣, 晾在一邊囉.

走出展覽館後, 看到名聞遐邇的市圖總館就在對面不遠處, 我跟市圖每月的交易量這麼大, 怎能不去拜一下碼頭呢? 剛好今天有街頭藝人認證活動, 一樓廣場鬧哄哄的, 非常多人圍觀. 我上去七樓電腦書庫逛, 不得了, 非常非常多的新書, 河堤跟左新根本比不上. 要是住在這附近, 我每天都會來報到, 只要看到這麼多新書, 我就會開始發狂.


2015年11月5日 星期四

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

這幾天晚飯後的空閒時間都在研究如何使用 Arduino+ESP8266 進行網路對時, 搞了老半天都徒勞無功, 不僅對傳輸層 UDP 的傳送接收霧煞煞, 應用層的 NTP 協定那更不用講了, 我對網路的了解太淺, 但要深入研究又太耗時間, 眼前我只不過是要利用 ESP8266 從網路取得精確的時間來對時而已.

今天找到 WeeESP8266 這個函式庫 (其實前幾天就搜尋過卻不記得了, 參閱 "撰寫 Arduino 的 ESP8266 WiFi 函式" 最底下的補充), 發現此函式庫非常完整, 乃軍工級精品, 我根本不需要花時間去自行撰寫 ESP8266 函式, 因為別人已造好輪子啦! 而且造輪技術遠遠在我之上, 毋須東施效顰也. 

 

WeeESP8266 函式庫可在 Git 下載 :



按右側欄底下的 "Download zip" 下載函式庫, 解壓縮後將整個目錄複製到 Arduino 安裝目錄的 libraries 下, 或文件的  Arduino/libraries 下亦可. 不過要注意的是, 如果像我一樣使用 Arduino Nano, 必須利用 SoftwareSerial 函式庫與 ESP8266 溝通的話, 務必將 ESP8266.h 檔案中第 27 列的最前面註解符號拿掉 (刪除兩個斜線) :

//#define ESP8266_USE_SOFTWARE_SERIAL

這樣才能編譯成功. 我已將函式庫的兩個主檔 ESP8266.h, ESP8266.cpp, 以及 NTP 的範例程式壓縮在一起放在 Dropbox :

# https://www.dropbox.com/s/5qq1424pc7iikl4/WeeESP8266.zip?dl=0 

裡面的 ESP8266.h 我已經啟動軟體序列埠設定, 所以不需要再去改了. Arduino 與 ESP01 模組的接線參考 :

撰寫 Arduino 的 ESP8266 WiFi 函式

我使用 ESP8266 轉換板簡化接線 :
使用轉換板

電路圖

利用 UDP 向 NTP 伺服器要求對時的範例程式如下 (我已加上備註, 並酌以更改程式撰寫風格), 測試 OK, 可以順利取得網路時間 :

#include "ESP8266.h"
#include <SoftwareSerial.h>
SoftwareSerial espSerial(10, 11); // (RX:D10, TX:D11)

#define HOST_NAME   "82.209.243.241"   //NTP 伺服器 IP
#define HOST_PORT   123  //NTP 埠號

ESP8266 wifi(espSerial);  //建立名為 wifi 的 ESP8266 物件

void setup() {
  Serial.begin(9600);  //設定硬體序列埠速率 (debug 用)
  Serial.print("setup begin\r\n");
  Serial.print("Join AP success\r\n");
  Serial.print("IP: ");
  Serial.println(wifi.getLocalIP().c_str()); //傳回本地 IP 的字元串
  if (wifi.disableMUX()) {Serial.print("single ok\r\n");} //關閉多重連線
  else {Serial.print("single err\r\n");}
  Serial.print("setup end\r\n");
  }
void loop() {
  ntpupdate();  //呼叫 NTP 對時函數
  delay(20000);
  }

void ntpupdate() {
  uint8_t buffer[128]={0};  //儲存 NTP 回應字元的緩衝器
  if (wifi.registerUDP(HOST_NAME, HOST_PORT)) { //註冊 UDP (CIPSTART)
    Serial.print("register udp ok\r\n");
    }
  else {Serial.print("register udp err\r\n");}
  //定義兩個程式記憶體變數 (Flash) 儲存要傳送的 UDP 資料 (0~3, 12~15)
  static const char PROGMEM timeReqA[]={227,0,6,236};  //E3,00,06,EC (前 4 Bytes)
  static const char PROGMEM timeReqB[]={49,78,49,52};  //31,4E,31,34 (索引 12~15)
  //製作要傳送給 NTP 伺服器的封包資料
  uint8_t buf[48];  //傳送與接收 UDP 封包的字元串緩衝器
  memset(buf, 0, sizeof(buf));  //全部先填入預設值 0
  //把存於 Flash 的陣列複製到字元串緩衝器 (0~3, 12~15)
  memcpy_P(buf, timeReqA, sizeof(timeReqA));
  memcpy_P(&buf[12], timeReqB, sizeof(timeReqB));
  //傳送資料給 NTP 伺服器
  wifi.send((const uint8_t*)buf, 48);
  //接收 NTP 伺服器回應至 buffer 緩衝器 (索引 43~40, timeout=1 秒)
  uint32_t len=wifi.recv(buffer, sizeof(buffer), 10000);
  if (len > 0) { //有收到 NTP 回應
    Serial.print("NTP time (s):");
    //計算 UNIX 開始到現在之毫秒數 (前面三個 byte 分別移位後做 OR 運算)
    unsigned long t=(((unsigned long)buffer[40] << 24) |
                     ((unsigned long)buffer[41] << 16) |
                     ((unsigned long)buffer[42] <<  8) |
                      (unsigned long)buffer[43]);
    Serial.println(t);
    Serial.print("UNIX time (s):");
    Serial.println(t - 2208988800UL);
    }
  //登出 UDP (CIPCLOSE)
  if (wifi.unregisterUDP()) {
    Serial.print("unregister udp ");
    Serial.println(" ok");
    }
  else {
    Serial.print("unregister udp ");
    Serial.println(" err");
    }
  }

此程式使用了 PROGMEM (程式記憶體, 即 Nano 的 ATMEL328P 處理器內置的 32KB Flash) 來儲存 NTP 伺服器的 IP 與傳回時間. 關於 PROGMEM 可參考官網或葉難關於 Arduino 記憶體的大作 :

https://www.arduino.cc/en/Reference/PROGMEM
# Arduino:關於記憶體之二三事

NTP 伺服器回應的是從 1900/01/01 以來至今的秒數, 放在回應字元的 byte 40 (MSB) ~43 (LSB), 因此 byte 40 要向左移 24 位元, byte 41 左移 16 位元, byte 42 左移 8 位元, byte 43 不用移, 這樣四個 byte 做 OR 後就組成 32 位元的長整數了, 也就是自 1900/1/1 以來的秒數.

此範例程式的輸出如下 :

setup begin
Join AP success
IP: 192.168.43.40
single ok
setup end
register udp err
NTP time (s):3655701527
UNIX time (s):1446712727
unregister udp  ok
register udp ok
NTP time (s):3655701548
UNIX time (s):1446712748
unregister udp  ok
register udp ok
NTP time (s):3655701569
UNIX time (s):1446712769
unregister udp  ok
register udp ok
NTP time (s):3655701590
UNIX time (s):1446712790
unregister udp  ok
register udp ok
NTP time (s):3655701610
UNIX time (s):1446712810
unregister udp  ok
......

如果要將 NTP 傳回的 UTC 時間戳記顯示為 "時:分:秒" 該怎麼做呢? 這就要用到餘數的觀念, 除以一天的秒數 86400 後的餘數, 就是小時的秒數, 再除以 一小時 3600 秒即得時數也. 除以一小時 3600 秒後的餘數, 即為分之秒數, 再除以 60 即得分; 同理除以 60 的餘數即為秒也, 程式如下 :

#include "ESP8266.h"
#include <SoftwareSerial.h>
SoftwareSerial espSerial(10, 11); // (RX:D10, TX:D11)

#define HOST_NAME   "82.209.243.241"   //NTP 伺服器 IP
#define HOST_PORT   123  //NTP 埠號

ESP8266 wifi(espSerial);  //建立名為 wifi 的 ESP8266 物件

void setup() {
  Serial.begin(9600);  //設定硬體序列埠速率 (debug 用)
  Serial.print("setup begin\r\n");
  Serial.print("Join AP success\r\n");
  Serial.print("IP: ");
  Serial.println(wifi.getLocalIP().c_str()); //傳回本地 IP 的字元串
  if (wifi.disableMUX()) {Serial.print("single ok\r\n");} //關閉多重連線
  else {Serial.print("single err\r\n");}
  Serial.print("setup end\r\n");
  }
void loop() {
  ntpupdate();  //呼叫 NTP 對時函數
  delay(20000);
  }

void ntpupdate() {
  uint8_t buffer[128]={0};  //儲存 NTP 回應字元的緩衝器
  if (wifi.registerUDP(HOST_NAME, HOST_PORT)) { //註冊 UDP (CIPSTART)
    Serial.print("register udp ok\r\n");
    }
  else {Serial.print("register udp err\r\n");}
  //定義兩個程式記憶體變數 (Flash) 儲存要傳送的 UDP 資料 (0~3, 12~15)
  static const char PROGMEM timeReqA[]={227,0,6,236};  //E3,00,06,EC (前 4 Bytes)
  static const char PROGMEM timeReqB[]={49,78,49,52};  //31,4E,31,34 (索引 12~15)
  //製作要傳送給 NTP 伺服器的封包資料
  uint8_t buf[48];  //傳送與接收 UDP 封包的字元串緩衝器
  memset(buf, 0, sizeof(buf));  //全部先填入預設值 0
  //把存於 Flash 的陣列複製到字元串緩衝器 (0~3, 12~15)
  memcpy_P(buf, timeReqA, sizeof(timeReqA));
  memcpy_P(&buf[12], timeReqB, sizeof(timeReqB));
  //傳送資料給 NTP 伺服器
  wifi.send((const uint8_t*)buf, 48);
  //接收 NTP 伺服器回應至 buffer 緩衝器 (索引 43~40, timeout=1 秒)
  uint32_t len=wifi.recv(buffer, sizeof(buffer), 10000);
  if (len > 0) { //有收到 NTP 回應
    //計算 UNIX 開始到現在之毫秒數 (後面三個 byte 分別移位後做 OR 運算)
    unsigned long t=(((unsigned long)buffer[40] << 24) |
                     ((unsigned long)buffer[41] << 16) |
                     ((unsigned long)buffer[42] <<  8) |
                      (unsigned long)buffer[43]) - 2208988800UL;
    //將 UTC 時間戳記轉換為 "時:分:秒"
    Serial.print("The UTC time is ");  //輸出 UTC 時間
    Serial.print((t % 86400L) / 3600); //時 (一天 86400 秒, 取餘數除 3600 為時)
    Serial.print(':');
    if (((t % 3600) / 60) < 10 ) {Serial.print('0');} //分 (小於 10 補 0)
    Serial.print((t  % 3600) / 60);  //分
    Serial.print(':');
    if ( (t % 60) < 10 ) {Serial.print('0');}  //秒 (小於 10 補 0)
    Serial.println(t % 60); //秒

    }
  //登出 UDP (CIPCLOSE)
  if (wifi.unregisterUDP()) {
    Serial.print("unregister udp ");
    Serial.println(" ok");
    }
  else {
    Serial.print("unregister udp ");
    Serial.println(" err");
    }
  }

其輸出為 :

setup begin
Join AP success
IP: 192.168.2.106
single ok
setup end
register udp ok
The UTC time is 15:49:56
unregister udp  ok
register udp ok
The UTC time is 15:50:16
unregister udp  ok
register udp ok
The UTC time is 15:50:37
unregister udp  ok
register udp ok
The UTC time is 15:50:58
unregister udp  ok
register udp ok
The UTC time is 15:51:19
unregister udp  ok

如果要顯示台灣的中原標準時間, 則 UTC 要加 8 小時, 上面程式 t 變數後面改成如下即可 :

    //將 CCT 時間戳記轉換為 "時:分:秒"
    Serial.print("The CCT time is ");  //輸出 CCT 時間 (UTC+8)
    if ((((t % 86400L) / 3600 + 8) % 24) < 10) {Serial.print('0');} //時 (小於 10 補 0)
    Serial.print(((t % 86400L) / 3600 + 8) % 24); //時 (一天 86400 秒, 取餘數除 3600 為時)
    Serial.print(':');
    if (((t % 3600) / 60) < 10 ) {Serial.print('0');} //分 (小於 10 補 0)
    Serial.print((t  % 3600) / 60);  //分
    Serial.print(':');
    if ( (t % 60) < 10 ) {Serial.print('0');}  //秒 (小於 10 補 0)
    Serial.println(t % 60); //秒

哈哈, 費了一周的功夫終於搞定網路對時的問題了, 開源的好處就是這樣, 高手貢獻的函式庫可以大大地節省我們學習以及開發的時間, 在此要向這些前輩致敬. 取之於網路, 用之於網路, 這就是我勤於撰寫實測紀錄的原因, 資訊的公開交流是文明進步的重要推手, 以前的科學家們還得透過寫信與同儕交換彼此的發現呢! 有網路真好.

2015-11-06 補充 :

此函式庫似乎只能在 0.9.2 版韌體上運行, 我用 0.9.5 版韌體的 ESP8266 跑, 結果在註冊 UDP 時開始失敗, 輸出如下 :

setup begin
Join AP success
IP: +CIFSR:STAIP,"0.0.0.0"
+CIFSR:STAMAC,"18:fe:34:f3:00:ae"
single ok
setup end
register udp err
unregister udp  err
register udp err
unregister udp  err
register udp err
unregister udp  err
register udp err
unregister udp  err
register udp err
unregister udp  err
.....

參考資料 :

# type: uint8_t, uint16_t, uint32_t, uint64_t
c_str()這個函數是做什麼用的
# Difference between memcpy_p and memcpy?
# memcpy
http://docs.iteadstudio.com/ITEADLIB_Arduino_WeeESP8266/index.html
http://phoenard.com/esp8266-connect-to-a-network/
PM2.5 空氣品質偵測與自動化控制器 使用Arduino UNO開發 (第一話) 數據記錄器篇

其他 ESP8266 WiFi 函式庫 :

ESP8266WiFi/examples/NTPClient/NTPClient.ino
sandeepmistry/esp8266-Arduino
ekstrand/ESP8266wifi

2015年11月4日 星期三

關於用 Arduino+ESP8266 進行網路對時的問題

我在 Oreilly 出版的 "Arduino Cookbook" (Arduino 錦囊妙計第二版) 15-14 節看到利用 NTP (Network Time Protocol) 協定從同步時鐘伺服器網站取得 UTC 時間 (Coordinated Universal Time, 世界標準時間) 的範例, 我覺得這在物聯網應用應該很常用, 例如為 RTC 模組更新時間, 以便與世界標準時間保持同步, 或者在記錄感測器數據到 SD 卡時, 如果沒有 RTC 模組可用, 也可以透過 WiFi 網路取得事件發生時間. 這本書的範例程式可從 Oreilly 網站下載 :

# ArduinoCookbook2E.zip

解開後在 ch15/ch15r14 子目錄下就可以找到範例程式檔. 我將此兩範例檔壓縮在下列 NTP_Ethernet.zip 中 :

# https://www.dropbox.com/s/7qz2v39ph9tv06u/NTP_Ethernet.zip?dl=0

不過, 這個範例使用的是乙太網函式庫 Ethernet.h, 主要是支援以 WZ5100 晶片為主的 Ethernet 網路擴充板, 無法用在 ESP8266 的 WiFi 網路.

在 TCP/IP 模型中, NTP 協定屬於應用層協定, 使用 port 123, 是目前仍在使用中的古老協定之一, 其傳輸層使用 UDP, 這是一個不可靠的非連接性協定, 適用於需要反應較即時的應用. 關於 NTP 可參閱維基與鳥哥的大作 :

# 鳥哥的 Linux 私房菜 : 第十五章、時間伺服器: NTP 伺服器
# Wiki : 網路時間協定
# Wiki : Network Time Protocol


大致摘要一下 NTP 用到的知識 :
  1. GMT (格林威治時間) 與 UTC (國際標準時間) 雖然都是以格林威治時區為參考, 但準度不同, GMT 是太陽通過格林威治天文臺為基準, 使用於 1880 年以前; 而 UTC 則以原子鐘的銫原子震盪週期來計算, 兩者約有 16 分鐘的誤差.
  2. 台灣位於東經 120 度北緯 25 度, 時區為 CCT (中原標準時區), GMT+8.
  3. NTP 伺服器傳回的是 1900/1/1 以來的總秒數, 若要換算成 UNIX 自 1970/1/1 運作以來的秒數, 必須減掉 2208988800 秒.
而 NTP 協定的結構可參考 :

# NTP Data Packet Structure of theNTP Data packet

在 ESP8266 的 AT 韌體中, UDP 訊息可以用 AT 指令 CIPSTART 來起始, 但這並不是建立連線, 而僅僅是註冊要傳送 IP 封包而已, 傳送資料要用 CIPSEND, 例如 :

AT+CIPSTART="UDP","129.6.15.28",123

AT+CIPSEND=48

但接下來要傳送甚麼資料給 NTP 伺服器呢? 下列這篇文章的範例似乎是要傳送 48 BYTES 的資料給 ESP8266 :

# Arduino ESP8266 and NTP

但我照著此文所述, 在 < 出現後傳送那幾個怪碼 (ãì 1N14) 給 ESP8266, 卻沒收到任何回應, 而作者的卻是回應如下 :

SEND OK

+IPD,48:$ ãACTSØ7×0Æêä

OK

難道這是編碼的問題嗎? 這篇文章也沒下文. 最後, 我在下面這篇找到可資參考範例, 是有回應了, 但還未成功 :

# NTP from ESP8266 via AT commands

此作者是參考 Arduino 論壇另一篇回應文章改寫的 :

Arduino ESP8266 and NTP

從這兩篇可以進一步了解, 傳送給 NTP 伺服器的資料有 48 個位元組, 其中 Byte 0~3 以及 12~15 須填入特定的值. 而傳回的時間戳記 (time stamp, 1900/1/1 以來之秒數) 是放在回應字元的 Byte 40 (MSB)~43 (LSB).

作者使用 Arduino Mega 當主控, 以 Serial1 與 ESP8266 介接, 但我是用軟體序列埠, 故我把這個程式中的 Serial1 全部改成 sSerial, 並添加 SoftwareSerial sSerial(10,11) 指令, 以便能移植到我的 Nano + ESP8266 實驗麵包板上 :

/*
ESP8266 connected to sSerial
*/
#include <SoftwareSerial.h>
SoftwareSerial sSerial(10,11); //(RX,TX) 與 ESP8266 介接的軟體串列埠

void setup() {
  // initialize both serial ports:
  Serial.begin(9600);
  sSerial.begin(9600);
  Serial.println("...");
  }

void loop() {
  delay(3000);
  Serial.println();
  Serial.println("Getting time..");
  int Epoch = GetTime();
  Serial.print("Epoch is: ");
  Serial.println(Epoch);
  delay(5000);
  }

const int NTP_PACKET_SIZE= 48; // NTP time stamp is in the first 48 bytes of the message
byte packetBuffer[ NTP_PACKET_SIZE]; //buffer to hold incoming and outgoing packets

unsigned long GetTime() {
  String cmd = "AT+CIPSTART=\"UDP\",\"130.102.128.23\",123"; // NTP server
  sSerial.println(cmd);
  delay(2000);
  if(sSerial.find("Error")) {
    Serial.print("RECEIVED: Error");
    return 0;
    }
  int counta = 0;
  memset(packetBuffer, 0, NTP_PACKET_SIZE);
  // Initialize values needed to form NTP request
  // (see URL above for details on the packets)
  packetBuffer[0] = 0b11100011;   // LI, Version, Mode
  packetBuffer[1] = 0;     // Stratum, or type of clock
  packetBuffer[2] = 6;     // Polling Interval
  packetBuffer[3] = 0xEC;  // Peer Clock Precision
  packetBuffer[12]=49;     //參考 ID (4 bytes)
  packetBuffer[13]=0x4E;
  packetBuffer[14]=49;
  packetBuffer[15]=52;

  sSerial.print("AT+CIPSEND=");
  sSerial.println(NTP_PACKET_SIZE);
  if (sSerial.find(">")) {
    for (byte i = 0; i < NTP_PACKET_SIZE; i++) {
      sSerial.write(packetBuffer[i]);
      delay(5);
      }
    }
  else{
    sSerial.println("AT+CIPCLOSE");
    return 0;
    }
  //sSerial.find("+IPD,48:");
  int acksize = NTP_PACKET_SIZE + 1 + 2 + 8; // ESP8266 adds a space, a CRLF and starts with "+IPD,48:"

  Serial.println("ESP2866 ACK : ");
  for (byte i = 0; i < acksize; i++) {
    while (sSerial.available() == 0) { // you may have to wait for some bytes
      counta += 1;
      Serial.print(".");
      delay(100);
      if (counta == 15) {return 0;}
      }
    byte ch = sSerial.read();
    if (ch < 0x10) {Serial.print('0');}
    Serial.print(ch,HEX);
    Serial.print(' ');
    if ( (((i+1) % 15) == 0) ) { Serial.println(); }
    }
  Serial.println();
  Serial.println();
  memset(packetBuffer, 0, NTP_PACKET_SIZE);
  Serial.println("Server answer : ");
  int i = 0;
  while (sSerial.available() > 0) {
    byte ch = sSerial.read();
    if (i <= NTP_PACKET_SIZE) {
      packetBuffer[i] = ch;
      }
    if (ch < 0x10) {Serial.print('0');}
    Serial.print(ch,HEX);
    Serial.print(' ');
    if ((((i+1) % 15) == 0)) {Serial.println();}
    delay(5);
    i++;
    if ((i < NTP_PACKET_SIZE) && (sSerial.available() == 0)) {
      while (sSerial.available() == 0) { // you may have to wait for some bytes
        counta += 1;
        Serial.print("!");
        delay(100);
        if (counta == 15) {return 0;}
        }
      }
    }
  Serial.println();
  Serial.println();
  Serial.print(i+1);
  Serial.println(" bytes received"); // will be more than 48
  Serial.print(packetBuffer[40],HEX);
  Serial.print(" ");
  Serial.print(packetBuffer[41],HEX);
  Serial.print(" ");
  Serial.print(packetBuffer[42],HEX);
  Serial.print(" ");
  Serial.print(packetBuffer[43],HEX);
  Serial.print(" = ");
  unsigned long highWord = word(packetBuffer[40], packetBuffer[41]);
  unsigned long lowWord = word(packetBuffer[42], packetBuffer[43]);
  // combine the four bytes (two words) into a long integer
  // this is NTP time (seconds since Jan 1 1900):
  unsigned long secsSince1900 = highWord << 16 | lowWord;
  Serial.print(secsSince1900,DEC);
  // Unix time starts on Jan 1 1970. In seconds, that's 2208988800:
  const unsigned long seventyYears  = 2208988800UL;
  // subtract seventy years:
  unsigned long epoch = secsSince1900 - seventyYears;
  unsigned long DST = 60*60*2; // adjust to your GMT+DST
  unsigned long timestamp = epoch + DST;
  Serial.println();
  Serial.print("Epoch : ");
  Serial.println(epoch,DEC);
  return epoch;
  }

此程式執行後, 序列埠監視視窗的輸出如下 :

...

Getting time..
ESP2866 ACK :
C6 00 06 EC 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 .0D 0A 53 45 4E 44 20 4F 4B 0D 0A

Server answer :


1 bytes received
0 0 0 0 = 0
Epoch : 2085978496
Epoch is: -32384

最後一列中小數點後面的資料之 ASCII 解碼如下 :

0D (CR)
0A (LF)
53 (S)
45 (E)
4E (N)
44 (D)
20 (SP)
4F (O)
4B (K)
0D (CR)
0A (LF)

但是從其輸出可知, 似乎沒有從 NTP 伺服器收到回應,  這是 NTP 伺服器的問題嗎? 我更改 IP 為 192.43.244.18 與 192.5.41.40 後有改善, 雖然大部分要求都沒有回應, 但偶而還是會收到伺服器回應, 例如 :


Getting time..
ESP2866 ACK :
C6 00 06 EC 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 ..0D 0A 53 45 4E 44 20 4F 4B 0D 0A

Server answer :
0D 0A 2B 49 50 44 2C 34 38 3A 24 01 06 E3 00
00 00 00 00 00 00 00 41 43 54 53 D9 E4 6E 2D
30 F4 36 DA 00 00 00 00 00 00 00 00 D9 E4 6E
51 80 26 3B F1 D9 E4 6E 51 80 27 8C 0A 4B

60 bytes received
0 0 D9 E4 = 55780
Epoch : 2086034276
Epoch is: 23396

但新的問題是, 此程式就算成功地收到 NTP 伺服器的回應, 其 Epoch 時間值都一樣不變, Why?

參考下列網址 :

http://www.stdtime.gov.tw/chinese/bulletin/NTP%20promo.txt
台灣合用的ntp server


可用的 NTP 伺服器網址收集如下, 但不是每一個都有回應 :

192.43.244.18    time.nist.gov (ACTS)
132.163.135.130  time-A.timefreq.bldrdoc.gov (ACTS)
192.5.41.40      tick.usno.navy.mil


tick.stdtime.gov.tw
tock.stdtime.gov.tw
time.stdtime.gov.tw
clock.stdtime.gov.tw
watch.stdtime.gov.tw

另外, 有一位德國人把支援 NTP AT 指令的 0.9.4 韌體的函式寫成函式庫 :

# Arduino Zeitsynchronisation mit ESP8266 und NTP

其函式庫含範例可從 Git 下載 :

https://drive.google.com/file/d/0B0j0q4jsSHdjMnVyajZZMXhvNTQ/view

而支援 NTP AT 指令的 0.9.4 版韌體可從下列網址下載 :

https://drive.google.com/file/d/0B0j0q4jsSHdjMnVyajZZMXhvNTQ/view

如果 0.9.2 版的韌體在 NTP 網路對時上一直無法成功的話, 考慮找一顆 ESP8266 來燒錄 0.9.4 版韌體試試看, 此版本有為了 NTP 新增 AT 指令.

其他參考資料 :


# How do es work?
# 有無人玩開ESP8266 (超平WIFI SOC MODULE)呢?
teos0009/ESP8266 Arduino Mega (有範例程式)
Tutorial: IoT Datalogger with ESP8266 WiFi Module and FRDM-KL25Z
# A Guide To Using ESP8266 With TEENSY 3
鳥哥的 Linux 私房菜 : 第十五章、時間伺服器: NTP 伺服器
NTP Request Packet
# Addition of NTP support for 0020000903/0020000904 (0.9.3 版韌體)
# Using an ESP8266 as a time source (Part 1) (0.9.3 版韌體)
# Using an ESP8266 as a time source (Part 2) (使用 NodeMCU)
# Display Internet based Time (NTP)
# UDP NTP CLIENT
# NTP DRIVER FOR ESP8266
Richard's stuff (ESP8266 剖析)
# A SIMPLE NTP CLIENT FOR ESP8266
# REAL TIME CLOCK (DS1307/DS3231) FOR THE ESP8266
# Low-memory footprint, scheduler-friendly NTP client
Arduino ESP8266 and NTP (Read 6501 times)
# Network Time Protocol (NTP) Client
# Arduino教學-使用ESP8266 wifi模組+DHT 溫溼度感測器上傳thingspeak
# 使用 Arduino IDE 開發 ESP8266 物聯網應用 - ThingSpeak, HTTP GET / POST 資料上傳方法
# Arduino 結合ESP8266將空氣資料送到Synology NAS(取代Thingspeak)
家庭環境自動監測管理器-Arduino nano與esp8266實作(韌體篇)
# UDP SERVER EXAMPLE
# A UDP example of ESP8266
# at_example_0020000903
IoT, Internet Of Thing, ESP8266 開始玩了
BROWSER CONTROLLED RASPBERRY PI CAMERA CAR
http://lindr.org/IoT/   (ESP8266 整合開發板 Uninus)
# Uninus 無線Wifi 手機控制 繼電器模組 $410
# [第3代] Uninus 無線Wifi 手機控制 繼電器模組 支援Web/REST/MQTT IoT 智慧開關 小K $420
# 第3代 Uninus ESP8266 NodeMCU ESP-12E/07 最小系統 測試版 $230
# AT+CIPSTART bug when using UDP and DNS lookup
help with esp8266
Arduino ESP8266 and NTP (Read 6723 times)
# Help to Decipher Arduino Code
https://forum.arduino.cc/index.php?topic=285188.0
# Arduino新款EEPROM函式庫小觀察
# Arduino 學習筆記

2016-06-08 補充 :

我已經在下列文章解決此問題 :

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

2016-06-12 補充 :

直接使用 AT 指令查詢 NTP 的問題已經解決了, 詳見 :

以 Arduino + ESP8266 物聯網模組重作 NTP 實驗 (二)


2015年11月3日 星期二

關於甘蔗榨汁機

最近為了避免菜園的甘蔗越長越多, 每周末回鄉下時都要砍個兩根來吃. 我發現種甘蔗有個好處, 它是隨時可用來招待訪客的水果. 其他水果像木瓜與香蕉雖然整年都能收獲, 但都沒辦法隨時取食, 更別說一年一獲的芒果與龍眼了.

但最近這個想法踢到鐵板. 我問同事要不要蔗種, 結果他說不想種, 因為他上回植了好幾顆牙, 價值百萬的植牙可不能壞在甘蔗手上. 其次是上周阿泉伯與伯母來時, 我削甘蔗給他們, 結果呢, 阿泉伯苦笑說, 他也很想吃, 如果還能嚼甘蔗的話會很高興. 沒錯, 上了年紀齒牙動搖的人, 只能望蔗興嘆.

因此我就想, 是不是有家用榨甘蔗汁的機器呢? 如果要像媽以前那樣削好拿去市集那邊請人用電動機器榨, 不僅麻煩, 而且也無法即時給客人喝. 我要的是在家裡馬上就能榨. 結果我在露天找到這款手動的不鏽鋼甘蔗榨汁機, 要價 5000 元 :

# 露天 : 不銹鋼家用 手搖榨甘蔗機 甘蔗榨汁機 果汁機 壓榨機 水果榨汁機 $5000

PChome 更貴, 要 5500 元 :

PChome : 全新~不銹鋼家用手搖式榨甘蔗機/甘蔗榨汁機 水果榨汁機 $5500

為了喝甘蔗汁而花 5000 元買榨汁機, 感覺有點那個. 所以我就上淘寶網找看看, 結果找到同款式產品, 最便宜是人民幣 360 元 :

淘寶 : 不鏽鋼家用手搖式榨甘蔗機/甘蔗榨汁機 水果榨汁機 江浙滬385包郵 $RMB 360

超商取貨付款加運費人民幣 25 元, 合計 385 元, 依今天台銀賣出匯價 5.18 算, 約台幣 1994 元左右, 2000 元有找, 若加購 1.6 元人民幣的退貨險 (萬一須退貨時不須再負擔退貨運費), 則總價是 386.6 元人民幣, 約合 2003 元台幣. 哇咧, 差價是 3000 元哪! 真難想像, 台灣電商還有機會嗎?

其他參考 :

特價 家用不鏽鋼手壓式榨甘蔗機/甘蔗榨汁機 $RMB370
# 家用不鏽鋼手搖手動式甘蔗榨汁機 小麥草水果榨汁機甘蔗機 特價 $RMB380 # PChome : 不鏽鋼果汁壓汁機 (大) 水果 榨汁機 榨汁器 壓汁機 擠檸檬 $349
# 永順利 甘蔗榨汁機

2016-03-05 補充 :

感謝 黃金檸檬 兄意見, 原來運費不能這樣算, 要看賣家算多少. 我也已打消買榨汁機念頭了. 


2015年11月2日 星期一

ESP8266 0.9.2.2 版韌體多重連線時 TCP 連線失敗問題

前天在撰寫自己的 AT 指令 WiFi 函數時發現, 如果設定為多重連線, 即 AT+CIPMUX=1 時, TCP 連線就無法建立, 這是在 0.9.2.2 版韌體上測試時發現的, 參考 :

# 撰寫 Arduino 的 ESP8266 WiFi 函式

為了確定是否跟 MODE 為 1 或 3 有關, 今天又在快完成的 WiFi 函數的基礎上再進行測試. 結果 : 與工作模式無關, 只要是多重連線, TCP 連線就會失敗. 測試程式如下 :

#include <SoftwareSerial.h>
SoftwareSerial sSerial(10,11); //(RX,TX) 與 ESP8266 介接的軟體串列埠
String ssid="EDIMAX-tony";  //無線基地台識別
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("Set Mode=1 : " + set_mode(1));
  Serial.println("Mode : " + get_mode());
  Serial.println("Mux : " + get_mux());
  Serial.println("Set Mux=0 : " + set_mux(0));
  Serial.println("Mux : " + get_mux());
  Serial.println("Get AP : " + get_ap());    
  Serial.println("Get IP : " + get_ip());
  Serial.println("Connect Google : " + start_tcp("www.google.com",80));
  Serial.println("Send GET : " + send_data("GET /"));
  Serial.println("Connect Thingspeak : " + start_tcp("184.106.153.149",80));
  Serial.println("Send GET : " + send_data("GET /update?api_key=NO5N8C7T2KINFCQE&field1=28.00&field2=82.40&field3=81.00"));     
  }

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_ESP8266_response() {  //取得 ESP8266 的回應字串
  String str="";  //儲存接收到的回應字串
  char c;  //儲存接收到的回應字元
  while (sSerial.available()) {  //若軟體序列埠接收緩衝器還有資料
    c=sSerial.read();  //必須放入宣告為 char 之變數 (才會轉成字元)
    str.concat(c);  //串接回應字元
    delay(10);  //務必要延遲, 否則太快
    }
  str.trim();  //去除頭尾空白字元
  return str;
  }

String get_version() {
  sSerial.println("AT+GMR");  //取得韌體版本
  sSerial.flush();  //等待序列埠傳送完畢
  delay(1000);
  String str=get_ESP8266_response();  //取得 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();  //等待序列埠傳送完畢
  delay(1000);
  String str=get_ESP8266_response();  //取得 ESP8266 回應字串
  if (str.indexOf("OK") == -1) {return "NG";}
  else {return str.substring(str.indexOf(":")+1,str.indexOf("\r\n"));}
  }

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

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

String set_mode(byte mode) {
  sSerial.println("AT+CWMODE=" + String(mode));  //設定工作模式
  sSerial.flush();  //等待序列埠傳送完畢
  delay(1000);
  String str=get_ESP8266_response();  //取得 ESP8266 回應字串
  if (str.indexOf("OK") != -1 || str.indexOf("no change") != -1) {return "OK";}
  else {return "NG";}
  }

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

String set_mux(byte mux) {  //0=single, 1=multiple
  sSerial.println("AT+CIPMUX=" + String(mux));  //設定連線模式
  sSerial.flush();  //等待序列埠傳送完畢
  delay(1000);
  String str=get_ESP8266_response();  //取得 ESP8266 回應字串
  if (str.indexOf("OK") != -1) {return "OK";}
  else {return "NG";}
  }

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

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

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

String start_tcp(String address, byte port) {
  sSerial.println("AT+CIPSTART=\"TCP\",\"" + address + "\"," + String(port));
  sSerial.flush();  //等待序列埠傳送完畢
  delay(1000);
  String str=get_ESP8266_response();  //取得 ESP8266 回應字串
  if (str.indexOf("Linked") != -1) {return "OK";}
  else {return "NG";}
  }

String send_data(String s) {
  String s1=s + "\r\n";  //務必加上跳行
  sSerial.println("AT+CIPSEND=" + String(s1.length()));
  sSerial.flush();  //等待序列埠傳送完畢
  delay(1000);
  String str=get_ESP8266_response();  //取得 ESP8266 回應字串
  if (str.indexOf(">") != -1) {  //收到 > 開始傳送資料
    sSerial.println(s1); //傳送資料
    sSerial.flush();  //等待序列埠傳送完畢
    delay(7000);
    str=get_ESP8266_response();  //取得 ESP8266 回應字串
    if (str.indexOf("+IPD") != -1) {return "OK";}  //傳送成功會自動拆線
    else {  //傳送不成功須自行拆線
      close_ip();  //關閉 IP 連線
      return "NG";
      }
    }
  else {  //傳送不成功須自行拆線
    close_ip();  //關閉 IP 連線
    return "NG";
    }
  }

String close_ip() {
  sSerial.println("AT+CIPCLOSE");  //關閉 IP 連線
  sSerial.flush();  //等待序列埠傳送完畢
  delay(1000);
  String str=get_ESP8266_response();  //取得 ESP8266 回應字串
  if (str.indexOf("OK") != -1) {return "OK";}
  else {return "NG";}
  }

其輸出如下 :

*** SoftSerial connection to ESP8266 ***
Firmware version : 0018000902
Set Mode=1 : OK
Mode : 1
Mux : 0
Set Mux=0 : OK
Mux : 0
Get AP : "H30-L02-webbot"
Get IP : 192.168.43.151
Connect Google : OK  (成功)
Send GET : OK
Connect Thingspeak : OK  (成功)
Send GET : OK

可見在 mode=1 (station), mux=0 (single) 時, TCP 連線可正常建立. 接下來測試 mode=1 (station), mux=1 (multiple) 情況, 下面只列測試程式, 函式不再列出 :

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("Set Mode=1 : " + set_mode(1));   //Station mode
  Serial.println("Mode : " + get_mode());
  Serial.println("Mux : " + get_mux());
  Serial.println("Set Mux=1 : " + set_mux(1));  //多重連線
  Serial.println("Mux : " + get_mux());
  Serial.println("Get AP : " + get_ap());    
  Serial.println("Get IP : " + get_ip());
  Serial.println("Connect Google : " + start_tcp("www.google.com",80));
  Serial.println("Send GET : " + send_data("GET /"));
  Serial.println("Connect Thingspeak : " + start_tcp("184.106.153.149",80));
  Serial.println("Send GET : " + send_data("GET /update?api_key=NO5N8C7T2KINFCQE&field1=28.00&field2=82.40&field3=81.00"));      
  }

其輸出為 :

*** SoftSerial connection to ESP8266 ***
Firmware version : 0018000902
Set Mode=1 : OK
Mode : 1
Mux : 0
Set Mux=1 : OK
Mux : 1
Get AP : "H30-L02-webbot"
Get IP : 192.168.43.151
Connect Google : NG (失敗)
Send GET : NG
Connect Thingspeak : NG (失敗)
Send GET : NG

可見只要一改到多重連線, TCP 連線就無法順利建立了. 接下來要測試是否 mode 會影響 TCP 連線, 程式如下 :

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("Set Mode=3 : " + set_mode(3));   //Station+AP mode
  Serial.println("Mode : " + get_mode());
  Serial.println("Mux : " + get_mux());
  Serial.println("Set Mux=0 : " + set_mux(0));  //單一連線
  Serial.println("Mux : " + get_mux());
  Serial.println("Get AP : " + get_ap());    
  Serial.println("Get IP : " + get_ip());
  Serial.println("Connect Google : " + start_tcp("www.google.com",80));
  Serial.println("Send GET : " + send_data("GET /"));
  Serial.println("Connect Thingspeak : " + start_tcp("184.106.153.149",80));
  Serial.println("Send GET : " + send_data("GET /update?api_key=NO5N8C7T2KINFCQE&field1=28.00&field2=82.40&field3=81.00"));      
  }

其輸出如下 :

*** SoftSerial connection to ESP8266 ***
Firmware version : 0018000902
Set Mode=3 : OK
Mode : 3
Mux : 0
Set Mux=0 : OK
Mux : 0
Get AP : "H30-L02-webbot"
Get IP : 192.168.4.1
Connect Google : OK (成功)
Send GET : OK
Connect Thingspeak : OK  (成功)
Send GET : OK

可見在模式 3, 單一連線情況下, TCP 連線沒問題. 若改為多重連線呢? 程式如下 :

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("Set Mode=3 : " + set_mode(3));   //Station+AP mode
  Serial.println("Mode : " + get_mode());
  Serial.println("Mux : " + get_mux());
  Serial.println("Set Mux=1 : " + set_mux(1));  //多重連線
  Serial.println("Mux : " + get_mux());
  Serial.println("Get AP : " + get_ap());    
  Serial.println("Get IP : " + get_ip());
  Serial.println("Connect Google : " + start_tcp("www.google.com",80));
  Serial.println("Send GET : " + send_data("GET /"));
  Serial.println("Connect Thingspeak : " + start_tcp("184.106.153.149",80));
  Serial.println("Send GET : " + send_data("GET /update?api_key=NO5N8C7T2KINFCQE&field1=28.00&field2=82.40&field3=81.00"));      
  }

輸出如下 :

*** SoftSerial connection to ESP8266 ***
Firmware version : 0018000902
Set Mode=3 : OK
Mode : 3
Mux : 0
Set Mux=1 : OK
Mux : 1
Get AP : "H30-L02-webbot"
Get IP : 192.168.4.1
Connect Google : NG  (失敗)
Send GET : NG
Connect Thingspeak : NG (失敗)
Send GET : NG

由上面測試可知,0.9.2.2 版韌體在多重連線下, TCP 連線無法建立, 與模式是 Station 或 Sattion + AP 無關. Why? 難道是與韌體版本有關嗎? 下次有空再來測試看看 0.9.5 版.

2015-11-09 補充 :

原因找到了, 今天在測試 WeeESP8266 函式庫時發現, 原來是在多重連線模式下,必須指定代表連線之通道編號 (0~4) 才行. 參考 :

# ITEAD WeeESP8266 函式庫測試

因此我加寫了 start_tcp() 與 send_data() 這兩個函式的多型, 即多一個通道 id 參數來處理 :

#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("Set Mode=3 : " + set_mode(3));   //Station+AP mode
  Serial.println("Mode : " + get_mode());
  Serial.println("Mux : " + get_mux());
  Serial.println("Set Mux=1 : " + set_mux(1));  //多重連線
  Serial.println("Mux : " + get_mux());
  Serial.println("Get AP : " + get_ap());  
  Serial.println("Get IP : " + get_ip());
  Serial.println("Connect Google : " + start_tcp(0,"www.google.com",80));
  Serial.println("Send GET : " + send_data(0,"GET /"));
  Serial.println("Connect Thingspeak : " + start_tcp(0,"184.106.153.149",80));
  Serial.println("Send GET : " + send_data(0,"GET /update?api_key=NO5N8C7T2KINFCQE&field1=28.00&field2=82.40&field3=81.00"));    
  }

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_ESP8266_response() {  //取得 ESP8266 的回應字串
  String str="";  //儲存接收到的回應字串
  char c;  //儲存接收到的回應字元
  while (sSerial.available()) {  //若軟體序列埠接收緩衝器還有資料
    c=sSerial.read();  //必須放入宣告為 char 之變數 (才會轉成字元)
    str.concat(c);  //串接回應字元
    delay(10);  //務必要延遲, 否則太快
    }
  str.trim();  //去除頭尾空白字元
  return str;
  }

String get_version() {
  sSerial.println("AT+GMR");  //取得韌體版本
  sSerial.flush();  //等待序列埠傳送完畢
  delay(1000);
  String str=get_ESP8266_response();  //取得 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();  //等待序列埠傳送完畢
  delay(1000);
  String str=get_ESP8266_response();  //取得 ESP8266 回應字串
  if (str.indexOf("OK") == -1) {return "NG";}
  else {return str.substring(str.indexOf(":")+1,str.indexOf("\r\n"));}
  }

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

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

String set_mode(byte mode) {
  sSerial.println("AT+CWMODE=" + String(mode));  //設定工作模式
  sSerial.flush();  //等待序列埠傳送完畢
  delay(1000);
  String str=get_ESP8266_response();  //取得 ESP8266 回應字串
  if (str.indexOf("OK") != -1 || str.indexOf("no change") != -1) {return "OK";}
  else {return "NG";}
  }

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

String set_mux(byte mux) {  //0=single, 1=multiple
  sSerial.println("AT+CIPMUX=" + String(mux));  //設定連線模式
  sSerial.flush();  //等待序列埠傳送完畢
  delay(1000);
  String str=get_ESP8266_response();  //取得 ESP8266 回應字串
  if (str.indexOf("OK") != -1) {return "OK";}
  else {return "NG";}
  }

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

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

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

String start_tcp(String address, byte port) {
  sSerial.println("AT+CIPSTART=\"TCP\",\"" + address + "\"," + String(port));
  sSerial.flush();  //等待序列埠傳送完畢
  delay(1000);
  String str=get_ESP8266_response();  //取得 ESP8266 回應字串
  if (str.indexOf("Linked") != -1) {return "OK";}
  else {return "NG";}
  }

String start_tcp(byte id, String address, byte port) {
  sSerial.println("AT+CIPSTART=" + String(id) + ",\"TCP\",\"" + address + "\"," + String(port));
  sSerial.flush();  //等待序列埠傳送完畢
  delay(1000);
  String str=get_ESP8266_response();  //取得 ESP8266 回應字串
  if (str.indexOf("Linked") != -1) {return "OK";}
  else {return "NG";}
  }

String send_data(String s) {
  String s1=s + "\r\n";  //務必加上跳行
  sSerial.println("AT+CIPSEND=" + String(s1.length()));
  sSerial.flush();  //等待序列埠傳送完畢
  delay(1000);
  String str=get_ESP8266_response();  //取得 ESP8266 回應字串
  if (str.indexOf(">") != -1) {  //收到 > 開始傳送資料
    sSerial.println(s1); //傳送資料
    sSerial.flush();  //等待序列埠傳送完畢
    delay(7000);
    str=get_ESP8266_response();  //取得 ESP8266 回應字串
    if (str.indexOf("+IPD") != -1) {return "OK";}  //傳送成功會自動拆線
    else {  //傳送不成功須自行拆線
      close_ip();  //關閉 IP 連線
      return "NG";
      }
    }
  else {  //傳送不成功須自行拆線
    close_ip();  //關閉 IP 連線
    return "NG";
    }
  }

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

String close_ip() {
  sSerial.println("AT+CIPCLOSE");  //關閉 IP 連線
  sSerial.flush();  //等待序列埠傳送完畢
  delay(1000);
  String str=get_ESP8266_response();  //取得 ESP8266 回應字串
  if (str.indexOf("OK") != -1) {return "OK";}
  else {return "NG";}
  }

這裡因通道 id 為 byte 類型, 因此務必用 String() 函數將其字串化後再串接, 其輸出如下 :

*** SoftSerial connection to ESP8266 ***
Firmware version : 0018000902
Set Mode=3 : OK
Mode : 3
Mux : 1
Set Mux=1 : OK
Mux : 1
Get AP : "H30-L02-webbot"
Get IP : 192.168.4.1
Connect Google : OK
Send GET : OK
Connect Thingspeak : OK
Send GET : OK

這個問題終於解決了. 


2015年11月1日 星期日

第 44 周記事

本周終於寫好大部分 ESP8266 的 WiFi 函式, 雖然還有伺服器功能尚未完成, 但目前夠用了, 等要用伺服功能時再繼續. 自從 6 月份組裝風力發電機後, 為了監看發電狀態, 開始專心研究 Arduino 以來, 已經過了 4 個月了, 由於閒暇時間不多, 只能斷斷續續地看書, 上網查資料, 以及動手做, 進度雖然很緩慢, 但只要能有進展, 就是收穫啦.

因為張同學今天要回美國, 週六 (10/31) 中午又在中華路的海王子聚餐, 連我共 8 個人, 原本以為是大家 share, 但訂桌的鍾同學堅持要請客, 說那裏是他的地盤云云. 說要付帳的去試試看有沒有人會收. 雖然他事業做得頗成功, 但這樣反而讓我們心裡覺得有負擔. 下次若再邀小學同學會, 務必先講好大家分攤才好. 不過今天大家相談甚歡, 第一次聽鍾同學談到他以前讀海專一學期就被退學開除學籍, 宿舍櫃子裡一打開都是金光閃閃的扁鑽的往事. 但他重考卻考上南一中, 真不簡單.

菜園的夏季冬瓜已近乎收成完畢, 但藤蔓仍未枯委, 繼續開花結果. 上週從高雄陽台盆栽移回來種的六株南瓜已茁壯, 相信這回這些廚餘南瓜可以成功收成. 而十月初 (10/3, 10/17) 開始種植的兩批玉米也 "亭亭玉立" 了 :


農夫的最大快樂大概就是看到自己栽種的作物逐漸長大吧! 我可不一樣, 我是一周回一次鄉下才會看到它們長多高了, 故有 "一暝大一吋" 的感覺. 而今年二月中從公司樹林裡挖回來種的香蕉, 至今約九個月就已經結果了, 預計兩周後就可以採收了 :


旁邊是同事給我的木瓜, 也已結果, 不過似乎還太小, 大約還要三周才會熟成 , 這是他給我的三株木瓜苗中唯一長大的, 長到膝蓋高時還曾經被野狗闖入菜園, 從 2/3 高度處折斷, 處於完全無葉狀態, 本以為沒救了, 沒想到竟然分出兩個幼枝, 就是現在看到的兩株狀態, 其實到根部附近合而為一, 其實是同一株 :


旁邊同時期種的甘蔗長得奇快, 有淹沒木瓜樹與香蕉樹, 遮住陽光之虞, 故而這個月以來, 我每星期天中午吃完飯後就去砍一棵甘蔗當飯後水果, 傍晚又砍一棵當點心, 以遏止甘蔗在菜園的囂張之勢.

傍晚跟姊姊兩人騎車去散步, 順便帶狗餅乾去給二路, 已經兩三個禮拜沒去了. 二路是我們騎車路徑中一戶人家養在路邊的看門狗, 愛狗成痴的菁菁每次一定會帶吃的給牠, 所以每次騎到轉角處拐彎時, 牠遠遠地看到我們時就會興奮地跳躍起來, 雀躍不已.

都市裡車在動, 人在動, 人聲吵雜,  似乎一切都死命地往前跑. 我喜歡鄉下的生活, 因為在這裡四處望過去, 幾乎甚麼都不會動, 彷彿世界停止轉動一般, 我喜歡這樣緩慢的步調.