SoftAPモードのWiFi, UDP双方向通信M5Stackスケッチ(プログラミング)例
では、Arduino IDE でスケッチ(プログラミング)してみます。
まず、M5Stack内のESP32自信がアクセスポイントおよびルーターになるSoftAPモードの場合で説明します。
注目してほしいのは、四角形を動かす位置データをM5StackからUDPで送信していて、同時にもう一方のM5StackからUDPで送られてくるデータを受信していることです。
アクセスポイント側のM5Stackのスケッチ例
アクセスポイント側のIPアドレスはデフォルトで192.168.4.1 となります。
そして、それに接続したM5Stackは192.168.4.2 になると思います。
【ソースコード】 (※無保証 ※PCの場合、ダブルクリックすればコード全体を選択できます)
#include <M5Stack.h> //ver 0.2.9
#include <WiFi.h>
#include <WiFiUdp.h>
const char* ssid = "aaaaaaaaa"; //9文字以上。SoftAPモードM5StackのSSIDに書き換えてください
const char* password = "bbbbbbbbb"; //9文字以上。SoftAPモードM5Stackのパスワードに書き換えてください
const char * to_udp_address = "192.168.4.2"; //送信先のIPアドレス
const int to_udp_port = 55556; //送信相手のポート番号
const int my_server_udp_port = 55555; //開放する自ポート
WiFiUDP udp;
TaskHandle_t task_handl; //マルチタスクハンドル定義
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);
setupWiFiUDPserver();
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 setupWiFiUDPserver(){
Serial.println("Connecting to WiFi network: " + String(ssid));
WiFi.disconnect(true, true); //WiFi config設定リセット
WiFi.softAP(ssid, password);
IPAddress myIP = WiFi.softAPIP();
Serial.println("WiFi connected!");
Serial.print("My IP address: ");
Serial.println(myIP);
udp.begin(myIP, my_server_udp_port);
delay(1000);
}
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行目:
M5Stack純正ライブラリをインクルードします。
●5-6行目:
自信がアクセスポイントおよびルーターになるSoftAPモードのSSIDとパスワードを指定します。
9文字以上にしてください。
●8行目:
このIPアドレスは、送信する相手先のIPアドレスです。
間違えないようにしてください。
●9-10行目:
9行目は相手の送信先の開いているUDPポートを指定します。
10行目は自分側で開くポート番号です。
●42行目:
これは、123-133行目の関数で、M5StackのWiFiを起動し、アクセスポイントにするSoftAPの動作を開始します。
そして、131行にあるように、udp.begin関数で、自ポート55555を開放してデータ待ち受け状態にします。
●43行目:
M5Stackに搭載されているESP32はデュアルコアです。
LCDディスプレイはWiFi, UDPの送受信とは別のCPU core で動かした方が遅延が少なくて済みます。
よって、Arduino core ESP32の中のFreeRTOS関数で、マルチコアおよびマルチタスクで動作させます。
setup関数と、メインloop関数はcore 1のタスクです。
よって、LCDディスプレイ表示はcore 0のタスクで実行させます。
ESP32のマルチタスクについては以下の記事を参照してください。
Arduino – ESP32 のマルチタスク ( Dual Core ) を試す
●44行目:
これは重要です。
43行目で別タスクでLCD表示させるので、このdelayが無いとすぐにloop関数に入ってしまい、M5Stackライブラリを初期化するM5.begin関数が実行される前にUDPデータを受信してしまって、ハングアップして強制リセットしてしまうという状態になってしまいます。
ですから、別タスクの54行目のM5.beginが実行完了するまで、充分なdelay時間を設けます。
●47-51行:
core 1 タスクのメインループです。
ここで、WiFi, UDPからのデータを受信します。
そして、UDPデータを相手先へ送信もしています。
●53-75行:
これは、core 0 でメインループとは別タスクです。
M5.begin関数はcore 0で動かした方が良いので、ここで実行します。
UDP受信して、isDisp_ok = true になったら、LCDディスプレイに四角形を表示させ、動くアニメーションにしています。
ちらつきを抑えるために、四角形表示が動いた部分のみを表示したり消したりしています。
また、M5Stackのボタン操作も制御しています。
●77-89行:
UDPデータを受信する関数です。
自M5Stackの55555ポートを開放しているので、届いたデータをudp.parsePacket関数で受信して、ライブラリ内の受信バッファに一時保存します。
そして、そのサイズが0以上ならば、受信データバッファからudp.read関数で1byte読み込みます。
ここで注目!!
udp.read関数で読み込んだデータ分だけ、受信データバッファのポインタが1byte分インクリメント(自動的にポインタアドレスが進む)されます。
つまり、次にudp.readする時には、受信データの2byte目から読み込みます。
その次にudp.readすると、3byte目のデータを読み込むわけです。
これを、
udp.read(buf, size);
という形式にすると、そのサイズ分だけ読込み位置が進みます。
これは多量のデータを受信する分にはとても便利ですが、ソースコードをパッと見ただけでは意味不明で勘違いが起きやすいところなので、注意してください。
●91-101行:
図形の座標位置や方向、色データをUDPで順次送信する関数です。
103-121行で座標データを計算して、計算終了したら、
isSet_send_data = true;
となり、udp.beginPacket関数で、UDP送信バッファ領域を1460byte確保します。
このbeginPacket関数は相手の送信先のIPアドレスとUDPポートを指定することに注意してください。
送信バッファにはwrite関数で順番に貯め込みます。
そして、udp.endPacket関数で実際にデータを一気に送信します。
write関数で送信しているわけではないことが要注意点です。
そして、貯め込むバッファは1460byte以上になると捨てられますので注意です。
●103-121行:
これは、図形位置を自動で0~255まで1ずつ増加して、255になったら1ずつ減らしていく関数です。
移動方向変数の値も順次切り替えています。
●123-133行:
M5StackをアクセスポイントおよびルーターにするSoftAPモードで起動し、131行のudp.begin関数で自デバイスをUDPサーバーとしてポートを開放して待ち受け状態にします。
●135-156行:
M5Stack純正ライブラリのボタン操作関数です。
左ボタン(Botton A) — 図形移動方向切り替え(左右)
中央ボタン(Botton B) — 図形色切り替え(7色)
右ボタン(Botton C) — UDP送信間隔調整
右ボタン瞬時押し — スピードアップ
右ボタン長押し — スピード初期状態リセット
クライアント(STAモード)側のM5Stackスケッチ例
対して、STAモード側のM5Stackは、SoftAPモード側のM5Stackにアクセスすると、自信のIPアドレスは192.168.4.2 に自動的に割り当てられます。
【ソースコード】 (※無保証 ※PCの場合、ダブルクリックすればコード全体を選択できます)
#include <M5Stack.h> //ver 0.2.9
#include <WiFi.h>
#include <WiFiUdp.h>
const char* ssid = "aaaaaaaaa"; //9文字以上。SoftAPモードM5StackのSSIDに書き換えてください
const char* password = "bbbbbbbbb"; //9文字以上。SoftAPモードM5Stackのパスワードに書き換えてください
const char * to_udp_address = "192.168.4.1"; //送信先のIPアドレス
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();
}
}
【解説】
基本的にSoftAPのスケッチとほぼ同じなので、重複しているところは解説を省略します。
WiFiはSTA(ステーション)モードで起動することと、宛先アドレスとポート番号が異なっています。
●5-6行目:
SoftAPのM5Stackで設定したSSIDとパスワードを設定します。
●8行目:
相手の送信先M5StackのIPアドレスを設定します。
●9-10行目:
9行目は相手の送信先の開かれているポート番号を設定します。
10行目は自M5Stackの開くポートを指定します。
先に紹介したSoftAPのスケッチのポート番号と比べて、それぞれ入れ替わっていることに注意してください。
●43-49行目:
ここは先に紹介したSoftAPのスケッチと異なることに注意してください。
128-156行でSoftAPアクセスポイントのM5Stackに接続を試みて、イベントハンドラが返ってきたら、udp.begin関数でポート55556を開くようになっています。
このイベントハンドラは別タスクか又は割り込みか何かで動いているようなので、44-46行のように、コネクション完了するまで待つようにしています。
つまり、最初にSoftAPモードのM5Stackを起動した後、このSTAモードのM5Stackを起動すると良いというわけです。
以上でその他は先に紹介したSoftAPモードのスケッチとほぼ同じです。
コンパイル書き込み実行
では、2台のM5Stackをそれぞれコンパイル書き込み実行させてみてください。
まず、M5Stack自信がアクセスポイントおよびルーターになるスケッチをコンパイル書き込みさせた後、間髪入れずに即シリアルモニターを115200bpsで起動してみてください。
下図の様に表示されると思います。
これで、M5Stack がアクセスポイントになり、ルートのIPアドレス
192.168.4.1
が表示されます。
次に、もう一方のM5StackをSTAモード用のスケッチをコンパイル書き込み実行すると下図の様に表示されます。
ここで、SoftAPのM5Stackに接続完了すれば、
192.168.4.2
と表示されると思います。
もし異なるアドレスならば、スケッチをそのアドレスに書き換えてください。
それぞれ、コネクションが完了したら、あとは最初に紹介した動画のようにボタンを操作させてみてください。
自分のM5Stackの図形が移動するのではなく、相手先の図形が移動または色が変更されればOKです。
これで注目して欲しいのは、相手のM5Stackの図形位置データを送信しながら、相手から送信されてくる位置データを受信して図形を動かしているというところですね。
では、次の節では外部のWiFiルーターを介す、STAモード同士のM5Stack通信のスケッチ例を紹介します。
コメント
「デバイス側の一つのポートには一方向しかデータは送れない」ですが、普通に間違いですね。
この記事の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 を参照しました。使用しているライブラリが違っていたらごめんなさい。
* 「ポート開放」ってファイアウォールの話と混ざってませんか……?
teruさん
先日別記事でご指摘いただいたのに、新たに記事をご覧いただいた上にご指摘いただき、本当にありがとうございます。
当方で検証する時間が取れなくて、お返事遅くなり申し訳ございません。
結論は、teruさんのおっしゃる通りでした。
シリアルモニターでremotePort関数出力をしてもイマイチ良く分からないので、Windows PC のソフトWiresharkでパケットを解析してみました。
udp.begin関数で、ポートを55555に設定した場合、受信ポートは55555ですが、送信ポート番号は下図の様になっていました。
これによると、送信ポート番号も受信ポートと同様に55555になっていることが判明しました。
おかげ様で、teruさんのご指摘が無かったら、Wiresharkで調べようという意欲が働かず、曖昧なまま終わっていたと思います。
とっても勉強になりました!!!
今、次にアップする記事の編集に追われているので、それが済み次第徐々にこの記事も修正していく予定です。
こんなド素人ブログを読んでいただき、そして適切なコメントをしていただき、感謝感謝感謝です。
ありがとうございました。
m(_ _)m
参考になります。
1対多の場合の方法をが知りたいのですが、やられていますか。
Craneさん
随分古い記事ですが、ご覧いただきありがとうございます。
1対多については、この記事で書いてあるようにブロードキャストアドレスを使えば良いと思います。
ただ、ブロードキャストアドレスを使うと、ルータの設定によって極端に速度が遅くなる場合があるようです。
現に私の場合は遅く過ぎて使い物にならなかった記憶があります。
ブロードキャストの場合、複数相手に同じデータを送信すると思うのですが、送信元が複数あり受信先は1つとして、送信されてきたデータを1箇所で集めることが出来たらと思った次第です。
Craneさん
なるほど、そういうことでしたか。
そういうのはUDPではやったことがないので残念ながら私にはわかりません。
ただ、その場合は動画ストリーミングの様な重いデータではなく、短いデータ送信ならば、UDPではなくTCPでM5Stackサーバーを作れば、複数端末から送信でも受信できるかと思います。要するにM5Stackでホームページを作るようなものです。
ただ、私は今は多忙でM5Stackを触ることもプログラミングすることもできない状態なので、あくまで想像でしかお答えできませんが…。
よく参考にさせていただいています。
すみませんお手数ですが、この「UDPによる双方向リアルタイム同時通信プログラム」でひとつ教えてください。
M5.begin() を setup()ではなく、別タスクで実行されている理由を教えていただけませんでしょうか。
よろしくお願いします。
RUNAさん
記事をご覧いただき、ありがとうございます。
この記事は2019年に書いた古いもので、私自身は今となってはM5Stackをまったく触っておりません。
そんなわけで、すっかり忘れてしまいました。
ですが、久々にソースコード見てみると、何となく思い出しました。
loop関数でUDPデータの送受信を行っていて、別タスクではLCD(液晶ディスプレイ)への描画をしています。
LCDの描画はかなりCPUを消費するので、同じタスクに置いてしまうとUDP送受信している間はLCD描画がストップしてしまいます。
すると、動画がカクカクした状態になって、スムースに動かなくなるためです。
よって、UDP送受信とLCD描画は別タスクにして、同時進行させています。
これはデュアルコアのESP32だから可能なのです。