2022年11月25日 星期五

露天購買燈座 10 個

上個月向露天賣家買的 11 個燈座用掉 10 個在獨立型太陽能供電系統中 (壞掉 1 個), 估計還需要至少 6 個, 昨天再向該賣加購買 10 個燈座 : 




1 個 19 元, 1 號與 2 號各買 5 個免運合計 190 元. 


MicroPython 學習筆記 : ESP8266/ESP32 網路存取測試 (二)

本篇繼續 MicroPython 網路存取之網頁伺服器測試, 我五年前直接使用 socket 模組測試過網頁伺服器, 這回則改用陳會安老師大作 "超簡單Python/MicroPython物聯網應用" 這本書中提供的 xtools.py 與 ESP8266WebServer.py 等函式庫重測, 程式碼會更精簡, 我把這當作是睽違五年後的 MicroPython 複習, 看看能否順便將之前想做的物聯網專案一併做完.   



5. 用 ESP8266WebServer 模組建立網頁伺服器 :

ESP8266WebServer.py 是一個極輕量的網頁伺服器模組, 可用來在 ESP8266/ESP32 開發板上建立網頁伺服器, 這比直接用 socket 實作簡單方便得多, 且支援網頁模板用法, 但目前僅支援客戶端用用 GET 方法提出請求. 此模組最新版本可在 GitHub 下載 (8 KB) :


但是我使用最新版去測試時會出現如下的 TypeError :

Traceback (most recent call last):
  File "<stdin>", line 24, in <module>
  File "ESP8266WebServer.py", line 60, in handleClient
  File "ESP8266WebServer.py", line 215, in handle
TypeError: function takes 2 positional arguments but 5 were given

應該是新版的 handlers 函式介面的參數增加所致, 故以下測試使用 "超簡單Python/MicroPython物聯網應用" 這本書範例程式所附的舊版模組, 我將其放在 GitHub (7 KB) :


直接用 Socket 建立網頁伺服器的原始做法參考以前的文章 : 


下面先來測試可回應 'Hello World!' 的網頁伺服器. 

首先來檢視 ESP8266WebServer 模組的內容 :

MicroPython v1.19.1 on 2022-06-18; ESP module with ESP8266

Type "help()" for more information.
>>> import ESP8266WebServer  
>>> dir(ESP8266WebServer)
['__class__', '__name__', 'close', 'machine', 'network', 'socket', 'uselect', 'os', 'poller', 'server', 'handlers', 'notFoundHandler', 'docPath', 'tplData', 'mimeTypes', 'begin', 'handleClient', 'handle', '__sendPage', 'err', 'ok', '__fileExist', 'onPath', 'onNotFound', 'setDocPath', 'setTplData']
>>> help(ESP8266WebServer)
object <module 'ESP8266WebServer'> is of type module
  handle -- <function handle at 0x3ffeff30>
  mimeTypes -- {'.png': 'image/png', '.jpg': 'image/jpg', '.css': 'text/css'}
  close -- <function close at 0x3ffefdf0>
  os -- <module 'uos'>
  __name__ -- ESP8266WebServer
  socket -- <module 'lwip'>
  ok -- <function ok at 0x3ffeff10>
  network -- <module 'network'>
  onPath -- <function onPath at 0x3ffeff40>
  onNotFound -- <function onNotFound at 0x3ffeff50>
  __fileExist -- <function __fileExist at 0x3ffeff20>
  err -- <function err at 0x3ffeff00>
  notFoundHandler -- None
  __sendPage -- <function __sendPage at 0x3ffefef0>
  docPath -- /
  begin -- <function begin at 0x3ffefec0>
  uselect -- <module 'uselect'>
  handlers -- {}
  tplData -- {}
  setDocPath -- <function setDocPath at 0x3ffeff60>
  setTplData -- <function setTplData at 0x3fff00b0>
  handleClient -- <function handleClient at 0x3ffefee0>
  server -- <socket state=0 timeout=-1 incoming=0 off=0>
  machine -- <module 'umachine'>
  poller -- <poll>

常用函式如下表 : 


 ESP8266WebServer 函式 說明
 begin(port) 指定 port=80 啟動網頁伺服器
 ok(socket, code, mime, html) 以 code='200', mime='text/html' 回應 html 字串
 onPath(path, handler) 指定請求路徑 path 時之處理函式 handler
 setDocPath(path) 指定存放 HTML 文件之路徑 path
 setTplData(data) 指定要傳給模板檔案 index.p.html 的字典變數 data
 handleClient() 在無限迴圈中呼叫此函式以監聽是否有客戶端請求出現


使用 ESP8266WebServer 建立網頁伺服器首先要匯入 xtools, config, 以及 ESP8266WebServer, 然後呼叫 xtools.connect_wifi_led() 連線 WiFi 基地台 :

>>> import xtools   
>>> import config   
>>> ip=xtools.connect_wifi_led(config.SSID, config.PASSWORD)   
>>> print("ip : ", ip)    
network config: ('192.168.43.54', '255.255.255.0', '192.168.43.1', '192.168.43.1')
ip :  192.168.43.54

這個 ip 就是伺服器的網址. 接著用長字串定義要回應的網頁內容變數 html :

>>> html="""  
<html>  
<head> 
   <meta charset="utf-8">
   <meta name="viewport" content="width=device-width,initial-scale=1">
</head>
<body>
   <h1>Hello World!</h1>
</body>
</html>"""

接著定義一個伺服器根目錄請求處理函式 handleRoot() : 

>>> def handleRoot(socket, args):    
    global html    
    ESP8266WebServer.ok(socket, "200", "text/html", html)     

此函式要傳入兩個參數 socket 與 args, 其中 socket 為模組內建立的 Socket 物件, 而 args 則是 GET 請求所傳遞的參數字典, 例如若請求網址為 /?para1=a&para2=b 則傳入之 args 為 {'para1': 'a', 'para2': 'b'}, 此例因為是請求根目錄沒有傳遞參數所以用不到, 而是直接用 global 宣告取用外部全域變數 html (即要回應之網頁內容) 後呼叫 ESP8266WebServer.ok() 函式將 html 網頁內容回應給客戶端. 

然後呼叫 ESP8266WebServer.begin() 啟動網頁伺服器, 傳入參數是埠號, 一般是 80 埠 : 

>>> ESP8266WebServer.begin(80)    

接下來呼叫 ESP8266WebServer.onPath() 定義路由 (routing), 其第一參數為資源路徑, 此處為網站根目錄 '/'; 第二參數為對應此資源請求之路由處理函式, 也就是上面所定義的回應函式 handleRoot : 

>>> ESP8266WebServer.onPath("/", handleRoot)       

最後在無限迴圈中呼叫 ESP8266WebServer.handleClient() 函式監視是否有客戶端請求即可 : 

>>> while True:     
    ESP8266WebServer.handleClient()     

這時在瀏覽器網址列輸入上面的伺服器連線網址即可看到 'Hello World!' 頁面了 : 


連線時會出現找不到 favicon.ico, 這是正常的, 因為網站上並未提供此圖示檔 :

/favicon.ico
Not Found.

限於通訊能力, ESP8266/ESP32 的 STA 介面只能容許五個客戶端同時連線. 由於伺服器是一個無限迴圈, 所以如果要停止伺服器只能用按板子上的 Reset 鍵方式終止此迴圈. 

以上測試完整程式碼如下 : 


測試 5-1 : 回應 Hello World! 的簡單網頁伺服器 [看原始碼] 

import ESP8266WebServer
import xtools
import config

ip=xtools.connect_wifi_led(config.SSID, config.PASSWORD) 

html="""
<html>
<head>
   <meta charset="utf-8">
   <meta name="viewport" content="width=device-width,initial-scale=1">
</head>
<body>
   <h1>Hello World!</h1>
</body>
</html>"""

def handleRoot(socket, args):       # 目錄請求之處理函式
    global html
    ESP8266WebServer.ok(socket, "200", "text/html", html)   # 回應網頁
    
ESP8266WebServer.begin(80)
ESP8266WebServer.onPath("/", handleRoot)     # 定義根目錄請求之處理函式
while True:
    ESP8266WebServer.handleClient()    # 監聽 80 埠是否有客戶端請求

ESP8266WebServer 模組支援網頁文件路徑功能, 這樣就可以把網頁檔案單獨存成 .htm 或 .html 檔放在開發板的特定檔案路徑下, 然後呼叫 ESP8266WebServer.setDocPath() 函式指定網頁檔路徑即可, 不需要像上面範例那樣在 MicroPython 程式中定義 html 變數來存放回應網頁. 

首先在本機工作目錄下建立一個 www 資料夾, 將上面範例的 html 變數內容存成 www 底下的 index.htm 檔 : 

<!--  index.htm -->
<html>
<head>
   <meta charset="utf-8">
   <meta name="viewport" content="width=device-width,initial-scale=1">
</head>
<body>
   <h1>Hello World!</h1>
</body>
</html>

然後將 www 資料夾上傳到板子的根目錄下 : 





這是因為 Thonny 的上傳功能只能傳到根目錄下的關係. 最後用下列指令指定網頁文件檔案在 www 下, 替換上面範例的 ESP8266WebServer.onPath() 而且也不需要定義 handlerRoot() 函式 :

ESP8266WebServer.setDocPath("/www")  

完整程式碼如下 : 


測試 5-2 : 使用 setDocPath() 設定網頁文件路徑 [看原始碼] 

import ESP8266WebServer
import xtools
import config

ip=xtools.connect_wifi_led(config.SSID, config.PASSWORD) 
  
ESP8266WebServer.begin(80)
ESP8266WebServer.setDocPath("/www") 
while True:
    ESP8266WebServer.handleClient()

注意, 這時在瀏覽器網址列輸入網址時必須輸入完整的網頁路徑 (例如 192.168.43.54/www/index.htm) 才會顯示與上面範例 1 之網頁結果 :


如果只輸入 IP (例如 192.168.43.54) 會得到 "Bad request" 回應 (400) :




如果只輸入到資料夾 192.168.43.54/www 則顯示 "Not found" : 



這是測試時必須注意的, 否則會誤以為程式有問題. 不過這與我的期待不符, 我以為呼叫 setDocPath() 後輸入網址 192.168.43.54 就應該要自動到 www 底下找預設的首頁檔 index.htm 或 index.html 才對, 但實測結果卻不是這樣. 其實不呼叫 setDocPath() 還是可以運作, 所以覺得此版的 setDocPath() 功能也許尚不完整. 

參考 :


另外 ESP8266WebServer 模組也提供網頁模板 (Template) 功能, 可透過呼叫 setTplData() 函式將字典型態的變數傳遞給模板網頁, 用法如下 :
  • 模板網頁檔名必須以 .p.htm 或 .p.html 結尾
  • 所傳遞之變數 var (即字典的 key) 以中括號 {var} 嵌入模板網頁中
首先將上面 /www 底下的 index.htm 改名為 index.p.html, 注意, 與上面兩個範例用 .htm 或 .html 都可以的情況不同, 此處一定要用 .p.html, 如果用 .p.htm 變數將無法傳遞, 而是直接顯示變數名稱. 其次, 編輯 index.p.html 網頁內容, 在 Hello 後面嵌入一個變數 name 為 Hello {name} :

<!--  index.p.html -->
<html>
<head>
   <meta charset="utf-8">
   <meta name="viewport" content="width=device-width,initial-scale=1">
</head>
<body>
   <h1>Hello {name}</h1>
</body>
</html>

然後將 /www 資料夾上傳到板子 :




接下來就可以呼叫 xtools.connect_wifi_led() 將板子連上 WiFi, 呼叫 ESP8266WebServer.begin() 啟動網頁伺服器, 呼叫 ESP8266WebServer.setDocPath() 設定 HTML 文件資料夾 (雖然這指令如上例所述似乎可有可無) : 

ESP8266WebServer.begin(80)
ESP8266WebServer.setDocPath("/www") 

然後定義要傳給模板網頁 index.p.html 的變數 name, 將其放入 data 字典中傳給 ESP8266WebServer.setTplData() :

data={"name": "Tony"}
ESP8266WebServer.setTplData(data)

最後在無限迴圈中呼叫 ESP8266WebServer.handleClient() 函式監視是否有客戶端請求 : 

while True:
    ESP8266WebServer.handleClient()

完整程式碼如下 : 


測試 5-3 : 呼叫 setTplData() 函式傳遞變數給 .p.html 網頁模板 [看原始碼] 

import ESP8266WebServer
import xtools
import config

ip=xtools.connect_wifi_led(config.SSID, config.PASSWORD)  
ESP8266WebServer.begin(80)              
ESP8266WebServer.setDocPath("/www") 
data={"name": "Tony"}
ESP8266WebServer.setTplData(data)    
while True:
    ESP8266WebServer.handleClient()

與上面範例 2 一樣, 瀏覽器網址列必須輸入完整的網頁路徑才會顯示正確的結果 (例如此處為 192.168.43.54/www/index.p.html), 否則會出現 Bad request 或 Not found. 

結果如下 :



可見字典 data 中的變數 name 已經傳遞到模版網頁 index.p.html 並內嵌到 {name} 裡面了. 

如果有多個頁面可以用超連結或按鈕來切換, 下面範例使用兩個網頁 page1.htm 與 page2.htm 利用超連結來切換, 先在本機建立 www2 資料夾, 裡面放兩個網頁檔 :

 第一頁網頁內容如下 : 

<!--  page1.html -->
<html>
<head>
   <meta charset="utf-8">
   <meta name="viewport" content="width=device-width,initial-scale=1">
</head>
<body>
   <h1>這是第 1 頁</h1>
   <h1><a href="/www2/page2.html">前往第 2 頁</a></h1>
</body>
</html>

第二頁網頁內容如下 : 

<!--  page2.html -->
<html>
<head>
   <meta charset="utf-8">
   <meta name="viewport" content="width=device-width,initial-scale=1">
</head>
<body>
   <h1>這是第 2 頁</h1>
   <h1><a href="/www2/page1.html">返回第 1 頁</a></h1>
</body>
</html>

將此 www2 資料夾上傳板子, 然後如上面範例 2 啟動伺服器, 完整程式碼如下 :


測試 5-4 : 用超連結切換多頁面 [看原始碼] 

import ESP8266WebServer
import xtools
import config

ip=xtools.connect_wifi_led(config.SSID, config.PASSWORD) 

ESP8266WebServer.begin(80)              
ESP8266WebServer.setDocPath("/www2") 

while True:
    ESP8266WebServer.handleClient()

瀏覽 page1.htm 完整網址 (例如 192.168.43.54/www2/page1.html), 結果如下 :



按 "前往第 2 頁" 超連結會切換到 page2.htm 網頁 : 



按 "返回第 1 頁" 又回到 page1.html 了.

前面的測試顯示, 我使用的這版 ESP8266WebServer.setDocPath() 並不會自動去指定資料夾下找尋首頁檔, 而是必須用完整的網頁路徑. 解決辦法就是以 RESTful 方式使用多個請求處理函式. 

下面範例改編自前一個測試, 網站總共有三個網頁 : index.html, page1.html, 以及 page2.html, 透過超連結以 RESTful 網址 /, /page1, 以及 /page2 切換, 首先在本機鍵一個 www3 資料夾, 編輯如下三個網頁檔後以 utf-8 編碼存檔 (因為網頁內容有中文) :

下面是首頁檔案 : 

<!--  index.html -->
<html>
<head>
   <meta charset="utf-8">
   <meta name="viewport" content="width=device-width,initial-scale=1">
</head>
<body>
   <h1>這是首頁</h1>
   <h1><a href="/page1">前往第 1 頁</a></h1>
</body>
</html>

下面是第一頁檔案 : 

<!--  page1.html -->
<html>
<head>
   <meta charset="utf-8">
   <meta name="viewport" content="width=device-width,initial-scale=1">
</head>
<body>
   <h1>這是第 1 頁</h1>
   <h1><a href="/page2">前往第 2 頁</a></h1>
   <h1><a href="/">返回首頁</a></h1>
</body>
</html>

下面是第二頁檔案 : 

<!--  page2.html -->
<html>
<head>
   <meta charset="utf-8">
   <meta name="viewport" content="width=device-width,initial-scale=1">
</head>
<body>
   <h1>這是第 2 頁</h1>
   <h1><a href="page1">返回第 1 頁</a></h1>
   <h1><a href="/">返回首頁</a></h1>
</body>
</html>

將 www3 資料夾上傳到板子 : 




接下來修改 MicroPython 程式, 為 /, /page1, 與 /page2 這三個網址建立請求處理函式 :

def handleRoot(socket, args): 
    with open('./www3/index.html', 'r', encoding='utf-8') as f:
        html=f.read()
        print(html)
    ESP8266WebServer.ok(socket, "200", "text/html", html)
def handlePage1(socket, args): 
    with open('./www3/page1.html', 'r', encoding='utf-8') as f:
        html=f.read()
        print(html)
    ESP8266WebServer.ok(socket, "200", "text/html", html) 
def handlePage2(socket, args): 
    with open('./www3/page2.html', 'r', encoding='utf-8') as f:
        html=f.read()
        print(html)
    ESP8266WebServer.ok(socket, "200", "text/html", html)

這三個函式都使用 open() 開啟位於 www3 資料夾下的網頁檔 (須指定 utf-8 編碼), 讀取後傳給 ESP8266WebServer.ok() 回應給客戶端. 

然後呼叫 onPath() 將網址對應到請求處理函式 :

ESP8266WebServer.onPath("/", handleRoot) 
ESP8266WebServer.onPath("/page1", handlePage1) 
ESP8266WebServer.onPath("/page2", handlePage2)

完整程式碼如下 : 


測試 5-5 : 用多個 onPath() 切換多個 RESTful 網址 [看原始碼] 

import ESP8266WebServer
import xtools
import config

ip=xtools.connect_wifi_led(config.SSID, config.PASSWORD) 

ESP8266WebServer.begin(80)              

def handleRoot(socket, args): 
    with open('./www3/index.html', 'r', encoding='utf-8') as f:
        html=f.read()
        print(html)
    ESP8266WebServer.ok(socket, "200", "text/html", html)
def handlePage1(socket, args): 
    with open('./www3/page1.html', 'r', encoding='utf-8') as f:
        html=f.read()
        print(html)
    ESP8266WebServer.ok(socket, "200", "text/html", html) 
def handlePage2(socket, args): 
    with open('./www3/page2.html', 'r', encoding='utf-8') as f:
        html=f.read()
        print(html)
    ESP8266WebServer.ok(socket, "200", "text/html", html)

ESP8266WebServer.onPath("/", handleRoot)               # / 處理函式
ESP8266WebServer.onPath("/page1", handlePage1)   # /page1 處理函式
ESP8266WebServer.onPath("/page2", handlePage2)   # /page2 處理函式

while True:
    ESP8266WebServer.handleClient()

瀏覽器網址列只要輸入 ip 即顯示首頁 index.html :



按 "前往第 1 頁" 會切換到 page1.html : 



按 "前往第 2 頁" 會切換到 page2.html : 



按 "返回首頁" 切換至 index.html, 按 "返回第 1 頁" 切換至 page1.html; 按 "返回第 2 頁" 切換至 page2.html, 這樣就完全不用去管網址列的網頁檔路徑了. 

下面範例使用如下超連結按鈕傳遞參數 led 來控制板載 LED (GPIO2) 的明滅 :

/?led=on : 開啟 LED
/?led=off : 關閉 LED

傳入 URL 請求處理函式的第二個參數 args 將是如下的字典 :

{'led': 'on'}
{'led': 'off'}

這樣就可以透過判斷 led 參數之值 args['led'] 是 'on' 還是 'off' 來設定 GPIO2 的值以控制板載 LED 的明滅了. 

首先在本機建立一個 www4 資料夾, 建立一個模板網頁 index.p.html :

<!--  index.p.html -->
<!DOCTYPE html>
<html>
<head>
   <meta charset="utf-8">
   <meta name="viewport" content="width=device-width,initial-scale=1">
</head>
<body>
   <h1>LED 狀態 : {0}</h1>
   <a href="/?led=on"><button style="font-size:40px;">點亮</button></a>
   <a href="/?led=off"><button style="font-size:40px;">熄滅</button></a>
</body>
</html>

此模板網頁中有兩個超連結按鈕, 透過 href 屬性設定 GET 方法的請求網址 /?led=on 與 /?led=off 分別用來控制板載 LED (GPIO2) 的明滅. 其次, LED 狀態後面的 {0} 用在程式中以字串的 format() 方法將 LED 狀態字串 ('點亮' 或 '熄滅') 嵌入此位置. 

將此 www4 資料夾用 Thonny 上傳到開發板根目錄下 :




然後從 machine 模組匯入 Pin 類別, 先將板載 LED 設為關閉 (滅) :

from machine import Pin
led=Pin(2, Pin.OUT)
led.value(0)

接著撰寫根目錄請求處理函式 :

def handleRoot(socket, args): 
    print(args)
    with open('./www4/index.p.html', 'r', encoding='utf-8') as f:
        html=f.read()
    state='熄滅' 
    if 'led' in args:
        if args['led']=='on':
            state='點亮'
            led.value(1)
        elif args['led']=='off':
            state='熄滅'
            led.value(0)
    response=html.format(state)
    ESP8266WebServer.ok(socket, "200", "text/html", response)

此函式從 /www4 底下讀取模板網頁 index.p.html 為 html 字串, 然後判斷傳入參數字典 args 是否有 led 參數, 有的話就依據其值為 'on' 或 'off' 來控制 LED 的明滅, 並將狀態字串利用 format() 方法嵌入 html 字串的 {0} 位置, 完整原始碼如下 : 


測試 5-6 : 用超連結按鈕點亮與熄滅板載 LED [看原始碼] 

import ESP8266WebServer
import xtools
import config
from machine import Pin 

ip=xtools.connect_wifi_led(config.SSID, config.PASSWORD) 
led=Pin(2, Pin.OUT)
led.value(1)     # 預設狀態: 熄滅
ESP8266WebServer.begin(80)              
ESP8266WebServer.setDocPath("/www4")

def handleRoot(socket, args): 
    print(args)
    with open('./www4/index.p.html', 'r', encoding='utf-8') as f:
        html=f.read()
    state='熄滅' 
    if 'led' in args:
        if args['led']=='on':
            state='點亮'
            led.value(0)    # GPIO2 板載 LED 輸出 low 為亮
        elif args['led']=='off':
            state='熄滅'
            led.value(1)    # GPIO2 板載 LED 輸出 high 為滅
    response=html.format(state)
    ESP8266WebServer.ok(socket, "200", "text/html", response)

ESP8266WebServer.onPath("/", handleRoot) 
data={"state": "熄滅"}
ESP8266WebServer.setTplData(data)

while True:
    ESP8266WebServer.handleClient()

注意, 由於板載 LED 使用 sink current 接法, 亦即 LED 是陽極經限流電阻接 GPIO2, 陰極則接至 VCC, 故輸出 0 為點亮 LED, 輸出 1 熄滅 LED (負邏輯), 這在 NodeMCU, D1 mini, 或 Witty Cloud 開發板皆是如此 (但 Witty Cloud 上的全彩 LED 則使用 source current, 即正邏輯). 

執行後在瀏覽器輸入網址, 例如 192.168.2.141 會顯示 index.p.html 網頁, 預設狀態是熄滅 : 



按 "點亮" 鈕板載 LED 亮, 且網頁中 LED 狀態也顯示 "點亮" :




按 "熄滅" 鈕板載 LED 會暗掉, LED 狀態恢復為 "熄滅". 

上面的範例每次按 "點亮" 或 "熄滅" 按鈕都會重新載入 index.p.html 網頁, 下面改用 jQuery 函式庫所提供的 Ajax 非同步功能向伺服器提出請求, 利用伺服器回傳的資料更新網頁中特定元素的內容, 不需要重新載入網頁即可改變網頁內容. 

首先在本機建立一個 www5 資料夾, 並在底下建立一個 index.html 網頁 :

<!--  index.html -->
<!DOCTYPE html>
<html>
<head>
   <meta charset="utf-8">
   <meta name="viewport" content="width=device-width,initial-scale=1">
   <script src="https://code.jquery.com/jquery-3.6.1.min.js"></script>
</head>
<body>
   <h1>LED 狀態 : <span id='status'>熄滅</span></h1>
   <button id="btn_on" style="font-size:40px;">點亮</button>
   <button id="btn_off" style="font-size:40px;">熄滅</button>
   <script>
     $(function(){
       $("#btn_on").click(function() {
         $.get({
           url: "/on",
           dataType: "html",
           success: function(data) {
             $("#status").html(data);
             }     
           });
         });
       $("#btn_off").click(function() {
         $.get({
           url: "/off",
           dataType: "html",
           success: function(data) {
             $("#status").html(data);
             }     
           });
         });
       });
   </script>
</body>
</html>

此網頁在 head 標籤內從 jQuery 官網 CDN 匯入 jQuery 函式庫 (目前最新為 3.6.1 版), 雖然也可以下載後上傳到 ESP8266/ESP32 開發板, 但這會佔 Flash 空間語頻寬, 不建議這麼做. 其次, 網頁中的 LED 狀態顯示改用一個具有 id=status 屬性的 span 元素取代, 預設狀態為 "熄滅". 按鈕也直接使用具有 id 屬性 btn_on 與 btn_off 之 button 元素, 去除超連結. 在按下 "點亮" 與 "熄滅" 按鈕時呼叫 jQuery 的捷徑函式 $.get() 對 /on 與 /off 這兩個網址提出 Ajax 請求, 利用傳回來 data 更新 span 元素的內容. 

關於 jQuery 的 Ajax 函式參考 : 


將 www5 資料夾上傳開發板 :




在 MicroPython 網頁伺服器程式中要針對根目錄 / 與兩個按鈕請求 URL 進行路由處理 : 

def handleRoot(socket, args):
    with open('./www5/index.html', 'r', encoding='utf-8') as f:
        html=f.read()
    ESP8266WebServer.ok(socket, "200", "text/html", html)
def handleOn(socket, args): 
    led.value(0) 
    ESP8266WebServer.ok(socket, "200", "text/html", "點亮")
def handleOff(socket, args): 
    led.value(1) 
    ESP8266WebServer.ok(socket, "200", "text/html", "熄滅")

ESP8266WebServer.onPath("/", handleRoot) 
ESP8266WebServer.onPath("/on", handleOn) 
ESP8266WebServer.onPath("/off", handleOff)

完整程式碼如下 :


測試 5-7 : 用 jQuery 的 Ajax 功能點亮與熄滅板載 LED [看原始碼] 

import ESP8266WebServer
import xtools
import config
from machine import Pin 
         
def handleRoot(socket, args):
    with open('./www5/index.html', 'r', encoding='utf-8') as f:
        html=f.read()
    ESP8266WebServer.ok(socket, "200", "text/html", html)

def handleOn(socket, args): 
    led.value(0)   # GPIO2 板載 LED 輸出 low 為亮
    ESP8266WebServer.ok(socket, "200", "text/html", "點亮")

def handleOff(socket, args): 
    led.value(1)   # GPIO2 板載 LED 輸出 high 為滅
    ESP8266WebServer.ok(socket, "200", "text/html", "熄滅")

ip=xtools.connect_wifi_led(config.SSID, config.PASSWORD) 
led=Pin(2, Pin.OUT)
led.value(1)
ESP8266WebServer.setDocPath("/www5")
ESP8266WebServer.begin(80)   
ESP8266WebServer.onPath("/", handleRoot) 
ESP8266WebServer.onPath("/on", handleOn) 
ESP8266WebServer.onPath("/off", handleOff) 

while True:
    ESP8266WebServer.handleClient()

結果與上面超連結按鈕範例一樣 (但 index.html 網頁只會載入一次). 

2022年11月24日 星期四

好站 : Micropython 錯誤碼 (Error Codes)

在寫 MicroPython 程式時不免會遇到板子吐出錯誤碼, 今天找到一個網頁羅列各種常見的錯誤碼 :


不過這只是簡單的指引而已, 真正要能找出錯誤之處, 還是要根據原始碼去推敲. 

2022年11月22日 星期二

好站 : 線上繁簡轉換

最近需要將一篇簡體文章轉成繁體, 找到下面這個線上轉換的網站 : 





開啟 WORD 轉換有點麻煩 (不常用會忘記功能項在哪), 這個直接貼上即可轉換. 

2022年11月21日 星期一

2022 年第 47 周記事

都已經 11 月底了, 我到現在還是穿薄夾克, 往年這時候厚夾克早已上身, 這氣候真的變很多, 高雄今年的冬天只有早晚稍涼, 近中午根本就是夏天. 

二哥週三去中壢與教授見面, 教授說他只收四個學生, 二哥是第四個, 以往都是根據 email 先後決定, 而二哥只比第五位的信早發了六分鐘. 雖然已經決定去中央, 但因原先台中訂的旅館要退也麻煩, 乾脆回程也去參加面試, 看看也好. 

本周繼續獨立行太陽能供電系統的室內配線, 週六完成儲物間 LED 照明燈, 同時延伸到客廳. 週日早上完成客廳插座盒, 但還沒將監視器與無線基地台等負載接上. 週日下午完成車庫前燈座裝設, 以及廚房開關盒鎖定, 但線都還沒拉, 下周繼續. 最近忙這項目周末已一個月沒去爬獅形頂了, 希望下周能全部完工. 





水某住清水里的阿姨前幾天因病過世, 以前也常照面很熟, 所以我週日早上一大早去捻香送行. 現在鄉下習俗改了, 很多喪家都不再收禮, 因為少子化與城市化關係, 人際關係不像以前農業社會緊密, 以前收禮是禮尚往來, 但下一輩都不熟, 以後禮怎麼還是個問題, 乾脆不收, 有來送就非常感謝. 

上週開始測試並整理 Pandas, 但本周因為想用 ESP32 製作智慧插座與擷取太陽能充電控制器資訊重新複習 MicroPython, 我通常無法一心二用, 所以 Pandas 又要暫時延一延了. 

料理實驗 : 阿慶師家常炒冬粉

我每隔兩周都會幫爸做螞蟻上樹 (炒冬粉), 以往做法都是先用油蔥酥將絞肉炒香起鍋備用, 然後原鍋鋪上大白菜或高麗菜, 再把浸軟的冬粉擺上去, 撒上炒香的絞肉, 倒入三碗水淋入醬油後蓋上鍋蓋悶煮, 等水差不多快乾時再加半碗水翻炒, 並酌加醬油調味, 作法輕鬆又簡單. 

最近在 Youtube 看到阿慶師的另類炒冬粉作法, 感覺也不難, 周末就試做了一次, 作法如下 :





材料 :





手邊沒有香菇與金勾蝦直接略去, 另外冬粉配蒜頭有點怪所以也沒加. 

作法 : 
  1. 胡蘿蔔切絲, 高麗菜切長條備用. 
  2. 起油鍋, 先下香菇大火炒香, 再放金勾蝦, 肉絲, 胡蘿蔔續炒 30 秒後放高麗菜與油蔥酥炒軟, 先關火調味, 醬油兩大匙, 烏醋一大匙, 兩杯量米杯水, 白胡椒粉與砂糖一小匙, 開大火煮滾後加入冬粉, 拌炒一分鐘後關火蓋上鍋蓋悶一分鐘, 然後開鍋開大火拌炒幾下關火即可. 




嗯, 新作法也很好吃 (其實冬粉怎麼煮都好吃). 

好站 : 小霸王的 Pico W 與 ESP32CAM 教學

周末這兩天晚上先後參加了小霸王尤博的遠距教學, 昨晚還為此延至晚上 10 點課程結束才從鄉下老家出發返回高雄, 今天才發現其實上課內容都有放在 YT 上, 事後再看也可以. 不論是 Pico W或 ESP32CAM 都是用 Arduino IDE 開發, 其實我比較偏愛用 MicroPython, 因為 C 語言實在太麻煩, 久沒碰真的會膽怯, 呵呵. 







 


我應該是最早跟小霸王買 ESP32CAM 的, 但很慚愧, 買到現在都還沒時間測試, 哈哈. 

PS : 小霸王的 ESP32CAM 天線自動切換功能不錯, 擴充版設計設想也很周到, 價格更是親民, 讓我很想去蝦皮買一組來玩.

好站 : Epoch & Unix Timestamp Conversion Tools

這兩天在測試 MicroPython 時發現這個網站, 它會即時顯示目前 Linux 的 Epoch, 也可以輸入時戳轉換成日期時間, 測試日期時間的功能可拿來相互驗證, 非常方便 :




2022年11月19日 星期六

MicroPython 學習筆記 : ESP8266/ESP32 網路存取測試 (一)

在上一篇文章中已將網路存取要用到的自訂模組上傳到開發板, 接下來就可以進行網路存取測試了. 本系列之前的文章參考 :


比較舊的 ESP8266/ESP32 測試文章參考 :



1. 連線 WiFi 基地台 :

以下以 ESP8266 開發板利用 xtools 函式庫的 connect_wifi_led() 函式連線 WiFi 熱點 : 

MicroPython v1.19.1 on 2022-06-18; ESP module with ESP8266
Type "help()" for more information.
>>> import xtools    
>>> import config     
>>> config.SSID  
'TonyNote8'   
>>> config.PASSWORD      
'a123456789'    
>>> ip=xtools.connect_wifi_led(config.SSID, config.PASSWORD)    
#5 ets_task(4020f560, 28, 3fff9ed8, 10)    
Connecting to network...   
network config: ('192.168.43.54', '255.255.255.0', '192.168.43.1', '192.168.43.1')   
>>> print(ip)    
'192.168.43.54'    
>>> id=xtools.get_id()   
>>> id   
b'14f5a800'          # MAC 位址
>>> type(id)     
<class 'bytes'>      # 型態為 bytes 

可見板載 LED 閃爍幾下後就連上基地台, connect_wifi_led() 函式會傳回開發板所取得之 ip; 呼叫 get_id() 則可取得 ESP8266 的 MAC 位址, 其傳回值為 bytes 串流物件, 可呼叫 encode('utf8') 方法轉成字串, 參考 :


>>> id.decode("utf-8")  
'14f5a800'

可見 MAC 已轉成字串. 

以上是在 Thonny 的互動環境測試的結果, 可將上面的程式碼寫成 main.py 上傳到板子, MicroPython 韌體相當於是一個微小版的作業系統, 每次板子重開機或 reset 時會先執行 boot.py, 然後會自動搜尋根目錄下是否有 main.py, 有的話就執行它, 執行程序如下 :




因此如果將連網的程式碼寫成如下的 main.py, 則開機後就會自動連上 WiFi 基地台了 :

# main,py 
import xtools    
import config   

ip=xtools.connect_wifi_led(config.SSID, config.PASSWORD)
mac=xtools.get_id().decode("utf-8")
print("ip : ", ip)
print("mac : ", mac)

上傳到開發板根目錄後按 reset 鈕 :

MicroPython v1.19.1 on 2022-06-18; ESP module with ESP8266
Type "help()" for more information.
Connecting to network...
network config: ('192.168.43.54', '255.255.255.0', '192.168.43.1', '192.168.43.1')  
ip :  192.168.43.54
mac :  14f5a800


如果專案需要視情況執行不同的 App, 則可以將 App 寫成含有 main() 函式的單獨程式, 例如 app1.py, 在 main.py 中判斷要執行哪一個 App, 然後於 main.py 內呼叫 app1.main() 即可將執行控制權交給 app1.py, 基本架構如下 :




範例程式如下, 首先是 App 程式 app1.py, 裡面要定義一個 main() 函式, 然後把 App 的邏輯寫在 main() 裡面, 然後用 if 判斷此程式是否是被呼叫的 (這時程式的內建隱含變數 __name__ 的值就是 '__main__', 如果是用 import 匯入就是 None, 避免 import 時 main() 就被執行一遍), 是的話就執行 main() : 

# app1.py
def main():
    #application codes are placed here  
    print('app1 執行中 ...')

if __name__ == "__main__":  
    main()  

其次是主程式 main.py, 裡面要先用 import 匯入會用到的全部 App (app1, app2, ...), 然後在符合條件時呼叫特定 App 的 main() 函式將執行控制權交給該 App, 例如 :

# main.py
import xtools    
import config
import app1    

ip=xtools.connect_wifi_led(config.SSID, config.PASSWORD)
mac=xtools.get_id()
print("ip : ", ip)
print("mac : ", mac)
if not ip:      # 連線成功會傳回 ip 字串, 否則為 None
    print('無法連線 WiFi 基地台')
else:
    app1.main()    # 連線成功才執行 App 程式


2. 掃描附近基地台 :

我將之前自己寫的掃瞄基地台函式添加到 xtools.py 中, 取名為 scan_ssid(), 只要呼叫它就會整齊地列出附近所有基地台的 ssid 與其功率強度, 例如 : 

>>> import xtools   
>>> xtools.scan_ssid()    
    EDIMAX-tony-Plus    5c:27:d4:f3:82:22     -75dBm
                JANE    cc:2d:21:42:9a:61     -84dBm
         EDIMAX-tony    80:1f:02:2d:5a:9e     -76dBm
          45N5F-2.4G    20:6a:94:3b:c9:7e     -93dBm


3. 日期時間測試 :

MicroPyhton 只支援 time (或 utime) 模組, 不支援 datetime 與 calendar, 所以用到日期時間時必須從 time 模組著手, 參考 : 


為了節省空間考量, MicroPython 的 utime 模組並未實作 ctime() 函式, 只能用 time(), maketime() 與 localtiome() 函式, time.time() 會傳回自 2000/1/1 起至今的累計秒數 (稱為 epoch, 但與 Linux 從 1970/1/1 起算日期不同), 而 time.localtime() 則傳回日期時間元組 (年, 月, 日, 時, 分, 秒, 星期, 天), 其中星期值為 0~6 (0 為星期天), 天值為 1~366 (一年中的第幾天), 例如 : 

>>> import time    
>>> time.time()    
78
>>> time.localtime()   
(2000, 1, 1, 0, 1, 25, 5, 1) 

time.maketime() 函式功能與 time.localtime() 相反, 它可以將 6 元素的日期時間元組轉回起自 2000/1/1 的 epoch 秒數, 例如 :

>>> time.mktime(time.localtime())    # 轉成起自 2000/1/1 的 epoch 秒數
368

因為板子內的 RTC 時鐘目前是 2000/1/1, 所以秒數僅 368 秒. 

事實上 time.localtime() 是透過 machine 模組內的 RTC 類別去查詢 ESP8266/ESP32 內部的即時時鐘 (RTC) 得到的日期時間, 這也可以直接用 machine 模組的 RTC 物件查詢, 參考 :  


>>> import machine     
>>> rtc=machine.RTC()               # 建立 RTC 物件
>>> rtc.datetime()                         # 取得 RTC 的日期時間
(2000, 1, 1, 5, 0, 4, 18, 586130)      # (年, 月, 日, 星期, 時, 分, 秒, 毫秒) 

但此 RTC 預設初始值是 2000/01/01 00:00:00, 必須透過 WiFi 連網後利用 ntptime 模組的 settime() 函式從 NTP 伺服器取得網路時間來同步, 這樣呼叫 time.localtime() 才會得到準確的時間. 

MicroPython 有內建 ntptime 模組以便與 NTP 伺服器連線取得 UTC 時間, 

>>> import ntptime   
>>> dir(ntptime)  
['__class__', '__name__', 'socket', 'struct', 'time', 'settime', 'NTP_DELTA', 'host']
>>> help(ntptime)    
object <module 'ntptime'> is of type module
  socket -- <module 'lwip'>
  NTP_DELTA -- 3155673600
  __name__ -- ntptime
  struct -- <module 'ustruct'>
  time -- <function time at 0x3ffeff50>
  host -- pool.ntp.org
  settime -- <function settime at 0x3fff0250> 

其中 host 變數用來儲存 NTP 伺服器網址, 預設為 pool.ntp.org, 亦可改用如下伺服器 : 
  • tock.stdtime.gov.tw
  • watch.stdtime.gov.tw
  • time.stdtime.gov.tw
  • clock.stdtime.gov.tw  
  • tick.stdtime.gov.tw 
  • time.nist.gov
NTP_DELTA 常數則是用來設定 NTP 與 Python 的基準時間差距, 因 NTP 時戳是從1900/1/1 起算的秒數; 但 Python 時戳卻是從 2000/1/1 起算的, 兩者差了 100 年, 所以要補上 36524 天 (含閏年)*24*60*60=3155673600 秒, 其值已預設到 NTP_DELTA 屬性 (勿改). 

ntp.time() 函式會傳回目前 NTP 伺服器的 epoch 秒數 (UTC/GMT 時區), 呼叫 ntp.settime() 函式會用 ntp.time() 來設定 ESP8266/ESP32 內部的 RTC 時鐘, 這樣就可以讓板子與網路時間同步了, 例如 :

同步前先查詢板子上的 RTC 時鐘 : 

>>> time.time()         
2172
>>> rtc.datetime()      
(2000, 1, 1, 5, 0, 36, 15, 615714)

可見此時 RTC 時鐘仍是預設值. 接著查詢 NTP 時間並呼叫 ntp.settime() 來同步 :

>>> ntptime.time()       
722416034
>>> ntptime.settime()    

注意, 以前 ntptime.settime() 會傳回所取得之時間, 現在新版的不會. 同步後查詢板子上的 RTC 時鐘 :

>>> time.time()      
722416046
>>> rtc.datetime()     
(2022, 11, 22, 1, 7, 7, 31, 572148)
>>> time.localtime()      
(2022, 11, 22, 7, 7, 42, 1, 326)

現在時間是下午 3 點多, 可見從 NTP 取得的是 UTC 時間, 必需再加上 28800 秒才是台灣時區的秒數 (UTC+8). xtools.py 工具函式庫的 format_datetime() 函式可用來製作 'YYYY-MM-DD HH:mm:SS' 格式的日期時間字串, 只要將 time.localtiome() 傳入 xtools.format_datetime() 即可, 例如 : 

>>> xtools.format_datetime(time.localtime())     
'2022-11-22 07:15:05'

但是此 format_datetime() 函式並沒有處理台灣時區秒差問題, 所以我參考自己之前寫的函式修改為 tw_now() 加到 xtools 裡面來彌補此缺陷 (需匯入 ntptime) :

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

此函式會先呼叫 ntptime.settime() 來同步內部 RTC 時鐘, 因為跑一段時間後 RTC 會與 NTP 伺服器有秒差, 但只要每次呼叫 tw_now() 就會同步一次, 可避免秒差越來越大. 將新版 xtools.py 上傳到板子, 然後在 Thonny 按 "執行/停止重新值行後端程式" 重設板子, 再次匯入 xtools 呼叫 tw_now() 即可得到台灣時間了 (此處使用 try except 避免 NTP 伺服器連線失敗) :

MicroPython v1.19.1 on 2022-06-18; ESP32 module with ESP32

Type "help()" for more information.
>>> import xtools   
>>> xtools.tw_now()    
'2022-11-22 15:27:30'   

這樣就得到台灣時間而非 UTC/GMT 時間了. 


4. 用 urequests 擷取網頁 :

較舊版的 MicroPython 韌體中有實作 urllib 模組, 但目前 (v1.19.1) 已經沒有了 : 

>>> import urllib    
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ImportError: no module named 'urllib'

但 MicroPython 內建了 CPython 第三方模組 requests 的子集 urequest 模組可用來擷取網頁, 我五年前曾經測試過, 參考 :


先檢視 urequests 模組內容 : 

>>> import urequests  
>>> dir(urequests)  
['__class__', '__name__', 'get', '__file__', 'put', 'usocket', 'Response', 'request', 'head', 'post', 'patch', 'delete']
>>> help(urequests)    
object <module 'urequests' from 'urequests.py'> is of type module
  put -- <function put at 0x3ffe5b30>
  post -- <function post at 0x3ffe5b20>
  usocket -- <module 'usocket'>
  patch -- <function patch at 0x3ffe5b40>
  request -- <function request at 0x3ffe5960>
  __file__ -- urequests.py
  __name__ -- urequests
  delete -- <function delete at 0x3ffe5b50>
  head -- <function head at 0x3ffe5890>
  Response -- <class 'Response'>
  get -- <function get at 0x3ffe58a0>

與以前相比可知新版韌體多了一個 Response 類別, 此類別用來放伺服器對 HTTP 請求之回應. 其實最常用的是 get() 與 post() 方法, 分別用來向伺服器提出 GET 與 POST 請求, 說明文件參考 :


首先用 urequest 擷取 MicroPython 官網測試網頁的範例 : 

>>> import urequests      
>>> r=urequests.get("http://micropython.org/ks/test.html")    
>>> r    
<Response object at 3fff3870>   

可見 get() 函式會傳回一個 Response 物件, 裡面存放伺服器對 HTTP 請求的回應, 先用 help() 檢視 Response 物件內容 :

>>> dir(r)   
['__class__', '__init__', '__module__', '__qualname__', 'close', '__dict__', 'encoding', 'text', 'json', 'status_code', 'reason', 'raw', '_cached', 'content']
>>> help(r)    
object <Response object at 3fff3870> is of type Response
  text -- <property>
  __init__ -- <function __init__ at 0x3ffe58d0>
  __qualname__ -- Response
  close -- <function close at 0x3ffe58c0>
  content -- <property>
  json -- <function json at 0x3ffe5950>
  __module__ -- urequests

其中 ststus_code 屬性存放 HTTP 請求的回應狀態, 正常為 200. encoding 是回應內容之編碼, 例如 'utf-8' 等. text 與 content 屬性用來存放回應訊息, text 是純文字內容, 而 content 則是原始 bytes 串流, 可以呼叫 decode('utf8') 轉成純文字. json() 方法用來將 json 格式的回應資料轉換成 dict 型態, 但若回應不是 json 格式會出現錯誤, 例如 : 

>>> r.status_code    
200
>>> r.encoding   
'utf-8'
>>> r.text          # 型態是字串
'<!DOCTYPE html>\n<html lang="en">\n    <head>\n        <title>Test</title>\n    </head>\n    <body>\n        <h1>Test</h1>\n        It\'s working if you can read this!\n    </body>\n</html>\n'
>>> r.content    # 型態是 bytes 串流 
b'<!DOCTYPE html>\n<html lang="en">\n    <head>\n        <title>Test</title>\n    </head>\n    <body>\n        <h1>Test</h1>\n        It\'s working if you can read this!\n    </body>\n</html>\n'
>>> type(r.content)   
<class 'bytes'>   
>>> r.content.decode('utf8')      # 將 bytes 串流轉成字串
'<!DOCTYPE html>\n<html lang="en">\n    <head>\n        <title>Test</title>\n    </head>\n    <body>\n        <h1>Test</h1>\n        It\'s working if you can read this!\n    </body>\n</html>\n'

可見 r.text 與 r.content 內容其實是一樣的, 只是資料型態不同而已. 對於非 json 格式資料若呼叫 json() 方法會出現 json 語法錯誤, 例如 : 

>>> r.json()    
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "urequests.py", line 33, in json
ValueError: syntax error in JSON

回應 json 格式資料的伺服器可用 http://ip-api.com/json/ 來測試, 例如 : 

>>> r=urequests.get("http://ip-api.com/json/")     
>>> r.text   
'{"status":"success","country":"Taiwan","countryCode":"TW","region":"TNN","regionName":"Tainan","city":"Tainan City","zip":"","lat":22.9917,"lon":120.2148,"timezone":"Asia/Taipei","isp":"Chunghwa Telecom Co., Ltd.","org":"Chunghwa Telecom Co. Ltd.","as":"AS3462 Data Communication Business Group","query":"218.166.10.110"}'
>>> r.content   
b'{"status":"success","country":"Taiwan","countryCode":"TW","region":"TNN","regionName":"Tainan","city":"Tainan City","zip":"","lat":22.9917,"lon":120.2148,"timezone":"Asia/Taipei","isp":"Chunghwa Telecom Co., Ltd.","org":"Chunghwa Telecom Co. Ltd.","as":"AS3462 Data Communication Business Group","query":"218.166.10.110"}'
>>> r.json()   
{'countryCode': 'TW', 'lon': 120.2148, 'timezone': 'Asia/Taipei', 'city': 'Tainan City', 'status': 'success', 'country': 'Taiwan', 'org': 'Chunghwa Telecom Co. Ltd.', 'query': '218.166.10.110', 'region': 'TNN', 'lat': 22.9917, 'zip': '', 'isp': 'Chunghwa Telecom Co., Ltd.', 'as': 'AS3462 Data Communication Business Group', 'regionName': 'Tainan'}
>>> type(r.json())   
<class 'dict'>

可見 json() 方法已將 json 格式的字串資料轉成 dict 型態資料. 

傳入 get() 方法的 URL 字串後面可以用 ? 與 & 攜帶查詢參數, 這些參數會放在網址中傳遞, 安全性低且可攜帶的參數量較少, 可以使用 httpbin.org 提供的 echo 服務來測試所傳遞之參數, 此網站會將 HTTP 請求的訊息放在回應中傳回來, 例如 : 

>>> r=urequests.get('http://httpbin.org/get?a=1&b=2')    
>>> print(r.text)      
{
  "args": {
    "a": "1"
    "b": "2"
  }, 
  "headers": {
    "Host": "httpbin.org", 
    "X-Amzn-Trace-Id": "Root=1-637ce676-76373b6349b758384140f662"
  }, 
  "origin": "218.166.10.110", 
  "url": "http://httpbin.org/get?a=1&b=2"
}

可見查詢參數 a 與 b 被放在 args 屬性中傳回來. 如果有多個參數就用 & 串接起來即可. 不過 r.text 是字串, 如果要用字典取得 args 等屬性, 可以利用 json 模組在 MicroPython 的內建子集模組 ujson 的 loads() 函式轉換成 dict 型態, 例如 :

>>> import ujson   
>>> ujson.loads(r.text)['args']    
{'a': '1', 'b': '2'}

或者直接呼叫 r.json() 取得 dict 型態資料更快, 不需要匯入 ujson, 例如 :

>>> r.json()['args']    
{'a': '1', 'b': '2'}

post() 方法是將查詢參數放在 HTTP 訊息的 body 中傳送, 安全性較高且可傳遞之參數量較大. 查詢參數是以 dict 方式表示, 傳給 post() 的 json 參數, 例如 : 

>>> data={'a':'1','b':'2'}    
>>> r=urequests.post("http://httpbin.org/post", json=data)   
>>> print(r.text)    
{
  "args": {}, 
  "data": "{\"a\": \"1\", \"b\": \"2\"}", 
  "files": {}, 
  "form": {}, 
  "headers": {
    "Content-Length": "20", 
    "Content-Type": "application/json", 
    "Host": "httpbin.org", 
    "X-Amzn-Trace-Id": "Root=1-637cea54-73807f7b4c9e23e56076edac"
  }, 
  "json": {
    "a": "1", 
    "b": "2"
  }, 
  "origin": "218.166.10.110", 
  "url": "http://httpbin.org/post"
}
 
也可以傳給 data 參數, 但 dict 資料須用 ujson.dumps() 序列化, 例如 : 

>>> import ujson   
>>> r=urequests.post("http://httpbin.org/post", data=ujson.dumps(data))   
>>> print(r.text)   
{
  "args": {}, 
  "data": "{\"a\": \"1\", \"b\": \"2\"}", 
  "files": {}, 
  "form": {}, 
  "headers": {
    "Content-Length": "20", 
    "Host": "httpbin.org", 
    "X-Amzn-Trace-Id": "Root=1-637cf4ab-6d1cd6a83e9c5e4e0f489370"
  }, 
  "json": {
    "a": "1", 
    "b": "2"
  }, 
  "origin": "218.166.10.110", 
  "url": "http://httpbin.org/post"
}

結果與使用 json 參數是一樣的. 

如果要用 data 參數傳遞 post 字典變數, 則必須將其變成字串型態 (這也是一種序列化), 例如 :

>>> import urequests   
>>> data="{'a':'1','b':'2'}"     # 以字串表示的字典變數 
>>> r=urequests.post("http://httpbin.org/post", data=data)    
>>> print(r.text)    
{
  "args": {}, 
  "data": "{'a':'1','b':'2'}"
  "files": {}, 
  "form": {}, 
  "headers": {
    "Content-Length": "17", 
    "Host": "httpbin.org", 
    "X-Amzn-Trace-Id": "Root=1-63903aaa-7b1086f156a7abf327d2eb8c"
  }, 
  "json": null, 
  "origin": "42.74.198.20", 
  "url": "http://httpbin.org/post"
}

如果 data 不是字面值而是變數, 則可以傳給 str() 轉成字串, 例如 : 

>>> import urequests   
>>> data={'a':'1','b':'2'}   
>>> r=urequests.post("http://httpbin.org/post", data=str(data)   
>>> print(r.text)    
{
  "args": {}, 
  "data": "{'a': '1', 'b': '2'}"
  "files": {}, 
  "form": {}, 
  "headers": {
    "Content-Length": "20", 
    "Host": "httpbin.org", 
    "X-Amzn-Trace-Id": "Root=1-63903bc9-5472953e51e26ee85ab8012a"
  }, 
  "json": null, 
  "origin": "42.74.198.20", 
  "url": "http://httpbin.org/post"
}

除了 urequests 外, 也可以使用從 urequest 修改而來的自訂模組 xrequest (添加 params 參數並對 data 套用 URL 編碼) 來擷取網頁, 用法跟 urequests 相同, 下面改用 xrequest 擷取 :

>>> import xrequests    
>>> r=xrequests.get("http://micropython.org/ks/test.html")    
>>> r.text   
'<!DOCTYPE html>\n<html lang="en">\n    <head>\n        <title>Test</title>\n    </head>\n    <body>\n        <h1>Test</h1>\n        It\'s working if you can read this!\n    </body>\n</html>\n'

網路存取具有不確定性, 如果沒有回應 (timeout) 會出現例外, 所以最好是將 HTTP 請求放在 try except 區塊中, 例如: 

try:
    r=requests.get(url, timeout=3)
    r.raise_for_status()
except:
    print ("HTTP Error!")


2022-11-22 補充 :

今天改寫了連線 NTP 伺服器以同步板子內部 RTC 時鐘以取得目前日期時間部分, 同時為 xtools.py 添加了 tw_now() 函式.