M5StackとUnityをUSBのシリアル通信で連携させてみた

# １．はじめに
　M5Stackを用いてモーションキャプチャを行っていきたいと思い，その前段階として加速度センサを用いてUnity内のオブジェクトを操作するちょっとしたプログラムを作りました．
![DSE.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3117483/b9871045-2f9f-caa5-b5cc-32fe4b055a9c.gif)


# ２．準備するもの
**必要機材**
* M5Stack
　本制作ではM5Stack Grayを用いましたが，加速度センサが内蔵されているM5Stack Core2でも使用できる．ただし，今回はM5Stack Grayに搭載されている加速度センサを用いたのでプログラムは多少変わる．
* PC
作成時はWindows10で動作

**必要ソフトウェア**
* Unity
　制作時はEditor version2021.3.4f1で動作
* 機体3Dモデル
BOOTHで無料ダウンロードできたものを使用『[先進無人航空機(UAV)3Dモデル](https://booth.pm/ja/items/2432627)』
* Unity空間背景
 Unityアセットストアにて無料ダウンロードできるものを使用『[AllSky Free - 10 Sky / Skybox Set](https://assetstore.unity.com/packages/2d/textures-materials/sky/allsky-free-10-sky-skybox-set-146014)』


# ３．M5Stackのプログラム
* 急峻に変化する加速度の値を20回測定毎に平均することで，滑らかにする
* ボタンAを押すことで，オフセットさせるように設定
```Arduino:MPU_Serial.ino
#include "BluetoothSerial.h"
BluetoothSerial SerialBT;
#include <M5Stack.h>
#include "utility/MPU9250.h"
#define MULTISAMPLE 20  // 複数回測定する回数
MPU9250 IMU;  // MPU9250のオブジェクトを定義
void readAccelMulti(float * accel, int multi) {  // 加速度を複数(multi)回測定して、平均する
    float ax, ay, az;

    ax = ay = az = 0.0;
    for (int i = 0; i < multi; i++) {
        while (! (IMU.readByte(MPU9250_ADDRESS, INT_STATUS) & 0x01)) ;  // 10.4m秒ほどかかる
//  readByte(A,B):指定アドレスAから読み込む。bool型で返答が１バイトの場合に使用。戻り値は①true:読込成功②false:読込失敗
// INT_STATUS:1バイトのレジスタで，最下位ビット0にデータ読み出し可否を示す
//&：ビット単位の論理積。ビット0の状態を調べ，読込成功時には論理積の結果，if条件式が1(つまりtrue)となる
        IMU.readAccelData(IMU.accelCount);  // 加速度データを取得する
        IMU.getAres();                      // スケールを取得する
        ax += (float)IMU.accelCount[0] * IMU.aRes;  // データを複数回足し込む
        ay += (float)IMU.accelCount[1] * IMU.aRes;
        az += (float)IMU.accelCount[2] * IMU.aRes;
    }
    accel[0] = ax / multi;  // 平均値を計算する
    accel[1] = ay / multi;
    accel[2] = az / multi;
}

float offset[3];  // オフセット  
#define MOVINGAVG 10  // 移動平均の長さ  
float movingavgx[MOVINGAVG], movingavgy[MOVINGAVG];  // 移動平均バッファ
int _indx = 0;  // 移動平均のインデックス

void setup() {
  delay(2000);
//  M5.begin();  // M5Stackを初期化する
  Serial.begin(19200);
  Wire.begin();  // I2C通信を初期
  IMU.initMPU9250();  // MPU9250を初期化
  readAccelMulti(offset, MULTISAMPLE);  // 加速度データを取得し、オフセット値にする
  for (int i = 0; i < MOVINGAVG; i++) {  // 移動平均バッファの初期値をセット 
      movingavgx[i] = offset[0];
      movingavgy[i] = offset[1];
  }
}

void loop() {
  float ax, ay;
  float ap, aq;
  float accel[3];
  M5.update();
  readAccelMulti(accel, MULTISAMPLE);  // 加速度データを取得  
  // 移動平均を計算  ---課題3の対策
  movingavgx[_indx] = accel[0];
  movingavgy[_indx] = accel[1];
  _indx = (_indx + 1) % MOVINGAVG;
  ax = ay = 0;
  for (int i = 0; i < MOVINGAVG; i++) {
      ax += movingavgx[i];
      ay += movingavgy[i];
  }
  ax /= MOVINGAVG;
  ay /= MOVINGAVG;
  if (M5.BtnA.wasPressed()) {  // ボタンAが押されていたら  
      offset[0] = ax;  // 現在の加速度データをオフセット値にセット
      offset[1] = ay;
  }
  drawSpot((int)((ax - offset[0]) * 1000), (int)((ay - offset[1]) * 1000));  
  ap = (ax - offset[0]) * 1000;
  aq = (ay - offset[1]) * 1000;
  Serial.println("Serial Try");
  Serial.println(ap);
  Serial.println(aq);
  delay(20);//ここでdelayさせるミリ秒によって，デバイス操作の反映スピードが変化
}

```

# ４．Unity側のSerial通信プログラム
* Unityを起動

```C#:SerialMove
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SerialMove : MonoBehaviour
{
    //https://qiita.com/yjiro0403/items/54e9518b5624c0030531
    //上記URLのSerialHandler.cのクラス
    public SerialHandler serialHandler;

  void Start()
    {
        //信号を受信したときに、そのメッセージの処理を行う
        serialHandler.OnDataReceived += OnDataReceived;
    }

    //受信した信号(message)に対する処理
    void OnDataReceived(string message)
    {
        var data = message.Split(
                new string[] { "\n" }, System.StringSplitOptions.None);
        try
        {
            Debug.Log(data[0]);//Unityのコンソールに受信データを表示
            float pose = float.Parse(data[0]);
            transform.position -= transform.forward * 0.002f * pose;
        }
        catch (System.Exception e)
        {
            Debug.LogWarning(e.Message);//エラーを表示
        }
    }
}
```
```C#:SerialHandler
using UnityEngine;
using System.Collections;
using System.IO.Ports;
using System.Threading;

public class SerialHandler : MonoBehaviour
{
    public delegate void SerialDataReceivedEventHandler(string message);
    public event SerialDataReceivedEventHandler OnDataReceived;

    public string portName = "COM3";//ポート名
    public int baudRate    = 19200;

    private SerialPort serialPort_;
    private Thread thread_;
    private bool isRunning_ = false;

    private string message_;
    private bool isNewMessageReceived_ = false;

    void Awake()
    {
        Open();
    }

    void Update()
    {
        if (isNewMessageReceived_) {
            OnDataReceived(message_);
        }
        isNewMessageReceived_ = false;
    }

    void OnDestroy()
    {
        Close();
    }

    private void Open()
    {
        serialPort_ = new SerialPort(portName, baudRate, Parity.None, 8, StopBits.One);
         //または
         //serialPort_ = new SerialPort(portName, baudRate);
        serialPort_.Open();

        isRunning_ = true;

        thread_ = new Thread(Read);
        thread_.Start();
    }

    private void Close()
    {
        isNewMessageReceived_ = false;
        isRunning_ = false;

        if (thread_ != null && thread_.IsAlive) {
            thread_.Join();
        }

        if (serialPort_ != null && serialPort_.IsOpen) {
            serialPort_.Close();
            serialPort_.Dispose();
        }
    }

    private void Read()
    {
        while (isRunning_ && serialPort_ != null && serialPort_.IsOpen) {
            try {
                message_ = serialPort_.ReadLine();
                isNewMessageReceived_ = true;
            } catch (System.Exception e) {
                Debug.LogWarning(e.Message);
            }
        }
    }

    public void Write(string message)
    {
        try {
            serialPort_.Write(message);
        } catch (System.Exception e) {
            Debug.LogWarning(e.Message);
        }
    }
}
```
論文落ち着き次第更新...