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 介面

沒有留言 :