LoginSignup
0
0

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

Last updated at Posted at 2023-05-28

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

きっかけと作ったもの

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

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

LEDデモ.png
↑腕を振るとその変化に合わせてグラデーションが動きます✨

全体の構成はこんな感じです。
構成図.png

UDP送信部

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

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

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で制御することができました。

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;
}

全体プログラム

#include <M5StickCPlus.h>

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

const char ssid[] = "mkwin1117"; //WiFISSIDを入力
const char pass[] = "password"; // WiFiのパスワードを入力
const char *pc_addr = "192.0.0.0";//pcIPアドレス
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];                                                 // 加速度測定値格納用(XYZ
float accOffset[3];                                           // 加速度オフセット格納用(XYZ
float gyro[3];                                                // 角速度測定値格納用(XYZ
float gyroOffset[3];                                          // 角速度オフセット格納用(XYZ

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マッピング

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

  • 内蔵IMUのプログラム

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0