三個簡單 ESP8266 與 MicroPython 物聯網(IoT)應用:任何人都能在家自己做,低成本、免焊接、免懂程式又實用

Alan Wang
27 min readMar 2, 2021

--

(上圖不是 ESP8266,不過就將就吧。)

前言

Raspberry Pi Pico 的問世,使更多人開始重新注意到 MicroPython 這個嵌入式系統用的 Python 語言。儘管 MicroPython 在執行速度上不及 C++ 等語言,但它有很多語言與功能上的便利優點。

ESP8266 和 IoT 自造專案已經不是新玩意,然而你仍然能用它們來做些低成本的物聯網裝置。若你對 MicroPython 與周邊裝置有一定了解,還可以隨自己的需求改造。不過,一般人對這會不會有興趣呢?

本文要展示的,就是筆者至目前為止最喜歡的三個 ESP8266 IoT 專案。你可以在完全不懂程式與硬體的前提下,照著以下步驟做出你自己的裝置,放在自己家裡或辦公室使用。也因為如此,這篇不會太深入講語言和硬體知識,但你能在書店和網路找到很多的補充材料。

什麼是 ESP8266 與開發板?

ESP8266 是上海樂鑫信息科技開發的低耗能微控制器(微處理器),基本時脈 80 MHz,其低廉的成本和不錯的性能多年來都廣受自造者社群注意,而且衍生出眾多後繼版本,包括性能優越且強大的 ESP32。

開發板是裝有微控制器的印刷電路板(PCB)裝置,包含了供電和周邊連接介面等等,但國外也會稱呼開發板為微控制器,一如我們會將裝有 ESP8266 處理器的板子直接稱為 ESP8266。ESP8266 具備 WiFi 連線能力,所以你會在 PCB 板上看到天線。

雖然能連接網路,但如同其他微控制器,ESP8266 的有限記憶體使其只能進行簡單的網路資料交換,不過以本文要介紹的專案來說都已經夠用了。

什麼是 MicroPython?

MicroPython 是精簡版本的 Python 程式語言,由澳洲物理學家兼電腦科學家 Damien George 於 2013 年開發,和後者一樣是使用直譯器的語言,但能夠在記憶體相對小很多的微控制器上執行。

MicroPython 事實上有好幾種版本,但都是以 Python 3.4 為基礎。我們這裡要使用的是針對 ESP8266 設計的版本(這和 ESP32 的版本也非常相近)。

在使用 MicroPython 之前,你必須先在開發板上安裝其韌體,這樣我們才能使用直譯器來執行程式。

材料準備

1. NodeMCU V2 開發板

NodeMCU 是一種常見的 ESP8266 開發板,而且價格不貴,台幣約兩百元上下,有的地方甚至低至百元出頭。它販賣時大多都已經焊好 GPIO 接腳,可以直接使用。

要注意的是有少數使用其他微控制器的開發板也會冠上 NodeMCU 的名稱,不過比較不常見。此外,ESP8266 NodeMCU 又有 V2 和 V3 兩種常見規格,V3 尺寸較大,兩者的 USB 介面晶片也不同。理論上兩種都可用,但在此我們要選用 V2,因為只有這種才適合下面要用的迷你麵包板。

下面較小的是 V2,使用 CP2102 USB 介面晶片;較大的 V3 使用長方形的 CH340 晶片

其實市面上還有一種是 ESP8266 晶片直接裸露出來的版本,其接腳與上面的完全相同,但可能就得自行焊接了。

事實上幾乎所有的 ESP8266 開發板,不論板子形狀,其接腳是共通的,也完全適用於本篇的操作,但筆者就不多介紹了。

對了,你也需要一條有傳輸功能的 micro USB 線,這應該不難取得。

2. 迷你麵包板和公母杜邦線

你需要一個 170 孔(10 x 17)的迷你麵包板,一般賣台幣 30 元,以及若干杜邦線。麵包板是電子實驗常用的平台,搭配杜邦線使用,如此一來無須焊接,就能隨意接上或移除電子線路。

杜邦線通常是 20 或 40 條一起賣,長度有分 10 或 20 公分,台幣約 20 到 40 元。杜邦線有公、母頭之分,顧名思義公頭有一根針,母頭則像插座一樣。我們需要 8 條如上圖的公/母頭線,其餘的可以留著備用。

3. 1602 LCD I2C 模組(HD44780)

本篇的三個專案,為了顯示各種訊息,自然需要一個顯示器裝置。1602 LCD 是市面上最悠久、好用又便宜的模組之一,有很多地方只要台幣百元、甚至 50 元就有。

HD44780 其實是日立在 1980 年代設計的液晶螢幕(LCD)控制器,本身內建有顯示字元,但如今我們會拿來稱呼使用此種控制器的 LCD 顯示模組。其實一般也很少會講 HD44780,大多就直接稱 LCD 1602 或 LCD 16x2。

LCD 1602 代表可以顯示 16 個字元 x 2 行。市面上還可以找到 LCD 2004,也就是 20 個字 x 4 行。兩者功能上是通用的,但這邊程式是以 LCD 1602 的格式來設計。市面上有藍底白字和綠底黑字兩種顯示顏色可買。

此外如今能買到的 LCD 1602 會在背面焊上一個 I2C 轉接板。I2C(Inter-Integrated Circuit,積體匯流排電路)是一種常用、控制外部裝置的協定,而轉接板大幅簡化了 HD44780 LCD 的接線。買的時候注意一下商品有沒有標明 I2C,最好是能確認背面有黑色的轉接板。

4. HC-SR04P 超音波模組

最後一樣是 HC-SR04P 超音波模組,藉由發出超音波並測量反彈時間的方式,可以測量約 2 公尺內物體的距離。這在第三個專案會用到。

要注意 HC-SR04P 和較舊的 HC-SR04 不同,舊版正面有個銀色的晶體振盪器,新版沒有,此外新版的接腳是焊接在前方而不是後面。

新舊版功能上是一樣的,為什麼一定要用新版呢?因為舊版需要 5 伏特的電壓才能運作,而新版可在 3.3 或 5 伏特運作。由於開發板需要從這模組讀取信號,而 ESP8266 接腳對 5V 電壓的保護有限,所以用新版當然比較安全囉。價格約台幣 30 到 40 元左右。

因此,完成這三個專案,硬體總成本不會超過台幣 500 元,很划算吧?

安裝韌體與驅動程式

1. 安裝 CP2102 驅動程式

為了能讓電腦與你的開發板溝通,你需要安裝其 USB 介面晶片的驅動程式。下載 CP210x driver 並點選壓縮檔中合適的版本來安裝:

(點進去後點 Overview 旁邊的 Downloads)

如果你買的板子使用長方形的 CH340 介面晶片,你必須下載並安裝它的驅動程式:

2. 下載 MicroPython 韌體

下載 ESP8266 用的 MicroPython 韌體:

下載像是 esp8266–20210202-v1.14.bin 這樣的檔案。v1.14 代表穩定版的版本(這也是本文示範用的版本)。

3. 下載 LCD/超音波模組驅動程式

1602 LCD 和 HC-SR04P 都需要驅動程式才能運作。幸好,有很多人已經寫出了它們專用的套件。

下面是 LCD 驅動程式:

按 Code → Download ZIP 下載原始碼:

解壓縮 ZIP 檔,我們只需要子目錄 lcd 底下的 lcd_api.pyesp8266_i2c_lcd.py 這兩個檔案(其他檔案是設計給不同開發板使用的):

接著來下載 HC-SR04 驅動程式:

這個比較單純,我們只需要 hcsr04.py 這個檔案。

.py 副檔名代表這些是 Python 草稿碼檔案。也就是說,這些驅動程式本身也是用 MicroPython 寫成的。

4. 下載 Thonny IDE和安裝 MicroPython 韌體

Thonny 編輯器是愛沙尼亞的塔圖大學發展的多用途 Python 編輯器,因為非常易用而廣受歡迎。它也支援多種 MicroPython 版本的開發。

下載適合你系統的 Thonny 版本:

打開編輯器後,到工具→選項,點「直譯器」並在下拉選單點選「 MicroPython (ESP8266)」:

如果你的介面是英文的,到選項中的「主題與字型」(Theme & Font)選擇繁體中文,並重新啟動編輯器。

選擇好 MicroPython 環境後,用 USB 線將開發板接到電腦。如果電腦沒有辨識到板子,看看之前的 USB 介面驅動程式是否有正確安裝。

點畫面右下角的「安裝或更新韌體」:

在安裝韌體的畫面中選擇板子(開發板的裝置名稱)並選擇前面下載的韌體檔,點選「安裝」。安裝韌體需要一點時間,耐心等待畫面上的操作結束。

安裝結束後,關閉韌體安裝畫面,然後點選項的「確認」。你應該要看到編輯器底下的互動環境視窗出現以下字樣:

MicroPython v1.14 on 2021-02-02; ESP module with ESP8266
Type "help()" for more information.
>>>

這表示編輯器已經順利跟板子的 MicroPython 直譯器連線,並準備好執行程式。

5. 上載驅動程式到開發板

在編輯器打開之前下載的 lcd_api.py、esp8266_i2c_lcd.py 和 hcsr04.py 三個檔案,然後點檔案→儲存複本→MicroPython 設備:

※當編輯器與開發板連線時,開啟舊檔也會出現「本機」與「MicroPython」兩個選項。這時當然就得從本機開啟以上檔案。

點選「MicroPython 設備」後,會出現開發板的內部儲存空間。將這三個檔案用完全一樣的檔名儲存到板子上,千萬別打錯了:

一開始只看到 boot.py、沒看到其他東西是正常的,這代表你還沒有存其他檔案到板子上。在下面輸入檔案名稱和按確認,就會看到上傳進度條。

另一個檢查板子上檔案的方式是使用 MicroPython 的 os 模組。你可在 REPL 介面連續輸入下面兩個指令:

>>> import os
>>> os.listdir()
['boot.py', 'esp8266_i2c.py', 'lcd_api.py', 'hcsr04.py']

os.listdir() 會列出裝置空間內的檔案。稍後你也可以用 os.remove() 來刪除指定檔案:

>>> os.remove('esp8266_i2c.py')

總之檔案都上傳後就一切就緒了。讓我們來看第一個專案吧。

第一個專案:自動對時鐘

Photo by CHUTTERSNAP on Unsplash

ESP8266 網路時鐘是筆者最喜歡的應用之一,因為只要家裡有 WiFi,這座鐘插著電就能保持恆久的準確狀態,不需要再煩惱換電池跟校正啥的。

連接 LCD 模組

首先我們得來將 LCD 模組接上 NodeMCU,這需要 4 條杜邦線。下面是 NodeMCU 的接腳圖,我們主要需要參考的是接腳的編號:

LCD 對開發板的接線對照如下:

  • VCC → Vin
  • GND → GND
  • SDA → D2(GPIO4)
  • SCL → D1(GPIO5)

把開發板(建議先拔掉 USB 線)插在迷你麵包板的正中央,然後將 4 條杜邦線拆下來,依下圖連接好。杜邦線的母頭連接 LCD 背後的 I2C 轉接板,公頭則接在麵包板上。

杜邦線的顏色不拘,但通常筆者會用紅色、橘色和紫色等代表電源,黑色或灰色代表接地(GND)。

開發板的 Vin 會輸出 USB 接頭輸入的電壓,而 USB 一般正常的供電電壓為 5V。LCD 的背光需要 5 伏特才能正常顯示,電壓太低就會看不見字,太高則可能使之燒壞。所以事後你要給這時鍾接上獨立電源時,請使用輸出 5 伏特的充電器,或接在電腦上也行。

不過,也有一種 1602A LCD(較不常見)的運作電壓是 3.3V — — 如果你買到的是這種,請將 VCC 改接到板子上的任一個 3V3 接腳。

無論如何,接線時請務必小心:接錯腳位時,輕微則只是裝置無法運作,但造成短路的話則可能損害腳位、甚至整個板子。如果接電後聞到焦味…請用最快的速度拔掉,然後祈禱板子沒有燒壞吧。

測試 LCD 是否正確連接

I2C 協定讓控制板只要用相同的兩個腳位,就可以控制多個不同的 I2C 裝置,但它們必須正確地通電,而且控制板要知道正確的 I2C 位址。

一般市面上的 LCD 的 I2C 位址是 0x27,但也有 0x3f 的版本。我們可以先在 REPL 用以下指令「掃描」I2C 腳位(即 D1、D2)上有哪些裝置:

>>> from machine import Pin, SoftI2C
>>> i2c = SoftI2C(scl=Pin(5), sda=Pin(4))
>>> i2c.scan()
[39]

結果是 39,這就是 0x27 這個 16 進位數的 10 進位值。如果是 0x3f 的話會看到 63。

如果完全沒有值出現,或者出現一大堆值,那很可能表示電線接錯了,或者 LCD/控制板腳位有損壞。但要如何判定是什麼狀況,這就是更深的主題了,本篇暫不討論。

執行程式

在編輯器新增一個檔案,並儲存為 xxx.py(名稱不拘,比如 esp8266_clock.py),然後貼上以下內容:

ssid = ''  # 輸入 WiFi 名稱
pw = '' # 輸入 WiFi 密碼
i2c_addr = 0x27 # LCD I2C 位址, 0x27 或 0x3f
tz_hour = 8 # 所在地時差 (小時)
# --------------------------------------------------import network, ntptime, utime, gc
from machine import Pin, SoftI2C, Timer
from esp8266_i2c_lcd import I2cLcd
gc.enable()
led = Pin(2, Pin.OUT, value=1)
lcd = I2cLcd(SoftI2C(scl=Pin(5), sda=Pin(4)), i2c_addr, 2, 16)
wday = ('Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun')def lcd_show(text, clear=True):
if clear:
lcd.clear()
lcd.move_to(0, 0)
lcd.putstr(text)
lcd_show('Connecting to\n WiFi...')
wifi = network.WLAN(network.STA_IF)
wifi.active(True)
wifi.connect(ssid, pw)
while not wifi.isconnected():
pass
def time_update(timer):
while True:
try:
ntptime.settime()
break
except:
utime.sleep(10)
def time_display(timer):
led.value(not wifi.isconnected())
dt = list(utime.localtime(utime.time() + tz_hour * 3600))
dt[6] = wday[dt[6]]
output = ' {0:4d}-{1:02d}-{2:02d} {6}\n {3:02d}:{4:02d}:{5:02d}'
lcd_show(output.format(*dt), clear=False)

time_update(None)
timer_ntp, timer_display = Timer(-1), Timer(-1)
timer_ntp.init(mode=Timer.PERIODIC, period=900000, callback=time_update)
timer_display.init(mode=Timer.PERIODIC, period=100, callback=time_display)

本文不會詳細解釋程式細節,不過它做的事如下:

  1. 連上你指定的 WiFi 網路。
  2. 每 15 分鐘查詢一次 NTP 伺服器,更新開發板本身的系統時間。( ESP8266 本身的計時精確度有一定極限,更新間隔太長就容易出現明顯誤差。)
  3. 持續把板子的系統時間顯示在 LCD 上。
  4. 板子本身的藍色 LCD 燈會顯示 WiFi 是否處於連線中狀態。

請記得在 ssid 和 pw 後面的單引號之間輸入你的 WiFi 帳密,以及確認所在地時差(8 即 UTC+8;如果是 -8 即 UTC - 8),因為我們取得的時間是世界協調時間。

如果出現 OSError 錯誤

剛開始學做創客,最常遇到的問題之一就是板子無法跟裝置連線。

若你看到類似以下訊息:

Traceback (most recent call last):
File "<stdin>", line 7, in <module>
File "esp8266_i2c_lcd.py", line 25, in __init__
OSError: [Errno 110] ETIMEDOUT

Traceback (most recent call last):
File "<stdin>", line 7, in <module>
File "esp8266_i2c_lcd.py", line 25, in __init__
OSError: [Errno 19] ENODEV

這表示 I2C 通訊失敗,而最常發生的原因就是 I2C 位址錯誤、接錯腳位或杜邦線有問題(沒通電)。比較罕見但不無可能的原因也包括 LCD/控制板有其中一者故障。

如果真的找不到原因或沒有工具來檢查硬體,也可試試接不同的腳位。下面即指定 SCL → D5(GPIO14)、SDA → D6(GPIO12):

lcd = I2cLcd(SoftI2C(scl=Pin(14), sda=Pin(12)), 0x27, 2, 16)

執行程式

點選編輯器畫面的綠色箭頭或按鍵盤的 F5 來執行程式:

在執行程式前,你必須確認板子有連接到電腦、編輯器也有跟其直譯器連線。你可按編輯器的紅色 STOP 鈕來強制重新連線。若板子接上電腦後沒有辨識到裝置,趕快拔掉電源,看看是不是 LCD 接線有誤。

第一次連 WiFi 時會久一點。注意若所在環境的 WiFi 訊號很差,程式也有可能無法順利執行。

另外也特別留意:這支程式使用了 Timer 來簡化程式碼,但這是個非同步運算機制,所以執行程式後第一次按編輯器的停止鈕時,板子可能會跟編輯器斷線。若欲重新取得連線,多按幾次停止鈕看看。

你可以用螺絲起子轉動 I2C 轉接板上的開關,來調整 LCD 背光的對比度。

上傳程式

上面執行程式時,只是從電腦把程式餵給開發板的直譯器而已,但現在我們希望程式能在板子上獨立運作。

這時的做法就跟之前上傳驅動程式一模一樣,只是你得把你的程式以 main.py 的名稱寫入開發板。在這之後,開發板只要一通電,就會自動執行 該程式。若你要在不同的地方使用此裝置,在程式修改 WiFi 帳密後重新上船即可。

現在你可以找個盒子,把開發板裝進去、將 LCD 嵌在表面上,然後找個 5V 充電器。一台簡易的自動對時鐘就這麼完成了!

第二個專案:氣象預報站

Photo by Osman Rana on Unsplash

有時待在室內準備出門,又不太確定外頭的天氣狀況。這時你當然可以上網查天氣,但你也可以做一個這樣的快查裝置放在桌邊。

這個應用會查詢一個名叫 OpenWeather 的網路查詢服務,取得特定城市的當下氣象(最高溫/最低溫、濕度、氣象概況)。取得和顯示資料之後,開發板會進入深度睡眠(deep sleep)狀態,直到我們按板子上的 RST(reset)鈕來重開它為止。

接線與前一個範例完全相同,只會使用到 LCD。

使用 OpenWeather API

你需要先在該網站註冊帳號,方法可參閱以下網址:

免費帳號有 1 分鐘不能呼叫超過 60 次的限制。反正我們只在需要的時候查一下氣象就行了。

https://home.openweathermap.org/api_keys, 你會得到一個 API key,這等於是你使用服務的授權碼,請記得將之寫在下面的程式裡。有人說 API key 有一年期限,不過屆時再申請就好了。

城市名稱是英文,你可點網站的 Maps 查看台灣或其他地方列出的城市名稱,照實輸入以下程式即可。

ssid = ''  # 輸入 WiFi 名稱
pw = '' # 輸入 WiFi 密碼
city = '' # 城市名稱
key = '' # API key
url = 'http://api.openweathermap.org/data/2.5/weather?q={}&appid={}&units=metric'
i2c_addr = 0x27 # LCD I2C 位址# --------------------------------------------------import network, urequests, utime, gc, math
from machine import Pin, SoftI2C, deepsleep
from esp8266_i2c_lcd import I2cLcd
gc.enable()
led = Pin(2, Pin.OUT, value=1)
lcd = I2cLcd(SoftI2C(scl=Pin(5), sda=Pin(4)), i2c_addr, 2, 16)
def lcd_show(text, row=0, clear=True):
if clear:
lcd.clear()
lcd.move_to(0, row)
lcd.putstr(text)
lcd_show('Connecting to\n WiFi...')
wifi = network.WLAN(network.STA_IF)
wifi.active(True)
wifi.connect(ssid, pw)
while not wifi.isconnected():
pass
lcd_show('Querying\n weather...')
try:
response = urequests.get(url.format(city, key))
data = response.json()
temps, state = data['main'], data['weather'][0]
output1 = '{temp_max:2.1f}-{temp_min:2.1f}C H:{humidity}%'
output2 = '{main}'
lcd_show(output1.format(**temps))
lcd_show(output2.format(**state), row=1, clear=False)
except:
lcd_show('Error:\n Query failed')
led.value(0)
deepsleep()

照第一個專案的方式,將程式上傳為板子上的 main.py,以便重複使用裝置。

第三個專案:雲端防盜器

ESP8266 IoT 應用不只能接收資料,也能遠端傳送資料,雖然這就需要一點額外服務的助力了。

這個應用會用到超音波模組,它會在偵測到一定距離內的障礙物時,透過網路傳送通知給你的 Line 帳號。如果長時間不在家,這就可以當成簡易的防盜裝置。

連接超音波模組

  • Vcc → 3V3
  • Gnd → GND
  • Trig → D5
  • Echo → D6

也請記得上傳驅動程式,否則程式會無法順利執行。理論上有些更簡單的感測器,例如 HC-SR501 人體紅外線感測器,可以在不需驅動程式下運作,但 HC-SR501 太容易被遠距離的人觸發,而且運作電壓是 5 伏特,使用上較為不便。

使用超音波模組的好處是,你可以把它安裝在門口或走廊之類的小空間,這麼一來有人經過就一定會觸發。當然你最好先花點時間測試,免得模組脫落或偵測距離不對時瘋狂觸發警報。

LCD 接線與前面兩個專案完全相同。在此我們還是接一個 LCD,這樣才能展示超音波模組有在運作、偵測距離大概如何。

建立與使用 IFTTT 服務

我們要使用 IFTTT(If this then that),免費帳號可以建立 3 個自訂服務。

你可照以下文章的教學來建立 Webhooks + Line 服務:

簡單來說,這使你在使用 HTTP GET 存取一個由 Webhooks 提供的網址時,就會觸發一個 Line 訊息,而且至多可傳 3 個參數過去。你可以隨時從 IFTTT 網站打開或關閉這些服務。

你可以自訂 Line 訊息的內容,而在此筆者的作法是只顯示參數 Value1 的值,我們會從程式中傳送事件的「名稱」作為此參數內容(下面程式中會傳送「Intruder detected!」),這樣你就可以將同一個服務應用在多個裝置上。

注意 Webhooks 的網址會包含其事件名稱(Event name)以及一個 API key,這也是你必須提供給程式的資訊。

ssid = ''  # 輸入 WiFi 名稱
pw = '' # 輸入 WiFi 密碼
name = 'Intruder detected!'
event = ''
key = ''
url = 'https://maker.ifttt.com/trigger/{}/with/key/{}?value1={}'
i2c_addr = 0x27 # LCD I2C 位址
detected_cm = 50 # 偵測距離 (公分)
detected_delay = 15 # 偵測後等待多久才能再度觸發 (秒)
# --------------------------------------------------import network, urequests, utime, gc
from machine import Pin, SoftI2C, Timer
from esp8266_i2c_lcd import I2cLcd
from hcsr04 import HCSR04
gc.enable()
led = Pin(2, Pin.OUT, value=1)
lcd = I2cLcd(SoftI2C(scl=Pin(5), sda=Pin(4)), i2c_addr, 2, 16)
sensor = HCSR04(trigger_pin=14, echo_pin=12)
def lcd_show(text, clear=True):
if clear:
lcd.clear()
lcd.move_to(0, 0)
lcd.putstr(text)
lcd_show('Connecting to\n WiFi...')
wifi = network.WLAN(network.STA_IF)
wifi.active(True)
wifi.connect(ssid, pw)
while not wifi.isconnected():
pass
lcd_show('')
countdown = 0
def detecter(timer):
global countdown
distance = sum([sensor.distance_cm() for _ in range(10)]) / 10
lcd_show('Distance:\n {:3.2f} cm'.format(distance), clear=False)
detected = True if 2 <= distance <= detected_cm else False
if detected and countdown == 0:
led.value(0)
lcd_show('Movement\n detected')
countdown = int(detected_delay / 0.5)
try:
urequests.get(url.format(event, key, name.replace(' ', '%20')))
except:
pass
if countdown > 0:
countdown -= 1
else:
led.value(1)

timer_detect = Timer(-1)
timer_detect.init(mode=Timer.PERIODIC, period=500, callback=detecter)

結語

本文展示了三個使用 ESP8266 搭配一、兩件外部模組的簡易 IoT 應用,包含一切所需的軟硬體準備過程,使你得以用半張小朋友的成本 DIY 出實用的居家物聯網設備。

大概就這樣啦。希望對各位有點幫助囉。

Photo by Diego PH on Unsplash

--

--

Alan Wang
Alan Wang

Responses (2)