ESP32のAsyncUDPを調べる

概要

UDPまわりの通信を調べてみました。

ESP32の通信について

主に有線と無線があります。

有線通信

SPI

一番高速ですが、3線以上の通信線が必要な方法です。有線LANもSPI接続の物があります。高速通信なので、ちょっとシビアです。

ESP32は親にあたるMaster側のみで利用することをおすすめします。Slaveで利用した場合には、最後の4バイトが受信できないハードウエア側のバグがあります。

I2C

比較的低速ですが、複数のデバイスとかんたんに通信することができます。

ESP32は親にあたるMaster側のみで利用することをおすすめします。Slaveでもハードウエア的には利用可能ですが、ESP-IDF環境以外で利用している例を見たことがありません。どこかでArduinoでのI2C Slaveの動作検証をしてみたいと思っています。

1wire

通信線が1本しかないのに通信できるプロトコルです。温度計などで使われていますが、ちょっと特殊なデバイス用です。

その他

パラレル通信とかいろいろありますが、独自プロトコルで汎用性はありませんので割愛します。

無線通信

Bluetooth

Bluetooth Classic

ちょっと古い規格で、主に使うのはSPP(Serial Port Profile)で、シリアル通信をかんたんに無線化できます。

非常にかんたんですが、テキストを送信するので冗長なプロトコルです。接続の待受をする方がSlaveで、利用する側がMasterになります。

一般的にはESP32がSlave側で使っていることが多いですが、他のSPPに接続するMaster側でも利用できますので、ESP32同士をSPPで接続もできるはずです。

上記が待ち受けを行うSlaveを利用した例です。

こちらは外部のSPPデバイスに接続する側のMasterの例です。

Bluetooth Low Energy

BLEと書かれている場合には、こっちのほうで本来のBluetoothの使い方に近いですが、かなり難しいです。

こちらも待ち受ける側がSlaveで、接続する側がMasterです。

上記が外部のBLEデバイスを操作するMaster側の例です。

こちらが、パソコンからの接続を待ち受けてキーボードとして振る舞うSlaveの例です。

Wi-Fi

ESP32はBluetoothの他に2.4G帯のWi-Fiにも対応しています。

ESP-NOW

ESP32を開発したEspressif社独自の通信方式です。相手のMacアドレス宛に通信をかんたんに行うことができます。

普通のWi-Fiと異なりアクセスポイントに接続する必要はなく、各端末同士が直接通信を行います。

プロトコル上は親子関係になっておらず、すべてのノードが同じ機能ですが、役割で束ねる場所にある端末をSlave、センサーデータなどを集める端末をControllerとよびます。

TCP/IP

一般的な通信で利用されるプロトコルです。Wi-FiルーターなどのWi-Fiアクセスポイントなどに接続するか、softAPを利用して自分がWi-Fiアクセスポイントになる必要があります。

上記がsoftAPを利用してWi-Fiアクセスポイントになりつつ、ブラウザからの接続を待ち受けている例です。

TCP/IPの場合には待ち受けている方をServer、接続する方をClientと呼ぶことが多いです。

上記がWi-Fiアクセスポイントに接続し、外部NTPサーバーから現在時刻を利用する例です。

UDP

TCP/IPと似たプロトコルですが、より単純な機能を提供しています。UDPではデータを送る機能のみを提供しており、相手に到達したのかがわかりません。

TCP/IPはUDPにデータ到達性の確認や、送信失敗した場合の再送処理などを追加したものになります。

そのためUDPはリアルタイム通信などには向いていますが、本当にデータが送信成功したかはわかりません。もし確認したい場合には、受取側が送信側に受信完了通知などを自分で送信する必要があります。

UDPもTCP/IPと同じように、待ち受けている方をServer、接続する方をClientとよびます。

通信の種類

通信をする場合には、主に送信先の選び方により3種類あります。

ユニキャスト

相手を指定して送信する方法。送信する前に相手を特定する情報が必要になります。

ESP-NOWの場合にはMACアドレスで固定ですが、TCP/IPやUDPの場合にはIPアドレスになります。IPアドレスはWi-Fiアクセスポイントなどにより変わる可能性があるので注意が必要です。

ブロードキャスト

近くにいる端末全員にデータを送信する方法です。

ESP-NOWの場合には端末同士のWi-Fi電波が届く範囲になります。

TCP/IPやUDPの場合には環境により異なりますが、同じWi-Fiアクセスポイントに接続している端末が多くの場合受信します。

ブロードキャストで近くにいる端末に通信を行い、相手を特定したところでユニキャストに移行するなどの方法もあります。ブロードキャストのみで大量通信を行うと、受信側に不要なデータが飛んでくる可能性があるので注意しましょう。

マルチキャスト

マルチキャストはちょっとわかりにくい通信方式で、送信したマルチキャストのIPアドレスで待ち受けている端末に送信を行います。

マルチキャストは特定のマルチキャストIPアドレスを事前に共有しておき、そのIPアドレスに送信すると、受信設定をしていた端末全員に対して送信がされます。

ブロードキャストは同じネットワークの人全員に届いてしまいますが、マルチキャストでは必要な複数の端末のみに一斉送信をすることが可能です。

事前準備

Wi-Fi設定

#include "WiFi.h"
void setup() {
  WiFi.begin("SSID", "KEY");
}
void loop() {
}

今回はWindowsのモバイルホットスポットという機能を利用して、パソコンをWi-Fiアクセスポイントにしました。

接続している端末のIPアドレスとMACアドレスがわかるので、非常に便利です。

ネットワーク名と、ネットワークパスワードを変更し、実験で利用する端末で一度実行しておきます。一度実行することで、次回から指定無しでWi-Fi接続することができますので、便利な機能です。

Arduino IDEを複数起動

通信のテストをする場合には、最低2台の端末が必要になります。そのためArduino IDEも2つ以上起動したほうが便利です。

通常に起動すると、2台別々のポートに接続できないので、上記の方法を利用して通常起動と、preferences.txtを切り替えた2台目用などを準備しておくとかなり便利です。

スケッチ例

AsyncUDPServer

#include "WiFi.h"
#include "AsyncUDP.h"
AsyncUDP udp;
void setup()
{
    Serial.begin(115200);
    WiFi.mode(WIFI_STA);
    WiFi.begin();
    if (WiFi.waitForConnectResult() != WL_CONNECTED) {
        Serial.println("WiFi Failed");
        while(1) {
            delay(1000);
        }
    }
    if(udp.listen(1234)) {
        Serial.print("UDP Listening on IP: ");
        Serial.println(WiFi.localIP());
        udp.onPacket([](AsyncUDPPacket packet) {
            Serial.print("UDP Packet Type: ");
            Serial.print(packet.isBroadcast()?"Broadcast":packet.isMulticast()?"Multicast":"Unicast");
            Serial.print(", From: ");
            Serial.print(packet.remoteIP());
            Serial.print(":");
            Serial.print(packet.remotePort());
            Serial.print(", To: ");
            Serial.print(packet.localIP());
            Serial.print(":");
            Serial.print(packet.localPort());
            Serial.print(", Length: ");
            Serial.print(packet.length());
            Serial.print(", Data: ");
            Serial.write(packet.data(), packet.length());
            Serial.println();
            //reply to the client
            packet.printf("Got %u bytes of data", packet.length());
        });
    }
}
void loop()
{
    delay(1000);
    //Send broadcast
    udp.broadcast("Anyone here?");
}

Arduinoに入っているESP32 Async UDPのスケッチ例のServer側のスケッチです。

const char * ssid = "***********";
const char * password = "***********";

上の方にあった上記2行を消してあります。

    WiFi.begin(ssid, password);

また、もともとはWiFi.begin()の引数を渡してありましたが、無指定に変更して、最後に接続したときのSSIDとKEYを利用するようにしました。

処理的にはWi-Fiアクセスポイントに接続し、udp.listen(1234)にて1234ポートで待ち受けを行っています。

ポート番号を変更することで、複数の用途で待ち受けをすることができます。

無名関数を利用しているので、ちょっとわかりにくいですが、AsyncUDPPacket packetを引数としたコールバック関数を定義して、UDPパケットを受信したら呼び出しています。

loop()の中でudp.broadcast()を送信しているので、自分が送信した通信も大量に受信しています。

AsyncUDPClient

#include "WiFi.h"
#include "AsyncUDP.h"
AsyncUDP udp;
void setup()
{
    Serial.begin(115200);
    WiFi.mode(WIFI_STA);
    WiFi.begin();
    if (WiFi.waitForConnectResult() != WL_CONNECTED) {
        Serial.println("WiFi Failed");
        while(1) {
            delay(1000);
        }
    }
    if(udp.connect(IPAddress(192,168,137,104), 1234)) {
        Serial.println("UDP connected");
        udp.onPacket([](AsyncUDPPacket packet) {
            Serial.print("UDP Packet Type: ");
            Serial.print(packet.isBroadcast()?"Broadcast":packet.isMulticast()?"Multicast":"Unicast");
            Serial.print(", From: ");
            Serial.print(packet.remoteIP());
            Serial.print(":");
            Serial.print(packet.remotePort());
            Serial.print(", To: ");
            Serial.print(packet.localIP());
            Serial.print(":");
            Serial.print(packet.localPort());
            Serial.print(", Length: ");
            Serial.print(packet.length());
            Serial.print(", Data: ");
            Serial.write(packet.data(), packet.length());
            Serial.println();
            //reply to the client
            packet.printf("Got %u bytes of data", packet.length());
        });
        //Send unicast
        udp.print("Hello Server!");
    }
}
void loop()
{
    delay(1000);
    //Send broadcast on port 1234
    udp.broadcastTo("Anyone here?", 1234);
}

こちらもServerと同じくSSIDとKEYの指定を消しています。

まずはServer側を起動して、シリアルコンソールに出力されたIPアドレスを調べます。

    if(udp.connect(IPAddress(192,168,137,104), 1234)) {

そして、Client側の上記の行を、調べたIPアドレスに書き換えます。IPアドレスは.(ドット)区切りの文字列ですが、関数の引数は,(カンマ)区切りなので気をつけてください。

UDP Listening on IP: 192.168.137.104
UDP Packet Type: Unicast, From: 192.168.137.104:1234, To: 192.168.137.104:1234, Length: 20, Data: Got 20 bytes of data
UDP Packet Type: Unicast, From: 192.168.137.167:49153, To: 192.168.137.104:1234, Length: 20, Data: Got 20 bytes of data
UDP Packet Type: Broadcast, From: 192.168.137.167:49153, To: 255.255.255.255:1234, Length: 12, Data: Anyone here?
UDP Packet Type: Broadcast, From: 192.168.137.104:1234, To: 255.255.255.255:1234, Length: 12, Data: Anyone here?

上記がServer側のシリアルコンソールの抜粋です。192.168.137.104がServerで、192.168.137.167がClient側です。

ServerからServer、ClientからServerへのユニキャストと、ServerとClientからのブロードキャストが確認できます。

AsyncUDPMulticastServer

#include "WiFi.h"
#include "AsyncUDP.h"
AsyncUDP udp;
void setup()
{
    Serial.begin(115200);
    WiFi.mode(WIFI_STA);
    WiFi.begin();
    if (WiFi.waitForConnectResult() != WL_CONNECTED) {
        Serial.println("WiFi Failed");
        while(1) {
            delay(1000);
        }
    }
    if(udp.listenMulticast(IPAddress(239,1,2,3), 1234)) {
        Serial.print("UDP Listening on IP: ");
        Serial.println(WiFi.localIP());
        udp.onPacket([](AsyncUDPPacket packet) {
            Serial.print("UDP Packet Type: ");
            Serial.print(packet.isBroadcast()?"Broadcast":packet.isMulticast()?"Multicast":"Unicast");
            Serial.print(", From: ");
            Serial.print(packet.remoteIP());
            Serial.print(":");
            Serial.print(packet.remotePort());
            Serial.print(", To: ");
            Serial.print(packet.localIP());
            Serial.print(":");
            Serial.print(packet.localPort());
            Serial.print(", Length: ");
            Serial.print(packet.length());
            Serial.print(", Data: ");
            Serial.write(packet.data(), packet.length());
            Serial.println();
            //reply to the client
            packet.printf("Got %u bytes of data", packet.length());
        });
        //Send multicast
        udp.print("Hello!");
    }
}
void loop()
{
    delay(1000);
    //Send multicast
    udp.print("Anyone here?");
}

こちらもServerと同じくSSIDとKEYの指定を消しています。

    if(udp.listenMulticast(IPAddress(239,1,2,3), 1234)) {

上記で239.1.2.3というマルチキャストIPアドレスで、待ち受けをしています。マルチキャストで利用できるIPアドレスは複数あるので、同じIPを複数の端末で待ち受けすることで、特定の端末グループにだけ一斉送信することが可能です。

    //Send multicast
    udp.print("Anyone here?");

送信は上記でしています。このサンプルはudp.listenMulticast()でマルチキャストしたudpに送信していますので、自分もマルチキャストを受信しています。

Clientのようにudp.connect(IPAddress(239,1,2,3), 1234)と、マルチキャスト宛に送信すると、自分は受信しなくて、複数の端末に一斉送信するような使い方ができると思います。

まとめ

マルチキャストはWebカメラの画像を特定マルチキャストIPアドレス宛に送信しておき、受信したい端末がそのマルチキャストIPアドレスを受信すれば、何台が受信しても送信側の負荷は上がらない場合などに利用されます。

通常はあまり使わないので、基本はユニキャストですが、最初のマッチングだけブロードキャストを少し使うなどの方法がが好ましいと思います。

リアルタイム性の高いデータを高速通信する場合にはUDPが適していますが、それ以外の利用法であればTCP/IPを利用したほうが、通信が安定しています。

一番かんたんなのがBluetoothSerialのSPPですが、1台のパソコンから複数台のESP32にSPP接続などをすると、ラグが大きくなるので注意してください。

ESP-NOWはWi-Fiアクセスポイントが必要ないのでお手軽ですが、UDPと同じく安定性が保証されていないので気をつけてください。

BLEは鬼門なので、極力使わないほうが幸せになれます。

コメント

  1. yamori813 より:

    試してないんですが、ESP32はPHY(LAN8720)をつなぐとEthernetが使えるようです。

    • たなかまさゆき より:

      情報ありがとうございます!
      何個か有線LAN用の部品買ってあるんですが試せてません、、、
      どこかで実験してみたいと思います!

  2. yuika より:

    esp-atファームウェアでマルチキャストを試しましたが、ユニキャストしか受信しないようです。

    • たなかまさゆき より:

      んー、なんでだろう?

      udp.listenMulticast(IPAddress(239,1,2,3), 1234)

      上記で待ち受けしているすべての端末に

      udp.connect(IPAddress(239,1,2,3), 1234)

      udp.listenMulticast(IPAddress(239,1,2,3), 1234)

      で接続したudp.print()のパケットが届くはずなんですが、、、
      利用しているIPアドレスって224.0.0.0/4のマルチキャストの範囲にはいっていますよね?

  3. […] ESP32のAsyncUDPを調べる https://lang-ship.com/blog/work/esp32-asyncudp/ ●探しあてたサンプルコード ESP32でのUDP記事は、スマホ、PCとマイコン接続が多くESP32同士でもPythonを使っていたりして、ArduinoIDEでESP32同士接続のサンプルコード以外と無かったです。WiFiルーターを介して2個のESp32で 接続する方法例が多くてルーター無しで、ESP32同士接続する事例は非常に少なかったです。 その中でようやく探しあてたのが ceramie様のブログに感謝です。ESP32同士によるwifiUDP通信 ここでは、ESP32同士で2個のESP32でUDP通信テストプログラムあります。 サーバーとクライアントと呼んでいて、UDPでは、クライアントが送信して 受信するのがサーバーというのが普通だそうです。 […]

  4. chrmlinux03 より:

    いつもお世話になっております
    明けましておめでとうございます
    https://qiita.com/chrmlinux03/items/e944b58189d92a654a74

    Wire(i2c)をWiFiプロトコルに乗せるのに成功いたしましたっ
    よろしくご賞味くださいませ