OGIMOノート

6歳の娘と、4歳の息子(脳性麻痺)を持った父親エンジニアの備忘録。自作の電子工作おもちゃ/リハビリ器具の製作記録、勉強記録を残していきます

【リハビリおもちゃ】息子のための電子工作ピタゴラ装置α0 with M5Stack

今回の学び技術: 距離計測ToFセンサ、NeoPixel LED制御

はじめに(作成動機)

きっかけは、8月のOGIMO家族会議。
(OGIMO家は今年から毎月家族会議を実施して、議事録に各自の目標を書いていってます)

各自の目標計画を立てる際に
「父ちゃんの電子工作モノづくりと、
 母ちゃんの手芸作り&絵描き、娘のお絵かき、このコラボ作品を作りたいね」

というのがきっかけ。すぐに思いついたのは、息子が喜んでくれる様なおもちゃ作り。

どんなおもちゃを作るか悩んだ結果、「OGIMO版ピタゴラスイッチ装置」を作ろうと決めました!

・ 理由① ピタゴラスイッチが超ーーーだいすき!
息子は毎日全録で貯まっている2週間分のピタゴラスイッチを見るのが日課になるくらい大好き。コロコロとボールが転がってくるのを見るのがたまらなく好きな模様です!
 
・理由②  おうちリハビリの促進
息子氏は脳性麻痺で体のバランスが取れず、まだ立位も取れないのですが、その要因の一つが背中の筋肉が収縮して丸まってしまう事なのです。なので、背中の筋肉を伸ばす=腕を思いっきり伸ばす動作を、毎日繰り返す事が大事。しかし、子供にとって楽しくない動作を毎日する事ほど苦痛はない。なので、今回ピタゴラ装置に手を伸ばす操作を通じて、楽しく手を伸ばす仕組みを作りたいと考えました。

・理由③ 保育園の先生のやさしさ 
上記②の様な話をしていたら、保育園の担任の先生が夏休みを利用して息子のためにボールころころ器を作ってくださったのです!息子大興奮! それからというもの保育園に入ると嬉しそうに笑い、飽きずにずっと遊んでるそうです!
f:id:motokiinfinity8:20180922215320j:plain
そんな姿を見て、「これを我が家でも導入したい!」と感じました。

完成品

完成品はこちら
f:id:motokiinfinity8:20180922175142j:plain

ピタゴラ大好きな息子トモカツのために家族全員で作った(リハビリ用)電子工作ピタゴラ装置α1。名付けて「トモカツスイッチα1」
ボールを転がすとLEDが光り、最後まで転がるとM5stackにてピタゴラフィニッシュ!
デザインは、妻&娘による手作り。図工と電子工作のあたたかいコラボです。妻の上手なイラストと、娘画伯の独特なイラストセンスか面白い!


そして、息子の反応!

早速、今朝に見せてみたのですが、ものすごい食い付きぶり!
楽しそうに「きゃっはー!!」と声を揚げながら喜んでくれてる。。。
父ちゃんやみんな、頑張ったかい、あるよ。そんだけ喜んでくれたら、なんだか目頭が熱くなる……

設計構想

では、順番に中身について触れていきます。
まずは、妻と共に構想設計を実施!

f:id:motokiinfinity8:20180922175625j:plain

ギミックとしては、息子の好きな光物(LED点灯)とピタゴラスイッチ音の組み合わせ。

【ギミック1】ボール転がりに従ってLEDも順番に光っていく
【ギミック2】最終段まで落ちると「音」と「ピタゴラスイッチ画像」でピタゴラフィニッシュ!


どんだけ凝ったことをしても息子が気に入らなければ意味がないので、まずはこの最低限の機能を付けていきます。
息子の視線を出来るだけ上げたいので、LED等は比較的上側に搭載。

そんな感じで、4段構成でじわじわ試行錯誤していたら、、、
息子が作成途中にも関わらずさっそく遊び始めてしまった!(笑)

f:id:motokiinfinity8:20180922173527j:plain

これは完成を急がねば、という事で、予定変更!!
下2段の完成を最優先に、まずは日程優先で取り組みを開始しました!

ハードウェア構成

f:id:motokiinfinity8:20180922175638j:plain

レール作成

①背面
段ボールをホームセンターで購入。これに画用紙を張り付ける。
(子供でも簡単に加工できる様にするため)
f:id:motokiinfinity8:20180922173434j:plain


②レール上段
同様にホームセンターで雨どいを購入。
軒とい(ハイ丸) 2700mm パナソニック(Panasonic) 雨どい 【通販モノタロウ】 KQ4124〜
これにドリルで穴をあけて麻紐で段ボールに括り付ける。
(子供がおもいっきり引っ張っても取れない様にするため)


f:id:motokiinfinity8:20180922213440j:plain



③レール下段
2ℓペットボトルを切って加工。同様にドリルで裏穴をあけて麻紐で段ボールに括り付ける。


f:id:motokiinfinity8:20180922213449j:plain



④上段のボール検知センサ
雨どいの先端に距離測定モジュールを接続。センサーを雨どいの先端に付ける事で一つのセンサーのみで済みます。センサーはレーザー式測距センサーVL53L0Xを使用。
VL53L0X TOFレーザー測距センサモジュール - VL53L0X - ネット販売

指向性が高く(10度以下)、測定精度が高いこと、レール距離が45cmあるため、こちらを使用しました。

しかし、指向性が高いとはいえ、センサーをしっかりレーザー方向と並行に設置しないと正しく測定できないため、今回は雨どいにはまる様な固定冶具を3Dプリンタで測定しました
雨どいの3Dデータはなかったので、気合いで実測して合わせこみました(笑)


f:id:motokiinfinity8:20180922213502j:plain




⑤下段のボール検知センサ
上段と同じく上記レーザー式測距センサーの近距離版VL6180を使いました。
VL6180X TOF近距離センサモジュール - VL6180X - ネット販売

別にVL53L0Xでも構いませんが、ここでの用途は15cm以下の検出で問題なかった事、そして単に自分がいろんなセンサーを使ってみたいという興味(笑)からあえて、異なるセンサーを使いました。
こちらは、ペットボトルの底にドリルで穴をあけて固定しました。
f:id:motokiinfinity8:20180922181650j:plain

⑥LEDテープ
雨どいに並行に張り付ける想定でしたが、テープが雨どいのコーティングのせいで全く粘着テープが張り付かないので、雨どいとLEDテープの両者を固定する冶具を3Dプリンタで作成しました。

f:id:motokiinfinity8:20180922211700j:plain


⑦M5Stack
見た目をよくするために段ボールに埋め込む様にしました。プロトモジュールを2枚重ね、基板は半分にカット。これにより配線はすべて後ろに隠れるので見た目も安心!


f:id:motokiinfinity8:20180922211730j:plain
f:id:motokiinfinity8:20180922211743j:plain



ソフトウェア開発

コード全体は以下GitHubにあげました。
github.com


なお、開発する上でのポイントは以下の三点でした。

① 二つの測距センサを同時に使用
②ボール転がり位置に応じた状態遷移
③上段センサーの測定値に応じてLED表示箇所を変更

①二つの測距センサを同時に使用

VL53L0Xのライブラリは以下を参照。
GitHub - pololu/vl53l0x-arduino: Pololu Arduino library for VL53L0X time-of-flight distance sensor

一方、VL6180Xのライブラリは以下を参照
GitHub - pololu/vl6180x-arduino: Pololu Arduino library for VL6180X distance and ambient light sensor

こちらのサンプルコードを使えば、距離を測る事は出来そうです。

問題は、同じI2Cラインに乗った二つのモジュールの判別。データシートによると初期アドレスはどうやらどちらも同じ 0x52である模様。「じゃあ、I2Cアドレスをどうやって変更するのか?」と調べていたところ、ST microサイトにあるアプリケーションに対応方法が記載されていた

https://www.st.com/content/ccc/resource/technical/document/application_note/group0/0e/0a/96/1b/82/19/4f/c2/DM00280486/files/DM00280486.pdf/jcr:content/translations/en.DM00280486.pdf
VL53L0Xについて,複数接続のための調査 - yuqlidの日記

ここにはGPIOエキスパンダを使えと書いてあるが、今回は2個のみなので無視。
要は、それぞれのデバイスのXSHUTピン(VL6180Xの場合GPIO0)を使う様だ。XSHUTがトリガされている場合は内部リセット状態になっているため、以下の手順でI2Cアドレスを書き換えてやればよさそうだ。

手順① デバイス1/デバイス2共にXSHUT端子をL固定 (Non-Active)
手順② デバイス1のXSHUT端子をH(Active)に変化、その後、デバイス1のI2Cアドレスを書き換え
手順③ デバイス2のXSHUT端子をH(Active)に変化、その後、デバイス2のI2Cアドレスを書き換え 

なるほど、こういう使い方をするのか…

#include <M5Stack.h>
#include <Wire.h>
#include <VL53L0X.h>
#include <VL6180X.h>

VL53L0X m_sensor;  //上段のセンサー
VL6180X f_sensor;   // 下段のセンサー(finish確認用)
unsigned int m_sensor_val, f_sensor_val;

void setup() 
{
  M5.begin();
  Wire.begin();
  pinMode(M_SENSE_RST_PIN, OUTPUT);
  pinMode(F_SENSE_RST_PIN, OUTPUT);
  digitalWrite(M_SENSE_RST_PIN, LOW);
  digitalWrite(F_SENSE_RST_PIN, LOW);

  // センサー初期化設定
  digitalWrite(M_SENSE_RST_PIN, HIGH);
  m_sensor.init();
  delay(100);
  m_sensor.setAddress(24);
  m_sensor.setTimeout(500);
  m_sensor.startContinuous();  

  digitalWrite(F_SENSE_RST_PIN, HIGH);
  f_sensor.init();
  delay(100);
  f_sensor.setAddress(25);
  f_sensor.configureDefault();
  f_sensor.setTimeout(500);
}

void loop() 
{
  unsigned int m_sensor_tmp[5];
  unsigned int f_sensor_tmp[5];

  // センサー測定値誤差ばらつきの平均化
  m_sensor_tmp[0] = m_sensor.readRangeContinuousMillimeters();
  f_sensor_tmp[0] = f_sensor.readRangeSingleMillimeters();
  delay(5);

  m_sensor_tmp[1] = m_sensor.readRangeContinuousMillimeters();
  f_sensor_tmp[1] = f_sensor.readRangeSingleMillimeters();
  delay(5);
  
  m_sensor_tmp[2] = m_sensor.readRangeContinuousMillimeters();
  f_sensor_tmp[2] = f_sensor.readRangeSingleMillimeters();
  delay(5);

  unsigned int m_sensor_val = (m_sensor_tmp[0] +m_sensor_tmp[1]+m_sensor_tmp[2])/3;    
  unsigned int f_sensor_val = (f_sensor_tmp[0] +f_sensor_tmp[1]+f_sensor_tmp[2])/3;   

  // LCD Display
  //M5.Lcd.setTextColor(GREEN ,BLACK);
    M5.Lcd.setTextSize(3);
    M5.Lcd.setCursor(0,  48); M5.Lcd.print("Dist:");
    M5.Lcd.setCursor(128,  48); M5.Lcd.print("          ");
    M5.Lcd.setCursor(128,  48); M5.Lcd.print(m_sensor_val);
    M5.Lcd.setCursor(128,  96); M5.Lcd.print("          ");
    M5.Lcd.setCursor(128,  96); M5.Lcd.print(f_sensor_val);

}
②ボール転がり位置に応じた状態遷移

今後の拡張も考えて、少しまじめに状態遷移図を作ってみました。
f:id:motokiinfinity8:20180922190355j:plain


前回の記事で学んだタスク管理を生かして、LED専用タスク、音声再生用タスクを実施しようとしましたが、ここでトラブル発生。

失敗談①: 音声再生タスクと、LED表示タスクを同時に実施した結果、音声カクツキ & LED表示の遅延が発生。タスク優先度を変更するが状況は変わらず
 ⇒ wav再生処理量とLED処理の性能問題か? 一旦、LED処理を行う状態1では音声処理を行わない方向に変更する事で暫定対策をしたが、原因解析は継続

③上段センサーの測定値に応じてLED表示箇所を変更

NeoPixelの使い方は前記事参照。
ogimotokin.hatenablog.com

  // LED Display
  // LED位置換算(実測による補正付)  25個まで(390mm以下)で条件分け
  int g_pos, g_pos_old;
  if(m_sensor_val < 40 || m_sensor_val > 1000) g_pos = -1;
  // 390mm以下の場合
  else if(m_sensor_val<= 390){
    g_pos = (m_sensor_val-40) / 14;
  }// 390mm以上の場合
  else  g_pos = 25 + (m_sensor_val-390) / 4.5;

  if(g_pos >= PIXEL_COUNT) g_pos = PIXEL_COUNT;

  
  int i;
  switch(g_state){
    case 0: 
      if(g_state_flg == 1){
          for(i=0; i<PIXEL_COUNT; i++){
            strip.setPixelColor(i, strip.Color(0, 0, 0));
          }
      }
      break;
    case 1:
    case 2: 
      //if( g_state_flg == 1){
      //if(g_pos != g_pos_old){
      for(i=0; i<PIXEL_COUNT; i++){
         if(i < g_pos)  strip.setPixelColor(i, Wheel(i * 255 / PIXEL_COUNT));
         else strip.setPixelColor(i, strip.Color(0, 0, 0));
       //}else if(g_pos > g_pos_old){
       //   if(i > g_pos_old && i < g_pos)  strip.setPixelColor(i, Wheel(i * 255 / PIXEL_COUNT));
       //}else if(g_pos < g_pos_old){
       //   if(i > g_pos && i < g_pos_old) strip.setPixelColor(i, strip.Color(0, 0, 0));
       //}
       }
       break;
    case 3: 
       strip.setPixelColor(i, Wheel(i * 255 / PIXEL_COUNT));
       break;           
    }
    delay(1);
    portDISABLE_INTERRUPTS();
    delay(1);
    strip.show();
    portENABLE_INTERRUPTS();

失敗談②: NeoPixel LEDが40個近く並ぶとたまに無表示時でもノイズが発生
⇒ 原因調査をしたところ、下記サイトに解決策を見つけた。
じわじわ進む: ESP32で33個以上のNeoPixelを使うと制御が乱れる

LEDが増えてshow()での処理に1mSec以上かかってしまうとデータ送出の最中にインタラプトが入ってしまいフォーマットが崩れる事が原因らしいです。解決策は上記コードにマージ済みです。

まとめ

今回は 家族の得意分野を使った合作になりました。手作り感満載ですが、そんなアナログ工作とデジタル工作のコラボレーションも、また温かみがあって良いかなぁ、って思います。
何より息子がこんなに喜んでくれて、
『モノづくりをすることの喜びの原点』
を思い返させてくれました!

ありがとう息子!

時間を見つけて、残りの上段部分の開発を実施します。せっかくなので、もう少しギミックを増やして、飽きのこない作りにしたいですね!

LED(NeoPixel)を使いこなし、リハビリ支援用光るジョイスティックを作ってみた

f:id:motokiinfinity8:20180901142652j:plain

今回の学び技術: NeoPixel LED制御、ESP32のマルチタスク処理 (別タスクでLED制御)

はじめに(作成動機)

息子向けの足の代わりになる様なモビリティの開発を構想しているのだが、まず一番大事なのは操作系!まず現状の電動車いすの標準操作であるジョイスティック操作」、これを使えるかどうかをまずは見極めたい。
息子はまだ意志ある言葉は話せないのだが、手は器用に動かせるので、、、
もしかしたらジョイスティックでの操作が詳細な行動意志(「前に行きたい」「後ろに戻りたい」)を表すツールになりえるかもしれない!

ただし、息子くんはまだ自分の搭乗している乗り物が勝手に動く事に恐怖心を覚えてしまう。
乗り物に乗る行動が、本当に生理的に嫌いな行動なのか、それとも一度楽しさを覚えればそこから新しい世界(経験)が広がるのか、現時点では不透明だが、
親としては後者の可能性を信じて広げる手助けをしていきたい!
なので、本人の可能性を広げるため、息子に楽しんでもらいながら(←ここ重要)
一歩ずつモビリティ操作の練習を進めていきたいと思う。

なので、以下の作戦でスティック操作 = 意志を伝達する練習をしていく!


◆Step1 : ジョイスティックを動かすと…何かが起こる!(物は動かない)
◆Step2 : ジョイスティックを動かすとロボットが動く!(物が動く)
◆Step3: ジョイスティックを動かすと自分の乗った乗り物が動く (自分が動く)

そこで、Step1~Step3まで使い続ける事のできる『息子専用ジョイスティック』を作ってみる事にした。

要求仕様

 ・スティックは大きく簡単な力で動く事
 ・防水性が高い事 (息子は気に入ると涎ナメナメするため)
 ・将来的にロボット/モビリティを操作できる事
    → 防水性を考えるとロボットとは無線通信
 ・ジョイスティック単体として、動かすと楽しい事が起こる事


最後の「楽しい事」ってなんだ!?
という事で、悩んでいましたが、、、

「そうだ、子供の大好きな光り物を使ってウェイウェイ感を出そう!」

という事で、フルカラーLEDをライン上に光らせる仕様を追加。

いやぁ、ちょうど夏休みの旅行に行ったハウステンボスのライトアップにて息子本人はかなり興奮!
体をフリフリしながらテンション上がっており、
「光と音のコラボ、最強!」
という体験をしてきたので、その体験をリモコンにも取り入れようかと。

完成品

完成したジョイスティックの写真は以下。

f:id:motokiinfinity8:20180901142652j:plain

完成品の動画と、息子の反応動画は以下。

遊んでくれてるー!!  作戦成功

方向ごとに色を分けていて、上が黄色、下が赤、左が緑、右が青 として、方向と色のことばも一緒に覚えてもらう作戦です。
また、上 or 下ならラインLEDの中央に、右ならラインLEDの右側に、左ならラインLEDの左側に LEDがウェーブして動く様にしてみました。
(最初はウェーブなしの静止状態だったのですが、息子の反応がイマイチだったので、ウェーブで動かしました。『光る+動く』が重要な様です)

そして、今回のあそびごころは「フィーバーモード」! スティックを中心以外の場所に5秒連続で倒しておくと、レインボーに光る様にする仕様です。
息子はこれにかなり食いついていて、一度フィーバーモードに入れるコツが分かったらその後は繰り返して遊んでくれてました!


ハードウェア構成

ハードシステム図は以下。

f:id:motokiinfinity8:20180901151750j:plain

将来的には、無線経由でHostロボットに操作命令を送るため、ESP32モジュール一択。
今回 M5stackを使わなかったのは防水性優先にしたかったから。
(現時点では開発途中なので電源はUSB、将来的には無接点充電を頑張る!)

■ESP32 ESP-32S NodeMCU開発ボード
  : ESP32搭載ボードなら何でもよいと思います
  Amazon CAPTCHA

ジョイスティック
: ゲーセン用。まだ細かい手の操作が難しい息子が操作しやすい様、大きめのスティックを選びました。
   ジョイスティック: センサ一般 秋月電子通商-電子部品・ネット通販

■LEDテープ(NeoPixel)
: 業界おなじみシリアル信号でフルカラーLED制御可能なLEDテープ。今回は9個分を追加います。
   本当は5V駆動がよいのだが、3.3Vでも動いたので3.3V接続とする (レベル変換回路簡略のため)
フルカラーシリアルLEDテープ(17cm) - スイッチサイエンス

■筐体
; 息子の前に置ける様に大きい箱を購入。

■LEDテープ張り付け台
  : 3Dプリンタで自作。正面から光って見える様に斜め向きにテープ張り付けできる様に設置。
   f:id:motokiinfinity8:20180901144526p:plain

ソフトウェア開発

開発する上でのポイントは以下の三点
 ① サンプルコードをベースに LEDを専用で光らせる
 ② 独自点灯パタンへの置き換え
 ③ LED制御を別タスク処理 (ジョイスティックの変化に対する応答性アップ)

①サンプルコードをベースに LEDを専用で光らせる

ライブラリはおなじみのAdafruit_NeoPixel。
github.com

#include <Adafruit_NeoPixel.h>

// 定義 (合計9個のLEDを GPIO13から制御)
Adafruit_NeoPixel strip = Adafruit_NeoPixel(9,  13, NEO_GRB + NEO_KHZ800);

void setup()
{
  strip.begin();
 strip.setPixelColor(2, strip.Color(255, 0, 0)); //先頭から3個目(id=2)を赤色表示
 strip.setPixelColor(3, strip.Color(0, 255, 0)); //先頭から4個目(id=3)を緑色表示  
 strip.setPixelColor(4, strip.Color(0, 0, 255)); //先頭から5個目(id=4)を青色表示    
  strip.show(); //設定反映
}

上記だけのイメージで好きな位置のLEDを自由に光らせらるのでめっちゃ便利!!

失敗談①:最初Arduino Unoでは動いたがESP32では動かず、結局 Adafruit_NeoPixelのライブラリが古かったらしい (2年前に入れたきり更新してなかった)
 ⇒ ライブラリバージョンは要注意!

② 独自点灯パタンへの置き換え

これだけでもLEDは光るが、せっかくならLEDを色々と動かしたいし、カラフルにしたい!
自分で作ってもいいが、効率化のため既に作成されているサンプルを使っていくとよい。
私は以下のサイトを参考にした。

www.tweaking4all.com

私はRunning Lights効果をベースに改造した。
右側に倒した場合は右方向へ、左側へ倒した場合は左方向へ動かした。
なお、前の場合はセンター方向に光が集まる様に(前に進んでいる様にしたかったので)上記を参考に自作した。

また、フィーバーモードのLED点灯効果は、rainbowCycle効果をベースに改造した。

失敗談②:上記のRunning Lightsのソース内部では、三角関数を使いSin角をずらす事で波みたいな光量差を計算している。しかし、なぜかこれを使うと先頭LED(id=0)にノイズが乗ってしまう。原因は不明。
 ⇒ sin関数計算は使わず固定値(テーブルを持つ事)で対応した

③ LED制御を別タスクで回す

②で面白いLED効果が出来たが、Main関数でloopでLED切り替え操作を実施しているため、LED操作が終わるまでジョイスティックの状態を読みにいけず、ジョイスティックを別方向に動かしてから、LED効果が変更なるまで時間差が発生する。これだけなら別にLED更新周期とジョイスティック確認周期を作りこめばよいが、将来的にスピーカ制御やロボットとの通信やり取りを考えると、面倒くさい。そこで LED制御はMain関数から分離して別タスクで回す事にする。

なんとESP32は、無線対応マイコンでありながら、デュアルコアであり、FreeRTOS というリアルタイム OS で Arduino core プログラムが組まれているため、マルチタスク制御ができるとの事!なんてすごいんだ!!

下記サイトを参考に、LED処理を別タスクで処理する様に変更してみた!

www.mgo-tec.com


setup時点で、xTaskCreatePinnedToCore関数でタスク生成をして、Main関数ではジョイコン読み出し、LEDタスクではLED制御を行う。タスク間通信手段として一旦グローバル変数を使った。(もっと良いやり方あれば教えてください!)
LEDタスク側処理としては、該当グローバル変数の変更フラグをReadしたらLED処理を停止→ジョイコン値に従って次のLED処理に即座に切り替えるイメージですね。

失敗談③:Task内はwhile(1)で回さないとプログラムダンプで落ちるので注意!

作成したコードの一部は以下。

#include <Adafruit_NeoPixel.h>

#define UP_STICK_PIN   33
#define LEFT_STICK_PIN   35 
#define RIGHT_STICK_PIN   32 
#define DOWN_STICK_PIN   34 
#define PIXEL_PIN    13

//GLOBAL変数
int stick_pos = 0;    // [0: センター、4: 上、-4:下 ] + [+1:右、-1:左]を補正
int stick_pos_now = 0;  //初期値
bool _stick_pos_is_changed = false;
bool _fever_mode = false;
uint8_t red, green, blue;
int start_led, end_led;
uint32_t start_time = 0;



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

  pinMode(PIXEL_PIN, OUTPUT);
  pinMode(UP_STICK_PIN, INPUT_PULLUP);
  pinMode(LEFT_STICK_PIN, INPUT_PULLUP);
  pinMode(RIGHT_STICK_PIN, INPUT_PULLUP);
  pinMode(DOWN_STICK_PIN, INPUT_PULLUP);
  strip.begin();
  strip.setBrightness(64);
  delay(1);  //安定待ち
  strip.show(); // Initialize all pixels to 'off'

  rainbow(10, 0, PIXEL_COUNT-1) 

  TaskHandle_t th; //マルチタスクハンドル定義
  xTaskCreatePinnedToCore(DisplayLED, "DisplayLED", 4096, NULL, 5, &th, 0); //マルチタスク起動
}




/////////////////////////////
// LED表示タスク処理
/////////////////////////////
void DisplayLED(void *pvParameters) {
    while(1){
     // もし5秒以上スティックが倒れていたら、フィーバーモード(レインボー)に遷移  
      if(Time_Mesure(g_start_time) > 5000){
        rainbowCycle(2, 0, PIXEL_COUNT-1);
      }else{
        // 5秒以下の場合、スティックの倒れ方に応じてモード変更
        switch(g_stick_pos){
          case  -5: //右後ろ
            RunningLights(255, 0,  0, 80,  2, 8,  0); 
            break;
          case -4:  //後ろ
            RunningLightsToCenter(255, 0,  0, 80,  0, 8,  0); 
            break;
          case  -3: //左後ろ
            RunningLights(255, 0,  0, 80,  0, 6,  1); 
            break;
          case  -1: //右
            RunningLights(0, 255, 255, 80,  2, 8,  0); 
            break;
          case  1:  //左
            RunningLights(0, 255, 0, 80,  0, 6,  1); 
            break;
          case  3:  //右前
            RunningLights(255, 255,  0, 80,  2, 8,  0); 
            break;
          case  4:  //前
            RunningLightsToCenter(255, 255,  0, 80,  0, 8,  1); 
            break;
          case  5:  //左前
            RunningLights(255, 255,  0, 80, 0, 6, 1);
            break;
          case  0:  //なし
           for(int i=0; i<strip.numPixels(); i++) {
              strip.setPixelColor(i, strip.Color(0, 0, 0));
              delay(10);  //安定待ち
           }
           break;
        }
         strip.show();
        }
        g_stick_pos_is_changed = false;
    }
}


/////////////////////////////
// Main処理
/////////////////////////////
void loop(){
  // スティック検出
  if(digitalRead(UP_STICK_PIN)==1){
    g_stick_pos = 4;
    wave_dir = 1;
  }else if(digitalRead(DOWN_STICK_PIN)==1){
    g_stick_pos = -4;
    wave_dir = 0;
  }else{
    g_stick_pos = 0;
    wave_dir = 0;
  }

  if(digitalRead(LEFT_STICK_PIN)==1){
    g_stick_pos += 1;
  }else if(digitalRead(RIGHT_STICK_PIN)==1){
    g_stick_pos -= 1;
  }

  //スティックが倒されている間の時間測定
  if(g_stick_pos == 0)  g_start_time = millis();  //時間リセット
  Serial.println(Time_Mesure(g_start_time));
  Serial.print("g_stick_pos:");
  Serial.println(g_stick_pos);

  if(g_stick_pos != g_stick_pos_now){
     g_stick_pos_is_changed = true;
  }
  g_stick_pos_now = g_stick_pos;

}

//////////////////////////////////
// サブ関数
/////////////////////////////////
void RunningLights(uint8_t red, uint8_t green, uint8_t blue, int WaveDelay, int start_led, int end_led) {
//コード省略
}

void RunningLightsToCenter(uint8_t red, uint8_t green, uint8_t blue, int WaveDelay, int start_led, int end_led) {
  int Position=100;
  
  set_led_out_of_range(start_led, end_led);
  
  for(int j=0; j<=(end_led-start_led)*2; j++)
  {
      if(wave_dir == 1)    Position++;
      else  Position--;
             
      for(int i=start_led; i<=(end_led-start_led); i++) {
        // sine wave, 3 offset waves make a rainbow!
        //float level = sin(i+Position) * 127 + 128;
        //setPixel(i,level,0,0);
        //double level = (sin((float)(i+Position)) * 127 + 128);
        double level;
        // sinを使うと先頭LEDに緑ノイズが発生する対策 (sinを使わない)
        // 周期 3.5LED分
        switch((i+Position)%7){
          case 0: level=0.5; break;
          case 1: level=0.99; break;
          case 2: level=0.30; break;
          case 3: level=0.10; break;
          case 4: level=0.87; break;
          case 5: level=0.75; break;
          case 6: level=0.05; break;        
        }
        // 2乗にする事で明暗差をつける対応
        uint8_t tmp_r = pow(level,2)*red;
        uint8_t tmp_g = pow(level,2)*green;
        uint8_t tmp_b = pow(level,2)*blue;                
        strip.setPixelColor(i,strip.Color(tmp_r, tmp_g, tmp_b));
        strip.setPixelColor(end_led-(i-start_led), strip.Color(tmp_r, tmp_g, tmp_b));
        delay(2);
        //strip.setPixelColor(i,
        //  strip.Color(((sin(i+Position) * 127 + 128)/255)*red,
        //              ((sin(i+Position) * 127 + 128)/255)*green,
        //              ((sin(i+Position) * 127 + 128)/255)*blue));
      }
      strip.setPixelColor((end_led-start_led)/2,strip.Color(red, green, blue));
      strip.show();

      if(_stick_pos_is_changed == true) return;
      delay(WaveDelay);
  }
}


void rainbowCycle(int SpeedDelay,  int start_led, int end_led) {
  uint16_t i, j;
  
  set_led_out_of_range(start_led, end_led);

  for(j=0; j<256*5; j++) { // 5 cycles of all colors on wheel
    for (int i=start_led; i<= end_led; i++) {
      strip.setPixelColor(i, Wheel(((i * 256 / (end_led-start_led)) + j) & 255));
    }
    strip.show();
    if(_stick_pos_is_changed == true) return;
    delay(SpeedDelay);
  }
}

uint32_t Wheel(byte WheelPos) {
  WheelPos = 255 - WheelPos;
  if(WheelPos < 85) {
    return strip.Color(255 - WheelPos * 3, 0, WheelPos * 3);
  }
  if(WheelPos < 170) {
    WheelPos -= 85;
    return strip.Color(0, WheelPos * 3, 255 - WheelPos * 3);
  }
  WheelPos -= 170;
  return strip.Color(WheelPos * 3, 255 - WheelPos * 3, 0);
}

void set_led_out_of_range(int start_led, int end_led){
  int i;
 
  for(i=0; i<start_led ;i++){
     strip.setPixelColor(i, strip.Color(0, 0, 0));
  }
  if(end_led < strip.numPixels()){
    for(i=(end_led+1) ; i<strip.numPixels();i++){
      strip.setPixelColor(i, strip.Color(0, 0, 0));
    }
  }
  delay(5);  //安定待ち
  strip.show();
}

uint32_t Time_Mesure( uint32_t st_time ){
  return millis() - st_time;
}


全体コードはGitにアップしました(9/23追記)
github.com

汚いコードですが、興味ある方はご参考までにどうぞ!


まとめ

こんな感じで、日常に役に立つ開発ネタを探しつつ、それに加えて 毎回必ず一つは新しいモジュールや技術手法を試す事を目標にしています!
今回は ESP32のタスク処理の実施方法を学べて良かった! LED制御と合わせて、子供むけリハビリ器具には必須な技術なので、どんどん使っていこうとおもう!
次は、音と組み合わせるか、いよいよロボット操作と組みあわえるか、だな♪
頑張ろう!!

M5stack(ESP32)で朝顔水やりロボットを作ってみた

はじめに(動機)

前記事では、M5stackを使ってキッチンの手前に置く「お風呂沸かしボタン」押しロボットを作った。

ogimotokin.hatenablog.com

これに味を占めて、M5Stackを使って遊んでみようと企んでいた。
先日に訪問したMaker Faire TokyoでM5stack開発者のJimmyさんとお話して、M5stackに思い入れが少し入った事もある(笑)

そんな矢先、8月初旬(一週間前)、妻&娘から
「夏休みの旅行期間中、
 朝顔に自動で水をやる機械ができないか?」
といった相談が入ってきた。
今年で小一になる娘が学校で育てて持ち帰った朝顔の鉢、忘れがちながらも水やりを毎日頑張って、ここまで枯らさずに育ててきた。しかし、明日からの旅行で長期間不在にしてしまうので、不在中に水分量が足りなくなったら、下手したら朝顔の鉢が枯れてしまう。。。

娘も「枯らしたらどうしよう…」と不安な顔をしていたので、これはもう「やるしかない!」という事で、
朝顔に水をやるロボット』
の開発に乗り出した

依頼要件

朝顔の鉢に定期的に水を与える事
(2回以上/日)
・水をちゃんとあげられたかどうかを
フィードバックして欲しい
(万が一、うまくいってなかった場合、ばぁば召喚の可能性も想定するため)
・開発期間: 旅行出発まで
(実働期間は深夜3日間のみ)

要件はいいが、厳しいのが開発期間! モノは作れても検証期間が取れない。。。

だが、父の名誉と妻娘からの信頼感、そして今後の自分の趣味時間の確保&拡大のためにも、ここはなんとかやり遂げる!

完成品

という訳で、短期間での開発を実現するため、手元にあったM5stackを活用して、朝顔水やりロボットを完成させた。

完成ハードウェア写真は以下。
f:id:motokiinfinity8:20180815000733j:plainf:id:motokiinfinity8:20180815000736j:plain

完成動画は以下。

ロボット仕様

・一定時間(4時間置き)にロボット起動。
 バケツの水からポンプでくみ上げて、朝顔鉢上に放水!
 放水箇所を分散させるためホースは可動式にする
・放水動作完了したら、Lineに完了通知をお知らせ
 その際、温度&湿度&土壌乾燥度の情報も通知。
 特に土壌乾燥度は放水前後の値を取得する事で、確実に放水されている事を検知する

正直なところ、M5stackである必要はあまりない。だって、屋外に置いて放置してる(しかも留守居にしている)ので、液晶&スピーカは本来は無用なのだ。
しかし、真の目的は、娘&妻へのプレゼン&信頼感を得る事であるため、
「映像&音でデモ映えする方が、より『おおーっ!』って言ってもらえるし、
 個人的にもモチベーション上がる!」
という事だ。個人モノづくりにとってモチベーションは何よりも最優先だと思う!




ハードウェア検討

全体システム図

f:id:motokiinfinity8:20180815231717j:plain

水やりのためのポンプ&ホース選定は以下のサイトを参考にした。
2m3g1.com

「水中用ポンプ」「ホース」は時間がなかったので全く同じものをAmazonで購入。
水中用ポンプが12V駆動なので、屋外コンセントレス環境での使用を想定して
「乾電池BOX 単三×8本」も購入した。
https://www.amazon.co.jp/gp/product/B0714BK7M5/ref=oh_aui_detailpage_o02_s00?ie=UTF8&psc=1

水中用ポンプの電源ON/OFFを切り替えるのは、トランジスタを使うのがラク
なので一般的であろうNPN型2SC1815-GRを購入。

また、M5stackへは5V入力が必要なので、12V→5V変換DC/DCモジュールも使用した。
https://www.amazon.co.jp/gp/product/B010RYGGJC/ref=oh_aui_detailpage_o01_s00?ie=UTF8&psc=1
サイズ感の割に対応電圧幅が広いため、まとめ購入して重宝しているDC/DCモジュールだ。

温度/湿度センサーは、以前にM5stackのプロトキットを購入した際に付いてきたDHP-12モジュールを使用。GROOVE対応のため、接続イメージは簡単。

土壌乾燥度センサーは、園芸系電子工作ではよくつかわれているYL-69を使用。
https://www.amazon.co.jp/gp/product/B01H6WK51I/ref=oh_aui_detailpage_o04_s00?ie=UTF8&psc=1
水分量(電圧)に応じたアナログ値を出力してくるため、M5stack側はAD端子で受ける必要がある




ソフトウェア検討

詳細に記載すると大変なので、メモとポインタだけ。

Deep Sleep遷移設定

電池駆動である事を考えると、常にマイコン駆動させておくのは非効率のため、この機会にESP32の売り機能であるDeep Sleep機能を使ってみる事にした。Deep Sleepモードとは、要はRTC/割込み系等最低限のハードウェアだけ生かして他コアはSleepさせるモードの事。Deep Sleep時の待機電流は数十uAなので、電池駆動機器に使うには必須となる機能である。

d.hatena.ne.jp

上記サイト等を参考にさせてもらった。Deep Sleepからのwake条件は、RTC Timer時間指定や、特定のGPIO値の割り込みなどが選択できる様だ。今回は RTC Timer時間指定をして、一定時間毎にモジュールを起動させる様にした。

#include "esp_deep_sleep.h"

void setup() {
   (中略)
  esp_deep_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_OFF);
  esp_deep_sleep_pd_config(ESP_PD_DOMAIN_RTC_SLOW_MEM, ESP_PD_OPTION_OFF);
  esp_deep_sleep_pd_config(ESP_PD_DOMAIN_RTC_FAST_MEM, ESP_PD_OPTION_OFF);
  esp_deep_sleep_pd_config(ESP_PD_DOMAIN_MAX, ESP_PD_OPTION_OFF); 
}

void loop() {
   (中略)
    // DEEP SLEEPモードに移行
    const uint32_t DEEP_SLEEP_uS = 1000*1000*60*60; //1時間置きに起動
    esp_deep_sleep_enable_timer_wakeup(DEEP_SLEEP_uS);  // wakeup(restart) after 6hours
    esp_deep_sleep_start();
}
温度/湿度センサDHT12動作

github.com
を参照。cloneデータを解凍してライブラリに入れておけばOK

Line通知

調べたところ、簡単なやり方としては、
 IFTTT連携 → Line Notify通知
になる様だ。

ESP32 Wifi経由でIFTTTへ接続しLINEへ通知 – thousandiy

上記サイトを参考にすればOK
 ・Lineへの投稿文章は、IFTTT側のアクションフィールド内で定義
  その際に、変数(String型)は3つまで設定でき、ESP32側で変数を作って投稿する

Arduino側の設定環境は以下

const char* ifttt = "maker.ifttt.com";  // Server URL
WiFiClient client;

void setup() {
   (中略)
}

void loop() {
   (中略)
      // LINEに完了通知を送信
      // Make a HTTP request:
      String  value1 = String(temper, DEC);    //value1: 外気温度表示
      String  value2 = String(humidity, DEC);  //value2: 外気湿度表示    
      String  value3 = String(dojyo_val, DEC);    //value3: 土壌乾燥値の表示(水やり前)
    
      while(!client.connect(ifttt, 80)){
         Serial.print(".");
         delay(500);
      } 
      Serial.print("client,Connect");
      String url = "/trigger/" + makerEvent + "/with/key/" + makerKey;
      url += "?value1=" + value1 + "&value2=" + value2 + "&value3=" + value3;
      client.println("GET " + url + " HTTP/1.1");
      client.print("Host: ");
      client.println(ifttt);
      client.println("Connection: close");
      client.println();

      int count = 0;
      while (!client.available()) {
        delay(50); //
        Serial.print(".");

        count++;
        if (count > 20 * 20) { // about 20s
          Serial.println("(send) failed!");
          return;
        }
      }
}

開発ハプニング事例

①定期Wake時間のSleep時間

esp_deep_sleep_enable_timer_wakeup関数の引数値を試しに6時間程度に指定してみたが、なぜか10分程度の間隔になってしまった。そこで、- Read the Docs for Businessなどのサイトを調べたところ、最大でInt32最大us=71.5分しかできない事が判明した。つまり、6時間Sleepはそのままでは不可である。
そこで、作戦を以下に変更した。
 ・モジュールは60分毎に起動
 ・起動したらRTCを取得して現在時刻を把握
 ・起動した時間情報が想定の時間であれば放水開始

RTCを取得するコードは以下。納品当日に変更したが、一発勝負で上手くいった!!

void setup() {
  //NTP設定
  configTime(9 * 3600L, 0, "ntp.nict.jp", "time.google.com", "ntp.jst.mfeed.ad.jp");
}

void loop(){
  struct tm timeInfo;
  getLocalTime(&timeInfo);
  Serial.print(timeInfo.tm_year);
  Serial.print(" ");
  Serial.print(timeInfo.tm_mon);
  Serial.print(" ");
  Serial.print(timeInfo.tm_mday);
  Serial.print(" ");
  Serial.print(timeInfo.tm_hour);
  Serial.print(" ");
  Serial.print(timeInfo.tm_min);
  Serial.print(" ");
  Serial.print(timeInfo.tm_sec);

 // 一定時刻(8時/12時/16時/20時/23時)のみ水やり動作実施
  if (timeInfo.tm_hour == 8 || timeInfo.tm_hour == 12  || timeInfo.tm_hour == 16 || timeInfo.tm_hour == 20 || timeInfo.tm_hour == 23 ){
 (中略)
 }
}
②M5stack ADC端子からanalogReadが出来ない

土壌センサの値がうまく読めず0値状態。信号の電圧値は3.3V近いのに何故!?
M5stack側の設定を疑いにかかった結果、ピン依存がある事は見えてきたが、原因解明まで至らず。。。

Pin25→〇(Speaker共用)
Pin26→〇
Pin35→×(0値)
Pin36→×(0値)

一旦時間がないのでPin26を使って回避しておくが、謎すぎる。。。
公式としてはPin35&36がADC推奨端子の様に見えるのだが、回路図を見ても、GPIO35/36共にESP32に直結してるだけだし…謎だ。

M5stack側の単品不良の可能性も踏まえて、後日追い込んでみよう。

③水回り周辺の電子機器開発トラブル

実は、初めての体験だった。やはり水と電気は相性が悪い。
事件は、納品前日の深夜3時。水を入れない状態で、一通りのモジュール結合動作を確認完了。
「さて、最後にポンプで水を吸い上げる事を確認したら寝よう」
と考えて、ベランダで最終確認をしようとしたのだが、その際に誤って、、、バケツの水をはねさせてしまい、モジュールに水がばさっとかかってもた!
突然M5stackが真っ白くざざっとノイズが乗って、その後沈黙。
これにより、M5stack本体と、DHT12モジュール/YL-69モジュールがやられたっぽい。

正直、納品前日(旅行出発前々日)の深夜3時にやらかしたので、、、もう絶望しかなかった。
幸い、M5stackは手元に予備があったので助かったが、、、DHT12モジュールは現品のみだったので、
温度/湿度/土壌乾燥度を通知する仕様はあきらざるを得なかった。

自分の不注意ではあるのだが、、、、今後は気をつける




最後に

今は、旅行先からの投稿になるのだが、無事に
朝顔の水やりが完了しました」
通知を受信しており、安定動作している様で、とても安心している。

f:id:motokiinfinity8:20180816080605j:plain

娘も「朝顔さん、枯れてなさそうでよかった」と嬉しそうにしていて、まさに『家族のためのモノづくり』が出来てる実感を持てて、嬉しい限り!


せっかく作った水やりロボットだが、この旅行を最後に使う事がなくなるのは大変もったいないので、
いっその事、『ロボットを使うために植物を育てる』事をやってやろうか!?


(これを、目的と手段の逆転、という)


本ロボットをバージョンアップさせるならば、
 ・土壌乾燥度を見ながら、
乾いてきたら自動で水を実施する
 ・M5stack→ESP32モジュール変更&
省電力機能をまじめに実装する

辺りを頑張ってみたいので、もし新たな植物を育てるネタができたら、かな。

M5stack(ESP32)でお風呂沸かしボタン押しロボットを作ってみた

はじめに(動機)

前記事に掲載したM5stackを使って、キッチンの手前に置く「お風呂沸かしボタン」押しロボットを作るためだった。

以前に、Amazon echo+Node-REDで家電をコントロールする仕組みをラピットプロト的に作った。

ogimotokin.hatenablog.com

これにより、赤外線で操作できる家電(照明/エアコン/扇風機/テレビ/レコーダ)はAmazon echoの制御管理下に入り、
実際に役に立っている(妻や子供たちからの評判も上々!)
「せっかくなら家じゅうの家電を制御してやろう」と欲望は尽きず。

ここで問題になってくるのは、「赤外線で制御できない操作機器」その中で、我が家で良く使うのは「お風呂沸かしボタン」だった。我が家では、お風呂の水の入れ替えは2,3日に1回、しかもお風呂に入るタイミングは家族バラバラなので、冷めた湯舟を沸かす機会は平均1回/日はある。ちなみに、遠隔制御が必要そうなのは「おいだき」ボタン。「おゆはり」の場合、必ずお風呂を洗いに風呂場に移動するので、実際に遠隔でボタンを押すケースがない。一方で、「おいだき」の場合、既に風呂桶には水が入っているケースがほとんどなので、遠隔操作の重要性が増してくる。(例えば、オンラインゲームに夢中になって物理的にも手が離せない状況だけど、ゲーム終了後にお風呂に入りたいなぁと思っている場合など)

なお、ボタン押し装置の既製品を使うのが一番早いとは思う。例えば、ボタン押しIoT機器として有名な「MicroBot Push」など。
microbot.is
しかし、我が家のお風呂ボタンはとても強敵で、おそらく既製品では安定してボタンを押せないと予想される。

理由1 : ボタンがとにかく固い!!
理由2:ボタン押し装置を固定する場所がない。両面テープによる壁固定は、壁紙をはがすリスクがありNG

難度の高そうな割に、たった2日に1回/10歩程度の手間を短縮するためだけの装置。
発案当初は正直そんなにモチベーションが上がらず、構想のみで2か月が過ぎていた。そんな時に出会ったのがM5stackだった。

M5stackを使ったロボット事例を見てるうちに
「これがキッチン近くにあったら可愛いな」
→「ん?こいつが、お風呂ボタンを押すと、、、なんだかシュールで面白い光景かも!?」
と思いたつと、急遽モチベーションが上がり、急ぎ作り上げてしまった。

完成写真
f:id:motokiinfinity8:20180726011918j:plain




完成動画


ね、狙い通り、シュールな光景でしょ(笑)

ハードウェア検討

サーボモータ検討

まずお試しで、低価格のSG90 (トルク 1.8kgf・cm)で試してみたが、残念ながらサーボモータがパワー負けしてる…
そこで、もう少しトルクが強い(かつ低価格でコンパクト)サーボモータを探してみた。
すると、スイッチサイエンスさんで以下のサーボモータを発見。
www.switch-science.com

トルク 3 kg·cmで小型で良い感じ!という事で、さっそく購入!
簡易実験をしたところ、サーボモータをきっちり固定して、簡易アームさえつければ押せそうな感触!

外形設計

サーボモータ+制御マイコンを購入したら完了ではない。むしろ、ここからがスタート!
どうやって、ボタンの下にサーボマイコンを固定するか、だ。

[案1]: サーボモータを壁に貼り付ける
f:id:motokiinfinity8:20180728015116j:plain
→ 押すとすぐ落ちる。見た目も美しくない
そこで、サーボモータを固定するための棚をコーナンで購入してきて設置。石膏ボードの壁に対して耐荷重3kgの棚、まぁ耐えられるでしょう

[案2] : 棚の上にサーボモータを置いて固定
f:id:motokiinfinity8:20180728015117j:plain
サーボモータがボタン押し回転方向の反力に耐えられず上に持ち上がる……
という事は、サーボモータを抑える冶具が必要!という事、で3Dプリンタで作る想定で追加。

[案3] : サーボモータの上に抑え込み冶具を追加
f:id:motokiinfinity8:20180728015119j:plain
→しかし、垂直方向の持ち上がりは回避できたが、水平方向の力の不均衡で前に押し出される。むむm…

[案4] : 台座の下に滑り止めシートを追加

ようやく安定して押せそうな構想は出来上がった。ふぅ~
f:id:motokiinfinity8:20180728015121j:plain
続いて、固定冶具を検討。

3Dプリンタでの筐体設計

サーボにつけるアーム部。タミヤのユニバーサルアームセットで作ろうとしたが、「見た目がダサいね」と妻に一蹴。
ここで、発想の転換。「機構じゃなくて、M5stackを活かしてロボットっぽくした方が楽しいんじゃないかな」

ならばいっその事、3Dプリンタで外装加工をしてやろうではないか、という事で以下の3種類の冶具を3Dプリンタで作成

①アーム部
②M5stack本体格納部
サーボモータ保持部

Fusion360で作成して出来たCAD図面は以下。結構ロボットぽくてイイね!



f:id:motokiinfinity8:20180728020521j:plain





ソフト設計

前回の記事のソフトをベースに、Wifiからの制御機能を追加しただけなので、それほど難しくはない


ただし、ラズパイのNode-REDからhttp通信で特定の情報を送るため、ロボット側のIPアドレスは固定にする必要があった。

調べたところ、Arduino環境でのESP32の固定IPの仕方は以下。

#include <WiFi.h>
IPAddress ip(192, 168, 0, 50); //固定IP 192.168.0.50
IPAddress gateway(192,168, 0, 0);
IPAddress subnet(255, 255, 255, 0);
IPAddress DNS(192, 168, 0, 0);

void setup(){
  // WiFi設定
  // We start by connecting to a WiFi network
  Serial.println();
  Serial.println();
  Serial.print("Connecting to ");
  Serial.println(ssid);

  WiFi.config(ip,gateway,subnet,DNS);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }

void loop(){
  // Check if a client has connected
  WiFiClient client = server.available();
  if (!client) {
    return;
  }

  // Wait until the client sends some data
  Serial.println("new client");
  while(!client.available()){
  delay(1);
  }
  
  // Read the first line of the request
  String req = client.readStringUntil('\r');
  Serial.println(req);
  client.flush();
  
  if (req.indexOf("/oyuhari_on") != -1){
       Serial.println("call http");
       avatar.stop();
       push_function();
       avatar.start();
  }
}

ただ、押すだけやったらM5stackをつけた意味がない。せっかくなので、押した瞬間に何かアクションを仕掛けようと思い、効果音サイトを探してみる。

効果音|音素材の01SoundEarth

辺りを探していたら、たまたまドラクエの魔法風の音源を発見! すかさず妻が一言「ギラでお風呂が沸いたらいいのにね」。

という訳で、あっさり効果音が決まりました(笑) ゲーム好きな妻でよかった~

まとめ

M5stack+3Dプリンタを使って、我が家固有の「お風呂制御ボタン」に最適な外装配置に仕上げる事で、強敵だった固いボタン押しを遠隔制御する方ができた。

しかし、せっかく作ったこのロボット、一日一回使う用途だけではもったいない。せっかくキッチン横という、LDK内でもリッチな場所に配置されているので、是非とも他の用途にも使えるロボットに進化させていきたいと思う。

うーん、他に何に使えるだろうか。悩むぜー

M5Stack(ESP32)を使い始めてみた

はじめに

 ちまたでブームになっている Arduino対応無線マイコンESP32が搭載されたモジュールキット”m5stack"!  2か月ほど前に衝動買いしていたものの、別件を優先してすっかり積みマイコン化していましたが、ようやく開封!

f:id:motokiinfinity8:20180722175725j:plain


M5Stackとは

 M5StackはEspressif社のArduino対応無線(Wifi/BL)マイコンESP32をベースに、ディスプレイ、スピーカ、バッテリー、SDカードスロット、ボタン、USBやGroveのコネクタを5cm四方の基板に搭載した統合開発モジュール。なので、ESP32マイコンを組み込む前のSDKボードとして、またM5stack単体としても工作品に導入できる。見た目もコンパクトで、液晶に顔を表現するだけでかわいくなる!
やはり、見た目は大事だ。

esp32マイコンは、Arduino UNOに比べて、
 ・マイコン周波数 → 30倍 (ESP32: 80MHz240MHz@2コア / Uno:16MHz)
 ・RAM容量 → 260倍(ESP32:520kB / Uno:2kB)
 ・ROM容量 → 128倍 (ESP32:4MB / Uno:32kB)
それでして、価格は esp32 の方が安いのだから、もう純正Arduinoマイコン系は出番がなくなるのではなかろうか。

個人的な使い道

ちょっと考えただけでも、いろんな工作品に即時導入できそう
 ① 小型ロボットのメインマイコンに出来る(液晶画面/スピーカがあるのは素敵)
   rosserialでHost PCからもROS制御できるので子ロボット向き
 ② リハビリ靴 ver.4のスピーカ部として使えそう
   (ver.3の課題になったディスプレイ課題がこれで解決する!)
 ③ 玄関お出迎えロボやボタン押しロボットにも使える
  (もともとESP32で簡易ロボを作ろうとしてたが、
   ディスプレイがある方が愛着がわきそう)
 ④ 業務でMEMSセンサーの性能実験をする際の簡易モニターに使える
  (シリアル値確認のためにPCを持ち運ばなくて良い!)
  ⑤ 子供のおもちゃ魔改造用!!

そんな事を視野に入れながら、今回のM5Stack 立ち上げで簡単にディスプレイ表示&スピーカ再生をしてみた。

うーん、いい感じ♪
これを自家製ピタゴラ装置の一つにしてしまったりも出来そう!(息子がピタゴラスイッチ大好きなので間違いなく喜ぶ!)
という訳で、さっそく模索開始!


環境構築

環境構築はそんなに難しくない。esp32環境が入っている前提であれば、M5stackのGitHubからダウンロードしてlibraryに加えてやればよい。
github.com


なお、esp32のArduino開発環境を入れるならば、こちらを参考するとよさそう
www.mgo-tec.com

M5stack(esp32)でサーボモータを動かしてみる

ここからは、M5stackで各モジュールを動かした際の注意事項を備忘録として記載しておく。意外と手間取ってしまった。

サーボーモータはArudinoでよく使われている"Servo.h"を使おうとしたが、結果としてはNGであった。

#include <Servo.h>
Servo myservo;

void setup() {
  myservo.attach( 1 );
}

void loop() {
  myservo.write(45);
  delay(5000);
}


で動かそうとしたら、、、

『警告:ライブラリServoはアーキテクチャ(avr, sam, samd, nrf52, stm32f4)に対応したものであり、アーキテクチャ(esp32)で動作するこのボードとは互換性がないかもしれません。 #error "This library only supports boards with an AVR, SAM, SAMD, NRF52 or STM32F4 processor."』

ですって。
ありゃま? ESP8266では使えてたのに。。。

どうやら調べてみたところ、ESP8266まではServo.h が使える ESP8266 等と Servo.h が使えない ESP32 だそうです。ソラシラナンダ

そこで、下記サイトを参考に、LEDを使用するledcWrite関数でPWM信号を使用するのがよさそうです。c( )関数でパラメータを設定し、ledcAttachPin( )関数で使用するピンを指定し、ledcWrite( )関数でPWM値を算出する。
ESP32: RCサーボの制御 | マイクロファン ラボ

ただし、使用CHには注意。M5stackの場合、CH0はスピーカ用、CH7はディスプレイのバックライト用で使っているので、それ以外を選ぶ必要がある。
間違って、CH0を使ってサーボを動かそうとしたら、スピーカノイズが発生してしまった。。

という訳で、CH3を使えばよさそう。このあたり、きちんとM5stackのハード構成を抑える必要がありそうだ。

#define LEDC_CHANNEL_3 3    //LEDCのチャンネル指定
#define LEDC_TIMER_BIT 16   //LEDCのPWMタイマーの精度設定
#define LEDC_SERVO_FREQ 50   //サーボ信号の1サイクル 50Hz:20ms
#define SERVO_PIN 2       //ServoPWMピン

int servo_pwm_count(int v)
{
  float vv = (v + 90) / 180.0 ;
  return (int)(65536 * (SERVO_MIN_WIDTH_MS + vv * (SERVO_MAX_WIDTH_MS -SERVO_MIN_WIDTH_MS)) / 20.0) ;
}

setup(){
   // servoモーター設定
  ledcSetup(LEDC_CHANNEL_3, LEDC_SERVO_FREQ, LEDC_TIMER_BIT) ; // 16ビット精度で制御
  ledcAttachPin(SERVO_PIN, LEDC_CHANNEL_3) ; // CH3をRC SERVOに
}

loop(){
    ledcWrite(LEDC_CHANNEL_3, servo_pwm_count(60)) ; 
}


M5stack(esp32)でディスプレイに表示を出してみる

ディスプレイドライバはILITEK 社の TFT LCD 用 ドライバー ILI9341 との事。詳細はこちらのサイトが詳しそう。
www.mgo-tec.com

どうやら SPI接続+バックライト制御の模様。M5stackの描画ライブラリは、ESP32のSPIレジスタに直接アクセスしてHW転送を使っている様で高速転送が期待できる模様。

SDカード上のJPEGデータを表示

一番楽な使い方。

  M5.Lcd.clear();
  M5.Lcd.drawJpgFile(SD, "/dora.jpg");

これだけでSDカードのルート直下にある"dora.jpg"が表示されるので簡単。
これは画面左上を原点とした場合の描画。任意の位置に描画させるためには以下の引数を設定すればよさそうな模様(まだ未実証)

M5.Lcd.drawJpgFile(fs::FS &fs, const char *path, uint16_t x, uint16_t y, uint16_t maxWidth, uint16_t maxHeight, uint16_t offX, uint16_t offY, jpeg_div_t scale);


公開されている顔アバターを使う

ロボットの顔としてディスプレイを使うため、顔の表示は必要!できれば、瞬きとかちょっとしたアクションの変化は付けたいなぁ。と探していたら、既にライブラリを作成されている方を発見!

github.com

サンプルコードを使わせてもらったが、とても使いやすく可愛い♪ 呼吸などのアクション動作をMainソフトから意識させないライブラリの作り方がとてもありがたい。一旦、ありがたく使わせていただきつつ、将来的には家族好みのキャラに作り替えていきたいなぁ、と。
是非とも、ドラえもん風のキャラクタ顔をここに表示させたい!

M5stack(esp32)でスピーカから音を鳴らしたい

ここが少しばかり苦労した。まず、M5stackとしてのwav/mp3ファイル再生のライブラリはなさそうで、ESP32で動作実績のあるライブラリを持ってくる必要がありそうだ。
ESP8266Audio / ESP8266_Spiramライブラリを使うのが推奨の様だ。M5stackのサンプルプログラムにも本ライブラリのヘッダ記載があった。

https://github.com/earlephilhower/ESP8266Audio
https://github.com/Gianbacchio/ESP8266_Spiram

一旦、
M5 Speaker and WAV files · Issue #40 · m5stack/M5Stack · GitHub
を参考に、ボタンを押す度にwavファイルが再生するコードを作成。

しかし、うまくいなかい。
課題1 : 1回目の再生終了後にノイズが発生して2回目に移行せず。
 ⇒ 試行錯誤の結果、都度 file = new AudioFileSourceSD関数を作り直したらうまくいった。ファイルポインタの位置を戻さないといけないのか?

課題2: WAV再生の場合、一度再生した後に無音ノイズが残る(MP3再生なら問題なし)
 ⇒ ライブラリのソースを見たところ、ライブラリ側の間違い発見!
AudioGeneratorWAV.cpp内のstop関数で停止処理がされておらず

bool AudioGeneratorWAV::stop()
{
  if (!running) return true;
  running = false;
  free(buff);
  buff = NULL;
  output->stop();  ★ここを追加
  return file->close();
}


なるほど、Output-> stop()が抜けていたから、無音再生しっぱなしだったのね。
という訳で、ローカルでライブラリ修正する事で無事に音源ファイルの連続再生ができた! (後ほどGithubに書いておこう)

という訳で、無事に音楽再生もクリア。参考コードは以下の形となった


AudioGeneratorWAV *wav;
AudioGeneratorMP3 *mp3;
AudioFileSourceSD *file_w,file_m;
AudioOutputI2S *out_w, out_m;
AudioFileSourceID3 *id3;

void setup() {
  M5.begin();

  //MP3の場合
  file_m = new AudioFileSourceSD("/sample.mp3");
  id3 = new AudioFileSourceID3(file_m);
  out_m = new AudioOutputI2S(0,1);
  out_m->SetOutputModeMono(true);
  out_m->SetGain(0.3);
  mp3 = new AudioGeneratorMP3();

  //WAVの場合
  file_w = new AudioFileSourceSD("/sample.wav");
  out_w = new AudioOutputI2S(0,1); 
  out_w->SetOutputModeMono(true);
  out_w->SetGain(0.3); 
  wav = new AudioGeneratorWAV(); 
}

void loop(){
  M5.update();
  if(M5.BtnC.wasPressed()){
    //MP3の場合
    file = new AudioFileSourceSD("/sample.mp3");
    id3 = new AudioFileSourceID3(file_m);
    out_m = new AudioOutputI2S(0,1);
    out_m->SetOutputModeMono(true);
    out_m->SetGain(0.3);
    mp3 = new AudioGeneratorMP3();

    mp3->begin(id3, out_m);
    while(mp3->isRunning()){
     if (!mp3->loop()) mp3->stop();
    }

    //WAVの場合
    file_w = new AudioFileSourceSD("/sample.wav");
    out_w = new AudioOutputI2S(0,1); 
    out_w->SetOutputModeMono(true);
    out_w->SetGain(0.3); 
    wav = new AudioGeneratorWAV(); 

    wav->begin(file_w, out_w);
    while(wav->isRunning()){
     if (!wav->loop()) wav->stop();
    }
  }

}

最後に

M5stackを扱った事のある電子工作技術者から見たら初歩的な内容だったかもしれないが、自分への備忘録もかねて記載してみた。いろんなマイコン/プロセッサ/PCを並行して触っていると細かい部分はすぐに忘れてしまうからなぁ。。「一週間前の自分は他人である」ってヤツですね(笑)

今回で M5stackの良さはかなり実感できたので、実際に我が家の工作物に導入していきたいと思う!

【自作リハビリ器具/電子工作】歩くと効果音の鳴るメロディ靴 Ver.3 を作ってみた

はじめに

以前に息子のリハビリのモチベーション向上&歩こうとする意欲を上げるために、『靴に体重を乗せると効果音の鳴るメロディ靴』を作ってみた。

ogimotokin.hatenablog.com

このコンセプト自体はよかった様で、最初は息子も「なんだなんだー?」と興味を持ってくれていたのですが、残念ながら長続きせず。
原因を分析したところ、
 ・靴とスピーカーボックスが有線ケーブルでつながっていて、足をケーブルに絡めてしまったりで運用に難色あり
 ・音だけだと息子の興味が長続きせず。。。

そんな中、先月に娘が「Nintendo LABOで弟が歩く練習が出来る靴を作ろう!」という事で、Nintendo LABOで『靴に体重を乗せると効果音の鳴るメロディ靴 Ver.2』を父娘合作で発明してみた。

これの反応はなかなか良好でした。良好だったポイントとしては
 ・足をフリフリすると靴も振動する(バイブレーション)
 ・足のフリフリに応じて、タブレット画面も光る&音が鳴る(音は娘チョイス)
でした。振動もさる事ながら、タブレットに大きく光が音と同時に見えるのがうけたみたい!

じゃあ、これを実際に運用していこうとしていたが、これも長続きせず。。。。

原因はというと、
妻&娘が毎日Nintendo Switchで別ゲームを遊び続けるので、リモコンが確保できない事!!!(笑)

まぁ、これだけが要因ではないのですが、せっかくなら専用で新たにメロディ靴を起こそうと思った訳です。

要求仕様

①靴部とスピーカ部はケーブルレス
 (可能ならスピーカも靴に入れたいが、まだそこまで至れず)
②スピーカ部はVer.1のものをできる限り流用する事
 (スピーカをもう一回作るのは面倒くさいので)
③靴に体重をかけた際に、靴を光らせる&振動させる
 (足を動かす事への興味を引き付ける目的)
④靴を光らせる際の体重量(閾値)は簡単に変更できる様にする
 (息子の成長に柔軟に対応できる様に)
⑤足が当たる事が懸念されるので靴部分のモジュールは壊れにくく作る
 (ちゃんとした外装をつける)

そんな完成品は以下。なお、靴部のモジュールは3Dプリンタで外装設計した筐体に収めてみた。
f:id:motokiinfinity8:20180715020713j:plain

TWELITE DIP導入

そもそもどうやってケーブルレスを実現するか、という事で、無線モジュールを検討開始。

WifiはAPが必要になるから宅外に持っていくとソフト書換えが発生して面倒やなぁ、
Bluetoothは親1対子2みたいな通信が簡単に構築できるか不安だなぁ、
と悩んでいた時、たまたま見つけたTWELITE DIPに注目!

TWELITEはモノワイヤレス株式会社のZigbee(2.4GH)無線モジュール。
消費電力が低い、簡単にネットワークが構築できる事、モジュールがコンパクトである事が特徴だ。

個人的に一番気に入ったのは、子機の入出力信号(デジタル/アナログ)が親機の入出力信号にミラーリングされる点。
つまり、無線接続であるのにあたかも有線でつなげている様な感覚で使える!

超簡単!標準アプリ(App_Twelite) - MONO-WIRELESS.COM

子機へのデジタル入力(4本)→親機からのデジタル出力(4本)
子機へのアナログ入力(4本)→親機からのデジタルPWM出力(4本)
親機へのデジタル入力(4本)→子機からのデジタル出力(4本)
親機へのアナログ入力(4本)→子機からのデジタルPWM出力(4本)

要求仕様で上げていた「スピーカー部はVer1(有線)と共用できる事」も実現できそう!
うまくやればソフト開発なしで出来る気がしてきた!!
という事で TWELITEを導入してみる事に。

全体ハードシステム

Ver1(有線版)と 今回のVer.3(TWELITE無線版)のシステム比較が以下の図。

f:id:motokiinfinity8:20180715010926j:plain

Ver.1では、靴からの感圧センサー値を見て、ある閾値以上の値になればスピーカ出音& LED制御という仕組みが入っていたので、その制御仕様をそのまま使えそうな事が確認できた。LED+振動モータをスピーカ部から制御せず、靴部内で完結させる案も考えたが将来的な仕様拡張性を優先して、スピーカ部で制御を集約する仕様とした。
(例えば、左足に体重をかけた後、右足に体重をかけた後でないと、左足のLED+振動モーターがONにならない様な仕様に変更する場合、左右両方の状態が分かるスピーカ部で制御する様がやりやすいから)

TWELITE設定

「超簡単!標準アプリ(App_Twelite)」モードを使うので、特に購入時からソフトの書き換えの必要はなし。ただし、1対2通信を行うため、設定値のみ変更を行った。
変更方法は下記参照
設定変更(インタラクティブ)モード - MONO-WIRELESS.COM

・a: set Application ID を変更 (なんでもよいが、全モジュールで統一させる)
・z: PWM周波数 を10KHzに変更 (LPF回路でリップル成分を減らすため)

回路設計(子機側)

コンパクトにしたいため、電源としてはボタン電池を採用。振動モーター等を動かすので電流容量は必要と思ったので、CR3023を使用。1個当たり3Vだが、TWELITE DIPは3.3V駆動でやや不安が残るため、「CR3023×2 (6V) → LDOで3.3V電圧駆動」とした。LEDは10mA程度でも十分光量が確保できそうなため、端子に直接(抵抗経由で)接続した。ただし、振動モーターは同程度の電流をひっぱる懸念があったため端子は別に分けた。

回路設計(親機側)

親機側はスピーカ部と有線接続する形とした。スピーカ部のマイコン端子電圧は5V、TWELITE DIP端子電圧は3.3Vなので、両者の接続のためレベルシフタICを間に挟んだ。注意すべきは、子機から来る感圧センサー値。子機側ではアナログ値で取得して、親機ではPWM値で来るため、5Vへのレベルシフト後にRCフィルタでLPFを形成してアナログ電圧にしてやる必要がある。
フィルタ計算に使ったサイトは以下。

CRローパス・フィルタ計算ツール

手元にある部品で極力カットオフ周波数を落とせる組み合わせが、C=1uF/R=510Ω であった。これでカットオフ周波数は約312Hz。1kHzのPWMに対してはかなり原信号成分が残ってしまうため、PWM周波数をdefaultから一桁分変更する事にした。まぁ、趣味なのでこの程度で良いだろう。

出来上がった回路は以下。
f:id:motokiinfinity8:20180715014607j:plain


なお、ブレッドボード図は、Fritzingで作成している。
Fritzing Download

外装設計

コンパクトに設計すべき子機側をくみ上げてみた。ユニバーサル基板にうまく積層できたのではなかろうか。


f:id:motokiinfinity8:20180715021012j:plain



ここまで来たら、筐体も付けたくなる!
ちょうど良い勉強の機会なので、自力で3D-CADで筐体設計もしてみた。

使用するソフトは fusion360
参考書を読みながら、試行錯誤しながら作ってみた。

3D-CADでの出来上がりはこんな感じ。サイズ感が分からないので、ユニバーサル基板や電池パックも書いてみた。
このおかげでかなり仕上がりのイメージがつかめた!やはり、視覚化するってとても大事。

f:id:motokiinfinity8:20180715014909j:plain

そして、3Dプリンタを所有する友人にお願いして作ってもらったのが以下。

f:id:motokiinfinity8:20180715020820j:plain


完成動画

マリオのコイン音の場合


息子の好きなピタゴラスイッチ音の場合

やはり、好きな音楽になると食いつきが違う!母の手を引っ張ってきて、「押してー!」とアピールしている様子が見える。
この様に、その時の好みに応じて柔軟に対応できるのが、このメロディ靴の良いところ。

LEDと振動モータをつけたのも良い傾向ですが、LEDを靴につけた分、下向きの姿勢になってしまうのが残念だった。
やはりLEDは体の正面にする必要がありそうですね。失敗したなぁ(>_<)
こりゃあ、さらに改良版が必要そうだ。

先は長いなぁ~ 頑張ろう!

OpenPoseを使って姿勢検出を試してみた

はじめに

ワークマシンでGPU環境を構築したので、早速いろいろと試してみたくなる。

子供らの遊び相手や息子のリハビリ支援に応用できそうなロボットを作る上で、子供らの姿勢状態を把握する仕組みは欲しいなぁ、と前から思っていたので、今回は姿勢推定(Human pose stimation)として有名どころであるOpenPoseを動かしてみた。

OpenPoseは、カーネギーメロン大学のZhe CaoらによってCVPR2017で発表された「Realtime Multi-Person 2D Pose Estimation using Part Affinity Fields」の有名な実装ライブラリで、人の関節位置合計18点を求めることができるとの事。

news.mynavi.jp

サンプルコード環境があるので、早速動かして動作確認してみた。

環境インストール

・CPU : Intel Core i7-7700
・メモリ : 16GB
GPU : GTX1080
・OS : Ubuntu16.04 LTS

基本的には、CMU-Perceptual-Computing-Labのインストールマニュアルを参照すれば良い。
https://github.com/CMU-Perceptual-Computing-Lab/openpose/blob/master/doc/installation.md

ただし、先に言っておくと、、、私の場合、Cuda/cudnnバージョン関連で苦労して、なかなかうまくいなかった。

まずは関連パッケージのインストール

sudo apt-get install cmake-qt-gui
sudo apt-get install libviennacl-dev

その後、Git Clone

git clone https://github.com/CMU-Perceptual-Computing-Lab/openpose

その後、cmake-guiGUI起動する。
・[Where is the source code] → git cloneしてきたディレクトリのパス、[..../openpose/]を指定
・[Where to build the binaries] → [..../openpose/build]を指定
[Configure]ボタンを押して、[Unix Makefile]を選択して、[finish]。
最後に[Generate]ボタンを押して完了

その後、以下のコマンドでビルドをしかける。

cd build/
make -j`nproc`

動作確認(トラブルあり)

動作確認のため、サンプル動画を使って姿勢検出をしてみた。

./build/examples/openpose/openpose.bin --video examples/media/video.avi

しかし、ここでエラー…

Check failed: status == CUDNN_STATUS_SUCCESS (3 vs. 0) CUDNN_STATUS_BAD_PARAM

むむむ、cudnn関連でエラーがでて、コアダンプしている始末。 ググッて調べていたところ、CUDA/cudnnのバージョンに依存していそうな雰囲気。
なので、バージョンを変更していろいろとトライ。

・CUDA=9.2/ cudnn=7.1  ⇛ NG (同様のエラーメッセージ)
・CUDA=8.0/ cudnn=5.1 ⇛ NG (同様のエラーメッセージ)
・CUDA=8.0/ cudnn=6.0 ⇛ OK!

という事で、無事に姿勢検出できた!
しかし、CUDA/cudnnバージョン依存があるのは厄介だなぁ。。 (CUDA9.0環境でも動いた情報があるので、後日また見てみようと思う)

姿勢検出結果

Webカメラの画像の確認方法は以下のコマンド

./build/examples/openpose/openpose.bin

これを使って、子供たちの姿勢検出ができるかを試してみた。
f:id:motokiinfinity8:20180709233943p:plainf:id:motokiinfinity8:20180709233647p:plain
f:id:motokiinfinity8:20180709234319p:plainf:id:motokiinfinity8:20180709234320p:plain

腕を曲げてもらったり、ズリバイしてもらったりしましたが、それなりの精度で予測している事がよくわかる。
(我が家として、ズリバイでも姿勢を検出できている事が好印象)

最高峰のGPU GTX1080 を持ってしても、8fpsとは、、、なかなか重たい処理やなぁ。
最終的には、本処理をロボット内に搭載しようと考えていたので、最低でもJetson TXシリーズで実用レベルの負荷で動いてほしいところ。そこは慎重に処理削減に取り組む必要がありそう。


ただ、このアルゴリズムを応用すれば、Kinectなどの距離検出カメラをつかわず単眼での距離算出もできるみたい。体の構成から算出した個人同定も目指せそう。
息子がプロンボードやリハビリをしている際に、姿勢が崩れているかを判定できるし、それに応じてモニター画面を変えるなど、いろいろとリハビリ支援に活かせる可能性もありそう。

ソースコード中身に踏み込んだサイトは以下
robonchu.hatenablog.com


姿勢情報の使い方やアプリケーションへの応用方法、ROSなどのロボット本体への組み込みを考えて、これからもう少し中身を見ていきたいと思う。