新しいものづくりがわかるメディア

RSS


それ、M5でつくれるよ

それ、M5でつくれるよ——激突! ソーシャルディスタンス鬼ごっこ

「M5Stack」の活用例を紹介する連載企画「それ、M5でつくれるよ」。
第2回は、Wi-Fi機能とOSC通信を利用したタッチなしの鬼ごっこを実現する方法を紹介します。

「大人の本気ダッシュ」が見たいんですわ

最近「本気のダッシュ」ってしました?
街を見回しても、全力でダッシュしてる人を見掛けることはほとんどありません。電車に乗り遅れそうになってちょっと小走りする人を見るくらい。膝を上げずに膝下だけ素早く動かして移動する、あの走り方。もっと激しく、動物的に思いっきり走ってる大人を私は見たいんですわ!
とはいえ、大人が本気でダッシュするには理由が必要で、電車を逃す程度では膝下しか動きません。何か理由があれば……。

鬼ごっこだ。

そう帰結した私は、いったん友人に「鬼ごっこしよう」と誘ってみたのですが、「いや、今タッチとかしたくないよね」と、新型コロナウイルス感染症拡大防止時代の真っ当な意見をいただきました。

そこで今回は、M5StackのWi-Fi通信機能を使ってタッチレスで鬼ごっこを行う方法を紹介し、街中で「本気のダッシュ」をする大人を量産して笑顔になろうと思います。

こんなふうに走る大人が生まれます。 こんなふうに走る大人が生まれます。

タッチレスにするために

M5Stackに搭載されているマイコン(マイクロコントローラ)には、Wi-Fi通信機能が搭載されています。
通信強度を示すRSSI(Received Signal Strength Indicator)という指標を使うことで、通信している2台のM5Stack間の距離が大まかに分かります。単位はdBm(デシベルミリワット)です。
つまり鬼ごっこのルールを、通信強度が一定以上に強くなった時にタッチしたと判定する、とすればタッチレスを実現できます!

今回はM5Stackを2台使うことになります。スタート時に鬼になる人(以下oni)と逃げる人(以下run)ですね。
タッチされたことやお互いの距離を相手側に伝えるのには、OSC通信を使います。
OSC通信はWi-Fiを経由してデータを送受信する仕組みで、数値や文字列を簡単に送受信できます。

oniをWi-Fiのアクセスポイントとして、runがWi-Fiに接続します。
タッチの判定は以下のように設計しました。

  1. runがoniに逐次RSSI値を送信
  2. RSSIが一定値以上になった時、oniがrunにタッチされた判定を送信
  3. oniとrunの役割交代

まとめると、今回の鬼ごっこシステムは以下の図のようになります。

photo

Wi-Fiの設定と表示

ソフトウェアは前回に引き続きVisual Studio CodeとPlatformIOを利用するので、環境構築ができていない場合は第1回を参照してください。

まずMicroSDカードを2枚用意して、画像と音声ファイルをoniとrun用にそれぞれ保存しM5Stackに挿入しておいてください。画像と音声ファイルはこちらからダウンロードしてね。

M5Stackの画面を見なくても自分の役割が分かるように、ゲームスタート時と役割交代時に音が鳴るようにしておきましょう。

次にPlatformIOでoni用のプロジェクトとrun用のプロジェクトをそれぞれ作成し、platformio.iniをどちらも次のようにします。

(platformio.ini)

[env:m5stack-core-esp32]
platform = espressif32
board = m5stack-core-esp32
framework = arduino
lib_deps =
    m5stack/M5Stack@^0.3.1
arduino-libraries/Ethernet@^2.0.0
    hideakitai/ArduinoOSC@^0.3.11
    earlephilhower/ESP8266Audio@^1.8.1
upload_speed = 115200

oniのmain.cppを以下のようにして書き込みましょう。

(oniのmain.cpp)

#include <Arduino.h>
#include <M5Stack.h>
#include <Ethernet.h>
#include <ArduinoOSC.h>
#include <ESP8266Audio.h>

bool ONI = true;
const int THRESH = -20; //タッチ判定をするRSSI値の閾値です。

//Wi-Fi用の設定を以下に書きます passだけ設定してください
const char *ssid = "m5_oni";         // SSID
const char *pass = "****"; // passwordは自由に設定してください
const int incomingPort = 7070;       // 受信ポート番号
const int outgoingPort = 8080;       // 送信ポート番号

const IPAddress ipServer(192, 168, 1, 1);   // server IPアドレス
const IPAddress ipGateway(192, 168, 1, 1);  // gateway IPアドレス
const IPAddress subnet(255, 255, 255, 0);   // サブネットマスク
const IPAddress ipClient(192, 168, 1, 255); // client IPアドレス

const char *host = "192.168.1.255";

#include "AudioFileSourceSD.h"
#include "AudioFileSourceID3.h"
#include "AudioGeneratorMP3.h"
#include "AudioOutputI2S.h"

AudioGeneratorMP3 *mp3;
AudioFileSourceSD *file;
AudioOutputI2S *out;
AudioFileSourceID3 *id3;

void playMP3(const char filename[]) //音を出すための関数です。
{
  file = new AudioFileSourceSD(filename);
  id3 = new AudioFileSourceID3(file);
  out = new AudioOutputI2S(0, 1);
  out->SetOutputModeMono(true);
  out->SetGain(1.0);
  mp3 = new AudioGeneratorMP3();
  mp3->begin(id3, out);
  while (mp3->isRunning())
  {
    if (!mp3->loop())
      mp3->stop();
  }
}

void rcv_rssi(const OscMessage &msg) //RSSIを受信した時に起動する関数です。
{
  int rssi = msg.arg<int>(0);

  M5.Lcd.setCursor(0, 195);
  M5.Lcd.printf("rssi: %d", rssi); //受信したRSSI値を画面に表示します

  if (rssi > THRESH) //RSSI値が閾値以上だった場合
  {
    if (ONI)
    {
      OscWiFi.send(host, outgoingPort, "/oni", 1); //runに通知して、
      OscWiFi.post();
      ONI = false;
      playMP3("/run.mp3"); //runに変化します。

      M5.Lcd.drawJpgFile(SD, "/run.JPG");
      M5.Lcd.setCursor(0, 20);
      M5.Lcd.printf("ssid: %s", ssid);
      M5.Lcd.setCursor(0, 65);
      M5.Lcd.printf("incomingPort: %d", incomingPort);
      M5.Lcd.setCursor(0, 105);
      M5.Lcd.printf("outgoingPort: %d", outgoingPort);
      M5.Lcd.setCursor(0, 155);
      M5.Lcd.printf("run!!!");
    }
    else
    {
      OscWiFi.send(host, outgoingPort, "/oni", 0);
      OscWiFi.post();
      ONI = true;
      playMP3("/oni.mp3");

      M5.Lcd.drawJpgFile(SD, "/oni.JPG");
      M5.Lcd.setCursor(0, 20);
      M5.Lcd.printf("ssid: %s", ssid);
      M5.Lcd.setCursor(0, 65);
      M5.Lcd.printf("incomingPort: %d", incomingPort);
      M5.Lcd.setCursor(0, 105);
      M5.Lcd.printf("outgoingPort: %d", outgoingPort);
      M5.Lcd.setCursor(0, 155);
      M5.Lcd.printf("Oni deeeeesu!!!");
    }
  }
}

void setup()
{
  M5.begin();
  M5.Power.begin();
  M5.Lcd.clearDisplay();
  M5.Lcd.drawJpgFile(SD, "/oni.JPG");
  M5.Lcd.setTextColor(GREEN, BLACK);
  M5.Lcd.setTextSize(2);

  WiFi.softAP(ssid, pass);
  delay(100);
  WiFi.softAPConfig(ipServer, ipGateway, subnet);
  M5.Lcd.setCursor(0, 20);
  M5.Lcd.printf("ssid: %s", ssid);
  M5.Lcd.setCursor(0, 65);
  M5.Lcd.printf("incomingPort: %d", incomingPort);
  M5.Lcd.setCursor(0, 105);
  M5.Lcd.printf("outgoingPort: %d", outgoingPort);
  M5.Lcd.setCursor(0, 155);
  M5.Lcd.printf("Oni deeeeesu!!!");
  delay(1000);
  playMP3("/oni.mp3");

  OscWiFi.subscribe(incomingPort, "/rssi", rcv_rssi); //この記述で、runからRSSI値が
送られてきた時にrcv_rssi関数が起動するようになります。 } void loop() { OscWiFi.parse(); //何もない場合はrunからの通信を待ち続けます。 }

runのmain.cppは以下の通りです。

(runのmain.cpp)

#include <Arduino.h>
#include <M5Stack.h>
#include <ArduinoOSC.h>
#include <ESP8266Audio.h>

// Wi-Fiの設定です passだけ設定してください
const char *ssid = "m5_oni";
const char *pwd = "****"; //oniで設定したものと同じものを使ってください
const IPAddress ip(192, 168, 1, 201);
const IPAddress gateway(192, 168, 1, 1);
const IPAddress subnet(255, 255, 255, 0);

// for ArduinoOSC
const char *host = "192.168.1.255"; //念の為同じネットワーク内の全員に送信します
const int incomingPort = 8080; // 受信ポート番号
const int outgoingPort = 7070; // 送信ポート番号

#include "AudioFileSourceSD.h"
#include "AudioFileSourceID3.h"
#include "AudioGeneratorMP3.h"
#include "AudioOutputI2S.h"

AudioGeneratorMP3 *mp3;
AudioFileSourceSD *file;
AudioOutputI2S *out;
AudioFileSourceID3 *id3;

void playMP3(const char filename[]) //音を出すための関数です
{
  file = new AudioFileSourceSD(filename);
  id3 = new AudioFileSourceID3(file);
  out = new AudioOutputI2S(0, 1);
  out->SetOutputModeMono(true);
  out->SetGain(1.0);
  mp3 = new AudioGeneratorMP3();
  mp3->begin(id3, out);
  while (mp3->isRunning())
  {
    if (!mp3->loop())
      mp3->stop();
  }
}

void rcv_oni(const OscMessage &msg) //タッチの通知が来たときに起動する関数です
{
  int oni = msg.arg<int>(0);
  if (oni == 1) //「あなたが鬼です!」という通知が来たら、
  {
    playMP3("/oni.mp3"); //鬼に変化します

    M5.Lcd.drawJpgFile(SD, "/oni.JPG");
    M5.Lcd.setCursor(0, 20);
    M5.Lcd.printf("ssid: %s", ssid);
    M5.Lcd.setCursor(0, 65);
    M5.Lcd.printf("incomingPort: %d", incomingPort);
    M5.Lcd.setCursor(0, 105);
    M5.Lcd.printf("outgoingPort: %d", outgoingPort);
    M5.Lcd.setCursor(0, 155);
    M5.Lcd.printf("Oni deeeeesu!!!");
  }
  else
  {
    playMP3("/run.mp3");

    M5.Lcd.drawJpgFile(SD, "/run.JPG");
    M5.Lcd.setCursor(0, 20);
    M5.Lcd.printf("ssid: %s", ssid);
    M5.Lcd.setCursor(0, 65);
    M5.Lcd.printf("incomingPort: %d", incomingPort);
    M5.Lcd.setCursor(0, 105);
    M5.Lcd.printf("outgoingPort: %d", outgoingPort);
    M5.Lcd.setCursor(0, 155);
    M5.Lcd.printf("run!!!");
  }
  delay(5000);
}

void setup()
{
  M5.begin();
  M5.Power.begin();
  M5.Lcd.clearDisplay();
  M5.Lcd.drawJpgFile(SD, "/run.JPG");
  M5.Lcd.setTextColor(GREEN, BLACK);
  M5.Lcd.setTextSize(2);

#ifdef ESP_PLATFORM
  WiFi.disconnect(true, true);
  delay(1000);
  WiFi.mode(WIFI_STA);
#endif
  WiFi.begin(ssid, pwd);
  WiFi.config(ip, gateway, subnet);
  while (WiFi.status() != WL_CONNECTED)
  {
    delay(500);
  }

  M5.Lcd.setCursor(0, 20);
  M5.Lcd.printf("ssid: %s", ssid);
  M5.Lcd.setCursor(0, 65);
  M5.Lcd.printf("incomingPort: %d", incomingPort);
  M5.Lcd.setCursor(0, 105);
  M5.Lcd.printf("outgoingPort: %d", outgoingPort);
  M5.Lcd.setCursor(0, 155);
  M5.Lcd.printf("run!!!");
  delay(1000);
  playMP3("/run.mp3");

  OscWiFi.subscribe(incomingPort, "/oni", rcv_oni); //この記述で、oniから通知が来た時に
rcv_oni関数が起動するようになります。 } void loop() { int rssi = 0; for (int i = 0; i < 100; i++) { rssi += WiFi.RSSI(); } rssi = rssi / -100; M5.Lcd.setCursor(0, 195); M5.Lcd.printf("rssi: %d", rssi); OscWiFi.send(host, outgoingPort, "/rssi", rssi); //何もない時はRSSI値を送り続けます OscWiFi.post(); delay(200); }

マスクにM5Stackを貼り付けて、

photo

電源を入れるとRSSIの値を確認できます。
以下の写真くらいの距離だと−70dBmくらい。

photo

ソーシャルディスタンスと呼ばれる2mくらいの距離だと、−30dBmくらいでした。
周囲の環境や機器によって数値は変わるので、タッチの閾値は適宜変更してください。

photo

「おれは人間をやめるぞ!」

ランニングはしているがダッシュはしていないライターの淺野(あさの)さんを、鬼ごっこのために公園に呼び出しました(既に上の画像で登場していますが)。

何も知らずにのこのこ来た淺野さんにM5Stackを片方渡し、私が人間をやめる旨を伝えました。

お互いに電源を入れると、私のM5が「おにぃぃぃ!」と叫びます。逆に淺野さんのM5は「逃げてえええ!」と叫びました。

「ん?なんのことだ?なにを言っているッ!」 という顔をしていた淺野さんも思わず走り出します。

photo

これが見たかったんだわ。大人が本気で走っています。
でも、鬼ごっこって往々にして隠れ鬼になってしまいますよね。でも「大人のダッシュ」を見るためのM5Stackはそんなの許しません。

茂みに身を隠す愚か者がここに。 茂みに身を隠す愚か者がここに。

M5ソーシャルディスタンス鬼ごっこではRSSIが画面に表示されているから、何となくの位置が分かってしまうのだッ!

青い影が忍び寄ります。 青い影が忍び寄ります。

「大人のダッシュ」を諦めた愚か者は鬼にしてしまえッ!

お前も鬼にならないか。 お前も鬼にならないか。

一定以上近づけば役割交代。
淺野さんのM5が「おにぃぃぃ!」と叫びます。先ほどまで正気だった淺野さんが鬼にッ!
逆に私のM5は「逃げてえええ!」と叫んでいます。

淺野さんの思う鬼っぽいポーズです。 淺野さんの思う鬼っぽいポーズです。

追い掛けられながら私は思いました。
私は「大人のダッシュ」を見たかっただけで、自分で走りたかったわけじゃないんだわ……。

でも楽しいからいいんです。 でも楽しいからいいんです。

今回は、M5StackのWi-Fi通信機能を使ってタッチなしで鬼ごっこをする方法を紹介しました。きっと皆さんダッシュしてないでしょうから、これを機会に思いっきりダッシュしてみてください。

OSC通信は今回のような1対1の使い方だけでなく、3つ以上のデバイス間でも通信できるので、100人で鬼ごっこしたい時にも使えます。またM5Stackは無線通信としてBluetoothも標準で使えるので、異なる方法でのM5鬼ごっこの開発にも、ぜひチャレンジしてみてください。

※このコーナーでは、皆さんの「それ、M5でつくれるよ」やアイデア、M5で解決してほしいことをお待ちしています。問い合わせフォームからドシドシご応募ください。

おすすめ記事

 

コメント

ニュース

編集部のおすすめ

連載・シリーズ

注目のキーワード

もっと見る