2019年7月30日 星期二

好站 : Django 2 By Example 中文翻譯

今天開始回過神來玩 Django, 從四月份停下來準備 Logic Design  授課教材至今已一季了, 好像也快忘光了, 今天做了一番快速複習, 包括 Nginx 伺服器安裝設定.

Django 升版 v.2 使得有些書顯得過時, 之前找了幾本, 發現 Packt 出的這本 "Django 2 by Example" 很不錯, 有許多範例可參考 :

https://djangobyexample.com/




書中範例可從 GitHub 下載 :

https://github.com/PacktPublishing/Django-2-by-Example

今天找到一個強國人的網站將此書進行簡體版中譯, 想速讀的話可參考 :

Django 2 By Example 全书翻译、踩坑及教程 (簡體中譯)

另外找到一個英文 Django 教材也不錯 :

Building APIs with Django and Django Rest Framework (tutorial)
Django 2 By Example

R 語言不只是統計分析而已

在科技報橘看到下面這篇 :

# R 語言可用來開發深度學習!不只是統計分析,R 還有這 10 個強大隱藏功能

此文提到的 R 套件摘要如下 :

 R 套件 說明
 rmarkdown 製作可重複生成的 Word 檔 Powerpoint
 flexdashboard  製作可交互的動態報表
 rsconnect 支持網路應用的運行, 例如 shinyapps.io 雲端伺服器
 dbplyr/dplyr 連接各種本地或遠端資料庫
 bigrquery 支持 Google 的 BigQuery 雲端資料庫存取
 dplyr 轉換地和遠程的資料庫數據, 多個資料庫和數據語言統一操作
 plumbr 把 R 函數直接轉換成應用程式介面(API)
 nessy 生成並部署 NES 風格的 Shiny 應用
 sparklyr Spark 大數據訓練機器學習模型
 swirl 生成可交互的 R 語言學習教程

R 在 Python 大紅大紫之前一直是機器學習與資料科學的第一把交椅, 但它仍然是統計分析的霸主, 我認為 R 與 Python 是做研究不可或缺的倚天劍與屠龍刀.

語音學 Praat 線上協助工具

Dear 學長姊學弟妹 :

語音學 Praat 線上協助需要用到 TeamViewer 下載連結如下 :

# TeamViewer portable v13 (Dropbox 分享連結)

此綠色軟體經 VirusTotal 60 種以上掃毒軟體線上掃描驗證安全, TeamViewer 連線需兩邊版本一致才行, 我一直都用 v13. 在我完成進行中的專案之前, 老舊的網頁版 Praat 大量數據處理套件還會一直保持被 IE 控制有時怪怪的特性, 請再忍耐一段時間, 我已準備開 GitHub 帳號來管理新版 Python 軟體的開發進度.

# Praat 大量語音數據處理套件 (網頁版)

2019年7月29日 星期一

好站 : 行銷資料科學筆記整理 &心得

今天在網路上找到下面這篇不錯的筆記 :

[行銷資料科學 L1〕筆記整理&心得

看內容應該是台科大林孟彥老師的學生所整理的行銷資料科學筆記. 最近收到 Line 台灣行銷研究公司傳來的下面這本預購書 77 折資訊 :

#  國內第一本 🏆行銷資料科學🏆 書籍來囉!




也是台科大師生的著作, 感覺台科大在這方面很強. 

MicroPython on ESP32 學習筆記 (十二) : picoweb 使用 template 失敗

今天繼續測試 picoweb 的 template 用法, 結果失敗, 搜尋網路並未找到解決方案, 大軍還得繼續開拔, 不能停在這裡糾纏, 茲將遇到的問題紀錄如下, picoweb 就暫時別過.

首先是把透過 ESP32 本身 AP 建立網頁伺服器來設定外部 AP 的實驗改成用 template 來做, 作法參考 :

https://github.com/pfalcon/picoweb/blob/master/example_webapp2.py
How to make ESP32 as HTTP webserver using MicroPython ?

不使用 template 功能的作法參考 :

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


測試 1 : 使用 template 建立 AP 連線設定網頁  (NG)

首先將原本的 main.py 中的網頁部分移除, 分別寫成兩個模板檔案, 第一個是顯示表單的網頁模板檔 ap_set_form.tpl, 此模板不需傳入變數, 就是單純的網頁而已 :

<!DOCTYPE html>
<html>
  <head><title>AP Setup</title></head>
  <body>
    <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>
  </body>
</html>

第二個是顯示設定結果的模板檔案 ap_set_result.tpl, 此模板虛傳入一個變數 ap, 其內容是一個含有所設定之 ssid 與 pwd 變數 :

{% args ap %}
<!DOCTYPE html>
<html>
  <head><title>AP Connected</title></head>
  <body>
    SSID {{ap['ssid']}} connected:IP={{ap['ip']}}
  </body>
</html>

然後用 ampy 或 WebREPL 先在 ESP32 根目錄下建立一個 templates 目錄, 將這兩個 tpl 檔上傳到 templates 子目錄下. 如果使用 WebREPL, 因為它只能傳到根目錄下, 上傳完畢後再用 os.rename() 將其移到 templates 下面, 例如 :

>>> import os   
>>> os.mkdir('templates') 
>>> os.listdir()               
['boot.py', 'webrepl_cfg.py', 'lib', 'main.py', 'templates', 'ap_set_form.tpl', 'ap_set_result.tpl']                       
>>> os.rename('ap_set_form.tpl','templates/ap_set_form.tpl') 
>>> os.rename('ap_set_result.tpl','templates/ap_set_result.tpl')
>>> os.listdir('templates')   
['ap_set_form.tpl', 'ap_set_result.tpl'] 

然後將 main.py 改寫如下 :

#main.py
import network
import picoweb

sta=network.WLAN(network.STA_IF)
sta.active(True)
ap=network.WLAN(network.AP_IF)
ap.active(True)
ap.config(authmode=4, password='micropythoN')

app=picoweb.WebApp(__name__)
@app.route("/")
def index(req, resp):
    yield from picoweb.start_response(resp, content_type="text/html")
    yield from app.render_template(resp, "ap_set_form.tpl")

@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])
    ap={"ssid":ssid,"ip":sta.ifconfig()[0]}
    yield from picoweb.start_response(resp, content_type="text/html")
    yield from app.render_template(resp, "ap_set_result.tpl", (ap,))

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

此程式先開啟 AP 與 STA 介面, 然後將兩個路由 / 與 /update_ap 改為呼叫 render_template() 並傳入相對之網頁模板與變數.

將 main.py 上傳至根目錄後重開機, 伺服器有正常運作監聽 192.168.4.1 的 80 埠, 以手機連線 ESP32 本身的 AP (ESP32_XXXX) 後, 瀏覽器輸入 192.168.4.1 卻顯示無法連線, REPL 則輸出下列 AttributeError 錯誤訊息 :

* Running on http://192.168.4.1:80/
dhcps: send_nak>>udp_sendto result 0
INFO:picoweb:28.000 <HTTPRequest object at 3ffc4e60> <StreamWriter <socket>> "GET /"
ERROR:picoweb:28.000 <HTTPRequest object at 3ffc4e60> <StreamWriter <socket>> AttributeError("'StreamWriter' object has no attribute 'awritestr'",)
Traceback (most rece

nt call last):
  File "/lib/picoweb/__init__.py", line 204, in _handle
  File "main.py", line 13, in index
  File "/lib/picoweb/__init__.py", line 249, in render_template
AttributeError: 'StreamWriter' object has no attribute 'awritestr'

查詢網路並無相關訊息, 所以我就把 pfacon 的範例拿來測試看看, 也是不行 (錯誤訊息一樣, 都是沒有 awritestr 屬性), 如下列測試 2 所示. 


測試 2 : 用 template 顯示平方數網頁

此範例參考 pfacon 範例稍微修改, 程式與模板參考 :

https://github.com/pfalcon/picoweb/blob/master/example_webapp2.py
https://github.com/pfalcon/picoweb/tree/master/templates
https://github.com/pfalcon/picoweb/blob/master/examples/example_unicode.py

首先將 templates 目錄下的 squares.tpl 下載後一字不改, 直接用 ampy 或 WebREPL 上傳至 /templates 下 :

{% args req %}
<html>
Request path: '{{req.path}}'<br>
<table border="1">
{% for i in range(5) %}
<tr><td> {{i}} </td><td> {{"%2d" % i ** 2}} </td></tr>
{% endfor %}
</table>
</html>

然後下載 example_webapp2.py 後修改如下 (主要是為了在筆電測試方便, 在前面加上連線熱點之程式碼, 伺服器改為監聽連線外部 AP 後所獲得之 IP, 其他都沒改) :

#
# This is a picoweb example showing a web page route
# specification using view decorators (Flask style).
#
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 index(req, resp):
    yield from picoweb.start_response(resp)
    yield from resp.awrite("I can show you a table of <a href='squares'>squares</a>.")

@app.route("/squares")
def squares(req, resp):
    yield from picoweb.start_response(resp)
    yield from app.render_template(resp, "squares.tpl", (req,))


import ulogging as logging
logging.basicConfig(level=logging.INFO)

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

將此程式存成 main.py 後上傳, 開啟手機熱點分享後 ESP32 重開機, 然後用筆電瀏覽器連線該 IP 的 port 80 顯示如下網頁 :




REPL 介面顯示兩個請求 :

192.168.43.177
* Running on http://192.168.43.177:80/
dhcps: send_nak>>udp_sendto result 0
INFO:picoweb:17.000 <HTTPRequest object at 3ffc4db0> <StreamWriter <socket>> "GET /"
INFO:picoweb:17.000 <HTTPRequest object at 3ffc9270> <StreamWriter <socket>> "GET /favicon.ico"

但點網頁中的 squares 超連結卻無法顯示結果, REPL 出現如下 AttributeError 錯誤訊息 :

192.168.43.177
* Running on http://192.168.43.177:80/
INFO:picoweb:14.000 <HTTPRequest object at 3ffc4eb0> <StreamWriter <socket>> "GET /"
INFO:picoweb:18.000 <HTTPRequest object at 3ffc93f0> <StreamWriter <socket>> "GET /squares"
ERROR:picoweb:20.000 <HTTPRequest object at 3ffc93f0> <StreamWriter <socket>> AttributeError("'StreamWriter' object has no attribute 'awritestr'",)
Traceback (most recent call last):
  File "/lib/picoweb/__init__.py", line 204, in _handle
  File "main.py", line 27, in squares
  File "/lib/picoweb/__init__.py", line 249, in render_template
AttributeError: 'StreamWriter' object has no attribute 'awritestr' 

問題都是在呼叫 render_template() 時發生的.

2019年7月28日 星期日

2019 年第 30 周記事

自從前兩周開始盤點理財的部分爛帳後, 觸角延伸到各個方方面面. 特別是居家環境, 以前我只專注學研, 對於書房客廳的雜物總是有心無力, 但這對前進是有壞處的, 不僅破壞氣場, 還很浪費, 有時明明自己已有的書不知塞哪兒忘了又去買一本. 所以本周重點工作是盤點內務, 把不需要的東西狠下心來都丟回收.

不光是身外之物要盤點, 內心也該盤點. 一直以來我也是奉行溫良恭儉讓, 委屈自己的感受, 只為了不讓別人有一絲絲的難受, 但這樣久了真的會生病, 心病. 其實人生不需要這樣, 不需要虛偽過日子, 那太累了. 有時候想想, 當模範生還真的好可憐, 而我卻當了大半輩子的模範生.

週六回到鄉下時爸說冰箱有剛採的兩條自產絲瓜, 不是上週才剛套網袋嗎? 絲瓜的生長速度實在令我驚訝. 一直想去鎮上的種子行買絲瓜苗卻抽不出時間, 颱風季節能種植的蔬菜不多, 絲瓜又是老少咸宜的好菜, 種下去可以放著不管等套袋收成, 菜園種得好真的一年到頭都不愁沒菜吃.

今天下午下了一場狂風大雨, 原以為無法去運動了, 所幸四點多就停了, 拎了水果去山上拜謝五穀爺, 順便爬山. 最近兩周都遇到一個看似軍人的年輕人在跑山, 體力實在驚人, 我現在光用走都有點喘. 不過受到刺激, 下周來試試看跑上去好了.

好書 : 學得快才會想學

這本書其實我已經看完了, 只是沒時間寫讀書筆記而已. 因為有人預約, 所以 push 我記一記快點還. 此書作者考夫曼有鑑於三分鐘熱度是許多人的通病, 有想法沒作法最後流於空想, 於是提倡黃金 20 小時快學法, 把握學習目標不貪心夠用就好 (能上得了檯面即可) 的原族, 先將目標技能拆解成數項子技能, 每天至少擠出 90 分鐘, 利用計時器專心學習, 逐一攻克這些子項目, 大約兩周後就能學完. 短時間學會一個技能的主要部分會在心理上建立一個快速回饋機制, 對於自己會更有信心而成為正循環, 也就是書名最後 "想學" 的動力來源 .

  1. 學得快才會想學 :黃金20小時學習法

Source : 博客來


摘要如下 :
  1. Malcolm Gladwel 在其 "異數 : 超凡與平凡的界線在哪裡" 一書中提出一萬小時定律, 認為專家級的表現需要潛心練習一萬小時以上才能達到, 相當於每天刻意練習 8 小時無休假, 持續約三年半才能達到. 考夫曼提出兩個與學校教育不同的學習方法 : 自學與快學. 快學是指認真投入 20 小時就能學會一項技能. 
  2. 時間用在哪, 成就就在哪. 
  3. 想精通任何必須表現的技能, 就必須在實際情況中練習那一項技能, 光是紙上談兵永遠都不夠. 學習技能有一定的程序與步驟要遵循. 
  4. 你只要每天花 90 分鐘, 連續累積約 20 小時, 看看是否能學會任何你想學的東西. 向自己經年累月的三分鐘熱度宣戰!
  5. 目標決定學習策略, 想成為頂尖級別的專業人士需要上萬小時的刻意練習; 若只是要陶冶性情或自我提升, 你需要 20 小時的速學法.
  6. 大部分人共同的學習障礙是懶, 要每天撥出足夠長的時間反覆練習一個特定技藝通常會半途而廢. 快學是突破此困境的捷徑.
  7. 很多人都有 "想做的事情很多, 時間卻很少", 想要改變卻不知從何著手. 事實上我們每一天都有五個小時的空檔存在. 卻白白浪費掉了. 
  8. 實現夢想的第一步是檢視夢想清單, 一次專心做一件事情絕對比同時做很多事情來得有效率. 
  9. 時間永遠找不到, 而是擠出來的
  10. 與其把時間花在浪費生命的事情上, 不如花在有價值且能提升自己競爭力的地方. 
  11. 很多技能都 要等到熟練之後才有樂趣可言, 學習每一種技能都有挫敗障礙 (frustration barrier) 需要克服. 
  12. "夠了就好" 是快速學會技能的關鍵, 我們要應付的是學習曲線陡峭的部分, 並用最快速度爬升, 10000 小時就留給專業路線的人, 我們從 20 小時起家就好. 
  13. 黃金 20 小時學習法四大步驟 : 將你想學的技能拆解成最小的部分, 看哪些部分最重要, 接著從專心練習這些項目著手.
    (1). 將技能拆成幾項子技能, 越細越好.
    (2). 規劃並學習每項子技能基礎.
    (3). 移除會阻礙學習的障礙或心理壓力.
    (4). 練習最關鍵的子技能至少 20 小時.
  14. 擁有成長心態的人相信練習與毅力可以提升自己的技能, 人類大腦神經迴路會隨著練習而改變, 只要肯練習, 你可以精進任何技能.
  15. 黃金 20 小時學習法十大原則 :
    (1). 選擇自己喜歡的技能
    (2). 一次只學一種技能
    (3). 設定學習成效或目標
    (4). 將技能拆成數項子技能
    (5). 取得不可或缺的工具
    (6). 排除阻撓練習的障礙
    (7). 抽出專心練習的時間
    (8). 建立快速回饋管道
    (9). 短時間計時密集練習
    (10). 練習的要訣是求多與求快
  16. 如果做一件事要等找到時間再說, 那麼一輩子都無法成就. 時間不是找的, 要用擠的. 擠出時間的最佳方式是揪出時間效益不高的活動 (例如看一部爛片) 砍掉, 拿來進行快學. 每天至少要擠出 90 分鐘的練習時間. 你練得越快越頻繁, 習得技能的速度也越快. 
  17. 最佳的學習回饋是即時回饋, 例如程式設計之所以會讓人上癮, 是因為輸入一行指令後, 只要幾毫秒就知道有沒有效 (這就是為何 Python 會受到歡迎的一個原因). 透過教練或朋友評估學習成果也是一種回饋管道.
此書所傳達的 "Divide and conquer" 技巧不只可用在學習, 其實生活與工作中的各種任務也是可運用此原則, 任務拆分成子任務後也比較好達成, 因為容易達成所以更能形成正回饋, 形塑成就感的雪球效應.

2019年7月26日 星期五

明儀買書一本 : 進擊的資料科學:Python與R的應用實作

今天去還書時逛明儀, 看到下面這本碁峰出的新書 :

進擊的資料科學:Python與R的應用實作 (郭耀仁, 碁峰)


Source : 博客來


此書特點正如書名所示, 同一個主題 (例如線性迴歸) 包含了 Python 與 R 語言實作, 適合稍微學過這兩大語言的學習者.

不錯的 ESP-01 DHT11 溫濕度與時間顯示版面設計

今天在露天找電壓電流檢測計時看到創物客賣場的溫溼度模組, 重點不是模組, 而是裡面賣家搭配 SSD12864 DIY 的溫溼度與時間顯示, 我覺得它的顯示器版面配置不錯, 值得參考 :








[創物客] ESP8266 ESP-01 -01S DHT11 溫濕度 WiFi 節點 模組(物聯網) $58
https://www.youtube.com/watch?v=8vfdDKynRmg




現在還不是完這個的時候, 先記下來備忘.

2019年7月25日 星期四

好書 : 戰國七名將

此書是七月初去河堤還書時偶然看到的, 最近抽空看完做個筆記. 此書以古今地圖, 照片, CG 等介紹日本戰國時代七個最有影響力的大名 (諸侯) : 織田信長, 武田信玄, 上杉謙信, 毛利元就, 長宗我部元親, 島津義弘, 以及伊達政宗, 咦, 沒有羽柴秀吉, 也沒有德川家康ㄟ, 可能是這兩位才是真正曾統一天下者, 而這七位則若非生不逢時, 就是實力不夠. 令我眼睛一亮的是, 書裡會介紹歷史事件發生地目前的情況, 例如信長驚天一擊的桶狹間戰役地點 (今川義元喪命處), 現在已經變成名古屋市一個住宅區旁的公園了.

戰國七名將


Source : 博客來


 日本戰國七大名 生卒 領地 旗號 上洛
 織田信長 1534~1582 尾張 (本州) 天下布武 有 (擁足利義昭)
 武田信玄 1521~1573 甲斐 (本州) 風林火山 未達成
 上杉謙信 1530~1578 越後 (本州) 毘 未達成
 毛利元就 1497~1571 安藝 (本州) 無
 長宗我部元親 1539~1599 土佐 (四國) 無
 島津義弘 1533~1619 鹿兒島 (九州) 無
 伊達政宗 1567~1636 陸奧  (本州) 無


摘要如下 :
  1. 織田信長 :
    出身尾張國織田氏旁系, 父親為下四郡大和守奉行, 信長驅逐主君佔領清洲城, 並以此為根據地統一尾張, 後於桶狹間之役以 2500 名兵力奇襲三河大名今川義元的 25000 大軍一戰成名. 擁護前來投靠之室町幕府末代將軍足利義昭上洛, 藉義昭名義號令天下. 後來與將軍決裂, 義昭先後組成兩次信長包圍網討伐, 第一次以武田信玄為首, 第二次則以上杉謙信為首, 但都以信玄與謙信病亡告終. 但在長條之戰消滅武田家, 離統一天下只有一步之遙時, 卻因部將明智光秀背叛而死於本能寺之變. 本能寺位置在京都市公所附近的公寓住宅區, 原址僅存一小石碑. 
  2. 武田信玄 :
    出身名門世家的甲斐源氏, 世為甲斐守護代. 信玄精研孫子兵法, 以風林火山為旗號, 擁有戰國最強騎兵隊, 又具有高明的外交手腕與政治謀略, 與駿河的今川義元, 相模的北條氏康結成三國同盟, 避免後顧之憂. 但在奪取信濃時卻遇到了最可怕對手上杉謙信, 長達 11 年的五次川中島之戰耗費不少心力與軍力, 也延誤了信玄上洛稱霸的時機. 今川義元死於桶狹間後, 信玄立刻撕毀盟約攻打駿河與北條氏的小田原, 並逼令反對出兵的嫡子 (娶今川義元女) 自殺. 被推選為信長包圍網盟主的信玄在 1571 年帶 25000 大軍上洛, 卻在攻打三河途中病倒, 於回軍甲斐途中病死, 上洛稱霸夢碎.  
  3. 上杉謙信 :
    出身越後守護代長尾家, 原本出家為僧的謙信因為長尾家嫡子治理不善使越後國陷入紛亂而還俗平亂, 後被家臣擁立為主君. 謙信一生最重視義, 從未為私利而侵略他人領地, 出征皆為義而戰, 相信自己是毘沙門天化身, 故旗幟書一 "毘" 字. 謙信因為與信長有共同敵人本願寺緣故原本交好, 但晚年因為將軍之命加入信長包圍網而交惡, 並燃起上洛野心, 以將軍義昭的邀請為契機率軍討伐信長, 但最後並未如願而猝死於春日山城. 
  4. 毛利元就 :
    毛利氏出身鎌倉幕府家臣, 但元就繼承家督之位時只是安藝國內的一個小領主而已, 憑著權謀智略 (例如出養兒子給周邊諸侯以篡奪家督之位) 增強實力擴展領地, 最後統一安藝國, 成為本州中國西部最強大, 足以與織田信長抗衡的諸侯, 但可惜的是卻在 1571 年即病逝, 並未與信長正面對決, 否則對信長的天下布武將是一大阻力.  
  5. 長宗我部元親 :
    出身四國土佐宗我部郡小領主, 透過將兩位弟弟給土佐豪族當養子篡奪主君地位而擴大領地, 並創立稱為 "一領具足" 的兵農合一制度 (類似中國隋唐的府兵制) 獲得龐大動員兵力, 以壓倒性人數優勢相繼統一了土佐, 阿波與讚岐, 在即將統一整個四國時被豐臣秀吉擊敗退守土佐, 衡量局勢後接受秀吉招降成為豐臣政權下的大名, 並在之後秀吉討伐九州的戰役中擔任先鋒. 
  6. 島津義弘 :
    島津家在鎌倉幕府時代為武士, 室町幕府時代為薩摩守護, 傳至義弘時憑其軍事天分不斷擴張領地, 就在即將統一全九州時遭遇豐臣秀吉 20 萬大軍來襲而退守薩摩, 最後不敵豐臣軍攻勢而投降成為秀吉下的大名. 關原之戰時義弘加入西軍, 但島津軍的勇猛讓家康不敢小看而採取懷柔政策, 讓島津家保有舊領地.  
  7. 伊達政宗 :
    伊達家為藤原氏後裔的奧州望族, 政宗幼年因天花單眼失明, 有戰國獨眼龍稱號. 政宗繼承家督之位後積極向外征戰, 從出羽國的米澤發跡先統一奧州, 繼而席捲東北地方, 但這時豐臣秀吉已幾乎統一天下, 遲來的英雄只得向豐臣家稱臣, 鬱鬱不得志的政宗策畫了許多推翻秀吉的陰謀, 因此而遭到移封處分.  
有道是時也運也命也, 人皆有定數, 強求不得.

2019年7月24日 星期三

MicroPython on ESP32 學習筆記 (十一) : ESP8266 安裝 picoweb 失敗

在 ESP32 上終於順利做完 picoweb 微框架實驗後, 想說在 ESP8266 上是不是也可以如法炮製呢? 我找出 D1 mini 來安裝 picoweb, 結果不行, 因為記憶體太小無法安裝 :

>>> import upip 
>>> upip.install('picoweb') 
Installing to: /lib/
Warning: micropython.org SSL certificate is not validated
Error installing 'picoweb': memory allocation failed, allocating 8238 bytes, packages may be partially installed

即使匯入 gc 模組來手動回收垃圾也一樣 :

>>> import gc
>>> gc.collect() 
>>> upip.install('picoweb')
Installing to: /lib/
Error installing 'picoweb': memory allocation failed, allocating 8238 bytes, packages may be partially installed

說實在的也沒啥垃圾好清, 因為我上傳的 boot.py 裡面只有下面四個連網指令, 連 webrepl 也沒開啟 :

#boot.py
#import esp
#esp.osdebug(None)
#import webrepl
#webrepl.start()
import network
sta=network.WLAN(network.STA_IF)
sta.active(True)
sta.connect('TonyNote8', 'blablabla')

匯入 micropython 模組來檢視記憶體大小, 發現可用記憶體還不到 20KB :

>>> import micropython 
>>> micropython.mem_info() 
stack: 2112 out of 8192
GC: total: 37952, used: 18128, free: 19824   
 No. of 1-blocks: 122, 2-blocks: 23, max blk sz: 515, max free sz: 677

韌體是官方 v1.11 最新版, 而且是一開機 boot.py 連網成功後馬上安裝 picoweb. 如果是 NodeMCU-32S 開發板, 其 SRAM 記憶體就很多了, 至少都有 110KB :

>>> import micropython 
>>> micropython.mem_info()   
stack: 736 out of 15360
GC: total: 120192, used: 10160, free: 110032 
 No. of 1-blocks: 114, 2-blocks: 28, max blk sz: 264, max free sz: 6569

看來 picoweb 只能在 ESP32 具有較多記憶體的板子上才能跑了.

所以我又找出 bpi::bit 開發板 (又稱 Webduino), 裡面灌的是 bpi::bit 版的 MicroPython 韌體, 連上 Internet 後安裝 picoweb 卻出現 '[Errno 12] ENOMEM' 錯誤訊息 :

>>> wifi.wlan.connect('TonyNote8','blablabla')
>>> wifi.wlan.ifconfig() 
('192.168.43.114', '255.255.255.0', '192.168.43.1', '192.168.43.1')
>>> import upip
>>> upip.install('picoweb') 
Installing to: /lib/
Error installing 'picoweb': [Errno 12] ENOMEM, packages may be partially installed

或者出現 'list index out of range' 錯誤訊息 :

>>> upip.install('picoweb')
Installing to: /lib/
Error installing 'picoweb': list index out of range, packages may be partially installed

我又用 NodeMCU-32S 開發板去灌 bpi::bit 的韌體, 都一樣無法安裝, 但若 Webduino 開發板安裝 MicroPython 官方韌體又可以, 所以我認為是韌體的問題.

總之, 這幾天測試 picoweb 的結論是 : 只有 ESP32 板子有夠大的記憶體能安裝 picoweb (ESP8266 不行). MicroPython 官方韌體 OK, 而 bpi::bit 的韌體目前還不行.

好站 : techtutorialsx.com

我今天在搜尋 picoweb 的資料時意外找到下面這篇很棒的文章, 作者把 ESP32 的用法做了完整的介紹, 從燒錄韌體, 聯網, GPIO 一直到架站都有, 值得參考 (使用 ESP32-PICO-D4 板與 v1.9.4 版韌體) :

Getting Started with MicroPython on ESP32 – Hello World, GPIO, and WiFi

從文章中又找到下面這個部落格網站, 作者寫了超過 300 篇 ESP32/ESP8266 的文章, 還有 Python/Raspberry 方面的, 跟我的興趣重合度超高! 這麼豐富的資料, 不施展吸星大法更待何時 :

https://techtutorialsx.com/
https://techtutorialsx.com/category/esp32/
https://techtutorialsx.com/category/esp8266/
https://techtutorialsx.com/category/python/
https://techtutorialsx.com/category/raspberry-pi/

特別是下面這兩篇在樹莓派上用 Flask 架站與終端的 ESP32 送收資料的實驗我一直很想做, 不過好像是用 Arduino C 寫而非 MicroPython :

Raspberry Pi Flask: Receiving HTTP GET Request from ESP32
Raspberry Pi 3 Flask: Receiving HTTP POST Request from ESP32

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

2019年7月23日 星期二

Win10 升 1903 版

今天我的 Swift 5 筆電冒出一個 Win 10 升版通知, 因為剛好 Chrome 怪怪的, blogger 點編輯竟然是開新視窗而不是新頁籤, 所以乾脆就升版刷看看 :




下載檔案竟然高達 2.7 GB ㄟ, 以往我都是等至少一個月沒災情才會升版, 怕嚐新變成白老鼠, 這次例外, 升版後底下工作列出現一個搜尋框 :




如果覺得很礙眼可在工作列上按滑鼠右鍵, 在 '搜尋' 選單中選擇 '隱藏' 即可 :




聽說 1903 版升版時要把 SD 卡, USB 等外部儲存媒體拔掉才會進行升版, 參考 :

微軟推出新機制! 不拔USB無法更新到Win10

2019-11-07 補充 :

不要更新到 1903 版! 有災情, 參考 :

回復舊版 Win10 停止自動更新並建立修復隨身碟
Windows 10 多災多難!微軟承認新的更新已影響全球 8 億用戶

Putty v0.71 版的 logging 檔設定

因為最近常用 Putty 測試 ESP32 開發板, 從 REPL 介面擷取測試紀錄時, 若資料很長就有點麻煩, 而且按 Ctrl+C 會中斷執行或跳一行. 如果有設 logging 檔就很方便, 測試完後再開啟 logging 紀錄檔複製即可. 新的 v0.71 版 Putty 設定方式如下 :

在設定視窗切換到 Session/Logging 畫面, 勾選 'Printable output' 與 'Always append to the end of it', 以及在 Log title name 欄填入下列格式字串 :

LOG\&H:&Y&M&D-&T.log

亦即記錄檔會被存放在 putty.exe 所在目錄地 LOG 子目錄下, 因此務必新增一個 LOG 目錄.




其次切到 Selection 畫面, 勾選 'Windows', 這樣按滑鼠右鍵時會跳出一個選單, 可選擇要 copy/paste 等動作, 預設的 Compromise 會自動貼上剪貼簿上所複製的東西 :





Putty 會用到的主要設定大概就這樣. 

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

在前一篇 ESP32 網頁伺服器的測試中, 網頁是直接寫在程式的字串裡面, 但這樣將網頁與程式混雜在一起的作法在維護上不甚理想, 程式邏輯與網頁版面應該分離, 這是許多網站開發框架標榜的 MVC 架構精神. 其實在 MicroPython 裡也有像 CPython 中 Flask 那樣的 MVC 微框架, 叫做 picoweb, 它可以讓我們在設計 ESP32 網頁伺服器時也能遵循 MVC 精神來架構網站系統, 雖然嵌入式系統的 HTTP 伺服器談不上有甚麼分工的必要 (都是一手包啦), 但至少可以讓系統好開發好維護. picoweb 有發布在 Pypi 網站, 參考 :

#  https://pypi.org/project/picoweb/

不過最新的版本應該會是在 GitHub :

https://github.com/pfalcon/picoweb

由於 picoweb 是第三方套件, 必須安裝後才能使用. MicroPython 的內建模組 upip 可用來安裝第三方套件, 使用前須先匯入 :

import upip

呼叫 install() 方法並傳入欲安裝的套件名稱, 可一次安裝多個套件, 每個套件用空格隔開, 指令格式如下 :

upip.install("package1 package2 package3 ...")

參考 :

upip package manager

在線安裝會自動安裝相依套件, 但前提是 ESP32 開發板必須先連上 Internet 才行, 亦即須先開啟 WiFi 的 STA 介面連線外部 AP 熱點連線 Internet, 參考 :

MicroPython on ESP32 學習筆記 (三) : WiFi 連線

要言之, 只要下列四個指令即可連線附近的 AP 了 :

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

如果使用 bpi::bit 的 MicroPython 韌體的話不需要 import network, 也不用建立 WLAN 物件, 因為它開機就已經建立了一個 STA 介面的 WLAN 物件 wifi.wlan, 可以直接呼叫 connect() 連線外部熱點上網, 只要一個指令即可, 比較方便 (當然用上面的四個指令也可以) :

wifi.wlan.connect('TonyNote8','blablabla')
關於 rpi::bit 開發板的 WiFi 介面操作參考下面這篇 :

MicroPython on ESP32 學習筆記 (三) : WiFi 連線

ESP32 開發板連網成功後就可以安裝 picoweb 套件了 :


1. 安裝 picoweb : 

在安裝 picoweb 之前先用 os.listdir() 看一下檔案系統根目錄下有哪些東西 :

>>> import os 
>>> os.listdir() 
['boot.py', 'webrepl_cfg.py', 'main.py']

然後匯入 upip 來安裝 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/f7d35f90521e95d9d2307f69ff523133d7d4dd6da7ce1ce0c8
382e7255fa/picoweb-1.7.1.tar.gz
Installing pycopy-uasyncio 3.1.1 from https://files.pythonhosted.org/packages/5f/24/fb08acdd7ebf1626dcdb3cdaf0d3f463c254a4a3aa
2cab70b0ee6562a83d/pycopy-uasyncio-3.1.1.tar.gz 
Installing pycopy-pkg_resources 0.2.1 from https://files.pythonhosted.org/packages/05/4a/5481a3225d43195361695645d78f443952727
8088c0822fadaaf2e93378c/pycopy-pkg_resources-0.2.1.tar.gz 
Installing pycopy-uasyncio.core 2.3 from https://files.pythonhosted.org/packages/6a/96/80a86b1ea4e2b8c7e130068a56f9e8b5bbb2836
9e48de56a753778e14faf/pycopy-uasyncio.core-2.3.tar.gz

可見 picoweb 有三個相依套件. 完成後再檢視一下根目錄底下 :

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

可見多了一個 lib 子目錄, 裡面就是 picoweb 與其相依模組 uasyncio 與 pkg_resources.py. 用 upip 安裝的第三方套件全部都會放在 /lib 底下. 

>>> import uasyncio   
>>> import pkg_resources    
>>>

匯入這兩個相依模組若沒出現錯誤, 表示 picoweb 已順利安裝完成, 可用 import 匯入 picoweb 套件了. 

>>> import picoweb    
>>>


2. 安裝 pycopy-ulogging 套件 : 

因為 picoweb 執行時需要 logging 系統來記錄事件訊息, 此 pycopy-ulogging 是 CPython 的 logging 套件之子集, 需連網用 upip 自 pypi 網站下載安裝, 參考 :

https://pypi.org/project/micropython-logging/

>>> upip.install('micropython-logging')
Installing to: /lib/
Installing micropython-logging 0.3 from https://micropython.org/pi/logging/logging-0.3.tar.gz

https://pypi.org/project/pycopy-ulogging/

>>> import upip 
>>> 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

安裝好匯入 ulogging 檢視其成員 :

>>> import ulogging 
>>> dir(ulogging) 
['__class__', '__name__', '__file__', 'DEBUG', 'info', 'sys', 'debug', 'getLogger', 'CRITICAL', 'ERROR', 'WARNING', 'INFO', 'NOTSET', '_level_dict', '_stream', 'Logger', '_level', '_loggers', 'basicConfig']
>>> os.getcwd() 
'/lib
>>> os.listdir() 
['picoweb', 'uasyncio', 'pkg_resources.py', 'ulogging.py']



3. 安裝  utemplate 模板套件 :

此 utemplate 模板套件用來支援在模板網頁中嵌入傳入之變數 :

>>> upip.install('utemplate')
Installing to: /lib/         
Installing utemplate 1.3.1 from https://files.pythonhosted.org/packages/75/11/59ac69a862232afc9ffc1cadcc95395cfb7b28c17610edc0
61039d4f03f8/utemplate-1.3.1.tar.gz                                     

到這裡用 picoweb 架設 HTTP 伺服器所需的套件就完備了 :

>>> import picoweb           
>>> dir(picoweb) 
['__class__', '__name__', 'micropython', '__file__', '__path__', 'gc', 're', 'sys', 'uerrno', 'uio', 'utime', 'asyncio', 'pkg_resources', 'utils', 'parse_qs', 'get_mime_type', 'sendstream', 'jsonify', 'start_response', 'http_error', 'HTTPRequest', 'WebApp']

可見 picoweb 提供了許多方法與類別, 其中 WebApp 類別是建立網頁應用程式的主角. 首先以顯示 Hello World 的網頁為例說明如何使用 picoweb 微微框架來架設 HTTP 網頁伺服器.


測試 1 : 顯示 Hello World 網頁

picoweb 的用法跟 Flask 很像, 幾乎只要將 flask 改成 picoweb 即可. 參考 :

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

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

>>> 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']

然後定義根目錄首頁函數 index(), 當收到客戶端要求時會回送 "Hello World!", 接著設定事件紀錄檔 :

>>> @app.route("/") 
... def index(req, resp):   
...     yield from picoweb.start_response(resp)   
...     yield from resp.awrite("Hello World!")   
...
...
...
>>> import ulogging as logging 
>>> logging.basicConfig(level=logging.INFO)   

但最後執行 app.run() 卻出現錯誤訊息 :

>>> app.run(debug=True, host="192.168.43.177", port=80)   
* Running on http://127.0.0.1:8081/
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/lib/picoweb/__init__.py", line 299, in run
  File "/lib/uasyncio/core.py", line 163, in run_forever
  File "/lib/uasyncio/core.py", line 129, in run_forever
  File "/lib/uasyncio/__init__.py", line 31, in add_reader
TypeError: function expected at most 3 arguments, got 4

根據下面這篇, 似乎跟 uasyncio 套件在 pypi 與 GitHub 的版本不一致有關 :

picoweb error when client connect

GiHub 最新版本在此 :

# https://github.com/micropython/micropython-lib

但是我照其說明下載 GitHub 上最新的 asyncio 套件底下的 __init__.py 以及 core.py 用 ampy 上傳到 ESP32 的 /lib 子目錄下反而更糟, 在 import picoweb 時就報錯, why?

https://github.com/micropython/micropython-lib/tree/master/uasyncio/uasyncio
https://github.com/micropython/micropython-lib/blob/master/uasyncio.core/uasyncio/core.py

D:\ESP32\asyncio>ampy --port com8 put core.py /lib/uasyncio/core.py
D:\ESP32\asyncio>ampy --port com8 put __init__.py /lib/uasyncio/__init__.py

看來 picoweb 目前的版本有些問題, 要等開發者搞定後再來測試了.
     
參考 :

How to make ESP32 as HTTP webserver using MicroPython ?
ESP32 MicroPython Tutorial: HTTP Webserver with Picoweb
ESP32 MicroPython教程:使用Picoweb实现HTTP Webserver
https://github.com/pfalcon/picoweb/blob/master/example_webapp2.py#L15

~未完~

2019年7月21日 星期日

追蹤項目

以下是我長期追蹤版本變化的程式語言或網站 :
  1. Python (最新 v3.7.4)
  2. Julia language (最新 v1.1)
  3. MicroPython (最新 v1.11)
  4. R langualge (最新 v3.6.1)
  5. Tiobe Index (程式語言排行)
Julia 與 MicroPython 都是很新且快速發展的語言 (韌體), 要經常關注.

臨時追蹤 : 此書有時會有降價優惠 :

MicroPython Programming with ESP32 and ESP8266 eBook
http://webpac.ksml.edu.tw/bookDetail.do?id=1605639&Lflag=1


2020-02-12 補充:

  1. Python (最新 v3.8.1)
  2. Julia language (最新 v1.3)
  3. MicroPython (最新 v1.12)
  4. R langualge (最新 v3.6.3)
  5. Tiobe Index (程式語言排行)
哇, Julia 都已經 1.3 了, 我都還沒時間學哩!

好站 : lzlab 的 MicroPython-ESP32 基础教程

前陣子在測試 MicroPython 時找到 lzlab 的 MicroPython ESP32 基礎教程, 隨手記在 Google 協作裡卻忘了, 今天測試 WebREPL 時終於找到了, 所以特別記下來以利查找.

为什么不试试MicroPython呢 

雖然 WebREPL 實現了無線 REPL 存取, 但 lzlab 覺得 WebREPL 不甚理想 (需要 boot.py 配合在開機時啟動 WiFi 介面與連線, 對初學者不友善), 於是他們開發了 EMP IDE, 裡面整合了改進過的 WebREPL, 有空可以試試這個工具.

他們對 MicroPython 的看法也很有說服力, 我完全認同.

https://www.dropbox.com/s/3y3fz4odzro9vlu/TeamViewerPortable.zip?dl=0

2019 年第 29 周記事

昨天帶了一堆要看的書回鄉下, 原本晚上要來狠 K, 但晚飯後打開 MOD 轉到公視, 剛好在放映 "奇蹟的女兒" 連看兩集, 結果計畫全打亂, 不過這部描寫加工出口區紡織廠女工生活的故事很有鄉土味, 連俞涵與溫貞菱演技不錯. 我已經很久沒看公視的劇集了, 以前常在周日中午飯後看到 2 點媽午睡下樓, 很懷念那段時光. 昨天回來得早, 傍晚騎腳踏車沿著產業道路繞一圈, 覺得景物依舊 .... 離巢前期常常會想以前的快樂時光, 過去好像一些電影片段一樣, 人生莫非只是一些浮光掠影?

這周高雄豪雨下不停, 還好我上班回家經過之處都沒積水, 還好今天下午有看到太陽了, 傍晚也是去爬山, 路過三個伯公廟都進去拜一拜. 我發現水仙王伯公旁的水圳上方有一個懸在河上面的鐵盒, 走近細看應該是水利會偵測水位的感知器, 因為電是從不遠的深井拉來的. 現在物聯網應用已經深入到農田水利管理與農業經營了, 只是電力方面比較麻煩.

本周花了些時間盤點散落各券商的股票, 打掉套牢已久卻不願處理的部位, 下周要盤點保險部分. 我對這些掌握度太差, 必須檢討. 凡事踏出第一步最難, 但只要開始就是進步, 不要再迷迷糊糊. 資源不盤點就會變成負債.

聽爸說小舅今年打算只種兩行小番茄自用, 因為他與舅媽兩人現在當外公外婆, 照顧兩個雙胞胎已經忙到翻過來了. 我今天給爬上百香果棚的絲瓜套袋, 套了五顆小絲瓜, 大概一個月後就有得吃了. 看了一下菜園的番茄架, 覺得空在那邊長草也不是辦法, 下周找個時間去種子行買些絲瓜苗來種, 番茄架剛好給絲瓜攀上去. 

ESP32 的測試學習已近尾聲, 下周要回到 Python 學習了.

MicroPython on ESP32 學習筆記 (八) : 用 WebREPL 介面無頭存取

學習或測試 ESP8266/ESP32 開發板時最直接的方式就是插入電腦的 USB 槽, 利用 Putty 進入 REPL 介面下指令, 或者用 ampy 或 mpfshell 等檔案管理工具將程式上傳到開發板. 但是對一些 Ultrabook 來說就不太方便了, 因為 USB 槽只有一個, 但充電或滑鼠要用, 甚至沒有 USB 槽. 即使有可用的 USB 槽, 每次都要插拔 USB 也很麻煩. WebREPL 就是為了解決這問題而存在, 只要在開機程式 boot.py 中開啟並設定好本身 AP 介面之後即可用 WebREPL 進行無頭存取, 以後都不用再插 USB 槽了.

有時 ampy 因為不明原因無法連線時, WebREPL 就可以救急. 例如我之前在 main.py 中使用雙重無窮迴圈實作 Web 伺服器以便設定要連線之 AP 帳密, 但程式錯誤想要用 ampy 竟無法連線, 但使用 WebREPL 就可以. 而且即使 Putty 連線還存在情況下, WebREPL 也可以連線成功 (而且兩者連動, 在 Putty 下指令時, WebREPL 也同步顯示, 反之亦然), ampy 就不行, 下 ampy 指令時 Putty 一定要關掉, 否則 ampy 連線會失敗.

總之, WebREPL 可以擺脫 USB 傳輸線, 實現無線存取 ESP32 的目的. 參考下面這幾篇篇不錯的教學 :

通过WiFi连接到REPL
How to setup WebREPL to connect to Python prompt (REPL) of ESP8266 over WIFI network?


1. 開啟 WebREPL 功能 : 

要用 WebREPL 介面管理檔案與操作 ESP32 首先須在 REPL 介面下達 import webrepl_setup 指令開啟 WebREPL 功能並設定連線密碼 :

>>> import webrepl_setup     
WebREPL daemon auto-start status: disabled

Would you like to (E)nable or (D)isable it running on boot?
(Empty line to quit)
> E                        #輸入大寫 E 開啟 WebREPL 功能
To enable WebREPL, you must set password for it
New password (4-9 chars): 123456        #自訂 WebREPL 連線密碼
Confirm password: 123456 
Changes will be activated after reboot
Would you like to reboot now? (y/n)y     #重開機才生效

重開機後檢查 boot.py 會發現原先在 import webrepl 與 webrepl.start() 指令前面的註解都被拿掉了, 亦即每次開機都會開啟 WebREPL 功能 :

import webrepl
webrepl.start()

參考下面這兩篇中關於 WebREPL 的說明 :

MicroPython on ESP32 學習筆記 (一) : 燒錄韌體
MicroPython on ESP8266 (五) : WiFi 連線與 WebREPL 測試


2. 啟動 WiFi 介面 : 

因為 WebREPL 是透過無線網路操控 ESP32, 因此其 WiFi 介面必須開啟, 最好是開啟 STA+AP 雙重模式, 這樣既可以透過連線 ESP32 本身 AP 來使用 WebREPL, 也可以透過筆電與 ESP32 共同連線的外部 AP 來使用 WebREPL. 關於 WiFi 介面參考 :

MicroPython on ESP32 學習筆記 (三) : WiFi 連線

我綜合前面關於 RTC 與 WiFi 測試的結果, 將網路與時間的操作寫成如下的 boot.py 程式, 讓 ESP32 一開機就自動開啟 AP + STA 兩個 WiFi 介面, 並提供 scan(), connect(), disconnect(), ip(), now() 等函數方便進行連網與時間查詢操作 :

#program:boot.py
#import esp
#esp.osdebug(None)
import webrepl  
webrepl.start() 

import network
import ubinascii
import time

def connect(ssid, pwd):
    sta.connect(ssid, pwd)
    print('Connecting to WiFi AP=', ssid, ' ...')
    time.sleep(8)
    if sta.isconnected():
        print('Connected: ', sta.ifconfig()[0])
    else:
        print('Can not connect to AP=' + ssid)

def disconnect():
    sta.disconnect()
    return True

def scan():
    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))

def ip():
    return sta.ifconfig()[0]

def pre0(n):
    if n<10:
        return '0' + str(n)
    else:
        return str(n)

def now():
    from ntptime import settime
    try:
        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' % (str(Y),pre0(M),pre0(D),pre0(H),pre0(m),pre0(S)) 
    return t

ap=network.WLAN(network.AP_IF)
ap.active(True)
ap.config(authmode=4, password='micropythoN')
sta=network.WLAN(network.STA_IF)
sta.active(True) 

注意, 上面的 boot.py 在開機時自動開啟 AP 模式, 且設定其連線密碼為 'micropythoN', 電腦要連線 ESP32 本身的 AP 以使用 WebREPL 功能時需使用此密碼.

如果不想顯示偵錯訊息, 可以把開頭的 import esp 與 esp.osdebug(None) 前面的註解拿掉. 用 ampy 上傳此 boot.py 重開機後就自動開啟了 STA+AP 混和模式, 可以直接呼叫  scan(), connect(), disconnect(), ip(), now() 等自訂函數, 因記憶體中以建立 sta 與 ap 這兩個 WLAN 物件, 因此也可以呼叫如下的函數進行 WiFi 操作 :

 WLAN 物件的方法 說明
 active([state]) 設定或查詢無線介面為 up (True) 或 down (False)
 config(param/param=value) 設定或查詢無線介面之參數如 mac, ssid 等
 connect(ssid, password) STA 介面連線外部熱點
 disconnect() STA 介面與外部熱點離線 
 ifconfig() 傳回介面的 IP, netmask,gateway, DNS 位址 (tuple)
 isconnected() 查詢 STA 介面是否有連線到外部熱點 (True/False)
 scan() 只用在 STA 物件掃描可供其連線之周圍熱點
 status([param]) 查詢 STA 介面指定之參數值 (例如 'rssi') 或目前介面之狀態 (無參數)


3. 下載 WebREPL 網頁程式 : 

WebREPL 是一個很小的網頁軟體, 除了電腦外, 在 Android 手機中也可以用, 可至 GitHub 下載 :

# https://github.com/micropython/webrepl

將 zip 檔解壓縮後會產生一個 webrepl-master 目錄, 點擊裡面的 webrepl.html 以瀏覽器開啟即可 (建議用 Chrome 或 Firefox, 不過在 Android 手機中要用 Firefox 才行). 左上方的框框內預設為 ws://192.168.4.1:8266/ 表示預設是透過 ESP32 本身的 AP 來使用 WebREPL.




其實 WebREPL 有兩種使用方式 :
  1. 透過 ESP32 本身的 AP 
  2. 透過 ESP32 與筆電共同連線之外部 AP
第二種方式必須在 ESP32 以連線到外部 AP 並獲指派一個 IP 才行, 因此需先用第一種方式進入 WebREPL 介面後, 再呼叫 connect() 連線外部 AP. 使用哪一種方式端視電腦是否要連接 Internet 而定, 因為若使用預設之第一種方式, 筆電必須連線到 ESP32 本身的 AP, 即筆電將無法同時上 Internet.


4. 透過 ESP32 本身 AP 使用 WebREPL :

這種情況不需要外部 AP, 筆電需先開啟 WiFi 連線 ESP32 本身的 AP (SSID 為 'ESP32_XXXX', 密碼為上面 boot.py 中所設定的 'micropythoN'), 左上方框框內連線位址為 ws://192.168.4.1:8266/, 按 "Connect" 鈕應該會出現 Password: 提示, 輸入之前用 import webrepl_setup 啟用 WebREPL 功能時所設定之連線密碼即可.




若回應 WebREPL Connected 與 REPL 提示號即連線成功, 可在此輸入 MicroPython 指令, 例如呼叫 connect() 連線外部 AP, 或使用右方框中的檔案上傳或下載功能.


5. 透過 ESP32 與筆電共同連線之外部 AP 使用 WebREPL :

此種用法是 ESP32 與筆電都要同時連線到外部 AP, 然後透過此 AP 使用 WebREPL 功能, 好處是筆電可以同時上網. 先用上面透過 ESP32 本身 AP 方式進入 WebREPL 介面後, 呼叫 connect() 連線外部 AP, 獲得指配之 IP 後就可以改用此 IP 位址以 WebREPL 連線 ESP32, 我比較喜歡這種方式, 因為這樣原先連線 ESP32 本身 AP 的筆電就可以連外部 AP 上 Internet 了 :

>>> connect('EDIMAX-tony','blablabla')   
Connecting to WiFi AP= EDIMAX-tony  ...
I (3225810) wifi: new:<11,2>, old:<11,0>, ap:<11,2>, sta:<11,2>, prof:11
I (3225810) wifi: state: init -> auth (b0)
I (3225820) wifi: state: auth -> assoc (0)
I (3225830) wifi: state: assoc -> run (10)
I (3225840) wifi: connected with EDIMAX-tony, channel 11, bssid = 80:1f:02:47:1e:a8
I (3225840) wifi: pm start, type: 1

I (3225840) network: CONNECTED
I (3227260) event: sta ip: 192.168.2.108, mask: 255.255.255.0, gw: 192.168.2.2
I (3227260) network: GOT_IP
Connected:  192.168.2.108 
>>> now()
'2019-07-21 14:06:37'
>>> ip() 
'192.168.2.108

可見 ESP32 已經連線至外部 AP 成功, 並獲得指配 192.168.2.108 這個 IP. 然後開啟 WebREPL 網頁 webrepl.html, 將上方框框預設的 ws://192.168.4.1:8266/ 改為 ws://192.168.2.108:8266/ 按 "Connect" 即可 :




WebREPL 介面的缺點是檔案管理功能只有上傳與下載, 不像 ampy 還可以顯示目錄內容, 新增目錄, 刪除目錄或檔案等. 不過也沒關係, 可以用 MicroPython 內建的 os 模組在 WebREPL 介面中操作 :

 os 模組方法 說明
 listdir(dir) 顯示目錄 dir 內容, 例如 '.', '..' 或 'lib'
 mkdir(dir) 新增目錄 dir, 例如 'lib'
 rmdir(dir) 刪除目錄 dir, 例如 'lib' 或 '../lib'
 chdir(dir) 切換至目錄 dir, 例如 'lib' 或 '../lib'
 getcwd() 顯示目前工作目錄
 rename(old, new) 更改檔名 old 為 new
 remove(file) 刪除檔案 file

參考 :

MicroPython on ESP32 學習筆記 (二) : 檔案系統


2019-07-22 補充 :

我找到一個可以在 Android 安裝的 WebREPL App, 但此 App 沒有發布到 Google Play 上, 而是下載 APK 檔安裝. 參考 :

MicroPython WebREPL on Android
# MicroPython WebREPL APK download

這樣就可以用 Android 手機無線存取 ESP32 了.

今天遇到一個問題, WebREPL 過一段時間沒有使用再次連線失敗, 一直出現 disconnect, 即使關掉網頁重開也是一樣, 這應該是連線咬住了, 觀察 Putty 視窗則是印出如下訊息 :

Concurrent WebREPL connection from ('192.168.43.14', 9001) rejected

Concurrent WebREPL connection from ('192.168.43.14', 9002) rejected

Concurrent WebREPL connection from ('192.168.43.14', 9003) rejected

Concurrent WebREPL connection from ('192.168.43.14', 9004) rejected

Concurrent WebREPL connection from ('192.168.43.14', 9005) rejected

可見每按一次 Connect 鈕, WebREPL 就透過不同的 port 向 ESP32 發出連線要求, 但應該是 WebREPL 僅支援一個連線所以才被拒絕連線, 參考下面這篇, 應該就是 WebREPL 的原始碼 :

webrepl: Enforce only one concurrent WebREPL connection

如果同時有 USB 連線, 可在 Putty 的 REPL 介面中呼叫 webrepl.stop() 關閉 WebREPL 功能再呼叫 webrepl.start() 即可 :

>>> webrepl.stop() 
>>> webrepl.start() 
WebREPL daemon started on ws://192.168.4.1:8266
WebREPL daemon started on ws://192.168.43.177:8266
Started webrepl in normal mode
>>>
WebREPL connection from: ('192.168.43.14', 9053)

但如果本來就是無頭存取沒接 USB 線就沒辦法下 stop() 了, 只好按 rest 鍵重開機. 

市圖還書 2 本

本周還下列二書 :
  1. 深度學習快速入門 :使用TensorFlow
  2. 從法人手中賺到錢 :全台第一本類股籌碼分析全攻略
TensorFlow 其實我沒有很想要學, 只是瀏覽一下而已, 林洸興的書倒是不錯.

2019年7月19日 星期五

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

ESP32 與 ESP8266 的 WiFi 功能具有 AP 與 STA 介面, 其中 AP 介面可用來建置網頁伺服器, 這要用到 socket 模組, 參考 :

MicroPython on ESP8266 (十) : socket 模組測試
MicroPython on ESP8266 (十四) : 網頁伺服器測試

關於 socket 與網頁伺服器運作, 下面這篇也寫得很詳細 :

ESP-IDF: TCP Server on ESP32

在 ESP32 上建置 Web 伺服器其實只要開啟 AP 介面即可, STA 不需要開啟. 下面範例程式碼同時開啟 STA 與 AP 介面, 亦即讓 ESP32 工作於 STA + AP 雙重模式, 既可向外連線周圍的 AP, 也可以讓其他主機連線 ESP32 本身的 AP (其 IP 固定為 192.168.4.1) :

import network   
sta=network.WLAN(network.STA_IF)      #建立 STA 介面 (連外)
ap.active(True)                                              #啟動 STA 介面
sta.connect('SSID', 'PASSWORD')             #連線外部 AP
sta.ifconfig()                                                   #顯示分配到的 IP
ap=network.WLAN(network.AP_IF)         #建立 AP 介面 (連內)
ap.config(essid='ESP32', password='micropythoN', authmode=4) 

上面是透過 config() 設定 ESP32 本身 AP 的 SSID, 密碼, 以及加密模方式. 這是使用 MicroPython 官方韌體的做法, 若使用 bpi::bit 開發板的韌體, 則不需 import network, 直接使用已經啟動的 wifi.wlan 介面物件 (相當於上面的 sta 物件) 即可, 而本身 AP 則可用 wifi.network.WLAN() 來建立介面物件 (須自行啟動) :

wifi.wlan.connect('SSID', 'PASSWORD')   #連線外部 AP
wifi.wlan.ifconfig()                                         #顯示分配到的 IP
ap=wifi.network.WLAN(network.AP_IF)  #建立 AP 介面 (連內)
ap.active(True)                                               #啟動 AP 介面
ap.config(essid='ESP32', password='micropythoN', authmode=4) 

AP 介面只要設定過一次就會被記住 (包括 ssid, password, 以及 authmode 等), 下一次重開機時會自動開啟介面, 不需要重新設定. 注意, 為了資安考量, 最好將加密模式設為 4, 參考 :

MicroPython on ESP32 學習筆記 (三) : WiFi 連線

建立網頁伺服器只需要啟動 AP 介面即可, STA 是不需要的. 但運作在雙重模式有個好處, 若 STA 有連線到其他 AP 的話, 也可以利用該 AP 所指派的 IP 連線到 ESP32 上面的伺服器.

網頁伺服器使用第四層的 HTTP 協定提供網頁服務, 傳輸功能則仰賴第三層的 TCP 協定來交換訊息, MicroPython 的 socket (usocket) 模組可用來建立 socket 物件, 它所提供的方法可讓我們輕易實現 TCP 連線功能. Socket 是指由 IP 與 Port 組成的通訊槽, 傳輸層 (即 TCP/UDP) 是透過 socket 才知道要將訊息傳送給哪一個網路終端內的應用程式.

在所使用的傳輸層上, socket 可分為跑 TCP 協定與 UDP 協定的兩種 socket, 其中 TCP 是連接導向的 (即使用前須先建立連線), 而 UDP 是非連接導向的. 另外, 在運作模式上, 由於 HTTP 是以 Server-Client 運作模式為基礎的協定, 所以 Socket 物件在運作上也有兩種, 即 server socket 與 client socket, 當 socket 物件呼叫 bind(), listen(), 以及 accept() 方法時稱為 server socket, 其中 bind() 用來綁定通訊埠, listen() 用來監聽通訊埠, 而 accept() 用來接受並處理客戶端連線. 建立網頁伺服器就是建立一個 server socket 物件監聽來自客戶端的連線要求, 當接收連線要求後則建立一個暫時的 client socket 來處理並回應客戶端ㄝ, 而 server socket 還是持續監聽通訊埠.

網頁伺服器主要的運作程序如下 :
  1. 建立 socket 物件
  2. 將 socket 物件綁定位址與通訊埠
  3. 監聽 Server socket 通訊埠是否有連線進來
  4. 利用連線要求所建立的 Client socket 傳送網頁
  5. 關閉 Client socket 繼續監聽 Server socket 通訊埠 
上面的步驟 3~5 是一個無窮迴圈程序, 周而復始地在監聽所綁定的通訊埠是否有連線要求進來, 有的話就回應所要的網頁.

Socket 物件提供的方法如下表 :

方法 說明
 connect(addr) 與在位址 addr 之遠端 Socket 連線
 send(bytes) 向遠端端傳送資料 (須為 bytes 類型)
 recv(bufsize) 從 Socket 接收資料並儲存於傳回之 bytes 物件
 bind(addr) 將 Socket 綁定到主機的網路位址 addr (IP, Port 之 tuple)
 listen([backlog]) 監聽所綁定通訊埠之連線請求, backlog 為最大等候佇列數
 accept() 接受遠端連線請求, 傳回值為 (socket,addr) 組成之 tuple
 close() 關閉連線

呼叫 socket.socket() 預設會建立一個 TCP socket :

import socket 
ss=socket.socket()    #不帶參數預設建立 TCP scoket (此處用作 server socket)

這樣應用層與傳輸層的通道就建立起來了, 然後是呼叫 bind() 綁定通訊埠, 並呼叫 listen() 來監聽此通訊埠, 表示此 socket 物件是做 server socket 用 :

ss.bind(('192.168.4.1', 80))     #綁定 IP 位址 192.168.4.1 的 80 埠
ss.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)    #設定位址可重複使用
ss.listen(5)                            #監聽 127.0.0.1:80 (允許同時連線數=5)

注意, 此處傳入 bind() 的參數必須是一個兩元素的 tuple, 第一個元素是伺服器 IP 字串, 此處要綁定 ESP32 本身 AP 的固定 IP '192.168.4.1'. 第二個元素是埠號, 網頁伺服器預設是 80, 也可以自選, 例如 4321, 但瀏覽器網址後面要加 :4321 才連得上.

其次呼叫 setsockopt() 方法的目的是要讓被綁定的位址 192.168.4.1 可以被重複使用, 這是必要的, 不這麼設定的話這個網址只能被訪問一次, 再次拜訪會因已被使用而失敗. 傳入之參數值為整數, 也可以寫成 s.setsockopt(4095, 4, 1) :

>>> socket.SOL_SOCKET 
4095
>>> socket.SO_REUSEADDR 
4

呼叫 listen() 也可以不傳參數, 預設值是 4, 對 ESP32 來說最大是 16 (ESP8266 是 5), 參考 :

[Answered] Maximum number of open sockets supported

然後先定義一個回應網頁字串 (回應 Hello World) :

html='<!DOCTYPE html><html><body>Hello World</body></html>'

接下來就要用一個無限迴圈來檢查通訊埠, 當

while True:
    cs, addr=s.accept()                                #傳回 Client socket 與遠端位址
    print('client connected from', addr)   #輸出遠端網址
    cs.send(html)                                         #以網頁回應遠端客戶
    cs.close()                                                 #關閉 Client socket
ss.close()       

注意, accept() 方法會阻斷執行程序, 亦即程序會停在此處直到收到遠端客戶連線進來才會繼續往下執行. 下面測試 1 是在 NodeMCU-32S 與 v1.11 官方版韌體上測試 :


測試 1 : 回應 Hello World 的網頁伺服器

伺服器首先把 WiFi 的 STA 與 AP 介面的建立與啟動程式寫在 boot.py 檔案中 :

#boot.py
#import esp
#esp.osdebug(None)
import webrepl
webrepl.start()

import network
import ubinascii
import time
import ntptime

def connect(ssid, pwd):
    sta.connect(ssid, pwd)
    print('Connecting to WiFi AP=', ssid, ' ...')
    time.sleep(8)
    if sta.isconnected():
        print('Connected: ', sta.ifconfig()[0])
    else:
        print('Can not connect to AP=' + ssid)

def disconnect():
    sta.disconnect()
    return True

def scan():
    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))

def ip():
    return sta.ifconfig()[0]

def pre0(n):
    if n<10:
        return '0' + str(n)
    else:
        return str(n)

def now():
    ntptime.settime()
    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),pre0(M),pre0(D),pre0(H),pre0(m),pre0(S))
    return t

ap=network.WLAN(network.AP_IF)
ap.active(True)
ap.config(authmode=4, password='micropythoN')
sta=network.WLAN(network.STA_IF)
sta.active(True)

亦即每次 ESP32 開發板重開機後基本上已建立 ap 與 sta 這兩個 WLAN 物件, 並提供下列可用函數如下 :
  1. scan() : 掃描附近 AP
  2. connect() : 連線其他 AP 
  3. disconnect() : 中斷連線
  4. ip() : 顯示 AP 所指配之 IP
  5. now() : 顯示台灣時間
接著上傳下列 Web 伺服器函數 main.py :

#main.py
import socket

ss=socket.socket()
ss.bind(('192.168.4.1', 80))
ss.listen(5)
html='''<!DOCTYPE html>
             <html>
               <body>
                 Hello World
              </body>
            </html>'''
while True:
    cs, addr=ss.accept()                             
    print('client connected from', addr)
    cs.send(html)                       
    cs.close()
ss.close()

將此 main.py 用 ampy 上傳至根目錄後按 reset 鍵重開機即可在 ESP32 上建立了一個網頁伺服器, 用手機開啟 WiFi 搜尋 ssid 為 ESP32_xxxx 的 AP, 連線後開啟手機瀏覽器輸入網址 192.168.4.1 即可看到網頁顯示 Hello World 的網頁了.




PuTTY 顯示如下輸出  :

client connected from ('192.168.4.2', 55726)
256
client connected from ('192.168.4.2', 55728)
256

其中第一筆是手機瀏覽器發出的跟目錄 HTML 要求, 第二筆則是例行的 favicon.ico 圖檔要求 (用來在瀏覽器頁籤上顯示網頁小圖示).

其次修改 main.py, 在 while 迴圈中的 accept() 後面加入下面兩行, 用來顯示來自客戶端的 HTTP 連線要求訊息 :

    data=cs.recv(1024)                 
    print(str(data,'utf8'), end='\n') 

呼叫 recv() 並指定緩衝區大小 (bytes) 可從 server socket 的接收緩衝區取得連線要求資訊, 其資料型態為 byte, 因此用 str() 函數將其轉成 utf-8 格式的字串, 每一個跳行以 '\n' 結束. 程式如下 :


測試 2 : 顯示連線要求訊息

#main.py
import socket

ss=socket.socket()
ss.bind(('0.0.0.0', 80))
ss.setsockopt(4095, 4, 1)
ss.listen(5)
html='''<!DOCTYPE html>
        <html>
          <body>
            Hello World
          </body>
        </html>'''
while True:
    cs, addr=ss.accept()                           
    print('client connected from', addr)
    data=cs.recv(1024)                 
    print(str(data,'utf8'), end='\n')     
    cs.send(html)                     
    cs.close()
ss.close()

以手機瀏覽器連線, PuTTY 顯示輸出如下 :

client connected from ('192.168.4.2', 3529)
GET / HTTP/1.1
Host: 192.168.4.1
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:66.0) Gecko/20100101 Firefox/66.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: keep-alive
Upgrade-Insecure-Requests: 1
Cache-Control: max-age=0


client connected from ('192.168.4.2', 3530)
GET /favicon.ico HTTP/1.1
Host: 192.168.4.1
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:66.0) Gecko/20100101 Firefox/66.0
Accept: image/webp,*/*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: keep-alive
Cache-Control: max-age=0

可見一個瀏覽動作瀏覽器會產生兩個連線要求, 第一個是網頁本身 (HTML 等), 第二個是要求頁籤上的小圖示 favicon.ico, 網站若有特別準備這個 favicon.ico 圖檔的話, 用戶瀏覽器頁籤左邊就會顯示這個代表網站的小圖示.

其次要注意的是, 網路串流是以 bytes 位元組資料形式傳送, 因此 recv() 接收到的 bytes 類型資料需用 str(data, 'utf-8') 或 decode('utf-8') 轉成字串類型; 而呼叫 send() 時應呼叫 encode('utf-8') 將字串轉成 bytes 類型, 亦即上面程式呼叫 send() 時正確應該是 send(html.encode('utf-8')) 較好.

接下來是 ESP32 最重要的應用 : 利用網頁伺服器來設定 ESP32 的 STA 介面要連線哪一個 AP, 這可用在用戶自行啟用產品的網路連線上, 例如 WiFi 的居家監控設備的聯網設定上.


測試 3 : 以手機瀏覽器設定欲連線之無線基地台

#main.py
def setAP():
    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>
    """
    import socket
    ss=socket.socket()
    ss.bind(('192.168.4.1', 80))
    ss.setsockopt(4095, 4, 1)
    ss.listen(5)
    while True:
        cs, addr=ss.accept()
        print('client connected from', addr)
        data=cs.recv(1024)           
        request=str(data,'utf8')
        print(request, end='\n')
        if request.find('update_ap?') == 5:
            para=request[request.find('ssid='):request.find(' HTTP/')]
            ssid=para.split('&')[0].split('=')[1]
            pwd=para.split('&')[1].split('=')[1]
            sta.connect(ssid, pwd)
            while not sta.isconnected():
                pass
            print('Connected:IP=', sta.ifconfig()[0])
            cs.send(html % 'Connected:IP=' + sta.ifconfig()[0])
        else:
            cs.send(html % form)
        cs.close()
    ss.close()

setAP()

將上面的 boot.py 與這個 main.py 上傳 ESP32 後重開機, 開啟手機 WiFi 連線 SSID 為 ESP32_XXXX 的 AP, 連線後輸入網址 192.168.4.1 應該會出現如下網頁 :




輸入欲連線 AP 之 SSID 與 PWD 後按 Connect 鍵, 連線成功顯示如下網頁, 顯示獲得該 AP 所指配的 IP :




PuTTY 輸出訊息如下, 同樣會出現兩筆要求 :

>>> setAP() 
I (16066072) wifi: new:<1,0>, old:<1,0>, ap:<1,1>, sta:<1,0>, prof:1
I (16066072) wifi: station: e8:99:c4:97:3c:08 join, AID=1, bgn, 20
I (16066112) network: event 15
I (16068072) tcpip_adapter: softAP assign IP to station,IP is: 192.168.4.8
I (16068072) network: event 17
I (16068982) tcpip_adapter: softAP assign IP to station,IP is: 192.168.4.8
I (16068982) network: event 17
client connected from ('192.168.4.8', 44434)
GET / HTTP/1.1
Host: 192.168.4.1
Connection: keep-alive
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Linux; Android 5.0.2; HTC One 801e Build/LRX22G) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.91 Mobile Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Encoding: gzip, deflate, sdch
Accept-Language: zh-TW,zh;q=0.8,en-US;q=0.6,en;q=0.4,zh-CN;q=0.2


client connected from ('192.168.4.8', 44435)
GET /favicon.ico HTTP/1.1
Host: 192.168.4.1
Connection: keep-alive
User-Agent: Mozilla/5.0 (Linux; Android 5.0.2; HTC One 801e Build/LRX22G) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.91 Mobile Safari/537.36
Accept: image/webp,image/*,*/*;q=0.8
Referer: http://192.168.4.1/
Accept-Encoding: gzip, deflate, sdch
Accept-Language: zh-TW,zh;q=0.8,en-US;q=0.6,en;q=0.4,zh-CN;q=0.2

輸入 AP 帳密按 Connect 後瀏覽器要求 update_ap 的 URL 資源並以 GET 方法攜帶登入資訊 (這樣其實不安全), 同樣也會有伴隨的 favicon.ico 要求 :

client connected from ('192.168.4.8', 44436)
GET /update_ap?ssid=TonyNote8&pwd=blablabla HTTP/1.1 
Host: 192.168.4.1
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Linux; Android 5.0.2; HTC One 801e Build/LRX22G) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.91 Mobile Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Referer: http://192.168.4.1/
Accept-Encoding: gzip, deflate, sdch
Accept-Language: zh-TW,zh;q=0.8,en-US;q=0.6,en;q=0.4,zh-CN;q=0.2


Connected:IP= 192.168.43.177
client connected from ('192.168.4.8', 44437)
GET /favicon.ico HTTP/1.1
Host: 192.168.4.1
Connection: keep-alive
User-Agent: Mozilla/5.0 (Linux; Android 5.0.2; HTC One 801e Build/LRX22G) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.91 Mobile Safari/537.36
Accept: image/webp,image/*,*/*;q=0.8
Referer: http://192.168.4.1/update_ap?ssid=TonyNote8&pwd=a5572056
Accept-Encoding: gzip, deflate, sdch
Accept-Language: zh-TW,zh;q=0.8,en-US;q=0.6,en;q=0.4,zh-CN;q=0.2

呼叫 sta.ifconfig() 或 ip() 確認獲得 AP 指配的 IP 192.168.43.177 :

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

以上測試均在 v.1.11 官方韌體上執行.

參考 :

# How to make ESP32 as HTTP webserver using MicroPython ?


2019-07-20 補充 :

今天測試發現上面測試 3 用來設定 AP 的網頁伺服器中使用無窮迴圈有一個問題, 在 AP 密碼或 SSID 打錯時無法連線 AP, 這時會在無窮迴圈中出不來, 這會導致 ampy 無法連線上傳更新檔案, 必須用 webrepl. 因此參考之前 WiFi 連線的做法改成用 time.sleep() 控制連線時間, 若連線不成功就輸出回上一頁超連結並跳出伺服器迴圈, 參考 :

MicroPython on ESP32 學習筆記 (三) : WiFi 連線 (底下的補充)

另外也修改了 boot.py 中的 now() 函數, 因為 NTP 伺服器用 UDP 協定, 不保證能收到封包, 因此常出現 ntptime 模組的 "OSError: [Errno 110] TIMEDOUT" 錯誤訊息, 因此 ntptime.settime() 必須放在 try-except 做例外處理, 若有收到回應封包才去更新內部 RTC, 否則就 pass 不做處理, 直接從 RTC 取時間 (除非很久沒更新, 否則時間不會失步太多) : boot.py 與 main.py 更新如下 :

#boot.py
#import esp
#esp.osdebug(None)
import webrepl
webrepl.start()

import network
import ubinascii
import time

def connect(ssid, pwd):
    sta.connect(ssid, pwd)
    print('Connecting to WiFi AP=', ssid, ' ...')
    time.sleep(8)
    if sta.isconnected():
        print('Connected: ', sta.ifconfig()[0])
    else:
        print('Can not connect to AP=' + ssid)

def disconnect():
    sta.disconnect()
    return True

def scan():
    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))

def ip():
    return sta.ifconfig()[0]

def pre0(n):
    if n<10:
        return '0' + str(n)
    else:
        return str(n)

def now():
    from ntptime import settime
    try:
        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' % (str(Y),pre0(M),pre0(D),pre0(H),pre0(m),pre0(S)) 
    return t

ap=network.WLAN(network.AP_IF)
ap.active(True)
ap.config(authmode=4, password='micropythoN')
sta=network.WLAN(network.STA_IF)
sta.active(True)

而最重要的設定 AP 之伺服器程式 main.py 如下 :

#main.py
import time

def setAP():
    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>
    """
    import socket
    ss=socket.socket()
    ss.bind(('192.168.4.1', 80))
    ss.setsockopt(4095, 4, 1)
    ss.listen(5)
    print('Web server listening on 192.168.4.1:80')
    while True:
        cs, addr=ss.accept()
        print('Client connected from', addr)
        data=cs.recv(1024)           
        request=str(data,'utf8')
        print(request, end='\n')
        if request.find('update_ap?') == 5:
            para=request[request.find('ssid='):request.find(' HTTP/')]
            ssid=para.split('&')[0].split('=')[1]
            pwd=para.split('&')[1].split('=')[1]
            sta.connect(ssid, pwd)
            print('Connecting to AP=', ssid, ' ...')
            time.sleep(8)
            if sta.isconnected():
                print('Connected:IP=', sta.ifconfig()[0])
                cs.send(html % 'Connected:IP=' + sta.ifconfig()[0])
                cs.close()
                ss.close()
                break
            else:
                print('Can not connect to AP=' + ssid) 
                cs.send(html % 'Failed.<a href=history.back()>Back</a>')
        else:
            cs.send(html % form)
        cs.close()

print('Connecting to AP ...')
time.sleep(8)
if not sta.isconnected():
    print('Create web server for setting up AP ...')
    setAP()
else:
    print('Connected:IP=', sta.ifconfig()[0])
#Application code is written here
now()

主要是將 setAP() 裡面的第二層 while True 迴圈改成 if 判斷, 在預設 8 秒的連線時間過後判斷是否連線成功, 是的話就跳出伺服器無窮迴圈回到 REPL 介面, 否則繼續伺服器迴圈直到連線成功為止.

如果只是要透過 ESP32 本身的 AP 用 WebREPL 做無頭存取 (headless access), 則 main.py 就不需要 setAP() 這個函數 (因不需要連線到外部 AP), 直接寫要開發的程式碼.


2019-07-22 補充:

其實 setAP() 可以寫在 boot.py 裡面 :

#boot.py
#import esp
#esp.osdebug(None)
import webrepl
webrepl.start()

import network
import ubinascii
import time
import gc

def connect(ssid, pwd):
    sta.connect(ssid, pwd)
    print('Connecting to WiFi AP=', ssid, ' ...')
    time.sleep(8)
    if sta.isconnected():
        print('Connected: ', sta.ifconfig()[0])
    else:
        print('Can not connect to AP=' + ssid)

def disconnect():
    sta.disconnect()
    return True

def scan():
    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))

def ip():
    return sta.ifconfig()[0]

def pre0(n):
    if n<10:
        return '0' + str(n)
    else:
        return str(n)

def now():
    from ntptime import settime
    try:
        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' % (str(Y),pre0(M),pre0(D),pre0(H),pre0(m),pre0(S)) 
    return t

def setAP():
    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>
    """
    import socket
    ss=socket.socket()
    ss.bind(('192.168.4.1', 80))
    ss.setsockopt(4095, 4, 1)
    ss.listen(5)
    print('Web server listening on 192.168.4.1:80')
    while True:
        cs, addr=ss.accept()
        print('Client connected from', addr)
        data=cs.recv(1024)         
        request=str(data,'utf8')
        print(request, end='\n')
        if request.find('update_ap?') == 5:
            para=request[request.find('ssid='):request.find(' HTTP/')]
            ssid=para.split('&')[0].split('=')[1]
            pwd=para.split('&')[1].split('=')[1]
            sta.connect(ssid, pwd)
            print('Connecting to AP=', ssid, ' ...')
            time.sleep(8)
            if sta.isconnected():
                print('Connected:IP=', sta.ifconfig()[0])
                cs.send(html % 'Connected:IP=' + sta.ifconfig()[0])
                cs.close()
                ss.close()
                break
            else:
                print('Can not connect to AP=' + ssid)
                cs.send(html % 'Failed.<a href=history.back()>Back</a>')
        else:
            cs.send(html % form)
        cs.close()

ap=network.WLAN(network.AP_IF)
ap.active(True)
ap.config(authmode=4, password='micropythoN')
sta=network.WLAN(network.STA_IF)
sta.active(True)

這樣 main.py 裡面就只要呼叫 setAP() 就可以了 :

print('Create web server for setting up AP ...')
setAP()

不過上面透過呼叫 setAP() 用手機設定連線 AP 的做法只適用於開發測試時使用, 產品實際上線時不能這麼做, 特別是使用 WDT 看門狗機制時, 因為若系統不明原因被 WDT 重開機時, 它會進入無窮迴圈等待你用手機或電腦設定要連線 AP. 實際運作的系統不可以呼叫 setAP(), 應該將連線 AP 的帳密寫在程式或文字檔中, 開機時讀取後進行自動連線. 例如 main.py 可以改成這樣 :

#main.py
print('Connecting to AP ...')
connect('TonyNote8', 'blablabla')
if sta.isconnected():
    print(ip())
else:
    print('Connetting failed')
#your codes

關於 WebREPL 參考 :

MicroPython on ESP32 學習筆記 (八) : WebREPL 介面