2015年11月30日 星期一

第 48 周記事

本周因為菁周日補習班要魔訓, 姊姊要去畫室補課, 只有我回鄉下. 舅媽說過幾年等她們都出去上大學, 到時就真的只有我一個人回鄉下矣. 現在要開始為離巢期作心理準備.

週六早上載姊姊去樹德考英檢中級複試, 記得國二考初級也是在這裡. 中午兩個人附近陶林吃刷刷鍋, 算是難得的小確幸啦! 單點的就吃得好飽, 可見吃到飽餐廳是多麼不健康.

週六傍晚回到家, 爸說小舅跟婷婷他們才剛回去, 他們已經幫我採收第一期玉米, 留下五支給我, 哇咧, 晚一步竟慘遭劫掠! 哈, 開玩笑的啦! 早一點回來結果也是差不多, 我種這些也是想要延續母親的慷慨, 有東西可以餽贈親友. 以前親友來訪, 媽總是有辦法從菜園弄出些東西讓他們帶回去. 晚上吃過飯趕緊將鮮採玉米下鍋, 必須在採摘後兩小時內煮, 這樣才能保住玉米的甜味, 我連煮玉米都用過濾水, 帶鬚帶殼煮過後當茶喝非常甜美.

週日去市集順便去種子行再買 10 棵甜玉米來種, 老闆沒聽清楚, 以為要買三十棵, 等到搞清楚才說 : 蛤? 我趕快解釋, 我是每一兩周種十棵, 這樣到過年時每周都有玉米收成, 不是很好嗎? 一下子收成太多很麻煩, 因為要分送親友時間太趕來不及, 玉米就是要鮮採下鍋才好吃.

晚飯後想起曾國藩說的 "飯後百步走, 活到九十九", 就在曬穀場走了二十分鐘, 算了一下, 繞一圈大概 70 步. 不過, 飯後散步不可快步, 必須悠閒慢走, 否則傷胃也.



紅頭髮的安妮

今天打開 Google 首頁, 發現每日圖檔是紀念加拿大作家-清秀佳人 (Anne of The Green Gables) 的作者露西·莫德·蒙哥馬利 (Lucy Maud Montgomery) 141 歲誕辰 :



我是在 1990~1991 年左右在電視上看到 "清秀佳人 (Anne of The Green Gables)" 這部迷你影集, 當時我在電腦公司做 IC 設計, 每天都要加班到 10 點以後 (最晚是到早上五點). 但為了看這部片 (精確一點, 是為了看梅根法蘿), 那幾周都提早在九點前回去隔壁棟的宿舍, 這是繁忙的研發工作生涯中的一段小確幸.

我不但迷梅根法蘿, 還買了整套清秀佳人原文小說 (好像有三, 四本). 有一段時間行囊中就是這本書, 一有空就看. 但是看到第二本就後繼乏力了, 因為我看的電視劇集內容取材於第一本 (Anne of The Green Gables),  後面的沒看所以就興趣缺缺了. 不過一次偶然機會裡讓我買到了原版 DVD, 偶而拿來看看, 回味一下當年在公司宿舍大客廳大家一起看的記憶.

電視劇是在露西的家鄉愛德華王子島拍攝的, 播出後曾掀起一陣觀光熱潮. 愛德華王子島位於加拿大東岸, 原為法國殖民地, 在英法七年戰爭後割讓給英國並改名, 以紀念英王喬治三世的第四子愛德華王子 (即維多利亞女王的爸爸). 愛德華王子島也是加拿大建國時各英屬殖民地代表第一次會議的地點, 但後來卻因為對條款不滿而未加入加拿大, 直到因建鐵路債台高築才加盟, 成為加拿大第七個省, 參考 :

愛德華王子島


露西在 1942 年死於血栓, 但據其孫女後來透露, 露西其實是死於自殺, 因為在長期照顧罹患憂鬱症的丈夫, 以及生活中的種種不順 (長子品行不好, 次子可能要上前線, 為了清秀佳人這本書而跟出版社打官司), 自己也得了憂鬱症而服食安眠藥過量死亡.

理想與現實之間總是有一個很大的鴻溝, 當文學世界中的美好映射到現實世界的不完美時, 總是讓人悵然若失. 但有幾個人會覺得生命是完美的呢? 文學的價值, 或許就是要填補這種不完美吧!


2015年11月25日 星期三

兩本 C 語言好書與 Dev C++ 安裝

最近因為測試 ARDUINO+ESP8266 伺服器程式遇到許多問題, 突然覺得得好好搞懂 C 語言的陣列, 字串, 以及指標這三個常用的技巧. 上週六第二次去市圖總館借書, 就找到一本 C 語言的好書, 現在手邊共有三本 C 語言的書, 但我覺得以下這兩本寫得最好 :

# 碁峰, 李啟龍, 第一次學就上手-從 C 程式範例到專題製作 (第一版)
這本書今年出第二版了, 我看目錄與第一版完全一樣, 應該是賣得不錯吧! 碁峰出的書我覺得品質是數一數二的. 作者曾當選大學優良教師, 寫作風格言簡意賅不囉嗦, 例如第九章介紹字串與字元陣列, 用一張圖就清楚說明兩者的差別是, 字串就是以 \0 (NULL) 結尾的字元陣列, 寫作風格乾脆俐落.

此書附錄 B 介紹許多 C 語言的線上評測系統 (Online Judge), 可以作為參加程式設計競賽磨練程式技巧的工具 :

# ZeroJudge
# USACO
# UVA
# TIOJ
# PKUOJ
# VIJOS
# SPOJ
# URAL
# SGU

第二本好書是日本人氣作家高橋麻奈寫的 :

# 博碩, 高橋麻奈, 最新 C 語言程式設計實例入門 (第四版)


這本最棒的地方是作者會深入探討一些初學者容易搞混的觀念, 例如指標與陣列有何不同?  也詳細解說傳遞函式參數的各種方法, 特別是利用指標當引數, 解決傳值呼叫只能傳回一個值的問題. 此外書中以漫畫圖解程式的運作, 特別是資料在記憶體中的分布情形, 以利初學者能快速理解.

學習標準 ANSI C 語言需要一個編譯器, 我覺得以 Dev C++ 最適合初學者使用 (不知道為什麼, 我對微軟的 Visual XXX 就是有偏見), 目前為 5.11 版 :

http://sourceforge.net/projects/orwelldevcpp/ (約 48MB)

安裝之前務必將之前的舊版 Dev C++ 移除, 再點 Dev-Cpp 5.11 TDM-GCC 4.9.2 Setup.exe 安裝. 安裝語言選擇英文, 等第一次執行時再改為中文 :


按 "I Agree" 才能繼續執行 :


用預設的 Full (完整安裝) 即可 :


使用預設的安裝路徑 (也可以去掉中間的部分, 改安裝在 C:\Dev-Cpp) :



安裝完畢第一次啟動 Dev C++ 有選單可選擇使用之語言 :


使用預設值按下一步即可 :




點選 "檔案/開新檔案" 可以新增原始碼或專案, 如果只是寫個小程式, 選原始碼即可. 編寫的原始碼 (xxx.cpp) 與可執行檔 (xxx.exe) 預設是放在媒體櫃的文件下面, 這可以在 "環境選項/檔案目錄" 中更改 :


編輯器設定部分, 點選 "工具/編輯器選項", 勾選 "文字邊界範圍提示" 裡的 "啟用", 預設是 80 字元, 這樣會在編輯器第 80 行處顯示一條垂直線, 提醒我們一列敘述不要寫太長, 以免列印時反折到下一列. 另外我也把 "Tab 長度" 由預設的 4 改為 2, 因為我習慣每個階層縮兩格, 4 格太佔空間了.


這樣就可以開始編寫 C 程式了. C 程式的基本架構如下 :

#include <stdio.h>   //載入標準輸出入函式庫
#include <stdlib.h>  //載入標準系統函式庫

int main(void) {
  printf("Hello World!");   //輸出訊息至標準輸出 (螢幕)
  system("pause");  //停住執行視窗
  return 0;   //結束程式
  }

先按 Ctrl+F9 編譯程式, 若無編譯錯誤或警告再按 Ctrl+F10 即可執行, 或者直接按 F9 (編譯後執行) 亦可, 如果對程式很有信心的話.

前面 include 是給編譯器讀的前置處理指令, 告訴編譯器在進行編譯時, 要將哪些函式庫含括進來. 注意, C 程式的每一個敘述都要用分號結尾, 唯獨前置指令 include 與 define 不能用分號, 否則會編譯失敗.

最常用的函式庫是 stdio.h, 此函式庫提供如 printf(), scanf() 等常用的標準 IO 指令. 另外一個常用函式庫 stdlib.h 主要是提供系統呼叫 system() 函式用, 例如要讓標準輸出入視窗 (即命令提示字元視窗) 不要一閃而逝, 而是停住出現 "請按任意鍵繼續 ... " 以便觀察輸出結果的話, 就要呼叫 system("pause") 來達成. 不過 Dev C++ 編譯器即使沒有載入此函式庫也會自動停住等待按任意鍵, 呼叫 system("pause") 反而會停住兩次 :


故用 Dev-C++ 開發時若不需要其他系統呼叫, 其實不需要含括 stdlib.h. 系統呼叫 system() 可以讓程式執行作業系統指令, 例如 DOS 指令 dir, 如下範例會將程式所在目錄之檔案列表存入 dir.txt 檔案中 :

#include <stdio.h>   //載入標準輸出入函式庫
#include <stdlib.h>  //載入標準系統函式庫
int main() {
system("dir > dir.txt");
return 0;
}

某些編譯器即使沒有用 include 將 stdio.h 與 stdlib.h 含括進來, 還是可以呼叫 printf() 與 system() 等函式, 那是因為編譯器會自動載入此二函式庫之故. 為了移植的相容性方便, 還是照規定使用 include 為宜. 特別是使用指標時常需要呼叫動態配置記憶體的函式 malloc() 與 free(), 這兩個函式都放在 stdlib.h 裡面, 一定要含括進來才能使用.

含括進來的函式庫因為都擺在最前面, 故又稱標頭檔 (header files). 在 Dev-C++ 編譯器裡, 所有內建函式庫放在 C:\Dev-Cpp\MinGW64\x86_64-w64-mingw32\include 下 :


進行編譯時, 指定的函式庫檔案就會被載入取代 include 指令. 最後是關於主程序 main() 的傳回值 int 與參數 void, 這兩個都沒有加的話, 在 Dev-C++ 都不會編譯錯誤, 不過為了可移植性, 還是加比較好.

為了編輯方便, 也可以將自己常用的程式片段做成樣板, 要用時可以立刻插入程式中, 點選 "工具/編輯器選項/插入程式碼", 按 "新增", 在項目名稱欄中為此樣板取名, 然後將程式片段貼到下方輸入框, 按確定即可 :


要插入樣板, 點選 "編輯/插入文字" 或第二排工具列最左邊的插入鈕, 即可挑選要插入之程式片段 :


雖然我學 C 的主要目的是為了掌握 Arduino 的嵌入式應用, 但因為去年用 Java 幫公司寫的大量數據擷取分析程式執行效能普普, 我打算將 Java 的數據處理函式庫改寫成 C 語言版本, PK 一下到底 C 語言在效能上有多優越. 關於 C 歷四十餘年而不衰, 且越來越紅火, 參考 :

# C 語言秘技 (1) – 使用 sscanf 模仿正規表達式的剖析功能 (作者:陳鍾誠)

此文提到用 sscanf 來模仿正規表示法, 正好用得上. 誠如此文作者所言, 程式語言如過江之鯽, 今日之星轉眼成昨日黃花, 而 C 因為擁有指標功能, 使其兼具高階與低階語言雙重身分, 寫驅動程式少不了它, 故能歷久不衰, 學習 C 的投資報酬率要以一輩子來衡量, 而不是十年二十年, 更何況 C 是掌握 Linux 內核的必殺技, 非學不可.

其他參考資料 :

# 在 C 程式中,使用 Regex (Regular Expression) library
# Using Regular expression in C/C++ 
# C 語言,使用 Regular Expressions
# 正規表示法:規則篇 (這個好)


2015年11月24日 星期二

第 47 周記事

本周六 (11/21) 公司福利會辦體育活動, 地點在駁二特區一帶. 之前報名時有想要帶菁菁去, 但後來想到段考又快到了, 好像聽說補習班又要魔訓, 所以就沒報名. 前幾天菁菁才說魔訓是周日啦, 我說好吧, 看天氣再說. 結果週六早上下起毛毛雨, 就打消騎腳踏車去的念頭, 菁菁就讓她繼續睡. 出發時雨稍停, 但騎到河東路又開始下小雨了. 到鹽埕埔站放好機車, 沿著活動路線走進駁二特區 :


沿路有兩站要簽名領抽獎券 (這是最重要的, 哈), 然後右轉越過鐵道故事館就是捷運哈瑪星站, 將摸彩券投入箱中就大功告成了. 但這時才剛過九點而已, 要十點半才抽獎, 我好像太早來了哩. 既然來到哈瑪星, 乾脆去我的母校中山大學晃一圈吧! 畢業後就很少有機會來這邊了. 走不到十分鐘就到西子灣隧道口, 以前讀書時都是騎機車走大門, 幾乎沒走過隧道呢!


 

看這隧道內壁, 我想開鑿年代應該很久遠了, 可能是日據時代吧. 隧道是一種很科幻又很神祕的建物, 讓人聯想到時光機器, 電影裡面回到過去或穿越未來總會看到它. 隧道盡頭就是中山大學校園了, 沒想到一晃, 畢業已十多年了. 其實我跟這個第二母校感情不是很深, 因為我只是要進修外文才來念書的, 學歷對我來說已經不是很重要. 不需要社團, 不需要宿舍, 不需要整天在學校晃, 當然也就沒有深厚的感情了. 

在校園逛一圈就循原路回到哈瑪星站, 這時已經有許多同事聚集, 等待抽獎了. 除了抽獎, 還有一碗阿婆冰, 以及一個帕莎蒂娜的酒釀桂圓麵包. 重頭戲是抽獎, 我手氣不錯抽到 1800 元的二獎, 回到家菁菁竟然說, 如果她有去的話一定抽到一獎.

活動結束後我沿著五福路轉成功路, 想說既然來到這區域, 順便去市圖總館好好搜刮一下, 上回匆匆逛了一下不過癮. 這次一共借了六本 TCP/IP 與網路相關的書.

回程經過同盟路發現一個新建築 : 黃金波蘿城堡, 門口有很多排隊參觀的人, 回來一查原來是鳳梨酥廠商維格餅家的觀光工廠, 適合親子共遊參觀, 不過小狐狸們已經不是那個年紀了, 菁菁勉強還會想去吧!

黃金波蘿城堡


因為菁菁要魔訓, 本周只有姐姐跟我回鄉下. 週日(11/22) 爸以前的同事娶媳婦, 上週是菁菁, 這次換姐姐吃喜酒啦! 而且這次有冰, 通常只有夏天的喜宴才會出水果+冰, 可能今年又是暖冬, 這幾天非常熱的關係.


2015年11月18日 星期三

好書 : 兩本 Wirshark 的中文書

最近因為研究 ESP8266 網路連線的關係, 發現自己對網路了解不夠, 加上工作上要用到 TCP/IP 的東西, 所以去市圖借了兩本協定分析軟體 Wireshark 的書, 打算好好地補強這方面的知識. 以往我都專注在應用層面的東西, 對於這些底層的東西興趣缺缺, 例如有一陣子公司鼓勵考證照, 我寧可去考 Java 認證, 也不要去考 CCNA. 總之, 我當時覺得, 路由器設定之類的東西好死板, 讀來味如嚼蠟. 但現在見解不同了, 從終端的手機, 到雲端的伺服器, 通通要用到 TCP/IP, 如果只有皮毛的認識, 恐怕要玩應用也玩不起來.

這兩本都是翻譯書, 第一本譯自 No starch press 的 "Practical Packet Analysis" :

# 碁峰-實戰封包分析 : 使用 Wireshark

Source : 博客來

這本書很受歡迎, 已經是第二版了, 還出現在維基百科的延伸導讀項目下, 參閱 :

Wiki : Wireshark.

此書的特色如其書名, 強調實戰, 後半部 (8~11 章) 提供各種日常網路管理會遇到的實際案例, 前半部 (1~7 章) 則是介紹網路分析的基礎知識. 書中的範例檔案可在出版社網站下載 :

# http://www.nostarch.com/packet2.htm

第二本書譯自日本人久米原榮與上田浩的著作, 其實這本我兩年前想寫 Java 網路程式時早以前就借過, 但一直沒時間閱讀. 這本書是以主題條列方式編輯, 所以不一定要從頭依序讀到尾, 比較像是手邊可隨時查閱的工具書.

# 博碩-Wireshark 網路協定分析與管理
Source : 博客來

Wireshark 是採用 GNU 授權的免費開源軟體, 其前身是 Ethereal, 由 Gerald Combs 於 1997 年開始研發, 後來吸引許多高手加入開發團隊. 不過當 Gerald Combs 於 2006 年離開 NIS 公司跳槽到另一家公司時, 由於 Ethereal 是 NIS 公司的商標, 所以就改名為 Wireshark 了.


2015年11月16日 星期一

ESP8266 網頁伺服器 AT 指令測試

七月底對 ESP8266 進行 AT 指令測試時, 對於伺服器部分僅做了簡單的 PING 及與 Java client 程式的 TCP 連線測試, 沒有進行網頁伺服器的測試, 參考 :

ESP8266 WiFi 模組 AT command 測試

今天看到 alselectro 的這篇文章, 介紹使用 AT 指令來測試 ESP8266 網頁伺服器功能的方法 :

# WiFi Module ESP8266 – 2. TCP CLIENT /Server mode

晚上就按圖索驥, 依照這篇描述來測試看看. 關於 AT 指令語法參考 :

# ESP8266 Serial WIFI Module
https://github.com/espressif/ESP8266_AT/wiki/AT_Description

首先, 做為伺服器用時, ESP8266 必須工作在 STATION (mode=1) 或 STATION+AP (mode=3) 模式, 因為這樣它才會從 DHCP 獲得一個區網 IP (作為區網中的一台主機) :

AT+CWMODE=1


OK
AT+CIFSR

192.168.2.116

OK

這是 STATION 模式下所取得之區網 IP. 去 PING 它應該要能獲得回應 :

C:\Users\petertw89>PING 192.168.2.116

Ping 192.168.2.116 (使用 32 位元組的資料):
回覆自 192.168.2.116: 位元組=32 時間=876ms TTL=255
回覆自 192.168.2.116: 位元組=32 時間=65ms TTL=255
回覆自 192.168.2.116: 位元組=32 時間=70ms TTL=255
回覆自 192.168.2.116: 位元組=32 時間=77ms TTL=255

192.168.2.116 的 Ping 統計資料:
    封包: 已傳送 = 4,已收到 = 4, 已遺失 = 0 (0% 遺失),
大約的來回時間 (毫秒):
    最小值 = 65ms,最大值 = 876ms,平均 = 272ms  

如果是在 STATION+AP 模式下, 將獲得兩個 IP :

AT+CWMODE=3

OK

AT+CIFSR

192.168.4.1         (SoftAP 的 IP)
192.168.2.116     (Station 的 IP)

OK

其中第一個 IP=192.168.4.1 是 ESP8266 作為 SoftAP 之 IP, 僅作為網路接取點用, 無法作為主機使用. 第二個 IP 192.168.2.116 則是作為 STATION 用, 從 DHCP 所分派而得之 IP, 在區網中做為其他主機的伺服器要用到的就是這個 IP. 如果把 ESP8266 設為 AP 模式, 那麼就只剩下這個 SoftAP 的 IP 了 : 

AT+CWMODE=2

AT+CIFSR

192.168.4.1

OK

這個 SoftAP 的 IP 在區網中 PING 不到 :

C:\Users\petertw89>PING 192.168.4.1

Ping 192.168.4.1 (使用 32 位元組的資料):
要求等候逾時。
要求等候逾時。
要求等候逾時。
要求等候逾時。

192.168.4.1 的 Ping 統計資料:
    封包: 已傳送 = 4,已收到 = 0, 已遺失 = 4 (100% 遺失),


搞定工作模式後 (即須在模式 1 或 模式 3), 還有一個必要的設定要做才能啟動伺服器, 那就是開啟多重連線, 因為所謂伺服器就是要服務許多客戶端的連線要求的 (但 ESP8266 同一時間最多只能接受 6 個連線) :

AT+CIPMUX=1

OK

開啟多重連線後, 才可以啟動 TCP 伺服器, 其 AT 指令需輸入兩個參數, 第一個參數必須為 1 (0 的話是關閉伺服器), 第二個參數是要開啟的埠號, 一般網頁伺服器為 port 80, 當然也可以開啟不是專用 (well-known) 與註冊 (registered) 的埠號, 例如 8080 :


AT+CIPSERVER=1,80

OK

如果是在單一連線下開啟伺服器功能, 會收到 ERROR 回應 :

AT+CIPSERVER=1,80


ERROR

好, 啟動伺服器後, 便可用手機, 平板, 或電腦連線到與 ESP8266 所連的相同 AP (這樣才會在同一區網內), 啟動瀏覽器, 在網址列輸入伺服器的 IP 位址 (此處是 192.168.2.116), 我是從手機連線的, 這時瀏覽器會停在那邊, 不會看到任何內容 (因為要等 ESP8266 伺服器回應才知道要呈現啥內容啊!) :


這時在序列埠監視視窗會看到 ESP8266 收到來自手機的連線要求 (Link), 可看到手機瀏覽器傳來的 HTTP 標頭 :

Link

+IPD,0,498:GET / HTTP/1.1
Host: 192.168.2.116
Connection: keep-alive
x-wap-profile: http://wap1.huawei.com/uaprof/HONOR_H30-L02_Global_UAProfile.xml
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
User-Agent: Mozilla/5.0 (Linux; Android 4.4.2; H30-L02 Build/HonorH30-L02) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/30.0.0.0 Mobile Safari/537.36
Accept-Encoding: gzip,deflate
Accept-Language: zh-TW,en-US;q=0.8
X-Requested-With: com.android.browser


OK

注意在 +IPD 後面的那個數字, 就是代表連線的通道編號, 此處為通道 0, 後面的 498 表示收到 498 個字元. 這時若用 AT+CIPSTATUS 去查看連線狀態 :


AT+CIPSTATUS

STATUS:3
+CIPSTATUS:0,"TCP","192.168.2.102",38679,1

OK

可以看到目前有來自 192.168.2.102 (就是我的手機連上 WiFi 所獲得的無線區網 IP) 埠號 38679 的一個 TCP 連線, STATUS 為 3 表示 Connected. 最後的 1 表示 ESP8266 目前是作為 Server (0 表示 Client), 參考 :

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

接下來我們必須在伺服器連線逾時 (timeout) 之前送出回應給客戶端, 這個逾時計時器預設是 180 秒, 可用 AT+CIPSTO? 查詢 :

AT+CIPSTO?

+CIPSTO:180

OK

當然也可以用 AT+CIPSTO=timeout 設定, 逾時秒數範圍 0~28800.

回到送出回應, 首先要用 AT+CIPSEND 指令告訴 ESP8266 要送出的回應長度, 以便它能組建 TCP 訊息. 例如伺服器回應 "Hello!", 此字串含有 6 個字元, 但要額外加上 "\r\n"  這兩個跳行字元, 一共是 8 個字元, 所以 AT 指令 :

AT+CIPSEND=0,8

這時 ESP8266 回應大於符號, 表示可以開始傳送資料, 接著送出 Hello! :

> Hello!
SEND OK

看到 SEND OK 表示傳送成功, 但這時瀏覽器還看不到伺服器回應, 必須關閉 TCP 連線才會收到回應 :


AT+CIPCLOSE=0    (關閉 TCP 連線)


OK
Unlink
Link

+IPD,0,357:GET /favicon.ico HTTP/1.1
Host: 192.168.2.116
Connection: keep-alive
Accept: */*
User-Agent: Mozilla/5.0 (Linux; Android 4.4.2; H30-L02 Build/HonorH30-L02) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/30.0.0.0 Mobile Safari/537.36
Accept-Encoding: gzip,deflate
Accept-Language: zh-TW,en-US;q=0.8
X-Requested-With: com.android.browser


OK
Unlink

這時手機瀏覽器才會收到伺服器回應的 "Hello!" :


再用 AT+CIPSTATUS 去查詢 IP 連線狀態, 可知連線已中斷 (4=DISCONNECTED) :

AT+CIPSTATUS

STATUS:4

OK

以上便是網頁伺服器的 Server-Client 互動流程.

最後, 若要關閉伺服器, 停止網頁服務, 同樣是下 AT+CIPSERVER 指令, 但只要給一個參數 0 即可 :

AT+CIPSERVER=0

we must restart

這表示關閉伺服器後必須重啟 ESP8266 :


AT+RST


OK
bB��Sb禔S��"�芀�侒��S��
[Vendor:www.ai-thinker.com Version:0.9.2.4]

ready

AT+CIPMUX?

+CIPMUX:0

OK

可見, 重啟後多重連線也自動被關閉了.

 

2015年11月15日 星期日

第 46 周記事

前幾天聽爸說有元哥要嫁女兒, 菁菁聽說後馬上問是甚麼時候, 她最喜歡吃喜酒了, 因為通常有冰可吃. 由於姐姐要準備英檢中級, 本周不能回鄉下; 而菁菁的補習班魔訓要下周才開始, 所以她很高興能去吃喜酒. 菁菁說咱們禮拜六早上就回去, 這樣可以趕上中午的喜宴.

週六 (11/14) 吃完喜酒回來, 下午睡過午覺後去理髮. 我都是在鄉下的 "不說話理髮師" 那裏理髮, 老闆是天生聾啞人, 大約長我幾歲, 在特教學校學得剪髮技藝後回鄉開店, 娶了宜蘭大同鄉的原住民為妻, 長女去年出嫁. 我自台北回來高雄工作, 就一直在他那裏理髮, 因為我想給 underprivileged 的人一些生意, 以前刻印我也都是去公司附近一位殘疾人開的刻印店. 而且理習慣後, 就不想去別處理. 不說話的老闆似乎跟斜對面開美容美髮的阿芳不太和, 好幾次跟我比手畫腳講阿芳家怎樣怎樣的, 我哪看得懂手語, 只大約知道又在談人家的是非, 他也知道以前我會帶小狐狸們去給阿芳剪頭髮, 所以我也只能打哈哈. 對, 跟我們無關的閒話聽後哈哈哈就好.

菜園的玉米前兩批約 30 株已經一樣高, 第一批的十株已結出果實, 第二批的二十株也抽穗了. 週五同事給我六棵北海道水果玉米種子, 打算去買一包培養土來育苗, 等長出來再移回鄉下種. 北海道的品種好, 上回同事有拿些來給大家嚐, 可以生吃, 就像水果一樣非常甜.

週六晚上跟菁菁重看了日本電影 "去看小洋蔥媽媽", 講的是失智老人照護問題, 是漫畫家岡田雄一的親身自述. 不知道為什麼, 這幾年失智問題變成好像家常便飯, 身邊的親戚, 朋友, 鄰居, 同事都有失智的親人. 住台南的同事他父親就已完全不認得他. 山邊的鄰居伯母, 以前跟媽很熟, 幾年前也失智了, 媽去年健在時還提起, 過年時去找她, 問她還認不認得, 這位伯母回答是 : 好像在哪裡見過.

 

2015年11月13日 星期五

Random Nerd Tutorials

今天在網路上找到一個網站 :

Random Nerd Tutorials

這是一位葡萄牙大學生 Rui Santos 所設立的電子實作教學網站, 這位創辦人年僅 21 歲, 目前就讀於葡萄牙波爾多工程大學 Faculdade de Engenharia da Universidade do Porto (FEUP) 電機系, 雖然年紀輕輕, 卻已在 Wiley 出版了一本 Beagleboard 的書了, 也剛推出兩本 ESP8266 家庭自動化應用的電子書 :

Download Home Automation Using ESP8266 eBook (US$19.95)
Password Protected Web Server Accessible from Anywhere using ESP8266 and Arduino IDE (US$11)

目前第一本電子書做特價, 只要 US$14.95, 約台幣 493 元. 第二本之前也有特價, 只要 US$4, 但已經過了, 以後或許還會再特價也說不定.

另外, 他也將做過的 18 個電子製作專案寫成一本教學電子書, 供同好免費下載, 只要輸入 Email, 收信確認後即可下載 :

# Download Random Nerd Tutorials eBook! (Free)

我瀏覽了他的 BLOG 與網站, 覺得如獲至寶. 他自 2012 年開始努力不懈地學習電子學, 三年就有這般成果, 這位年輕人的精神值得學習.

他的臉書, G+, 與推特如下 :

https://twitter.com/RuiSantosdotme

https://plus.google.com/u/0/+RuiSantosdotme/posts

https://www.facebook.com/RandomNerdTutorials

這是我在其 G+ 上留言後的回應 :



以後有機會要跟這位小朋友討教討教.

關於家庭自動化 (Home Automation), Adafruit 也有一本運用 ESP8266 的 14 頁免費電子書 :

home-automation-in-the-cloud-with-the-esp8266-and-adafruit-io


ESP8266 函式庫記憶體耗損比較

在測試 WeeESP8266 函式庫時發現, 程式好像也沒多長, 但編譯後竟然吃掉非常多的 SRAM (動態記憶體) 與 Flash (程式記憶體), 甚至會讓部分函式破功 (例如列出附近可用 AP), 我想會不會是因為函式庫裡許多沒用到的函式還是會占用動態記憶體呢? 於是就回頭把自己土法煉鋼的函式庫改版, 完成後我迫不及待想知道它在記憶體耗損方面, 是否真的有比 WeeESP8266 函式庫好很多呢? 參考 :

# ESP8266 函式庫 v2
# ITEAD WeeESP8266 函式庫測試

其實並非每一個函式都會在應用中使用到, 對 ESP8266 的物聯網應用而言, 最常用的當然就是 createTCP() 與 send() 這兩個函式, 其他的函式幾乎用不到, 因為 ESP8266 會記住工作模式, 連線多寡, 以及上一次連線的 AP 之 SSID 與密碼, 只要一送電就會馬上連線. 除非下指令去改這些參數, 否則都會被記在不會揮發的 Flash 記憶體裡. 所以如果能卸除這些不會用到的函式, 就不會虛耗記憶體了. 但是對於封裝好的 WeeESP8266 函式庫而言, 這就難辦了, 只有自己寫的函式庫才能這麼做.

以下我就以向 Thingspeak 物聯網服務伺服器傳送 "隨機溫度" 為例, 來測試看看滿載的 WeeESP8266 函式庫與減載後的 v2 在記憶體耗損上有何差異. 對於一般的 IoT 應用, 只要用到自己寫的函式庫裡的五個函式而已 : get_response(), reset(), start_tcp(), send_data(), 以及 release().

在測試前先來看一個空白的 Arduino 程式的記憶體損耗 :

void setup() {}
void loop() {}


編譯後幾乎沒耗損多少記憶體, 訊息如下 :

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

如果載入 SoftwareSerial 與 WeeEsp8266 函式庫 :

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

SoftwareSerial sSerial(10,11); //(RX,TX) 與 ESP8266 介接的軟體串列埠
ESP8266 wifi(sSerial);  //建立名為 wifi 的 ESP8266 物件

void setup() {}
void loop() {}


編譯後程式增加逾 2KB, 變數增加逾 100B, 占了約 6~8%, 訊息如下 :

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


測試 1 使用 WeeESP8266 函式庫 :

測試 1 : WeeESP8266

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

SoftwareSerial sSerial(10,11); //(RX,TX) 與 ESP8266 介接的軟體串列埠
ESP8266 wifi(sSerial);  //建立名為 wifi 的 ESP8266 物件
byte c; //傳遞給 Thingspeak 的變數
String GET;  //GET 字串

void setup() {
  sSerial.begin(9600);  //設定軟體序列埠速率 (to ESP8266)
  Serial.begin(9600);  //設定軟體序列埠速率 (to PC)
  Serial.println("*** SoftSerial connection to ESP8266 ***");
  }

void loop() {
  test();
  delay(16000);  //Thingspeak limit
  }

void test() {
  if (wifi.restart()) {Serial.println("Reset ESP8266 ... OK");}  //重設 ESP8266
  else {Serial.println("Reset ESP8266 ... NG");}
  delay(5000);  //等待 ESP8266 與 AP 連線
  c=random(10,50);
  if (wifi.createTCP("184.106.153.149",80)) {  //連線 Thingspeak
    Serial.println("Connect Thingspeak ... OK");
    GET="GET /update?api_key=NO5N8C7T2KINFCQE&field1=" + String(c) + "\r\n";
    const char *data=GET.c_str();
    if (wifi.send((const uint8_t*)data, strlen(data))) {Serial.println("Send GET/ ... OK");}
    else {Serial.println("Send GET/ ... NG");}  
    }
  else {Serial.println("Connect Thingspeak ... NG");}
  wifi.releaseTCP();
  }

此程式編譯後的訊息為 :

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

可見程式耗掉 31% 的 Flash 記憶體, 而變數耗掉 44% 的 SRAM.

這裡我用了一個隨機變數 c 代表攝氏溫度, 利用 random() 函式產生 10~50 間不斷變化的溫度值, 將其放在 GET 方法字串中傳送至 Thingspeak 伺服器. 要注意的是, 傳給 WeeESP8266 的 send() 函式之參數 data 必須是一個指向字元陣列的指標, 這裡用字串的 c_str() 函式將字串轉成字元陣列. 其次是, 此陣列需用 const 宣告為常數. 參考 :

# Convert string to const char* issue [duplicate]

還有一點是非常重要, 傳送字串後面要自行加上跳行 "\r\n", 否則雖然 send() 回傳 true 表示成功, 事實上資料並未成功地寫入伺服器.

下面就是這個 "隨機溫度" 在 Thingspeak 上的圖形 :


接下來測試我的 WiFi 函式庫 v2, 只要留下 get_response(), start_tcp(), send_data(), 以及 release() 這四個函式即可, 其餘用不到都刪掉 :

測試 2 : TonyWiFi v2

#include <SoftwareSerial.h>

SoftwareSerial sSerial(10,11); //(RX,TX) 與 ESP8266 介接的軟體串列埠
byte c; //傳遞給 Thingspeak 的變數
String GET;  //GET 字串

void setup() {
  sSerial.begin(9600);  //設定軟體序列埠速率 (to ESP8266)
  Serial.begin(9600);  //設定軟體序列埠速率 (to PC)
  Serial.println("*** SoftSerial connection to ESP8266 ***");
  }

void loop() {
  test();
  delay(16000);  //Thingspeak limit
  }

void test() {
  if (reset()) {Serial.println("Reset ESP8266 ... OK");}  //重設 ESP8266
  else {Serial.println("Reset ESP8266 ... NG");}
  delay(5000);  //等待 ESP8266 與 AP 連線
  if (start_tcp("www.google.com",80)) {  //連線 Google 首頁
    Serial.println("Connect Google ... OK");
    if (send_data("GET /")) {Serial.println("Send GET/ ... OK");}
    else {Serial.println("Send GET/ ... NG");}  
    }
  else {Serial.println("Connect Google ... NG");}
  c=random(10,50);
  if (start_tcp("184.106.153.149",80)) {  //連線 Thingspeak
    Serial.println("Connect Thingspeak ... OK");
    GET="GET /update?api_key=NO5N8C7T2KINFCQE&field1=" + String(c);
    if (send_data(GET)) {Serial.println("Send GET/ ... OK");}
    else {Serial.println("Send GET/ ... NG");}  
    }
  else {Serial.println("Connect Thingspeak ... NG");}
  }

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

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

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

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

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

編譯訊息如下 :

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

可見, 同樣的功能, 減載的 WiFi v2 在程式記憶體耗損上只是少一些而已, 但在動態記憶體上卻少了 10%, 確實有比較省.

   

ESP8266 函式庫 v2

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

# 撰寫 Arduino 的 ESP8266 WiFi 函式

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

Using an ESP8266 as a time source (Part 1)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


2015-11-13 補充 :

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

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

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

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

參考 : ESP8266 WiFi 模組 AT command 測試

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

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

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



林埈永的 MV-愛走了

這首歌是以前韓劇 "擁抱太陽的月亮" 在有線電視播放時的片尾曲, 旋律與劇情非常搭, 但當時沒有記下演唱者與歌名, 所以後來要找也很難. 最近聽到姐姐從手機播放這首歌, 我趕緊問歌名, 上網一查原來是林埈永的 "愛走了".

# 綠茶 林埈永 - 愛走了 MV無字完整版HD


林埈永是台韓混血兒, 所以 MV 前後都有一段韓語獨白.

歌詞如下 : 

作曲:謝松輝  作詞:謝松輝

孤單的房間 還殘留你的氣味
心碎哪一天 徘徊在寂寞黑夜
深情的思念 被關在心的深淵
我聽見幸福說抱歉

當平行線 錯過時間
我們之間已經沒有交叉點
才一瞬間 回頭路已經看不見
拼湊不了的畫面 (我喜歡這種感覺 我們可以在一起嗎)

不在乎 付出我的全部
才明白 是我自己殘酷
愛走了 我聽見幸福說抱歉
該結束我比誰都清楚 

當平行線 錯過時間
我們之間已經沒有交叉點
才一瞬間 回頭路已經看不見
拼湊不了的畫面

不在乎 付出我的全部
才明白 是我自己殘酷
愛走了 我聽見幸福說抱歉
該結束我比誰都清楚 我只求你會過得精彩

我不會再哭 裝作已經覺悟
我依然 相信還沒結束
愛走了 我聽見幸福說抱歉
該結束我比誰都清楚


2015年11月12日 星期四

關於螢幕錄影軟體

昨天為了錄製 Arduino IDE 1.6.6 版新增的 Serial Plotter 功能, 找出多年前使用的 CamStudio 2.0 來用, 無奈現在已是 Win7/8 電腦, 這款 XP 時代的軟體已無法正常運作. 於是上網找新款的, 試過好幾種免費版, 特別是免費的 oCam 似乎很不錯, 官網最新版為 v1.5.7 :

http://ohsoft.net/en/download.php

但我下載 zip 檔傳到 VirusTotal 去掃描卻發現疑似有 Trojan 木馬 (DrWEB 軟體掃出) :


雖然有可能是誤判, 但我還是不放心. 後來在阿榮福利味找到了舊版 oCam (v1.3.6), 沒想到竟然是零掃出耶!

參考 :

# oCam 136.0 中文版 (11.5 免安裝中文版) - 簡單的螢幕錄影軟體
# 免費螢幕錄影軟體oCam
# oCam v130.0 免費螢幕錄影、抓圖工具(繁體中文版)

不過我還是沒有用 oCam, 因為我又找到一個線上免費錄影軟體 : Apowersoft 的 Screen Recorder (螢幕錄影王), 發現其操作方便直觀, 具備錄影, 錄音, 抓圖, 上傳, 分享等功能, 我想 oCam 應該不會比它好, 參考官網 :

http://www.apowersoft.com/free-online-screen-recorder


點網頁上的 "Start Recording" 按鈕會出現跳出視窗, 按上面的 "Download Launcher" 按鈕會下載線上啟動軟體 Launcher (應該是 Java Web Start) :


先安裝這個 924KB 的啟動軟體 apowersoft-online-launcher.exe (VirusTotal 掃過 OK) :


然後回到螢幕錄影王的網頁, 按 "Start Recording" 鈕, 這時頁面上會出現一個錄影框與下方的控制鈕, 它會一直浮在最上層, 這時切換到要錄影的畫面, 然後拉四個角調整錄影框的大小, 再按左下角的紅色錄影鈕就可以了 :



在開始錄影之前會跳出視窗, 提示操控錄影的快捷鍵 :

Ctrl+F7 : 暫停錄影
Ctrl+F10 : 停止錄影
Ctrl+Alt+E : 顯示錄影工具鈕

按 OK 就會開始錄影了. 暫停錄影也可以按左下角的暫停鈕, 如下圖所示 :


錄影暫停之後, 右下角會出現兩個按鈕, 按打叉會跳出詢問視窗, 按 Delete 刪除此次的錄影檔; 如果要重錄就按 "Start over" :



如果按打勾, 會打開播放器播放所錄的影像 :



按右下角的向上箭頭, 點 Save as Video File 即可選擇存檔的類型與目錄, 再按 Save 即顯示存檔完成畫面 :


也可以付費購買電腦安裝版,  按官網的 "Download Desktop Version", 目前最新是 v2.0.8 版, 專業版個人授權一套 NT$990, 還不算貴, 安裝後可免費體驗 3 天 (體驗版會在錄影檔上打上浮水印), 滿意再向官網購買授權碼即可.

點左上角 "錄製" 鈕右邊的三角形, 選 "自訂區域", 螢幕上會出現一個十字線以選擇要錄影的區域, 選好後按確定, 經 3 秒倒數計時即開始錄影了.



也可以只錄音, 如下圖所示選取要存檔的音訊格式, 此軟體預設會從系統聲音 (即混音器) 錄製聲音檔 :


如果只想錄製麥克風輸入之聲音, 可以在 "音訊輸入" 選單選取 :


如果選 "麥克風+系統聲音", 就會從混音器輸出錄製, 包括電腦發出的叮咚聲, 網路播放的串流音樂, 以及麥克風講話的聲音都會一起被錄進去.

此軟體也可以像 pcipick 一樣選取區域後擷取螢幕 :



擷取後按右下角的磁碟圖案即可存檔. 螢幕截圖之圖檔不會列入程式中央的檔案列表中.

另外, 它也可以將影片一鍵上傳 Youtube 或 FTP :


但必須先設好上傳設定 :


除此之外也可以分享到社群網站, 例如臉書, 推特, Google+ 等 :



螢幕錄影王最厲害的是連 Youtube, 土豆網, 愛奇藝, 優酷, Skype 等網路串流視頻都能錄, 支援影音同步錄製, 還可以加入文字描述. 我使用線上方式開啟土豆網播放影來錄影, 確實可以錄製網頁上的串流.

螢幕錄影預設是存成 wmv 檔, 但可以在 "工具/選項" 中更改為存成 avi, mp4 等各種檔案 :


螢幕錄影王也有台灣繁體中文網站, 參考 :

Apowersoft免費線上螢幕錄影 
 

按 "開始錄製" 可以線上錄影, 不過執行的軟體版本與英文官網的不同, 操作介面與桌上安裝版一樣, 是 v2.0.3 版的. 注意, 因為 Java Web Start 會從官網載入 Java 程式, 當軟體載入完成, 出現如下畫面時, 要按執行 :


執行後會要求准許此軟體的使用者帳戶控制, 需准許才會出現下列程式畫面 :


這跟桌上版是一樣的介面, 不再重複. 有了這個工具就不必再用手機拍攝螢幕操作了, 影像也比較清晰. 最重要的是 : 線上版完全免費!