2016年5月31日 星期二

AllAboutEE 的 ESP8266 伺服器測試 (二)

久攻不下的 ESP8266 伺服器測試終於在昨晚搞定了! 弄到 12 點多確定成功了就大笑幾聲, 滿意的進入夢鄉. 晚上回到家又重做一遍, 確定沒問題就可以來寫測試紀錄啦! 硬體接線此處不再重貼, 參考本篇測試紀錄的前篇暖身測試 :

# AllAboutEE 的 ESP8266 伺服器測試 (一)

此測試的目的, 是要演示是否可從手機的瀏覽器 (或許以後可以設計一個 App 更好), 設定 Arduino+ESP8266 模組的 Wifi 上網環境. 會有這個想法, 來自於過去一段時間玩 ESP8266 這個 CP 值超高的 Wifi 模組時, 發現要在 Arduino 程式中設定 ESP8266 連上家中的無線基地台, 讓 Arduino 連上物聯網服務並不難, 就是在 setup() 函數中用 AT+CWJAP 這個 AT 指令來設定即可.

問題是, 以我的情況而言, 我每周要來往都市的工寮與鄉下的老家, 兩個地方都有 Wifi, 但這兩台無線基地台的 SSID 與密碼都不同, 基於不可救藥的念舊, 我都沒想要將他們改為一致的衝動. 結果是, 當我把 Arduino+ESP8266 模組帶回去鄉下做實驗時, 就要修改程式, 換成連線鄉下AP 的 SSID. 同樣地, 回到高雄時又要改, 真的很煩.

對會寫程式的人而言就只是煩而已, 但對使用者而言就頭大了, 我們不可能要求使用者自己下載 Arduino IDE 自己改程式吧! 但是, 難道要裝個 1602 顯示模組跟小鍵盤來讓使用者輸入嗎? SSID 是英文怎麼辦? 後來接觸到 Webduino 這個以 Web 技術來開發 Arduino 創意的產品, 受其採用手機為輸出入介面的做法感到振奮, 沒錯, 手機就是一應具全的超完美輸出入介面呀! 只要能與手機連上線, 就可以利用手機的虛擬鍵盤與螢幕來跟 Arduino+ESP8266 溝通啦!

我買了 "實戰 Webduino" 這本書來看 (也買了 Webduino 的馬克 1 號實驗板, 但是到現在都還沒空把玩), 我從書裡面揣摩它應該是將 ESP8266 設在模式 3 (AP+STA), 並啟動 80 埠網頁伺服器功能, 這樣可讓我們在 ESP8266 還沒連上家中 Wifi 之前, 可以使用手機透過其 SoftAP 的固定網址 192.168.4.1 連線 ESP8266 伺服器. 但伺服器的回應事實上是利用 Arduino 來控制的, ESP8266 只是提供 Web 伺服器功能而已. 參考 :

關於 Webduino 開發板

兩周前二哥考完會考後, 我就開始思考如何實作這種機制, 因為一想到之前寫的 ESP8266 函式庫就頭大, 因為光載入這函式庫我看起碼佔掉近 3 成記憶體. 正在煩惱怎麼做比較好時找到 AllAboutEE 所寫的 ESP8266 函式, 我覺得非常精簡好用, 於是決定重起爐灶以此為基礎來測試囉! 參考 :

How To Use the ESP8266 and Arduino as a Webserver
ESP8266 函式庫 v2

OK, 前情提要交代完, 接下來要來處理 HTTP 標頭中的路徑 (path 或 router), 來控制 wifi 設定的網頁切換. 我參考了 Oreilly 出版的 "Arduino Cookbook 錦囊妙計 第二版" 第 15-8 節 "處理特定網頁需求" (p540) 之作法, 主要利用 Serial 物件的 find() 與 readBytesUntil(), 以及 C 語言的字串處理函數 strcmp() 來擷取與剖析 HTTP 標頭中的路徑字串.

三民

此外在字串處理部分, 我也參考了下列文章 :

https://www.arduino.cc/en/Reference/Serial
Arduino: Sending and Receiving Multi-Digit Integers
SO, HOW DOES SERIAL.READBYTESUNTIL() WORK?

我的構想是以下列的網址來更新 ESP8266 的 Wifi 連線設定 :

http://192.168.4.1/update/?ssid=myssid&pwd=mypwd

而任何其他路徑就顯示 Wifi 設定畫面, 例如 :

http://192.168.4.1

前篇所述, 當使用者連線 192.168.4.1 時, ES8266 會回應如下 HTTP 標頭 (GET 以後的, 前面的 +IPD 是 ESP8266 的回應標頭, 跟著的 0 是連線通道, 362 是回應字元數) :

+IPD,0,362:GET / HTTP/1.1

這裡顯示前端瀏覽器是以 GET 方法提出網頁要求, 其路徑是第 1 個斜線 "/" 表示存取根目錄. 如果是送出 Wifi 設定要求, 那麼網址會變成 /update/?ssid=myssid&pwd=mypwd, 而 ESP8266 會回應如下 HTTP 標頭 :

+IPD,0,426:GET /update/?ssid=myssid&pwd=mypwd HTTP/1.1

我們必須從這個回應字串中取得其中第一個斜線後面的路徑字串, 如果是 update 就進入設定區塊, 然後繼續擷取問號後面的參數 ssid 與 pwd, 以便填入 AT+CWJAP 後面來設定要連線之 AP. 如果路徑不是 update, 那就顯示設定表單網頁. 我把前篇的程式修改為如下 :

#include <SoftwareSerial.h>
#define DEBUG true

SoftwareSerial esp8266(10,11); //(RX,TX)
const int MAX_PAGE_NAME_LEN=48;
char buffer[MAX_PAGE_NAME_LEN + 1];

void setup() {
  Serial.begin(9600);
  esp8266.begin(9600);
  sendData("AT+RST\r\n",2000,DEBUG); // reset module
  sendData("AT+CWMODE=3\r\n",1000,DEBUG); // configure as access point
  sendData("AT+CIFSR\r\n",1000,DEBUG); // get ip address
  sendData("AT+CIPMUX=1\r\n",1000,DEBUG); // configure for multiple connections
  sendData("AT+CIPSERVER=1,80\r\n",1000,DEBUG); // turn on server on port 80
  }

void loop() {
  if (esp8266.available()) { // check if the esp is sending a message  
    if (esp8266.find("+IPD,")) {
      delay(1000);
      //esp8266 link response : +IPD,0,498:GET / HTTP/1.1
      //retrieve connection ID from response (0~4, after "+IPD,")
      int connectionId=esp8266.read()-48;  //from ASCII to number
      //subtract 48 because read() returns ASCII decimal value
      //and in ASCII, "0" (the first decimal number) starts at 48
      if (esp8266.find("GET ")) { //retrieve page router from remaining response
        memset(buffer, 0, sizeof(buffer));  //clear buffer (all set to 0)
        if (esp8266.find("/")) { //find page router start char
          if (esp8266.readBytesUntil('/', buffer, sizeof(buffer))) {
            if (strcmp(buffer, "update") == 0) { //update wifi
              //"?ssid=aaa&pwd=bbb HTTP/1.1"            
              esp8266.find("?ssid="); //skip ssid token
              memset(buffer, 0, sizeof(buffer));  //clear buffer (all set to 0)
              esp8266.readBytesUntil('&', buffer, sizeof(buffer)); //retrieve ssid
              String ssid=buffer;
              esp8266.find("pwd="); //skip pwd token
              memset(buffer, 0, sizeof(buffer));  //clear buffer (all set to 0)
              esp8266.readBytesUntil(' ', buffer, sizeof(buffer)); //retrieve pwd
              String pwd=buffer;
              //set joint AP
              sendData("AT+CWJAP=\"" + ssid + "\",\"" + pwd + "\"\r\n",6000,DEBUG);
              sendData("AT+CIFSR\r\n",1000,DEBUG);
              //show result
              String webpage="<html>Wifi setup OK!</html>";
              String cipSend = "AT+CIPSEND=";
              cipSend += connectionId;
              cipSend += ",";
              cipSend +=webpage.length();
              cipSend +="\r\n";
              sendData(cipSend,1000,DEBUG);
              sendData(webpage,2000,DEBUG);
           
              String closeCommand = "AT+CIPCLOSE=";
              closeCommand+=connectionId; // append connection id
              closeCommand+="\r\n";  
              sendData(closeCommand,3000,DEBUG);                
              }
            else { //show setup page
              String webpage="<html><form method=get action='/update/'>SSID <input name=ssid type=text><br>";
              String cipSend = "AT+CIPSEND=";
              cipSend = "AT+CIPSEND=";
              cipSend += connectionId;
              cipSend += ",";
              cipSend +=webpage.length();
              cipSend +="\r\n";
              sendData(cipSend,1000,DEBUG);
              sendData(webpage,2000,DEBUG);

              webpage="PWD <input name=pwd type=text> ";
              cipSend = "AT+CIPSEND=";
              cipSend += connectionId;
              cipSend += ",";
              cipSend +=webpage.length();
              cipSend +="\r\n";
              sendData(cipSend,1000,DEBUG);
              sendData(webpage,2000,DEBUG);

              webpage="<input type=submit value=Connect></form></html>";
              cipSend = "AT+CIPSEND=";
              cipSend += connectionId;
              cipSend += ",";
              cipSend +=webpage.length();
              cipSend +="\r\n";
              sendData(cipSend,1000,DEBUG);
              sendData(webpage,2000,DEBUG);

              String closeCommand = "AT+CIPCLOSE=";
              closeCommand+=connectionId; // append connection id
              closeCommand+="\r\n";  
              sendData(closeCommand,3000,DEBUG);          
              }
            }
          }
        }
      }
    }
  }

String sendData(String command, const int timeout, boolean debug) {
  String response="";
  esp8266.print(command); // send the read character to the esp8266
  long int time=millis();
  while ((time+timeout) > millis()) {
    while(esp8266.available()) {
      // The esp has data so display its output to the serial window
      char c=esp8266.read(); // read the next character.
      response += c;
      }
    }
  if (debug) {Serial.print(response);}
  return response;
  }

在這程式中, 我指定了一個 48 個字元的暫存區 buffer, 用來儲存擷取到的網頁路徑以及參數, 48 個 Bytes 應該綽綽有餘了. 這裡要注意的是, 我為了精簡路徑長度, 將密碼欄位的 name 由前篇中的 password 改為 pwd. 而且表單內也加入 action="/update/" 屬性, 這樣提交表單時便會向 192.168.4.1 的 port 80 要求取得 /update/?ssid=myssid&pwd=mypwd 網頁了 (GET 方法會把要傳遞的參數以 ? 黏在 action 路徑後面傳送, 每個參數以 & 串接).

當我們從 ESP8266 回應的 +IPD 後面擷取出通道號碼後, 接著就用 find() 函數從剩下的回應字串中尋找 "GET " 字串 (注意 T 後面有一個空格), 這是 HTTP 標頭的開始. 找到後再往下搜尋斜線字元 "/", 找到的話表示後面接著的便是路徑了, 依續將回應字串讀取到 buffer 陣列內, 直到第二個斜線出現為止. 然後用 strcmp() 函數比對 buffer 內所儲存的是否為 "update", 是的話就進入設定區塊, 繼續往下擷取 ssid 與 pwd 參數, 否則就進入 wifi 設定表單頁面.

程式上傳 Arduino 後, 打開序列埠監控視窗, 將手機的 Wifi 功能打開, 連線 ESP_ 開頭的基地台, 這是 ESP8266 在模式 3 下所建立的基地台 :


然後打開瀏覽器, 輸入 SoftAP 的網址 192.168.4.1, Arduino 會透過 ESP8266 的伺服器回應 Wifi 設定網頁 :

輸入我家無線基地台的 ssid 與密碼後, 按 Connect 即顯示設定成功訊息 :


從下面序列埠監控視窗擷取的輸出訊息可知, 設定 Wifi 連線成功後, AT+CIFSR 指令顯示 ESP8266 的 STA 從家中無線基地台的 DHCP 獲得 192.168.2.102 這個區網 IP, 顯示此設定確實已成功地讓 ESP8266 連上家中的區網, 當然就可連到互聯網了 :

AT+RST


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

no change
AT+CIFSR

192.168.4.1  (SoftAP 網址)
0.0.0.0          (STA 尚未連上 wifi 基地台, 預設網址 0.0.0.0)

OK
AT+CIPMUX=1


OK
AT+CIPSERVER=1,80


OK
1.1
Host: 192.168.4.1
Connection: keep-aliveAT+CIPSEND=0,77

> <html><form method=get action='/update/'>SSID <input name=ssid
SEND OK
AT+CIPSEND=0,31

> PWD <input name=pwd type=text>
SEND OK
AT+CIPSEND=0,47

> <input type=submit value=Connect></form></html>
SEND OK
AT+CIPCLOSE=0


OK
Unlink
Link

+IPD,0,355:GET /favicon.ico HTTP/1.1
Host: 192.168.4.1
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
HTTP/1.1
AT+CWJAP="EDIMAX-tony","1234567890"


OK
AT+CIFSR

192.168.4.1       (SoftAP 網址)
192.168.2.102   (連上 wifi 基地台後 DHCP 所指派的區域網址)

OK
AT+CIPSEND=1,27

> <html>Wifi setup OK!</html>
SEND OK
AT+CIPCLOSE=1


OK

感謝 AllAboutEE 的程式碼, 上面整個編譯後只佔了 27% 的程式儲存空間, 還有 70% 以上可用來開發應用 :


以上便是本次測試紀錄, 終於把 Arduino+ESP8266 模組連網的最後一道障礙拆除啦! 好了, 寫完就要睡覺去囉!

馬上補充 :

其實上面的程式可以稍微再精簡, 就是 find("GET ") 與 find("/") 可以合而為一, 這樣 if 就少一層啦!

      if (esp8266.find("GET /")) { //retrieve page router from remaining response
        memset(buffer, 0, sizeof(buffer));  //clear buffer (all set to 0)
        if (esp8266.readBytesUntil('/', buffer, sizeof(buffer))) {
          if (strcmp(buffer, "update") == 0) { //update wifi
            //"?ssid=aaa&pwd=bbb HTTP/1.1"            
            esp8266.find("?ssid="); //skip ssid token
            memset(buffer, 0, sizeof(buffer));  //clear buffer (all set to 0)
            esp8266.readBytesUntil('&', buffer, sizeof(buffer)); //retrieve ssid
            String ssid=buffer;
            esp8266.find("pwd="); //skip pwd token
            memset(buffer, 0, sizeof(buffer));  //clear buffer (all set to 0)
            esp8266.readBytesUntil(' ', buffer, sizeof(buffer)); //retrieve pwd
            String pwd=buffer;
            //set joint AP
            sendData("AT+CWJAP=\"" + ssid + "\",\"" + pwd + "\"\r\n",6000,DEBUG);
            sendData("AT+CIFSR\r\n",1000,DEBUG);
            //Serial.print("AT+CWJAP=\"" + ssid + "\",\"" + pwd + "\"\r\n");
            //show result
            String webpage="<html>Wifi setup OK!</html>";
            String cipSend = "AT+CIPSEND=";
            cipSend += connectionId;
            cipSend += ",";
            cipSend +=webpage.length();
            cipSend +="\r\n";
            sendData(cipSend,1000,DEBUG);
            sendData(webpage,2000,DEBUG);
           
            String closeCommand = "AT+CIPCLOSE=";
            closeCommand+=connectionId; // append connection id
            closeCommand+="\r\n";  
            sendData(closeCommand,3000,DEBUG);                
            }
          else { //show setup page
            String webpage="<html><form method=get action='/update/'>SSID <input name=ssid type=text><br>";
            String cipSend = "AT+CIPSEND=";
            cipSend = "AT+CIPSEND=";
            cipSend += connectionId;
            cipSend += ",";
            cipSend +=webpage.length();
            cipSend +="\r\n";
            sendData(cipSend,1000,DEBUG);
            sendData(webpage,2000,DEBUG);

            webpage="PWD <input name=pwd type=text> ";
            cipSend = "AT+CIPSEND=";
            cipSend += connectionId;
            cipSend += ",";
            cipSend +=webpage.length();
            cipSend +="\r\n";
            sendData(cipSend,1000,DEBUG);
            sendData(webpage,2000,DEBUG);

            webpage="<input type=submit value=Connect></form></html>";
            cipSend = "AT+CIPSEND=";
            cipSend += connectionId;
            cipSend += ",";
            cipSend +=webpage.length();
            cipSend +="\r\n";
            sendData(cipSend,1000,DEBUG);
            sendData(webpage,2000,DEBUG);

            String closeCommand = "AT+CIPCLOSE=";
            closeCommand+=connectionId; // append connection id
            closeCommand+="\r\n";  
            sendData(closeCommand,3000,DEBUG);
            }
          }
        }
      }
    }
  }


2016-06-01 補充 :

今早重作此實驗卻發現出現 "busy ...", Reset 後重新做也一樣 :

HTTP/1.1
HostAT+CWJAP="EDIMAX-tony","1234567890"

AT+CIFSR

busy p...
AT+CIPSEND=0,27

busy p...
<html>Wifi setup OK!</html>AT+CIPCLOSE=0

busy p...


不知原因為何, 難道如下列網頁所言是韌體版本的關係?

參考 :

Page impossible to be refreshed / AT+CIPCLOSE= causes "busy p..." message
Page impossible to be refreshed / AT+CIPCLOSE= busy p... 
ESP8266 Wi-Fi + Arduino upload to Xively and ThingsSpeak

2016-06-03 補充 :

注意, 以上只是一個測試專案的中間過程記錄, 不是最終結果. 參看 :

AllAboutEE 的 ESP8266 伺服器測試 (四) : 完結篇


2016年5月29日 星期日

2016 年第 21 周記事

本周因姊姊要去畫室補課, 二哥要去補習班上高一先修課, 因此只有菁菁跟我回鄉下啦. 除了幫爸煮珈哩與湯品外, 這個周末似乎一事無成. 原本打算完成 ESP8266 的 wifi 設定功能, 但是不知道在拖甚麼, 老是在關鍵時刻躊躇不前.

中午爸從阿泉伯家又帶回一袋玉米, 是阿正兄田裡收成的, 天啊, 跟種在我菜園裡的是同一批耶! 上週採收都嫌太老了, 他還拖到這周! 果然, 我挑了七條看來較小的來煮, 真的太硬啦! 沒辦法, 現在沒人養豬了, 我看只能當堆肥矣! 真是可惜.

週四 5/26 邀二哥去游泳, 買了新的泳鏡 (399) 與泳褲 (599), 那麼久沒游了 (上次是他小二的時後吧), 原本以為他早忘了, 沒想到試游幾次, 已可連續游一趟了. 只是體力有點遜, 沒多久就說有點累了, 要多操練. 自從會考結束, 游泳池就突然人多起來, 非常擠. 現在要挑九點去才不會人擠人.

連日太陽毒辣, 水圳也沒水, 菜園被蒸發得乾乾的. 爸說用自來水澆水划不來, 本月水單由 100 多暴增為 200 多元, 我的雨水收集與自動澆水系統卻還在只聞樓梯響階段. 下午表弟打電話來問這問題, 提到乾脆買現成的自動澆水控制器就好啦, 反正只要設定每 12 小時澆個 5~10 分鐘即可. 我想也是, 先解決目前的問題再說.


2016年5月28日 星期六

月娘的眼淚

昨天同事的手機又改鈴聲了, 是一首 2014 年發行的台語歌, 我只聽到兩句而已, 耳朵就已經豎了起來. 原來這是向蕙玲的 "月娘的眼淚". 搜尋 Youtube 發現這首歌有 MV, 不僅詞曲寫得深有韻味, MV 更是拍得催淚, 感情脆弱的會怪眼淚不爭氣喔 ...

# 向蕙玲『月娘的眼淚』MV

無對白版

上面的 MV 為配合歌曲剪輯, 沒有對白. 完整版的微電影有對話, 更能看出導演與編劇的功力 :

# 向蕙玲-月娘的眼淚(官方完整版微電影) HD【民視『風水世家』/三立『世間情』片尾曲】

完整版

她唱現場也很好聽, 唱功真的不錯 :

向蕙玲 {月娘的眼淚}



向蕙玲本名鄭愛樺, 台南人, 文藻日文系畢, 通過日文最高一級試驗. 向蕙玲是唱片公司老闆取的藝名, 有期許她向江蕙與黃乙玲看齊的意味在. 向蕙玲從小愛唱歌, 為了鍛鍊肺活量, 長跑逾 20 年, 毅力真是驚人. 詳見 :

# 維基 : 向蕙玲

MV 中那位爸爸跟女兒的男朋友說, "我跟你講喔! 我就這麼一個女兒, 你們兩個交往我是不反對, 但是如果你敢欺負她, 我就跟你拚!", 每一個有女兒的爸爸都會有同感吧! 做爸爸的剛開始都會拿著蒼蠅拍到處揮舞啊!


月娘的眼淚

作詞:石國人
作曲:石國人
編曲:張振杰

有情人的心總是藏情海 隨著漫天思念飄向天涯
無邊的雲海是多情世界 真心的愛有誰人會了解

若問你甘會知 雨水的由來 伊是月娘的眼淚
只有真愛才知伊藏在 思念尚深的所在
若問你甘會知 女人的心內 總是心情落情海
只有真愛才知不應該 尚深的愛是等待
像月眉 等待雲開的未來

有情人的心總是藏情海 隨著漫天思念飄向天涯 
無邊的雲海是多情世界 真心的愛有誰人會了解

若問你甘會知 雨水的由來 伊是月娘的眼淚
只有真愛才知伊藏在 思念尚深的所在
若問你甘會知 女人的心內 總是癡心落情海
只有真愛才知不應該 尚深的愛是等待
像月眉 等待雲開的未來

若問你甘會知 雨水的由來 伊是月娘的眼淚
只有真愛才知伊藏在 思念尚深的所在
若問你甘會知 女人的心內 總是癡心落情海
只有真愛才知不應該 尚深的愛是等待
像月眉 等待雲開的未來

人生苦短, 能聚在一起全是緣分, 想起茶道的一期一會哲學, 茫茫人海, 在有限的人生中能相遇實在不容易, 此生過後, 下輩子可能不會再見, 就算千年後再相逢, 都不記得了, 只剩下似曾相識的感覺. 能常常反思一期一會, 即使是惡緣也能化解為善緣, 人生在宇宙的生命中, 只不過是電光火石的一瞬而已.

珍惜時光啊! 人生就像沙漏一般, 昨日還在揮霍青春呢, 才一轉眼, 沙子就所剩不多了 ...


2016年5月27日 星期五

高雄股東會紀念品代領處

這兩天聽同事在講股東會紀念品, 我才想起前兩周陸續收到的股東會通知, 趕緊找出來剔除今年沒紀念品的, 昨天跑去中正三路的代領處領取, 領到超商卡 150 元以及米一包. 以前我都沒在領紀念品的, 原因是大部分公司都在台北, 而我不知道原來還可以代領. 幾年前知道後, 也沒時間去找到底要去哪裡領, 直到三年前聽同事講才曉得原來公司不遠處就有一個.

據說代領紀念品也成為一個生意, 獲利還不少哩!  參考 :

就賺股東會! 代領達人2個月進帳30萬
# 原本以為只是 1 張廢紙,差點把它扔掉... 今年的「股東會紀念品」,你領到什麼?

下面這篇有全國各代領處的列表 :

# 2015(104)年股東會贈品代換處及內容/股務查詢@ 米達想想

其中高雄離我上下班路線較近的有兩個 :

新興區中正三路 98 號 (07) 2379898

此代領處離公司較近, 我都中午吃飯時間去領. 不過它友達沒有代領. 下面這家在建興路與大順路交叉口附近的就有代領友達, 營業到晚上八點 :

三民區建興路192號 (07) 3849900


2016-05-30 補充 :

收到股東會通知單要趁早去領, 因為我去領的時候, 旁邊一位阿伯拿一疊出來, 結果老闆說這張這張都過期了云云. 我也有兩張過期, 本來有卻變沒有, 那種感覺就像對完發票發現中了 1000 塊, 結果放在皮夾忘記去領, 這感覺 ... 寧可沒中還比較好.

2016-05-31 補充 :

今天下班後繞到建興路去領友達紀念品 (是一包 600g 的米), 這家代領處其實就是櫻花熱水器的店面. 我發現建興路靠近大順路那一帶蠻熱鬧的, 做吃的店很多, 比大順路還多得多哩.


2016年5月24日 星期二

Line 實在很煩

最近對 Line 越來越煩了, 兩天就累積一堆, 根本不想一一去看. 今天上班忘了帶手機, 同事就說我中午休息一定會跑回去拿吧? 要不然可能會無心上班喔. 我說才不要咧! 我又不是沒有 Line 就無法過生活的人.

Line 令我厭煩的最大理由是, 到處瘋傳轉貼, 搞得笑話變得不好笑, 感人的小故事也一點都不感人, 每天一定報到的早安問候簡直變成垃圾. 走到哪裡都可能碰到有人跟你轉述在最近 48 小時內你已經看過, 聽過或許不只一遍的笑話, 這讓我真正理解甚麼叫做無感!

週日去文昌廟的路上, 二哥問我說鄉下孩子不知道都在玩些甚麼? 我說窮有窮的玩法, 鄉下孩子可能不像都市孩子人手一支智慧型手機, 也不可能家家有電腦, 他們可能看電視, 看書, 騎腳踏車兜風, 聊天, 玩遊戲, 或者要幫忙農事吧. 如果有了智慧型手機, 他們的生活內容也會完全改變.

放眼望去, 現在的人沒手機沒 Line 不知道怎麼過日子, 這種情況是比較幸福了嗎? 整天被大量重複的資訊覆蓋, 還有時間去思考與感動嗎? 有時候將手機放得遠遠的一整天, 會突然覺得一切都慢了下來, 少了資訊轟炸, 至少讓我可以回想一下, 沒有這些東西的時候, 日子都是怎麼過的.

2016-05-26 補充 :

今天看到同學用 Line 傳來的訊息, 說最近宜蘭大學的校醫遭電梯夾死事件, 監視器影片顯示由於電梯門開時, 醫師邊看手機邊進電梯, 沒有注意到電梯故障定位不良, 貿然踩進去導致憾事. 其實現在大街小巷也不乏邊走邊看手機的人, 這不只可能自己遭殃, 對於公共安全危害不小, 會害人害己.

當然這不是 Line 的錯, 它是很好的即時通訊工具, 糟糕的是, 跟菸一樣, 網路太方便了會使人上癮, 一旦無法 online 就不知道怎樣過生活.


可以儲存指令的 SSCOM 通訊軟體

昨天在 "用 Arduino 全面打造物聯網 (碁峰, 孫駿榮)" 這本書的 4-6 節看到它介紹了 SSCOM 這款通訊軟體, 可用來與 Arduino 或 ESP8266 通訊. 這本書是我從母校高應大圖書館借來的.


這本書雖然不算厚, 但包含了各項技術的精華介紹, 例如 RF 的頻譜, 藍芽/Zigbee 的網路拓樸等等, 甚至於 ESP8266 也有介紹其 AT 指令用法, 可能是市面上 Arduino 書籍中的唯一一本. 這 SSCOM 就是在 4.6 無線網路談 ESP8266 時順便提到的.

SSCOM 好用之處是具有指令儲存功能, 可以讓我們將 AT 指令儲存起來, 按右邊的 SEND 按鍵即可送出 AT 指令, 即使程式關掉這些指令也不會消失 (它會存在 sscom.ini 檔案裡). SSCOM 非常小, 可從下列網址下載 :

http://www.electrodragon.com/w/File:Sscom32E.zip

用法也很簡單, 參考下面這篇 :

# Windows下用SSCOM學習ESP8266 AT Command

最重要的是點選所連接之 COM 埠, 選擇 Baud rate, 在 Data input 框輸入 AT 指令後按 SEND 鈕即可 :


勾選右邊的 HexData 會改用 16 進制顯示, 按旁邊的 Clear 則會清除訊息視窗內的資料 :


按 EXT 鈕儲存指令會顯示指令表, 最多可儲存 26 組指令 (要將螢幕放到最大) :


我覺得這款比 Realterm 簡單易用, 非常適合使用 com port 的操作, 例如 ESP8266 等. 特別是儲存指令功能實在方便, 而 Arduino IDE 目前還沒有此功能, 每一個指令都必須硬敲.


2016年5月23日 星期一

AllAboutEE 的 ESP8266 伺服器測試 (一)

上週六日國中會考 (5/14~15), 我去雄工給二哥陪考, 看完兩本 Arduino 的書, 第一天看完剛從明儀買到的 "Arduino 物聯網專案實作 (博碩, 江良志譯)", 第二天看完跟市圖借的 "實戰數位家庭自動化-使用 Arduino", 兩天下來看得興味盎然, 又重新燃起 Arduino + ESP8266 的熱情. 奇怪, 記得去年底 Arduino 玩得不亦樂乎, 是為啥停下腳步呢?

我去翻查去年的網誌, 原來去年底要把剩下的休假休掉, 在家連休五天時, 看到自己前年底寫的如何在 GAE 部署 jQuery EasyUI 的文章, 發現這題目我只起了個頭, 做完初步測試證明可行後就切換到別的事情上了 (應該是 PHP/Java). 既然一時興起就繼續把 jQuery EasyUI on GAE 一直做下去, 弄到三月初終於搞定了, 把 PHP 架站機幾乎整個全部改寫為 Python 版並移植到 GAE 上頭去了. 然後三月底, 因為跟市圖借了一本 "Raspberry Pi 最佳入門與實戰應用", 又把塵封已久的樹莓派拿出來玩, 一直到現在. 因為樹莓派的關係, Arduino 也重獲我之芳心, 希望這次能像 EasyUI on GAE 一樣能集中火力攻關.

今天在網路上找到下面這個網站 :

ESP8266 Arduino Webserver Tutorial Code


這是 AllAboutEE 所製作的教育影片, 其網址在 :

How To Use the ESP8266 and Arduino as a Webserver
# Send Data From Webpage to ESP8266 (Toggle Arduino Pins From Webpage)

這讓我想起上一次玩 Arduino+ESP8266 時所寫的函式庫, 當時似乎遇到記憶體運用的瓶頸, 為了改進函式庫, 我還借了好幾本 C 語言的書來鑽研哩! 參考 :

ESP8266 函式庫 v2

不過今天看了這個 AllAboutEE 的做法, 覺得其寫法也很不錯, 其實原理都差不多, 但是他沒有寫一堆函式, 只寫了 sendData() 這個主要函式而已, 其他的就依據需要, 直接將 AT 指令當參數用 sendData() 傳送即可, 好處是不須匯入整個函式庫, 讓那些用不到的函式占用記憶體位置; 而且直接接觸 AT 指令可增強熟悉度.

此次測試我使用去年底所製作的 ESP8266 轉接板, 基本原理就是利用 Arduino 的內建函式庫 SoftwareSerial 指定兩個 Pin 與 ESP8266 做 UART 通訊, 因為 Arduino 的硬體串列埠 TX 與 RX 只有一對, 已經被用來接 PC 的 USB 以便上傳程式與 Debug 用. 必須透過軟體序列埠來向 ESP8266 下 AT 指令與接收回應.



參考下面兩篇文章, 裡面有紀錄轉接板製作方式, 沒有轉接板也可以參考所附電路圖在麵包板上直接接線 :

# 製作 ESP8266 轉接板
# 撰寫 Arduino 的 ESP8266 WiFi 函式


ESP8266 的 Vcc 可直接取用 Nano 的 3.3V 輸出即可, 但如果發現 ESP8266 不穩定, 通常是電源不夠力, 因為 PC 的 USB 輸出約 500mA, 而 Arduino 的 3.3V 輸出最多只有 150~200mA 而已, 參考 :

How much current can I draw from the Arduino's pins?
# How much current can be drawn from an Arduino Uno's 3.3V rail?

這時就需要獨立的 3.3V 電源供應, 可用兩個 1.5V 乾電池串聯起來供電, 但切記 ESP8266 的 GND 必須跟 Arduino 的 GND 共接地以形成迴路, 很多人會忽略這個, 當然怎麼測都不會 Work. 建議使用一個麵包板電源模組, 這種板子很便宜, 可直接插在麵包板的上下電源槽, 板上使用 AMS1117 穩壓晶片提供 3.3V 800mA 的驅動力.

 
本測試需要對 ESP8266 的 AT 指令操作有基本的了解, 參考 :

ESP8266 WiFi 模組 AT command 測試

之前曾對 ESP8266 做過伺服器功能測試, 不過當時是將 ESP8266 設在模式 1 (Station), 亦即只能連上指定的無線基地台, 本身不當 AP, 這做法跟 AllAboutEE 是一樣的. 當連線無線基地台成功時會從無線 AP 的 DHCP 取得一個區網 IP. 在模式 1 下用 AT+CIPMUX=1 與 AT+CIPSERVER=1,80 開啟多重連線與 80 埠的網頁伺服器, 這樣連線到 ESP8266 的區網 IP 時就會連線到該網頁伺服器. 參考 :

# ESP8266 網頁伺服器 AT 指令測試

我修改了 AllAboutEE 網頁中的程式, 不同之處是將工作模式改為 3 (STA+AP), 這是我參考 Webduino 的做法, 希望可以用手機或筆電來連線 ESP8266 的 SoftAP 以便設定 wifi 的 ssid 與 password, 讓 ESP8266 連上無線基地台. 因為一個 Arduino + ESP8266 物聯網專案必須透過 wifi 連上 Internet, 在測試時我們是將無線基地台的 ssid 與密碼寫在程式中, 但實務上可不能要使用者自己用 Arduino IDE 去改源碼, 編譯後上傳 Arduino 吧?

我覺得 Webduino 的做法很棒, 就是讓 ESP8266 工作在模式 3, 同樣開啟多重連線與網頁伺服器, 這時所建立的 SoftAP 無線基地台 (網址固定是 192.168.4.1) 可讓使用者用手機或筆電連線, 跟連線區網 IP 一樣會連到此網頁伺服器. 然後用瀏覽器來設定 Station 部分要連線之聯外無線基地台的 SSID 與密碼. 這個做法的好處是 SoftAP 的網址固定就是 192.168.4.1, 如果是設為模式 1, 因為我們無法得知 DHCP 到底會指派甚麼 IP 給 ESP8266, 當然就無法連線此網頁伺服器了 (當然可以去聯外無線基地台的管理頁面依據 MAC 來查詢, 但我們不能要求使用者這麼做, 因為不是每個人都了解網路設定).

下列程式碼只是初步讓 Arduino 透過 ESP8266 建立一個網頁伺服器, 當客戶端連線 ESP8266 的 SoftAP 網址 192.168.4.1 時, 回應客戶端 (即手機瀏覽器) 一個無線基地台的連線設定頁面而已.

#include <SoftwareSerial.h>
#define DEBUG true

SoftwareSerial esp8266(10,11); //(RX,TX)

void setup() {
  Serial.begin(9600); //start hardware serial port
  esp8266.begin(9600); //start soft serial port
  sendData("AT+RST\r\n",2000,DEBUG); // reset module
  sendData("AT+CWMODE=3\r\n",1000,DEBUG); // configure as STA+AP
  sendData("AT+CIFSR\r\n",1000,DEBUG); // get ip address
  sendData("AT+CIPMUX=1\r\n",1000,DEBUG); // configure for multiple connections
  sendData("AT+CIPSERVER=1,80\r\n",1000,DEBUG); // turn on server on port 80
  }

void loop() {
  if (esp8266.available()) { // check if the esp is sending a message
    /*
    while(esp8266.available())
    {
      // The esp has data so display its output to the serial window
      char c = esp8266.read(); // read the next character.
      Serial.write(c);
    } */
 
    if (esp8266.find("+IPD,")) {  //收到客戶端的連線要求, 進行回應
      delay(1000);
      // +IPD, 後的字元是連線 ID (ASCII碼), 用 read() 讀取後減 48 為數字
      int connectionId = esp8266.read()-48;
      //subtract 48 because the read() function returns
      //the ASCII decimal value and 0 (the first decimal number) starts at 48
      String webpage="<html><form method=get>SSID <input name=ssid type=text><br>";
      String cipSend = "AT+CIPSEND=";
      cipSend += connectionId;
      cipSend += ",";
      cipSend +=webpage.length();
      cipSend +="\r\n";
      sendData(cipSend,1000,DEBUG);
      sendData(webpage,2000,DEBUG);

      webpage="PWD <input name=password type=text> ";
      cipSend = "AT+CIPSEND=";
      cipSend += connectionId;
      cipSend += ",";
      cipSend +=webpage.length();
      cipSend +="\r\n";
      sendData(cipSend,1000,DEBUG);
      sendData(webpage,2000,DEBUG);

      webpage="<input type=submit value=Connect></form></html>";
      cipSend = "AT+CIPSEND=";
      cipSend += connectionId;
      cipSend += ",";
      cipSend +=webpage.length();
      cipSend +="\r\n";
      sendData(cipSend,1000,DEBUG);
      sendData(webpage,2000,DEBUG);

      String closeCommand = "AT+CIPCLOSE=";
      closeCommand+=connectionId; // append connection id
      closeCommand+="\r\n";  
      sendData(closeCommand,3000,DEBUG);
      }
    }
  }

String sendData(String command, const int timeout, boolean debug) {
  String response="";
  esp8266.print(command); // send the read character to the esp8266
  long int time=millis();
  while ((time+timeout) > millis()) {
    while(esp8266.available()) {
      // The esp has data so display its output to the serial window
      char c=esp8266.read(); // read the next character.
      response += c;
      }
    }
  if (debug) {Serial.print(response);}
  return response;
  }

此程式在 setup() 部分有三個最重要的 AT 指令 :
  1. AT+CWMODE=3 : 設定 ESP8266 工作於 STA+AP 模式
  2. AT+CIPMUX=1 :  開啟多重連線 (作為伺服器之必須)
  3. AT+CIPSERVER=1,80 : 開啟伺服器之 80 埠做網頁服務
原作者只寫了一個 sendData() 函數來傳送 AT 指令, 此分成兩部分, 先傳送包含連線 ID 與網頁資料長度的 CIPSEND, 然後才是傳送網頁本身. 雖然分成兩步驟, 但這樣將 CIPSEND 分離出來的作法讓 sendData() 可以用來傳送任何 AT 指令, 共用效果最大.

注意, 程式中使用序列埠的 find() 函數尋找回應字串 "+IPD,", 這是 ESP8266 接收到來自客戶端連線後的回應字串開頭,  後面跟著的是連線通道 id, 是 0~4 的數字, 可用 read() 函數讀取, 但讀到的是以 ASCII 編碼的通道號碼, 其值為 48~52 的數值 (48 是 "0" 的 ASCII 碼), 因此必須減 48 才會得到正確的通道號碼. 關於 ESP8266 連線回應字串 +IPD 參考 :

# ESP8266 網頁伺服器 AT 指令測試

程式上傳 Arduino 後, 打開手機的 wifi, 應該可以偵測到 SSID 是 "ESP_" 開頭的無線基地台 (例如 ESP_9CBD07), 這就是 ESP8266 模式 2 或 3 下所建立的 SoftAP, 打開手機 wifi 連線此無線基地台 (密碼可事先用 AT+CWSAP 指令設定, ESP8266 預設無密碼) :



連線此 SoftAP 成功後, 打開手機瀏覽器, 連線 ESP8266 伺服器網址 192.168.4.1, 上面的程式碼中, Arduino 就會透過軟體序列埠將網頁碼輸出給 ESP8266, 而 ESP8266 的伺服器就將收到的網頁碼回應給客戶端, 即手機瀏覽器 :



輸入 SSID 與 PWD 後, 按 Connect 鈕就會將此兩參數傳遞給 ESP8266, 但是因為上面的程式沒有進一步處理收到的參數, 因此還是回應同樣的網頁. 因手機瀏覽器沒有開發工具, 無法觀察 HTTP 訊息中的參數, 因此我改用筆電來連線 ESP8266 之 SoftAP :


連線成功後, 同樣打開瀏覽器, 輸入網址 192.168.4.1, 同時按下 F12 觀察 HTTP 訊息 :


可見按下 Connect 後, 此網頁確實向後端 (即 ESP8266) 傳遞了 ssid 與 password 這兩個參數, ESP8266 的網頁伺服器會對此 Request 做出回應 (即 +IPD 字串), 參考這篇 :

ESP8266 網頁伺服器 AT 指令測試

Arduino 透過軟體序列埠可以接收到這個回應字串, 我們接下來必須剖析這個回應字串中的位址訊息, 即 HTTP 後面的網址, 擷取出 GET 方法傳送的兩個參數 ssid 與 password, 然後由 Arduino 向 ESP8266 下達 AT+CWJAP 指令, 即可讓 ESP8266 的 Station 部分連上指定的無線基地台.

下面是序列埠監視視窗擷取之訊息 :

AT+RST


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

no change
AT+CIFSR

192.168.4.1
0.0.0.0

OK
AT+CIPMUX=1


OK
AT+CIPSERVER=1,80


OK
,362:GET / HTTP/1.1
Host: 192.168.4.1
User-Agent: Mozilla/5.A
> <html><form method=get>SSID <input name=ssid type=text><br>
SEND OK
AT+CIPSEND=0,36

> PWD <input name=password type=text>
SEND OK
AT+CIPSEND=0,47

> <input type=submit value=Connect></form></html>
SEND OK
AT+CIPCLOSE=0


OK
Unlink
,358:GET /?ssid=ttt&password=aaa HTTP/1.1
Host: 192.168.4.1
A
> <html><form method=get>SSID <input name=ssid type=text><br>
SEND OK
AT+CIPSEND=0,36

> PWD <input name=password type=text>
SEND OK
AT+CIPSEND=0,47

> <input type=submit value=Connect></form></html>
SEND OK
AT+CIPCLOSE=0


OK
Unlink

注意喲, 當使用手機瀏覽器連線 SoftAP 網址 192.168.4.1 成功時, ESP8266 會回應 Link 以及隨後的 +IPD 回應字串, 但因為 Serial 物件的 find() 函數在讀取串列埠緩衝器時會刪除已讀的字元, 所以當其讀到 "+IPD," 時也把這五個字元同時從緩衝器刪除 (每讀一個字元刪除一個), 接著用 read() 函數讀取連線通道字元時, 又把連線通道 '0' 刪除, 所以在 sendData() 函式中最後第二行輸出回應字串時就少了這六個字元了, 實際上完整的第一行如下 :

+IPD,0,362:GET / HTTP/1.1

但最後輸出時第一行變成 :

,362:GET / HTTP/1.1

OK, 到此已完成伺服器測試的第一步, 接下來要修改上面的程式, 進一步剖析 HTTP 表頭資訊, 擷取其中之網址與所傳遞之參數, 以便能透過 AT+CWJAP 指令來設定想要連線之無線基地台.

關於剖析表頭字串, 需要使用到串列埠之 find(), readBytesUntil() 等函數, 可參考下列文章 :

https://www.arduino.cc/en/Reference/Serial
# Arduino: Sending and Receiving Multi-Digit Integers
# SO, HOW DOES SERIAL.READBYTESUNTIL() WORK?


2016-05-28 補充 :

我在 0.9.5 韌體的 ESP8266 發現, 其 SoftAP 不是以 "ESP_" 開頭, 而是 "AI-THINKER_".

其他參考 :

# ESP8266 - AT Command Reference
ESP8266 WiFi Module Quick Start Guide
# wifiwebserver
http://www.ebook777.com/make-sensors-projects-experiments-measure-world-arduino-raspberry-pi/

2016-06-03 補充 :

以上只是一個測試專案的中間過程記錄, 不是最終結果. 參看 :

AllAboutEE 的 ESP8266 伺服器測試 (四) : 完結篇


2016年5月22日 星期日

2016 年第 20 周記事

時間好快, 轉眼一年已經快過一半了. 本周重要事項是今天要去家祠祭拜母親, 因上個月加金, 滿一個月後須備牲禮果品祭祀. 原本是 5/9 日滿一個月, 但該日為周一上班上課日, 欲延至 5/15 又適逢二哥國中會考, 故又再延一周至今日.

早上天氣陰晴不定, 趁早去市場買菜並取回代辦之牲禮回來後, 九點出發前往家祠. 但是到了那邊太陽又出來了, 雖然不用再擔心下雨, 但是悶熱的天氣讓人汗水淋漓. 辦完滿月祭拜, 母親大人的身後事就全部圓滿了, 這些都是她生前所交代的, 我已如實完成了. 

下午帶二哥去文昌廟上香回來後, 爸說上周被狂風暴雨吹得東倒西歪的玉米再不採收就要太老了, 於是冒著一陣陣的間歇午後雨全數採收, 大概有五個水桶之多, 這麼多怎麼可能吃得完呢? 於是拿了一桶去對面張家伙房, 半桶給了松盛伯家, 另外半桶給了榮彩伯. 爸又載一桶去給阿運伯母, 其他全部載來高雄, 明天要分送給二阿姨, 左營阿姨, 辦公室同事, 以及岳家. 其實這些玉米是阿泉伯的兒子借我家菜園種的, 本來輪不到我來收成, 只是他另外還種了七厘地的玉米, 實在分身乏術, 就要我們自行處理. 本來只能流口水的, 現在變成猛吞口水, 因為接下來一周我每天的早餐就是玉米啦!


2016年5月21日 星期六

買三用電表

現用的三用電表只有一個, 來往鄉下與高雄時帶來帶去很麻煩, 想再買一個放在鄉下, 上露天找到了這個在高雄可以面交取貨的, 價格不貴, 可用就好啦!

# 數位三用電表 萬用電表 DT-9205A $149

店家週三, 五可在建工路大豐路口的萊爾富取貨, 昨天拿回來使用 OK, 只是 AUTO POWER OFF 似乎較久, 約十分鐘.


2016年5月20日 星期五

Python 內建 GUI 模組 tkinter 測試 (一) : 建立視窗

最近因為玩樹苺派的關係, 接觸到 Python 內建的 GUI 開發模組 Tkinter (意思是 Tk Interface), 初步覺得比用 Java 的 Swing 還要來得容易, 因此就來學看看唄!

Tk 原先是為 Tcl 語言所開發的 GUI 套件, 因為是 Tcl 的第一個擴充, 所以現在都合起來稱呼為 Tcl/Tk. Tcl 是一種以 string-based 的跨平台工具命令式直譯語言 (Tool command language), 繼承了 LISP/C/Shell 等語言的優點, 並具有語法簡單, 容易擴展與可靈活嵌入其他語言的特點, 而且全面支持 unicode.

而 Tkinter 是 Python 內建的標準模組, 內部嵌入了 Tcl/Tk GUI 套件, 用來在 Python 中建構 GUI 圖形介面程式, 它具有如下優點 :
  1. 簡單易學 :
    比 Python 其他 GUI 要容易, 甚至於我覺得比學 Java Swing 還容易.
  2. 程式碼精簡 :
    以很短的程式碼便能產生強大功能的 GUI 程式.
  3. 跨平台 :
    同樣的程式可以在 Linux/Windows/Mac 等系統上執行.
不過在 Python 2 中的模組名稱 Tkinter 到 Python 3 版後已被改為小寫的 tkinter, 使用時要注意所用之 Python 版本, 匯入時注意該用首字大寫與否. Tkinter 教學文件參考 :

https://pythonspot.com
http://effbot.org/tkinterbook/

在 Python 2 下以 Tkinter 所寫的 GUI 程式可以利用 2to3 程式轉成 Python 3 版的程式, 詳見 :

https://docs.python.org/2/library/tkinter.html

在 Python 2 中使用 Tkinter 需先匯入模組 : 

import Tkinter



from Tkinter import *

但書裡說不建議使用後面這種方式, 因為此方法是匯入 Tkinter 所有函數與屬性, 這樣會占用較多記憶體, 而且容易讓命名空間混淆, 並使得 debug 難度增加. 不過, 這種方式使用起來較方便 (凡事皆有代價).

比較常見的匯入方式是幫 Tkinter 取個別名 :

import Tkinter as tk

注意, 在 Python 3 要用小寫的 tkinter :

import tkinter as tk

這樣就能使用 tk 這個別名來呼叫此模組內的函數 :

tk.函數名稱()

匯入模組就可以呼叫 Tk() 函數建立一個根視窗實體 :

import Tkinter as tk
root=tk.Tk()

如果沒有取別名, 就要用 Tkinter.Tk() :

import Tkinter
root=Tkinter.Tk()

如果是 Python3 模組名稱要用小寫, 但 Tk() 函數一樣是首字大寫 :

import tkinter
root=tkinter.Tk()

然後呼叫此視窗實體之 mainloop() 函數將此視窗加入事件監視迴圈, 這樣就會產生 GUI 視窗了. 注意, mainloop() 除了監視事件外, 也是維持視窗持續存在之方法 :

root.mainloop()

真是太簡單了, 建立一個空視窗只要三行程式碼!

如果用 Java Swing 來產生一個空視窗, 至少需要 10 行以上的程式碼 :

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.event.*;

public class JFrame1 {
  JFrame f;
  public static void main(String argv[]) {
    new JFrame1();
    }
  public JFrame1() {
    f=new JFrame("JFrame 1");
    f.setBounds(0,0,400,300);
    f.setVisible(true);
    }
  }

這就是為什麼 Tkinter 非常適合用來快速開發 GUI 程式的原因.

在 IDLE 介面輸入此三行程式, 馬上就建立一個視窗了 :

Python 2.7.6 (default, Nov 10 2013, 19:24:18) [MSC v.1500 32 bit (Intel)] on win32
Type "copyright", "credits" or "license()" for more information.
>>> import Tkinter as tk
>>> root=tk.Tk()
>>> root.mainloop()




注意, 視窗描繪是在加入事件監視函數 mainloop() 後才發生的唷! 注意, 在互動環境例如 Python IDLE 介面只要呼叫 tk.Tk() 就會出現視窗了, 但在寫成 .py 程式時則必須要呼叫 mainloop() 函數才會出現視窗.

此空視窗右上角有標準視窗的縮小, 放大, 以及關閉按鈕, 還可以拖曳調整視窗大小. 預設標題 (title) 是 "tk", 我們可以呼叫 title 函數來加以設定 :

>>> import Tkinter as tk
>>> root=tk.Tk()
>>> root.title("My First Tk GUI")
>>> root.mainloop()




如果不想讓使用者調整視窗大小, 可以呼叫 resizable(0, 0) 來禁止, 以下在 Win10 下使用 Python 3 測試 :

import tkinter as tk
root=tk.Tk()
root.title("My First Tk GUI")
root.resizable(0,0)
root.mainloop()




這樣不僅無法拖曳縮放視窗, 右上角的放大按鈕也被禁能了, 只能縮小與關閉. 此 resizable() 函數事實上是用來設定視窗左上角座標, 設為 0,0 表示無法縮放.

tkinter 提供了下列 21 種 GUI 元件, 稱為 widget (視窗元件) 或 control (控制項), 所謂 widget 乃 Window gadget 之意  :
  1. Label (標籤)
  2. Button (按鈕)
  3. Radiobutton (單選圓鈕)
  4. Checkbutton (核取方塊)
  5. Entry (文字欄位)
  6. Frame (框)
  7. LabelFrame (文字框)
  8. Listbox (清單方塊)
  9. Text (文字框)
  10. Message 
  11. PanedWindow
  12. Scrollbar (捲軸)
  13. Scale
  14. Spinbox
  15. Menu (選單)
  16. OptionMenu
  17. Menubutton (選單按鈕)
  18. Canvas (畫布)
  19. Image (圖片)
  20. Bitmap (點陣圖)
  21. Toplevel

下面先來測試其中最常用的 Button (按鈕) 與 Label (標籤) 元件, 把下列程式用記事本存成 test.py :

import tkinter as tk
root=tk.Tk()     #建立視窗容器物件
root.title("Tk GUI")
label=tk.Label(root, text="Hello World!")   #建立標籤物件
label.pack()       #將元件放入容器
button=tk.Button(root, text="OK")
button.pack()     #將元件放入容器
root.mainloop()

然後在命令提示字元視窗執行 :

D:\Python\test>python test.py




可見在呼叫 pack() 函數做版面管理的話, tkinter 會將元件由上而下水平置中順序排版.

注意, 在建立 GUI 元件時, 第一個參數必須為其容器物件 (此處為 root 變數所代表之視窗), 格式如下 :

元件變數=元件名稱(容器物件變數, [元件選項]) 

其他為元件之選項參數, 例如標籤文字 (text), 大小(size), 邊框 (border), 前景顏色 (foreground), 或背景顏色 (background), 有些選項所有元件都有, 但每一種元件也有特定之選項, 可以在建立元件時直接設定, 也可以在建立之後呼叫 configure() 函數或其別名 config() 來設定.

其次, 所建立的元件必須利用版面管理員 (geometry manager) 於視窗容器中定位, 這樣它才會在視窗中顯現, 而此 pack() 函數就是一個版面管理員, 它會由上而下擺放元件.

如果是使用 star import 匯入方式, 上面程式的寫法要改為 :

from tkinter import *
root=Tk()
root.title("Tk GUI")
label=Label(root, text="Hello World!")
button=Button(root, text="OK")
label.pack()
button.pack()
root.mainloop()

ㄟ, 這個寫法好像比較簡單哩! 不用寫 tk. 如果只是寫個簡單的視窗程式或做測試這樣寫很簡便, 但如果寫比較複雜的系統就不宜用 star import 方式, 因為可能會使命名空間衝突導致 debug 變得很麻煩.

除了由上而下的 pack() 版面管理員外, 也可以呼叫 grid() 函數使用網格版面來管理元件 :

from tkinter import *
root=Tk()
root.title("Tk GUI")
label=Label(root, text="Hello World!")
button=Button(root, text="OK")
label.grid(column=0,row=0)
button.grid(column=1,row=0)
root.mainloop()




在呼叫 grid() 時, 我們將 label 放在 0 行 0 列, button 放在 1 行 0 列, 亦即這是一個 1x2 (1 列 2 行) 的網格, 因此元件是左右各排一個.

目前這個按鈕按下去不會有反應, 因為我們還沒有幫它設定事件處理函數. 在下面範例中, 我定義了一個按鈕事件處理函數 clickOK() 來處理 OK 鍵被按事件, 並設定了一個全域變數 count 來記錄 OK 鍵被按了幾次, 當 OK 被按時, count 會增量, 並且透過呼叫元件的 configure() 或 config() 函數來更改標籤元件的文字內容 (text 屬性) :

from tkinter import *
root=Tk()
root.title("Tk GUI")
label=Label(root, text="Hello World!")
count=0
def clickOK():
    global count
    count=count + 1
    label.configure(text="Click OK " + str(count) + " times")
button=Button(root, text="OK", command=clickOK)
label.pack()
button.pack()
root.mainloop()




注意, 事件處理函數與 GUI 元件之間是透過 command 參數來綁定的. 另外上面也用到了 Python 內建函數 str() 來將數值類型的 count 轉成字串類型.

上面範例中的按鈕看起來似乎不是那麼好看, 所以 Tkinter 後來又推出了加強版的 ttk 模組來美化元件的外觀, 這 ttk 意思是 Themed Tkinter, 亦即主題化版本, 參考 :

https://docs.python.org/2/library/ttk.html

此 ttk 模組包含了 17 種元件, 其中的 11 種是 Tkinter 原本已經有的 :
  1. Label
  2. Button
  3. Radiobutton
  4. Checkbutton
  5. Entry
  6. Frame
  7. Labelframe
  8. Menubutton
  9. Scale
  10. Scrollbar
  11. Panedwindow
另外 6 個是 ttk 推出的新元件 :
  1. Combobox
  2. Notebook
  3. Progressbar
  4. Separator
  5. Sizegrip
  6. Treeview
注意, ttk 須在匯入 tkinter 後才能匯入, 這樣才會蓋掉原來的 tkinter 元件 :

from tkinter import ttk     (建議)



from tkinter.ttk import *    (不建議)

將上面範例用 ttk 改寫如下 :

from tkinter import *
from tkinter.ttk import *
root=Tk()
root.title("ttk GUI")
label=Label(root, text="Hello World!")
count=0
def clickOK():
    global count
    count=count + 1
    label.config(text="Click OK " + str(count) + " times")
button=Button(root, text="OK", command=clickOK)
label.pack()
button.pack()
root.mainloop()




可見按鈕真的變漂亮了!

上面範例使用 star import 共用命名空間不容易 debug, 建議用下面寫法 :

import tkinter as tk
from tkinter import ttk

root=tk.Tk()
root.title("ttk GUI")
label=ttk.Label(root, text="Hello World!")
count=0
def clickOK():
    global count
    count=count + 1
    label.config(text="Click OK " + str(count) + " times")
button=ttk.Button(root, text="OK", command=clickOK)
label.pack()
button.pack()
root.mainloop()


Tk 模組有一個測試函數 _test(), 會顯示一個內建的測試視窗 :

Python 2.7.8 (default, Jun 30 2014, 16:03:49) [MSC v.1500 32 bit (Intel)] on win32
Type "copyright", "credits" or "license()" for more information.
>>> from Tkinter import *
>>> Tkinter._test()




呼叫 Tcl().eval() 函數可以查詢 Tk 版本 :

>>> Tkinter.Tcl().eval('info patchlevel')
'8.5.15'

我這是在 Python 2.7.6 上測試的, Tk 的版本是 8.5, 如果在 Python 3 上測試, Tk 版本應該是 8.6 版以上. 8.6 版在功能上比起 8.5 有大幅改進. Python 3 的測試函數寫法如下 :

Python 3.6.1 (v3.6.1:69c0db5, Mar 21 2017, 18:41:36) [MSC v.1900 64 bit (AMD64)] on win32
Type "copyright", "credits" or "license()" for more information.
>>> import tkinter as tk 
>>> tk._test() 




>>> tk.Tcl().eval('info patchlevel') 
'8.6.6'

可見 Python 3 使用的是 Tk 8.6 版.

用 Tkinter 模組寫好的 Python 程式可以用 py2exe 模組轉成 Windows 執行檔, 參考 :

將python轉成執行檔(py2exe)
http://www.py2exe.org/index.cgi/Tutorial
# Tk : Dialog window

建立 tkinter 元件時需傳入參數, 這些參數依元件而異, 但有一些與尺寸, 顏色, 字型或樣式有關的參數是共通的, 如下表所示 :

 長度參數 說明
 width 元件寬度
 height 元件高度
 wraplength 跳行長度
 padX 與鄰近元件之水平間隔
 padY 與鄰近元件之垂直間隔
 borderwidth 邊框寬度

 顏色參數 說明
 background (bg) 背景顏色
 foreground(fg) 前景顏色
 hightcolor 高亮度時顏色
 activebackground 致能時背景顏色 
 activeforeground 致能時前景顏色
 disablebackground 禁能時背景顏色
 disableforeground 禁能時前景顏色
 selectbackground 被選擇時背景顏色
 selectforeground 被選擇時前景顏色


其他參考 :

# 傻貓布落格 : Tkinter
# TutorialsPoint
# An Introduction to Tkinter
# Thinking in Tkinter
https://docs.python.org/3.1/library/tkinter.ttk.html

2016年5月19日 星期四

濃濃台味的嘻哈庄腳情

前陣子菁菁突然改變喜歡的曲風, 從英文與韓語歌曲改為充滿台味的台語歌, 原來是受到班上一位同學滿口台語, 常哼台語流行歌曲影響的關係. 今天在 Youtube 上看到這首 "嘻哈庄腳情", 咦, 這不是菁菁前不久常聽的 911 樂團唱的嗎? 剛開始覺得好聳, 但幾次洗腦下來, 仔細聽還真的蠻不錯聽的 :

https://www.youtube.com/watch?v=JvkqZrYJUe8


歌詞寫得很有意思, 後面 lulu (黃路梓茵) 的歌聲也好特別, 聽過後會想一聽再聽哩!

2016-06-02 補充 :

今天看到這篇 :

# 「玖壹壹」在紅什麼?

原來 911 這麼受歡迎是因為唱出了小市民的心聲.


2016年5月17日 星期二

跟三民買兩本機器人簡體書

今天上博客來要買之前想買的 "Raspberry Pi機器人制作實例:用Python Linux和傳感器搭建智能小車", 發現竟然已經賣完, 還好三民還有貨, 但是這兩家網路書店都要滿 350 元才免運費, 所以又多買了一本 Arduino 的 :

ISBN
書籍名稱
作者/
出版社
單價
折扣
特價
()Wolfram Donat
人民郵電出版社
294
87
256
毛勇
清華大學出版社(大陸)
336
87
292

因為是海外庫存, 所以可能要等三周才會到貨 (光興店).

2016-07-09 補充 :

在下訂一個半月後, 這兩本小書才到貨, 有夠慢的. 結果發現毛勇這本機器人的天空根本不用買, 因為市圖就有這本書的繁體版, 我還寫了書評哩, 參考 :

# 好書 : 13 堂課學會 Arduino 機器人製作

而 Raspberry Pi 機器人製作實例這本書雖然薄薄一本, 但也還頗有參考價值. 這本書提到好幾個好用的感測器 :

  1. SHT15 溫度感測器
  2. HC-SR04 超音波感測器
  3. MCP3008 ADC 轉換晶片
  4. 霍爾磁場感測器



2016年5月16日 星期一

購買無線鍵盤滑鼠組

昨天跟二哥從總館回來路過建國路時, 進去這家賣電玩遊戲的店, 買了下面這組無線鍵盤滑鼠, 幾周前有在露天搜尋, 發現最便宜的是這組雷柏的, 剛好又在高雄 (順發附近), 自取就免郵資更划算 :

# [哈GAME族]●可超商取付● 雷柏 RAPOO X1800 2.4G 無線鍵盤滑鼠組 多媒體鍵 超薄外框 簡約時尚 $400

買這組主要是給樹莓派用的, 因為原本給 Android Mini PC 用的迷你鍵盤不是很好用, 它沒有滑鼠, 而是按鍵盤上一個鍵就會切換為遙控器, 可以在螢幕上移動游標, 要輸入文字時再切回來, 很憋扭. 而且它的某些特殊鍵 (例如 $ 或 @) 與標準鍵盤不同, 之前在測試 Raspberry Pi A+ 板子時, 無線網路設定檔的密碼明明沒錯, 就是無法連上 wifi, 後來搞清楚原來是密碼部分某些鍵實際與輸入不同所致, 雖然在鍵盤設定我有改為美規 105 鍵也一樣. 所以乾脆買個標準鍵鼠組來解決.

今晚拆開裝上電池後, 插入樹莓派 B+, 開機發現此款鍵鼠組可正常被樹莓派抓到 :

pi@raspberrypi:~ $ lsusb
Bus 001 Device 005: ID 148f:5370 Ralink Technology, Corp. RT5370 Wireless Adapter
Bus 001 Device 004: ID 24ae:2000
Bus 001 Device 003: ID 0424:ec00 Standard Microsystems Corp. SMSC9512/9514 Fast Ethernet Adapter
Bus 001 Device 002: ID 0424:9514 Standard Microsystems Corp.
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub

不過此款鍵鼠組有個缺點, 就是其 USB dongle 較長, 是一般的約四倍長, 而且不用時沒辦法收納到滑鼠肚子裡, 比較容易遺失.

本來有考慮下面這款 KTNET 鍵鼠組 Z1, 這個 dongle 就可收納於滑鼠內, 店家也在建國路, 但因貴了 40 元, 且造型凹凹凸凸, 我不太喜歡 :

# 【上震科技】KTNET Z1 2.4G 無線鍵盤滑鼠組 6鍵式電競無線光學滑鼠 $440



2016年5月15日 星期日

2016 年第 19 周記事

為期兩天的國中會考, 到中午 12:30 結束了, 還好天氣不錯沒下雨, 雖然炎熱但在樹蔭下涼風徐徐, 總比在休息教室好得多.

週五下午請了兩個小時回家載二哥去雄工看考場, 在第一棟樓下繞了一圈, 當下就選定試務中心旁懷泗亭前的大樹下當陪考基地. 週六 5/14 早上 06:40 就出發, 到達時才七點剛過, 那邊都還沒有人進駐哩!

第一天考社會, 數學, 國文與作文, 第二天自然, 英文與英聽. 我照美珠老師說的, 每節考完出來也不問考得如何, 以免增加壓力. 下午考完後, 二哥說要去總館借書, 因美珠老師說明日起大家都要帶小說去學校看. 晚上則去大八聚餐, 以往都因為要補習, 所以生日, 母親節, 父親節都沒辦法一起去吃飯.

去總館只借了一本 C 語言的書 :

# C語言程式設計剖析(附範例光碟)(第三版)

這本由萬能科大簡聰海寫的書我覺得甚好, 一些眉角都有提到, 且範例超多, 有 400 餘個. 剛好二哥考完會考, 我想跟他一起來學.

這周因為陪考的關係, 沒有回鄉下, 下周 5/22 大家都能回去, 補辦母親進金滿月之祭拜.

2016年5月12日 星期四

非善非惡

北捷隨機殺人案兇手鄭捷被執行死刑了, 辦公室裡聊起這件事, 正如輿論有七八成贊成死刑一樣, 大家不是說早該槍決了, 就是撻伐廢死聯盟應該改稱廢物聯盟云云 ... 沒錯, 每個人必須為自己的行為負責, 但是關心教育的人會想得更深, 想要探究事件的原因, 從中汲取教訓, 避免悲劇再發生.

說實在的, 我也很想加入激情行列, 因為罵完就沒事了, 比較輕鬆呢. 可恨的是我總是無可自拔地要往深一層去想, 鄭捷怎麼會走到這一步呢? 他如何從一張白紙變成滿手血腥, 害如此多家庭破碎的兇手?

每一個人出生的時候, 都是白白胖胖的可愛娃, 都是爸媽心中的寶貝, 他們的皮膚是如此地柔嫩, 鄭捷出生時也是如此啊! 但為何二十多年後, 法警必須在那曾經是細皮嫩肉的背後開槍, 結束他年輕的生命? 所有的死刑犯都曾經是個可愛娃 ... 這何以致之? 這個國家有人在檢討到底為什麼嗎?

槍聲過後, 不需要很久 (通常是一周不到), 這件事就會像過去所有的震驚社會案件一樣雲淡風輕 ... 我們不知道為什麼會發生這樣的事, 政府不會就任何一個社會案件提出檢討, 所以這些悲劇其實也沒有甚麼教育意義, 政府認為槍斃死刑犯就算是給個交代了, 而受害者家屬心中的痛只能靠時間來稀釋 ...

今天看到小燈泡媽媽對鄭捷執行死刑的看法, 我深有同感 :

# 「他死掉了,然後呢?」小燈泡媽媽:國家有沒有剖析犯罪者的成因?

"小燈泡媽媽說,鄭捷很快地就執行死刑,意味著就這樣失去了一個研究與了解的對象,「他死掉了,然後呢?」,她認為國家是不是最少應該要給大家一個交代,好好剖析犯罪者以及整個事件的原因,給社會一個「理解」的機會,社會才有辦法走的更遠更好。
唯有當我們瞭解這個人是怎麼一步步走向犯罪,我們才有機會談改善、談預防」小燈泡媽媽認為,若不這樣做,就真的只是永遠活在一個未知的恐懼之中。"

# 回應小燈泡媽媽,羅瑩雪:她不了解才這麼說

"羅瑩雪說,研究跟他(鄭捷)在不在沒有關係,這種無差別殺人或隨機殺人的案件是很嚴重的事,所以法務部也有研究這種案子的計劃在。"

這種研究還真厲害 (這好比研究語言學的人, 說即使西夏語早已失傳, 仍然有辦法研究出西夏王趙元昊如何說 "我愛你" 這三個字, 佩服, 佩服).

另外受害者之一的陳家慧說的一段話, 也值得思考 : 

# 羅瑩雪下台9天前槍決鄭捷 受害者陳家慧:政治操弄

"比起執行死刑,國家更應該做的是教育及防範未然。陳家慧也強調,自己一直希望能去探監,深入研究鄭捷的想法,作為日後教育的參考依據,也許能避免類似的人格及事件再度出現。"

20160514 補充 :

今天看到這條新聞 :

# 小燈泡母不認同火速槍決 蔡正元批:她才是值得研究的心理疾病案例

"蔡正元12日在臉書上抒發己見,對於所謂「讓死刑犯活著,才可以研究為什麼犯下滔天大罪」的說法不以為然,因為包括台灣在內,世界各國對於死刑犯犯罪行為的相關研究報告早已汗牛充棟,「請廢死教徒更新一下說詞和論述吧!」"

我看了真是五味雜陳, 這個人是不是有點那個的哦. 人家小燈泡媽媽不是已經說明白了嗎?

"她不是不願意表態,而是真的沒有想透;如今,她不但沒有想得更清楚,反而想得更多,更難輕易選邊站。".

好像對於這件事只要不是講殺得好的人, 就是廢死那一邊的一樣. 我只是覺得, 執行了這麼多死刑, 為什麼還沒看到效果, 顯然真正的問題從來都沒有被解決, 慘絕人寰的刑事案件一再發生, 原因並非沒有死刑, 而可能是我們漏掉了甚麼. 除了依法律執行死刑外, 我們是否還需要做些甚麼才能避免這種事情再發生, 如果是家庭教育出問題, 做父母的該怎麼做 (我們的教育裡面有所謂的父母學嗎?), 如果是出在學校教育, 老師對學生的行為偏差現象該怎麼做 (我們的輔導教育很成功嗎)? 如果問題是出在社會, 社會關懷體系有甚麼問題嗎? 只是想知道為什麼而已, 要更新甚麼說詞跟論述呢?

"包括台灣在內,世界各國對於死刑犯犯罪行為的相關研究報告早已汗牛充棟", 所以呢? 你的意思是 "投資一定有風險,基金投資有賺有賠,申購前應詳閱公開說明書"? 是嗎?

不管死刑應不應該存在 (這是社會共識問題), 悲劇總要讓我們得到些教訓吧!

PS : 笨蛋! 我們應該關心的是這個 :

# 防鄭捷殺人案重演 「家庭教育」是關鍵