あいまいまいんの生物学

あいまいまいんの生物学

生物学が好き。勉強したり遊んだり。

日頃感じたこと思ったこと、出来事など

勉強して面白かった話

授業で使えそうな生物学の知識・雑談・小ネタ

などなどを紹介していきたいと思います

コメント大歓迎!気軽にコメントして下さい

ゲームサイトはこちら

M5StackでMAX30100使って心拍を画面上に表示する

きっかけについて

はい
久しぶりのプログラミング系記事です!

ずーっと前にM5Stack買ったんですけど
i-my-mine.hatenablog.com

全然使えてなかったっていう…ハイ

そもそもM5Stack動かす時にはArduinoを使わなければいけないんですけど
Arduinoの記法がイマイチよくわかってないし
そもそもくっつけるものごとに記法があるんですけど
それを逐一調べるのも面倒だし

ということで
やりたいな~やりたいな~と思いながらちょっと触ってみても
ちょっと検索して心折れて「これは無理やで…」ってなって終わる
そんな日々を繰り返して半年近く放置してしまっていました。反省。

でもさすがと寝かしすぎたのでね?
熟成しちゃうからね?
そろそろ使いたいなと。

そんな時にRingFitAdventureを買ったんですけど
そしたらその心拍測定装置がどうも旦那さんにフィットしなくてうまく測れない。
「まいんさんが持ってたM5Stackで確か測れるようにできるよね?心不全とか怖いから測れるようにしてよ」と言われ
Oh... と思いながらもちょっと取り組んでみました。

ちなみにこの記事は発展途上であって完成品の話ではないので気をつけて欲しい。

目標:
MAX30100でとったデータを元にM5Stack画面上に心電図のような心拍推移グラフとBPMを表示させること

MAX30100は心拍を取得するためのセンサユニットですが、
これにはBPMを自動で計算するor取得するという機能がありません(他のタイプだとコードで一言かけばあっという間にとれるっぽい)。
ので、安価でいいじゃんってポチッたけど困る元になりました。BPMくらい計算してくれまじで。
更に心拍についても、サンプルコード(以下でリンクあり)使えばシリアルプロッタでパソコン画面上なら見れるようにはなるんですが、
このコードのままだとM5Stackの画面はまったくもって無意味です。ただの中継メカです。
かといってじゃあM5Stack画面上に出力しようというコードも公開されていないんだな。なんでだよ…
てなわけでここらへんで悪戦苦闘してます。

使用機材およびリファレンス

ではまず使ったものとか参考にしたページとか貼っときます。

使ったもの

M5Stackはこのタイプ
M5Stack Gray(9軸IMU搭載) - スイッチサイエンス
心拍取得はMAX30100
M5Stack用心拍センサユニット - スイッチサイエンス


Rawデータをとるサンプルコード
M5-ProductExampleCodes/MAX30100_RawData.ino at master · m5stack/M5-ProductExampleCodes · GitHub
商品ページのドキュメントのところに載っています。
先にも述べましたが、これを使うとシリアルプロッタでパソコン画面上で心拍の様子が見れるようにはなります。
あとはRawデータはとれるのでこれを元に画面に出そうとかBPM計算しようみたいなノリになるので大事。

参考にしたやつ

ambidata.io
違う心拍センサを用いて私が欲しい感じのやつを開発した人のブログ。めちゃ助かりました、ありがとうございました…
ただ心拍センサが違うので実装は地味に変えなければいけなかったけれど、ほとんど画面に表示するベースはここから移植している。
GPSのところは使ってないです。
ブログ内に以下のgithubに飛べるリンクがある↓
M5Stack_PulseSensor/M5Stack_PulseSensor.ino at master · AmbientDataInc/M5Stack_PulseSensor · GitHub


Arduino APIはここ
M5Stack - A series of modular stackable development devices

心拍センサーの記法はこれを見た
Arduino-MAX30100/MAX30100.h at master · oxullo/Arduino-MAX30100 · GitHub

実装していくぞ

取り敢えずRawデータとる

まずは単純に心拍センサユニットをM5Stackにつけ、上で紹介したMAX30100の商品ページにあるサンプルコード(Rawデータ取得用)を考え無で実装してみる。
すると普通にシリアルプロッタで線が出てくる。
第一段階はクリア?(何をしたんだ感)

M5Stack画面上にシリアルプロッタに出てくるやつみたいなのを描画する

ここからだぜ…
それをやるためにまず、なんにも手がでなかったので同じようなことしてる人いないの?って調べてみた。
そしたら上のリファレンスで紹介した人のブログが出てきたので、眺めながら必要そうなものだけ移植。
ありがたいことにほとんどセンサ特異的な書き方ではなかったので、Rawデータどりのサンプルコードと組み合わせるだけでそこそこの土台ができた。

ちなみに今回の描画実装のイメージとしては
・まずM5Stackの画面は縦240横320で左上が(0,0)
・maxSとminSは画面の上と下を指していて、横幅いっぱいにグラフ描画が終わったらその回の縦軸MAXと横軸MAXを参考に縦の幅が修正されるようになっている(綺麗に心拍の上下が見えるようになるように)
・心拍描画は前回の値との差でプロットされていく
こんな感じか?(どこまで書いておくべきかわかんない…)


一回ほとんど値いじらないまま実装したら死人になった。
f:id:I_my_mine:20191215165658j:plain
面白すぎ。


値いじくり倒して取り敢えず一回実装したのが下の感じ。
ただしこの段階ではBPMはずっと1になるようにしてある。
あとめっちゃ旦那さん頼ってる。ほぼ旦那さんの功績では?(私はリファレンス引っ張ってくっつけただけ感ある)

/*
    Install MAX30100lib Library first.

    MAX30100_RawData.ino
*/
  
#include <M5Stack.h>
#include <Wire.h>
#include "MAX30100.h"

#define SAMPLING_RATE   MAX30100_SAMPRATE_100HZ
#define IR_LED_CURRENT  MAX30100_LED_CURR_50MA
#define RED_LED_CURRENT MAX30100_LED_CURR_27_1MA
// set HIGHRES_MODE to true only
// when setting PULSE_WIDTH to MAX30100_SPC_PW_1600US_16BITS
#define PULSE_WIDTH MAX30100_SPC_PW_1600US_16BITS
#define HIGHRES_MODE    true

// new a object
MAX30100 sensor;

void setup() {
    M5.begin();
    Serial.begin(115200);
    Serial.print("Initializing MAX30100..");
    if (!sensor.begin()) {
        Serial.println("FAILED");
        for(;;);
    } else {
        Serial.println("SUCCESS");
    }
    sensor.setMode(MAX30100_MODE_SPO2_HR);
    sensor.setLedsCurrent(IR_LED_CURRENT, RED_LED_CURRENT);
    sensor.setLedsPulseWidth(PULSE_WIDTH);
    sensor.setSamplingRate(SAMPLING_RATE);
    sensor.setHighresModeEnabled(HIGHRES_MODE);
}


const int LCD_WIDTH = 320;
const int LCD_HEIGHT = 240;
const int DOTS_DIV = 30;
#define GREY 0x7BEF

void DrawGrid() {
    for (int x = 0; x <= LCD_WIDTH; x += 2) { // Horizontal Line
        for (int y = 0; y <= LCD_HEIGHT; y += DOTS_DIV) {
            M5.Lcd.drawPixel(x, y, GREY);
        }
        if (LCD_HEIGHT == 240) {
            M5.Lcd.drawPixel(x, LCD_HEIGHT - 1, GREY);
        }
    }
    for (int x = 0; x <= LCD_WIDTH; x += DOTS_DIV) { // Vertical Line
        for (int y = 0; y <= LCD_HEIGHT; y += 2) {
            M5.Lcd.drawPixel(x, y, GREY);
        }
    }
}

#define REDRAW 20 // msec

int lastMin = 65000, lastMax = 50000;
int minS= 65000, maxS = 50000;
int lastY = 65000;
int x = 0;




void loop()
{
    M5.update();
    delay(REDRAW);
    uint16_t ir, red;
    sensor.update();
    while(sensor.getRawValues(&ir, &red)){
      //Serial.println(ir);
      //Serial.print('\t');
      //Serial.println(red);
    };
    uint16_t y = red;
    if (y < minS) minS = y;
    if (maxS < y) maxS = y;
    if (x > 0) {
        y = (int)(LCD_HEIGHT - (float)(y - lastMin) / (lastMax - lastMin) * LCD_HEIGHT);
        M5.Lcd.drawLine(x - 1, lastY, x, y, WHITE);
        lastY = y;
    }
    Serial.print("minS: ");
    Serial.print(minS);
    Serial.print(" maxS: ");
    Serial.print(maxS);
    Serial.print(" y: ");
    Serial.println(y);
    if (++x > LCD_WIDTH) {
        x = 0;
        M5.Lcd.fillScreen(BLACK);
        DrawGrid();
        lastMin = minS - 20;
        lastMax = maxS + 20;
        minS = 65000;
        maxS = 50000;
        M5.Lcd.setCursor(0, 0);
        M5.Lcd.setTextSize(4);
        M5.Lcd.printf("BPM: %d", 1);
    }
}

www.youtube.com

これ、maxSとminSがうまくいくようになるまで2周期くらい待たないといけない。それまで心拍がなんかヤバいことになる。
でも取り敢えずたたき台はできたでしょといって一応満足する…。

BPMをとっていく

一番困ったのがBPMをとるアルゴリズムだった。
本当は移動平均とかとってスムーズにして、傾きや差を判定して「心拍の下がった瞬間」数をカウントしていくのがベストかなと個人的には思うのだけれど、さすがとちょっと気力がなかったので
一番アホみたいなアルゴリズムで「15回分のyの値を蓄積(p)して直前の15回分y総和(lastp)よりも低くなってたらカウントしていく」みたいななんともいえないやつで作ってみた。
でもこれだとなんかうまくでなかったのでよりちゃんと差があるときだけひっかかるようにpとlastpの差が3マス分になったときに変更してみた。
BPMは画面更新と同時に前回の画面のBPMが表示されるように、
「前回の画面中のpがlastpを下回った回数/6.5×60」にした。
ちなみに6.5というのは、データ取得が20msecごとで横幅320であることから、画面いっぱいにデータが集まるときに320*20/1000 = 6.5sec分のデータになるというところから出した値。

以下がコード。

/*
    Install MAX30100lib Library first.

    MAX30100_RawData.ino
*/
  
#include <M5Stack.h>
#include <Wire.h>
#include "MAX30100.h"

#define SAMPLING_RATE   MAX30100_SAMPRATE_100HZ
#define IR_LED_CURRENT  MAX30100_LED_CURR_50MA
#define RED_LED_CURRENT MAX30100_LED_CURR_27_1MA
// set HIGHRES_MODE to true only
// when setting PULSE_WIDTH to MAX30100_SPC_PW_1600US_16BITS
#define PULSE_WIDTH MAX30100_SPC_PW_1600US_16BITS
#define HIGHRES_MODE    true

// new a object
MAX30100 sensor;

void setup() {
    M5.begin();
    Serial.begin(115200);
    Serial.print("Initializing MAX30100..");
    if (!sensor.begin()) {
        Serial.println("FAILED");
        for(;;);
    } else {
        Serial.println("SUCCESS");
    }
    sensor.setMode(MAX30100_MODE_SPO2_HR);
    sensor.setLedsCurrent(IR_LED_CURRENT, RED_LED_CURRENT);
    sensor.setLedsPulseWidth(PULSE_WIDTH);
    sensor.setSamplingRate(SAMPLING_RATE);
    sensor.setHighresModeEnabled(HIGHRES_MODE);
}


const int LCD_WIDTH = 320;
const int LCD_HEIGHT = 240;
const int DOTS_DIV = 30;
#define GREY 0x7BEF

void DrawGrid() {
    for (int x = 0; x <= LCD_WIDTH; x += 2) { // Horizontal Line
        for (int y = 0; y <= LCD_HEIGHT; y += DOTS_DIV) {
            M5.Lcd.drawPixel(x, y, GREY);
        }
        if (LCD_HEIGHT == 240) {
            M5.Lcd.drawPixel(x, LCD_HEIGHT - 1, GREY);
        }
    }
    for (int x = 0; x <= LCD_WIDTH; x += DOTS_DIV) { // Vertical Line
        for (int y = 0; y <= LCD_HEIGHT; y += 2) {
            M5.Lcd.drawPixel(x, y, GREY);
        }
    }
}

#define REDRAW 20 // msec

int lastMin = 65000, lastMax = 50000;
int minS= 65000, maxS = 50000;
int lastY = 65000;
int x = 0;
int count1 = 0;
int count2 = 0;
int p = 0;
int lastp = 0;
int bpm;



void loop()
{
    M5.update();
    delay(REDRAW);
    uint16_t ir, red;
    sensor.update();
    while(sensor.getRawValues(&ir, &red)){
      //Serial.println(ir);
      //Serial.print('\t');
      //Serial.println(red);
    };
    uint16_t y = red;
    if (y < minS) minS = y;
    if (maxS < y) maxS = y;
    if (x > 0) {
        y = (int)(LCD_HEIGHT - (float)(y - lastMin) / (lastMax - lastMin) * LCD_HEIGHT);
        M5.Lcd.drawLine(x - 1, lastY, x, y, WHITE);
        lastY = y;
        p += y;
        count2++;
        if(count2 == 15){
          p/=15;
          if(lastp > p+(LCD_HEIGHT/8)) ++count1;
          lastp = p;
          p = 0;
          count2 = 0;  
        }
    }
    Serial.print("minS: ");
    Serial.print(minS);
    Serial.print(" maxS: ");
    Serial.print(maxS);
    Serial.print(" y: ");
    Serial.println(y);
    if (++x > LCD_WIDTH) {
        x = 0;
        M5.Lcd.fillScreen(BLACK);
        DrawGrid();
        lastMin = minS - 20;
        lastMax = maxS + 20;
        minS = 65000;
        maxS = 50000;
        bpm = (float)count1/6.5 * 60;
        M5.Lcd.setCursor(0, 0);
        M5.Lcd.setTextSize(4);
        M5.Lcd.printf("BPM: %d", bpm);
        count1 = 0;
    }
}

もはやごりごり系の実装になってしまっているが知らない。
で、実際やってみる。
www.youtube.com


うーん、良いときとヤバい時があるね。
安定しないね…

今後の展望

・まず心拍プロットの方法を改善する(すぐ安定して~~~~~たのむ~~~~~~~)
初期値とかminSとmaxS更新規則とかをいじればいい気がする。
またはいっそ値を縦軸に入り切るように圧縮するとか。
シリアルモニタで値を追いながらここは調整かな…
BPM取得方法を考え直す
移動平均実装するのいやや~やれば一瞬なんだろうけど…
上でも言ったけど15×20msec = 300msecごと平均とって比べていくのだと心拍数に絶対上限あるし取りこぼすんだよね。ピークの場所ずれたら終わるし。
そもそも心拍一回分を何から判別するかも考えなおした方がいいかもしれない。
どのみち今のBPM計算、心拍クソ早い時に絶対機能しないからね?これが問題だと個人的には思っている。

色んな人に協力してもらって挙動を確認して改善していくしか…という感じだ。
うまくいけば授業でも使えるんじゃん?と目論んだりしているんだけど…


余談

あともう一個開発したいのが一問一答アプリで、日本語が出てくる→英語に直す系のやつを作りたい。
生物専門用語だけの。
自分が英語で生物説明しようとすると出てこないから…
論文読んでると英語→日本語変換はできるようになるんだけど逆が全然身に着かなくてだめ。
これもやっていくぞ…