| ESP32 教學 | MicroPython | 認識 Socket 與 TCP Server 實現 ESP32 遠端控制 | 307 |

socket 是什麼?socket 是在 TCP/IP 傳輸重要的環節,透過這樣的介面 API,讓我們不用去處理 TCP/UDP 通訊協議的底層,進而執行網路通信。micropython 也移植電腦平台 python 的 socket 方法,這篇就來瞭解如何使用 usocket 類別來做最基礎的 tcp 訊息傳送。

小提醒: 進行 socket 通訊前,ESP32 必須要連上網路,如果不熟悉怎麼 wifi 上網的朋友,可以參考下面這篇:

1. socket 簡介

socket 在 wiki 中的定義如下:

在作業系統中,通常會為應用程式提供一組應用程式介面(API),稱為插座介面(socket API)。應用程式可以通過插座介面,來使用網路插座,以進行資料交換。

有點難懂對吧? 😆 換個角度思考一下,如果有一天你想要撰寫網路應用程式時,什麼是第一個需要面對的課題? 想必就是搞定 TCP/UDP 通訊協定! 幸運的是,OS(作業系統) 通常會提供 socket 這樣的介面,搭起一個橋樑,概念有點像是把 底層 TCP/IP 的工作『封裝』起來,程式設計師僅需專注在 Server 與 Client 端的上層應用開發即可,所以 socket 放在 TCP/IP 網路層級的位置,就會像下面這樣:

socket

有了基本的認知後,現在就來看一下怎麼在 ESP32 內利用 socket API 進行網路通訊。

2. socket 類別與 TCP 連線流程

socket 介面可以用的方法有很多,有分 Server 或 Client 端使用的,也有兩者都可共用的,如果要以 ESP32 的作為遠端受控的角色來說,server 端還是比較常用的,因此哥這邊整理一些關於伺服器端,如採用 TCP 協定時會用到的方法:

  1. socket.socket(socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP): 初始化 socket 物件,可以用來設定 TCP或 UDP通訊協定; IPv4 或 IPv6 等。
  2. socket.bind(address):Server 端用來綁定指定 IP 與 Port。
  3. socket.listen(backlog):設定最大的連線數量。(這邊的 backlog 就是指定在 server 端最大的連線數量)
  4. socket.accept():接收來自 client 的連接。
  5. socket.send(bytes):在建立連線後,利用此方法來傳送資料訊息,小地方提醒一下,傳送的資料是 bytes 型態。
  6. socket.recv(bufsize):接收來自 client 傳遞的資料,bufsize 指定最大的資料 bytes大小。

上面的這些方法先大概有個基本認知,後面會在解釋更清楚,搭配下面的這張圖表應該就會更瞭解完整的TCP連線流程。

首先來看一下 server 端, 一開始初始化 socket(),透過 bind() 綁定 Ip 與 服務 port ,並設定最大連線數,緊接等待 client 的連線,此處由於是阻斷模式(Blocking Mode),也就是程式會在 此處停下等待連線;待 tcp 建立連線成功後(也就是 accept()成功連到 client ),之後接收來自 client 訊息,並回傳訊息,完成關閉此次連線狀態,以上就是 tcp server 最基本的連線流程。

相較於 server 較多的連線步驟, client 的容易理解多了, 初始化 socket() => 嘗試建立連線connect() => 傳送訊息 => 接收訊息=> 結束連線,整個流程就跟我們一般認知的過程類似,所以哥這邊就不再多加著墨。

3. 建立簡易 TCP Server

接著我們可以利用 socket 方法在 ESP32 架設一個簡易 TCP server,並透過電腦平台的 TCP 測試軟體做為進行,實現遠端控制 IO 功能。

>> 電路連接

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

>> 完整程式碼

# ESP32 as a tcp server for remote control
# 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('tp_station','xxxxxxxx')
        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)

gc.collect()
wifi= network.WLAN(network.STA_IF)
go_wifi()
hostip = wifi.ifconfig()[0]
tcp_server=socket.socket(socket.AF_INET,socket.SOCK_STREAM) 
#tcp_server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
tcp_server.bind((hostip,2000))        	#server ip and port
tcp_server.listen(1)             		#listen only 1 connection
rev_num=0								#calculate the message number 

while 1:
    print("tcp server is listening")
    tcp_conn,client_addr = tcp_server.accept() 
    print(client_addr, "Client connects sucessfully")
    while 1:       
        msg=tcp_conn.recv(128)
        if len(msg) > 0:          
            msg=msg.decode()        #convert bytes to strings
            if msg[0:4]=="led=":
                if(msg[4]=='1'):
                    led.value(1)
                else: 
                    led.value(0)
            else:
                print('This comannd can not be recognized')    
            rev_num +=1
            tcp_conn.send('the server has received your msg {}'.format(rev_num))
        else :
            print('client socket is disconnected')
            break

>> 程式解說

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('tp_station','xxxxxxxx')
        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 連網的機制, 在程式第 9 行 wifi.connect() 內的兩個參數,分別為無線基地台的 ssid 與密碼;程式第 11 行開始利用 for 迴圈設計1個 20 秒的 timeout 機制,連線成功後,顯示相關網路資訊。

gc.collect()
wifi= network.WLAN(network.STA_IF)
go_wifi()
hostip = wifi.ifconfig()[0]
tcp_server=socket.socket(socket.AF_INET,socket.SOCK_STREAM) 
# tcp_server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
tcp_server.bind((hostip,2000))        #server ip and port
tcp_server.listen(1)             	#listen only 1 connection
rev_num=0							#calculate the message number

這邊開始為主程式流程,首先執行 gc.collect() 手動將多餘的記憶體空間回收,在建立 wifi 的物件後,執行上網動作取得 IP,相關的網路參數會放在 wifi.ifconfig() 這個回傳值內,格式為 ( “本機IP”,”子網路遮罩”,”Gateway”,”DNS”),所以如果需要當下 ESP32 的 ip 位址,就可以利用 wifi.ifconfig()[0] 得知。

程式第 29 行開始就是我們上一節提到的 tcp 連線流程,各位關注在 tcp server 端即可,socket.socket 方法中的socket.AF_INET是 IPv4 協定(也就是一般常用的4個數字定義的 ip 版本,還有 socket.AF_INET6 則是 IPv6 協定),socket.SOCK_STREAM 是設定 TCP 通訊。

初始化 socket 後,tcp_server.bind((hostip,2000)) 設定主機的ip 與服務 port,這邊隨機選定 2000;tcp_server.listen(1) 限制tcp連線的數量僅能有1個。 最後 rev_num 此參數用來記憶收到簡訊的次數。

while 1:
    print("tcp server is listening")
    tcp_conn,client_addr = tcp_server.accept() 
    print(client_addr, "Client connects sucessfully")
    while 1:       
        msg=tcp_conn.recv(128)
        if len(msg) > 0:          	#check if tcp connection is alive 
            msg=msg.decode()        #convert bytes to strings
            if msg[0:4]=="led=":
                if(msg[4]=='1'):
                    led.value(1)
                else: 
                    led.value(0)
            else:
                print('This comannd can not be recognized')    
            rev_num +=1
            tcp_conn.send('the server has received your msg {}'.format(rev_num))
        else :
            print('client socket is disconnected')
            break

程式第 35 行主要的 while 迴圈在於 等待遠端 client 的連線用,在一次成功的 tcp 斷線後,便會重新起動等待再一次連線。當程式跑到第 37 行 tcp_server.accept() 時,將會啟動 Blocking Mode,也就是主執行緒會在此等待 client 端連上我們的 tcp server 。一旦 server 接收 client 連線請求,系統會針對此次連線建立一個新的 socket 物件(tcp_conn) 與 用戶端位址資訊(client_addr),我們也就可以針對此次連線進行後續操作。

程式第 39 行的次 while 迴圈功能為連線成功後的訊息傳遞循環,直到此次連線關閉。msg=tcp_conn.recv(128) 除將接收訊息放入msg變數內, 並指定接收訊息最大的 bufsize 為 128 bytes。還記得我們先前提到 tcp 傳送訊息都是用 bytes 型態傳送嗎?為了能夠順利解析訊息內容,msg=msg.decode() 將訊息的資料從 bytes 型態轉成 string 型態。

程式第 41 – 47 行用來快速判斷伺服器接收的資訊是否符合指定要求,這邊我們用很簡單的指令: led=1 代表 LED 點亮, led= x 其他數字就是熄滅 LED, 若格式不對,則回顯示指令無法辨識(not recognized)。而 server 端在每一次接收接收 client 端送出的訊息後,便透過 tcp_conn.send() 回傳 client 端,表示 server 端有收到訊息。

4. 測試遠端 IO 控制

現在我們的 ESP32 tcp server 已經架設完畢,該怎麼測試?如果各位使用者會電腦平台上的 python 語言,當然可以自己利用 socket api 再寫一個PC版的 tcp client,不過其實可以不用這麼麻煩啦!哥這邊推薦大家可以使用一套 SocketTest ,軟體作者為 Akshathkumar Shetty,執行的環境需安裝 JRE( java runtime environment),完整的安裝與執行過程如下:

  1. 先到 java 的官網下載 ( https://www.java.com/zh-TW/download/ ),下載 java 軟體,並且進行安裝。(如先前已安裝過就可以跳到步驟 3)
  2. 將下載下來的檔案,點擊進行安裝,直到安裝完成。

  3. 接著到作者的 github 網站 https://github.com/akshath/SocketTest,下載打包整個檔案:

  4. 解壓縮下載的檔案,找到 dist 目錄內的 socketTest.jar,按下滑鼠右鍵,選擇 java platform 執行。

  5. 此時可以開始測試我們ESP32 的 tcp server,軟體操作很直覺,選擇 client 的標籤頁,輸入 EPS32 TCP Server 的 IP 與服務 Port,按下 Connect 按鈕。

  6. 連線成功後在訊息欄輸入 led=1,順利的話就可以看到 ESP32 的控制板上的 LED 點亮,也可以看到 server 回傳的相關訊息;若訊息輸入 led=0,LED 則會熄滅。

5. 小結

想要將 ESP32 硬體做成一個可以遠端控制的模組,TCP Server 是最直覺的方法,建立 TCP 連線後,訊息解析與傳遞,就可以實現 Remote IO 的機制,但還是有些不便的地方,也就是在 Client 端軟體部分。此篇是透過現有的 socketTest 工具進行用戶端測試,應用到實際面向的話,程式設計者通常得開發特定的軟體給使用者操作(也就是自己寫介面,不管用 java 或 python 等等),這就得花費更多的時間與成本。有更好的方法可實現簡單的遠端控制嗎?有!就是把 ESP32 架設成 webserver,使用者僅需要瀏覽器,意味著手機或電腦都可進行操作,下一篇哥在來分享 ESP32 如何架設簡易 webserver 步驟與程式碼!

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

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

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

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

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

發佈留言

Close Menu