まるぼ実験場

アプリの開発日記を載せるサイトでしたが、ただの技術ブログになりました。

M5StickCでバイブレーションタイマーを作りたい

自分が欲しいタイマー

自分が今やっているバイトは休憩時間が不定で、日によって休憩に入る時間が異なる。
なので、自分で休憩時間を管理する必要がある。
バイトの休憩時間を活用して積み読崩したり勉強したりしていきたいのだが、まぁ、時計が気になって集中出来ないこと。

音が出るタイマーでは他の人の迷惑になってしまうので、バイブレーションで設定時間を伝えるタイマーとかあったら良いんじゃないだろうか、と考えた。たとえば図書館のような時間を忘れがちな環境で使えるタイマー……。
スマートフォンのアラーム機能でもいいと思うが、前述のように毎回時間が異なり、一々設定するのは手間なのでなにか考えてみることにした。

検討した方法

最初はスマホアプリとして作ろうと考えたが、
ウィジェットとしてホームに置けて一回ボタン押しただけで設定できるようにしたい……ウィジェットってどうやって作るんだ?と調べるも手持ちの資料には載っていなくてめんどそうだったのでそこで壁にぶつかり中止。
あとスマホを開くならついでに……とついつい別のことをしてしまいそうなのがよくない。

なので久々の電子工作で行くことにした。

一応作る前に、キッチンタイマーあたりでバイブレーション機能が付いているものが存在しないか確認しに行ってみた。大手ではタニタさんが出しているようだ。コレ良さそう。
手持ちの部品かき集めれば新たに買うより安価にできそう&もっとシンプルなものが良いのと個人的に欲しい機能が無いので自作してみることに。値段によってはここで記事が終わってた可能性。

最初はAVRあたりを使って実装しようかな、と思っていたのだが、
・ワンボタンですぐ使えるようにしたい
・筐体は電源含めて胸ポケットに入るくらい(お菓子ケースくらい)が良い
・残り時間表示用に7セグなどあれば尚良し

……ここまで考えて、よくよく考えれば家にあんまり活用されていないM5StickC(注・うちにあったのはPLUSじゃないほう)があったじゃないか!

M5StickCならば本体にボタンと画面とLEDと内蔵電源があるので、振動モーターだけを調達してポン付けすればほぼほぼ目的を達成できそう。これを使って実装しようと考えた。

振動モーターの調達

一気に完成形が見えてきた。
問題はモーターの調達方法だが、秋月電子とか共立エレショップとかを眺めながら、あー部品一つで送料かかるのめんどくさいなー日本橋行きてぇなーなどと呟きながら、
このときの自分は何を思ったのか、壊れた携帯のバイブレーションに使われる部品を再利用できないかと考えていた。
星形ネジや接着剤と格闘すること小一時間……。


どう見ても秋月にある部品(通販コード:P-06784)を発見!60円と送料浮いた!ラッキー!

プログラミング

振動モーター自体の使い方はシンプルで、G26とGNDに繋ぎ、G26から3Vの電気を流している間バイブレーションする。
プログラムの流れは、時間を設定しているとき、設定時間になって電気流してるとき、タイマーで待機してるとき、という状態に分けて考えた。
下にソースコード。PLUSじゃないほうのM5StickC用です。

#include <M5StickC.h>

#define uS_TO_S_FACTOR 1000000ULL  /* Conversion factor for micro seconds to seconds */

RTC_TimeTypeDef RTC_TimeStruct;

enum State{
  STARTSETTING,
  READY,
  VIBRATION,
  TIMECHECK,
};
enum State state_mode = READY;
uint64_t Timer_min = 1;
long lefttime;
unsigned long startMillis,ElaspledMillis,nowMillis;
int wait_lefttimedraw_time = 3000;
int wait_vibrationswitch_time = 2000;
int vibration_count = 0;

void setup() {
  M5.begin();
  M5.Lcd.setRotation(3);
  M5.Axp.ScreenBreath( 12 );
  M5.Lcd.fillScreen(BLACK);
  esp_sleep_wakeup_cause_t wakeup_reason;
  wakeup_reason = esp_sleep_get_wakeup_cause();

  switch(wakeup_reason){
    case ESP_SLEEP_WAKEUP_EXT0:   // deepsleepから起動
      state_mode = TIMECHECK;
      startMillis = millis();
      break;
    case ESP_SLEEP_WAKEUP_TIMER : // タイマー指定時間が経った
      state_mode = VIBRATION;
      pinMode(M5_LED, OUTPUT);
      pinMode(26, OUTPUT);
      startMillis = millis();
      vibration_count = startMillis;
      break;
    default:                      // 電源OFFから起動
      // 初期時刻セット
      RTC_TimeTypeDef TimeStruct;
      TimeStruct.Hours   = 1;  // Set the time.
      TimeStruct.Minutes = 00;
      TimeStruct.Seconds = 00;
      M5.Rtc.SetTime(&TimeStruct);
      state_mode = STARTSETTING;
      startMillis = millis();
      break;
  }
}

void loop() {
  M5.update();
  nowMillis = millis();
  ElaspledMillis = nowMillis - startMillis;
  
  switch (state_mode){
    case READY :
      // 実装予定
      break;
    case STARTSETTING :
      M5.Lcd.setCursor(10, 0, 2);
      M5.Lcd.printf("Timer Set: %02d min", Timer_min);
      
      if(ElaspledMillis > wait_lefttimedraw_time){
        // タイマーセット
        pinMode(GPIO_NUM_37, INPUT_PULLUP);
        esp_sleep_enable_ext0_wakeup(GPIO_NUM_37, LOW);
        esp_sleep_enable_timer_wakeup(SLEEP_MIN(Timer_min));
        
        // ディープスリープ開始
        M5.Axp.SetSleep();
        esp_deep_sleep_start();
      }
      break;
    case VIBRATION :
      M5.Lcd.setCursor(40, 0, 2);
      M5.Lcd.println("it's time!!");
      
      // バイブレーション&LED点灯開始
      if(ElaspledMillis - vibration_count < wait_vibrationswitch_time){
        dacWrite(26, 255);
        digitalWrite(M5_LED, LOW);
      }else if(ElaspledMillis - vibration_count < wait_vibrationswitch_time * 2){
        dacWrite(26, 0);
        digitalWrite(M5_LED, HIGH);
      }else{
        vibration_count = ElaspledMillis;
      }
      
      if(M5.BtnA.wasReleased()){
        // ボタンが押されたとき
        state_mode = READY;
        digitalWrite(M5_LED, HIGH);
        M5.Lcd.setCursor(40, 0, 2);
        M5.Lcd.println("Timer Stop\n");
        delay(1500);
        M5.Axp.PowerOff();
      }
      break;
    case TIMECHECK :
        // 残り時間を表示
        M5.Rtc.GetTime(&RTC_TimeStruct);
        lefttime = (Timer_min * 60) - (RTC_TimeStruct.Minutes * 60) - RTC_TimeStruct.Seconds;

        M5.Lcd.setCursor(20, 0, 2);
        M5.Lcd.printf("Left Time: %02d", lefttime);

        // 表示時間終了後、再スリープモードに入る
        if(ElaspledMillis > wait_lefttimedraw_time){
          pinMode(GPIO_NUM_37, INPUT_PULLUP);
          esp_sleep_enable_ext0_wakeup(GPIO_NUM_37, LOW);
          esp_sleep_enable_timer_wakeup(lefttime * uS_TO_S_FACTOR);
          // ディープスリープ開始
          M5.Axp.SetSleep();
          esp_deep_sleep_start();
        }
        break;
      default:
        break;
  }
}

(電源を入れる)
時間を設定
タイマースタート
スリープ状態へ移行
設定時間後、スリープを解除しバイブレーションする。
ボタンを押すとバイブが止まる
(電源を切る)
以上がメインループとなっており、
スリープ中にボタンを押すことで残り時間を表示する機能を付けている。
電池の節電のことを考えてディープスリープを使ってみた。
長時間を想定したタイマーなので、なるべくRTC(内蔵時計)を使ったほうが良いのかな?と考え実装してみたが、スリープ復帰時にタイマーを再計算する事によってズレたりとか精度はどうなのかちょっとよく分からない。

改善・確認したいところ

・長時間の精度はどんなものなのか実際の休憩時間に使って確認したい。ボタンを押すたびにスリープ時間を再設定しているので、繰り返すとバグったりしやしないかと。
・実験中に振動モーターの足が折れてしまったので結局実装できず。本体への固定方法も考える必要がある。
・5min、10min、60minから選べる時間設定機能を付けたい。
・本体設定の時刻を起動時にリセットしてしまっているので、実際の時刻を入れられるようにして時計としても使えるようにしたいところ。(計算式の変更が必要になりますね……)

あとはスリープ状態への移行は任意のほうがいいかも?と思ってたり。
丸々1日+αでM5Stickと格闘してなんとか欲しいものと近いものが形になったと思う……(休日一日で済んで良かったなホント)