今天來整理陳會安老師的大作 "超簡單Python/MicroPython物聯網應用" 這本書中的網路存取相關函式庫, 以便後續進行測試. 本系列之前的文章參考 :
比較舊的 ESP8266/ESP32 測試文章參考 :
在 CPython 中常用的第三方網路請求模組 requests 在 MicroPython 中已被改寫內建為 urequests 模組, 用法與 requests 相同, 即匯入後呼叫 urequests.get() 與 urequests.post() 進行 GET 與 POST 方法提交網頁請求. 如果請求參數值是英數字, 則使用 urequests 即可; 否則要使用此書作者修改過的 xrequests.py.
除了 MicroPython 內建的 urequests 模組外, 這本書還提供了如下四個自訂模組 :
自訂模組 | 說明 |
config.py | 紀錄 WiFi 基地台 SSID 與連線密碼的檔案 |
xtools.py | 網路連線工具函式庫 |
urlencode.py | URL 字串編碼模組 |
xrequests.py | 改編自內建的 urequests, 添加 params 參數並替 data 參數加上 URL 編碼 |
其中 urlencode.py 與 xrequests.py 都是從 MicroPython 原始碼中抽出修改而來, 其實這兩個模組都是為了 xtools.py 服務的. 這些好工具可以讓我們在開發物聯網應用時更方便, 程式碼更簡短. 程式員最重要的守則是~不要重複造輪子, 站在巨人的肩膀上看得更遠.
1. 基地台模組 config.py :
此檔案用來儲存 ESP8266/ESP32 要連線之基地台 SSID 與密碼, 格式如下 :
# config.py
SSID = "<WiFi名稱>" # WiFi名稱, 例如 "EDIMAX-tony"
PASSWORD = "<WiFi密碼>" # WiFi密碼, 例如 "blabla123"
在 xtools.py 模組的連線函式 conect_wifi_led() 會用到其中的 SSID 與 PASSWD 來連線, 這樣要改連其他基地台時只要修改此檔案即可, 而不必去改 connect_wifi_led(). 用法是先匯入 config :
import config
然後用 config.SSID 與 config.PASSWORD 取得設定值, 詳見下面的 xtools.py 模組.
2. URL 字串編碼模組 urlencode.py :
此模組的 urlencode(query) 函式用來將字典型式的查詢參數 query 轉成 URL 查詢字串後傳回, 尤其是這些參數值是中文時必須這樣處理轉成 '%' 開頭編碼之字串才行, 例如用 urequests.get() 去觸發 IFTTT 服務時就會用到, 使用方式如下 :
from urlencode import urlencode
import urequests
param={"account": "admin", "pwd": "123456"}
url = "https://abc.com/?" + urlencode(param)
urequest.get(url)
字典 param 經 urlencode() 轉換後會變成 GET 查詢字串 "account=admin&pwd=123456".
此模組原始碼參考 :
3. 網頁請求模組 xrequests.py :
由於 MicroPython 內建的 urequests 模組並無 params 參數, 且 data 參數沒有經過 URL 編碼 (可能是沒考慮非英文參數值), 在進行 GET 與 POST 請求時可能會出問題, 此書作者將 urequests 原始碼取出修改, 添加 params 參數並把 data 參數套用 URL 編碼 (即呼叫上面改良過的 urlencode) 後改名為 xrequests. 下面的 xtools.py 中的 webhook_get() 與 webhook_post() 即使用了 xrequests 模組, 而不是 urequests.
此模組原始碼參考 :
4. 工具模組 xtools.py :
此模組的兩個主角為 connect_wifi_led() 與 line_msg() 函式, 前者用來簡化 ESP8266/ESP32 WiFi 連線操作, 後者用來發送 Line 訊息, 此外也收納了一些好用的輔助函式. 為了方便取用, 我將其複製一份在 GitHub :
xtools.py 中的主要函式功能說明如下 :
函式 | 說明 |
get_id() | 傳回開發板的 MAC 位址字串 |
get_num(x) | 傳回字串 x 中的數字 (含浮點數) |
random_in_range(low, high) | 傳回 low~high 之間的整數亂數 (預設 0~1000) |
map_range(x, in_min, in_max, out_min, out_max) | 將數值 x 從 [in_min, in_max] 映射至 [out_min, out_max] 區間 |
conect_wifi_led(ssid, passwd, timeout=15) | 以密碼 passwd 連線 ssid 基地台 (15 秒內閃 LED), 成功 LED 恆亮並傳回所獲取之 IP |
show_error(final_state=0) | 閃爍板載 LED 3 秒以表示錯誤 (預設結束後恆亮) |
webhook_post(url, value) | 利用 xrequests.py 模組送出 POST 請求, 失敗呼叫 show_error() |
webhook_get(value) | 利用 urequests.py 模組送出 GET 請求, 失敗呼叫 show_error() |
line_msg(token, message) | 使用 xrequests.py 模組送出 LINE 訊息 (POST) |
format_datetime(localtime) | 將元組 localtime 轉成日期時間字串 'YYYY-MM-DD HH:mm:SS' |
使用方法是匯入 xtools 後直接呼叫以上這些函式. 例如連線 WiFi 基地台 :
import xtools
import config
ip=xtools.connect_wifi_led(config.SSID, config.PASSWORD)
print(ip)
可見程式碼非常簡潔.
注意, 書中範例所用的原始 get_ip() 函式傳回的是 bytes 類型資料, 但實務上較常用的是字串型態, 所以我已經修改 get_id() 函式, 在後面附加呼叫 decode('utf8'), 故傳回值為字串 :
def get_id():
return ubinascii.hexlify(machine.unique_id()).decode('utf8')
另外, get_id() 其實只是傳回 MAC 位址的後 6 碼而已 (且 high/low 順序顛倒), 完整的 MAC 位址要用 WLAN 物件 (AP/STA 模式均可) 的 config('mac') 方法查詢, 例如 AP 模式下 :
>>> import network
>>> ap=network.WLAN(network.AP_IF)
>>> type(ap)
<class 'WLAN'>
>>> ap.active(True) # 啟用 AP 模式
呼叫 WLAN 物件的 config('mac') 可以取得 bytes 類型的 MAC 位址, 但它是 16 進制表示法, 每一個 byte 前面都有 \x, 這可以用可用 ubinascii 模組的 hexlify() 方法去除, 不過結果仍然是 bytes 類型, 可呼叫 decode('utf8') 方法轉成 UTF-8 字串 :
>>> mac=ap.config('mac')
>>> mac
b'\xea\xdb\x84\xa8\xf5\x14'
>>> import ubinascii
>>> ubinascii.hexlify(mac)
b'eadb84a8f514'
>>> ubinascii.hexlify(mac).decode('utf8')
'eadb84a8f514'
hexlify() 函式可傳入每個 byte 16 進制值的分隔字元當第二參數, 例如 ":" :
>>> ubinascii.hexlify(mac, ':')
b'ea:db:84:a8:f5:14'
>>> ubinascii.hexlify(mac, ':').decode('utf8')
'ea:db:84:a8:f5:14'
xtools 透過 machine 模組的 unique_id() 函式取得的其實是 MAC 位址的後六碼加上 '00' :
>>> import machine
>>> machine.unique_id()
b'\x14\xf5\xa8\x00'
>>> ubinascii.hexlify(machine.unique_id()).decode('utf8')
'14f5a800'
>>> import xtools
>>> xtools.get_id()
'14f5a800'
其餘函式會在後續測試中 用到. AP 模式下 ESP32/ESP8266 建立的熱點, 其 SSID 為 "MicroPython-xxxxxx", 後面的 6 個 x 就是用此末六碼, 例如上面這塊板子 AP 模式的 SSID 即為 "MicroPython-a8f514", 密碼固定為 "micropythoN" (注意 N 大寫).
5. 網頁伺服器模組 ESP8266WebServer.py :
在該書第 14 章介紹的 ESP8266WebServer.py 可用來在 ESP8266/ESP32 開發板上建立網頁伺服器, 比起以前直接用 socket 實作方便得多, 此模組可在 GitHub 下載 :
不過我下載最新版測試書中範例卻出現錯誤, 原因是新版模組中的函式介面有修改, 所以我還是去博碩下載書本範例中所附的 ESP8266WebServer.py, 另存於 GitHub :
# https://github.com/tony1966/tony1966.github.io/blob/master/test/MicroPython/lib/ESP8266WebServer.py
使用 Socket 建立網頁伺服器的原始做法參考 :
將以上五個模組檔案用 AmpyGUI 或 Thonny 上傳到 ESP8266/ESP32 開發板後即可開始用它們來進行網路存取了.
AmpyGUI 可以一次上傳多個檔案, 如果用 Thonny 則一次只能傳一個檔案, 點選左上方本機資料夾底下的檔案, 按滑鼠右鍵選 "上傳到 /" 即可, 參考 :
這樣就準備妥當, 可以開始做實驗了.
2022-11-18 補充 :
我今天在檢視之前自己寫的 WiFi 連線函式庫時發現掃描附近基地台的函式 scan(), 我將其改名為 scan_ssid() 添加到 xtools.py 裡面 :
def scan_ssid():
sta=network.WLAN(network.STA_IF)
sta.active(True)
aps=sta.scan()
for ap in aps:
ssid=ap[0].decode()
mac=ubinascii.hexlify(ap[1], ':').decode()
rssi=str(ap[3]) + 'dBm'
print('{:>20} {:>20} {:>10}'.format(ssid, mac, rssi))
參考 :
2022-11-22 補充 :
因為 format_datetime() 函式傳回的是 GMT/UTC 時間, 今天改寫了 xtools.py, 新增 import ntptime 與 tw_now() 函式, 可取得台灣時區 (GMT/UTC+8) 目前的時間.
import ntptime
def tw_now():
try:
ntptime.settime()
except:
pass
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' % \
(Y, pad_zero(M), pad_zero(D), pad_zero(H), pad_zero(m), pad_zero(S))
return t
參考 :
2022-12-03 補充 :
如上所述, xtools.get_id() 經修改後會傳回 STA 模式 下 WLAN 介面的 MAC 位址字串 (而非原版 xtools 傳回之 bytes 型態), 因為 MAC 位址是唯一的, 因此可用來當作 id. 其實 MAC 位址還可以用 WLAN 物件的 config('mac') 方法求得,
>>> import network
>>> sta=network.WLAN(network.STA_IF)
>>> mac=sta.config('mac')
>>> mac
b'\x80}:\xb7\xc0\x8c'
>>> import ubinascii
>>> ubinascii.hexlify(mac).decode('utf8')
'807d3ab7c08c'
>>> ubinascii.hexlify(mac, ':').decode('utf8')
'80:7d:3a:b7:c0:8c'
這與 get_id() 用 machine.unique_id() 取得的結果是一樣的 :
>>> import machine
>>> ubinascii.hexlify(machine.unique_id()).decode('utf8')
'807d3ab7c08c'
>>> ubinascii.hexlify(machine.unique_id(), ':').decode('utf8')
'80:7d:3a:b7:c0:8c'
因為有時會用到用冒號區隔的 MAC 位址, 所以我用 STA 模式的 WLAN 物件寫了一個 get_mac() 函式添加到 xtools.py 模組中 :
def get_mac():
sta=network.WLAN(network.STA_IF)
mac=sta.config('mac')
return ubinascii.hexlify(mac, ':').decode('utf8')
注意, 此處必須使用 STA 模式才會取得與 machine.unique_id() 一樣的結果, 如果使用 AP 模式, 則得到的 MAC 位址將與 machine.unique_id() 取得者不同 :
>>> ap=network.WLAN(network.AP_IF)
>>> mac=ap.config('mac')
>>> mac
b'\x80}:\xb7\xc0\x8d'
>>> ubinascii.hexlify(mac, ':').decode('utf8')
'80:7d:3a:b7:c0:8d'
可見 AP 模式的 MAC 位址比 STA 模式的多了 1.
2022-12-03 補充 :
今天完成自訂的 set_ap() 函式添加到 xtools.py 裡面, 利用 AP 模式用手機連線 SSID=MicroPython-xxxxxx, 密碼為 micropythoN 的 ESP32/ESP8266 本身基地台, 用瀏覽器連線 192.168.4.1 會顯示 WiFi 基地台設定網頁, 連線成功會更新 config.py, 可用來現場設定要連線的 WiFi 基地台, 不需要寫死在 config.py 裡 :
def set_ap(led=2):
html='''
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
</head>
<body>
%s
</body>
</html>
'''
form='''
<form method='get' action='/update_ap'
style='width: max-content; margin: 10px auto'>
<h2 style='text-align: center; font-size: 20px'>設定 WiFi 基地台</h2>
<div style='display: block; margin-bottom: 20px'>
<label for='ssid' style='display: block; font-size: 16px'>SSID</label>
<input type='text' id='ssid' name='ssid'
style='padding: 10px 8px; width: 100%; font-size: 16px'>
</div>
<div style='display: block; margin-bottom: 20px'>
<label for='pwd' style='display: block; font-size: 16px'>Password</label>
<input type='text' id='pwd' name='pwd'
style='padding: 10px 8px; width: 100%; font-size: 16px'>
</div>
<button type="submit" style='width:100%;font-size: 16px'>連線</button>
</form>
'''
ok='''
<h2>WiFi 連線成功<br>IP : <a href={0}>{0}</a></h2>
<a href=192.168.4.1>
<button style="width:100%;font-size: 16px">重新設定</button>
</a>
'''
ng='''
<h2 style="text-align: center;">WiFi 基地台連線失敗<br>
按 Reset 鈕後重新設定</h2>
<a href="192.168.4.1">
<button style="width:100%;font-size: 16px">重新設定</button>
</a>
'''
wifi_led=Pin(led, Pin.OUT, value=1) # 預設熄滅板上 LED
ap=network.WLAN(network.AP_IF) # 開啟 AP 模式
ap.active(True)
sta=network.WLAN(network.STA_IF) # 開啟 STA 模式
sta.active(True)
import socket
addr=socket.getaddrinfo('192.168.4.1', 80)[0][-1] # 傳回 (ip, port)
s=socket.socket() # 建立伺服端 TCP socket
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # 網址可重複請求
s.bind(addr) # 綁定 192.168.4.1 的 80 埠
s.listen(5) # 最多同時 5 個連線
print('網頁伺服器正在監聽 : ', addr)
while True: # 監聽 192.168.4.1 的 80 埠
cs, addr=s.accept()
print('發現來自客戶端的連線 : ', addr)
data=cs.recv(1024)
request=str(data, 'utf8')
print(request, end='\n')
if request.find('update_ap?') == 5: # 檢查是否為更新之 URL
# 擷取請求參數中的 SSID 與密碼
para=request[request.find('ssid='):request.find(' HTTP/')]
ssid=para.split('&')[0].split('=')[1]
pwd=para.split('&')[1].split('=')[1]
sta.connect(ssid, pwd) # 連線 WiFi 基地台
start_time=time.time() # 紀錄起始時間
while not sta.isconnected(): # 連線 WiFi (15 秒)
wifi_led.value(0) # 讓板載 LED 閃爍
time.sleep_ms(300)
wifi_led.value(1)
time.sleep_ms(300)
if time.time()-start_time > 15: # 是否超過連線秒數
print('WiFi 連線逾時!')
break # 逾時跳出無限迴圈
# 確認是否連線成功
if sta.isconnected(): # WiFi 連線成功
print('WiFi 連線成功 : ', sta.ifconfig())
ip=sta.ifconfig()[0] # 取得 ip
print('取得 IP : ' + ip)
with open('config.py', 'w', encoding='utf-8') as f:
f.write(f'SSID="{ssid}"\nPASSWORD="{pwd}"') # 更新設定檔
cs.send(html % ok.format(ip)) # 回應連線成功頁面
for i in range(25): # 連線成功 : 快閃 5 秒
wifi_led.value(0)
time.sleep_ms(100)
wifi_led.value(1)
time.sleep_ms(100)
cs.close()
s.close()
return ip
else:
print('WiFi 連線失敗 : 請按 Reset 鈕後重設.')
wifi_led.value(1) # 連線失敗 : 熄滅 LED
cs.send(html % ng) # 回應連線失敗頁面
cs.close()
s.close()
return None
else: # 顯示設定 WiFi 頁面
cs.send(html % form) # 回應設定 WiFi 頁面
cs.close()
del cs, addr, data, requestxtools
同時也修改了 connect_wifi_led() 內連線成功後的通知方式, 原始的 xtools.py 是將板載 LED 設為 0 點亮 LED, 但這是在 Witty Cloud 與 D1 mini 等板子才會這樣, 在 ESP32-WROOM 卻剛好相反, value(0) 是熄滅 LED, 因此我將其與 set_ap() 一樣改為快閃 :
if sta.isconnected():
for i in range(25): # 連線成功 : 快閃 5 秒
wifi_led.value(0)
time.sleep_ms(100)
wifi_led.value(1)
time.sleep_ms(100)
print("network config:", sta.ifconfig())
return sta.ifconfig()[0]
即連線中是慢閃 (300 ms), 連線成功變快閃 (100 ms).
2022-12-07 補充 :
經測試發現上面我自訂添加地的 tw_now() 函式有 bug, 在網路未連線或 NTP 伺服器查詢失敗時從 RTC 讀取的本地時間會加上 28800 秒變成超前正確時間 8 小時, 改正如下 :
def tw_now():
try:
print('Querying NTP server and set RTC time ... ', end='')
ntptime.settime()
print('OK.')
delta=28800 # 從 NTP 取得的是 UTC 時間, 要加 28800 秒
except:
print('Failed.')
delta=0 # 從 localtime() 取得的是本地時間, 不必調整
utc_epoch=time.mktime(time.localtime())
Y,M,D,H,m,S,ms,W=time.localtime(utc_epoch + delta)
t='%s-%s-%s %s:%s:%s' % \
(Y, pad_zero(M), pad_zero(D), pad_zero(H), pad_zero(m), pad_zero(S))
return t
在連網情況下每次呼叫 tw_now() 會從 NTP 取得 UTC 時間更新 RTC (同步); 在斷網或 NTP 無回應情況下由 localtime() 取得的就是本地時間, 不需加 8 小時, 測試如下 :
MicroPython v1.19.1 on 2022-06-18; ESP module with ESP8266
Type "help()" for more information.
>>> import xtools
>>> xtools.tw_now() # 未連網情況下呼叫 tw_now()
Querying NTP server and set RTC time ... Failed.
'2022-12-07 22:02:21'
>>> xtools.connect_wifi_led() # 連網
Connecting to network...
network config: ('192.168.2.110', '255.255.255.0', '192.168.2.1', '168.95.1.1')
'192.168.2.110'
>>> xtools.tw_now() # 連網後呼叫 tw_now() 但 NTP 無回應
Querying NTP server and set RTC time ... Failed.
'2022-12-07 22:02:58'
>>> xtools.tw_now() # 連網後呼叫 tw_now() NTP 有回應
Querying NTP server and set RTC time ... OK.
'2022-12-07 22:03:39'
可見不論有無連網都能取得正確的台灣時間 (有連網且 NTP 有回應時就會同步 RTC).
沒有留言:
張貼留言