Arduino+Groveシステムでつくる フラフープカウンター

最近お腹周りが若干だらしなくなってきたので、ダイエットを始めることにしました。といっても、ヘビィなことをするワケでは全然なくて、普段の食事で気持ち脂質を避ける、日々の運動をちょっとプラスする、ぐらいのものです。

やっている運動としては、毎日20分程度のウォーキングと、それからもっとお腹に直接ダメージを与える(?)手段として、フラフープを導入することにしました。

これです。購入3日目にして踏んづけて破壊するという愚行を犯してしまったので、現在利用しているのは改めて買い直したものです。

… いいんです、次壊れたときのスペアパーツが5個も残っていると思えば(←このフラフープは6個のパーツをつなぎ合わせるタイプ)。フラフープはちゃんと部屋の隅に立てかけて保管しておきましょう……

 

さて、フラフープをやっていると腹筋と背筋が鍛えられるし、それなりに汗もかくのでとても良いのですが、延々回しているのは若干退屈です。慣れればフラフープしながらギターも弾けるようですが、自分はまだそこまで慣れていないので、せいぜいテレビやらビデオやらを観るしかありません。

また、10分20分達成、というのが、個人的にはあんまり嬉しくなく。10分もやっていれば結構な数を回しているハズなので、回数がわかる方がモチベーションになりやすいのではないか、と。「30分回したぜ!」より、「5000回まわしてやったぜ!」の方が、自分としては達成感があるのです。

でも、そんなものイチイチ数えるは大変面倒だし、ほぼ確実に「あれ、今何回だっけ?」となるでしょう。おちおちテレビも観てられません。

 

というわけで、フラフープの回数を自動的にカウントしてくれるマシーンを作ることにしました。要件はおよそ以下のような感じで。

  • フープを回し始めると自動的に計測スタート
  • フープを回し終わると自動的に計測ストップ
  • 1回転を検出すると、LEDが一瞬点灯
  • 達成回数の目安を音でお知らせ
  • できれば非接触で計測
  • 多少の測定誤差は許容

 

ではでは、さっそく。使う部品は以下です。

Arduino本体。必要に応じて、ケースとかもあった方がよいかも。

サクッと作ることを優先するため、Groveシステムを使います。使うのは、このセットの中のGroveシールド、ケーブル、LCDディスプレイ、LEDです。

このセットには音を鳴らすためのブザーも含まれているので、これを使えば簡単に音を出すことができます。が、自分のセットに入っていたものはあまり音が綺麗ではなかったので、手元にあったこっちの圧電ブザー使いました。音にこだわらなければ、セットの中のブザーを使うと良いと思います。

それから、肝心のフープの回った数をカウントするためのセンサとして、超音波距離センサを使うことにします。以下のようにしてカウントします。

①設置

hoop-1

②デフォルト値計測

hoop-2

③距離変化検知

hoop-3

こんな感じです。

ちなみにモーションセンサやら色々試してみたのですが、最終的に距離センサを使うことに落ち着きました(モーションセンサの場合、反応速度がフープの回転速度に追いつかなかったのです)。

 

では、これらのパーツを実際に組み上げていきます。

th_hoop-6

LCDディスプレイをGroveシールドのI2C、LEDをD7、超音波距離センサをD4に接続しています。圧電ブザーはD12とGNDに直挿しです。

th_hoop-7

余ったケーブルは、Groveシールドのコネクタの間を這わせたり、シールドとArduino本体の間に押し込むなりしてまとめます。

ハードのセットアップは以上です。簡単。Groveシールドの供給電力切り替えスイッチを5.0Vにしておくことだけ注意が必要です(3.3Vだと、LCDが表示されません)。

 

さて、あとはカウントするためのソースを書いていくだけですが、ひとつ注意しなくてはいけないのが、センサの上をフープが通過する際の、通過の仕方です。

hoop-4

こんな感じで、フープが1回転する間にセンサの上を1回通過するのであれば話は早いのですが、これだと自分と距離センサの距離を確実に一定に保たねばならず、あまり現実的ではありません。

hoop-5

実際には、こんな感じで2回通過することの方が多いハズです。でも、毎回必ず2回通過するかというと、そうとも言い切れません。回している間に、たまたま1回しか通過しないこともあるでしょう。

というわけで、距離変化の回数と回転数のカウントの対応づけには注意が必要です。自分の場合、フープの1回転にかかる時間がだいたい0.5秒ぐらいだったので、「一度距離変化を検出したら、そこから0.25秒以内の距離変化は無視する」というルールにすることで対応しました。つまり、「1回転でフープが2回通過する場合は、その間の時間間隔は0.25秒もかからないであろう」(=0.25秒以内に起こった距離変化は、フープが1回転しているうちの2回目の通過であろう)と決めつけているのです。シンプルなルールですが、これで一応「1回の距離変化=1回転」と対応づけることができました。

以下、ソースです。

#include <Wire.h>
#include "rgb_lcd.h"
#include "Ultrasonic.h"
#include "pitches.h"

rgb_lcd lcd;

const int colorR = 64;
const int colorG = 64;
const int colorB = 64;

// 以下、0.5秒でフープ1回転を基本に考える

const int LOOP_DELAY = 10;
const int ONE_UP_COUNT   = 1000;
const int COIN_GET_COUNT = 100;
const int MEASURING_STOP = 3000; // 3秒間カウントされないと、計測を止める

// 1度検出したら、250msは検出を無効にする
const int HOOP_DETECT_INTERVAL = 250;

const int COUNTUP_MARGIN = 80;
const int TIME_INDEX  = 6;
const int COUNT_INDEX = 6;

unsigned long current_millis;
unsigned long before_millis_time;
unsigned long before_millis_detect;
unsigned long before_millis_stop;
int hour   = 0;
int minute = 0;
int second = 0;
char char_time[9]; // "0:00:00 ¥0" の入れ物

const int SPEAKER_PIN = 12;
const int LED_PIN     =  7;
Ultrasonic ultrasonic(4);

long RangeInCentimeters;
int  default_value = 0;
int  hoop_count = 0;
bool isMeasuring = false;
bool isDetected  = false;

void mario_1up(){
  // 全体で1秒になるように設定
  tone(SPEAKER_PIN,NOTE_E6,125);
  delay(130);
  tone(SPEAKER_PIN,NOTE_G6,125);
  delay(130);
  tone(SPEAKER_PIN,NOTE_E7,125);
  delay(130);
  tone(SPEAKER_PIN,NOTE_C7,125);
  delay(130);
  tone(SPEAKER_PIN,NOTE_D7,125);
  delay(130);
  tone(SPEAKER_PIN,NOTE_G7,125);
  delay(125);
  noTone(SPEAKER_PIN);
  delay(225);
}

void get_coin(){
  // 全体で1秒になるように設定
  tone(SPEAKER_PIN,NOTE_B5,100);
  delay(100);
  tone(SPEAKER_PIN,NOTE_E6,850);
  //delay(800);
  delay(500);
  noTone(SPEAKER_PIN);
  delay(400);
}

void lcd_line_clear(int line){
  lcd.setCursor(0,line);
  lcd.print("                ");
  lcd.home();
}

void update_time(unsigned long now){
    if(now - before_millis_time > 1000){
      second++;
      before_millis_time = now;
    }
    if(second == 60){
      second = 0;
      minute++;
    }else if(second == 61){
      second = 1;
      minute++;
    }
    if(minute == 60){
      minute = 0;
      hour++;
    }

    sprintf(char_time, "%d:%02d:%02d ", hour, minute, second);
    lcd.setCursor(TIME_INDEX, 0);
    lcd.print(char_time);
}

bool detect_hoop(long RangeInCentimeters, unsigned long now){
  if(now - before_millis_detect > HOOP_DETECT_INTERVAL){
    if(RangeInCentimeters < (default_value-COUNTUP_MARGIN)){
      before_millis_detect = now;
      return true;
    }else{
      return false;
    }
  }else{
    return false;
  }
}

void setup() 
{
  Serial.begin(57600);

  pinMode(SPEAKER_PIN, OUTPUT);
  pinMode(LED_PIN, OUTPUT);
  digitalWrite(SPEAKER_PIN, LOW);
  digitalWrite(LED_PIN, LOW);

  // set up the LCD's number of columns and rows:
  lcd.begin(16, 2);
  lcd.setRGB(colorR, colorG, colorB);

  // Print a message to the LCD.
  lcd.print("Ready");

  delay(3000);
  // デフォルト値をここで計測しておく
  int temp_value = 0;
  int display_index = 4; // "Ready"の"y"の位置
  for(int i=0;i<20;i++){
    temp_value += ultrasonic.MeasureInCentimeters();
    delay(100);
    if(i%2 == 0){
      display_index++;
      lcd.setCursor(display_index,0);
      lcd.print(".");     
    }
  }
  default_value = temp_value/20;

  lcd_line_clear(0); // 0行目をクリアする
  lcd.print("OK! Default:");
  lcd.print(default_value,DEC);

  delay(3000);
  lcd.clear();
  lcd.setCursor(0,0); // カーソルを左上に
  lcd.print("Time  0:00:00");
  lcd.setCursor(0,1); // カーソルを左下に
  lcd.print("Count 0");
}

void loop() 
{
    current_millis = millis();

    if(isMeasuring){
      update_time(current_millis);
    }

    RangeInCentimeters = ultrasonic.MeasureInCentimeters();
    Serial.print(RangeInCentimeters);
    Serial.print(",");
    isDetected = detect_hoop(RangeInCentimeters, current_millis);

    if(isDetected){
      // フープの1回転を検出したら
      if(isMeasuring == false){
        // 1回フープが回ると、計測スタート
        isMeasuring = true;
      }

      // フープカウントの更新
      hoop_count++;
      lcd.setCursor(COUNT_INDEX,1);
      lcd.print(hoop_count,DEC);

      // 達成回数に応じてサウンドを鳴らす
      // サウンド再生に1秒かかるので、ここで1秒分をカウントアップしておく。
      // また、サウンド再生中に2回回すと仮定して、2回分もカウントアップしておく。
      if(hoop_count%ONE_UP_COUNT == 0){
        mario_1up();
        second++;
        hoop_count += 2;
      }else if(hoop_count%COIN_GET_COUNT == 0){
        get_coin();
        second++;
        hoop_count += 2;
      }else{
        digitalWrite(LED_PIN, HIGH);
        delay(LOOP_DELAY);
        digitalWrite(LED_PIN, LOW);
      }

      // 計測停止用タイマーのリセット
      before_millis_stop = current_millis;
    }

    // 計測停止用チェック
    if(current_millis - before_millis_stop> MEASURING_STOP){
      isMeasuring = false;      
      // 正確には表示をMEASURING_STOPミリ秒戻すべきだが、時間計算が面倒なので、この時間は誤差扱いにする
    }

    if(!isDetected){
      // LED発光分のdelayの調整
      delay(LOOP_DELAY);
    }
}

冒頭で、LCDと超音波距離センサを使用するためのヘッダファイルをincludeしています。これらは、こちらにある”libraries/Grove_LCD_RGB_Backlight”と、こちらにあるファイルをまるっと自身のArudino IDEのlibraliesフォルダにコピーすると使えるようになります。

また、”pitches.h”は、Arduino IDEのサンプル”toneMelody”などで使われている”pitches.h”をそのまま利用しています。これは、フープの回転数が特定の回数に達したときにメロディを鳴らすためのものです。今回は、100回達成でマリオのコインのゲット音(coin_get関数)、1000回達成でマリオの1UP音(mario_1up関数)を鳴らすようにしています(厳密には10回でコインゲット、1000回で1UPが正しいと思いますが、さすがに10回ごとに音が鳴るとやかましいので)。

これらの8bitサウンドのプログラムは、以下を参考にさせていただきました。

プログラムの仕様上、これらのサウンドを鳴らしている間、フープの回転検出機能が一時停止してしまうため、サウンドを鳴らしている間(1秒)の回転数は2回とみなしてカウントアップするようにしています。

ということで、厳密なカウントはできませんが、何回か使用してみたところ、だいたい誤差±10%ぐらいでカウントできているようなので、自分としてはこれでOKとしています。正確な数勘定が目的ではなく、あくまでモチベーションアップが目的なので。

で、実際に使ってみた様子がこちらです。

映っていませんが、すぐ横で本人が頑張ってフープを回しております。

 

というわけで、珍しく、そこそこ役に立ちそうなものができました。同じような仕組みで腕立て伏せカウンターとかも作れそう(←アゴとセンサの間の距離変化を検出)なので、もう少し欲張って、多機能なフィットネス器具に仕立て上げてもよいかもしれません。

 

今回、フラフープを導入する契機となった本です。別にナイスバディになりたかったから読んだのではなく、単純に柴田亜美先生のファンなのです。『未来冒険チャンネル5』最高。