OGIMOノート~家族のためのモノづくり~

OGIMOテックノート ~家族のためのモノづくり~

重度障害の息子を持つ父親エンジニアの備忘録。自作の電子工作おもちゃ/リハビリ器具/ロボット関係の製作記録、思った事を残していきます

【リハビリおもちゃ】息子のための電子工作ピタゴラ装置α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以上かかってしまうとデータ送出の最中にインタラプトが入ってしまいフォーマットが崩れる事が原因らしいです。解決策は上記コードにマージ済みです。

まとめ

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

ありがとう息子!

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