M5Stackでタイと浜松を繋げてみた

# IoTの力でバンコクと浜松を短絡！

「浜松に滞在して制作活動をする予定だったタイのアーティスト『ヘンリー・タン』がコロナで来日できなくなってしまったので、IoTの力を使って遠隔で浜松に滞在してもらおう」という企画に参加しました。

[遠隔アーティストインレジデンス　浜松とバンコクを短絡！ | | 鴨江アートセンター](https://kamoeartcenter.org/events/henrytan/)

この企画ではM5StackやM5StickC等を沢山使ってタイにいるアーティストとつないでインタラクションを実現したので、M5Stackアドベントカレンダーということで記事にすることにしました。

正直Zoomなどのビデオ会議ツールを使えば簡単に双方向での対話はできるのですが、あくまでそれはやりたくないということで、いろいろなセンサーやアクチュエーターを浜松とバンコクの双方に設置し、インターネットを介した物理的データのやりとりを通じて浜松を「感じる」ということにこだわりました。

# 構想

FabLab浜松のメンバーに「MQTTを使ってタイと日本を繋ぐおもしろい企画があるけど参加しない？」と言われて、興味があったので参加してみることにしました。
ミーティング（チャット）に参加してみると、タイのアーティスト「ヘンリー」の書いたこんなスケッチが飛び交っていました。

![118766346_692043641423363_1084982983013995138_n.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70022/1c24b129-7646-e02f-d53d-2275f2e307f2.jpeg)

……アーティストの考えることはよくわからないですね。

具体的な制作物はおいておいてとりあえずシステム全体を構想。

![ヘンリープロジェクト.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70022/dce60f15-63bc-ac96-b5c8-df370d26b209.png)

* デバイスはタイでも入手できるというM5Stackを採用
* 通信は双方向通信で簡単なコマンドをやりとりしたいのでMQTTを採用
* MQTTブローカーとして、[CloudMQTT](https://www.cloudmqtt.com/)というサービスを使用（初めて使いました）

# 基本的な通信のソースコード

M5StackシリーズでWifiを使いMQTTを使うサンプルコードはネット上にいろいろ転がっていますが、よくあるサンプルコードは以下のようになっていることが多いです。

* プログラム開始時にWifiの接続が完了しないとプログラムが動き始めない(while文で待っている)
* MQTTブローカーへの接続が切れると再接続に時間がかかりその間プログラムが停止する

会場にWifiデバイスがいっぱいあったりしたためかWifiが不安定で、よく調子が悪くなりました。そのため以下の改善を行いました。

* プログラム開始時にWifiに接続完了しなくても
* WifiやMQTTが切断しても再接続する
* MQTT再接続などの処理は別タスクで行う

この改善をしたのが下記のソースコードです。

```c++

#include <M5Atom.h>
#include <WiFi.h>
#include <PubSubClient.h>
#include <string>
#include <freertos/FreeRTOS.h>
#include <freertos/timers.h>

// WiFi settings
const char ssid[] = "";         //  #### Your Wifi ID
const char password[] = "";     //  #### Your Wifi PW
WiFiClient wifiClient;

// MQTT settings
const char* mqttBrokerAddr = "";
const char* mqttUserName = "";
const char* mqttPassword = "";
const int mqttPort = 1883;
const char* mqttClientID = "";   // unique id that anything you like
PubSubClient mqttClient(wifiClient);

void setup() {
  M5.begin(true, false, true);
  WiFi.begin(ssid, password);
  Serial.begin(115200);
  Serial.println("setup done!!");
  //  MQTT
  mqttClient.setServer(mqttBrokerAddr, mqttPort);
  mqttClient.setCallback(mqttCallback);
  // 別タスクの起動
  xTaskCreatePinnedToCore(task0, "Task0", 4096, NULL, 1, NULL, 0);
}

// MQTTの再接続に時間がかかるみたいなので、別タスクで動かす
void task0(void* param)
{
  while (true) {
    if (checkWifi()) {
      if (checkMQTT()) {
        M5.dis.drawpix(0, 0x0000ff);  // WIFI,MQTT接続は青
        mqttClient.loop();
      } else {
        M5.dis.drawpix(0, 0xffff00);  // MQTT未接続は黄色
        vTaskDelay(1000); // 再接続まで間隔を開ける
      }
    } else {
      M5.dis.drawpix(0, 0x00ff00);  // Wifi切断は赤（色はなぜかBRG順）
    }
    vTaskDelay(1); // タスク内の無限ループには必ず入れる
  }
}

void loop() {
  M5.update();
  // ###ここでデバイスを動かしたりする###
}

// MQTTでトピックを受信したときに呼ばれる
void mqttCallback(char* topic, byte * payload, unsigned int length)
{
  String yd = topic;
  payload[length] = '\0';
  if (yd.equals("hoge")) {
    const String msg = (char*)payload;
    float value = atof(msg.c_str());
    // ###ここでデバイスを動かしたりする###
  }
}

// Wifiの状態をチェックし切断していたら再接続を試みる
boolean checkWifi() {
  static int lastWiFiStatus = WL_IDLE_STATUS;
  int wifiStatus = WiFi.status();
  if (lastWiFiStatus != WL_CONNECTED && wifiStatus == WL_CONNECTED) {
    Serial.println("WiFi connected.");
    Serial.println("IP address: ");
    Serial.println(WiFi.localIP().toString().c_str());
  } else if (lastWiFiStatus != wifiStatus && wifiStatus != WL_CONNECTED) {
    Serial.println("WiFi disconnected.");
    Serial.println(wifiStatus);
    if (wifiStatus == WL_CONNECT_FAILED) {
      WiFi.begin(ssid, password);
    }
  }
  lastWiFiStatus = wifiStatus;
  return (wifiStatus == WL_CONNECTED);
}

// MQTTの状態をチェックし切断していたら再接続を試みる
boolean checkMQTT() {
  unsigned long t = millis();
  boolean conn = mqttClient.connected();
  if (!conn) {
    Serial.println("MQTT Disconnect. Connecting...");
    conn = mqttClient.connect(mqttClientID, mqttUserName, mqttPassword);
    if (conn) {
      Serial.println("MQTT Connect OK.");
      mqttClient.subscribe("lantern/#");
    } else {
      Serial.println("MQTT Connect failed, rc=");
      // http://pubsubclient.knolleary.net/api.html#state に state 一覧が書いてある
      Serial.println(mqttClient.state());
    }
  }
  return conn;
}

```

M5Stackで初めてマルチタスクを使いましたがうまく動いてくれています。
このコードはM5Atom Liteの例ですが、WifiやMQTTの接続状況がわかるようにLEDで表示しています。こういうのが地味に大事です。

## MQTTを多数のデバイスで使う際のポイント

### クライアントIDは被らせない

複数のデバイスでクライアントIDが重複していて、接続が不安定になることがありました。IDなのでかぶってはいけませんね。上記ソースコードだと`mqttClientID`です。
タイのデバイスとIDがかぶったりしていて、かなりハマりました。

### デバイス名を先頭につけよう

MQTTではトピックの購読（Subscribe）でワイルドカードを使えます。`M5StickC/#`のように書くと、`M5StickC/`以下の全てのトピックを購読できます。これを利用して、デバイス名を先頭に書くことでどのデバイスが送信したかわかりやすくなります。

# 作品

基本的な通信を確認するのと並行して、具体的な作品を制作しました。
ここからは各作品を簡単に紹介します。


## 会場全体

会場のセッティングはこんな風になりました。

![IMG_0617_s.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70022/259e9b49-154f-5dfa-a093-fa332a9757e3.jpeg)

## 浜松の音キーボード

浜松の会場に設置したキーボードを弾くと、事前に採取しておいた浜松の音がタイで再生されます。

![PXL_20201107_052716423_2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70022/2ce83e87-69ce-1493-125f-fc78655ee9a1.jpeg)

**日本側:**

* キーボードで弾いたキー情報をMIDIコネクタを通じてM5Stack Basicに送信
* M5Stackからキー情報をブローカーにPublish          

**タイ側：**

* キー情報をM5Stack BasicでSubscribe
* キーに応じてSDカードに保存しておいた浜松の音（MP3）をM5Stackのスピーカーで再生

## ヘンリー脳波マッサージチェア

タイにいるヘンリーの脳波のパターンに応じて座面が動く椅子です。ヘンリーのシルエットの形をしています。

![PXL_20201101_064658533_s.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70022/fc9d3cbc-5795-6c3f-f715-374d81722497.jpeg)

**タイ側：**

* MUSE2というデバイスを使ってヘンリーの脳波を計測
* MAXというビジュアルプログラミングツールで脳波データを受信してMQTTでブローカーにPublish
  * 実際には脳波を5パターンに分類してPublishしています

MUSE2やMAXについては詳しくなく、タイ側でやっていることはあまり把握していないので正確ではないかもしれません。
MUSE2についてはこちら https://choosemuse.com/muse-2/
MAXについてはこちら https://cycling74.com/products/max

**日本側：**

* M5StickCで脳波のパターンをSubscribe
* パターンに応じてリニアアクチュエーター（DCモーター）で椅子の座面を波打たせる

## ヘンリー脳波マッサージクッション

タイにいるヘンリーの脳波のパターンに応じて人形が振動します。
これ搬入する姿を見られたら通報されかねないですね…

![PXL_20201101_064710716_s.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70022/9ef7903f-c62f-6fc5-4574-e3b3742107d6.jpeg)

中にはこんなマッサージ機が入っています。

![119061391_1054520798302109_7798766930021976456_n_sa.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70022/38790567-f7d8-e0a8-d90b-d67578993de4.jpeg)

**タイ側：**

* マッサージチェアと同じなので割愛

**日本側：**

* M5StickCで脳波のパターンをSubscribe
* パターンに応じて振動のパターンを変える

## ヘンリー心拍数ゾートロープロボット

ヘンリーの心拍数に応じて回転数が変わるゾートロープロボットです。
足回りはRoombaを使っており自由に移動ができます。掃除もできます。

![PXL_20201115_074607093_ts.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70022/a40e0810-3629-a746-204b-d23ac9fe58ea.jpeg)

**タイ側：**

* [Heart Rate Unit](https://m5stack.com/collections/m5-unit/products/mini-heart-unit) でヘンリーの心拍数を取得
  * M5StickCで心拍数をMQTTブローカーにPublish
* MUSE2のジャイロセンサーの値をRoombaの走る方向に変換し、MQTTブローカーにPublish
* Roombaのバンパーの衝突情報をM5StackでSubscribe、衝突したらビープ音が鳴る

**日本側：**

* M5Atomでヘンリーの心拍数をSubscribe
  * [ATOMICステッピングモータードライバ](https://m5stack.com/collections/m5-atom/products/atom-stepmotor-kit) でステッピングモーターを駆動し、ゾートロープを回転
* M5StickCで走る方向をSubscribe
  * Roombaをコントロール
    * Roombaはシリアルポートがついており、シリアル通信の簡単なコマンドでコントロールできる。「Roomba Open Interface」で調べると情報が手に入る。
  * 左右のタイヤの速度をコントロール
  * 前方のバンパーが当たったらMQTTブローカーにPublish

## ムエタイOtto

ヘンリーの脳波で動くOttoロボットです。[Otto](https://www.ottodiy.com/)とはオープンソースの二足歩行ロボットで、サーボモーター4つで可愛く動きます。

![IMG_0615.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70022/62afab23-1979-dca5-e1f2-67698fb935e0.jpeg)

**タイ側：**

* マッサージチェアと同じなので割愛

**日本側：**

* M5Atom Liteで脳波のパターンをSubscribe
* パターンに応じてムエタイのような踊りをします
* Otto自体はArduino Nanoで動作しており、M5Atom Liteとはシリアルで通信しています。

# Node-REDダッシュボード

デバッグ用を兼ねてNode-REDでダッシュボードを作成しました。
MQTTノードを使ってトピックをPublishしたりSubscribeしたりします。
動作確認がとてもしやすくなってよかったです。

![123092190_812712022603680_9047198969097869156_n.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70022/7d06f9c7-24d3-91f0-1591-1debb1aeaec0.png)

# まとめ

* M5Stack社製品を多数使いました。数えてみると、タイ／日本あわせてこれだけありました。
  * M5Stack Basic ×2
  * M5StickC ×3
  * ATOM Lite ×6
  * ATOMIC ステッピングモータードライバキット ×1
  * Heart Rate Unit ×1
* M5Stack社製品はIoT作品制作に使いやすく、タイでも入手できるので助かりました。
* MQTTは双方向通信を簡単に実現でき、割と遅延も少ない。「短絡」できたと言ってもいいかと思います。
* 2週間の展示期間でだいたい問題なく動いていてくれた。
* アーティストの考えていることはやっぱりわからん。