🔖

ESP32でUDP通信する方法を誰よりもわかりやすく解説 お試し編

2023/01/08に公開

「esp32 UDP 方法」でググると、色々な記事がヒットしますが、断片的な情報しか書かれていない記事が多すぎます。
また、ホストやポートなどで、いきなり「127.0.0.1」と書かれても、それが任意の数値なのか何か意味のある数値なのか解説されていないので、初心者にはあまりにも不親切です。

そこで、この記事では工作をするために必要な情報を解説しつつ、読んだ人が誰でも作れることを目的に解説します。

※ ぶっちゃけ、レイヤーとかプロトコルなんて工作する上ではどうでもいいので、その辺の解説はしません

UDPとTCP どちらを選ぶか

前提として、この記事に辿り着いた時点で、読者は「ESP32やラズパイを使って、無線通信をしたい」という人でしょう。

そして、UDPは信頼性が少ないものの高速に転送できて、TCPは反対に信頼性は高いもののやや重いくらいのイメージはついてるかと思います。

結論から言うとUDPの方がおすすめです。

まずコードがシンプル。これが一番重要です。

ほとんどの場合、デバイス側は温度計や光度計などと繋いだESP32となるでしょう。
デバイス側は、なるべくシンプルなプログラムで消費電力を抑えたほうが良いですし、故障などした時は簡単に交換できた方が良いです。

「信頼性が少ない」という点について懸念されるかもしれませんが、普通に使う分には問題ないです。
(そもそも問題あれば使い物になりませんよね)

データの精製やDBへの書き込みなど、重い操作はサーバー側(ラズパイやfirebaseなど)に任せて、デバイス側は愚直に測定した温度データを送るようにすると、役割が明確になって組みやすいです。

まずはUDP通信を試す

お手持ちのMacBookでUDP通信を試してみましょう。
python3.9がインストールされている前提で話をします。

UDP通信では、情報を受け取る側をserver側、情報を発信する側をclient側と呼びます。

server.py
from socket import socket, AF_INET, SOCK_DGRAM

HOST = '127.0.0.1'
PORT = 9000 # 何でも良い

s = socket(AF_INET, SOCK_DGRAM)

s.bind((HOST_NAME, PORT))

while True:
    rcv_data, addr = s.recvfrom(1024)
    print("receive data : [{}]  from {}".format(rcv_data,addr))
    
s.close()

HOSTのIPアドレスは、必ず127.0.0.1 にしてください。
127.0.0.1は自分自身(今の状態では貴方のMacBook)を指す特別なIPアドレスです。

それ以外のアドレスを指定すると、以下のようにエラーが出ます。

Traceback (most recent call last):
  File "/Users/myname/Desktop/udp/server2.py", line 9, in <module>
    s.bind((HOST, PORT))
OSError: [Errno 49] Can't assign requested address

PORTは何でも良いです。4桁に限らず5桁でも良いです。

s = socket(AF_INET, SOCK_DGRAM)

この部分はUDP用のソケットを作成しています。
AF_INETはIPv4でインターネット接続をすること、SOCK_DGRAMはUDP通信の意味です。

s.bind((HOST_NAME, PORT))

この部分は、IPアドレスとポート番号を指定し、受け取り準備をしています。

s.recvfrom(1024)

client側からのメッセージを待ち受けている部分です。
メッセージがない限りは、ここで停止しています。
返り値として、受け取ったデータと、送信元アドレスを返します。

続いて、メッセージを送る側(client側)を作成しましょう

client.py
from socket import socket, AF_INET, SOCK_DGRAM

HOST = '127.0.0.1'
PORT = 9000 # server側で設定した番号と同じ

client = socket(AF_INET, SOCK_DGRAM)
client.sendto(b'Hello UDP', (HOST, PORT))
client.close()

sendtoの引数に、バイナリ形式にしたデータを入れて送ることができます。
以下、数値や文字列をバイナリ形式にする方法です。

数値

to_bytes()メソッドを使います
int.to_bytes(length=1, byteorder='big', *, signed=False)

client.py
intToBin = (12345).to_bytes(2,'little')

数値はカッコで囲む必要があります。int(12345)としても良いです。
2は2バイトを示しています。1バイトなら0~255の256通りを指定できますし、2バイトなら65536通りです。
第二引数はbyteorderで、'big'か'little'を指定します。littleは数列のエンド側から送ります。
どちらを指定しても良いですが、送る側と受け取る側で合わせましょう(でないと、受け取った数列が逆になってしまいます)
数値がマイナスになる可能性がある場合は、第三引数にTrueを指定します

server.py
data_int = int.from_bytes(2,'little')

受け取り側では、from_bytes()メソッドで数値に変換します。

文字列

encode()メソッドを使います

client.py
strToBin = 'message'.encode()

特に解説は不要ですね。
エンコードというのは、人間が読める情報をコンピューターが読める情報に変換することです。

server.py
data_str = rcv_data.decode()

反対にコンピューター用語を人間が読める情報に変換することをデコードと言います。

dict型

json形式に変換してから、文字列としてバイナリ型に変換します

client.py
import json

dictSample = dict({'id':'aaa','value':123})
dictToJson = json.dumps(dictSample)
dictToBin = dictToJson.encode()

dictというのは、Javascriptで言うオブジェクトです。
実践では、おそらく数値や文字列だけを送るケースは少なく、データとしてまとまった形のほうが扱いやすいです
(私がweb系を主に扱っていた経験からかもしれませんが)

そのままでは変換できないので、一旦JSON形式に変換してから、文字列と同様にエンコードします。
ちなみにjsonを扱うにはjsonライブラリをインポートする必要があるので注意

server.py
data_json = rcv_data.decode()
data_dict = json.loads(data_json)
print(data_dict)

jsonをdictに変換するには、json.loads()を使います。
(json.loadsのsは三単現のsではなく、stringのsですかね)

ESP32で通信する

UDP通信がおおよそわかったところで、ESP32で通信しましょう。

先ほどはMacBook内での通信でしたので、ローカルホストで良かった(wifiに繋いでなくてもOK)でしたが、
今回はMacBookもESP32もネットワーク(自宅wifi)に接続されている必要があります。

MacBookのIPアドレスは、システム環境設定 > ネットワークで調べられます。

この例だとIPアドレスは'100.64.1.51'です。

server側(MacBook側で受け取る方)を書き直しましょう。

server.py
from socket import socket, AF_INET, SOCK_DGRAM

HOST = '100.64.1.51' # 書き換え
PORT = 9000 # 何でも良い

s = socket(AF_INET, SOCK_DGRAM)

s.bind((HOST_NAME, PORT))

while True:
    rcv_data, addr = s.recvfrom(1024)
    print("receive data : [{}]  from {}".format(rcv_data,addr))
    
s.close()

続いてESP32です。

import ujson
from usocket import socket, AF_INET, SOCK_DGRAM

################## wifi接続 ##############

SSID = "自宅wifiのSSID"
PASSWORD = "自宅wifiのパスワード"

def do_connect():
    import network
    wlan = network.WLAN(network.STA_IF)
    wlan.active(True)
    if not wlan.isconnected():
        print('connecting to network...')
        wlan.connect(SSID, PASSWORD)
        while not wlan.isconnected():
            pass
    print('network config:', wlan.ifconfig())

do_connect()

################## UDP通信 ##############

HOST = '100.64.1.50'
PORT = 9000 # server側で設定した番号と同じ

s = socket(AF_INET, SOCK_DGRAM)

for i in range(3):
    data = int(i+1).to_bytes(2,'little')
    s.sendto(data,(HOST, PORT))
    print(i)

s.close()

Discussion