0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

M5Stack Gray

Posted at

はじめに

よくある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というポートが追加されます。
image.png
ドライバを入れる際に、
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に教えてもらった。
image.png

ボード > M5Stack Arduino → M5Stack-Core-ESP32を指定。
※最新データでは、これが一覧にない。
image.png

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

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?