私もM5Stack心拍モニタを作って見ました

私もM5Stack心拍モニタを作って見ました ^_^

f:id:tsun226:20181014221926j:plain

 

 当初は脈にあわせてVUメータの針が振れるものを作って、それを胸のあたりに装着し、ドキがムネムネしてるのがわかるようにしようと思ってたのですが、M5Stackを入手したので、Arduino IDEを使ってディスプレイにハートマークを点滅させて簡単に見栄えのするものを作って見ました。

注意:あくまでおもちゃなので、医療用には使えません(^_^)

動画

 

構成

特徴

  • 2つの表示モードがあります。
    + グラフモード           心拍をグラフ表示します。
    + 画像モード               脈に合わせて画像を点滅表示します。
  • Ambient連携機能        20秒間隔で心拍数をambientに送信します。
  • Beep音ON/OFF          脈に合わせて音が鳴ります。
  • ファイルによる設定   WiFi,Ambientの設定をmicroSDから読み込みます。

microSDカード

 microSDカードに設定情報、画像データを用意します。

設定

Ambientに連携するための設定です。

設定情報が不足している場合、連携機能は起動しません。

  • /config/wifi_ssid               WiFi SSID
  • /config/wifi_passwd         WiFi パスワード
  • /config/ambi_channelid   AmbientチャネルID
  • /config/ambi_writekey     Ambient write key
  • /config/ambi_field            Ambient フィールド番号

空白、タブ、改行等を含まないでください。

画像データ

画像モードで表示する画像を用意してください。

  • /images/heart1.jpg     脈拍<80bpmのときに表示する画像
  • /images/heart2.jpg     脈拍≧80bpmのときに表示する画像

サイズ 320x240

画像形式jpeg(プログレッシブ指定をしないでください)

 

操作

  • Aボタン
    グラフモードと画像モードを切り替えます。
  • Bボタン
    Ambientへの計測データの送信をON/OFFします。
  • Cボタン
    脈にあわせたビープ音をON/OFFします。

心拍センサとM5Stackの接続

  • 黒ケーブル → GND
  • 赤ケーブル → 3V3
  • 紫ケーブル → 36

センサ部分は指や耳たぶに装着するものらしいですが、私の場合は掌の親指の下の膨らんでいるあたりがうまく測れました。

f:id:tsun226:20181021081918j:plain

 

sketch

 

#include  <M5Stack.h>
#include "Ambient.h"

// Pulse sensor
#define ST_INTERVAL          (320 - 1)             // count of interval for calculating statistics
#define S_PIN                36                    // Pin No. for Pulse Sensor
#define VALUE_MAX            4096                  // value max. from Pulse Sensor
#define VALUE_CONT_BEAT      5                     // to avoid chattering for judging beat
#define VALUE_THRESH_COEF    20                    // thereshold : coef of (max - avg) for judging beat
#define THRESH_HIGH_BPM      80                    // >=80 high bpm, < 80 low bpm (clolor/sound depends)
// LCD
#define TSIZE_S              2                     // text font size
#define TSIZE_M              3
#define TSIZE_L              7
#define BRIGHTNESS           200                   // LCD brightness
#define FILE_IMAGE_1         "/images/heart1.jpg"  // image file for low bmp
#define FILE_IMAGE_2         "/images/heart2.jpg"  // image file for high bmp
// Sound
#define BEEP_FREQ_1          2000                  // beep frequency for low bpm
#define BEEP_FREQ_2          4000                  // beep frequency for high bpm
#define BEEP_TIME            50                    // beep time
// Configuration files
#define FILE_CONF_WIFI_SSID  "/config/wifi_ssid"
#define FILE_CONF_WIFI_PASS  "/config/wifi_passwd"
#define FILE_CONF_AMBI_CHID  "/config/ambi_channelid"
#define FILE_CONF_AMBI_WKEY  "/config/ambi_writekey"
#define FILE_CONF_AMBI_FIELD "/config/ambi_field"
// misc.
#define TEXT_SIZE            256                   // text buffer size

// display mode (graph or image)
#define MODE_GRAPH  0         // graph mode
#define MODE_IMAGE  1         // image display mode
// sensProc()
#define S_SETUP     0
#define S_LOOP      1
#define S_INTERVAL  10L       // sampling every 10 msec
#define B_INTERVAL  (20000L)  // BPM calculating interval
#define B_MULTI     (60000L / B_INTERVAL)
// dispProc()
#define D_INIT      0
#define D_UPD       1
#define D_BPM       2
// imageProc()
#define I_INIT      0
#define I_UPD       1

// global variables
int          mode = MODE_GRAPH;
bool         soundOn = false;
bool         ambiOn = true;
// WiFi
WiFiClient   client;
char         wifiSsid[TEXT_SIZE];
char         wifiPasswd[TEXT_SIZE];
// Ambidata
Ambient      ambient;
unsigned int ambiChannelId;           // Channel ID
char         ambiWriteKey[TEXT_SIZE]; // Write key
int          ambiField = 1;           // field no.
// image(jpg)
uint8_t *imageHeart1, *imageHeart2;
size_t imageHeart1Len, imageHeart2Len;

// Setup
void setup(){
  delay(1000);
  Serial.begin(115200);
  Serial.println("Setup.");
  M5.begin();
  M5.Lcd.clear();
  M5.Lcd.setBrightness(BRIGHTNESS);
  M5.update();
  dispMssg("Setup.", WHITE, BLACK);
  readConfig();
  if(ambiOn) {
    if(wifiConnection()) {
      ambient.begin(ambiChannelId, ambiWriteKey, &client);
    } else {
      dispMssg("Ambient OFF", WHITE, RED);
      ambiOn = false;
    }
    delay(1500);
  }
  sensProc(S_SETUP, 0);
}

// Loop
void loop() {
  sensProc(S_LOOP, 0);
  checkButtons();
}


// Configuration
void readConfig() {
  size_t len;
  char buf[256];

  SD.begin();
  // read configurations
  len = readFile(FILE_CONF_WIFI_SSID, (uint8_t*)wifiSsid, TEXT_SIZE - 1);
  wifiSsid[len] = '\0';
  len = readFile(FILE_CONF_WIFI_PASS, (uint8_t*)wifiPasswd, TEXT_SIZE - 1);
  wifiPasswd[len] = '\0';
  len = readFile(FILE_CONF_AMBI_CHID, (uint8_t*)buf, TEXT_SIZE - 1);
  buf[len] = '\0';
  ambiChannelId = String(buf).toInt();
  len = readFile(FILE_CONF_AMBI_WKEY, (uint8_t*)ambiWriteKey, TEXT_SIZE - 1);
  ambiWriteKey[len] = '\0';
  len = readFile(FILE_CONF_AMBI_FIELD, (uint8_t*)buf, TEXT_SIZE - 1);
  buf[len] = '\0';
  ambiField = String(buf).toInt();
  if(! wifiSsid[0] || ! wifiPasswd[0] || ! buf[0] || ! ambiWriteKey[0]) {
    ambiOn = false;
  }
  // read images
  imageHeart1Len = readFileMalloc(FILE_IMAGE_1, &imageHeart1);
  imageHeart2Len = readFileMalloc(FILE_IMAGE_2, &imageHeart2);
}

// Read file and save to buffer
size_t readFile(const char *path, uint8_t *buf, size_t buf_len) {
  File file = SD.open(path, FILE_READ);
  size_t len = file.size();
  if(len > buf_len) len = buf_len;
  size_t len1 = file.read(buf, len);
  file.close();
  return(len);
}

// Allocate memory, read file and save
size_t readFileMalloc(const char *path, uint8_t **bufP) {
  File file = SD.open(path, FILE_READ);
  size_t len = file.size();
  *bufP = (uint8_t*)malloc(len);
  size_t len1 = file.read(*bufP, len);
  file.close();
  return(len);
}

// WiFi
boolean wifiConnection() {
  WiFi.disconnect();
  WiFi.begin(wifiSsid, wifiPasswd);
  int count = 0;
  Serial.print("Connecting Wi-Fi");
  while (count < 50) {
    dispMssg("Wi-Fi...", WHITE, BLACK);
    if (WiFi.status() == WL_CONNECTED) {
      String mssg = "Connected.\nIP: " + WiFi.localIP().toString();
      Serial.println();
      Serial.println(mssg);
      dispMssg("Connected", GREEN, BLACK);
      return(true);
    }
    if(M5.BtnB.wasPressed()) {
      dispMssg("Ambient OFF", RED, BLACK);
      M5.update();
      return(false);
    }
    M5.update();
    delay(250);
    dispMssg("", WHITE, BLACK);
    delay(250);
    Serial.print(".");
    count++;
  }
  Serial.println("Timed out.");
  dispMssg("TIME OUT.", WHITE, RED);
  return(false);
}

// Buttons
void checkButtons() {
  static unsigned long last;
  
  if(last + 10 > millis()) {
    return; 
  }
  last = millis();
  if(M5.BtnA.wasPressed()) {  // Button A -> change mode
    if(mode == MODE_GRAPH) {
      imageProc(I_INIT, 0, false);
      mode = MODE_IMAGE;
    } else if(mode == MODE_IMAGE) {
      dispProc(D_INIT, 0, 0, false);
      mode = MODE_GRAPH;
    }
  }
  if(M5.BtnB.wasPressed()) {  // Button B -> ambient switch
    if(ambiOn) {
       dispMssg("Ambient OFF", RED, BLACK);
       ambiOn = false;
    } else {
       dispMssg("Ambient ON", GREEN, BLACK);
       ambiOn = true;
    }
  }
  if(M5.BtnC.wasPressed()) {  // Button B -> sound switch
    if(soundOn) {
       dispMssg("Sound OFF", RED, BLACK);
       soundOn = false;
    } else {
       dispMssg("Sound ON", GREEN, BLACK);
       soundOn = true;
    }
  }
  M5.update();
}

// Read data from sensor and proceed
void sensProc(int cmd, int param) {
  static unsigned long timeLast, timeLastBeat;
  static int valueLast;
  static int countHigh, countLow;
  static int stSum, stMax, stMin, stCount, stAvg, stThresh, stMaxTmp, stMinTmp;
  static int beatCount, bpm;
  static bool beatHigh;
  int value;

  switch(cmd) {
  case S_SETUP:
    pinMode(S_PIN, INPUT);
    dispProc(D_INIT, 0, 0, false);
    timeLast = timeLastBeat = millis();
    bpm = 0;
    stMax = VALUE_MAX;            // inrerim values
    stMin = 0;
    stThresh = stAvg = VALUE_MAX / 2;
    beatHigh = false;
    valueLast = analogRead(S_PIN);
    countHigh = countLow = beatCount = stSum = stCount = 0;
    stMinTmp = VALUE_MAX;         // inrerim values
    stMaxTmp = 0;    
    break;
  case S_LOOP:
    if(millis() >= timeLastBeat + B_INTERVAL) {                 // update bpm
      bpm = beatCount * B_MULTI;
      if(ambiOn) {
        if(WiFi.status() == WL_CONNECTED || wifiConnection()) { // check and reconnect Wifi
          ambient.set(1, String(bpm).c_str());
          ambient.send();
        } else {
          ambiOn = false;
        }
      }
      if(mode == MODE_GRAPH) dispProc(D_BPM, bpm, 0, false);    // update for image
      // initialize for next bpm calculation
      beatCount = 0;
      timeLastBeat += B_INTERVAL;
      if(timeLast + S_INTERVAL < millis()) {                    // in case of reconnect Wifi
        timeLast = millis();
        break;
      }
    }
    if(millis() < timeLast + S_INTERVAL) {
      break;
    }
    value = analogRead(S_PIN);                  // get value from sorsor
    Serial.print(value);Serial.print(" ");Serial.print(stAvg);Serial.print(" ");Serial.print(stMin);Serial.print(" ");Serial.print(stMax);Serial.print(" ");Serial.println(stThresh);
    // statistics
    stSum += value;
    stCount++;
    if(value > stMaxTmp) stMaxTmp = value;
    if(value < stMinTmp) stMinTmp = value;
    if(stCount == ST_INTERVAL) {
      stAvg = stSum / stCount;
      stMax = stMaxTmp;
      stMin = stMinTmp;
      stThresh = ((stMax - stAvg) * VALUE_THRESH_COEF / 100) + stAvg;
      stSum = stCount = 0;
      stMinTmp = VALUE_MAX;
      stMaxTmp = 0;
    }
    if(value > stThresh) {                      // check beat
      countHigh++;
      countLow = 0;
      if(countHigh == VALUE_CONT_BEAT && ! beatHigh) {
        beatCount++;
        beatHigh = true;
        if(soundOn) soundStart(bpm);
      }
    } else {
      countHigh = 0;
      countLow++;
      if(countLow == VALUE_CONT_BEAT) {
        beatHigh = false;
      }
    }
    // update graph/image
    if(mode == MODE_GRAPH) dispProc(D_UPD, valueLast, value, beatHigh);
    if(mode == MODE_IMAGE) imageProc(I_UPD, bpm, beatHigh);
    timeLast += S_INTERVAL;
    valueLast = value;
    break;
  }
}

// Draw graph
#define GRAPH_X_MAX    (320 - 1)
#define GRAPH_Y_OFFSET 40
#define GRAPH_Y_MAX    (240 - GRAPH_Y_OFFSET)
#define GRAPH_SIZING   200
#define GRAPH_BG       BLACK
#define GRAPH_FG       WHITE
void dispProc(int cmd, int vLast, int v, bool flag) {
  static int cursorLast, valueMin, valueMax;
  int cursorNext;

  switch(cmd) {
  case D_INIT:
    M5.Lcd.fillScreen(0);
    M5.Lcd.setBrightness(BRIGHTNESS);
    cursorLast = 0;
    valueMin = 0;
    valueMax = VALUE_MAX;
    break;
  case D_UPD:
    cursorNext = cursorLast + 1;
    M5.Lcd.setTextColor(RED, 0); M5.Lcd.setCursor(0, 0); M5.Lcd.setTextSize(TSIZE_M);
    if(flag) {
      M5.Lcd.print("@");
    } else {
      M5.Lcd.print(" ");
    }
    M5.Lcd.setTextColor(BLUE, 0); M5.Lcd.setCursor(300, 0); M5.Lcd.setTextSize(TSIZE_M);
    if(ambiOn) {
      M5.Lcd.print("A");
    } else {
      M5.Lcd.print(" ");
    }
    // DEBUG
//    Serial.print(valueMin);Serial.print(" ");Serial.print(valueMax);Serial.print(" ");Serial.print(v);Serial.print(" ");Serial.print(disp_pos_y(v, valueMin, valueMax));Serial.print(" - ");
//    Serial.print(v - valueMin);Serial.print(" ");Serial.print(valueMax - valueMin);Serial.print(" ");Serial.print((v - valueMin) * GRAPH_Y_MAX);Serial.print(" ");Serial.println(((v - valueMin) * GRAPH_Y_MAX) / (valueMax - valueMin));
    M5.Lcd.drawLine((int16_t)cursorNext,                // clear next virtical line area
                    (int16_t)GRAPH_Y_OFFSET,
                    (int16_t)cursorNext,
                    (int16_t)(GRAPH_Y_OFFSET + GRAPH_Y_MAX),
                    GRAPH_BG);
    M5.Lcd.drawLine((int16_t)cursorLast,                // draw line
                    disp_pos_y(vLast, valueMin, valueMax),
                    (int16_t)cursorNext,
                    disp_pos_y(v, valueMin, valueMax),
                    GRAPH_FG);
    cursorLast = cursorNext;
    cursorLast %= GRAPH_X_MAX;
    if(cursorLast == (int16_t)0) {
      M5.Lcd.drawLine((int16_t)0,                       // clear 1st virtical line area
                    (int16_t)GRAPH_Y_OFFSET,
                    (int16_t)0,
                    (int16_t)(GRAPH_Y_OFFSET + GRAPH_Y_MAX),
                    GRAPH_BG);
    }
    break;
  case D_BPM:
    int16_t color;
    char buf[20];
    color = (vLast >= THRESH_HIGH_BPM) ? YELLOW : GREEN;
    sprintf(buf, "  BPM: %3d", vLast);
    dispMssg(buf, color, BLACK);
    break;
  }
}

// Subroutine for dispProc()
int16_t disp_pos_y(int v, int min, int max) {
  if(v > max) v = max;
  if(v < min) v = min;
  return((int16_t)((GRAPH_Y_OFFSET + GRAPH_Y_MAX) - (((v - min) * GRAPH_Y_MAX) / (max - min))));
}

// Draw jpg image
void imageProc(int cmd, int bpm, bool beatHigh) {
  static bool lastBeat;
  static int lastImage;
  switch(cmd) {
  case I_INIT:
    M5.Lcd.setBrightness(0);
    M5.Lcd.clear(0);
    lastBeat = false;
    lastImage = 0;
    break;
  case I_UPD:
    if(beatHigh && ! lastBeat) {                    // on or off
      if(bpm >= THRESH_HIGH_BPM && lastImage != 2) {
        M5.Lcd.drawJpg(imageHeart2, imageHeart2Len);
        lastImage = 2;
      }
      if(bpm < THRESH_HIGH_BPM && lastImage != 1) {
        M5.Lcd.drawJpg(imageHeart1, imageHeart1Len);
        lastImage = 1;
      }
      M5.Lcd.setBrightness(BRIGHTNESS);
      lastBeat = true;
    } else if(! beatHigh && lastBeat) {
      M5.Lcd.setBrightness(0);
      lastBeat = false;
    }
    break;
  }
}

// Display text message on top
void dispMssg(const char* mssg, int16_t fg, int16_t bg){
  M5.Lcd.setTextColor(fg, bg);
  M5.Lcd.setCursor(40, 0);
  M5.Lcd.setTextSize(TSIZE_M);
  M5.Lcd.printf("%-14s", mssg);
}

// Beep sound
void soundStart(int bpm) {
  M5.Speaker.tone((bpm >= THRESH_HIGH_BPM) ? BEEP_FREQ_2 : BEEP_FREQ_1, BEEP_TIME);
}