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 (網頁伺服器)

沒有留言 :