【M5StickCPlus/UDP/TouchDesigner】心拍と腕の振りでリアルタイムに変化するペンライト(LED)を作る！

※ライブラリやTouchDesigner内の処理については参考資料をご覧ください。

# きっかけと作ったもの
音楽ライブで「歓声」以外の手法で観客の反応を演者に伝えるデバイスを作りたい！というきっかけのもと、観客の心拍の上昇によって色が変化し、さらに腕の振りでインタラクションができるペンライト(の、中身( ˘ω˘ ))を作ってみました！

元々はM5Core2のシリアル通信でやっていたものを、小型化・無線化してさらに使いやすさを向上させたのがこちらです。
M5StickC用の心拍センサHat(MAX30102)と内蔵IMUの値をUDP経由でTouchDesignerに送り、その値によって変化させたエフェクトをリアルタイムにLEDマッピングします。

![LEDデモ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3458615/48017398-6703-3c8e-6975-6e725bc8229c.png)
↑腕を振るとその変化に合わせてグラデーションが動きます✨

全体の構成はこんな感じです。
![構成図.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3458615/ca5e3dfd-68f5-690e-576f-3f847b69eefb.png)

# UDP送信部

ここは、以下のサイトのプログラムをそのまま使用しました。

https://deviceplus.jp/mc-general/color-liquid-with-m5stack-02/#055

得られた心拍とIMUの値を、"心拍値,IMU値"という状態の文字列としてTouchDesignerに送信します(その後TouchDesignerのConvertDATで","によって区切る)。

```powershell
void sendUDP(String _msg, int _port) { //_msg="心拍値,IMU値"
  int len = _msg.length() + 1;
  char charArray[len];
  _msg.toCharArray( charArray, len );

  uint8_t message[len]; //Uint8型の配列を用意
  for (int i = 0; i < len; i++) {
    message[i] = uint8_t(charArray[i]);
  }
  wifiUdp.beginPacket(pc_addr, _port);
  wifiUdp.write(message, sizeof(message));
  wifiUdp.endPacket();
}
```

# UDP受信部(+LEDマッピング)
ここは、以下のサイトのプログラムをAsyncUDPからWifiUDPに変更しました。⇒ここが躓きポイントでした(>_<)
WifiUDPの送信形式に変えるだけではうまくいかず...。
色々試してみたところ、「rData[ ],gData[ ],bData[ ]」をCRGBではなく、uint8_t型で定義するとうまくいきました！
TouchDesigner内のPythonコードはサイトそのままです。
LEDマッピングはNeopixelライブラリだとうまくいかず、FastLEDで制御することができました。

https://qiita.com/foka22ok/items/b789864bf3c7ffc130ce

```powershell
if (wifiUdp.parsePacket()) {
  isDataReceive = true;
  uint8_t packetBuffer[NUM_LEDS * 3];
  wifiUdp.read((uint8_t*)packetBuffer, NUM_LEDS * 3);
  for (int i = 0; i < NUM_LEDS; i++) {
    rData[i] = (uint8_t)packetBuffer[i * 3];
    gData[i] = (uint8_t)packetBuffer[i * 3 + 1];
    bData[i] = (uint8_t)packetBuffer[i * 3 + 2];
  }
  for (int i = 0; i < NUM_LEDS * 3; i++) {
    Serial.printf("%d", (uint8_t)packetBuffer[i]);
  } Serial.printf("\n");
}
if (isDataReceive) {
  for (int i = 0; i < NUM_LEDS; i++) {
    leds[i] = CRGB(rData[i], gData[i], bData[i]);
  }
  FastLED.show();
  isDataReceive = 0;
}
```

# 全体プログラム

```powershell
#include <M5StickCPlus.h>

//--------------------------------------------------------
// UDP設定
//--------------------------------------------------------
#include <WiFi.h>
#include <WiFiUDP.h>
WiFiUDP wifiUdp;

const char ssid[] = "mkwin1117"; //WiFIのSSIDを入力
const char pass[] = "password"; // WiFiのパスワードを入力
const char *pc_addr = "192.0.0.0";//pcのIPアドレス
const int pc_port = 50000;
const int my_port = 50001;
bool isDebug = false;//デバッグモードのフラグ

// -------------------------------------------------------
// HeartRate設定
// -------------------------------------------------------
#include <Wire.h>
#include "MAX30105.h"
#include "heartRate.h"
#include "spo2_algorithm.h"

TFT_eSprite Disbuff = TFT_eSprite(&M5.Lcd);
MAX30105 Sensor;

#define MAX_BRIGHTNESS 255
#define bufferLength   100
const byte Button_A = 37;
const byte pulseLED = 26;

uint32_t irBuffer[bufferLength];
uint32_t redBuffer[bufferLength];

int8_t V_Button, flag_Reset;
int32_t spo2, heartRate, old_spo2;
int8_t validSPO2, validHeartRate;
const byte RATE_SIZE = 5;
uint16_t rate_begin  = 0;
uint16_t rates[RATE_SIZE];
byte rateSpot = 0;
float beatsPerMinute;
int beatAvg;
byte num_fail;

uint16_t line[2][320] = {0};

uint32_t red_pos = 0, ir_pos = 0;
uint16_t ir_max = 0, red_max = 0, ir_min = 0, red_min = 0, ir_last = 0,
         red_last    = 0;
uint16_t ir_last_raw = 0, red_last_raw = 0;
uint16_t ir_disdata, red_disdata;
uint16_t Alpha = 0.3 * 256;
uint32_t t1, t2, last_beat, Program_freq;
// --------------------------------------------------------
// IMU設定
// --------------------------------------------------------
float acc[3];                                                 // 加速度測定値格納用（X、Y、Z）
float accOffset[3];                                           // 加速度オフセット格納用（X、Y、Z）
float gyro[3];                                                // 角速度測定値格納用（X、Y、Z）
float gyroOffset[3];                                          // 角速度オフセット格納用（X、Y、Z）

float roll  = 0.0F;                                           // ロール格納用
float pitch = 0.0F;                                           // ピッチ格納用
float yaw   = 0.0F;                                           // ヨー格納用

const float pi = 3.14;

uint8_t axLastReport = 0;

// --------------------------------------------------------
// LED設定
// --------------------------------------------------------
#include "FastLED.h"

#define DATA_PIN 32
#define LED_TYPE SK6812
#define COLOR_ORDER GRB
#define NUM_LEDS 20
#define BRIGHTNESS 3

CRGB leds[NUM_LEDS * 3];
uint8_t rData[NUM_LEDS];
uint8_t gData[NUM_LEDS];
uint8_t bData[NUM_LEDS];

bool isDataReceive = false;
//uint8_t* packetBuffer;

// --------------------------------------------------------
// センサ値UDP送信部
// --------------------------------------------------------
void sendUDP(String _msg, int _port) {
  int len = _msg.length() + 1;
  char charArray[len];
  _msg.toCharArray( charArray, len );

  uint8_t message[len];//Uint8型の配列を用意
  for (int i = 0; i < len; i++) {
    message[i] = uint8_t(charArray[i]);
  }
  wifiUdp.beginPacket(pc_addr, _port);
  wifiUdp.write(message, sizeof(message));
  wifiUdp.endPacket();
}
// --------------------------------------------------------
// UDPセットアップ
// --------------------------------------------------------
void setupNetwork() {
  WiFi.begin(ssid, pass);
  M5.Lcd.print("WiFi connecting\n> ");
  M5.Lcd.print(ssid);
  M5.Lcd.print("\n");
  while (WiFi.status() != WL_CONNECTED) {
    M5.Lcd.print(".");
    delay(100);
  }
  M5.Lcd.print("WiFi connected!");
  M5.Lcd.print("UDP Listening on IP: ");
  M5.Lcd.print(WiFi.localIP());
  wifiUdp.begin(my_port);
}

void callBack(void) {
  V_Button = digitalRead(Button_A);
  if (V_Button == 0) flag_Reset = 1;
  delay(10);
}

void setup() {
  M5.begin();
  Serial.begin(115200);
  // ----------------------------------------
  // HeartRate
  // ----------------------------------------
  pinMode(25, INPUT_PULLUP);  // ピンモード
  pinMode(pulseLED, OUTPUT);
  pinMode(Button_A, INPUT);
  Wire.begin(0, 26);  // I2C通信初期設定

  // センサ初期設定
  if (!Sensor.begin(Wire, I2C_SPEED_FAST)) {
    // 初期化失敗
    M5.Lcd.print("Init Failed");
    Serial.println(F("MAX30102 was not found. Please check wiring/power."));
    while (1)
      ;
  }
  Serial.println(
    F("Place your index finger on the Sensor with steady pressure"));

  attachInterrupt(Button_A, callBack, FALLING);
  // Max30102設定
  Sensor.setup();
  // ----------------------------------------
  // IMU
  // ----------------------------------------
  M5.Imu.Init();

  // ----------------------------------------
  // UDP
  // ----------------------------------------
  M5.Lcd.fillScreen(BLACK);
  M5.Lcd.setTextColor(GREEN , BLACK);
  M5.Lcd.setTextSize(0.5);
  setupNetwork();

  // ----------------------------------------
  // LED
  // ----------------------------------------
  FastLED.addLeds<LED_TYPE, DATA_PIN, COLOR_ORDER>(leds, NUM_LEDS).setCorrection(TypicalLEDStrip);
  FastLED.setBrightness(BRIGHTNESS);
}

void loop() {
  M5.update();
  // ----------------------------------------
  // HeartRate
  // ----------------------------------------
  uint16_t ir, red;

  if (flag_Reset) {
    Sensor.clearFIFO();
    delay(5);
    flag_Reset = 0;
  }

  while (flag_Reset == 0) {
    while (Sensor.available() == false) {
      delay(10);
      Sensor.check();
    }

    while (1) {
      red = Sensor.getRed();
      ir  = Sensor.getIR();

      if ((ir > 1000) && (red > 1000)) {
        num_fail                         = 0;
        t1                               = millis();
        redBuffer[(red_pos + 100) % 100] = red;
        irBuffer[(ir_pos + 100) % 100]   = ir;

        t2 = millis();
        Program_freq++;
        if (checkForBeat(ir) == true) {
          long delta = millis() - last_beat - (t2 - t1) * (Program_freq - 1);
          last_beat = millis();

          Program_freq   = 0;
          beatsPerMinute = 60 / (delta / 1000.0);
          if ((beatsPerMinute > 30) && (beatsPerMinute < 120)) {
            rate_begin++;
            if ((abs(beatsPerMinute - beatAvg) > 15) && ((beatsPerMinute < 55) || (beatsPerMinute > 95)))
              beatsPerMinute = beatAvg * 0.9 + beatsPerMinute * 0.1;
            if ((abs(beatsPerMinute - beatAvg) > 10) && (beatAvg > 60) && ((beatsPerMinute < 65) || (beatsPerMinute > 90)))
              beatsPerMinute = beatsPerMinute * 0.4 + beatAvg * 0.6;

            rates[rateSpot++] = (byte) beatsPerMinute;
            rateSpot %= RATE_SIZE;

            beatAvg = 0;
            if ((beatsPerMinute == 0) && (heartRate > 60) && (heartRate < 90))
              beatsPerMinute = heartRate;
            if (rate_begin > RATE_SIZE) {
              for (byte x = 0; x < RATE_SIZE; x++)
                beatAvg += rates[x];
              beatAvg /= RATE_SIZE;
            } else {
              for (byte x = 0; x < rate_begin; x++)
                beatAvg += rates[x];
              beatAvg /= rate_begin;
            }
          }
        }
      }
      else
        num_fail++;
      if ((Sensor.check() == false) || flag_Reset) break;
    }
    Sensor.clearFIFO();
    Disbuff.fillRect(0, 0, 240, 135, BLACK);
    old_spo2 = spo2;
    if (red_pos > 100)
      maxim_heart_rate_and_oxygen_saturation(irBuffer, bufferLength, redBuffer, &spo2, &validSPO2, &heartRate, &validHeartRate);
    if (!validSPO2) spo2 = old_spo2;
    if (num_fail < 10) {
      Disbuff.setTextColor(GREEN);
      Disbuff.printf("spo2:%d,", spo2);
      Disbuff.setCursor(60, 25);
      Disbuff.printf(" BPM:%d", beatAvg);
      //Serial.printf("BPM:%d\n", beatAvg);
      // ----------------------------------------
      // IMU
      // ----------------------------------------
      M5.IMU.getAccelData(&acc[0], &acc[1], &acc[2]);             // 加速度の取得
      M5.IMU.getGyroData(&gyro[0], &gyro[1], &gyro[2]);           // 角速度の取得
      roll  =  atan(acc[0] / sqrt((acc[1] * acc[1]) + (acc[2] * acc[2]))) * 180 / pi;
      sendUDP(String(beatAvg) + "," + String(roll), pc_port ); //"心拍値,IMU値"をUDP送信
    } else {
      Disbuff.setTextColor(RED);
      Disbuff.printf("No Finger!!");
    }
    Disbuff.pushSprite(0, 0);
    // ----------------------------------------
    // LED
    // ----------------------------------------
    if (wifiUdp.parsePacket()) {
      isDataReceive = true;
      uint8_t packetBuffer[NUM_LEDS * 3];
      wifiUdp.read((uint8_t*)packetBuffer, NUM_LEDS * 3);
      for (int i = 0; i < NUM_LEDS; i++) {
        rData[i] = (uint8_t)packetBuffer[i * 3];
        gData[i] = (uint8_t)packetBuffer[i * 3 + 1];
        bData[i] = (uint8_t)packetBuffer[i * 3 + 2];
      }
      for (int i = 0; i < NUM_LEDS * 3; i++) {
        Serial.printf("%d", (uint8_t)packetBuffer[i]);
      } Serial.printf("\n");
    }
    if (isDataReceive) {
      for (int i = 0; i < NUM_LEDS; i++) {
        leds[i] = CRGB(rData[i], gData[i], bData[i]);
      }
      FastLED.show();
      isDataReceive = 0;
    }
    delay(1);
  }
}
```

# まとめ
今回は演者から見えるペンライトという形で、観客の盛り上がりを表現しました。ペンライト以外にもいろんな方法で応用ができると思うので、まだまだ試してみたいところです(。-`ω-)

# その他参考サイト
* M5StickCでLEDマッピング

https://tetsuakibaba.jp/ws/doku.php?id=arduino:m5stickc:06.neopixel

* 心拍センサHatのプログラム

https://github.com/m5stack/M5StickC-Plus/blob/master/examples/Hat/HEART_RATE_MAX30102/HEART_RATE_MAX30102.ino

* 内蔵IMUのプログラム

https://karakuri-musha.com/inside-technology/arduino-m5stickc-plus-10-imu-01/