Riho Yamamoto
Published

Anytime! Perfect Deep Breathing

This device visualizes your real-time breathing against an ideal pace, helping you easily build a healthy habit of deep breathing.

BeginnerFull instructions provided46
Anytime! Perfect Deep Breathing

Things used in this project

Hardware components

ESP32 Basic Core IoT Development Kit
M5Stack ESP32 Basic Core IoT Development Kit
×1
Sensor: Diode Respiration Sensor (for GROVE terminal - RESP02)
https://norulab.base.shop/items/39415492
×1

Software apps and online services

Arduino IDE
Arduino IDE

Story

Read more

Schematics

image

Code

deepBreathing

Arduino
#include <M5Stack.h>

// 定数定義
#define SPEAKER_PIN    25       //M5Stackの内蔵スピーカーのピン番号
#define MAXADRANGE   4096       //センサーから送られてくるアナログ値(AD値)の最大範囲(12ビット = 0〜4095)
#define DELIMITCODE  0x0a       // センサから送られてくるデータの区切り文字(0x0a:改行コード(LF))1回分のデータ送信の終わり.データの途切れの判断

#define SWEEPMAG    5           //描画を間引くための値.データが送られてくるたびに描画すると速すぎる.3回に1回に調整
#define LCDWIDTH    320           //画面横幅
#define LCDHEIGHT   240         //画面縦幅

// センサへ送る設定変更コマンド「RG」・基準値をリセットするコマンド「BR」を、プログラムが判別しやすいように数値化
#define COMMAND_RG      ('R'*256+'G')       // AD Range Mag         'RGX'   X<-1-8 (Default4)
#define COMMAND_BR      ('B'*256+'R')       // Base Level Reset     'BR' 

TFT_eSprite canvas = TFT_eSprite(&M5.Lcd); 

// 変数の宣言
// unsigned long: マイナスを含まない大きな数値を扱えるデータ型。
static  unsigned long chktime;// 時間経過をチェックするためのタイマー用変数
static  unsigned long nowtime; // 現在の時間を記録する変数

static  int   sweepptr = 0; //データを何回受け取ったかを数えるカウンター(3回に1回描画)

// static  int   lastY = 0; // 直前のY座標(高さ)グラフの線
// static  int   posx = 0; // 現在のX座標(横位置)グラフの線
// static  float fConv;  // センサの値を画面ぴったりに表示するための計算用の倍率

static  int gain; // センサの感度設定の数値
static  int chkgain; // 設定した感度が、センサ側に正しく反映されたかを確認するための変数

// ↓↓↓ ここから変更 ↓↓↓
// 動的キャリブレーション:変数の追加

static float dynMin = 2000; // 動的な最小値(最初は適当な中央値)
static float dynMax = 2100; // 動的な最大値
static unsigned long lastDecayTime = 0; // 最後に減衰(忘却)させた時間

// ↑↑↑ ここまで ↑↑↑


void setup() {
    M5.begin();
    dacWrite(SPEAKER_PIN, 0); // Speaker OFF
    M5.Lcd.fillScreen(BLACK); // 背景
    M5.Lcd.setTextColor(GREEN); // 文字色
    M5.Lcd.setTextSize(4);    // 文字サイズ
    Serial2.begin(19200, SERIAL_8N1, 21, 22);       // GROVE端子(ピン21番と22番),通信速度19200
    Serial2.setTimeout(10); // データの待ち時間を10ミリ秒で打ち切る
    canvas.setColorDepth(8); // 色深度ピクセルあたりに使用するビット数を指定。8ビット:256色
    canvas.createSprite(320, 240); // スプライト(仮想描画領域)の作成。最後にpushSprite(0, 0); で画面の指定座標に一括出力

    //  EchoBack(送ったコマンドの返事をセンサに求める)の有効化
    // 10回求めて動作するようにする
    for (byte i=0; i<10; i++){       
      Serial2.print("@EB1");     // EchoBack の有効コマンド
      Serial2.write(0x0a);
    }
    gain    = 2; //センサの感度の目標値
    chkgain = 0; // 感度の確認用変数のリセット
    // センサに向けて、感度変更コマンド(@RG)と基準値リセットコマンド(@BR)を送信
    Serial2.print("@RG");         
    Serial2.print(gain);          
    Serial2.write(0x0a);
    Serial2.print("@BR");         
    Serial2.write(0x0a);
    chktime  = millis()+100; // 100ミリ秒後に、コマンドが正常に反映されたかチェックするタイマー。millis(): 起動時からの経過時間をミリ秒単位で返す関数。
    // fConv = ((float)LCDHEIGHT/2-20)/(float)MAXADRANGE;  //センサの値を円の最大半径(100)に合わせる倍率
}

void loop() {
// 変数の宣言
  String   strInput;  
  unsigned  int command;
  int val;
  // int posy;
  static int xr = 0;

  unsigned long currentTime = millis(); // M5Stackが起動してからの時間を取得(ミリ秒単位)
  unsigned long cycleTime = currentTime % 12000; // 12秒(12000ミリ秒)周期のループを作る (0 ~ 7999の間を繰り返す)
  int r; // 円の半径

  if (cycleTime < 4000) {
    // map(value, fromLow, fromHigh, toLow, toHigh)
    // map関数(変換したい元の値、元の値の最小値、元の値の最大値、変換後の最小値、変換後の最大値)
    
    // 【最初の4秒】 0〜4000ms の時間を、半径 0〜100 に変換 (大きくなる)
    r = map(cycleTime, 0, 4000, 0, 100);
  } else {
    // 【次の8秒】 4000〜12000ms の時間を、半径 100〜0 に変換 (小さくなる)
    r = map(cycleTime, 4000, 12000, 100, 0);
  }


  // ↓↓↓ ここから変更 ↓↓↓
  // 動的キャリブレーション:忘却処理

  // 50ミリ秒ごとに、MaxとMinを少しずつ内側(中央)に寄せていく
  if (currentTime - lastDecayTime > 50) {
    dynMax -= 2.0; 
    dynMin += 2.0; 
    // 範囲が狭くなりすぎないよう保護(最低でも幅300はキープ)
    if ((dynMax - dynMin) < 300) {
      float center = (dynMax + dynMin) / 2.0;
      dynMax = center + 150;
      dynMin = center - 150;
    }
    lastDecayTime = currentTime;
  }
  // ↑↑↑ ここまで ↑↑↑

// データの受信と仕分け・測定値を反映した円の描画
 if(Serial2.available() > 0) { // センサから新しいデータが届いているか確認
    strInput = Serial2.readStringUntil(DELIMITCODE); // 改行コードが来るまでデータを読み込み、文字として保存
    switch(strInput[0]){ // 受け取った文字の1文字目でデータの内容を判断
      // 1文字目が「@」の場合:センサからの「コマンド受け取ったよ」という返事(EchoBack)
      case '@':       // Echo Back Commandがきたらセットする #2020/08/17
        command = 256*strInput[1] + strInput[2]; // 返事の内容を解析し、どのコマンドに対する返答かを特定
        // 感度設定(RGコマンド)への返事だった場合、設定が完了した印として chkgain を更新
        switch(command){
          case COMMAND_RG:
            chkgain = gain;
            break;
        }
        break;    
      // 円の描画(数値データだった場合)
      default: // 「@」以外だった場合、呼吸センサの数値データとして処理
        sweepptr++; // データを受け取った回数
        if (sweepptr>=SWEEPMAG){ // カウントが3(SWEEPMAG)になった時だけ、以下のグラフ描画処理を実行
          sweepptr = 0; // カウントのリセット
          val = strInput.toInt(); // 文字のデータを計算できるように整数の数値に変換(str型→int型)

          // ↓↓↓ ここから変更 ↓↓↓
            // // 呼吸で動く範囲にしたい、試しに1500~2500
            // val = constrain(val, 1500, 2500);
            // xr = map(val, 1500, 2500, 0, 100); 

          // 動的キャリブレーション:更新と反映
          if(val > 0 && val <= MAXADRANGE) { // 異常なノイズを弾く
            if (val > dynMax) dynMax = val; // 新しい最大値を記録
            if (val < dynMin) dynMin = val; // 新しい最小値を記録
            
            // 動的に変化するMinとMaxを使って計算
            xr = map(val, (int)dynMin, (int)dynMax, 0, 100);
            xr = constrain(xr, 0, 100); // 念のため0〜100に収める(安全対策)
          }

          // ↑↑↑ ここまで ↑↑↑

          // if (val >= MAXADRANGE/2) {
          //   // 【吸う】 2048〜4095の値を、半径 0〜100 に変換 (大きくなる)
          //   xr = map(val, MAXADRANGE/2, MAXADRANGE, 0, 100);
          // } else {
          //   // 【吐く】 0~2048の値を、半径 100〜0 に変換 (小さくなる)
          //   xr = map(val, 0, MAXADRANGE/2, 100, 0);
          // }
          
          // // 次に波形を描く縦1列だけを黒い線で塗りつぶし、画面右端まで行った時に古いグラフを消す 
          // M5.Lcd.drawLine(posx, 0, posx, LCDHEIGHT-1, BLACK);
          // // センサの値を先ほど作った倍率(fConv)で掛け算し、画面上のY座標(高さ)を計算.画面の下が0になるよう引き算
          // posy = LCDHEIGHT - (int)((float)val*fConv);
          // // 直前の位置(lastY)から現在の位置(posy)に向かって、白い線を引いて波形を作る
          // if (posx > 0) {
          //   M5.Lcd.drawLine(posx-1, lastY, posx, posy, WHITE);
          // }else{
          //   M5.Lcd.drawLine(posx, lastY, posx, posy, WHITE);
          // }
          // lastY = posy; // 次の線を引くために、今回の高さを記憶
          // posx++; // グラフを横に進めるため、X座標を右に1つずらす
          // // X座標が画面の右端(320)に到達したら、左端(0)に戻して最初から描き直す
          // if (posx >= LCDWIDTH) posx=0;
        }
        break;
    }
  }

  // 描画処理(黒、グレー、緑)
  canvas.fillSprite(BLACK); // Sprite領域の塗りつぶし
  // canvas.fillCircle(160, 120, r, WHITE);
  canvas.fillCircle(160, 120, r, DARKGREY);
  canvas.drawCircle(160, 120, xr, GREEN);     // 基準の円

  // 2パターン目の色組み合わせはちょい見にくいかも
  // canvas.fillSprite(NAVY); // Sprite領域の塗りつぶし
  // canvas.fillCircle(160, 120, r, CYAN);
  // canvas.drawCircle(160, 120, xr, GREEN);     // 基準の円

  // 3ピクセルの太さにしたかったけどフリーズしたからいったんなし
  // if (xr > 0) {
  //   canvas.drawCircle(160, 120, xr - 1, GREEN); // 1ピクセル内側の円
  // } // 半径が0より大きい時だけ内側を描く
  // canvas.drawCircle(160, 120, xr + 1, TFT_GREEN); // 1ピクセル外側の円
  canvas.pushSprite(0, 0);
  
  // マイコンの負荷を下げるためのわずかな休憩(アニメーションのスピードには影響なし)
  delay(5); 

//-- EchoBackが来たかを確認(通信エラーのリカバリー処理)
  nowtime  =  millis(); // 現在の時間を取得
  // setup()で仕掛けた「100ミリ秒」が経過したか確認
  if (chktime>0 && nowtime>=chktime){
    chktime = 0;
    // センサからの返事が来ておらず、設定が完了していないか確認
    // 設定が完了していなければ、何度でも感度設定コマンドを再送信して、確実に動作させる
    if (chkgain != gain){
      Serial2.print("@RG");           // Gain=2  4->2
      Serial2.print(gain);            // Gain=2  4->2
      Serial2.write(0x0a);
      chktime = nowtime+100;
    }
  }
//--



    
}

Credits

Riho Yamamoto
1 project • 0 followers

Comments