私もM5Stack心拍モニタを作って見ました
私もM5Stack心拍モニタを作って見ました ^_^
当初は脈にあわせてVUメータの針が振れるものを作って、それを胸のあたりに装着し、ドキがムネムネしてるのがわかるようにしようと思ってたのですが、M5Stackを入手したので、Arduino IDEを使ってディスプレイにハートマークを点滅させて簡単に見栄えのするものを作って見ました。
注意:あくまでおもちゃなので、医療用には使えません(^_^)
動画
構成
- M5Stack
- 心拍センサ Puslsesensor.com
- microSDカード
特徴
- 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
操作
- Aボタン
グラフモードと画像モードを切り替えます。 - Bボタン
Ambientへの計測データの送信をON/OFFします。 - Cボタン
脈にあわせたビープ音をON/OFFします。
心拍センサとM5Stackの接続
- 黒ケーブル → GND
- 赤ケーブル → 3V3
- 紫ケーブル → 36
センサ部分は指や耳たぶに装着するものらしいですが、私の場合は掌の親指の下の膨らんでいるあたりがうまく測れました。
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); }