2019年7月3日 星期三

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

WiFi 是 ESP8266/ESP32 的基本功能, 也是它們在 IoT 開發板中的特色與亮點. ESP32 的 WiFi 操作與 ESP8266 相同, 同樣具有 STA 與 AP 兩種無線介面 (interface), 可同時啟動, 因此有如下三種操作模式 :


 WiFi 模式 說明
 STA (station 站台) ESP32 可連線至一路由器 (AP)
 AP (access point 熱點) 作為熱點讓其他設備連線至 ESP32
 STA + AP (雙重模式) 混和模式 (同時開啟 STA 與 AP 功能)


參考 :

MicroPython on ESP8266 (五) : WiFi 連線與 WebREPL 測試
# ESP32 官網教學文件 : Networking
WiFi有兩種mode: STA 與AP (又叫作SoftAP,HostAP)
Getting Started with MicroPython on ESP32 – Hello World, GPIO, and WiFi

ESP32 的 WiFi 功能寫在內建的 network 模組中, 使用前要先 import 進來, 呼叫 dir() 函數可顯示其內容 :

>>> import network 
>>> dir(network) 
['__class__', '__init__', '__name__', 'AP_IF', 'AUTH_MAX', 'AUTH_OPEN', 'AUTH_WEP', 'AUTH_WPA2_PSK', 'AUTH_WPA_PSK', 'AUTH_WPA_WPA2_PSK', 'ETH_CLOCK_GPIO0_IN', 'ETH_CLOCK_GPIO16_OUT', 'ETH_CLOCK_GPIO17_OUT', 'LAN', 'MODE_11B', 'MODE_11G', 'MODE_11N', 'PHY_LAN8720', 'PHY_TLK110', 'PPP', 'STAT_ASSOC_FAIL', 'STAT_BEACON_TIMEOUT', 'STAT_CONNECTING', 'STAT_GOT_IP', 'STAT_HANDSHAKE_TIMEOUT', 'STAT_IDLE', 'STAT_NO_AP_FOUND', 'STAT_WRONG_PASSWORD', 'STA_IF', 'WLAN', 'phy_mode']

這裡面會用到的是 WLAN 類別與兩個屬性 AP_IF (熱點=1) 與 STA_IF (站台=0), 這兩個屬性的資料類型是整數 1 與 0 :

>>> type(network.AP_IF) 
<class 'int'>
>>> type(network.STA_IF) 
<class 'int'>
>>> print(network.AP_IF)    
1
>>> print(network.STA_IF)       
0

呼叫 network.WLAN() 建構子並分別傳入 network.AP_IF 與 network.STA_IF 將傳回代表熱點或站台的 WLAN 物件, 呼叫 dir() 可顯示其成員, 共有 8 個方法 :

>>>sta=network.WLAN(network.STA_IF)      #傳回 WLAN 物件 (也可傳入 0)
>>>ap=network.WLAN(network.AP_IF)         #傳回 WLAN 物件 (也可傳入 1)
>>> type(sta) 
<class 'WLAN'>
>>> type(ap) 
<class 'WLAN'>
>>> dir(ap)                                                            #顯示 WLAN 物件之成員
['__class__', 'active', 'config', 'connect', 'disconnect', 'ifconfig', 'isconnected', 'scan', 'status']
>>> dir(sta)                                                           #顯示 WLAN 物件之成員
['__class__', 'active', 'config', 'connect', 'disconnect', 'ifconfig', 'isconnected', 'scan', 'status']

WLAN 物件的 8 個方法說明如下表 :


 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') 或目前介面之狀態 (無參數)


首先測試 active() 方法, 此方法可傳入 True 或 False 來啟動 (up) 或關閉 (down) 無線介面 (setter), 若未傳入參數參數為查詢該介面之狀態 (getter), 同樣是 1.11 版韌體, 與 ESP8266 不同的是, ESP32 的 STA 與 AP 介面預設均為關閉 (down) 狀態 (ESP8266 的 AP 預設是開啟的) :

>>> sta.active()
False                                     #STA 預設為關閉
>>> ap.active() 
False                                     #AP 預設為關閉 (ESP32)

在操作 STA 與 AP 介面之前必須即傳入 True 將介面啟動才行, 首先啟動 STA 介面 :

>>> sta.active(True)   
I (7552260) phy: phy_version: 4100, 2a5dd04, Jan 23 2019, 21:00:07, 0, 0
I (7552260) wifi: mode : sta (80:7d:3a:b7:a7:5c)
True
>>> I (7552260) wifi: STA_START

可見此時 WiFi 模式為 STA. 接著啟動 AP 介面 :

>>> ap.active(True) 
I (7570230) wifi: mode : sta (80:7d:3a:b7:a7:5c) + softAP (80:7d:3a:b7:a7:5d)
I (7570230) wifi: Total power save buffer number: 16
I (7570230) wifi: Init max length of beacon: 752/752
I (7570240) wifi: Init max length of beacon: 752/752
True
>>> I (7570240) network: event 13

可見此時 WiFi 模式變成 STA + AP 雙重模式了, 這時再去查詢介面狀態, 兩個都傳回 True 了 : 

>>> sta.active()    
True  
>>> ap.active()    
True  

這時呼叫介面的 ifconfig() 方法會傳回一個表示該介面之 IP, netmask, gateway, 與 DNS 位址的 tuple, 例如 :  

>>> ap.ifconfig()   
('192.168.4.1', '255.255.255.0', '192.168.4.1', '0.0.0.0')

其中 192.168.4.1 就是 ESP32 作為熱點的 IP, 可供其他站台連線. 查詢 STA 介面則全都是 0, 這是因為 STA 介面尚未連線外部熱點之故 : 

>>> sta.ifconfig()    
('0.0.0.0', '0.0.0.0', '0.0.0.0', '0.0.0.0')     

先用 scan() 方法掃描周圍的熱點 (注意, 此方法只能用在 STA 介面物件) :

>>> aps=sta.scan() 
>>> print(aps)
[(b'TonyNote8', b'\x06\xd6\xaa\x04\xfcK', 11, -66, 3, False), (b'EDIMAX-tony', b'\x80\x1f\x02-Z\x9e', 11, -93, 4, False), (b'EDIMAX', b'\x80\x1f\x02\xef@@', 11, -93, 3, False)]

方法 scan() 會傳回 tuple 組成之串列, 其定義如下 :

(ssid, bssid, channel, RSSI, authmode, hidden)

這裡 bssid 指的是 AP 的 mac 位址, 但上面顯示的 mac 位址似乎怪怪的, 這是因為 mac 為 bytes 類型資料, 用 ASCII 格式顯示時有些會變成亂碼. 解決辦法是用內建的 ubinascii 模組將 binary 轉換成 ASCII 碼, 參考 :

MAC Address is printed as a bytes object #3115

>>> aps=sta.scan()
>>> aps[0][0]   
b'TonyNote8'
>>> type(aps[0][0])           
<class 'bytes'>
>>> aps[0][0].decode()       #呼叫 decode() 轉成字串
'TonyNote8'
>>> aps[0][1] 
b'\x06\xd6\xaa\x04\xfcK'
>>> type(aps[0][1])   
<class 'bytes'> 
>>> import ubinascii         #引入 ubinascii 模組
>>> mac=ubinascii.hexlify(aps[0][1], ':')     #轉成以 : 隔開的 16 進位
>>> mac 
b'06:d6:aa:04:fc:4a'
>>> type(mac) 
<class 'bytes'> 
>>> mac.decode()               #呼叫 decode() 轉成字串
'06:d6:aa:04:fc:4a'

可以把轉換函數放在迴圈內解譯周邊熱點的 ssid 與 mac :

>>> import ubinascii   
>>> for ap in aps:   
...     ssid=ap[0].decode() 
...     mac=ubinascii.hexlify(ap[1], ':').decode() 
...     print('{:>20} {:>20}'.format(ssid, mac)
...
...
...
  MicroPython-07a0bc    a2:20:a6:07:a0:bc
    CHT Wi-Fi(HiNet)    02:16:16:29:e6:94
       .1.Free Wi-Fi    12:16:16:29:e6:94
      CHT Wi-Fi Auto    00:16:16:29:e6:94
                P883    ec:43:f6:f5:ae:a5
               17N4F    a8:4e:3f:0d:f7:78




上面為了輸出整齊使用了字串的排印格式讓 ssid 與 mac 都有 20 個字元位置 (實際執行很整齊, 但網頁中看不出來), 參考 :

Python: Format output string, right alignment

完整程式如下 :

import network
import ubinascii

sta=network.WLAN(0)
sta.active(1)
aps=sta.scan()
for ap in aps:
    ssid=ap[0].decode()
    mac=ubinascii.hexlify(ap[1], ':').decode()
    print('{:>20} {:>20}'.format(ssid, mac))

channel 為 WiFi 的頻道或通道, 總共分成 13 個重疊的頻道, 編號 1~13, 其中 1, 6, 11 頻道沒有重疊, 比較不會有干擾問題.

RSSI 是收到的熱點信號強度 (單位 dBm), 是個負值, 越接近 0 信號越強, -50 以內表示信號優良 (Excellent), -50 到 -60 之間信號良好 (Good), -60 到 -70 還可以 (Fair), 低於 -70 就算是弱了 (Weak), -100 則表示完全無信號.

authmode 表示認證加密模式, 其值為整數, 定義如下 :


 authmode 說明
 0 open 無加密
 1 WEP 加密
 2 WPA-PSK 加密
 3 WPA2-PSK 加密
 4 WPA/WPA2-PSK 加密


hidden 表示此熱點是否為隱藏, True 為隱藏, False 為不隱藏 (預設).

若呼叫 connect() 連線外部熱點, 就會獲得熱點派發的網址了 :

>>> sta.connect('TonyNote8', 'blablabla') 
>>> I (9620180) wifi: new:<1,1>, old:<1,0>, ap:<1,1>, sta:<1,0>, prof:1
I (9620750) wifi: state: init -> auth (b0)
I (9620770) wifi: state: auth -> assoc (0)
I (9620790) wifi: state: assoc -> run (10)
I (9620840) wifi: connected with TonyNote8, channel 1, bssid = 06:d6:aa:04:fc:4a
I (9620840) wifi: pm start, type: 1

I (9620840) network: CONNECTED
I (9624680) event: sta ip: 192.168.43.177, mask: 255.255.255.0, gw: 192.168.43.1
I (9624680) network: GOT_IP

可見 ESP32 的 STA 介面獲得熱點 (Gateway IP=192.168.43.1) 派發 192.168.43.177 這個 IP. 再次呼叫 ifconfig() 就會傳回 STA 介面的新 IP, netmask, gateway, 與 DNS 位址 :

>>> sta.ifconfig()
('192.168.43.177', '255.255.255.0', '192.168.43.1', '192.168.43.1')
>>> dhcps: send_nak>>udp_sendto result 0

使用連線到同一熱點的筆電 ping ESP32 的 IP 應可獲得回應, 表示網路連線正常 :

C:\Users\User>ping 192.168.43.177 

Ping 192.168.43.177 (使用 32 位元組的資料):
回覆自 192.168.43.177: 位元組=32 時間=449ms TTL=255
回覆自 192.168.43.177: 位元組=32 時間=50ms TTL=255
回覆自 192.168.43.177: 位元組=32 時間=257ms TTL=255
回覆自 192.168.43.177: 位元組=32 時間=85ms TTL=255

192.168.43.177 的 Ping 統計資料:
    封包: 已傳送 = 4,已收到 = 4, 已遺失 = 0 (0% 遺失),
大約的來回時間 (毫秒):
    最小值 = 50ms,最大值 = 449ms,平均 = 210ms

這時呼叫 STA 介面物件的 isconnected() 方法將傳回 True, 這可用來檢測 connect() 是否成功, 以便進入下個執行階段 :

>>> sta.isconnected() 
True 

由於使用 ESP32 主要是著眼於其網路連線能力, 連線失敗的話後面的作業也不用執行了, 故通常會用一個 while 迴圈來檢查, 例如 :

while not sta.isconnected():
    pass 

方法 status() 用在查詢 STA 介面之狀態或指定參數如 'rssi' (連線熱點之信號強度), 傳回值為整數, 但查詢 AP 介面時無傳回值或錯誤 (rssi) :

>>> ap.status()     #AP 無傳回值
>>> sta.status()    #STA 介面傳回狀態值
1010                                      #已取得 IP

STA 介面的狀態值 1010 表示 'STAT_CONNECTING' (介面連線中), 其他狀態如下表 :


 status() 傳回值 說明
 201 network.STAT_NO_AP_FOUND (搜尋不到 AP)
 202 network.STAT_WRONG_PASSWORD (密碼錯誤)
 1000  network.STAT_IDLE (介面閒置中 )
 1001 network.CONNECTING (連線中)
 1010 network.STAT_GOT_IP (已取得 IP)


>>> sta.status('rssi')      #所連線熱點信號強度
-49                                                   #強度 -49dBm
>>> ap.status('rssi')       #參數 'rssi' 只定義於 STA介面
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
OSError: STA required 

方法 config() 用來查詢 (傳入參數字串) 或設定 AP 介面之參數例如 essis, password, channel, 或 mac 等 :


 config() 參數 說明
 mac MAC 位址 (bytes 類型)
 essid AP 介面的 SSID (str 類型)
 hidden AP 介面的 SSID 是否隱藏 (boolean 類型)
 channel AP 介面的 WiFi 通道 1~13 (int 類型)
 authmode AP 介面的加密認證模式 (int 類型)
 password AP 介面的登入密碼 (str 類型)
 dhcp_hostname DHCP 主機名稱


這裡的 essid 為 ESS (Extend Service Set 延伸服務群集) 的名稱, ESS 是數個相鄰的 BSS (Basic Service Set 基本服務群集) 組成, 站台 (Station) 在同一個 ESS 範圍內會在各 BSS 間漫遊 (roaming). 簡言之, 一個 AP 就形成一個 BSS, 多個相鄰具有相同 essid 的 AP 就形成一個 ESS, 參考 :

WiFi 網路的識別: BSS, ESS, SSID, ESSID, BSSID
何謂SSID、BSSID、ESSID、HESSID ? @ 香腸炒章魚
連線到AP的種類有哪些及天線訊號容易被干擾嗎?

其中 STA 介面可用的僅 mac 與 dhcp_hostname 兩個參數, 其他都是 AP 介面專用. 注意, 查詢參數時, 參數名必須以字串傳入, 例如 ap.config('authmode'), 但設定時不要用字串, 例如 ap.config(authmode=4).

例如 :

>>> ap.config('mac')            #查詢 AP 介面之 MAC 位址
b'\x80}:\xb7\xa7]'
>>> ap.config('essid')           #查詢 AP 介面之 SSID
'ESP_B7A75D'
>>> ap.config('hidden')       #查詢 AP 介面之 SSID 是否隱藏
False
>>> ap.config('channel')      #查詢 AP 介面之通道
1
>>> ap.config('authmode')  #查詢 AP 介面之加密模式
0
>>> ap.config('password')   #查詢 AP 介面之登入密碼 (無此參數)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: unknown config param 
>>> ap.config('dhcp_hostname')    #查詢 AP 介面之 DHCP 名稱
'espressif'
>>> sta.config('mac')            #查詢 STA 介面之 mac 位址
b'\x80}:\xb7\xa7\\'
>>> sta.config('dhcp_hostname')   #查詢 STA 介面之 DHCP 名稱 
'espressif'
>>> sta.config('authmode')    #此參數為 AP 專用
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
OSError: AP required 

可見 AP 介面的密碼 password 不開放查詢, why?

雖然無法查詢 AP 密碼, 但可以用參數 password 去設定 AP 的密碼. 由於此 1.11 版韌體預設 STA 與 AP 介面都關閉 (v1.10 以前預設開啟 AP), 因此開啟 AP 介面後期預設加密模式為 0 (無加密), 必須先用 authmode 參數更改加密模式, 然後用 password 去設定密碼 (可以同時設定).

>>> ap.config(authmode=4, password='micropythoN')

另外, 上面兩個 mac 位址似乎怪怪的, 怎麼只有三組號碼呢? 這與上面 scan() 傳回的熱點 mac 一樣, 因為 config('mac') 傳回的是 byte 類型的資料, 用 print() 以 ASCII 顯示就變成亂碼了. 解決辦法是使用內建的 ubinascii 模組來轉換 byte 為 ASCII 碼 :

>>> import ubinascii 
>>> mac=ubinascii.hexlify(ap.config('mac'), ':') 
>>> print(mac) 
b'80:7d:3a:b7:a7:5d' 
>>> mac=ubinascii.hexlify(sta.config('mac'), ':')   
>>> print(mac)     
b'80:7d:3a:b7:a7:5c'   

此 ubinascii 來自 CPython 的 binascii 模組, 參考 : 


我用上傳 binascii.py 到 ESP32 也是可以用的 :

D:\Python\test>ampy --port COM3 put binascii.py /lib/binascii.py 

>>> import lib.binascii   
>>> mac=lib.binascii.hexlify(ap.config('mac'), ':')    
>>> print(mac)
b'80:7d:3a:b7:a7:5d'

config() 方法也可以設定參數值, 例如更改 AP 介面的 SSID :

>>> ap.config(essid='MYESP32')         #設定新的 SSID
I (18657780) wifi: Total power save buffer number: 16
>>> I (18657780) network: event 14
I (18657780) network: event 13
>>> ap.config('essid') 
'MYESP32

可見 SSID 已經被更新了.

但是 channel 似乎固定在通道 1 無法更改 :

>>> ap.config(channel=2)       #改為通道 2
I (18739770) wifi: Total power save buffer number: 16
>>> I (18739770) network: event 14
I (18739780) network: event 13
>>> ap.config('channel')          #查詢還是通道 1


而 password 參數可設定卻無法查詢 :

>>> ap.config(password='123456')               #更改 AP 密碼
I (19449800) wifi: Total power save buffer number: 16
>>> I (19449800) network: event 14
I (19449800) network: event 13
>>> ap.config('password')                              #密碼無法查詢
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: unknown config param

也可以一次設定好幾個參數 : 

>>> ap.config(essid='MYESP32', password='123456')

最後呼叫 STA 介面物件之 disconnect() 方法與熱點離線 : 

>>> sta.disconnect() 
I (20046240) wifi: state: run -> init (0)
I (20046240) wifi: pm stop, total sleep time: 85768549 us / 97553286 us

I (20046240) wifi: new:<11,0>, old:<11,0>, ap:<11,2>, sta:<11,0>, prof:11
>>> I (20046250) wifi: STA_DISCONNECTED, reason:8 

>>> I (20067130) event: station ip lost
I (20067130) network: event 8
>>> sta.isconnected() 
False 
>>> sta.status()    
8
>>> sta.status('rssi')         #沒有連線到熱點
W (20500530) wifi: Haven't to connect to a suitable AP now!
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
OSError: Wifi Not Connected 

>>> sta.ifconfig() 
('0.0.0.0', '0.0.0.0', '0.0.0.0', '192.168.43.1')

可見與熱點離線後 IP 又變回 0.0.0.0 了.

最後我將上面的 WiFi 功能整合為如下的 main.py 程式, 系統重啟時會自動執行 main.py 開啟 AP + STA 雙重模式 :

#main.py
import network
import time
import ubinascii

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

def scanAP():
    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 getIP():
  return sta.ifconfig()[0]

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


注意, 由於 ESP 本身熱點 AP 開啟後預設加密模式為 0 (無加密), 此處用 authmode 參數將其設為模式 4 (WPA/WPA2-PSK),  並用 password 設定其密碼為 'micropythoN'. 程式存成 main.py 後利用 ampy 上傳到檔案系統根目錄下 :

D:\ESP32>ampy --port COM8 put main.py 

然後開啟 Putty 連線至 ESP32 時會自動重啟, 這時會去執行 main.py, 用 dir() 檢視目前記憶體中的變數, 可見 network, time, ubinascii 等模組, 以及 ap, sta, showAP(), getIP() 等變數都載入記憶體可馬上存取 :

>>> dir() 
['uos', 'getIP', 'ap', 'sta', 'bdev', 'time', '__name__', 'showAP', 'ubinascii', 'gc', 'connectAP', 'network'] 
>>> showAP() 
I (62721) network: event 1
           TonyNote8    06:d6:aa:04:fc:4a     -54dBm
              EDIMAX    80:1f:02:ef:40:40     -90dBm
>>> connectAP('TonyNote8','blablabla') 
Connecting to WiFi AP ...
I (116151) wifi: ap channel adjust o:1,1 n:6,2
I (116151) wifi: new:<6,0>, old:<1,0>, ap:<6,2>, sta:<6,0>, prof:1
I (116711) wifi: state: init -> auth (b0)
I (116721) wifi: state: auth -> assoc (0)
I (116721) wifi: state: assoc -> run (10)
I (116761) wifi: connected with TonyNote8, channel 6, bssid = 06:d6:aa:04:fc:4a
I (116761) wifi: pm start, type: 1

I (116761) network: CONNECTED
I (119711) event: sta ip: 192.168.43.177, mask: 255.255.255.0, gw: 192.168.43.1
I (119711) network: GOT_IP
Connected:  192.168.43.177 
>>> getIP() 
'192.168.43.177'

另外一個 main.py 寫法 :

import network
import ubinascii

def connect(ssid, pwd):
    if not sta.isconnected():
        print('connecting to WiFi AP ...')
        sta.connect(ssid, pwd)
        while not sta.isconnected():
            pass
    print('Connected: ', sta.ifconfig()[0])


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]

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


參考 : 

MicroPython 功能集
ESP32 MicroPython: Connecting to a WiFi Network
https://docs.micropython.org/en/latest/library/network.WLAN.html
https://docs.micropython.org/en/latest/wipy/tutorial/wlan.html
https://docs.micropython.org/en/latest/esp32/tutorial/intro.html
BugWorkShop - 甲蟲工作室
MicroPython on ESP32
MicroPython Programming with ESP32 and ESP8266 eBook
# 如何選擇最佳的 Wi-Fi 無線網路頻道,獲得最佳的傳輸速度
連線到AP的種類有哪些及天線訊號容易被干擾嗎? 
什么是RSSI以及它与Wi-Fi网络的关系


2019-07-05 補充 :

我用 ESP-01 模組燒錄 v1.11 版韌體測試, 發現同樣版本的韌體, WALN 介面的預設是不同的 :
  1. ESP8266 : STA 關閉, AP 已開啟, 加密模式 4, 預設密碼 micropythoN
  2. ESP32 : STA 關閉, AP 關閉
我猜可能是為了維持用戶以前程式的相容性, ESP8266 新版韌體還是維持舊有的設定.

2019-07-09 補充 :

我發現上面那個 connectAP() 的寫法不妥, 當 ssid 或 password 錯誤時會因為連線不成功而在 while 迴圈裡無限循環, 應該改成如下, 使用 time 模組預設 8 秒來等候連線 :

#main.py
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 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]

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 裡面亦可, 把 main.py 留給應用程式, 但要保留 boot.py 裡面原來的程式設定, 例如 gc 與 webrepl 等.


2019-07-15 補充 :

今天在 NodeMCU-32S 板子上燒錄 bpi:bit 的韌體 (v1.10), 結果是可用的 (還沒發現是否有綁硬體的地方, 例如板上內建之感測器或致動器), 燒錄部分參考下面這篇的結尾 :

# bpi:bit ESP32 開發板測試 (一) : 燒錄 MicroPython 韌體

bpi::bit 最新 MicroPython 韌體下載網頁 :

https://github.com/BPI-STEAM/BPI-BIT-MicroPython/releases

我燒錄的是 firmware-20190623.bin 映像檔, 完成後用 ampy --port com3 get boot.py 檢查開機檔, 發現開機時會先載入自訂的 wifi 模組, 然後呼叫 wifi.ready() 方法 :

import esp
esp.osdebug(None)
#import webrepl
#webrepl.start()
import wifi 
wifi.ready() 

用 dir() 檢查記憶體中的系統預載物件, 發現確實有一個 wifi 物件存在, 以 type() 檢視發現它是一個模組 :

>>> dir() 
['bdev', 'bluetooth', '__name__', 'lines', 'uos', 'esp', 'wifi', 'gc', 'line']
>>> type(wifi) 
<class 'module'>

呼叫 wifi.ready() 發現它的作用其實是用來在開機時選擇 smartconfig 用的 : 

>>> wifi.ready() 
Press "A" to enter the smartconfig mode while the led is rolling
Started wifi in normal mode

用 dir(wifi) 檢視 wifi 模組, 發現它的架構有點怪, 裡面還包含了 network 模組, 與 MicroPython 原本的 network 模組比對發現, 其實 network 與 wifi.network 這兩個是一樣的 :

>>> dir(wifi)   
['__class__', '__name__', 'close', 'disconnect', 'ifconfig', 'isconnected', 'network', 'smartconfig', 'wlan', 'try_connect', 'is_smartconfig', '__irq_sc', 'ready']
>>> type(wifi.network)       #指向 network 模組
<class 'module'>
>>> dir(wifi.network) 
['__class__', '__init__', '__name__', 'AP_IF', 'AUTH_MAX', 'AUTH_OPEN', 'AUTH_WEP', 'AUTH_WPA2_PSK', 'AUTH_WPA_PSK', 'AUTH_WPA_WPA2_PSK', 'ETH_CLOCK_GPIO0_IN', 'ETH_CLOCK_GPIO16_OUT', 'ETH_CLOCK_GPIO17_OUT', 'LAN', 'MODE_11B', 'MODE_11G', 'MODE_11N', 'PHY_LAN8720', 'PHY_TLK110', 'PPP', 'STAT_ASSOC_FAIL', 'STAT_BEACON_TIMEOUT', 'STAT_CONNECTING', 'STAT_GOT_IP', 'STAT_HANDSHAKE_TIMEOUT', 'STAT_IDLE', 'STAT_NO_AP_FOUND', 'STAT_WRONG_PASSWORD', 'STA_IF', 'WLAN', 'mDNS', 'phy_mode', 'smartconfig']
>>> import network 
>>> dir(network)   
['__class__', '__init__', '__name__', 'AP_IF', 'AUTH_MAX', 'AUTH_OPEN', 'AUTH_WEP', 'AUTH_WPA2_PSK', 'AUTH_WPA_PSK', 'AUTH_WPA_WPA2_PSK', 'ETH_CLOCK_GPIO0_IN', 'ETH_CLOCK_GPIO16_OUT', 'ETH_CLOCK_GPIO17_OUT', 'LAN', 'MODE_11B', 'MODE_11G', 'MODE_11N', 'PHY_LAN8720', 'PHY_TLK110', 'PPP', 'STAT_ASSOC_FAIL', 'STAT_BEACON_TIMEOUT', 'STAT_CONNECTING', 'STAT_GOT_IP', 'STAT_HANDSHAKE_TIMEOUT', 'STAT_IDLE', 'STAT_NO_AP_FOUND', 'STAT_WRONG_PASSWORD', 'STA_IF', 'WLAN', 'mDNS', 'phy_mode', 'smartconfig']

再來看一下 wifi.wlan, 其實這是一個呼叫 network.WLAN(network.STA_IF) 後建立的 WLAN 物件 (STA), 我們可以自己建立一個 WLAN 物件來比較 (預設是 STA) :

>>> type(wifi.wlan)     
<class 'WLAN'> 
>>> dir(wifi.wlan) 
['__class__', 'active', 'config', 'connect', 'disconnect', 'ficonfig', 'isconnected', 'scan', 'status']
>>> w=network.WLAN() 
>>> dir(w)
['__class__', 'active', 'config', 'connect', 'disconnect', 'ifconfig', 'isconnected', 'scan', 'status']

可見 wifi.wlan 是一個 STA 介面的 WLAN 物件, 可以呼叫 scan() 來掃描周圍 AP, 然後呼叫 connect() 與 AP 連線, 所有 STA 的方法都能呼叫 :

>>> wifi.wlan.scan() 
[(b'TonyNote8', b'\x06\xd6\xaa\x04\xfcK', 11, -60, 3, False), (b'EDIMAX-meinung', b'\x80\x1f\x02G\x1e\xa8', 11, -75, 4, False)]
>>> type(network.WLAN) 
<class 'function'>
>>> wifi.wlan.connect('TonyNote8','blablabla')   
>>> wifi.wlan.isconnected() 
True
>>> wifi.wlan.status()
1010
>>> wifi.wlan.ifconfig()
('192.168.43.68', '255.255.255.0', '192.168.43.1', '192.168.43.1')
>>> wifi.wlan.status('rssi') 
-38
>>> wifi.wlan.config('mac')
b'0\xae\xa4\x8f\xc6L'
>>> wifi.wlan.config('essid')   
'TonyNote8'
>>> wifi.wlan.config('dhcp_hostname') 
'espressif'

總之, 若使用 bpi::bit 的 MicroPython 韌體, 在開機後就已經有 wifi.wlan 這個 STA 介面物件可直接使用, 而且要用到 network 模組時也是直接用 wifi.network 即可, 不須再 import network.

不過, 若要用到 AP 介面 (例如架設網頁伺服器) 則須自行建立, 不過方便的地方是如上所述, 可直接用 wifi.network.WLAN(1) 來建立 AP 介面物件, 不必再 import network :

>>> ap=wifi.network.WLAN(1) 
>>> type(ap) 
<class 'WLAN'>
>>> dir(ap)      #與 STA 一樣, 但 scan() 與 connect() 等無作用
['__class__', 'active', 'config', 'connect', 'disconnect', 'ifconfig', 'isconnected', 'scan', 'status']
>>> ap.active() 
False
>>> ap.active(True)   
True
>>> ap.ifconfig()   
('192.168.4.1', '255.255.255.0', '192.168.4.1', '0.0.0.0')
>>> ap.config('mac')     
b'0\xae\xa4\x8f\xc6M'
>>> ap.config('essid')   
'ESP_8FC64D'
>>> ap.config('hidden')   
False
>>> ap.config('channel')   
11
>>> ap.config('authmode')   
0
>>> ap.config('dhcp_hostname')   
'espressif'

可見 AP 預設的認證模式是 0 (未加密), 最好用 config() 將其改為 4 : 

>>> ap.config(authmode=4, password='micropythoN') 
>>> ap.config('authmode') 
4
>>> ap.config('password')   
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: unknown config param

此處為了測試方便, 我將其設定為舊版韌體 AP 的預設密碼 'micropythoN', 實際上線運轉時務必更改. 同樣地, 密碼是不能查詢的. 整體來說, 我覺得 bpi::bit 的韌體比官方的更好用. 

5 則留言 :

手塚玥 提到...

您好!請問為何當main.py put到ESP32後就會無法再與電腦連線呢?不論是用ampy或用uPyCraft都沒辦法連線,只能重新erase_flash!

小狐狸事務所 提到...

Hi, ampy 會跟 webrepl 沖突 (搶 com port), 所以用 ampy 時 webrepl 要關掉網頁. 參考 :

http://yhhuang1966.blogspot.com/2017/05/micropython-on-esp8266_18.html

資工路人 提到...
作者已經移除這則留言。
資工路人 提到...

請問network模組要去哪下載,我用pip3 install network的方式下載的沒有WLAN這個類別

小狐狸事務所 提到...

Hi, network 模組是 MicroPython 內建, 燒錄韌體後即已有, 不需安裝喔.