2017年8月31日 星期四

如何防堵陌生的推特跟隨者

這兩天在測試用 MicroPython on ESP8266 傳送訊息到 Twitter 時發現, 我的推特帳號有一大堆跟隨者的推文源源不絕湧進來, 可以說每一秒都有, 幾乎全都是垃圾訊息, 害我必須在茫茫垃圾推海中尋找我的程式送出的推文, 實在很辛苦.

上谷歌搜尋解決辦法, 原來可以封鎖這些發送垃圾推文者, 點選推文後點右上角的三個點, 然後點選封鎖即可 :




但我發現這樣一個一個去封鎖根本不是辦法, 殺了一個小強還有千千萬萬個小強冒出來, 太多了, 封不勝封. 後來我在下面這篇文章中找到原因與解法了 :

https://www.plurk.com/p/lomp8k

原來是帳戶設定中有一個 "根據你的應用程式量身打造Twitter" 選項不知何時被勾選, 這會允許推特將適合的推文推到我的帳號裡. 只要將其取消勾選就可以徹底防堵這些雜七雜八的推文了.




這麼做之後果然有效擋掉全部垃圾推文了.

2017年8月30日 星期三

MicroPython on ESP8266 (二十一) : 使用 ThingTweet 傳送推文

去年在玩 Arduino + ESP8266 時曾測試過利用 ThingSpeak App 裡面的 ThingTweet 功能, 透過 ESP8266 連結 WiFi 從 Arduino 傳送推文到自己的推特帳號, 今天我想改用 MicroPython on ESP8266 來泡製一番.

在此之前必須先註冊一個推特帳號, 然後在其 App 功能的 ThingTweet 下授權 ThingSpeak 利用自己的推特帳號發送推文, 產生一個 API Key 放在 HTTPS 要求中讓 ThingTweet 知道要傳送到哪一個推特帳號, 過程參考 :

使用 ESP8266 傳送 Twitter 訊息

本系列 MicroPython on ESP8266 測試文章參考 :

MicroPython on ESP8266 (一) : 燒錄韌體
MicroPython on ESP8266 (二) : 數值型別測試
MicroPython on ESP8266 (三) : 序列型別測試
MicroPython on ESP8266 (四) : 字典與集合型別測試
MicroPython on ESP8266 (五) : WiFi 連線與 WebREPL 測試
MicroPython on ESP8266 (六) : 檔案系統測試
MicroPython on ESP8266 (七) : 時間日期測試
MicroPython on ESP8266 (八) : GPIO 測試
MicroPython on ESP8266 (九) : PIR 紅外線移動偵測
MicroPython v1.9.1 版韌體測試
MicroPython on ESP8266 (十) : socket 模組測試
MicroPython on ESP8266 (十一) : urllib.urequest 模組測試
MicroPython on ESP8266 (十二) : urequests 模組測試
MicroPython on ESP8266 (十三) : DHT11 溫溼度感測器測試
MicroPython 使用 ampy 突然無法上傳檔案問題
MicroPython on ESP8266 (十四) : 網頁伺服器測試
WeMOS D1 Mini 開發板測試
MicroPython on ESP8266 (十五) : 光敏電阻與 ADC 測試
MicroPython on ESP8266 (十六) : 蜂鳴器測試
MicroPython on ESP8266 (十七) : 液晶顯示器 1602A 測試
MicroPython on ESP8266 (十八) : SSD1306 液晶顯示器測試
MicroPython v1.9.2 版釋出
MicroPython on ESP8266 (十九) : 太陽能測候站與移動偵測控制照明
MicroPython on ESP8266 (二十) : 從 ThingSpeak 讀取資料

MicroPython 文件參考 :

MicroPython tutorial for ESP8266  (官方教學)
http://docs.micropython.org/en/latest/micropython-esp8266.pdf
http://docs.micropython.org/en/latest/pyboard/library/usocket.html#class-socket
http://docs.micropython.org/en/v1.8.7/esp8266/library/usocket.html#module-usocket
https://gist.github.com/xyb/9a4c8d7fba92e6e3761a (驅動程式)


下面的 Python 程式碼複製了上面這篇文章的測試, 每分鐘向自己推特帳號傳送 'Hello! xx times.' 的推文. 注意, 推文有如下限制 :
  1. 推送週期必須大於 1 分鐘
  2. 前後兩篇推文內容不可相同
因此在下面測試中我用增量變數 i 來讓前後篇推文前後內容不同.


測試 1 : 每分鐘發送 'Hello! xx times' 推文 (GET 方法)

import urequests
import time

host='https://api.thingspeak.com'
api_key='QC73W06AWWX27AW1'
i=1

while True:
    msg='Hello! %d times.' %(i)
    url='%s/apps/thingtweet/1/statuses/update?api_key=%s&status=%s' \
         %(host,api_key,msg)
    try:
        r=urequests.get(url)
        print(type(r.text))
        if r.text == '1':
            print('Message is successfully sent.')
        else:
            print('Message-sending failed.')
    except:
        print('urequests.post() exception occurred!')
    i=i+1
    time.sleep(60)



注意, 這裡 urequests() 的傳回值 r 是一個物件, 其 text 屬性類型是 str (字串), 若傳送成功會傳回 '1', 可用來判別是否傳送成功, 例如 :

PYB: soft reboot
#11 ets_task(40100164, 3, 3fff837c, 4)
Connecting to AP ...
Connected:IP= 192.168.43.252
<class 'str'>
Message is successfully sent.

如果傳送失敗, text 屬性會傳回錯誤原因字串, 例如 :

PYB: soft reboot
#10 ets_task(40100164, 3, 3fff837c, 4)
Connecting to AP ...
Connected:IP= 192.168.43.252
Invalid API Key
Message-sending failed.

以上是使用 GET 方法提出 HTTPS 要求, 在 ThingTweet 網頁右下角的範例使用的是 POST 方法, 其 URL 為 "https://api.thingspeak.com/apps/thingtweet/1/statuses/update", 傳送參數為 api_key 與 status (即要傳送之訊息), 我參考下列這篇傳送 POST 要求 :

MicroPython on ESP8266 (十二) : urequests 模組測試


測試 2 : 每分鐘發送 'Hello! xx times' 推文 (POST 方法)     

#myapp.py
import urequests
import time

url='https://api.thingspeak.com/apps/thingtweet/1/statuses/update'
i=1

while True:
    msg='Hello! %d times.' %(i)
    data={'api_key':'QC73W06AWWX27AW1','status':msg}
    try:
        r=urequests.post(url,json=data)
        print(r.text)
        if r.text == '1':
            print('Message is successfully sent.')
        else:
            print('Message-sending failed.')
    except:
        print('urequests.post() exception occurred!')
    i=i+1
    time.sleep(60)

但測試結果卻是失敗, 原因是 Invalid API Key, 但我確定 key 是正確無誤的, why?


2017-08-30 補充 :


今天我參考了下面這篇文章修改上面測試 2 程式 :

HTTP POST with Micropython and an ESP8266

除了 body 有送參數外之外, 在 headers 中也傳送 api_key, 同時參數改用字串的 data 傳遞, 如下所示 :


測試 3 : 每分鐘發送 'Hello! xx times' 推文 (POST 方法)


import urequests
import time

url='https://api.thingspeak.com/apps/thingtweet/1/statuses/update'
api_key='BCXIQK4OZO55NP22'
headers={'api_key':api_key,'content-type':'application/json'}
i=1

while True:
    msg='Hello! %d times.' %(i)
    data="{'api_key':'%s','status':'%s'}" % (api_key,msg)
    print(data)
    try:
        r=urequests.post(url,data=data,headers=headers)
        print(r.text)
        if r.text == '1':
            print('Message is successfully sent.')
        else:
            print('Message-sending failed.')
    except:
        print('urequests.post() exception occurred!')
    i=i+1
    time.sleep(30)

但這樣仍然是不行, why?  

ó#5 ets_task(40100164, 3, 3fff837c, 4)
Connecting to AP ...
Connected:IP= 192.168.43.252
{'x-ha-access':'BCXIQK4OZO55NP22','status':'Hello! 1 times.'}

Message-sending failed.
{'x-ha-access':'BCXIQK4OZO55NP22','status':'Hello! 2 times.'}


我比較了 "HTTP POST with Micropython and an ESP8266" 這篇文章中的參數寫法, 發現差別是 data 參數中的屬性與值採用雙引號, 外面標示字串用的是單引號, 上面的測試 3 則是相反, 我抱著姑且一試的心情外單內雙, 測試結果 ~~~  It work!  程式如下 :


測試 4 : 每分鐘發送 'Hello! xx times' 推文 (POST 方法 : 用 data 參數)

import urequests
import time

url='https://api.thingspeak.com/apps/thingtweet/1/statuses/update'
api_key='BCXIQK4OZO55NP22'
headers={'api_key':api_key,'content-type':'application/json'}
i=1

while True:
    msg='Hello! %d times.' %(i)
    data='{"api_key":"%s","status":"%s"}' % (api_key,msg)    
    try:
        r=urequests.post(url,data=data,headers=headers)
        print(r.text)
        if r.text == '1':
            print('Message is successfully sent.')
        else:
            print('Message-sending failed.')
    except:
        print('urequests.post() exception occurred!')
    i=i+1
    time.sleep(60)

REPL 介面輸出正常 :


PYB: soft reboot
#9 ets_task(40100164, 3, 3fff837c, 4)
Connecting to AP ...
Connected:IP= 192.168.43.252
1
Message is successfully sent.

原來一切是引號搞的鬼!  那用 json 參數傳遞只要用雙引號也是 OK 的, 如下所示 :


測試 5 : 每分鐘發送 'Hello! xx times' 推文 (POST 方法 : 用 json 參數)


import urequests
import time

url='https://api.thingspeak.com/apps/thingtweet/1/statuses/update'
api_key='BCXIQK4OZO55NP22'
headers={'api_key':api_key,'content-type':'application/json'}
i=1

while True:
    msg='Hello! %d times.' %(i)
    data={"api_key":api_key,"status":msg}
    try:
        r=urequests.post(url,json=data,headers=headers)
        print(r.text)
        if r.text == '1':
            print('Message is successfully sent.')
        else:
            print('Message-sending failed.')
    except:
        print('urequests.post() exception occurred!')
    i=i+1
    time.sleep(60)

我試著將 headers 參數拿掉, 結果不行, 所以總結以上測試結果, 可以得到以下兩個結論 :

  1. 使用 post 方法時必須同時在 header 與 body 中傳送參數
  2. 參數不論是用字典物件 (json) 或字串 (data) 傳送, 屬性與值必須用雙引號

這項測試也讓我對 urequests 模組的運用有了更深的了解. 

2017年8月29日 星期二

鄉村基礎木作第六課 : 完成門板與抽屜把手

上週六興沖沖跑去木樂園, 發現大門深鎖, 查臉書課表才知休息日, 出發前應該先查才不會白跑一趟. 結果上週都沒去上到半堂課. 今天天氣不錯, 吃過晚飯決定去學木工, 順便完成春嬌的委託~幫她切割書櫥隔板.

今天劉老師指派郭老師來指導我, 人很親切, 我問怎以前都沒見過你? 他說前陣子去南投受訓, 都已經是木工師傅了還是精益求精哩.

上次做到收納櫃門框, 上膠固定後用夾子夾緊, 劉老師說第二天他會幫我取下夾子, 我在料件堆找到了我的門框, 先用修邊機在門框內緣分兩次铣出 6 分深的槽用來安裝較薄的門板, 這是類似鐵皮浪板有榫可前後接合的實木薄板.


铣好槽邊後要用鑿子將四個圓角鑿成垂直角才能塞下門板. 另一個做法是將門板四個角修成圓形也可以, 只要能塞下去即可. 完成後即可在铣出的槽邊上膠, 然後再用蚊子釘固定.



然後在門框上左邊適當位置畫上十字線鑽孔, 裝上門把用螺絲鎖緊. 最後是拿出以前完成抽屜同樣在前板中心鑽孔, 用長螺絲鎖上把手.


上面那兩個小木塊是要裝在底板下面左右兩側裝飾用的, 上一堂課劉老師教我用手持線鋸機製作的成品實在不及格, 因為這工具有點重, 不熟練會因緊張而肌肉緊繃, 就鋸得歪七扭八. 今天一上課郭老師就再次要我多練習幾次, 果然在鋸掉三塊練習板後就弄出還算滿意的作品了.

2017年8月28日 星期一

MicroPython on ESP8266 (二十) : 從 ThingSpeak 讀取資料

昨天完成 MicroPython on ESP8266 的太陽能測候站後, 可以在電腦上連線 ThingSpeak 網站遠端觀察 ESP8266 傳遞上來的數據, 也可以在手機上安裝 ThingView 這個 App 來隨時檢視監測數據, 關於 ThingView 參考 :

MicroPython on ESP8266 (十三) : DHT11 溫溼度感測器測試

回高雄的途中突然想到, ThingSpeak 上除了 Write API Key 外, 還有 Read API Key, 亦即可以從遠端設備直接讀取 ThingSpeak 資料庫上儲存的通道數據. 我的想法是用 ESP8266 模組讀取 ThingSpeak 數據後將資料顯示在 1602 LCD 或 SSD1306 OLED 顯示器上, 這樣便毋須開啟電腦或拿出手機檢視了. 在 ThingSpeak 網站的 Channels/API Keys 裡可查得通道的 Channel ID 與 Read API Keys (可自行建立多個讀取 API Key) :




ThingSpeak 的寫入週期免費用戶限制最快 15 秒一次, 付費用戶則是每秒一次, 超過限制會產生錯誤而無法寫入. 不過讀取週期卻對任何用戶而言都沒有限制, 參考 :

https://www.mathworks.com/help/thingspeak/channel-settings.html#keys

"API Rate Limits
A free user can update a ThingSpeak channel every 15 seconds, and a paid user can update every 1 second. Updating more frequently results in an error. The time between read requests is not limited by ThingSpeak for any users. To change settings to meet your application requirements locally, download the source code from GitHub."

以下記錄測試過程, 這次我使用 WeMOS D1 Mini 搭配 SSD1306 OLED 顯示器. 本系列 MicroPython on ESP8266 測試文章參考 :

MicroPython on ESP8266 (一) : 燒錄韌體
MicroPython on ESP8266 (二) : 數值型別測試
MicroPython on ESP8266 (三) : 序列型別測試
MicroPython on ESP8266 (四) : 字典與集合型別測試
MicroPython on ESP8266 (五) : WiFi 連線與 WebREPL 測試
MicroPython on ESP8266 (六) : 檔案系統測試
MicroPython on ESP8266 (七) : 時間日期測試
MicroPython on ESP8266 (八) : GPIO 測試
MicroPython on ESP8266 (九) : PIR 紅外線移動偵測
MicroPython v1.9.1 版韌體測試
MicroPython on ESP8266 (十) : socket 模組測試
MicroPython on ESP8266 (十一) : urllib.urequest 模組測試
MicroPython on ESP8266 (十二) : urequests 模組測試
MicroPython on ESP8266 (十三) : DHT11 溫溼度感測器測試
MicroPython 使用 ampy 突然無法上傳檔案問題
MicroPython on ESP8266 (十四) : 網頁伺服器測試
WeMOS D1 Mini 開發板測試
MicroPython on ESP8266 (十五) : 光敏電阻與 ADC 測試
MicroPython on ESP8266 (十六) : 蜂鳴器測試
MicroPython on ESP8266 (十七) : 液晶顯示器 1602A 測試
MicroPython on ESP8266 (十八) : SSD1306 液晶顯示器測試
MicroPython v1.9.2 版釋出
MicroPython on ESP8266 (十九) : 太陽能測候站與移動偵測控制照明

MicroPython 文件參考 :

MicroPython tutorial for ESP8266  (官方教學)
http://docs.micropython.org/en/latest/micropython-esp8266.pdf
http://docs.micropython.org/en/latest/pyboard/library/usocket.html#class-socket
http://docs.micropython.org/en/v1.8.7/esp8266/library/usocket.html#module-usocket
https://gist.github.com/xyb/9a4c8d7fba92e6e3761a (驅動程式)

實際做法我參考了下面這篇文章 :

How to read data from a Thingspeak channel using a Raspberry Pi | Raspberry Pi | Forum

此文是針對 Raspberry Pi 上的 CPython, 使用 urllib2 模組來處理 HTTP 協定; 我則是偏好使用 requests (在 MicroPython 上為 urequests) 模組, 我只參考了 API 的 URL 格式 :

GET https://api.thingspeak.com/channels/58096/feeds/last.json?api_key=PWEAKSDX9X9QP1V6

這個 Query 字串中的關鍵資訊是黃底部分的 Channel ID 與 Read API Key, 預期 ThingSpeak 伺服器將傳回最近一筆寫入數據. 其他 Feeds 的擷取方法參考 :

https://www.mathworks.com/help/thingspeak/get-a-channel-feed.html


測試 1 : 讀取 ThingSpeak 通道資料

#myapp.py
import urequests
import time

host='http://api.thingspeak.com'
read_api_key='PWEAKSDX9X9QP1V6'
channel_id='58096'

url='%s/channels/%s/feeds/last.json?api_key=%s' \
     %(host, channel_id, read_api_key)

while True:
    try:
        r=urequests.get(url)
        print(r.text)
        print(r.json())
    except:
        print('urequests.get() exception occurred!')
    time.sleep(3)

將此檔案存成 myapp.py, 用 ampy 傳 ESP8266 後開啟 PuTTY 按 Ctrl+D 軟開機即可. 連接 WiFi 工作交由 WiFi 連網程式 main.py 負責, 參考下面這篇文章中的 "3.  SSD1306 OLED 版 AP 設定程式 " :

WeMOS D1 Mini 開發板測試

在上面的程式中, 我以 3 秒週期去 ThingSpeak 讀取指定通道之數據, 分別印出回應物件的 text 屬性與 json() 方法的傳回值, 兩者之差別是 text 屬性為字串, 而 json() 傳回的是 JSON 資料轉成的字典物件, 參考 :

MicroPython on ESP8266 (十二) : urequests 模組測試

REPL 輸出如下 :

PYB: soft reboot
#9 ets_task(40100164, 3, 3fff829c, 4)
WebREPL is not configured, run 'import webrepl_setup'
Connecting to AP ...
Connected:IP= 192.168.43.252
{"created_at":"2017-08-28T03:03:22Z","entry_id":75291,"field1":"30","field2":"86","field3":"39","field4":"4","field5":"0"}
{'created_at': '2017-08-28T03:03:22Z', 'field5': '0', 'entry_id': 75291, 'field3': '39', 'field2': '86', 'field1': '30', 'field4': '4'}
{"created_at":"2017-08-28T03:03:22Z","entry_id":75291,"field1":"30","field2":"86","field3":"39","field4":"4","field5":"0"}
{'created_at': '2017-08-28T03:03:22Z', 'field5': '0', 'entry_id': 75291, 'field3': '39', 'field2': '86', 'field1': '30', 'field4': '4'}
{"created_at":"2017-08-28T03:03:22Z","entry_id":75291,"field1":"30","field2":"86","field3":"39","field4":"4","field5":"0"}
{'created_at': '2017-08-28T03:03:22Z', 'field5': '0', 'entry_id': 75291, 'field3': '39', 'field2': '86', 'field1': '30', 'field4': '4'}
{"created_at":"2017-08-28T03:03:22Z","entry_id":75291,"field1":"30","field2":"86","field3":"39","field4":"4","field5":"0"}
{'created_at': '2017-08-28T03:03:22Z', 'field5': '0', 'entry_id': 75291, 'field3': '39', 'field2': '86', 'field1': '30', 'field4': '4'}

由於在上一篇測試 "太陽能測候站與移動偵測控制照明" 中是以 20 秒的週期將測候站量得之數據寫入 ThingSpeak 資料庫, 而此處卻以 3 秒週期讀取最新一筆寫入資料, 所以會連續讀到好幾筆重複的資料. 同時也可看出 text 屬性因為是字串, 鍵與值都用雙引號; 而 json() 傳回值 (字典) 則以單引號括起來. 傳回的每一筆資料包括下列欄位 :
  1. created_at : 數據寫入日期與時間 (注意, 這是 GMT 時間)
  2. entry_id : 紀錄的 ID
  3. field1 : 攝氏溫度
  4. field2 : 華氏溫度
  5. field3 : 濕度
  6. field4 : 亮度
  7. field5 : PIR 觸發次數


既然 json() 傳回值是 dict 物件, 那麼就可以透過鍵來取得其值, 如下列測試 2 所示 :

測試 2 : 讀取 ThingSpeak 通道資料 (取得字典內各欄位之值)

#myapp.py
import urequests
import time

host='http://api.thingspeak.com'
read_api_key='PWEAKSDX9X9QP1V6'
channel_id='58096'

url='%s/channels/%s/feeds/last.json?api_key=%s' \
     %(host, channel_id, read_api_key)

while True:
    try:
        r=urequests.get(url)
        d=r.json()
        print(d['created_at'],d['entry_id'],d['field1'],d['field2'],
              d['field3'],d['field4'],d['field5'])
    except:
        print('urequests.get() exception occurred!')
    time.sleep(10)

此處我將讀取週期改為 10 秒, 而數據來源端寫入週期為 20 秒, 因此將能大幅降低讀到多筆重複資料情形, 預期頂多重複一次, REPL 輸出如下 :

PYB: soft reboot
#9 ets_task(40100164, 3, 3fff829c, 4)
WebREPL is not configured, run 'import webrepl_setup'
Connecting to AP ...
Connected:IP= 192.168.43.252
2017-08-28T06:56:33Z 75985 32 90 35 3 0
2017-08-28T06:56:54Z 75986 32 90 35 4 0
2017-08-28T06:56:54Z 75986 32 90 35 4 0
2017-08-28T06:57:14Z 75987 32 90 35 4 0
2017-08-28T06:57:34Z 75988 32 90 38 3 0
2017-08-28T06:57:34Z 75988 32 90 38 3 0
2017-08-28T06:57:54Z 75989 32 90 35 4 0
2017-08-28T06:57:54Z 75989 32 90 35 4 0
2017-08-28T06:58:14Z 75990 32 90 35 4 0
2017-08-28T06:58:14Z 75990 32 90 35 4 0
2017-08-28T06:58:34Z 75991 32 90 36 4 0
2017-08-28T06:58:34Z 75991 32 90 36 4 0
2017-08-28T06:58:54Z 75992 33 91 34 3 0
2017-08-28T06:58:54Z 75992 33 91 34 3 0
2017-08-28T06:59:14Z 75993 32 90 38 4 0
2017-08-28T06:59:14Z 75993 32 90 38 4 0
2017-08-28T06:59:35Z 75994 32 90 35 3 0
2017-08-28T06:59:55Z 75995 32 90 35 3 0
2017-08-28T06:59:55Z 75995 32 90 35 3 0
2017-08-28T07:00:15Z 75996 32 90 35 3 0
2017-08-28T07:00:15Z 75996 32 90 35 3 0
2017-08-28T07:00:35Z 75997 32 90 36 3 0

沒錯, 重複次數最多一次.

接下來要將自 ThingSpeak 取得的數據顯示在 SSD1306 OLED 顯示器, 作法參考下面這篇 :

MicroPython on ESP8266 (十八) : SSD1306 液晶顯示器測試

D1 Mini 或 ESP-12 模組與 SSD1306 的連接方式如下 :


SSD1306 使用 I2C 介面, D1 Mini 使用 D1 接 SCL, D2 接 SDA, 但在建立 I2C 物件時必須傳入 D1 與 D2 腳相對應的 GPIO5 與 GPIO4.

使用 SSD1306 需先下載其 MicroPython 驅動程式 ssd1306.py 並用 ampy 上傳 ESP8266 :

micropython/drivers/display/ssd1306.py  (MicroPython 官網)

下面程式是參考 "SSD1306 液晶顯示器測試" 修改測試 2 而得 :


測試 3 : 讀取 ThingSpeak 通道資料並顯示於 OLED 顯示器 (GMT 時間)

import urequests
import time
import ssd1306  
from machine import Pin, I2C

def fill_blank(n):      
    if n<10:
        return ' ' + str(n)
    else:
        return str(n)

i2c=I2C(scl=Pin(5), sda=Pin(4))              
oled=ssd1306.SSD1306_I2C(128, 32, i2c)

host='http://api.thingspeak.com'
read_api_key='PWEAKSDX9X9QP1V6'
channel_id='58096'

url='%s/channels/%s/feeds/last.json?api_key=%s' \
     %(host, channel_id, read_api_key)

while True:
    try:
        r=urequests.get(url)
    except:
        print('urequests.get() exception occurred!')
    j=r.json()
    print(j['created_at'],j['entry_id'],j['field1'],j['field2'],
          j['field3'],j['field4'],j['field5'])

    YMD,HmS=j['created_at'].split('T')      #以 'T' 拆分日期時間
    HmS=HmS.replace('Z','')                       #去除時間結尾的 'Z'
    T='%sC' % (fill_blank(int(j['field1'])))  
    H='%sH' % (fill_blank(int(j['field3'])))  
    L='%s%%' % (fill_blank(int(j['field4'])))  

    line1='%s' % (YMD)  
    line2='%s' % (HmS)  
    line3='%s %s %s' % (T, H, L)  
    line4='Trigger:%s' % (j['field5'])  
    oled.fill(0)                
    oled.text(line1, 0, 0)    
    oled.text(line2, 0, 8)    
    oled.text(line3, 0, 16)  
    oled.text(line4, 0, 24)  
    oled.show()    
    time.sleep(10)

上面程式中我直接將 'created_at' 欄位裡的日期時間字串以 'T' 為界拆出日期與時間, 然後將溫溼度亮度字串轉成整數後丟給 fill_blank() 製作成固定兩字元字串, 以免個位數時顯示位置位移. 注意, 上面的日期時間是 GMT 時間, 亦即數據來源寫入 ThingSpeak 資料庫時的 GMT 時間. 如果要改為台灣時間可參考 "SSD1306 液晶顯示器測試" 的測試 4 作法 :


測試 4 : 讀取 ThingSpeak 通道資料並顯示於 OLED 顯示器 (台灣時間)

import urequests
import time
import ssd1306
from machine import Pin, I2C

def fill_zero(n):  
    if n < 10:  
        return '0' + str(n)
    else:  
        return str(n)

def fill_blank(n):    
    if n<10:
        return ' ' + str(n)
    else:
        return str(n)

week={0:'Mon',1:'Tue',2:'Wed',3:'Thu',4:'Fri',5:'Sat',6:'Sun'}

i2c=I2C(scl=Pin(5), sda=Pin(4))              
oled=ssd1306.SSD1306_I2C(128, 32, i2c)

host='http://api.thingspeak.com'
read_api_key='PWEAKSDX9X9QP1V6'
channel_id='58096'

url='%s/channels/%s/feeds/last.json?api_key=%s' \
     %(host, channel_id, read_api_key)

while True:
    try:
        r=urequests.get(url)
    except:
        print('urequests.get() exception occurred!')
    j=r.json()
    print(j['created_at'],j['entry_id'],j['field1'],j['field2'],
          j['field3'],j['field4'],j['field5'])

    DT=j['created_at'].replace('T','-')  
    DT=DT.replace('Z','')     #去除時間結尾的 'Z'
    DT=DT.replace(':','-')     #將時間的 ':' 全改為 '-' 以利 split()
    DT=DT + '-0-0'              #因 time.mktime() 需 8 個整數參數故補兩個 0
    DT=DT.split('-')              #拆成 6 元素的串列
    DT=[int(i) for i in DT]   #因 time.mktime() 需 8 個整數參數
    Y,M,D,H,m,S,W,DY=time.localtime(time.mktime(DT) + 28800)    #轉成台灣時間
    YMD='%s-%s-%s' % (str(Y),fill_zero(M),fill_zero(D))  
    WD=week[W]      
    HmS='%s:%s:%s' % (fill_zero(H),fill_zero(m),fill_zero(S))  
    T='%sC' % (fill_blank(int(j['field1'])))
    H='%sH' % (fill_blank(int(j['field3'])))  
    L='%s%%' % (fill_blank(int(j['field4'])))  

    line1='%s %s' % (YMD, WD)
    line2=HmS
    line3='%s %s %s' % (T, H, L)
    line4='Trigger:%s' % (j['field5'])
    oled.fill(0)              
    oled.text(line1, 0, 0)  
    oled.text(line2, 0, 8)  
    oled.text(line3, 0, 16)
    oled.text(line4, 0, 24)
    oled.show()
    time.sleep(10)

此程式將 ThingSpeak 紀錄的 GMT 日期時間字串修整後, 在後面補上兩個 0 拆成 8 元素的串列後傳給 time.maketime() 轉成自 2000/1/1 以來的時戳 (秒), 再加上 28800 秒 (8 小時) 即為台灣時間之時戳, 傳給 time.localtime() 會傳回該時戳對應之日期與時間串列. 由於 MicroPython 沒有實作 strptime() 函數, 所以只好用上面這個方法來轉換.

以上是在 SSD1306 OLED 上顯示從 ThingSpeak 讀取之資料, 如果要在 1602 LCD 上顯示, 可參考 "液晶顯示器 1602A 測試" 這篇的作法. 使用 1602 前須先下載 esp8266_i2c_lcd.py 與 lcd_api.py 這兩個 Python 驅動程式, 下載後用 ampy 上傳到 ESP8266 即可.

將上面測試 4 程式修改為 1602 版如下 :

測試 5 : 讀取 ThingSpeak 通道資料並顯示於 1602 LCD 顯示器 (台灣時間)

import urequests
import time
from machine import Pin, I2C
from esp8266_i2c_lcd import I2cLcd

def fill_zero(n):
    if n < 10:
        return '0' + str(n)
    else:
        return str(n)

def fill_blank(n):    
    if n<10:
        return ' ' + str(n)
    else:
        return str(n)

i2c=I2C(scl=Pin(5),sda=Pin(4),freq=400000)
lcd=I2cLcd(i2c, 0x27, 2, 16)  
lcd.custom_char(0, bytearray([0x1C,0x14,0x1C,0x00,0x00,0x00,0x00,0x00]))    

host='http://api.thingspeak.com'
read_api_key='PWEAKSDX9X9QP1V6'
channel_id='58096'

url='%s/channels/%s/feeds/last.json?api_key=%s' \
     %(host, channel_id, read_api_key)

while True:
    try:
        r=urequests.get(url)
    except:
        print('urequests.get() exception occurred!')
    j=r.json()
    print(j['created_at'],j['entry_id'],j['field1'],j['field2'],
          j['field3'],j['field4'],j['field5'])

    DT=j['created_at'].replace('T','-')
    DT=DT.replace('Z','')
    DT=DT.replace(':','-')
    DT=DT + '-0-0'
    DT=DT.split('-')
    DT=[int(i) for i in DT]
    Y,M,D,H,m,S,W,DY=time.localtime(time.mktime(DT) + 28800)
    YMD='%s-%s-%s' % (str(Y),fill_zero(M),fill_zero(D))
    HmS='%s:%s:%s' % (fill_zero(H),fill_zero(m),fill_zero(S))
    T='%s%s' % (fill_blank(int(j['field1'])),chr(0))  
    H='%sH' % (fill_blank(int(j['field3'])))
    L='%s%%' % (fill_blank(int(j['field4'])))
    Trigger=j['field5']

    lcd.move_to(0, 0)
    lcd.putstr(YMD)
    lcd.move_to(11, 0)    
    lcd.putstr(L)    
    lcd.move_to(15, 0)  
    lcd.putstr(Trigger)  
    lcd.move_to(0, 1)    
    lcd.putstr(HmS)
    lcd.move_to(9, 1)  
    lcd.putstr(T)
    lcd.move_to(13, 1)
    lcd.putstr(H)
    time.sleep(10)




注意, 由於 1602 僅能顯示 16*2=32 個字元, 所以我將顯示資訊的位置重新做了規劃, 完整的時間 HmS 改在第二列開頭顯示, 溫度放在其後; 亮度改至日期後面, 而觸發次數放在第一列的最後一個字元.

2017年8月27日 星期日

2017 年第 34 周記事

越來越能適應一個人回鄉下的週末了, 雖然有時瞥見空蕩蕩的書房, 思緒又會跳躍到往日小狐狸們都回來時各自在書桌前做功課的時光. 跟同事談起離巢前期的憂鬱, 他說向前看吧! 老是向後看只會加速老化, 或許他說的是對的, 他比我早幾年進入孩子長大的世界.

這個周末集中精神終於完成 20W小型太陽能板的負載-太陽能測候站與移動偵測, 在返回高雄前的一小時做完最後測試與安裝, 接下來的一周就可以透過 ThingSpeak 物聯網服務觀察所收集的資料. 我發現直接使用 ESP8266 連網似乎比 Arduino + ESP8266 還要穩一些, 但還得看未來一周是否都能維持穩定運轉才能下定論.

今天去市場後順路進去圖書館逛逛, 但只搜到下面這本  :
  1.  Processing 入門-互動式圖形實作介紹
書後有提到與 Arduino 介接的方法, 有空來玩一下, 或許可以用來製作超音波雷達的顯示幕, 哈! 又多了個新玩具了.

2017年8月26日 星期六

MicroPython on ESP8266 (十九) : 太陽能測候站與移動偵測控制照明

早上興沖沖地跑去木樂園要上第五堂木作課, 九點準時到門口才發現鐵門緊閉, 打開手機查一下臉書才知道今天休息啦! 出來前應該先查一下才對. 無奈只好回家做太陽能測候站與移動偵測的實驗囉, 先做好再帶回鄉下, 這樣就不需要帶零件箱回去.

這實驗可以說是對過去四個月學習 MicroPython on ESP8266 的小結, 綜合光敏電阻的亮度測量, DHT11 的溫濕度測量, 以及 PIR 紅外線移動偵測控制照明, 然後將測量資料透過網路傳送到 ThingSpeak 物聯網網站記錄起來.

供電來源為今年初在舊豬舍廁所屋頂裝置的 20W 小型太陽能板, 經過充電控制器向一顆湯淺 7 AH 蓄電池充電, 充電控制器輸出端經過一個降壓模組將 12~13V 直流轉成 5V 電源. 主控裝置是 ESP-12E 開發板, 所需模組與零組件列之如下 :
  1. esp-12 開發板/D1 Mini 開發板/NodeMCU 開發板
  2. DHT11 溫溼度感測器
  3. CdS 光敏電阻與 10K 電阻
  4. PIR 紅外線移動偵測模組 HC-SR501
  5. 1 路繼電器
  6. 12V-5V 降壓模組
  7. 12V LED 照明燈
  8. 小型麵包板
  9. 杜邦線 
注意, 由於 ESP-12E 的 ADC 腳最高飽和電壓為 1V, 因此需使用 220+100 歐姆分壓電路從 100 歐姆端取出 1V 電壓, 再饋給 CdS 與 10K 歐姆組成的分壓電路, 從 10K 端取得與亮度成正比的壓降再接到 ESP-12E 的 ADC 腳 :


但如果使用 WeMOS D1 Mini 開發板的話就不需要 220+100 歐姆的分壓電路, 直接將 CdS+10K 歐姆的分壓電路接到 3.3V 電源即可, 因為 D1 Mini 的 ADC 腳飽和電壓是 3.3V 而非 1V.

另外, 由於繼電器是 5V 驅動的, ESP8266 的 GPIO 是 3.3V, 無法驅動繼電器, 必須使用位準轉換器將 3.3V 轉成 5, 一個通道不需要用到模組, 因為模組通常是 4 或 8 個通道, 其他通道用不到就浪費了. 參考下列這篇, 只需要一個 MOSFET 2N7000 與兩個 4.7K 歐姆電阻即可做出單一通道的雙向位準轉換器 :

用 MOFFET 2N7000 做 5V 與 3.3V 位準轉換





本實驗主要參考下面一系列測試紀錄中底色為黃色的文章 :

MicroPython on ESP8266 (一) : 燒錄韌體
MicroPython on ESP8266 (二) : 數值型別測試
MicroPython on ESP8266 (三) : 序列型別測試
MicroPython on ESP8266 (四) : 字典與集合型別測試
MicroPython on ESP8266 (五) : WiFi 連線與 WebREPL 測試
MicroPython on ESP8266 (六) : 檔案系統測試
MicroPython on ESP8266 (七) : 時間日期測試
MicroPython on ESP8266 (八) : GPIO 測試
MicroPython on ESP8266 (九) : PIR 紅外線移動偵測
MicroPython v1.9.1 版韌體測試
MicroPython on ESP8266 (十) : socket 模組測試
MicroPython on ESP8266 (十一) : urllib.urequest 模組測試
MicroPython on ESP8266 (十二) : urequests 模組測試
MicroPython on ESP8266 (十三) : DHT11 溫溼度感測器測試
MicroPython 使用 ampy 突然無法上傳檔案問題
MicroPython on ESP8266 (十四) : 網頁伺服器測試
WeMOS D1 Mini 開發板測試
MicroPython on ESP8266 (十五) : 光敏電阻與 ADC 測試
MicroPython on ESP8266 (十六) : 蜂鳴器測試
MicroPython on ESP8266 (十七) : 液晶顯示器 1602A 測試
MicroPython on ESP8266 (十八) : SSD1306 液晶顯示器測試
MicroPython v1.9.2 版釋出

MicroPython 文件參考 :

MicroPython tutorial for ESP8266  (官方教學)
http://docs.micropython.org/en/latest/micropython-esp8266.pdf
http://docs.micropython.org/en/latest/pyboard/library/usocket.html#class-socket
http://docs.micropython.org/en/v1.8.7/esp8266/library/usocket.html#module-usocket
https://gist.github.com/xyb/9a4c8d7fba92e6e3761a (驅動程式)


測試 1 : 太陽能測候站 + 使用 PIR 延時控制 LED 照明燈

from machine import Pin,ADC
import dht
import time
import urequests

PIRPIN=Pin(4, Pin.IN)
RELAYPIN=Pin(5, Pin.OUT, Pin.PULL_UP)
RELAYPIN.value(1)
LAMP_state=0
triggerCount=0    
triggerLimit=5

def IRQ(p):                      
    global state                
    global triggerCount    
    global triggerLimit      
    global LAMP_state
    LAMP_state=1
    RELAYPIN.value(0)
    if triggerCount < triggerLimit:      
        triggerCount=triggerCount + 1
    print(p,"IRQ Triggered! Trigger Count:",triggerCount,'LAMP State:',LAMP_state)

PIRPIN.irq(trigger=Pin.IRQ_RISING, handler=IRQ)

def LED_blink(pin,s):
    for i in range(1,s):
        pin.value(1)
        time.sleep_ms(500)
        pin.value(0)
        time.sleep_ms(500)

DHTPIN=Pin(0, Pin.IN)
LEDPIN=Pin(2, Pin.OUT)
d=dht.DHT11(DHTPIN)
adc=ADC(0)

host='http://api.thingspeak.com'
api_key='NO5N8C7T2KINFCQE'

while True:
    if LAMP_state==1:
        RELAYPIN.value(0)
        triggerCount=triggerCount - 1
        if triggerCount <= 0:                    
            LAMP_state=0
            triggerCount=0
    else:
        RELAYPIN.value(1)
    print("IRQ Trigger Count:",triggerCount,'LAMP State:',LAMP_state)

    try:      
        d.measure()              
        t=d.temperature()    
        f=round(t * 9/5 + 32)
        h=d.humidity()
        a=adc.read()
        a=int(0.3*a + 0.7*adc.read())
        a=round(a*100/1024)
        url='%s/update?api_key=%s&field1=%s&field2=%s&field3=%s&field4=%s&field5=%d' %(host, api_key, t, f, h, a, triggerCount)
        print('Temperature=', t, 'C', '/', f, 'F', 'Humidity=', h, '%', 'Luminance=', a, '%')
        r=urequests.get(url)
        print('response=', r.text)
    except:
        print('urequests.get() exception occurred!')

    LED_blink(LEDPIN,20)





此程式中每個迴圈改為 20 秒 (加上前面的處理時間大約 21 秒), 所以每分鐘約傳送 5 次資料給 ThingSpeak, 每小時 300 次, 每天傳送 7200 次. 由於迴圈時間約 20 秒, 因此在 PIR 觸發中斷時須馬上點亮 LED 燈, 否則最糟情況是在迴圈一開始時觸發, 還要等 20 秒才點亮 LED 燈, 這樣就不實用了. 此外, 由於這次使用的繼電器是 LOW 觸發, 所以點亮燈須對 GPIO 輸出 0 : RELAYPIN.value(0); 反之熄滅是輸出 1 : RELAYPIN.value(1), 與一般邏輯剛好相反.

由於 triggerLimit 設為 5, 因此碰頂後若 PIR 未再偵測到人體移動, 理論上經過 5 次迴圈後, triggerCount 就會歸零, 使得 LED 燈熄滅, 大約延時 5*20=100 分鐘, 即一分半鐘內未再觸發就會熄燈, 欲延長時間可加大 triggerLimit 設定值.

上面測試 1 沒有考慮時間因素, 亦即不論白天黑夜 LED 照明燈都會在感應到有人經過時點亮, 其實白天並不需要, 這樣會浪費蓄電池的電量. 在下面的測試 2 裡, 我加上了時間控制功能, 利用 NTP 伺服器取得現在本地的時間, 如果是傍晚 6 點後或者早上 6 點之前, 偵測到有人經過才會點亮 LED 照明燈, 其餘時間不會亮燈, 但 PIR 觸發次數仍然會傳送到 ThingSpeak 伺服器 :


測試 2 : 太陽能測候站 + 使用 PIR 延時控制 LED 照明燈 (指定時段)

from machine import Pin,ADC
import dht
import time
import urequests
import ntptime  

LAMP_enabled=False  

def fill_zero(n):
    if n<10:  
        return '0' + str(n)  
    else:  
        return str(n)  

PIRPIN=Pin(4, Pin.IN)
RELAYPIN=Pin(5, Pin.OUT, Pin.PULL_UP)
RELAYPIN.value(1)
LAMP_state=0
triggerCount=0    
triggerLimit=5

def IRQ(p):                      
    global state                
    global triggerCount    
    global triggerLimit      
    global LAMP_state
    global LAMP_enabled
    LAMP_state=1
    if LAMP_enabled:  
        RELAYPIN.value(0)  
    if triggerCount < triggerLimit:      
        triggerCount=triggerCount + 1
    print(p,"IRQ Triggered! Trigger Count:",triggerCount,'LAMP State:',LAMP_state)

PIRPIN.irq(trigger=Pin.IRQ_RISING, handler=IRQ)

def LED_blink(pin,s):
    for i in range(1,s):
        pin.value(1)
        time.sleep_ms(500)
        pin.value(0)
        time.sleep_ms(500)

DHTPIN=Pin(0, Pin.IN)
LEDPIN=Pin(2, Pin.OUT)
d=dht.DHT11(DHTPIN)
adc=ADC(0)

host='http://api.thingspeak.com'
api_key='NO5N8C7T2KINFCQE'

try:  
    ntptime.settime()  
except:  
    pass  

while True:
    utc_epoch=time.mktime(time.localtime())  
    Y,M,D,H,m,S,ms,W=time.localtime(utc_epoch + 28800)  
    t='%s-%s-%s %s:%s:%s' % (str(Y),fill_zero(M),fill_zero(D),\  
                             fill_zero(H),fill_zero(m),fill_zero(S))  
    print(t)  
    if m==0:    
        try:
            ntptime.settime()  
        except:  
            pass
    if H > 18 or H < 6:    
        LAMP_enabled=True 
    else:
        LAMP_enabled=False 
    print('LAMP_enabled:',LAMP_enabled)    

    if LAMP_state==1:
        if LAMP_enabled:    
            RELAYPIN.value(0)  
        triggerCount=triggerCount - 1    
        if triggerCount <= 0:                    
            LAMP_state=0
            triggerCount=0
    else:
        RELAYPIN.value(1)
    print("IRQ Trigger Count:",triggerCount,'LAMP State:',LAMP_state)

    try:      
        d.measure()              
        t=d.temperature()    
        f=round(t * 9/5 + 32)
        h=d.humidity()
        a=adc.read()
        a=int(0.3*a + 0.7*adc.read())
        a=round(a*100/1024)
        url='%s/update?api_key=%s&field1=%s&field2=%s&field3=%s&field4=%s&field5=%d' %(host, api_key, t, f, h, a, triggerCount)
        print('Temperature=', t, 'C', '/', f, 'F', 'Humidity=', h, '%', 'Luminance=', a, '%')
        r=urequests.get(url)
        print('response=', r.text)
    except:
        print('urequests.get() exception occurred!')

    LED_blink(LEDPIN,20)

此程式中我增加了一個全域變數 LAMP_enabled 來表示是否為 LED 可點亮之狀態,  True 表示傍晚六點至早上六點時段, 主迴圈中從 NTP 取得之時間資訊中擷取現在是幾點 (H) 幾分 (m), 其中的 H 用來更新 LAMP_enabled 的狀態, 然後依此狀態判別於 LAMP_state 為 1 時是否要點亮 LED 燈; 而 m 用來在整點時與 NTP 同步一次, 以免 ESP8266 內部時鐘久了之後產生時間誤差. 這個做法上回測試 1602 與 SSD1360 時有用到, 參考 :

MicroPython on ESP8266 (十七) : 液晶顯示器 1602A 測試
MicroPython on ESP8266 (十八) : SSD1306 液晶顯示器測試

另外, 當 PIR 偵測到有人移動時, 在 IRQ() 函數中也是要先判斷 LAMP_enabled 是否為 True, 是的話才點亮 LED.

最後還有一個考量是陰雨天時天色較暗, 雖然不是在指定的 PM 06:00 ~ AM 06:00 的亮燈時段, 有人經過時也應該讓 LED 點亮才對, 經測試亮度變數 a 若低於 15 時通常天色已夠暗, 為此我又增加了一個全域變數 Luminance 來記錄最近的亮度值, 程式碼修改如下 :

測試 3 : 太陽能測候站 + 使用 PIR 延時控制 LED 照明燈 (指定時段 + 亮度控制)

from machine import Pin,ADC
import dht
import time
import urequests
import ntptime

LAMP_enabled=False
Luminance=0  

def fill_zero(n):
    if n<10:
        return '0' + str(n)
    else:
        return str(n)

PIRPIN=Pin(4, Pin.IN)
RELAYPIN=Pin(5, Pin.OUT, Pin.PULL_UP)
RELAYPIN.value(1)
LAMP_state=0
triggerCount=0    
triggerLimit=5

def IRQ(p):                      
    global state                
    global triggerCount    
    global triggerLimit      
    global LAMP_state
    global LAMP_enabled
    LAMP_state=1
    if LAMP_enabled:
        RELAYPIN.value(0)
    if triggerCount < triggerLimit:      
        triggerCount=triggerCount + 1
    print(p,"IRQ Triggered! Trigger Count:",triggerCount,'LAMP State:',LAMP_state)

PIRPIN.irq(trigger=Pin.IRQ_RISING, handler=IRQ)

def LED_blink(pin,s):
    for i in range(1,s):
        pin.value(1)
        time.sleep_ms(500)
        pin.value(0)
        time.sleep_ms(500)

DHTPIN=Pin(0, Pin.IN)
LEDPIN=Pin(2, Pin.OUT)
d=dht.DHT11(DHTPIN)
adc=ADC(0)

host='http://api.thingspeak.com'
api_key='NO5N8C7T2KINFCQE'

try:
    ntptime.settime()
except:
    pass

while True:
    utc_epoch=time.mktime(time.localtime())
    Y,M,D,H,m,S,ms,W=time.localtime(utc_epoch + 28800)
    t='%s-%s-%s %s:%s:%s' % (str(Y),fill_zero(M),fill_zero(D),\
                             fill_zero(H),fill_zero(m),fill_zero(S))
    print(t)
    if m==0:
        try:
            ntptime.settime()
        except:
            pass
    if H > 18 or H < 6 or Luminance < 15:
        LAMP_enabled=True
    else:
        LAMP_enabled=False
    print('LAMP_enabled:',LAMP_enabled)

    if LAMP_state==1:
        if LAMP_enabled:
            RELAYPIN.value(0)
        triggerCount=triggerCount - 1
        if triggerCount <= 0:                    
            LAMP_state=0
            triggerCount=0
    else:
        RELAYPIN.value(1)
    print("IRQ Trigger Count:",triggerCount,'LAMP State:',LAMP_state)

    try:      
        d.measure()              
        t=d.temperature()    
        f=round(t * 9/5 + 32)
        h=d.humidity()
        a=adc.read()
        a=int(0.3*a + 0.7*adc.read())
        a=round(a*100/1024)
        Luminance=a
        url='%s/update?api_key=%s&field1=%s&field2=%s&field3=%s&field4=%s&field5=%d' %(host, api_key, t, f, h, a, triggerCount)
        print('Temperature=', t, 'C', '/', f, 'F', 'Humidity=', h, '%', 'Luminance=', a, '%')
        r=urequests.get(url)
        print('response=', r.text)
    except:
        print('urequests.get() exception occurred!')

    LED_blink(LEDPIN,20)

這裡要注意的是, 在指定時段 PM 06:00 ~ AM 06:00 之外, 當亮度低於 15% 時, 若 PIR 偵測到有人移動不會馬上亮燈, 而是要等 20 秒後進入下一個迴圈時才會亮, 這是因為受到要將資料紀錄到 ThingSpeak, 必須遵守更新週期大於 15 秒之限制 (此處取整數 20 秒), 使用 LED_blink() 來達成之故. 如果不需要紀錄到 ThingSpeak, 則可以將迴圈週期拉低到 1 秒, 因為 DHT11 的讀取頻率最高為 1Hz, 這樣亮度低於 15% 時就會迅速反應了. 參考 :

Using the DHT11/DHT22 Temperature/Humidity Sensor with a FRDM Board

呵呵, 拖了快一個月的作業就這樣大功告成了.

參考 :

Controlling relays using Micropython and an ESP8266 (網頁伺服器)

2017年8月25日 星期五

MicroPython v1.9.2 版釋出

又到週五了, 連續三周小周末想要搞定鄉下 20W 太陽能板負載 (夜間照明 + 溫濕亮度感知紀錄 + PIR 監測) 都沒時間, 今晚拿出零組件重新組裝, 發現 ESP-12E 板子似乎怪怪的, 想要重新燒錄韌體, 順便去 MicroPython 網站看看是否有新版韌體, 果不其然, 已於 8/23 日釋出 1.9.2 版, 距離 6/12 日釋出的 1.9.1 版已超過兩個月. 更新記錄檔參考 :

http://micropython.org/resources/micropython-ChangeLog.txt

主要的更新有兩個 :
  1. 新增 mpy_bin2res.py 工具可將二進檔轉為 Python 模組
  2. 改進了 ussl 模組, 增加 server_hostname 與 server_side 模式
既然有新版, 那就下載新版韌體來燒錄 D1 Mini 吧! 燒錄程序參考 :

MicroPython on ESP8266 (一) : 燒錄韌體
MicroPython v1.9.1 版韌體測試

以下為在 D1 Mini 上的測試結果 :

b▒#5 ets_task(40100164, 3, 3fff837c, 4)
Performing initial setup
OSError: [Errno 2] ENOENT

MicroPython v1.9.2-8-gbf8f45cf on 2017-08-23; ESP module with ESP8266
Type "help()" for more information.
>>> import port_diag    #系統診斷
FlashROM:
Flash ID: 1640e0 (Vendor: e0 Device: 4016)
Flash bootloader data:
Byte @2: 02
Byte @3: 40 (Flash size: 4MB Flash freq: 40MHZ)
Firmware checksum:
size: 601120      #韌體 bytes 數
md5: ac146b0593c8dd61b2b032233bc4a417
True                   #韌體完好無損

Networking:
STA ifconfig: ('0.0.0.0', '0.0.0.0', '0.0.0.0', '208.67.222.222')
AP ifconfig: ('192.168.4.1', '255.255.255.0', '192.168.4.1', '208.67.222.222')
Free WiFi driver buffers of type:
0: 8 (1,2 TX)
1: 0 (4 Mngmt TX(len: 0x41-0x100))
2: 8 (5 Mngmt TX (len: 0-0x40))
3: 4 (7)
4: 7 (8 RX)
lwIP PCBs:
Active PCB states:
Listen PCB states:
TIME-WAIT PCB states:

>>> import esp   
>>> esp.check_fw()             #檢查韌體是否有問題
size: 601120
md5: ac146b0593c8dd61b2b032233bc4a417
True                                    #True 表示韌體正常無損壞

>>> import os  
>>> os.uname()                   #檢查韌體備註 (版本與釋出日期)
(sysname='esp8266', nodename='esp8266', release='2.0.0(5a875ba)', version='v1.9.2-8-gbf8f45cf on 2017-08-23', machine='ESP module with ESP8266')

>>> import os   
>>> os.listdir()                     #檢查檔案系統初始內容
['boot.py']                             #只有 boot.py 一個檔案


>>> import os
>>> os.statvfs("/")                                        #查詢檔案之記憶體占用情形
(4096, 4096, 860, 852, 852, 0, 0, 0, 0, 255)    #總共 860 區塊, 可用 852 區塊

>>> for f in os.listdir():  
...     print('File: {} stats: {}'.format(f, os.stat(f)))   #檢查檔案系統使用情形
...
...
...
File: boot.py stats: (32768, 0, 0, 0, 0, 0, 162, 0, 0, 0)   #boot.py 占 162 bytes

>>> f=open('boot.py','r')    #開啟檔案 boot.py
>>> ls=f.readlines()             #顯示檔案內容
>>> for i in ls:
...     print(i)
...
...
...
# This file is executed on every boot (including wake-boot from deepsleep)

#import esp

#esp.osdebug(None)

import gc  

#import webrepl

#webrepl.start()

gc.collect()  

可見新版韌體的 boot.py 預設不啟動 WebREPL, 只開啟 gc 垃圾收集機制而已. 若要啟動 WebREPL 功能, 只要匯入 webrepl_setup 模組即可 :

>>> import webrepl_setup  
WebREPL daemon auto-start status: disabled    

Would you like to (E)nable or (D)isable it running on boot?
(Empty line to quit)
> E        #大寫 E 為啟動
To enable WebREPL, you must set password for it
New password: 123456           #WebREPL 登入密碼
Confirm password: 123456
Changes will be activated after reboot
Would you like to reboot now? (y/n) y    #小寫 y 重新啟動才會生效 (不可用大寫 Y)

重新開機 WebREPL 才會生效 :

lÜŸ<ÿƒ{g#‡þ#5 ets_task(40100164, 3, 3fff837c, 4)
WebREPL daemon started on ws://192.168.4.1:8266
WebREPL daemon started on ws://0.0.0.0:8266
Started webrepl in normal mode

MicroPython v1.9.2-8-gbf8f45cf on 2017-08-23; ESP module with ESP8266
Type "help()" for more information.
>>> import os
>>> os.listdir()
['boot.py', 'webrepl_cfg.py']
>>>
>>> f=open('webrepl_cfg.py','r')
>>> ls=f.readlines()
>>> for i in ls:
...     print(i)
...
...
...
PASS = '123456'  

可見啟動 WebREPL 後, 根目錄下多了 webrepl_cfg.py 這檔案, 其內容就是密碼 PASS 而已.

利用 Google gTTS 文字轉語音 API 讓電腦說話

測試過 Google 的語音辨識轉文字後, 接著要反過來測試 Google 的 gTTS 模組將文字轉成語音, 參考下面這篇大數軟體的教學影片 :

[Open Jarvis] 如何用Python 讓電腦說話?



D:\Python>pip3 install gTTS

D:\Python\test>pip3 install gTTS
Collecting gTTS
  Downloading gTTS-1.2.2.tar.gz
Requirement already satisfied: six in c:\python36\lib\site-packages (from gTTS)
Requirement already satisfied: requests in c:\python36\lib\site-packages (from gTTS)
Collecting gtts_token (from gTTS)
  Downloading gTTS-token-1.1.1.zip
Installing collected packages: gtts-token, gTTS
  Running setup.py install for gtts-token ... done
  Running setup.py install for gTTS ... done
Successfully installed gTTS-1.2.2 gtts-token-1.1.1

如果要離線安裝, 則除了下載 gTTS 模組外, 還必須下載 gTTS-token 模組, 而且要先安裝 gTTS-token 後才能再安裝 gTTS :

https://pypi.python.org/pypi/gTTS/1.2.2
https://pypi.python.org/pypi/gTTS-token/1.1.1

D:\Python>pip3 install gTTS-token-1.1.1.zip
Processing d:\python\gtts-token-1.1.1.zip
Requirement already satisfied: requests in c:\python36\lib\site-packages (from g
TTS-token==1.1.1)
Installing collected packages: gTTS-token
  Running setup.py install for gTTS-token ... done
Successfully installed gTTS-token-1.1.1

D:\Python>pip3 install gTTS-1.2.2.tar.gz
Processing d:\python\gtts-1.2.2.tar.gz
Requirement already satisfied: six in c:\python36\lib\site-packages (from gTTS==
1.2.2)
Requirement already satisfied: requests in c:\python36\lib\site-packages (from g
TTS==1.2.2)
Requirement already satisfied: gtts_token in c:\python36\lib\site-packages (from
 gTTS==1.2.2)
Installing collected packages: gTTS
  Running setup.py install for gTTS ... done
Successfully installed gTTS-1.2.2

安裝好 gTTS 後, 就可以開始進行測試了. 首先是從 gtts 套件匯入 gTTS 模組 , 接著呼叫建構式 gTTS() 傳入文字字串並指定語言以建立一個 gTTS 物件, 然後呼叫 gTTS 物件的 save() 方法將轉出來的語音存成本地的 mp3 檔 :

測試 1 : 中文 TTS 生日快樂

from gtts import gTTS
tts=gTTS(text='生日快樂', lang='zh')
tts.save("E:\python\test\happybirthday.mp3")

此處 save() 的參數為欲儲存的檔名, 可以帶路徑, 若未指定路徑就存在目前工作路徑下.

gTTS() 建構式的 lang 參數為 ISO 639-1 語言代碼, zh 是漢語, 英文是 en, 日文是 ja, 法文是 fr, 俄語是 ru, 西班牙語是 es, 任何語言都可以, 參考 :

List of ISO 639-1 codes

例如要發出日文的 "ありがとう (謝謝)" 的話, 程式要寫成 :

測試 2 : 日文 TTS 謝謝  

from gtts import gTTS
tts=gTTS(text='ありがとう', lang='ja')
tts.save("japanese_thank_you.mp3")

測試結果真的是日文發音的 "阿麗嘎多", 不管哪一種語言都是女聲.

gTTS() 建構式還有一個預設值為 False 的 slow 參數, 若設為 True 會產生一個發音速度比較慢的 mp3 檔案, 不過使用的機會應該不多.

上面的測試程式產生的 mp3 檔案必須手動到檔案總管點擊才會播放, 有沒有辦法用 Python 程式播放呢? 上面教學影片中的第二部分介紹利用 pygame 模組中的 mixer 混音器來播放音檔.

首先要先安裝 pygame 套件 :

https://pypi.python.org/pypi/Pygame/1.9.3

D:\Python>pip3 install pygame 
Collecting pygame
  Downloading pygame-1.9.3-cp36-cp36m-win_amd64.whl (4.2MB)
Installing collected packages: pygame
Successfully installed pygame-1.9.3

這樣就可以來測試音檔播放了, 關於 mixer 模組的用法, 參考 :

https://www.pygame.org/docs/ref/music.html

測試 3 : 播放 mp3 檔案  

from pygame import mixer
mixer.init()
mixer.music.load('E:\\Python\\test\\japanese_thank_you.mp3')
mixer.music.play()

注意, load() 方法的參數若有含路徑符號 '\' 必須跳脫, 即須使用 '\\'. 而 play() 預設是播放一次, 若要播放多次, 可用 loops 參數指定次數, 例如 :

mixer.music.play(loops=5)

"The loops argument controls the number of repeats a music will play. play(5) will cause the music to played once, then repeated five times, for a total of six. If the loops is -1 then the music will repeat indefinitely."

但我實際測試似乎次數不到 5 次, 只有 3 次, 很奇怪. 更奇怪的是, 上面測試 3 的程式在 IDLE 介面執行沒問題, 但若存成 .py 檔案用 python 指令去執行卻不會播放語音, 但也不會出現錯誤. (是我筆電的問題).

上面範例是明確指定 mp3 音檔, 可以利用 tempfile 模組使用暫存檔, 執行過後即自動刪除, 如下範例所示 :

測試 4 : 使用暫存檔儲存音檔 

from gtts import gTTS
from pygame import mixer
import tempfile
with tempfile.NamedTemporaryFile(delete=True) as fp:
    tts=gTTS(text='ありがとう', lang='ja')
    tts.save('{}.mp3'.format(fp.name))
    mixer.init()
    mixer.music.load('{}.mp3'.format(fp.name))
    mixer.music.play()

同樣地, 上述程式要在 IDLE 中執行才有效, 將其存成 tts.py 後用 python tts.py 雖可執行卻沒有播放. 上面測試 4 程式可進一步寫成函數, 如下所示 :

測試 5 : 寫成 spesk() 函數

import time
from gtts import gTTS
from pygame import mixer
import tempfile

def speak(sentence, lang, loops=1):
    with tempfile.NamedTemporaryFile(delete=True) as fp:
        tts=gTTS(text=sentence, lang=lang)
        tts.save('{}.mp3'.format(fp.name))
        mixer.init()
        mixer.music.load('{}.mp3'.format(fp.name))
        mixer.music.play(loop)

speak('ありがとう', 'ja')
time.sleep(3)
speak('全國的軍民同胞們, 川普是笨蛋', 'zh')
time.sleep(10)
speak('Hello World!', 'en')
time.sleep(3)

以上便是 Google TTS 的測試, 語音品質還不錯. 只是為何用執行 .py 檔案無效果還有待研究. (好像是我筆電的問題).

2017-08-25 :

用家裡電腦測試結果, 執行 .py 檔案沒問題, 可能我筆電有問題.

2017年8月24日 星期四

利用 Google 語音辨識 API 將語音轉成文字

今天在 Youtube 看到大數學堂的教學影片, 介紹如何利用 Google 語音辨識服務 (Google Speech Recognition Service) 將語音轉成文字, 據說可以翻譯 50 幾種語言  :

[Open Jarvis] 如何讓Python 自動將語音轉譯成文字?



我發現這個大數學堂有許多非常棒的教學, 例如網路爬蟲, R 語言, 影像辨識, 交易系統, Open Jarvis 等等, 參考 :

大數軟體有限公司 (Youtube)
http://www.largitdata.com/course_list/14
https://devpost.com/software/open-jarvis

看完上面影片後覺得利用 Python 透過 Google 語音辨識 API 做語音轉文字竟然如此簡單, 不禁躍躍欲試, 晚飯過後迫不及待要驗證一番.

安裝 SpeechRecognition 套件只要在命令提示字元視窗下 pip 或 pip3 install SpeechRecognition 指令即可 :

D:\Python>pip3 install SpeechRecognition

如果是在防火牆內 (例如公司網路) 無法直接安裝, 出現如下錯誤訊息 :

" Could not find a version that satisfies the requirement SpeechRecognition (from versions: )
No matching distribution found for SpeechRecognition"

可先到 PyPi 網站下載 whl 檔安裝 (約 32MB) :

https://pypi.python.org/pypi/SpeechRecognition/



D:\>cd python

D:\Python>pip3 install SpeechRecognition-3.7.1-py2.py3-none-any.whl
Processing d:\python\speechrecognition-3.7.1-py2.py3-none-any.whl
Installing collected packages: SpeechRecognition
Successfully installed SpeechRecognition-3.7.1

用 pip3 list 檢查確實已安裝此套件 :

D:\Python>pip3 list
DEPRECATION: The default format will switch to columns in the future. You can us
e --format=(legacy|columns) (or define a format=(legacy|columns) in your pip.con
f under the [list] section) to disable this warning.
adafruit-ampy (1.0.1)
beautifulsoup4 (4.5.3)
click (6.7)
colorama (0.3.9)
cycler (0.10.0)
Django (1.8.18)
matplotlib (2.0.0)
mpfshell (0.8.0)
numpy (1.12.1+mkl)
olefile (0.44)
pandas (0.19.2)
Pillow (4.1.0)
pip (9.0.1)
py2exe (0.9.2.0)
PyAutoGUI (0.9.36)
pyFirmata (1.0.3)
PyMsgBox (1.0.6)
pyparsing (2.2.0)
PyScreeze (0.1.9)
pyserial (3.3)
python-dateutil (2.6.0)
PyTweening (1.0.3)
pytz (2017.2)
pyudev (0.21.0)
requests (2.13.0)
rshell (0.0.9)
scikit-learn (0.18.1)
scipy (0.19.0)
setuptools (28.8.0)
six (1.10.0)
SpeechRecognition (3.7.1)
virtualenv (15.1.0)
websocket-client (0.40.0)

然後馬上照教學影片中的範例程式來測試 :

>>> import speech_recognition
>>> r=speech_recognition.Recognizer()
>>> with speech_recognition.Microphone() as source:
audio=r.listen(source)

但是呼叫 listen() 方法時卻出現錯誤 :

Traceback (most recent call last):
  File "C:\Python36\lib\site-packages\speech_recognition\__init__.py", line 108, in get_pyaudio
    import pyaudio
ModuleNotFoundError: No module named 'pyaudio'  

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "<pyshell#4>", line 1, in <module>
    with speech_recognition.Microphone() as source:
  File "C:\Python36\lib\site-packages\speech_recognition\__init__.py", line 79, in __init__
    self.pyaudio_module = self.get_pyaudio()
  File "C:\Python36\lib\site-packages\speech_recognition\__init__.py", line 110, in get_pyaudio
    raise AttributeError("Could not find PyAudio; check installation")
AttributeError: Could not find PyAudio; check installation

意思是還缺一個 PyAudio 模組, 於是回到 PyPi 閱讀 SpeechRecognition 套件說明, 原來此套件若使用麥克風當作音源輸入的話, 必須安裝 PyAudio 模組才行 :

"PyAudio is required if and only if you want to use microphone input (Microphone). PyAudio version 0.2.11+ is required, as earlier versions have known memory management bugs when recording from microphones in certain situations.
If not installed, everything in the library will still work, except attempting to instantiate a Microphone object will raise an AttributeError."

用下列指令安裝 PyAudio :

D:\Python> pip3 install PyAudio  

或者從 PyPi 下載 whl 安裝檔 :

https://pypi.python.org/pypi/PyAudio/0.2.11#downloads

D:\Python>pip3 install PyAudio-0.2.11-cp36-cp36m-win_amd64.whl  
Processing d:\python\pyaudio-0.2.11-cp36-cp36m-win_amd64.whl
Installing collected packages: PyAudio
Successfully installed PyAudio-0.2.11

安裝好後再次執行 listen() 呼叫仍然出現錯誤訊息 "No Default Input Device Available" :

>>> with speech_recognition.Microphone() as source:
audio=r.listen(source)

Traceback (most recent call last):
  File "<pyshell#7>", line 1, in <module>
    with speech_recognition.Microphone() as source:
  File "C:\Python36\lib\site-packages\speech_recognition\__init__.py", line 86, in __init__
    device_info = audio.get_device_info_by_index(device_index) if device_index is not None else audio.get_default_input_device_info()
  File "C:\Python36\lib\site-packages\pyaudio.py", line 949, in get_default_input_device_info
    device_index = pa.get_default_input_device()
OSError: No Default Input Device Available

我查 Google 找到下面這篇, 有人建議還要安裝 "PortAudio" :

PyAudio IOError: No Default Input Device Available

我找到 PortAudio 網站下載 tgz 檔, 解開後發現是一堆包含 html 與 sh 副檔名的檔案, 搞不清楚是要安裝還是要放在哪裡. 後來想到該不會是因為我還沒插上麥克風的關係吧? 找出已經很久沒用的麥克風插進電腦的麥克風孔, 再次執行 listen() 就不會報錯了.

不過執行 listen() 後我對麥克風說了句 "您好嗎" 就停在那邊很久都沒反應, 後來找到下面文章, 原來要先用 adjust_for_ambient_noise() 函數調整麥克風的噪音 :

Easy Speech Recognition in Python with PyAudio and Pocketsphinx

我將其範例程式修改如下 :

import speech_recognition as sr

#obtain audio from the microphone
r=sr.Recognizer()
with sr.Microphone() as source:
    print("Please wait. Calibrating microphone...")
    #listen for 5 seconds and create the ambient noise energy level
    r.adjust_for_ambient_noise(source, duration=5)
    print("Say something!")
    audio=r.listen(source)

# recognize speech using Google Speech Recognition
try:
    print("Google Speech Recognition thinks you said:")
    print(r.recognize_google(audio, language="zh-TW"))
except sr.UnknownValueError:
    print("Google Speech Recognition could not understand audio")
except sr.RequestError as e:
    print("No response from Google Speech Recognition service: {0}".format(e))

上面程式是利用 SpeechRecognition 模組中的 recognixe_google() 函數透過 Google 語音辨識 API 來將麥克風收到的語音物件 audio 辨識成指定語系的文字 :

r.recognize_google(audio, language='zh-TW')

這裡要傳入語音物件 audio 與 language 參數, 指定語系為繁體中文的 "zh-TW", 不過中文語系下唸英文也是可以辨識出來的. 將此程式存成 google_sr.py 後執行, 果然就成功了 :

D:\ESP8266\test>python google_sr.py
Please wait. Calibrating microphone...
Say something!
Google Speech Recognition thinks you said:
你好嗎

D:\ESP8266\test>python google_sr.py
Please wait. Calibrating microphone...
Say something!
Google Speech Recognition thinks you said:
Google Speech Recognition could not understand audio

D:\ESP8266\test>python google_sr.py
Please wait. Calibrating microphone...
Say something!
Google Speech Recognition thinks you said:
新年快樂

D:\ESP8266\test>python google_sr.py
Please wait. Calibrating microphone...
Say something!
Google Speech Recognition thinks you said:
川普是笨蛋

D:\ESP8266\test>python google_sr.py
Please wait. Calibrating microphone...
Say something!
Google Speech Recognition thinks you said:
金正恩是瘋子

D:\ESP8266\test>python google_sr.py
Please wait. Calibrating microphone...
Say something!
Google Speech Recognition thinks you said:
good morning

D:\ESP8266\test>python google_sr.py
Please wait. Calibrating microphone...
Say something!
Google Speech Recognition thinks you said:
Donald trump is stupid

Google 果然厲害! 辨識率 100%!

參考 :

SPEECH RECOGNİTİON WİTH PYTHON
Coding Jarvis in Python in 2016
Speech Recognition with Python
透過 Python 使用 Google Speech Recognition 語音辨識服務
https://www.youtube.com/channel/UCFdTiwvDjyc62DBWrlYDtlQ


2017-08-25 補充 :

早上提早到公司也如法泡製一番, 發現可能因為防火牆無法運作 :

D:\Python\test>python google_sr.py
Please wait. Calibrating microphone...
Say something!
Google Speech Recognition thinks you said:
No response from Google Speech Recognition service: recognition connection faile
d: [WinError 10060] 連線嘗試失敗,因為連線對象有一段時間並未正確回應,或是連線建
立失敗,因為連線的主機無法回應。