M5StickVで音ゲーをプレイしてみよう

以前M5Stackで画面をタップするものを作り、

M5Stackでタブレットクリッカーを作る - Qiita
https://qiita.com/coppercele/items/a46bb2437f9c73ffa061

色認識してターゲットを追跡するものを作ったので、

RoverCとM5StickCとM5StickVでグリッパロボットを作る - Qiita
https://qiita.com/coppercele/items/2c4e6c84a7a96e348dc1

これなら音ゲーをプレイするものが作れるんじゃないか？と思ったので作ってみました

ミリシタはレーンが2個しかないモードがあるのでうってつけでした

**ミリシタでレッツリズム！**

アイドルマスター ミリオンライブ！ シアターデイズ | バンダイナムコエンターテインメント公式サイト
https://millionlive.idolmaster.jp/theaterdays/

ちなみにミリシタの仕様として
・ホールドは途中で離していい
・ホールドの最後はタップでもリリースでもどっちでもいい
という感じになっているのでピンクの玉をタップすればフリック以外は取れます

##ハードウェアの構築

**M5stack**
M5Stackでタブレットクリッカーを作る - Qiita
https://qiita.com/coppercele/items/a46bb2437f9c73ffa061

こちらで使ったものをそのまま使用します

**M5StickV**
音ゲーの画面をカメラに映すためレゴでやぐらを組んで設置します
M5StickVを固定している透明の部品は以前3Dプリンタで作ったホルダーを流用しました

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/298776/8bada876-d242-c2c6-466e-59175d6ab1a6.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/298776/96b84d71-f0f7-2fb6-cc71-06082b6cc4ee.png)


上手いことこういう感じに映るように調整します

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/298776/7aed130f-a034-1309-fc58-1f5247f06e42.png)
※白と緑の四角は画面上の座標を確認するため

M5StickVとM5StackをGroveケーブルで直結します
ちなみに**M5StackのGroveは21,22を使うのでリレータッチボードを21に接続してると使えなくなります(Groveが優先)
上側のGPIOは22,23,19,18は他の機能と兼用されてるので避けないといけないので左側のGPIO16に接続しました**


上手いこと調整できたら~~歌織さんの胸~~ノーツを叩くポイントにリレータッチボードを貼り付けてセッティングします

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/298776/ff6e006d-b7d8-0639-5091-61b5c2a9bbb1.png)

##ソフトウェアの構築

M5StickVとM5Stack間はUARTで通信します

M5SticV側ソース(送信のみ)

```python
from fpioa_manager import fm
from machine import UART
fm.register(35, fm.fpioa.UART2_TX, force=True)
fm.register(34, fm.fpioa.UART2_RX, force=True)
uart_Port = UART(UART.UART2, 115200,8,0,0, timeout=1000, read_buf_len= 4096)

data_packet = bytearray([0, 0])
uart_Port.write(data_packet)

```

M5Stack側(受信のみ)

```cpp
void setup() {
  Serial1.begin(115200, SERIAL_8N1, 21, 22);
}


void loop() {
    M5.update();
    if (Serial1.available()) {
        uint8_t rx_buffer[1];
        int rx_size = Serial1.readBytes(rx_buffer, 1);

        for (int i = 0; i < rx_size; i++) {
            Serial.printf("%d, ", rx_buffer[i]);
        }
        Serial.printf("\n");
    }
  delay(1);
}
```

##ノーツを認識する

こちらを参考にして閾値エディタでピンクを認識できるようにします

【キョロキョロV④】m5StickV サーボ2軸マシンでまずは色検出をしてみました - パスコンパスの日記
https://yoichi-41.hatenablog.com/entry/2019/09/15/204835

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/298776/7aed130f-a034-1309-fc58-1f5247f06e42.png)

また、色認識するには周囲の環境の影響が大きいです
昼光色、電球色、自然光や画面に表示してるもの明るさによってカメラの露出などが変わってしまいさっきまで認識できていたのに認識できなくなってしまうということがよく起きます
なのでなるべくカメラの影響が少なくなるように設定します

```python

sensor.set_pixformat(sensor.RGB565)
sensor.set_framesize(sensor.QVGA)
sensor.set_brightness(0) #カメラの明るさ
sensor.set_saturation(0) #カメラの飽和度?
sensor.set_contrast(0)   #カメラのコントラスト
sensor.run(1)

```

それぞれ-2~2の間で設定できるので自分の環境に合わせて調整しましょう

ちゃんと設定するとこんな感じで認識できるようになります
また、PerfectのPの赤に反応したりするので認識する部分を緑の部分だけに限定します

```python
red_threshold   = (88, 54, 21, 93, -6, 60)

img=sensor.snapshot()
blobs = img.find_blobs([red_threshold])
    if blobs:
        for blob in reversed(blobs):
            #認識したノーツを逆順に処理する
            if (blob[2] * blob[3] < 200):
                #幅*高さで面積を出し小さいものは除外
                continue
            if  160 < blob[6]  :
                #緑枠の下限より下の物は除外
                #blob[6]は認識領域の中心y座標
                continue

            if  blob[6]  < 180 and blob[5] < 90 or  230 < blob[5] :
                #緑領域の中か判定
                #blob[5]は認識領域の中心x座標
                #四角と十字を描画
                tmp=img.draw_rectangle(blob[0:4],color=(128,0,0))
                tmp=img.draw_cross(blob[5], blob[6],color=(128,0,0))
                c=img.get_pixel(blob[5], blob[6])
                if blob[5] < 160:
                    #左側ならM5Stackに0を送信
                    data_packet = bytearray([0])
                else:
                    #右側ならM5Stackに1を送信
                    data_packet = bytearray([1])
                uart_Port.write(data_packet)
                break #タイミングラインに近い一番後ろの要素を処理したら終了
```

音ゲーである以上ノーツを認識してからいい感じのタイミングでタップしないとミスになってしまいます

タイミングラインの近くで認識したら短く、遠くで認識されたら長くsleepを入れます

```python

                if blob[5] < 160:
                    #左側ならM5Stackに0を送信
                    data_packet = bytearray([0])
                else:
                    #右側ならM5Stackに1を送信
                    data_packet = bytearray([1])
                # 画面の1番上なら0.6秒、タイミングラインに近ければ0になるようにwaitを計算する
                wait =  (160 - blob[6]) / 160 * 0.6
                print("wait=" + str(wait))
                #待ってから送信
                time.sleep(wait)
                uart_Port.write(data_packet)
                break #タイミングラインに近い一番後ろの要素を処理したら終了

```


M5Stack側では受信した内容に合わせて画面をタップします

```cpp

void setup() {
  M5.begin();
  pinMode(26, OUTPUT);
  pinMode(16, OUTPUT);
  digitalWrite(26, LOW);
  digitalWrite(16, LOW);

}

void loop() {
    M5.update();
    if (Serial1.available()) {
        uint8_t rx_buffer[1];
        int rx_size = Serial1.readBytes(rx_buffer, 1);

        if (rx_buffer[0] == 0x00) {
          // 0なら左タップ
            Serial.printf("left\n");
            // 左
            digitalWrite(16, HIGH);
            delay(50);
            // リリース
            digitalWrite(16, LOW);
        }
        else {
          // 0以外なら右タップ
            Serial.printf("right\n");
            // 右
            digitalWrite(26, HIGH);
            delay(50);
            // リリース
            digitalWrite(26, LOW);
        }
    }
```

##音ゲーをプレイしてみよう

いい感じで構成してからミリシタのリハーサルで試してみます(通常プレイするとBANされるかも知れない)

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">認識率9割は行ったんじゃね？🤣🤣🤣<br>(フリックはそもそも無理なので除く)<a href="https://twitter.com/hashtag/M5StickV?src=hash&amp;ref_src=twsrc%5Etfw">#M5StickV</a> <a href="https://twitter.com/hashtag/M5Stack?src=hash&amp;ref_src=twsrc%5Etfw">#M5Stack</a> <a href="https://t.co/mtqY703U7p">pic.twitter.com/mtqY703U7p</a></p>&mdash; もけ@ムギ㌠ (@coppercele) <a href="https://twitter.com/coppercele/status/1263117657731026945?ref_src=twsrc%5Etfw">May 20, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

このツイートでは9割と言ってますがフリック以外フルコン取れるようになってます

ソース
M5SticV側
<details><summary>m5sticv</summary>
<div>

```python
import sensor
import image
import lcd
import time
from fpioa_manager import fm
from machine import UART
fm.register(35, fm.fpioa.UART2_TX, force=True)
fm.register(34, fm.fpioa.UART2_RX, force=True)

lcd.init()
lcd.rotation(2)
sensor.reset()
sensor.set_pixformat(sensor.RGB565)
sensor.set_framesize(sensor.QVGA)
sensor.set_brightness(0)
sensor.set_saturation(0)
sensor.set_contrast(0)
sensor.run(1)
red_threshold   =            (88, 54, 21, 93, -6, 60)
uart_Port = UART(UART.UART2, 115200,8,0,0, timeout=1000, read_buf_len= 4096)

while True:
    img=sensor.snapshot()
    img.draw_rectangle(140, 0, 40, 40, color=(255,255,255))
    img.draw_rectangle(100, 40, 120, 40, color=(255,255,255))
    img.draw_rectangle(90, 80, 140, 40, color=(255,255,255))
    img.draw_rectangle(90, 120, 140, 40, color=(255,255,255))
    img.draw_rectangle(90, 160, 140  , 40, color=(255,255,255))
    img.draw_rectangle(0, 0, 90  , 180, color=(0,255,0))
    img.draw_rectangle(230, 0, 90  , 180, color=(0,255,0))


    blobs = img.find_blobs([red_threshold])
    if blobs:
        for blob in reversed(blobs):
            if (blob[2] * blob[3] < 200):
                continue
            if  160 < blob[6]  :
                continue

            if  blob[6]  < 180 and blob[5] < 90 or  230 < blob[5] :


                tmp=img.draw_rectangle(blob[0:4],color=(128,0,0))
                tmp=img.draw_cross(blob[5], blob[6],color=(128,0,0))
                c=img.get_pixel(blob[5], blob[6])
                if blob[5] < 160:
                    data_packet = bytearray([0])
                else:
                    data_packet = bytearray([1])
                wait =  (160 - blob[6]) / 160 * 0.6
                print("wait=" + str(wait))
                time.sleep(wait)
                uart_Port.write(data_packet)
                break

    lcd.display(img)

```

</div>
</details>

M5Stack側
<details><summary>M5Stack</summary>
<div>

```cpp
#include <M5Stack.h>

// タッチの間隔
int milli = 500;
// スイッチ連打状態かのフラグ
bool state = false;

void drawScreen() {
  M5.Lcd.fillScreen(BLACK);
  M5.Lcd.setCursor(0,0);
  // 間隔を表示
  M5.Lcd.printf("%4dms",milli);
  
  M5.Lcd.setCursor(0,220);
  // 画面下部の表示
  M5.Lcd.printf("     -      %s      +", state ? "OFF" : "ON ");
}

void setup() {
 Serial.begin(115200);
  M5.begin();
  M5.Lcd.setRotation(1);
  M5.Lcd.fillScreen(BLACK);
  M5.Lcd.setTextSize(2);
  M5.Lcd.setBrightness(64);
  M5.Lcd.fillScreen(BLACK);
  pinMode(26, OUTPUT);
  pinMode(16, OUTPUT);
  digitalWrite(26, LOW);
  digitalWrite(16, LOW);
  drawScreen();
  Serial1.begin(115200, SERIAL_8N1, 21, 22);

}

void loop() {
    M5.update();
    if (Serial1.available()) {
        uint8_t rx_buffer[1];
        int rx_size = Serial1.readBytes(rx_buffer, 1);

        for (int i = 0; i < rx_size; i++) {
            Serial.printf("%d, ", rx_buffer[i]);
        }
        Serial.printf("\n");
        if (rx_buffer[0] == 0x00) {
          if(state) {
            Serial.printf("left\n");
            // 左
            digitalWrite(16, HIGH);
            delay(50);
            // リリース
            digitalWrite(16, LOW);
          }
        }
        else {
          if(state) {
            Serial.printf("right\n");
            // 右
            digitalWrite(26, HIGH);
            delay(50);
            // リリース
            digitalWrite(26, LOW);
          }
        }
    }
  if ( M5.BtnA.wasPressed() ) {
             digitalWrite(16, HIGH);
            delay(50);
            // リリース
            digitalWrite(16, LOW);
  }
  if ( M5.BtnB.wasPressed() ) {
    state = !state;
    drawScreen();
  }
  if ( M5.BtnC.wasPressed() ) {
            // 右
            digitalWrite(26, HIGH);
            delay(50);
            // リリース
            digitalWrite(26, LOW);

  }
  delay(1);
}

```

</div>
</details>