2019年7月24日 星期三

MicroPython on ESP32 學習筆記 (十) : upip 安裝網頁框架 picoweb 成功

昨天測試 picoweb 微框架失敗, 研判原因是 asyncio 套件版本不一致, 原本想等待開發者修正出新版後再來測試, 但今天仔細看下面這兩篇文章 :

ESP32 MicroPython: HTTP Webserver with Picoweb
ESP32 MicroPython Tutorial: HTTP Webserver with Picoweb

發現作者另外還安裝了兩個套件 :

upip.install('micropython-uasyncio')
upip.install('micropython-pkg_resources') 

我補安裝這兩個套件後重開機再試就成功了. 為了保留之前的測試紀錄, 把測試成功的紀錄寫在這裡, 前篇失敗的那篇參考 :

MicroPython on ESP32 學習筆記 (九) : upip 安裝網頁框架 picoweb 失敗

安裝前須先讓 ESP32 連上 Internet :

import network
sta=network.WLAN(network.STA_IF)
sta.active(True)
sta.connect('TonyNote8', 'blablabla')

完整的安裝程序如下 :


1. 安裝 picoweb : 

>>> import upip 
>>> upip.install('picoweb')   
Installing to: /lib/
Warning: micropython.org SSL certificate is not validated
Installing picoweb 1.7.1 from https://files.pythonhosted.org/packages/1b/4f/f7d35f90521e95d9d2307f69ff523133d7d4dd6da7ce1ce0c8382e7255fa/picoweb-1.7.1.tar.gz
Installing pycopy-uasyncio 3.1.1 from https://files.pythonhosted.org/packages/5f/24/fb08acdd7ebf1626dcdb3cdaf0d3f463c254a4a3aa2cab70b0ee6562a83d/pycopy-uasyncio-3.1.1.tar.gz
Installing pycopy-pkg_resources 0.2.1 from https://files.pythonhosted.org/packages/05/4a/5481a3225d43195361695645d78f4439527278088c0822fadaaf2e93378c/pycopy-pkg_resources-0.2.1.tar.gz
Installing pycopy-uasyncio.core 2.3 from https://files.pythonhosted.org/packages/6a/96/80a86b1ea4e2b8c7e130068a56f9e8b5bbb28369e48de56a753778e14faf/pycopy-uasyncio.core-2.3.tar.gz

安裝完成匯入 picoweb, 沒有出現錯誤訊息 :

>>> import picoweb 
>>>

檢查根目錄發現多出一個 lib 子目錄, 底下存放了剛剛安裝的第三方套件 picoweb 與其相依套件 uasyncio 與 pkg_resources :

>>> os.listdir()
['boot.py', 'webrepl_cfg.py', 'main.py', 'lib']
>>> os.chdir('lib')
>>> os.listdir() 
['picoweb', 'uasyncio', 'pkg_resources.py'] 

關於 picoweb 用法範例, 參考 pfacon 的 GitHub :

https://github.com/pfalcon/picoweb
https://github.com/pfalcon/picoweb/tree/master/examples


2. 安裝 pycopy-ulogging 套件 :

這是 picoweb 運作必須用到的記錄檔套件 :

>>> upip.install('pycopy-ulogging') 
Installing to: /lib/
Installing pycopy-ulogging 0.3 from https://files.pythonhosted.org/packages/56/85/47a6790260c85f0dad460124d1f9a6dbdaa0b0ac33b0ac89194f6f106276/pycopy-ulogging-0.3.tar.gz

沒有安裝此套件的話, 在執行 App 時會出現如下錯誤 :

>>> app.run(debug=True, host="192.168.43.177", port=80)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/lib/picoweb/__init__.py", line 284, in run
ImportError: no module named 'ulogging' 


3. 安裝 utemplate 套件 : 

此套件提供網頁模版 (template) 功能, 支援在模板網頁中嵌入傳入之變數, 若 App 有用到模版必須安裝, 如果沒用到模版並不影響 App 的執行.

>>> upip.install('utemplate') 
Installing to: /lib/
Warning: micropython.org SSL certificate is not validated
Installing utemplate 1.3.1 from https://files.pythonhosted.org/packages/75/11/59ac69a862232afc9ffc1cadcc95395cfb7b28c17610edc061039d4f03f8/utemplate-1.3.1.tar.gz

此時再去檢查 /lib 目錄, 會發現新增了 ulogging.py 與 utemplate 這兩個套件 :

>>> os.chdir('/lib') 
>>> os.listdir() 
['picoweb', 'uasyncio', 'pkg_resources.py', 'ulogging.py', 'utemplate']

關於 utemplate 的用法參考 :

https://github.com/pfalcon/utemplate

以上三個步驟與昨天程序相同, 但只做到這樣的話, 目前的相依套件版本會在執行 App 時會發生錯誤, 還要安裝下面兩個套件才行 (如果以後改版修正此問題後應該就不需要了).


4. 安裝  micropython-uasyncio 與 micropython-pkg_resources 套件 : 

這兩個套件是今天測試成功的關鍵, 注意其版本編號 :

>>> upip.install('micropython-uasyncio') 
Installing to: /lib/
Warning: micropython.org SSL certificate is not validated
Installing micropython-uasyncio 2.0 from https://micropython.org/pi/uasyncio/uasyncio-2.0.tar.gz
Installing micropython-uasyncio.core 2.0 from https://micropython.org/pi/uasyncio.core/uasyncio.core-2.0.tar.gz
>>> upip.install('micropython-pkg_resources') 
Installing to: /lib/
Installing micropython-pkg_resources 0.2.1 from https://micropython.org/pi/pkg_resources/pkg_resources-0.2.1.tar.gz

這時再去檢查 /lib 目錄, 發現並沒有變化 :

>>> os.chdir('/lib')   
>>> os.listdir()    
['picoweb', 'uasyncio', 'pkg_resources.py', 'ulogging.py', 'utemplate']

可見此步驟所安裝的兩個 picoweb 相依套件應該是覆蓋了前面步驟 1 安裝 picoweb 時所安裝之 pycopy-uasyncio 與 pycopy-pkg_resources.py. 比對套件版本, pkg_resources.py 版本相同, 都是 0.2.1, 而 uasyncio 就不同, 此處為 2.0, 而步驟 1 的是 3.1.1, 版本比較新結果卻不行, 要此處舊版的 2.0 才行. 這樣就可以順利執行網頁應用程式了.

MicroPython 開發者 pfacon 在 GitHub 提供了一些 picoweb 的範例, 參考 :

https://github.com/pfalcon/picoweb/tree/master/examples
https://github.com/pfalcon/picoweb


測試 1 : 顯示 Hello World 網頁

其實 picoweb 的用法跟 Flask 很像, 幾乎只要將 Flask 改成 picoweb 即可. 關於 Flask 的基本用法參考 :

Python Web Flask 實戰開發教學 - 簡介與環境建置

首先呼叫 picoweb.WebApp() 並傳入 __name__ 參數建立一個 WebApp 物件 app :

>>> app=picoweb.WebApp(__name__)
>>> type(app)
<class 'WebApp'> 
>>> dir(app)
['__class__', '__init__', '__module__', '__qualname__', '__dict__', 'init', 'mount', 'run', 'pkg', 'url_map', 'handle_static', 'mounts', 'inited', 'template_loader', 'headers_mode', 'parse_headers', '_handle', 'route', 'add_url_rule', '_load_template', 'render_template', 'render_str', 'sendfile']

然後用裝飾器 (decorator) @ 定義 App 的路由 (routing), 此測試只有根目錄一個路由, 只要用戶瀏覽根目錄就回應 'Hello World', 然後緊跟著要定義此路由之處理函數 hello(), 需傳入兩個參數來處理要求與回應, 第一個是要求 req, 第二個是回應 resp, 呼叫 resp.awrite() 送出回應訊息 :

>>> @app.route("/")
... def hello(req, resp): 
...     yield from picoweb.start_response(resp) 
...     yield from resp.awrite("Hello World!") 
...
...
...

然後呼叫 STA 介面的 ifconfig() 查詢所分配到的 IP 作為網頁伺服器的 host 位址 :

>>> sta.ifconfig() 
('192.168.43.177', '255.255.255.0', '192.168.43.1', '192.168.43.1') 

最後呼叫 WebApp 物件的 run() 方法, 傳入伺服器的網址與埠號 (預設是 5000) 即可 :

>>> app.run(debug=True, host="192.168.43.177", port=80) 
* Running on http://192.168.43.177:80/

可見網頁伺服器成功運行了.

接著用連線到同一熱點的電腦瀏覽器, 輸入 ESP32 Web 伺服器網址 192.168.43.177 即顯示 Hello World 網頁 :




REPL 顯示如下兩筆要求訊息, 第一筆是要求 HTML 網頁本身, 第二筆是要求網頁上的小圖示 favicon.ico :

INFO:picoweb:952.000 <HTTPRequest object at 3ffc47a0> <StreamWriter <socket>> "GET /"
INFO:picoweb:953.000 <HTTPRequest object at 3ffc84d0> <StreamWriter <socket>> "GET /favicon.ico"

完整程式如下 :

#main.py
import network
import picoweb

sta=network.WLAN(network.STA_IF)
sta.active(True)
sta.connect('TonyNote8', 'blablabla')
while not sta.isconnected():
    pass
host=sta.ifconfig()[0]
print(host)

app=picoweb.WebApp(__name__)
@app.route("/")
def hello(req, resp):
    yield from picoweb.start_response(resp)
    yield from resp.awrite("Hello World!")

if __name__ == "__main__":
    app.run(debug=True, host=host, port=80)

注意, 呼叫 sta.connect() 後必須用迴圈或 time.sleep() 等待直到連線成功再取得 host, 否則會取到 0.0.0.0. 將此程式存成 main.py, 用 ampy 或 WebREPL 上傳到 ESP32 即可. 程式最後的 if __name__ 判斷目的是用 python 執行此程式時才呼叫 app.run(), 在 import 時不會.

接下來要用 picoweb 來建立一個網頁伺服器, 讓使用者可以連線到 ESP32 本身的 AP (192.168.4.1) 輸入要連線的 WiFi 熱點, 前面的測試中我們是使用底層的 socket 模組來做, 參考 :

MicroPython on ESP32 學習筆記 (七) : socket 與網頁伺服器


測試 2 : 用內部 AP 建立 Web 伺服器設定要連線之熱點 (GET)

#main.py
import network
import picoweb

html="""
<!DOCTYPE html>
<html>
  <head><title>AP Setup</title></head>
  <body>
    %s
  </body>
</html>
"""
form="""
    <form method=get action='/update_ap'>
      <table border="0">
        <tr>
          <td>SSID</td>
          <td><input name=ssid type=text></td>
        </tr>
        <tr>
          <td>PWD </td>
          <td><input name=pwd type=text></td>
        </tr>
        <tr>
          <td></td>
          <td align=right><input type=submit value=Connect></td>
        </tr>
      </table>
    </form>
"""

app=picoweb.WebApp(__name__)
@app.route("/")
def hello(req, resp):
    yield from picoweb.start_response(resp)
    yield from resp.awrite(html % form)

@app.route("/update_ap")
def update_ap(req, resp):
    req.parse_qs()                 #剖析請求表單
    ssid=req.form['ssid']       #取得表單中的參數
    pwd=req.form['pwd']      #取得表單中的參數
    print(ssid,pwd)
    sta.connect(ssid, pwd)
    print('Connecting to AP=', ssid, ' ...')
    while not sta.isconnected():
        pass
    print('Connected IP=', sta.ifconfig()[0])
    yield from picoweb.start_response(resp)
    yield from resp.awrite(html % 'Connected:IP=' + sta.ifconfig()[0])

if __name__ == "__main__":
    app.run(debug=True, host='192.168.4.1', port=80)

此程式宣告了兩個路由 : 根目錄 / (顯示設定表單) 以及 /update_ap (連線所輸入之熱點). 注意, 上面程式碼中最重要的部分是, 如果客戶端請求中的表單是以 GET 方法提交, 則需呼叫請求物件 req 的 query_qs() 方法來剖析請求表單, 然後用 form[] 即可取得請求表單中的參數. 如果是用 POST 方法提交, 則需呼叫 yield from req.read_form_data(), 參考開發者 pfacon 的範例 :

https://github.com/pfalcon/picoweb/blob/master/examples/example_form.py

將上面的程式存成 main.py 上傳 ESP32, 重開機後以手機連線 ESP32 本身 AP (ESP32_XXXX), 開啟瀏覽器連線 192.168.4.1 應該會顯示一個 SSID 與 PWD 的表單, 輸入後按 Connect 即可.





Putty REPL 介面輸出如下 :

connect('TonyNote8','blablabla')

Started webrepl in normal mode
I (429) phy: phy_version: 4100, 2a5dd04, Jan 23 2019, 21:00:07, 0, 0
* Running on http://192.168.4.1:80/
dhcps: send_nak>>udp_sendto result 0
INFO:picoweb:27.000 <HTTPRequest object at 3ffc4850> <StreamWriter <socket>> "GET /"
INFO:picoweb:37.000 <HTTPRequest object at 3ffc9420> <StreamWriter <socket>> "GET /update_ap?ssid=TonyNote8&pwd=blablabla"
TonyNote8 a5572056
Connecting to AP= TonyNote8  ...

可見使用 GET 方法時表單內所傳送的參數會放在表頭 (headers) 的 URL 中傳送, 比較不安全, 應該用 POST 方法. 下面測試 3 是將上面的測試 2 改成用 POST 方法提交請求, 並將 update_ap() 中的處理方式一般化 :


測試 3 : 用內部 AP 建立 Web 伺服器設定要連線之熱點 (POST)

#main.py
import network
import picoweb
import time

html="""
<!DOCTYPE html>
<html>
  <head><title>AP Setup</title></head>
  <body>
    %s
  </body>
</html>
"""
form="""
    <form method=post action='/update_ap'>
      <table border="0">
        <tr>
          <td>SSID</td>
          <td><input name=ssid type=text></td>
        </tr>
        <tr>
          <td>PWD </td>
          <td><input name=pwd type=text></td>
        </tr>
        <tr>
          <td></td>
          <td align=right><input type=submit value=Connect></td>
        </tr>
      </table>
    </form>
"""

app=picoweb.WebApp(__name__)
@app.route("/")
def hello(req, resp):
    yield from picoweb.start_response(resp)
    yield from resp.awrite(html % form)

@app.route("/update_ap")
def update_ap(req, resp):
    if req.method == "POST":    
        yield from req.read_form_data()    #取得請求表單
    else:    
        req.parse_qs()     
    ssid=req.form['ssid']      #取得表單中的參數
    pwd=req.form['pwd']     #取得表單中的參數
    print(ssid,pwd)
    sta.connect(ssid, pwd)
    print('Connecting to AP=', ssid, ' ...')
    while not sta.isconnected():
        pass
    print('Connected IP=', sta.ifconfig()[0])
    yield from picoweb.start_response(resp)
    yield from resp.awrite(html % 'Connected:IP=' + sta.ifconfig()[0])

if __name__ == "__main__":
    app.run(debug=True, host='192.168.4.1', port=80)

注意上面 HTML 的表單中的 method 是 POST, 因此 update_ap() 處理此呼叫時是執行 yield from req.read_form_data() 取得請求表單中的資料. 取得表單參數一樣是用 reg.from['param'] 不變.

REPL 介面輸出訊息如下 :

* Running on http://192.168.4.1:80/
INFO:picoweb:51.000 <HTTPRequest object at 3ffc4850> <StreamWriter <socket>> "GET /"
INFO:picoweb:58.000 <HTTPRequest object at 3ffc9510> <StreamWriter <socket>> "POST /update_ap"
TonyNote8 blablabla
Connecting to AP= TonyNote8  ...
Connected IP= 192.168.43.177

其中第一個訊息是 GET 方法, 這是要求根目錄的請求; 第二個是 POST 為設定 AP 之請求. 與上面用 GET 方法比較, POST 方法不會在表頭中攜帶表單參數, 比較安全一些.

參考 :

ESP32 MicroPython: HTTP Webserver with Picoweb
ESP32 MicroPython: Serving HTML from the file system in Picoweb
ESP32 MicroPython: Changing the HTTP response content-type of Picoweb route
ESP32 Picoweb: Serving JSON content
ESP32 Picoweb: Changing the returned HTTP code
ESP32 Picoweb: Obtaining the HTTP Method of the request
https://forum.micropython.org/viewtopic.php?t=3651
ESP32 Picoweb教程:修改返回的HTTP代码
# Getting Started with MicroPython on ESP32 – Hello World, GPIO, and WiFi
ESP32 MicroPython教程:使用Picoweb实现HTTP Webserver

沒有留言 :