M5core2でプリズムウォッチを作ってみた

# 概要
作中に出てくるプリズムウォッチというデバイスを、応援上映で利用できるグッズとして組み上げた。
- SDカードによる画像読み込み　表示
- SDカードからの音声再生
- LEDのカラー制御
- 心拍センサーの搭載（未着手）
- ウォッチ型デバイスとして変形

以上の要素が記事内に含まれる。
なお、私はゴリゴリの文系なのでこの記事が簡潔な試行錯誤にはなり得ない点をご留意いただきたい。

## 動機
応援上映という映画鑑賞の形をご存知だろうか。映画館で映画に合わせて声を上げたり、歌を歌ったり、サイリウムなどの光り物を振ったり、コスプレをしたりする鑑賞の形だ。
そんな応援上映で人気を博したアニメ[King of Prism](https://kinpri.com/)の新作、DramaticPRISM.1（以下キンドラ）の[前売り券に付属していたグッズ](https://goods.moviewalker.jp/items/88370812?srsltid=AfmBOopRq1Tw8fJfzJ-KaZBTZldrWtN85V04Sjn5WAbfkSRTV-xKKjKP)がある。

<img width="600" src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3911544/3ca0a2f3-aec7-d2ca-4089-93cb5325ef4a.png>

”光るプリズムウォッチ”だ。作中に登場する、スマートウォッチ型デバイスを光るグッズに落とし込んだものなのだが、これが瞬く間に完売した。手に入らなかった。
同じく手に入らなかった人たちが百均でライトを入手し自作し始めていた。

ここで私は新卒研修を思い出す。

## きっかけ
弊社新卒研修では多くの分野に触れることとなっている。IoTもその一つだ。
ラズパイを使用し、Lチカ、音再生、遅延を使用した信号機再現や、温度センサーを使用した室内環境アラートや、ボタンからの信号でリアクションゲームを制作するなどの項目をこなしていた。

そして私はこう思う。---プリズムウォッチ、作れるのでは？　と。
どうせやるなら、単色に光るだけじゃないものを作ろう、と。

## 準備物
###### ハード
- [M5core2](https://www.switch-science.com/collections/m5stack/products/9349)
- [ウォッチ型キット](https://www.switch-science.com/collections/m5stack/products/9122)
- [拡張ハブ](https://www.switch-science.com/collections/m5stack/products/5696)
- [心拍センサー](https://www.switch-science.com/collections/m5stack/products/5695)
- [LEDテープ](https://www.switch-science.com/collections/m5stack/products/5208)
- スペーサー
- 4cmねじ
- ウォッチバンド
- 16GBのmicroSD
###### コーダー
- [Arduino IDE](https://www.arduino.cc/en/software)
    - Mac環境で制作している

## ウォッチ型デバイスへ
ウォッチ型キットを使用してウォッチバンドをつけられるようになった。
そして、LEDテープ、心拍センサー、拡張ハブとケーブルを収納するためにスペーサーを噛ませた。
[ユニバーサル基盤キット](https://www.windesign.work/%E8%A3%BD%E5%93%81%E4%B8%80%E8%A6%A7/%E3%83%8F%E3%83%BC%E3%83%89%E3%82%A6%E3%82%A7%E3%82%A2/m5stack/m5stack%E7%94%A8%E3%83%A6%E3%83%8B%E3%83%90%E3%83%BC%E3%82%B5%E3%83%AB%E5%9F%BA%E6%9D%BF/)のページよりスペーサーのデータをDLし、各種センサー用にBlenderで高さを追加して、[DMM.make](https://make.dmm.com/)からレジンで出力した。
<img width="400" src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3911544/394b5418-3a0e-a212-2a43-a9243c459975.png>
出力結果に対する反省としては、内部のネジ穴がこの目的では不要だったことと、ケーブルを通す凹み部（画像左側）が狭く、双方カッターにて加工した。
また、ウォッチ型キット底面の穴から心拍センサーのライトを出すために穴も拡張している。

## LEDカラー制御
サイリウムのように色を可変させていきたい。
メンバーカラーが光るようにRGB値を調整しつつ、輝度や順番などの調整を行なった。めっちゃ楽しい。
なお、以下コーディングの大半はChatGPTと格闘している。
```c:prismwatch.ino
// LED制御に関する変数
#define PIN 32  // M5Stack Grobe Pin
#define NUMPIXELS 16  // LED数
Adafruit_NeoPixel pixels(NUMPIXELS, PIN, NEO_GRB + NEO_KHZ800);

int colors[][3] = {
  {255, 0, 0},    // 赤
  {0, 0, 255},    // 青
  {0, 255, 0},    // 緑
  {255, 95, 0},   // オレンジ
  {255, 230, 39}, // 薄い黄色
  {0, 221, 237},  // 淡い水色
  {0, 255, 128},  // 緑がかった水色
  {255, 45, 205}, // ピンク
  {178, 0, 255},  // 赤紫
  {102, 0, 255},  // 青紫
  {255, 255, 255} // 白
};
int currentColorIndex = 0;

int activeMode = 0;  // 0: No action, 1: Button A action (CAUTION), 2: Button C action (LED colors), 3: Play button (audio)
```
## SDカードによる画像読み込みと表示
作中のプリズムシステムの画面を再現するために画像の描画ではなく表示を行なった
画像サイズの大きいものをコーディングでサイズ変更して表示しようかとしていたが、最終的に画面サイズの画像を制作するという方向で解決した。
[有志による盤面](https://x.com/koko_chi10/status/1812426459161743479?s=46&t=JWFK7z4fSH56hvMKUxGPtw)を編集し、横を足した画像を制作した。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3911544/94f97957-185c-f490-53d1-9c897b448048.png)
<img width="400" src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3911544/1badfa6e-c89f-e9a9-3ff6-087d6613aa5c.png>
呼び出しやすいよう画像はSD内にフォルダなど作らずそのまま入れ、ファイル名を数字で入力している。
```c:prismwatch.ino
 // 色に応じた画像を表示
  char imagePath[16];
  sprintf(imagePath, "/%d.jpg", currentColorIndex + 1);
  if (SD.exists(imagePath)) {
    M5.Lcd.drawJpgFile(SD, imagePath);
  }

  Serial.printf("LED color changed to index: %d\n", currentColorIndex);
```

## SDカードからの音楽再生
応援グッズには一切関係ないのだが、作中でこの”デバイスを制作した会社の社歌”が存在する。
スピーカー機能で遊ぶついでに試してみた。
[参考](https://dev.classmethod.jp/articles/m5stack-core2-for-aws-arduino-audio/)ページを見ながら組み、どうにか聞けるものとなった。
途中異常に再生速度が遅く、ホラー映像と化してしまったこともあったが、なんとか解決した。
ここのなんとかの部分は今失念したため、後ほど更新したい。

## ここまでのコード
```c:prismwatch.ino
#include <M5Core2.h>
#include <Adafruit_NeoPixel.h>
#include <driver/i2s.h>
#include "AudioFileSourceSD.h"
#include "AudioGeneratorWAV.h"
#include "AudioOutputI2S.h"
#include "FS.h"
#include "SD.h"

// WAV再生に関する変数
AudioGeneratorWAV *wav = NULL;
AudioFileSourceSD *file;
AudioOutputI2S *out;

#define BCLK_PIN 12
#define LRCK_PIN 0
#define SADTA_PIN 2
#define EXTERNAL_I2S 0
#define OUTPUT_GAIN 20  // ボリューム75%

// LED制御に関する変数
#define PIN 32  // M5Stack Grobe Pin
#define NUMPIXELS 16  // LED数
Adafruit_NeoPixel pixels(NUMPIXELS, PIN, NEO_GRB + NEO_KHZ800);

int colors[][3] = {
  {255, 0, 0},    // 赤
  {0, 0, 255},    // 青
  {0, 255, 0},    // 緑
  {255, 95, 0},   // オレンジ
  {255, 230, 39}, // 薄い黄色
  {0, 221, 237},  // 淡い水色
  {0, 255, 128},  // 緑がかった水色
  {255, 45, 205}, // ピンク
  {178, 0, 255},  // 赤紫
  {102, 0, 255},  // 青紫
  {255, 255, 255} // 白
};
int currentColorIndex = 0;

int activeMode = 0;  // 0: No action, 1: Button A action (CAUTION), 2: Button C action (LED colors), 3: Play button (audio)

// すべての機能を停止して最初の画面に戻す関数
void resetToInitialScreen() {
  stopAllFunctions();  // すべての機能を停止
  activeMode = 0;  // モードを初期化
  
  // 画面をクリアして再生ボタンを再描画
 if (SD.exists("/0.jpg")) {
      M5.Lcd.drawJpgFile(SD, "/0.jpg");
    }
}

// すべての機能を停止する関数
void stopAllFunctions() {
  // WAV停止
  if (wav != NULL && wav->isRunning()) {
    wav->stop();
  }
  
  // LED消灯
  pixels.clear();
  pixels.show();
  
  // 画面クリア
  M5.Lcd.fillScreen(BLACK);
}

// 音声を再生する関数
void playYurihaka() {
    stopAllFunctions();  // 他の機能を停止

    file = new AudioFileSourceSD("/yurihaka.wav");
    out = new AudioOutputI2S(I2S_NUM_1, EXTERNAL); 
    out->SetPinout(BCLK_PIN, LRCK_PIN, SADTA_PIN);
    out->SetOutputModeMono(true);
    out->SetGain((float)OUTPUT_GAIN / 100.0);  // 音量75%
    wav = new AudioGeneratorWAV();
    wav->begin(file, out);

    // nanikore.jpgを表示
    if (SD.exists("/nanikore.jpg")) {
      M5.Lcd.drawJpgFile(SD, "/nanikore.jpg");
    }
}

// CAUTION表示の関数
void displayCaution() {
  stopAllFunctions();  // 他の機能を停止
  activeMode = 1;  // CAUTIONモード

  M5.Axp.SetVibration(true);  // バイブレーションオン
  delay(100);
  M5.Axp.SetVibration(false);

  // CAUTIONと白いテキストを表示
  M5.Lcd.fillScreen(BLACK);
  M5.Lcd.setTextColor(WHITE);
  M5.Lcd.setTextSize(1);
  M5.Lcd.setCursor(0, 96);
  M5.Lcd.println("There is No Sparkle of The Prism.");

  while (activeMode == 1) {
    M5.update();  // ボタンの状態を更新
    if (M5.BtnC.wasReleased()) {  // Button Cで停止
      activeMode = 0;
      break;
    }

    // "CAUTION!"点滅
    M5.Lcd.setTextColor(RED);
    M5.Lcd.setTextSize(4);
    M5.Lcd.setCursor(0, 0);
    M5.Lcd.printf("\nCAUTION!\n");
    delay(500);
    M5.Lcd.fillRect(0, 0, 320, 96, BLACK);  // CAUTION部分を消す
    delay(500);
  }
}

// LEDの色を変更する関数
void changeLEDColors() {
  stopAllFunctions();  // 他の機能を停止
  activeMode = 2;  // LEDモード

  currentColorIndex = (currentColorIndex + 1) % (sizeof(colors) / sizeof(colors[0]));
  
  // LED色変更
  for (int i = 0; i < NUMPIXELS; i++) {
    pixels.setPixelColor(i, pixels.Color(colors[currentColorIndex][0], colors[currentColorIndex][1], colors[currentColorIndex][2]));
  }
  pixels.show();

  // 色に応じた画像を表示
  char imagePath[16];
  sprintf(imagePath, "/%d.jpg", currentColorIndex + 1);
  if (SD.exists(imagePath)) {
    M5.Lcd.drawJpgFile(SD, imagePath);
  }

  Serial.printf("LED color changed to index: %d\n", currentColorIndex);
}

void setup() {
  M5.begin();
  pixels.begin();
  pixels.setBrightness(30);  // LED輝度設定

  // SDカード初期化
  if (!SD.begin()) {
    Serial.println("SD initialization failed!");
    return;
  }

  // スピーカー初期化
  M5.Axp.SetSpkEnable(true); 

  // 右上の再生ボタン表示
if (SD.exists("/0.jpg")) {
      M5.Lcd.drawJpgFile(SD, "/0.jpg");
    }
}

void loop() {
  M5.update();

  // ボタンA: CAUTION
  if (M5.BtnA.wasReleased()) {
    displayCaution();
  }

  // ボタンC: LED色変更
  if (M5.BtnC.wasReleased()) {
    changeLEDColors();
  }

  // ボタンB: すべての処理を止めて初期画面に戻す
  if (M5.BtnB.wasReleased()) {
    resetToInitialScreen();
  }

  // 右上の再生ボタンが押された場合
  TouchPoint_t pos = M5.Touch.getPressPoint();
  if (pos.x > 240 && pos.y < 80) {
    playYurihaka();
    activeMode = 3;  // 再生モード
  }

  // WAV再生中の更新処理
  if (activeMode == 3 && wav != NULL) {
    if (!wav->loop()) {
      wav->stop();
      activeMode = 0;
    }
  }
}

```
## 心拍センサーの展望
心拍数をとって、そこになんらかの係数をかけていい感じに10000を上限として5000以上の数字が出るような機能を搭載したい。が、現状心拍数を心拍数として抜き出す点と、画像の上に変動する文字（心拍数をいい感じにした文字）を表示する点にいまいち知見がなく止まっている。
スケッチ例などは見ているのだがうまくいっていないため、何かアドバイスや知見があったらぜひ教えていただきたい。
## 感想・反省
まず機器選択で大いに研修講師の力をお借りしました。ありがとうございます。
新卒研修程度の知識で、応援グッズ程度の機能であれば「やりたいこと」が実現できるというのは非常に楽しい経験となった。
反省としては思ったより高くついてしまったことと、心拍数部分が完成していないこと。
3Dデータ作成や電子部品のショップへ足を運ぶことなど、更に得られた体験は多く、ぜひいろいろやってみて欲しいなと感じている。


さてこれをお読み頂いたエリートの方々。全部教えるのでみんなで作ろうプリズムウォッチ。
通信機能があるので、つくってくれたらなんとか頑張ってバトルモードの実装を試行錯誤します。
「プリズムの煌めきが消えた！？」＼ほんとだー！／
<img width="250" src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3911544/4efd03cc-e12d-d385-7dde-973c2cbf751c.png>


### 完成品
<img width="500" src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3911544/7c4b2eed-44a6-e9ca-8b62-6abcbcbe5d51.png>