M5Stack同士でWiFi, UDPによる双方向リアルタイム同時通信する実験

M5Stackを2台使ってWiFi,,UDP双方向送受信通信 M5Stack

外部ルーターを介すSTAモード同士のWiFi, UDP双方向通信M5Stackスケッチ例

では、今度は外部ルーターを介す、STAモード同士のM5Stackで、UDP双方向通信するスケッチを紹介します。
基本構造は先に紹介したSoftAPモードのスケッチとほぼ同じです。

1台目のM5Stackスケッチ例

外部ルーターを介す場合は、ローカルIPアドレスがルーターによって自動的に割り当てられます。
相手先のIPアドレスが分からない場合は、とりあえず以下のスケッチを書き込んでコンパイル書き込みした後、シリアルモニターでそのデバイスのIPアドレスを確認すると良いと思います。

【ソースコード】 (※無保証 ※PCの場合、ダブルクリックすればコード全体を選択できます)

#include <M5Stack.h> //ver 0.2.9
#include <WiFi.h>
#include <WiFiUdp.h>

const char* ssid = "xxxxxxxxx"; //ご自分のルーターのSSIDに書き換えてください
const char* password = "xxxxxxxxx"; //ご自分のルーターのパスワードに書き換えてください

const char * to_udp_address = "192.168.0.13"; //送信先のアドレス
const int to_udp_port = 55556; //送信相手のポート番号
const int my_server_udp_port = 55555; //開放する自ポート

WiFiUDP udp;
TaskHandle_t task_handl; //マルチタスクハンドル定義

boolean connected = false;
uint8_t receive_position_data = 0; //受信図形の座標位置
uint8_t receive_direction = 0; //受信図形の動作方向変数
uint8_t receive_color_data = 0; //受信図形のカラー番号
uint8_t old_line_data = 0;
uint8_t old_color_data = 0;
boolean isDisp_ok = false; //ディスプレイ表示フラグ
boolean isSet_send_data = false;
boolean isSend_rect_move = false; //図形動作開始フラグ
int16_t send_position = 0; //送信図形の座標位置
uint8_t send_direction = 0; //送信図形の動作方向変数
uint8_t send_color_num = 0; //送信図形のカラー番号
uint32_t now_time = 0;
int16_t interval = 100; //UDPデータ送信間隔
uint8_t rect_width = 63; //図形の幅
uint8_t rect_height = 100; //図形の高さ
uint32_t color_data[7] = {TFT_WHITE,
                          TFT_RED,
                          TFT_GREEN,
                          TFT_BLUE,
                          TFT_YELLOW,
                          TFT_MAGENTA,
                          TFT_CYAN};

//********* core 1 task ************
void setup() {
  Serial.begin(115200);
  delay(1000);
  connectToWiFi();
  while(!connected){
    delay(1);
  }
  xTaskCreatePinnedToCore(&taskDisplay, "taskDisplay", 8192, NULL, 10, &task_handl, 0);
  delay(500); //これ重要。別タスクでM5.begin関数が起動するまで待つ。
}

void loop() {
  receiveUDP();
  if(isSend_rect_move) autoIncDec(interval);
  sendUDP();
}
//******** core 0 task *************
void taskDisplay(void *pvParameters){
  M5.begin();
  M5.Lcd.fillScreen(TFT_BLACK);
  M5.Lcd.fillRect(0, 50, rect_width, rect_height, TFT_WHITE);
  while(true){
    M5.update(); //Update M5Stack button state
    if(isDisp_ok){
      if(old_line_data != receive_position_data || old_color_data != receive_color_data){
        if(receive_direction){
          M5.Lcd.fillRect(old_line_data, 50, receive_position_data - old_line_data, rect_height, TFT_BLACK);
        }else{
          M5.Lcd.fillRect(receive_position_data + rect_width, 50, old_line_data + rect_width, rect_height, TFT_BLACK);
        }
        M5.Lcd.fillRect(receive_position_data, 50, rect_width, rect_height, color_data[receive_color_data]);
        old_line_data = receive_position_data;
        old_color_data = receive_color_data;
      }
      isDisp_ok = false;
    }
    button_action();
    delay(1);
  }
}
//************************************
void receiveUDP(){
  if(!isDisp_ok){
    int packetSize = udp.parsePacket();
    if(packetSize > 0){
      receive_position_data = udp.read();
      receive_direction = udp.read();
      receive_color_data = udp.read();
      //Serial.println(receive_position_data);
      //Serial.printf("receive_position_data=%d, receive_color_data=%d\r\n", receive_position_data, receive_color_data);
      isDisp_ok = true;
    }
  }
}

void sendUDP(){
  if(isSet_send_data){
    udp.beginPacket(to_udp_address, to_udp_port);
    udp.write((uint8_t)send_position);
    udp.write(send_direction);
    udp.write(send_color_num);
    //Serial.printf("send_position=%d, send_color_num=%d\r\n", send_position, send_color_num);
    udp.endPacket();
    isSet_send_data = false;
  }
}

void autoIncDec(int16_t interval) {
  if(millis() - now_time > interval){
    if(send_direction){
      send_position++;
      if(send_position > 255) {
        send_position = 255;
        send_direction = 0;
      }
    }else{
      send_position--;
      if(send_position < 0) {
        send_position = 0;
        send_direction = 1;
      }
    }
    isSet_send_data = true;
    now_time = millis();
  }
}

void connectToWiFi(){
  Serial.println("Connecting to WiFi network: " + String(ssid));
  WiFi.disconnect(true, true);
  delay(1000);
  WiFi.onEvent(WiFiEvent);
  WiFi.begin(ssid, password);
  Serial.println("Waiting for WIFI connection...");
}

void WiFiEvent(WiFiEvent_t event){
  IPAddress myIP = WiFi.localIP();
  switch(event) {
    case SYSTEM_EVENT_STA_GOT_IP:
      Serial.println("WiFi connected!");
      Serial.print("My IP address: ");
      Serial.println(myIP);
      //udp.begin関数は自サーバーの待ち受けポート開放する関数である
      udp.begin(myIP, my_server_udp_port);
      delay(1000);
      connected = true;
      break;
    case SYSTEM_EVENT_STA_DISCONNECTED:
      Serial.println("WiFi lost connection");
      connected = false;
      break;
    default:
      break;
  }
}

void button_action(){
  if (M5.BtnA.wasReleased()) {
    if(send_direction) {
      send_direction = 0;
    }else{
      send_direction = 1;
    }
    isSend_rect_move = true;
  } else if (M5.BtnB.wasReleased()) {
    send_color_num++;
    if(send_color_num > 6) send_color_num = 0;
    isSet_send_data = true;
  } else if (M5.BtnC.wasReleased()) {
    interval = interval - 5;
    if(interval < 0) interval = 0;
    Serial.printf("interval=%d\r\n", interval);
  } else if (M5.BtnC.pressedFor(500)) {
    interval = 100;
    Serial.printf("interval=%d\r\n", interval);
    now_time = millis();
  }
}

【解説】

先に紹介したSoftAP用のスケッチとほぼ同じなので重複しているところは解説を割愛します。

●5-6行:
外部ルーターのSSIDとパスワードに書き換えてください。

●8行目:
相手先のローカルIPアドレスを指定します。
分らない場合はとりあえず送信先のM5Stackをコンパイル書き込み実行してシリアルモニターで確認してから、この行を修正してください。

●9-10行目:
9行目は送信相手の開かれたポート番号を指定します。
10行目は自デバイスの開くポート番号を指定します。

その他は先に紹介したSoftAPモード用のSTAスケッチと同じです。

2台目のM5Stackスケッチ例

もう一方のM5Stackスケッチは、1台目のスケッチの8-10行目を書き換えただけで、他は同じです。

【ソースコード】 (※無保証 ※PCの場合、ダブルクリックすればコード全体を選択できます)

#include <M5Stack.h> //ver 0.2.9
#include <WiFi.h>
#include <WiFiUdp.h>

const char* ssid = "xxxxxxxxx"; //ご自分のルーターのSSIDに書き換えてください
const char* password = "xxxxxxxxx"; //ご自分のルーターのパスワードに書き換えてください

const char * to_udp_address = "192.168.0.14"; //送信先のアドレス
const int to_udp_port = 55555; //送信相手のポート番号
const int my_server_udp_port = 55556; //開放する自ポート

WiFiUDP udp;
TaskHandle_t task_handl; //マルチタスクハンドル定義

boolean connected = false;
uint8_t receive_position_data = 0;
uint8_t receive_direction = 0;
uint8_t receive_color_data = 0;
uint8_t old_line_data = 0;
uint8_t old_color_data = 0;
boolean isDisp_ok = false; //ディスプレイ表示フラグ
boolean isSet_send_data = false;
boolean isSend_rect_move = false; //図形動作開始フラグ
int16_t send_position = 0; //送信図形の座標位置
uint8_t send_direction = 0; //送信図形の動作方向変数
uint8_t send_color_num = 0; //送信図形のカラー番号
uint32_t now_time = 0;
int16_t interval = 100; //UDPデータ送信間隔
uint8_t rect_width = 63; //図形の幅
uint8_t rect_height = 100; //図形の高さ
uint32_t color_data[7] = {TFT_WHITE,
                          TFT_RED,
                          TFT_GREEN,
                          TFT_BLUE,
                          TFT_YELLOW,
                          TFT_MAGENTA,
                          TFT_CYAN};

//********* core 1 task ***************
void setup(){
  Serial.begin(115200);
  delay(1000);
  connectToWiFi();
  while(!connected){
    delay(1);
  }
  xTaskCreatePinnedToCore(&taskDisplay, "taskDisplay", 8192, NULL, 10, &task_handl, 0);
  delay(500); //これ重要。別タスクでM5.begin関数が起動するまで待つ。
}

void loop(){
  receiveUDP();
  if(isSend_rect_move) autoIncDec(interval);
  sendUDP();
}
//******** core 0 task *************
void taskDisplay(void *pvParameters){
  M5.begin();
  M5.Lcd.fillScreen(TFT_BLACK);
  M5.Lcd.fillRect(0, 50, rect_width, rect_height, TFT_WHITE);
  while(true){
    M5.update(); //Update M5Stack button state
    if(isDisp_ok){
      if(old_line_data != receive_position_data || old_color_data != receive_color_data){
        if(receive_direction){
          M5.Lcd.fillRect(old_line_data, 50, receive_position_data - old_line_data, rect_height, TFT_BLACK);
        }else{
          M5.Lcd.fillRect(receive_position_data + rect_width, 50, old_line_data + rect_width, rect_height, TFT_BLACK);
        }
        M5.Lcd.fillRect(receive_position_data, 50, rect_width, rect_height, color_data[receive_color_data]);
        old_line_data = receive_position_data;
        old_color_data = receive_color_data;
      }
      isDisp_ok = false;
    }
    button_action();
    delay(1);
  }
}
//***********************************
void receiveUDP(){
  if(!isDisp_ok){
    if(connected){
      int packetSize = udp.parsePacket();
      if(packetSize > 0){
        receive_position_data = udp.read();
        receive_direction = udp.read();
        receive_color_data = udp.read();
        //Serial.printf("receive_position_data=%d, receive_direction=%d, receive_color_data=%d\r\n", receive_position_data, receive_direction, receive_color_data);
        isDisp_ok = true;
      }
    }
  }
}

void sendUDP(){
  if(isSet_send_data){
    udp.beginPacket(to_udp_address, to_udp_port);
    udp.write((uint8_t)send_position);
    udp.write(send_direction);
    udp.write(send_color_num);
    //Serial.printf("send_position=%d, send_color_num=%d\r\n", send_position, send_color_num);
    udp.endPacket();
    isSet_send_data = false;
  }
}

void autoIncDec(int16_t interval) {
  if(millis() - now_time > interval){
    if(send_direction){
      send_position++;
      if(send_position > 255) {
        send_position = 255;
        send_direction = 0;
      }
    }else{
      send_position--;
      if(send_position < 0) {
        send_position = 0;
        send_direction = 1;
      }
    }
    isSet_send_data = true;
    now_time = millis();
  }
}

void connectToWiFi(){
  Serial.println("Connecting to WiFi network: " + String(ssid));
  WiFi.disconnect(true, true);
  delay(1000);
  WiFi.onEvent(WiFiEvent);
  WiFi.begin(ssid, password);
  Serial.println("Waiting for WIFI connection...");
}

void WiFiEvent(WiFiEvent_t event){
  IPAddress myIP = WiFi.localIP();
  switch(event) {
    case SYSTEM_EVENT_STA_GOT_IP:
      Serial.println("WiFi connected!");
      Serial.print("My IP address: ");
      Serial.println(myIP);
      //udp.begin関数は自サーバーの待ち受けポート開放する関数である
      udp.begin(myIP, my_server_udp_port);
      delay(1000);
      connected = true;
      break;
    case SYSTEM_EVENT_STA_DISCONNECTED:
      Serial.println("WiFi lost connection");
      connected = false;
      break;
    default:
      break;
  }
}

void button_action(){
  if (M5.BtnA.wasReleased()) {
    if(send_direction) {
      send_direction = 0;
    }else{
      send_direction = 1;
    }
    isSend_rect_move = true;
  } else if (M5.BtnB.wasReleased()) {
    send_color_num++;
    if(send_color_num > 6) send_color_num = 0;
    isSet_send_data = true;
  } else if (M5.BtnC.wasReleased()) {
    interval = interval - 5;
    if(interval < 0) interval = 0;
    Serial.printf("interval=%d\r\n", interval);
  } else if (M5.BtnC.pressedFor(500)) {
    interval = 100;
    Serial.printf("interval=%d\r\n", interval);
    now_time = millis();
  }
}

【解説】

1台目と殆ど同じです。
8-10行目だけは書き換えてください。
IPアドレスが分からない場合は、1台目のスケッチをコンパイル書き込み実行して、シリアルモニターで確認してください。

コンパイル書き込み実行

では、2台のM5Stackをそれぞれコンパイル書き込み実行させてみてください。
そして間髪入れずにシリアルモニターを115200bpsで起動し、それぞれのローカルIPアドレスを確認してください。
下図の様に表示されます。

送信相手先のIPアドレスが自M5Stackのto_udp_addressと合っているか確認してください。
それで、ボタンを押してみて、最初に紹介した動画のように表示されればOKです。

以上、M5Stack同士のWiFi, UDP双方向通信の方法でした。

編集後記

どうでしたか?
上手く動きましたでしょうか?

以前はスマホとM5Stack間で双方向リアルタイム通信したり、Firebase等のクラウドサーバーを介して双方向通信したりしていましたが、ローカルWiFiでM5Stack同士で双方向リアルタイム通信をしたのは今回が初めてでした。
WebSocketみたいで、なかなか面白いですよね。
な~んだ!
UDPでも出来るじゃないか!!!
っていう感じですね。

これを応用すれば、カメラ画像等の重いデータをM5Stack間で送受信できます。
次回はそれを紹介したいと思います。

なかなかディープラーニング学習をはじめられないこの性・・・。

ではまた・・・。

コメント

  1. teru より:

    「デバイス側の一つのポートには一方向しかデータは送れない」ですが、普通に間違いですね。
    この記事のUDP通信サンプルの場合、 udp.begin(myIP, my_server_udp_port); でソケットにポートを割り当て、同じ udp インスタンスを使用してパケット受信とパケット送信をしているので、「1つのポートで送受信」を行っています。
    パケットには宛先IPアドレス・ポートだけではなく、送信元IPアドレス・ポートが含まれており、通常アプリケーションから取得可能です。
    WiFiUdpのソースコードを読んでみたところ、 udp.parsePacket() でデータを受信した後に、 udp.remoteIP()、 udp.remotePort() で取得できるようですので、確認してみてください。
    udp.beginPacket(to_udp_address, to_udp_port) を呼び出すと、 remoteIP、remotePort の値は上書きされてしまうようですので、出力するタイミングにはご注意ください。
    下記のようなコードで、別の WiFiUdp インスタンスを使用する場合と比べると、違いが分かりやすいと思います。

    WiFiUdp udp_send;
    udp_send.beginPacket(to_udp_address, to_udp_port);
    udp_send.write((uint8_t)send_position);
    udp_send.write(send_direction);
    udp_send.write(send_color_num);
    udp_send.endPacket();

    Webページを取得するのに、相手先IPアドレス・ポートを指定して自分のIPアドレス・ポートを指定しなくてよいのは、送信元IPアドレス・ポートは、OSが自動的に割り当てるためです。

    WiFiUdp がどこで送受信のIPアドレスとポートを割り当てているかというと…… bind 関数の呼び出しがそれになります。
    bind をせずにいきなり sendto すると、その時にOSが自動的にIPアドレスとポートを選択して、sendtoに指定したソケットに割り当てます。

    * 「OSが~」と書いていますが、これは普通のPCを念頭においていいます。ESP32の場合は事情が異なる気がします。
    * WiFiUdpの動作に付いては、ソースコード見て判断した内容になります。Arduinoは使用していないので実際に確認していない点、ご容赦ください。
    * WiFiUdpのソースコードは https://github.com/espressif/arduino-esp32/blob/master/libraries/WiFi/src/WiFiUdp.cpp を参照しました。使用しているライブラリが違っていたらごめんなさい。
    * 「ポート開放」ってファイアウォールの話と混ざってませんか……?

    • mgo-tec mgo-tec より:

      teruさん

      先日別記事でご指摘いただいたのに、新たに記事をご覧いただいた上にご指摘いただき、本当にありがとうございます。
      当方で検証する時間が取れなくて、お返事遅くなり申し訳ございません。

      結論は、teruさんのおっしゃる通りでした。
      シリアルモニターでremotePort関数出力をしてもイマイチ良く分からないので、Windows PC のソフトWiresharkでパケットを解析してみました。
      udp.begin関数で、ポートを55555に設定した場合、受信ポートは55555ですが、送信ポート番号は下図の様になっていました。

      Wireshark解析

      これによると、送信ポート番号も受信ポートと同様に55555になっていることが判明しました。
      おかげ様で、teruさんのご指摘が無かったら、Wiresharkで調べようという意欲が働かず、曖昧なまま終わっていたと思います。
      とっても勉強になりました!!!

      今、次にアップする記事の編集に追われているので、それが済み次第徐々にこの記事も修正していく予定です。
      こんなド素人ブログを読んでいただき、そして適切なコメントをしていただき、感謝感謝感謝です。
      ありがとうございました。
      m(_ _)m

  2. Crane より:

    参考になります。
    1対多の場合の方法をが知りたいのですが、やられていますか。

    • mgo-tec mgo-tec より:

      Craneさん

      随分古い記事ですが、ご覧いただきありがとうございます。
      1対多については、この記事で書いてあるようにブロードキャストアドレスを使えば良いと思います。
      ただ、ブロードキャストアドレスを使うと、ルータの設定によって極端に速度が遅くなる場合があるようです。
      現に私の場合は遅く過ぎて使い物にならなかった記憶があります。

      • Crane より:

        ブロードキャストの場合、複数相手に同じデータを送信すると思うのですが、送信元が複数あり受信先は1つとして、送信されてきたデータを1箇所で集めることが出来たらと思った次第です。

        • mgo-tec mgo-tec より:

          Craneさん

          なるほど、そういうことでしたか。
          そういうのはUDPではやったことがないので残念ながら私にはわかりません。
          ただ、その場合は動画ストリーミングの様な重いデータではなく、短いデータ送信ならば、UDPではなくTCPでM5Stackサーバーを作れば、複数端末から送信でも受信できるかと思います。要するにM5Stackでホームページを作るようなものです。
          ただ、私は今は多忙でM5Stackを触ることもプログラミングすることもできない状態なので、あくまで想像でしかお答えできませんが…。

          • RUNA より:

            よく参考にさせていただいています。
            すみませんお手数ですが、この「UDPによる双方向リアルタイム同時通信プログラム」でひとつ教えてください。

            M5.begin() を setup()ではなく、別タスクで実行されている理由を教えていただけませんでしょうか。

            よろしくお願いします。

          • mgo-tec mgo-tec より:

            RUNAさん

            記事をご覧いただき、ありがとうございます。

            この記事は2019年に書いた古いもので、私自身は今となってはM5Stackをまったく触っておりません。
            そんなわけで、すっかり忘れてしまいました。

            ですが、久々にソースコード見てみると、何となく思い出しました。

            loop関数でUDPデータの送受信を行っていて、別タスクではLCD(液晶ディスプレイ)への描画をしています。
            LCDの描画はかなりCPUを消費するので、同じタスクに置いてしまうとUDP送受信している間はLCD描画がストップしてしまいます。
            すると、動画がカクカクした状態になって、スムースに動かなくなるためです。
            よって、UDP送受信とLCD描画は別タスクにして、同時進行させています。
            これはデュアルコアのESP32だから可能なのです。

タイトルとURLをコピーしました