| ESP32 教學 | MicroPython | ESP32 WebServer 實現遠端控制 | 308 |

在上一篇中我們已經知道如何使用 socket ,來建立一個可以實現遠端控制 IO 的 Tcp Sever,但以使用者介面來說,TCP的操作並不是那麼直覺,如果可以將 ESP32 架設成網頁伺服器的話,無論是移動裝置或電腦,就可以利用瀏覽器軟體進行操作,所以這篇就來分享如何透過 ESP32 WebServer 實現遠端控制功能。

Tips: 在進行此篇測試前,對 socket 操作不太瞭解的朋友可以先參考這篇: 認識 Socket 與 TCP Server 實現 ESP32 遠端控制

1. Socket 與網頁伺服器

要怎麼使用 Socket 這樣的介面來做出簡單的網頁伺服器呢?用白話一點的方式說,網頁伺服器就像我們之前練習過的 TCP Server 的升級版本,網頁服務所需要的 HTTP 通訊協定也就是架構在 TCP/IP 層級上的應用,所以我們只要讓 Server 與 Client 之間的訊息,『符合』HTTP 規定就可以啦!這邊再一次複習基本 TCP 建立連線的步驟:

將上面這張圖用網頁服務的角度來看,流程圖的 Server 端我們就可以視為網頁伺服器,右邊的 Client 端也就是用戶的瀏覽器設備(像 Firefox、Chrome),當我們用戶端使用瀏覽器來嘗試連線主機時(也就是建立 Tcp 連線),主機接收瀏覽器的訊息後(訊息就是”用GET 指令取得網頁內容”),主機就會透過 send() 指令回傳網頁的訊息給用戶端,至於網頁的訊息格式是什麼? 這邊就是我們常見的 html 語法,瀏覽器會將 html 語法,轉換成看的懂的網頁內容,以上就是整個網頁伺服器與瀏覽器溝通的過程。

2. Html 語法

這篇主要不是要教大家寫網頁(哥也不是專家😂),但為了要能夠用 ESP32 架起簡單的 WebServer, 一些基本的概念還是要有,簡單的說,網頁都是透過一種叫 html 語法所組成的,語法的概念為成對標籤的用法,例如像是 <html></html> ,各位如果想知道某個網站網頁的原始型態長什麼樣,每個瀏覽器都有支援檢視功能,只要在該網頁的空白處按下滑鼠的右鍵, Firefox 是『檢視原始碼』,Chrome 是『檢視網頁原始碼』,就可以看到如下面的畫面:

看起來很密密麻麻吧! 不過不用擔心,現在大多寫網頁很少直接用 html 語法直接寫,很多都是透過所見即所得的HTML 編輯器來進行,比較能夠快速做出美觀又豐富的網頁,當然以我們的遠端控制需求來說,如果要追求好看的畫面,也可以此方法進行開發,不過最重要的問題就是 ESP32 的效能問題,畢竟 ESP32 並不是一顆所謂的高效能並針對網路優化的CPU(看價格就知道了😂),要實現簡單的網頁伺服器功能還可,但稍複雜的應用就不太行,回到我們的需求來說,能夠進行遠端控制才是最重要的,所以我們只需要簡單瞭解常用的 html 標籤,並構建出基本畫面滿足功能即可。

下面為一些常用的html標籤:

  1. <html></html>: 看到 <html> 這樣的標籤,表示這是 html 格式文件,瀏覽器會針對兩個標籤所包圍的內容,開始進行 html 元素轉換。
  2. <head> </head> :此部分內容就是『標頭』,包含了網站標題、網頁格式、meta等資訊。
  3. <body> </body>: 為網頁內容的主體標示,所有要呈現給使用者看到的資訊,都會放在這個標籤包含的內容內。
  4. <h1> </h1>: 內容標題的標籤,通常會有<h1><h2><h3>等,表示不同大小的標題。
  5. <p> </p>: 段落的標籤,被此標籤包圍表示該內容為一個文字上的段落。
  6. <input>: 可以應用於按鈕輸入,如果此按鈕帶有外部連結,格式為 <input type="button" value="文字" onclick="location.href='網址'">
  7. <title></titile>:”網站”標題的標籤,通常使用一個即可。

以上就是常見的 html標籤,所以一個最基礎的網頁架構長的像下面這樣:

<html>
    <head>
        <title>ESP Web Server</title>
    </head>
    <body>
        <h1>ESP32 Web Server</h1> 
    	<p>ESP32 + Micropython are so GOOD </p>
    </body>
</html>

最後 JIMI哥 這邊分享一個好用的 html 線上編輯器 https://html-online.com/editor/

你可以將編輯好的 html 文件貼到右邊的編輯區,檢查左邊實際畫面是否 OK,當然也可以直接編輯左邊的實際畫面,html 語法區也會新增對應的內容,非常方便試做簡單的網頁。

3. 建立簡易 ESP32 WebServer

接著我們可以利用 socket 介面與 html 網頁來架設一個簡單的 ESP32 Web Server,進而實現遠端控制 IO 功能。

>> 電路連線

此次我們一樣透過一個 LED 的亮滅來作為遠端的 IO 控制測試,此顆 LED 連線到 GPIO14 腳位。

>> 完整程式碼

#The details in https://jimirobot.tw
import machine,network,socket,gc,utime
from machine import Pin
led=Pin(14,Pin.OUT)

def go_wifi():
    try:
        wifi.active(False)
        wifi.active(True)
        wifi.connect('JAMES123','10xxxxxxxxx')
        print('start to connect wifi')
        for i in range(20):
            print('try to connect wifi in {}s'.format(i))
            utime.sleep(1)
            if wifi.isconnected():
                break          
        if wifi.isconnected():
            print('WiFi connection OK!')
            print('Network Config=',wifi.ifconfig())
        else:
            print('WiFi connection Error') 
    except Exception as e: print(e)


def html_content():
    html = """<html><title>ESP Web Server</title> 
    <body> <h1>ESP32 Web Server</h1> 
    <p>LED Status =  """+ str(led.value()) +"""</p>
    <p> <input type="button" value="LED ON" onclick="location.href='/?led1'">
        <input type="button" value="LED OFF" onclick="location.href='/?led0'">
    </body></html>"""
    return html
  
gc.collect()
wifi= network.WLAN(network.STA_IF)
go_wifi()
hostip = wifi.ifconfig()[0]
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) 
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind((hostip,80))        #server ip and port
s.listen(3)                #listen only 3 connection

while 1:
    gc.collect()
    print("web server is listening")
    tcp_conn,client_addr = s.accept() 
    print(client_addr, "Client connects sucessfully")
    http_req = tcp_conn.recv(1024)
    http_sreq = str(http_req)
    #print('https REQ-Content ={}'.format(http_sreq))
    str1='GET /?led1'
    str2='GET /?led0'
    if (http_sreq.find(str1) !=-1):
        led.value(1)
    elif (http_sreq.find(str2) !=-1) :
        led.value(0)
    else :
        pass
    response = html_content()      # return html string to response
    tcp_conn.sendall('HTTP/1.1 200 OK\n')
    tcp_conn.sendall('Content-Type: text/html\n')
    tcp_conn.sendall('Connection: close\n\n')
    tcp_conn.sendall(response)              #send out the html content
    tcp_conn.close()

>> 程式解說

#The details in https://jimirobot.tw
import machine,network,socket,gc,utime
from machine import Pin
led=Pin(14,Pin.OUT)

def go_wifi():
    try:
        wifi.active(False)
        wifi.active(True)
        wifi.connect('JAMES123','10xxxxxxxxx')
        print('start to connect wifi')
        for i in range(20):
            print('try to connect wifi in {}s'.format(i))
            utime.sleep(1)
            if wifi.isconnected():
                break          
        if wifi.isconnected():
            print('WiFi connection OK!')
            print('Network Config=',wifi.ifconfig())
        else:
            print('WiFi connection Error') 
    except Exception as e: print(e)

首先匯入一些需要的模組,machine、socket、utime 等,設定 GPIO14 腳位為數位輸出。go_wifi() 函式內容為進行wifi 連網的動作, 在程式第10 行 wifi.connect() 內的兩個參數,分別為無線基地台的 ssid 與密碼;程式第 12 行開始利用 for 迴圈設計 1 個 20 秒的 timeout 機制,連線成功後,顯示相關網路資訊。

def html_content():
    html = """<html><title>ESP Web Server</title> 
    <body> <h1>ESP32 Web Server</h1> 
    <p>LED Status =  """+ str(led.value()) +"""</p>
    <p> <input type="button" value="LED ON" onclick="location.href='/?led1'">
        <input type="button" value="LED OFF" onclick="location.href='/?led0'">
    </body></html>"""
    return html	

還記得本文第一節中提到用戶 client 成功連接網頁伺服器 server 後,伺服器要回傳訊息嗎? html_content() 函式的功能,就是回傳 html 格式的文件訊息(程式第 32 行 return 指令),第 26-31 行的 “html” 字串,就是預計要傳送的網頁文件,透過<html></html>標籤標示網頁內容,待用戶瀏覽器接收到後,便會轉化成一般的網頁圖面。

由於我們同時想要顯示目前的 LED 狀態,所以程式第 28 行的 str(led.value()),便是讀取目前led的值,轉換成字串型態放入網頁文字。

程式第 29-30 行:分別在畫面上設計兩顆按鈕 LED ON 跟 LED OFF 來作為切換 LED 亮暗, onclick="location.href='/?led1'",這邊參數的意思是指一旦按鈕按下去後,會啟動外部連結,而外部連結網址會放在 location.href 後面的單引號內容,也就是『/?led1』,所以完整的網址連結就是『目前主機IP/?led1』, 而對於用戶來說,就是發送了一個 Http Request GET 指令,我們也就可以透過這樣的 Reqest 指令,伺服器來判斷 LED 是需要點亮(?LED1)或關閉(?LED0)。

Tips: 如果對於Http Request 概念不是那麼熟的朋友,可以參考這篇的基礎介紹:305 使用 ThingSpeak HTTP API 上傳 DHT11 資訊

gc.collect()
wifi= network.WLAN(network.STA_IF)
go_wifi()
hostip = wifi.ifconfig()[0]
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) 
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind((hostip,80))        #server ip and port
s.listen(3)                #listen only 3 connection

這段為主程式開始,流程跟之前提到的 TCP Server 類似,首先利用 gc.collect() 回收記憶體空間,並設定 wifi 相關參數,執行 go_wifi() 嘗試連上指定的 AP,程式第 38 – 41 行就是在 Server 端建立 TCP 連線時用的步驟,初始化 socket() -> 綁定通訊埠 bind()-> 設定最大連線數 listen(),需注意的是通訊埠要設定 80,這是 Http 協定預設的,如指定其他通訊埠的話,在用戶瀏覽器( Firefox、Chrome )網址就得多填入 PORT 的參數。

while 1:
    gc.collect()
    print("web server is listening")
    tcp_conn,client_addr = s.accept() 
    print(client_addr, "Client connects sucessfully")
    http_req = tcp_conn.recv(1024)
    http_sreq = str(http_req)
    #print('https REQ-Content ={}'.format(http_sreq))
    str1='GET /?led1'
    str2='GET /?led0'
    if (http_sreq.find(str1) !=-1):
        led.value(1)
    elif (http_sreq.find(str2) !=-1) :
        led.value(0)
    else :
        pass

主 while 迴圈開始便是 webserver 與 client 互動的程式碼,當主程序執行到第 46 行時(s.accept()),等待用戶端連線成功後才會繼續往下;程式 47-49 行,在成功建立 TCP 連線後,print 輸出 client 端資訊,並接收 client 傳過來的訊息,將其換成字串型態。這邊 JIMI哥 並沒有把接收到的訊息透過 print 輸出(程式第 50 行),主要考量是如果透過 print 輸出 http request 這類較多的文字訊息時,會增加網頁伺服器與用戶端之間的回應時間(也就是你可能會需要多等一些時間才會等到網頁伺服器回應),不過有興趣的朋友可以將#號刪掉,列印出瀏覽器傳來的訊息參考,內容應該類似下面:

b’GET / HTTP/1.1\r\nHost: 172.20.10.3\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,/;q=0.8\r\nAccept-Language: zh-TW,en-US;q=0.7,en;q=0.3\r\nAccept-Encoding: gzip, deflate\r\nConnection: keep-alive\r\nUpgrade-Insecure-Requests: 1\r\n\r\n’

看起來很亂對吧!我們稍微的將上面的內容字串透過換行整理一下:

b'GET / HTTP/1.1\r\n
Host: 172.20.10.3\r\n
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0\r\n
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8\r\n
Accept-Language: zh-TW,en-US;q=0.7,en;q=0.3\r\n
Accept-Encoding: gzip, deflate\r\n
Connection: keep-alive\r\n
Upgrade-Insecure-Requests: 1\r\n
\r\n'

上面就是一個很標準的 HTTP Request,相較於簡易 TCP Server,網頁伺服器(HttpServer)與瀏覽器端設備(Firefox / Chrome)建立 TCP 連線時,意味兩者間的訊息傳遞是遵守 『HTTP 通訊協定』,除了 Client 用戶端發出 Request 外,網頁伺服器也會跟據需求進行內部處理,並回應所需要的網頁內容(html 格式的訊息),藉由這樣的機制介面,我們也就可以使用電腦或手機瀏覽器軟體來做 ESP32 遠端控制 IO 。

而程式第 51-58 行是 ESP32 在接收到 Request 訊息的處理,這邊是利用 HTTP GET 指令結合前面所設計的網頁按鈕來進行判斷,大致上可以分為三種情況,開啟 LED、關閉 LED、跟其他情況(也就是回傳預設網頁),實際的網頁設計如下:

webserver畫面中將出現兩個按鈕,LED ON 跟 LED OFF,對使用者來說,當按下按鈕時,其實就是將瀏覽器網址列換成 http:// serverIP/?led1http:// serverIP/?led0,但對於伺服器所接收的底層 HTTP 訊息就是 『GET /?led1 HTTP/1.1\r\n…….』或『GET /?led0 HTTP/1.1\r\n…….』(不懂的朋友可以參考上面說明的 HTTP Request 訊息內容,應該就懂了),所以只要將用戶端發送的訊息內容,搜尋關鍵字,就可以用來控制 LED 的亮滅 led.value()

response = html_content()      # return html string to response
    tcp_conn.sendall('HTTP/1.1 200 OK\n')
    tcp_conn.sendall('Content-Type: text/html\n')
    tcp_conn.sendall('Connection: close\n\n')
    tcp_conn.sendall(response)              #send out the html content
    tcp_conn.close()

最後的這段程式,是將設計好的網頁內容(字串型態),存入 response ,再來就是伺服器端回覆 response 的程序,從 “HTTP/1.1 200 OK\n……” 依序透過 sendall() 丟出,程式第 63 行回傳 html 文件訊息到用戶端,讓瀏覽器轉換成可與使用者互動的圖形介面,完成連線後,結束 tcp 連線 ( http 連線),回到 while 迴圈內的前面程序,等待用戶再次連線。

>> 操作結果

實際執行完整程式碼,並透過瀏覽器打入 http:// 172.20.10.3 (記得不是 https;這邊的 IP 是 ESP32 自動取得的 IP,各位朋友記得換成自己網路環境配發的位址),在網頁操作 LED ON 或 OFF按鈕,REPL 的輸出結果如下:

4. 小結

將 ESP32 變成簡易的網頁伺服器後,使用者就可以透過網頁的方式來進行遠端的 IO 控制,最大的好處當然是可以跨平台控制,不論是電腦/手機/平台皆可,但如果要運行複雜度高的網頁功能,ESP32 就略顯吃力,這也是我們需要依據專案情況考量的地方。而遠端控制除了利用 HTTP 協定外, MQTT 也是不錯的選擇,特別適合網路頻寬較低或環境較差的環境,未來 JIMI哥會在針對 MQTT 部分, 分享如何進行遠端控制。

今天的內容就到這邊,如果各位有遇到什麼問題,也歡迎在下面留言或寫信與我討論囉~

↓↓↓↓↓↓賣場連結↓↓↓↓↓↓

歡迎大家有需要的話,可以多多支持一下我們的蝦皮賣場喔! 😀 

吉米家官方店-創客機器人材料專賣 https://shopee.tw/jimirobot.tw

Follow JIMI哥 Twitter : https://twitter.com/jimirobot  <–得到最新文章通知

This Post Has 2 Comments

  1. 可以加實體按鈕讓網頁和實體都能控制燈嗎?

    1. Hi~ 關於如果要同時可透過實體按鈕與網頁並行的方式控制LED的話,因為此範例程式在 socket 為所謂 blocking(程式第46,48行)的方式寫的,所以如果要再加上實體按鈕去控制的話,2個想法提供給您:

      1.可以將socket通訊程式改成non-blocking的方式,再把實體按鈕判斷的機制加入主要的while迴圈判斷即可
      2.利用中斷的方式加入實體按鈕判斷機制(可用定時中斷或按鈕觸發中斷)

      這兩個作法應都可以解決這樣的需求,如果有其他問題,歡迎留言討論,謝謝!

發佈留言

Close Menu