はじめに
よくある3本線の心拍センサだと、振幅がイマイチと感じていたので、MAX30102センサ搭載に切り替えました。
Arduino Unoでは問題なく動作しましたが、ESP32-C3で知識不足で動かせなかったので、7年以上前のM5Stack Grayを引っ張り出し、動かしたメモです。
環境
MacOS 13.4.1
Arduino 2.2.1
M5Stack Gray(2018.3)(MPU9250)
MAX30102搭載心拍センサ
M5Stack Gray
https://docs.m5stack.com/en/core/gray
説明や、ドライバがある。
電源オン:左の赤いボタンを押す
電源オフ:左の赤いボタンを2回素早く押す
※USB電源の場合、オフにはできない
完全放電後は、USB指しても、すぐには使えないっぽい。
書き込めないとき、赤いボタン長押しが必要かも。
M5Stack Basic V2.7が最新(ESP32)¥7000
CORE2シリーズ(ESP32-D0WD-V3)¥9000
CORE S3シリーズ(ESP32-S3)¥7000~
準備
①M5StackのUSBドライバをインストール
(自分の場合はガチャガチャやってたので、もしかしたら不要かも。)
(cu.usbserial-01897F3Dを使っても、大丈夫かも。)
(Windowsならそもそも不要かも。)
CP210x(Silicon Labs 製)
ドライバをいれると/dev/cu.SLAB_USBtoUART
というポートが追加されます。
ドライバを入れる際に、
macOSが警告を出す場合「セキュリティとプライバシー」に「許可」ボタンを押す必要がある。
②Arduinoの環境設定の追加のボードマネージャのURLを追加
https://dl.espressif.com/dl/package_esp32_index.json
多分この1行の追加で問題ないが、だめそうなら、下記も。
ガチャガチャやったので、どれがどれやら。
http://drazzy.com/package_drazzy.com_index.json
https://m5stack.oss-cn-shenzhen.aliyuncs.com/resource/arduino/package_m5stack_index.json
下記は、M5Unified用。今回は使わない。
https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
③Arduinoのボードマネージャからesp32(2.0.14)をインストール
これが一番、ハマった。
最新データではダメ。
chatGPTに教えてもらった。
ボード > M5Stack Arduino → M5Stack-Core-ESP32を指定。
※最新データでは、これが一覧にない。
④ArduinoのライブラリマネージャからM5Stackをインストール
これがまた曲者。新旧の環境で、M5Stack.hが2つ入ってしまい、問題が生じた。
最近のボードであれば、最新のボードマネージャを入れ、M5Unifiedライブラリを入れれば良い。
一旦ライブラリを削除して、まっさらな状態で、下記ライブラリを入れた。
M5Stack by M5Stack
MAX30105 by SparkFun(← MAX30102にも対応)
プログラム
#include <M5Stack.h>
#include "MAX30105.h"
MAX30105 particleSensor;
const int graphHeight = 100;
const int graphBaseY = 160;
const int graphWidth = 320;
int x = 0;
float lastValue = 0;
unsigned long lastBeatTime = 0;
int bpm = 0;
const int AVG_COUNT = 4;
int bpmHistory[AVG_COUNT] = {0};
int bpmIndex = 0;
int avgBpm = 0;
const int scaleWindow = 150;
float irBuffer[scaleWindow];
int bufferIndex = 0;
const int SPO2_WINDOW = 100;
long redBuffer[SPO2_WINDOW];
long irRawBuffer[SPO2_WINDOW];
int spo2Index = 0;
void setup() {
M5.begin();
Wire.begin(21, 22);
M5.Lcd.fillScreen(BLACK);
M5.Lcd.setTextSize(2);
if (!particleSensor.begin(Wire, I2C_SPEED_STANDARD)) {
M5.Lcd.println("MAX30102 not found!");
while (1);
}
particleSensor.setup();
M5.Lcd.println("Sensor OK");
M5.Lcd.fillScreen(BLACK);
}
void loop() {
long irRaw = particleSensor.getIR();
long redRaw = particleSensor.getRed();
static float irSmoothed = 0;
const float alpha = 0.3;
irSmoothed = alpha * irRaw + (1.0 - alpha) * irSmoothed;
unsigned long now = millis();
// グラフ用スケーリング
irBuffer[bufferIndex] = irSmoothed;
bufferIndex = (bufferIndex + 1) % scaleWindow;
float minIR = irBuffer[0], maxIR = irBuffer[0];
for (int i = 1; i < scaleWindow; i++) {
if (irBuffer[i] < minIR) minIR = irBuffer[i];
if (irBuffer[i] > maxIR) maxIR = irBuffer[i];
}
if (maxIR - minIR < 1000) maxIR = minIR + 1000;
float dynamicThreshold = minIR + (maxIR - minIR) * 0.6;
int y = graphBaseY - map(irSmoothed, minIR, maxIR, -graphHeight / 2, graphHeight / 2);
y = constrain(y, 0, 240);
int lastY = graphBaseY - map(lastValue, minIR, maxIR, -graphHeight / 2, graphHeight / 2);
lastY = constrain(lastY, 0, 240);
M5.Lcd.drawLine(x, 0, x, 240, BLACK);
M5.Lcd.drawLine(x - 1, lastY, x, y, GREEN);
lastValue = irSmoothed;
// ピーク検出(マーク表示)
static float prev1 = 0, prev2 = 0;
static bool isRising = false;
if (irSmoothed > prev1 && prev1 > prev2) {
isRising = true;
}
if (isRising && prev1 > irSmoothed && prev1 > dynamicThreshold) {
int peakY = graphBaseY - map(prev1, minIR, maxIR, -graphHeight / 2, graphHeight / 2);
peakY = constrain(peakY, 0, 240);
M5.Lcd.fillCircle(x - 1, peakY, 3, RED);
isRising = false;
}
prev2 = prev1;
prev1 = irSmoothed;
// BPM計算
static bool wasLow = true;
if (irSmoothed > dynamicThreshold && wasLow) {
unsigned long dt = now - lastBeatTime;
if (dt > 300) {
bpm = 60000 / dt;
lastBeatTime = now;
bpmHistory[bpmIndex] = bpm;
bpmIndex = (bpmIndex + 1) % AVG_COUNT;
int sum = 0;
for (int i = 0; i < AVG_COUNT; i++) sum += bpmHistory[i];
avgBpm = sum / AVG_COUNT;
}
wasLow = false;
} else if (irSmoothed < dynamicThreshold) {
wasLow = true;
}
// SpO2計算
redBuffer[spo2Index] = redRaw;
irRawBuffer[spo2Index] = irRaw;
spo2Index = (spo2Index + 1) % SPO2_WINDOW;
long redMin = redBuffer[0], redMax = redBuffer[0];
long irMin = irRawBuffer[0], irMax = irRawBuffer[0];
for (int i = 1; i < SPO2_WINDOW; i++) {
if (redBuffer[i] < redMin) redMin = redBuffer[i];
if (redBuffer[i] > redMax) redMax = redBuffer[i];
if (irRawBuffer[i] < irMin) irMin = irRawBuffer[i];
if (irRawBuffer[i] > irMax) irMax = irRawBuffer[i];
}
float acRed = redMax - redMin;
float dcRed = redMax;
float acIr = irMax - irMin;
float dcIr = irMax;
float ratio = (acRed / dcRed) / (acIr / dcIr);
int spo2 = 110 - 25 * ratio;
spo2 = constrain(spo2, 70, 100);
// 表示更新
M5.Lcd.fillRect(0, 0, 160, 40, BLACK);
M5.Lcd.setCursor(0, 0);
M5.Lcd.print("BPM: ");
M5.Lcd.print(avgBpm);
M5.Lcd.setCursor(0, 20);
M5.Lcd.print("SpO2: ");
M5.Lcd.print(spo2);
M5.Lcd.print(" %");
x++;
if (x >= graphWidth) {
x = 0;
M5.Lcd.fillScreen(BLACK);
}
delay(20); // 50Hz
}