在上一篇中我們已經知道如何使用 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標籤:
<html></html>
: 看到<html>
這樣的標籤,表示這是 html 格式文件,瀏覽器會針對兩個標籤所包圍的內容,開始進行 html 元素轉換。<head> </head>
:此部分內容就是『標頭』,包含了網站標題、網頁格式、meta等資訊。<body> </body>
: 為網頁內容的主體標示,所有要呈現給使用者看到的資訊,都會放在這個標籤包含的內容內。<h1> </h1>
: 內容標題的標籤,通常會有<h1><h2><h3>
等,表示不同大小的標題。<p> </p>
: 段落的標籤,被此標籤包圍表示該內容為一個文字上的段落。<input>
: 可以應用於按鈕輸入,如果此按鈕帶有外部連結,格式為<input type="button" value="文字" onclick="location.href='網址'">
。<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、跟其他情況(也就是回傳預設網頁),實際的網頁設計如下:
畫面中將出現兩個按鈕,LED ON 跟 LED OFF,對使用者來說,當按下按鈕時,其實就是將瀏覽器網址列換成 http:// serverIP/?led1 或 http:// 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
Ian
5 8 月 2021可以加實體按鈕讓網頁和實體都能控制燈嗎?
jimi
6 8 月 2021Hi~ 關於如果要同時可透過實體按鈕與網頁並行的方式控制LED的話,因為此範例程式在 socket 為所謂 blocking(程式第46,48行)的方式寫的,所以如果要再加上實體按鈕去控制的話,2個想法提供給您:
1.可以將socket通訊程式改成non-blocking的方式,再把實體按鈕判斷的機制加入主要的while迴圈判斷即可
2.利用中斷的方式加入實體按鈕判斷機制(可用定時中斷或按鈕觸發中斷)
這兩個作法應都可以解決這樣的需求,如果有其他問題,歡迎留言討論,謝謝!