※ライブラリやTouchDesigner内の処理については参考資料をご覧ください。
きっかけと作ったもの
音楽ライブで「歓声」以外の手法で観客の反応を演者に伝えるデバイスを作りたい!というきっかけのもと、観客の心拍の上昇によって色が変化し、さらに腕の振りでインタラクションができるペンライト(の、中身( ˘ω˘ ))を作ってみました!
元々はM5Core2のシリアル通信でやっていたものを、小型化・無線化してさらに使いやすさを向上させたのがこちらです。
M5StickC用の心拍センサHat(MAX30102)と内蔵IMUの値をUDP経由でTouchDesignerに送り、その値によって変化させたエフェクトをリアルタイムにLEDマッピングします。
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"; //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マッピング
- 心拍センサHatのプログラム
- 内蔵IMUのプログラム