OGIMOノート

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

息子の歩行リハビリを楽しくするメロディ靴 Ver.4 を作ってみた

f:id:motokiinfinity8:20190216161758j:plain

はじめに

昨年7月に息子の歩行リハビリのモチベーションを上げるため、
足を踏み込むと楽しい音の鳴る電子工作靴の試作3号機を製作しました。

ogimotokin.hatenablog.com

作って以降、おうちリハビリで定期的に使ってきましたが、
息子自身の認知反応が薄かった事、足の成長に伴い靴のサイズが合わなくなった事もあり、少し様子見をしていました。
(別の音の鳴る市販おもちゃ等をリハビリ時に使用してなんとかカバーしてました)

そんな中、昨年秋ごろに周囲の環境に対する息子の認識力、好奇心がぐっと上がってきた事を感じ、
『自分の意思で前に進む体験をたくさん得て欲しい!』
という親の想いから、「歩行器練習に適した靴」として再度 メロディ靴を見直す事にしました!

前試作における良かった点/改善点と対応方針

OKだった点

・靴を無線にした事で歩行練習しやすくなった
・「好きな音」+ 「光」「振動」で息子の多少の興味を掴んだ!

NGだった点

・靴を光らせると下ばかり見てしまう、前に注目するモノが必要
・「歩きたい!」と思わせるほどの爆発的な面白さが足りない
・スピーカのボリュームが小さい、音質が悪い

対応方針

以上の事を踏まえて、前試作の靴部分は変更なし(靴のサイズのみ修正)として、
『制御部+スピーカ部をアップデート』する事にしました。
試行錯誤として、単なる足踏みすると楽しい音が鳴る機能に加えて、以下の2種類の追加機能を試しました。

① 液晶画面に好きな映像を表示
 ・息子の大好きなピタゴラスイッチの音源を再生
 ・数歩歩くと「ピタッゴラスイッチ~♪」とフィニッシュ表示
 ・ネタとして、妻や娘が好きなドラクエ・マリオのコイン音を再生

② カラフルLEDイルミネーションを光らせる
 ・右足を踏むとLED右側、左足を踏むとLED左側を光らせる
 ・足の踏み込む力(重さ)に応じて、LED光る量を変える
 ・音源は、息子が好きな妻のピアノ生音に変更
 ・左右交互に足踏みした時だけ正しく音が鳴る。失敗すると失敗のイヤな音

という事で両方を作ってみた
①の動画

②の動画


それぞれを使ってみて息子自身の反応を見たところ、①は画面がちっちゃすぎてあまり惹きつけ効果が薄かったです。
一方で、②の方で特にLEDに息子の目が釘付けだったので、最終的には②を採用して、実際に歩行器に搭載して試しました。

その結果、
不器用ながらも左右交互に足を出せる様に!!

もちろんこの発明の成果だけではなく、冬休み前から地道に歩行器練習を繰り返してきた経験が大きいとは思いますが、


父ちゃん、嬉しいであります!!!


全体ハードウェア

まずは、①の取り組みですが、制御+液晶表示+スピーカ再生手段として【M5Stack】に置き換えました。
過去のスピーカソフト@Arduinoとの互換性、私自身が開発に慣れたデバイスである事、内蔵スピーカ等を自作せずに済む事による開発のスピード感を優先したためです。

ブロック図は以下の通り。
f:id:motokiinfinity8:20190216145633j:plain

M5Stack 1つで素早く色々と実装できるので、やはり便利なデバイスだなぁ~!

一方で、②の取り組みですが、制御にはESP32 DevKit、LED表示にはLEDテープ(NeoPixel)、音再生にはDFplayerMiniモジュールを使用しています。前記事にも書きましたが、液晶ディスプレイを使わない場合、この組み合わせの方が非常にコスパ良く、色々と応用出来て良さそうです!

ブロック図は以下の通り。
f:id:motokiinfinity8:20190216150053j:plain

上記の①②を簡単に入れ替えて試せるのも、靴側は特にハード変更しなくて良い事、両者ともESP32ベースのソフトコードだからです。
モジュール共通化の考え方は、この様に色々と試してみたい場合に向いてますね。

②については、自作スピーカーとして音の改善を少しだけ検討してみました。
結果として、秋月電子に売っていた3W8Ωのスピーカを使い、スピーカ筐体は100均で売ってるプラスチックのタッパを使いました。
プラスチックの筐体内で音の響きあって、なかなか質の良い音が鳴りました。
見た目は悪いですが、コスパは素晴らしいので是非試してみてください。

f:id:motokiinfinity8:20190216162005j:plain


工夫した点は、スピーカ部のTWILIteモジュールとHost(M5stack/ESP32)との接続方法

本試作では、TWILITE DIPからの靴の圧力センサ値をUART信号で受信する方法にしました。
以前の3号機では、有線式試作とスピーカ部を共用させるという設計制約を設けていたので、
 ・靴にある圧力センサ値はスピーカ側のTWELITE DIPからPWMとして出力
 ・途中でRCフィルタを経由してアナログ電圧値に変換、マイコン側のアナログ端子で電圧を図る
という面倒くさいことをやっていました。
しかし、今回のスピーカ側の改定に伴い、一旦再度見直したところ、TWELITE DIPからのUART信号を使うのが簡単&応用しやすそうでした。

全体ソフトウェア

TWILITE DIPからの受信信号

TWELITEからのUART信号は、以下の様な信号が送信されてきます。

:7881150178810000380027D3000C05230000FFFFFFFFFF97
:788115017881000038002813000C02230000FFFFFFFFFF59
:78811501758100003800284F000C02230000FFFFFFFFFF20
:788115017581000038002899000C04230000FFFFFFFFFFD4
:0281150145810D0AFD000567000B171A8000FFFFFF007FE9 [右足]
:018115019C81003C170006F3000CDE18000041FFFFFFFDC2 [左足]

^1^2^3^4^5^^^^^^^6^7^^^8^9^^^a^b^c^de1e2e3e4ef^g
 
データフォーマット
 1: 1バイト:[0-1]送信元の論理デバイスID (0x78 は子機からの通知)
 2: 1バイト:[2-3]コマンド(0x81: IO状態の通知)
 3: 1バイト:[4-5]パケット識別子 (アプリケーションIDより生成される)
 4: 1バイト:[6-7]プロトコルバージョン (0x01 固定)
 5: 1バイト:[8-9]LQI値、電波強度に応じた値で 0xFF が最大、0x00 が最小
 6: 4バイト:[10-17]送信元の個体識別番号
 7: 1バイト:[18-19]宛先の論理デバイスID
 8: 2バイト:[20-23]タイムスタンプ (秒64カウント)
 9: 1バイト:[24-25]中継フラグ(中継回数0~3)
 a: 2バイト:[26-29]電源電圧[mV]
 b: 1バイト:[30-31]未使用
 c: 1バイト:[32-33]D1バイト:I の状態ビット。DI1(0x1) DI2(0x2) DI3(0x4) DI4(0x8)。1がOn(Lowレベル)。
 d: 1バイト:[34-35]DI の変更状態ビット。DI1(0x1) DI2(0x2) DI3(0x4) DI4(0x8)。1が変更対象。
 e1~e4: [36-37][38-39][40-41][42-43]  AD1~AD4の変換値。0~2000[mV]のAD値を16で割った値を格納。
 ef: [44-45] AD1~AD4の補正値  (LSBから順に2ビットずつ補正値、LSB側が  AD1, MSB側が AD4)
 g: [46-47] 1バイト:チェックサム

要は、":"記号をトリガにして送信される24Byte程度のデータから適切なものを抜き出すイメージです。
今回使うのは、
  1: 送信元の論理デバイスID →右足/左足判別用
 e1: 左足の圧力センサ値
 e4: 右足の圧力センサ値

の3つのデータになるので、これらを抜きとるコードを書けばOKです。
(e1/e2/e3/e4のどれを使うかは、回路構成に依存します)


以下のコードでは、シリアル送信信号(文字列)から左右の圧力センサ値(整数値)に変換するコードの例になります。

void loop() {

  (省略)

  // Serial受信(内部バッファへのコピー)
  rcv_buf_update();

  // Serialバッファからコマンド取得
  String cmdmsg = getCmdmsg();

  if(cmdmsg != ""){
    String device_id = (cmdmsg.substring(0,2)); //送信元の論理デバイスID
    String A1_tmp    = (cmdmsg.substring(36,38)); //左足
    String A4_tmp    = (cmdmsg.substring(42,44)); //右足

    // 左足の場合
    if(device_id.toInt()==0x1){
      //16進数文字列→Int変換
      if(A1_tmp[0] >= 0x30 && A1_tmp[0] <= 0x39)  l_sense_val = (A1_tmp[0]-48)*16*16;
      else if(A1_tmp[0] >= 0x41 && A1_tmp[0] <= 0x46)  l_sense_val = (A1_tmp[0]-55)*16*16;
      else  l_sense_val = 0;
      if(A1_tmp[1] >= 0x30 && A1_tmp[1] <= 0x39)  l_sense_val += (A1_tmp[1]-48)*16;
      else if(A1_tmp[0] >= 0x41 && A1_tmp[0] <= 0x46)  l_sense_val += (A1_tmp[1]-55)*16;

 //右足の場合
    }else if(device_id.toInt()==0x2){
      //16進数文字列→Int変換
      if(A4_tmp[0] >= 0x30 && A4_tmp[0] <= 0x39)  r_sense_val = (A4_tmp[0]-48)*16*16;
      else if(A4_tmp[0] >= 0x41 && A4_tmp[0] <= 0x46)  r_sense_val = (A4_tmp[0]-55)*16*16;
      else  r_sense_val = 0;
      if(A4_tmp[1] >= 0x30 && A4_tmp[1] <= 0x39)  r_sense_val += (A4_tmp[1]-48)*16;
      else if(A4_tmp[0] >= 0x41 && A4_tmp[0] <= 0x46)  r_sense_val += (A4_tmp[1]-55)*16;   
    }
  }

  (省略)
}



//内部バッファからコマンド命令の検索・抽出
String getCmdmsg(){
  String result = "";
  bool incmd = false;
  int start_pos, end_pos = 0;
  int i;

  //Serial.println(recv_buffer.indexOf(':')); 
  
  for( i=0; i<recv_buffer.length(); i++){
    if(incmd == false){
      // ":"捜索
      if(recv_buffer.charAt(i) == ':'){
        //Serial.println(" ");
        incmd = true;
        start_pos = i;
      }
    }else{
      // 改行コード捜索
      if(recv_buffer.charAt(i) == 0x0A){
        incmd = false;
        end_pos = i;
        // メッセージ抽出 & メッセージより前のフレーム破棄
        result = recv_buffer.substring( start_pos+1, end_pos);
        recv_buffer = recv_buffer.substring(end_pos+1, recv_buffer.length());
        break;
      }
    }
  }
  return result;
}

既存発明品の流用の数々

それ以外の制御については、今まで作ってきた工作品での要素技術を組み合わせて実現しました。

(1)については、過去に作ったM5Stackの音声/映像再生部を流用しました
ogimotokin.hatenablog.com
ogimotokin.hatenablog.com

(2)については、クリスマスに子供のために作ったクリスマスツリーのイルミネーション技術
音声再生についてはじゃんけんロボットの音声再生モジュールを転用しました。

ogimotokin.hatenablog.com
ogimotokin.hatenablog.com

今まで短期間でたくさんモノを作ってきたノウハウを組み合わせてます!

開発ソース

別途 Github に公開予定

まとめ

今回は、息子の歩行器練習のモチベーションを上げるためのメロディ靴を作り、実際のリハビリ練習で定期的に使えるものが出来ました!
ただモノを作って終わりにするではなく、
実際に継続して使い続けれること
にとにかくこだわりました。
家族で試行錯誤しながら機能を考えたり、オンラインコミュ、SNS、ピッチイベント等で試作品を公開してこれをベースに一緒に改良アイデアを検討して頂けたり、PT/OT先生と一緒に現物でチューニングを繰り返せたり……

息子という実際に使ってくれるユーザーが目の前にいるからこそ出来る
家族のためのモノづくり

私自身も楽しみながら出来ました!


息子向けとしては、もうしばらく使って貰いながら、必要に応じて機能追加&改善を試みたいと思います。
今のところ、気になっているのは、靴部分の電池。現在はCR2032×2個使いなので、使えば使うほど電池を消費してしまうので、例えば充電タイプのものに変更するなどは検討しています。(とはいえ、子供の足部分にリチウムポリマー電池を置くのはちょっと抵抗あるから、そこは悩みどころ…)

もし息子以外で同様にお困りごとを抱えている方、「作ってみたい」という方がいるならば、改めてもう少し簡単に作れる手順を模索してみようかなぁ~

簡単にスピーカ再生機能を追加! DFPlayer MiniでMP3音源再生!

はじめに

アレコレとモノを作るのに熱中しているとブログ更新をしなくなってしまいますね(-.-)
「作ったモノの情報をしっかり整理して書き残そう」と考えると、
ついつち後回しになって記事が進まない。。。

なので、改めて初心に戻り、「自分の技術メモ+備忘録を気軽に残す」事を目的として、簡単に試してみたデバイスの記録メモや試行錯誤の履歴も一緒に残していきたいと思います。
(需要なかったらスイマセン)

今回試してみたのは、『簡単にスピーカ再生できるデバイスです。
子供のおもちゃ等を作る際に、とりあえず簡単に音を鳴らしたい!」というニーズ、色々とありますよね。

私が作った工作モノでも音の鳴る機器はいくつかありますが、その実現手段は大きく2つ。

ArduinoにSDカードをSPI接続して無理やりSDカード上のWAVファイルを再生
実施例:【電子工作(Arduino)】歩くと効果音の鳴るメロディ靴 - OGIMOノート

② M5StackでSDカード上のMP3ファイル再生
実施例:M5Stack(ESP32)を使い始めてみた - OGIMOノート

最近は、もっぱら②を使用。
②のM5stackはとにかくお手軽だしラピットプロトにはすごく便利なのですが、
 ・液晶表示がいらない場合、音声再生のみの用途には高価 (約5000円)
 ・MP3再生はそれなりに処理量をくうので、他処理(例えばLEDイルミネーション)を並行実行させるには厳しい
  (音かイルミネーションのどちらかが処理が追い付かず、不連続になってかっこ悪くなる)
というデメリットもある訳ですね。やっぱりお値段は重要。

そんな中、最近見つけた3つ目の手段を紹介!!

f:id:motokiinfinity8:20190124015146j:plain
Arduinoマイコン/ESP32 にMP3再生モジュール『DFPlayer Mini』を接続
です!

他の①②に比べて、
[メリット1] 導入が簡単!
  モジュールにマイクロSDカードを指して、Host機器(Arduino UnoやESP32)とUARTで接続するだけ。
  ライブラリを使えばマイクロSDカード内の任意のファイルを再生可。
[メリット2] 安い!
 Amazonでは2個で900円程度で売ってます。
www.amazon.co.jp

特に値段の観点は重要!
ESP32モジュール(1500円程度)+DFPlayer Mini(450円)+スピーカ(300円程度) = 2250円なので、
M5stack半額以下で「無線対応 簡易音声再生デバイス」が出来る
ので、素敵です!
ぬいぐるみをしゃべらせたり、玄関で音を鳴らしたり、と、『お手軽おしゃべり機器』が作れてしまう!!

モジュールの説明は、こちらのサイトが詳しそうです。
https://www.dfrobot.com/wiki/index.php/DFPlayer_Mini_SKU:DFR0299

なお、モジュールのピン配置はこちら
f:id:motokiinfinity8:20190124013245p:plain

ハードウェア構成図

Arduino Uno Rev.3(5V I/O)と接続する際の回路構成はこちら。
f:id:motokiinfinity8:20190124012719j:plain

モジュール側のI/Oは3.3Vとの事ですので、電圧5VのArduinoと接続するときは1kΩの抵抗を間に挟むことが推奨されているとの事。UARTは、ArduinoのRxとDFPlayerのTx、ArduinoのTxとDFPlayerのRxを繋ぎます。
電源は 5V/3.3VのどちらをつないでもOKですが、ここの電源ノイズが音質に直接影響してくるため、コンデンサ等の配慮が必要です。私の場合、子供おもちゃ用は音声ボリュームが必要なので、駆動電圧の高い5Vを使いました。

スピーカは3Ωまで対応。アンプ不要で、ダイレクトにDFPlayerに繋げるため、とても楽ですね。
なお、私は100均ショップで買ってきた安いスピーカ機器を分解してスピーカユニットのみ取り出すのが一番コスト安くすみます。

SDカードはFAT形式でフォーマットして、順番に再生音源を書き込んでいきます。
どうやらFATエントリ順(平たく言えば書き込んだ順)でindex番号が1,2…と割り振られていく様です。
(他ブログでは"0001.mp3""0002.mp3"と書く事を推奨されていますが、おそらく結果としてFATエントリー順に並ぶから、だと思われます)

なお、実際のDF Player miniの接続写真はこちら
f:id:motokiinfinity8:20190124003218j:plain


ソフトウェア

本体部とは UARTで通信します。
ボーレートは 9600kbps限定の様なので注意。
ライブラリは、”DFPlayer-Mini-mp3”と"DFRobotDFPlayerMini"の2種類がある様子。Web検索する限りでは、前者が多そうですが、どうやらこちらは現在非推奨になっている様なので、私は後者の"DFRobotDFPlayerMini"を使いました。

github.com





上記の情報を参考に。

これを使って、簡単に娘とじゃんけんをするジャンケンロボットを作ってみました。
(ボタンを長押しする時間に応じてグー/チョキ/パーを変える & 音声
今回は参考までに、ボタン押す時間に応じて、グー/チョキ/パーの再生音声を変えるサンプルコードを作ってみました。

>|c|
#include "SoftwareSerial.h"
#include "DFRobotDFPlayerMini.h"

#define BUTTON_PIN 3
#define SW_RX_PIN 10
#define SW_TX_PIN 11

SoftwareSerial mySoftwareSerial(SW_RX_PIN, SW_TX_PIN);
DFRobotDFPlayerMini myDFPlayer;

bool buttonState = LOW;
bool buttonStete_old = LOW;

uint32_t pushStart_time = 0;
uint32_t push_time = 0;

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

pinMode(BUTTON_PIN, INPUT_PULLUP);
attachInterrupt(1, interruptsw, CHANGE); // Pin3をスイッチ割込みとして使用

//スピーカ出力開始
myDFPlayer.begin(mySoftwareSerial);
delay(100); //設定待ち時間は仮置き

myDFPlayer.volume(30); //Set volume value. From 0 to 30
}

void loop() {
if(jyanken_start == true){
myDFPlayer.play(1); //Play the first mp3 (0001:じゃんけん合図)
delay(1500);

// ③じゃんけんの手(グー/チョキ/パー)を出す
// (1) ボタン長押し時間が1000ms以下ならグー
if(push_time < 1000){
myDFPlayer.play(3); //Play the first mp3 (0003:グー)
// (2) ボタン長押し時間が2000ms以下ならチョキー
}else if(push_time < 2000){
myDFPlayer.play(4); //Play the first mp3 (0004:チョキ)
// (3) ボタン長押し時間が2000ms以上ならパー
}else{
myDFPlayer.play(5); //Play the first mp3 (0005:パー)
Serial.println("pa");
}
}



// スイッチボタンの割り込み処理
void interruptsw(){

delay(10); // 5ms待ち (チャタリング対策)
buttonState = digitalRead(BUTTON_PIN);

//ボタン押し判定 (押し時間の測定開始)
if(buttonState == LOW && buttonStete_old == HIGH){
pushStart_time = millis();
jyanken_start = true;
//ボタン離し判定 (押し続けた時間を測定)
}else if(buttonState == HIGH && buttonStete_old == LOW){
push_time = millis() - pushStart_time;
}
buttonStete_old = buttonState;
}
|


終わりに

使ってみて今後も使えそうな 音源再生モジュール DF Player Miniのメモを記載しました。
今回は、仮としてArduino UNO rev3+DF Player Miniとしましたが、最終的には ESP32+DF Player Miniとして使う想定で進めていく予定です。
また、今回上記で記載した「ジャンケン読み上げ機」ですが、なぜジャンケンなのか等については、次回ブログで取り上げたいと思います。ありがとうございました。

M5Stackで作る 子供のためのクリスマスツリーイルミネーション

はじめに

この記事は、M5Stack Advent Calendar 2018の25日目の記事になります。
qiita.com

私のプロフィールは右側カテゴリ欄の「自己紹介」を参照ください!
普段は、子供・家族が実生活で使える様なリハビリ器具・電子工作機器を作っています! 子供向けの機器を作る上で大事なのはとにかく「思いついたらすぐに作ること」です。クオリティより速度優先です。
子供の場合、何がヒットするか分からないし、興味の移り変わりも激しいので、とにかく「作って試す」を高速に繰り返せる事が大事だと思ってます!

私がM5Stack を愛用しているのも、その辺りが理由になります!
・好きな音をすぐに出せる
・好きなイラストをすぐに表示できる
・無線での他機器との通信がすぐに出来る(子供曰く、「魔法みたい」)
・かわいい

など子供向けの電子工作を作るのに最適すぎるので、今年5月から使い始めて以降たくさん活用させて頂きました♪
(右側のM5Stackのカテゴリに活用例があるのでご参照ください)


クリスマスツリーのイルミネーション

M5Stack Advent Calendar 2018の機会を1週間前に頂きました。当初では、直近に作ったリハビリ機器の紹介(未掲載)を考えていたのですが、ちょうど投稿予定日が12/25という事もあり、「クリスマスになぞらえた新作を作りたい!」と思い、短期間での新規開発を決意しました

そこで今回は、愛用のM5Stackを使って、
我が家のクリスマスツリーのイルミネーションを
アップデートする

取り組みを紹介します!

■開発要件

緊急で家族会議をした結果、開発要件を以下に定めました
・きれいなイルミネーションとそれに合わせた音楽をかける
・ただ光らすのではなく簡易ストーリーを作る
・家族みんなで分担して「皆で作った一体感」を出したい
・息子の作業リハビリにも使えるとよい
・とにかく子どもたちが喜ぶ事!
・遊びゴコロも加える!


…なかなかのボリューム感(笑)

■開発体制

イルミネーション回路&ソフト担当:おとん
ストーリー&羊毛フェルト担当 : おかん
飾り付け&折り紙担当 : 娘(7)
監修&モニター担当 : 息子(5)


ただ作るだけではなく、「みんなで作った一体感」も大事にしました(^ ^)

設計構想を練るために、妻にイルミネーションストーリーの作成を依頼。


f:id:motokiinfinity8:20181225003313j:plain



おっと!?
炎!? 歓喜の舞!?

なかなかハードな要求ですが、頑張って実現しました!

完成品

f:id:motokiinfinity8:20181224112514j:plain

こんな感じ、妻作成のストーリーボードをベースに作りました。
子供たちが喜びそうな仕掛けをもう一工夫!
・クリスマス靴を軽く踏むと、空から流れ星!
・長く踏むと、、、ボールが『びよよよーん』って、下から飛び出す仕掛け(笑)

ボールが転がるのが好きな息子へのささやかな遊び心プレゼント!


無事にクリスマスパーティまでに間に合った!!
おかげさまでクリスマスパーティー、盛り上りました(^ ^)
子供たちは、びよよーんの仕掛けや、クリスマスツリーが燃える演出に大笑いする等、ものすごく良い反応をしてくれて、作りがいがありました!
何より、自分たちが作りたいと思うものを作るのは、時間を忘れるくらい熱中出来るし、楽しいですね♪



ハードウェア

今回のハードウェア構成はとてもシンプル!

まず思いついたのは、「M5stack」「NEOPIXELのLEDテープ」「スイッチ」のシンプル構成。

しかし、一点課題がありました。それは、「LEDのなめらかな動きと音楽再生&ディスプレイ表示が両立できない事」でした。ESP32マイコンは高性能だしマルチタスク処理も可能なのですが、人に違和感を与えないリアルタイム処理を2つ並行に回すのは大変そうでした。(軽く実装確認してみたところ、音が一瞬途切れたり、LEDの動きがぎこちなくなってしまいます)
各処理を最適化してやる事も考えましたが、出来るかも目処がなかったので、ここはスピード優先! もう一つESP32マイコンを追加して処理を分担する事にしました

以下がブロック図。

f:id:motokiinfinity8:20181224231858j:plain

■準備物

①LEDテープ NeoPixel
Amazon CAPTCHA
イルミネーションが詰まって映える様に、1m 144連の密集タイプを使用。これをクリスマスツリーに斜めに巻き付けます

② M5Stack
これはツリーに引っ掻けて見える様にします


f:id:motokiinfinity8:20181225003705j:plain



③ ESP32 開発ボード
Amazon CAPTCHA

こちらはブレッドボードの形でクリスマスブーツに入れ込んで外装の雰囲気を出します


f:id:motokiinfinity8:20181225003530j:plain


f:id:motokiinfinity8:20181225003554j:plain




④ スイッチ
プッシュタイプのもの。これを、100均で購入したクリスマスブーツの裏に穴を開けて、スイッチを付けました


f:id:motokiinfinity8:20181225003349j:plain


f:id:motokiinfinity8:20181225003615j:plain





⑤その他
せっかくのクリスマスツリーなので、家族で分担して色々と飾りつけを追加。
妻作成の羊毛フェルトを装飾した白色LED照明や、娘作成のおりがみサンタさんなど。



f:id:motokiinfinity8:20181225003722j:plain
f:id:motokiinfinity8:20181225003735j:plain



どんどんと家庭ごとのオリジナリティを出していくと良いと思います。

ソフトウェア

■ストーリ構成検討

妻作成のストーリボードを元にまずは簡単な状態遷移を検討しました。

f:id:motokiinfinity8:20181224233716j:plain

ボタン押しがなければ6状態をループ。ボタン押しが発生すると速やかに2アクションに移行、その後に元状態に戻るイメージです。

それぞれの状態に遷移した際に、イラスト表示&音楽再生を指示する形としました。

■ベース素材

(1)イラスト
google等で「クリスマス」と表示して良さげなイラストを個人利用という事で使わせて頂きました。
これを画像縮小アプリ等をつかって「横320pixel×縦240pixel」に縮小して使用します。

(2)BGM音源
BGMについては、下記サイト様の音源を使わせて頂きました。
クリスマス用音楽素材|著作権フリーの無料音楽素材ダウンロードサイト「ミュージックノート」

波形編集アプリ等をつかって、部分切り出し等をして再生時間を調整したりしました。

(3)LEDイルミネーションパタン
以前に紹介した下記サイトに豊富なLED表示パタンがあります。
www.tweaking4all.com

下記ストーリの表現については、上記サイトの各コードを使います.

①白キラキラ/青キラキラ :Sparkle
②下から白&赤が上がる :Meteor Rain
③炎のエフェクト :Fire
④白&赤キラキラ : Sparkle
⑤下から白&水色が上がる : Meteor Rain
⑥レインボー : Theatre Chase Rainbow
⑦流れ星 : Meteor Rain
⑧ボール飛びあげ :Multi Color Bouncing Balls

ただ、サンプルコードのままだと色々と使いづらかったので、各サンプル関数に以下の工夫を追加しています。

Sparkle : 複数の色を組み合わせれる様に修正
Meteor Rain : 星と残像の色を変更できる様に修正、逆方向への移動も対応
Theatre Chase Rainbow : ボタン押し割込み時、すぐに状態遷移できる配慮を

なお、ライブラリは前回記事をベースに、NeoPixelBusライブラリに変更しています。
ogimotokin.hatenablog.com

ソースコード

ざっとしたコードイメージは以下です。別途GitHubに公開予定でうs。

【M5Stack側】

#include <WiFi.h>
#include <M5Stack.h>
#include "esp_system.h"
#include "AudioFileSourceSD.h"
#include "AudioFileSourceID3.h"
#include "AudioGeneratorMP3.h"
#include "AudioOutputI2S.h"
#include "AudioGeneratorWAV.h"

// initial value for audio
AudioGeneratorWAV *wav;
AudioGeneratorMP3 *mp3;
AudioFileSourceSD *file;
AudioOutputI2S *out;
AudioFileSourceID3 *id3;

//UART2設定
HardwareSerial Serial2(2);

String recv_buffer;
String mode_cmd;
       
void setup(){
  M5.begin();
  Serial.begin(115200);
  Serial2.begin(115200);
  
  // Display設定
  M5.Lcd.clear();
  M5.Lcd.drawJpgFile(SD, "/open.jpg");

  //WAVの場合
  out = new AudioOutputI2S(0,1); 
  out->SetOutputModeMono(true);
  out->SetGain(0.5); 
  mp3 = new AudioGeneratorMP3(); 
  file = new AudioFileSourceSD("/open.mp3");
  mp3->begin(file, out);

  delay(2000);
  recv_buffer = "";
}

void loop() {
  M5.update();

  // Serial受信(内部バッファへのコピー)
  rcv_buf_update();

  // Serialバッファからコマンド取得
  String cmdmsg = getCmdmsg();


  if(cmdmsg != ""){
    Serial.print(cmdmsg);
    mode_cmd = (cmdmsg.substring(0,2));
    Serial.print(mode_cmd);
    Serial2.print(mode_cmd);
    Serial.print(" ");
    Serial2.print(" ");    

    //CMDが正しく受信できた場合の処理
    if(mode_cmd.equals("M0")){
        replayview("/m0.jpg", "/m0.mp3"); //映像&音声更新
    }else if(mode_cmd.equals("M1")){
//        M5.Lcd.clear();
//        M5.Lcd.drawJpgFile(SD, "/m1.jpg");
    }else if(mode_cmd.equals("M2")){
        replayview("/m2.jpg", "/m2.mp3"); //映像&音声更新
    }else if(mode_cmd.equals("M3")){
        replayview("/m3.jpg", "/m3.mp3"); //映像&音声更新
    }else if(mode_cmd.equals("M4")){
        replayview("/m4.jpg", "/m4.mp3"); //映像&音声更新
    }else if(mode_cmd.equals("M5")){
        replayview("/m5.jpg", "/m5.mp3"); //映像&音声更新
    }else if(mode_cmd.equals("M6")){
        replayview("/m6.jpg", "/m6.mp3"); //映像&音声更新
    }else if(mode_cmd.equals("M7")){
//        M5.Lcd.clear();
//        M5.Lcd.drawJpgFile(SD, "/m7.jpg");
    }else if(mode_cmd.equals("P1")){
        replayview("/p1.jpg", "/p1.mp3"); //映像&音声更新
    }else if(mode_cmd.equals("P2")){
        replayview("/p2.jpg", "/p2.mp3"); //映像&音声更新
    }else if(mode_cmd.equals("P3")){
       mp3->stop();
       file = new AudioFileSourceSD("/p3.mp3");
       mp3 = new AudioGeneratorMP3();
       mp3->begin(file, out);
    }else{
    }
  }
   
    ///////////////////////////
    // 再生継続処理
    ///////////////////////////
   if(mp3->isRunning()){
     if (!mp3->loop()) mp3->stop();
   }  
}

//SDカードのAudio Play関数
void replayview(const char *display_file, const char *audio_file){
    mp3->stop();
    M5.Lcd.clear();
    M5.Lcd.drawJpgFile(SD, display_file);

    file = new AudioFileSourceSD(audio_file);
    mp3 = new AudioGeneratorMP3();
    mp3->begin(file, out);
}

//受信用内部バッファの更新
void rcv_buf_update(){
  int size = Serial2.available();
  char c;
  int i;

  for(i=0; i<size; i++){
    c = Serial2.read();
    Serial.print(c);
    recv_buffer += c;
  }
}


//内部バッファからコマンド命令の検索・抽出
String getCmdmsg(){
  String result = "";
  bool incmd = false;
  int start_pos, end_pos = 0;
  int i;
  
  for( i=0; i<recv_buffer.length(); i++){
    if(incmd == false){
      // ":"捜索
      if(recv_buffer.charAt(i) == ':'){
        //Serial.println(" ");
        incmd = true;
        start_pos = i;
      }
    }else{
      // 改行コード捜索
      if(recv_buffer.charAt(i) == 0x0A){
        incmd = false;
        end_pos = i;
        // メッセージ抽出 & メッセージより前のフレーム破棄
        result = recv_buffer.substring( start_pos+1, end_pos);
        recv_buffer = recv_buffer.substring(end_pos+1, recv_buffer.length());
        break;
      }
    }
  }
  return result;
}

【ESP32 DevKit側】

#include <NeoPixelBus.h>
#include <SoftwareSerial.h>

#define PIXEL_PIN    33
#define SWITCH_PIN   32
#define SW_UART_TX 25
#define SW_UART_RX 26
#define PIXEL_COUNT 144

NeoPixelBus<NeoGrbFeature, Neo800KbpsMethod> strip(PIXEL_COUNT, PIXEL_PIN);
SoftwareSerial softSerial(SW_UART_RX, SW_UART_TX, false, 256);

uint32_t g_start_time = 0;
uint32_t g_push_time = 0;
uint32_t g_release_time = 0;
int illumi_mode = 0;
int illumi_mode_old = -1;
int g_switch_state =1;
bool g_switch_is_pushed =false;
bool g_switch_is_released = false;
bool illumi_mode_changed = false;
bool g_push_action = false;

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

  pinMode(SWITCH_PIN, INPUT);
  pinMode(PIXEL_PIN, OUTPUT);

  strip.Begin();
  strip.Show();

  rainbow(30, 0, PIXEL_COUNT-1) ;
  //rainbowCycle(2, 0, 8);
  
  g_start_time = millis(); //時間リセット

  xTaskCreate(DisplayLED, "DisplayLED", 4096, NULL, 10, NULL); //マルチタスク起動
}


/////////////////////////////
// LED表示タスク処理
/////////////////////////////
void DisplayLED(void *pvParameters) {
    while(1){
        //rainbowCycle(2, 0, PIXEL_COUNT-1);

        if( g_switch_is_released == true){
            g_switch_is_released = false;
            g_push_action = true;
            // ボタン単押し時
            if((g_release_time - g_push_time) <500){
              // 上から流星が落ちてくるイメージ
              Serial.println(":P1");
              softSerial.println(":P1");
              meteorRain(RgbColor(0xff, 0xff, 0xff),RgbColor(0xff, 0xff, 0x3f), 10, true, 30, false);
            // ボタン長押し時
            }else{
            // 下からボールが打ちあがるイメージ
                Serial.println(":P2");
                softSerial.println(":P2");
                byte colors[3][3] = { {0xff, 0xff,0}, 
                                      {0x00, 0xff, 0xff}, 
                                      {0xff, 0x00, 0xff} };
                BouncingColoredBalls(3, colors);
            }
           g_push_action = false;
           illumi_mode_old = -1;
           //g_start_time = millis();  //時間リセット
        }else{
          switch(illumi_mode){
            // 白ピカピカ (モード0)
            case 0:
              Sparkle(0xff, 0xff, 0xff, 0);
              break;
            // 白青ピカピカ (モード1) 
            case 1:
              Sparkle(0xff, 0xff, 0xff, 0);
              Sparkle(0x0,  0x0, 0xff, 0);              
              break;
            // 昇りサンタさん(モード2)  
            case 2:
              setAll(0,0,0);
              delay(2000);
              meteorRain(RgbColor(0xff, 0xff, 0xff),RgbColor(0x3f, 0xff, 0xff), 15, true, 55, true);
              illumi_mode = 3; 
              g_start_time = millis();  //時間リセット
              break;
            // 炎を燃やす(モード3)
            case 3:
              Fire(55,120,15);
              break;
            // 白赤ピカピカ (モード4)
            case 4:
              Sparkle(0xff, 0xff, 0xff, 0);
              Sparkle(0xff,  0x0, 0x0, 0);              
              break;
            // 白→青(モード5)
            case 5:
              //delay(500);
              meteorRain(RgbColor(0xff, 0xff, 0xff),RgbColor(0x3f, 0x0, 0x0), 10, true, 20, true);
              meteorRain(RgbColor(0xff, 0xff, 0xff),RgbColor(0x3f, 0x0, 0x0), 10, true, 40, true);
              illumi_mode = 6; 
              g_start_time = millis();  //時間リセット
              break;
            // レインボー (モード6)
            case 6:
              //rainbowCycle(2, 0, PIXEL_COUNT-1);
              rainbow(10, 0, PIXEL_COUNT) ;
              break;
           // レインボーピカピカ (モード7) 
            case 7:
              theaterChaseRainbow(50, 0,  PIXEL_COUNT);
              break;
           } 
           delay(1);
        }
    }
}


/////////////////////////////
// Main処理
/////////////////////////////
void loop(){

  // Pushアクション中は経過時間をカウントしない
  if(g_push_action == true){
     g_start_time = millis();  //時間リセット
  }


  // イルミネーションモード選択
  // 白ピカピカ (モード0)  一定時間ループ
  // 白青ピカピカ (モード1) 一定時間ループ
  // 昇り流れ星 (モード2)  一回のみ
  // 炎を燃やす(モード3)  一定時間ループ
  // 白赤ピカピカ (モード4)  一定時間ループ
  // 水色 下から上 (モード5)   1回のみ
  // レインボー (モード6)    一定時間ループ
  // レインボーピカピカ (モード7)    一定時間ループ
  if(illumi_mode ==0 && Time_Mesure(g_start_time) > 16000){
    illumi_mode = 1;
  }else if(illumi_mode ==1 && Time_Mesure(g_start_time) > 16000){
    illumi_mode = 2; 
  }else if(illumi_mode ==3 && Time_Mesure(g_start_time) > 12000){
    illumi_mode = 4; 
  }else if(illumi_mode ==4 && Time_Mesure(g_start_time) > 15000){
    illumi_mode = 5; 
  }else if(illumi_mode ==6 && Time_Mesure(g_start_time) > 20000){
    illumi_mode = 7; 
  }else if(illumi_mode ==7 && Time_Mesure(g_start_time) > 20000){
    illumi_mode = 0;
  }


  //モード変更確認
  if(illumi_mode != illumi_mode_old && g_push_action == false){
    Serial.print(":M");
    softSerial.print(":M");
    Serial.println(illumi_mode);
    softSerial.println(illumi_mode);
    g_start_time = millis();  //時間リセット
  }
  illumi_mode_old = illumi_mode;
  
  //スイッチ入力確認
  int switch_tmp = digitalRead(SWITCH_PIN);
  //Serial.print(switch_state);
  if(switch_tmp == 0 && g_switch_state == 1){
     g_switch_is_pushed = true;
     g_push_time = millis();
  }else if(switch_tmp == 1 && g_switch_state == 0){
     g_switch_is_released = true;
     g_release_time = millis();
  }
  g_switch_state = switch_tmp;
  delay(10);
  
}

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

おわりに

今回は、M5Stack Advent Calendar 2018 という機会を頂き、良い意味で自分にプレッシャーを与えつつ、1週間の短期間で集中して作成できた。
妻や子供たちと一緒にモノづくりした事で、完成した時の嬉しさもひとしお。
改めて、モノづくりの楽しさの原点、「誰かに喜んでもらえる嬉しさ」「自分が最大限楽しめる事」をかみしめる事が出来ました!

そして、せっかく作ったLEDイルミネーションパタンなので、他のデバイスからの無線通信を受けてLED表現を変える様な機器に応用したいと思います!

二輪走行ロボット「mBot」をWifiコントローラ(息子リハビリ用)で操作してみた

はじめに

前回は息子用モビリティを作成しましたが、そちらのブラッシュアップに現在取り組み中。特に、姿勢保持の課題に現在トライ中。

ogimotokin.hatenablog.com
ogimotokin.hatenablog.com

その一方で、ジョイスティックの操作感に不慣れ」という課題に対応するため、以下にしてジョイスティックの操作するモチベーションを上げる取り組みの必要性を感じました。
その操作練習のためにモビリティを使う手もありましたが、
 ・広くない我が家にて、操作練習のために常時モビリティを置いておくのが困難ために、同じ操作コントローラで動く小型二輪走行ロボットを作ってみました!

f:id:motokiinfinity8:20181220005017j:plain
f:id:motokiinfinity8:20181220005153j:plain



小型二輪走行ロボットはいくつか試してみたのですが、個人的に好きなのは
MakeBlock社のmBot です。

mBot robot kit - educational programmable robot | Makeblock®

子供のプログラミング教育用のロボットです。専用アプリでScratchベースの開発が出来る事に加えて、搭載されているマイコンArduinoベースのため、普通にArduinoベースで開発できる!
これを選んだ理由としては、
 ・二輪駆動が滑らかかつコンパクト
 ・筐体が頑丈で、多少の乱暴な扱いにも耐えられる
 ・見た目が可愛い (超音波センサーを目に見立てたデザインが子供ウケしそう)

といったとこ。

しかし、今回の私の作成した操作コントローラとは直接接続できない。
理由は、「無線モジュールが搭載されていない」点。
mBotの標準オプションとして、Blutooth搭載版はあるが、Classicのみ対応(BLE非対応)である。
一方で、今回私が使用しているジョイスティックはESP32モジュールのためBlutoothは対応しているのだが、現状世の中で普及しているソフトライブラリはBLEのため、そのままでは使うのは難しい。
BLE対応モジュールをmBotに搭載する事も検討したが、
「そもそもESP32間でどうやってペアリングするんだろう」
と悩みはじめたので、一旦これも保留。

結局 mBotに 無線対応モジュールとして最も小型化可能な ESP8266 を搭載する事にしました。

完成品


動画の通り、小型ロボットにした事で息子自身は自分の目線で動く事で親近感が沸いたらしく、それ以降はかなり積極的にジョイスティックを操作する様になりました!!
毎日使い続けようという工夫、だいじ!

しかし、前後の突起物は寝転んで遊ぶ息子には危なそうだなぁ。。
なので、あそこにはカバーが必要そう!という事で、早速3Dプリンターでカバーを設計。
既製品をベースにして、足りない部分は個々でカスタマイズ!

ただのカバーではなく、ちょっとした『遊びゴコロ』も忘れずに!
前側はカワイイお手て、そして後ろ側は……そりゃあカワイイおしりでしょ!!
という事でと無駄にモデリング頑張って、3Dプリンタで出力。

結果、なかなか愛嬌溢れた子になっちゃいました(笑)



f:id:motokiinfinity8:20181220005017j:plain



f:id:motokiinfinity8:20181220004951j:plain





その愛嬌ある見た目と、持ち運んでも壊れない頑丈さのおかげで、息子の通う療育園でも使ってもらえる様になりました!

息子の療育園お友達、普段硬直が激しくモノを触ることが出来ないのですが、ロボットの操作を行った時は硬直なく自分の意思で動かせたらしいです!
お母さんが涙流して喜んでくれたり、「うちにも欲しい」という話も頂いたりで、とても胸が熱くなりました!


ハードウェア構成

ハードウェア構成は以下。

f:id:motokiinfinity8:20181220005845j:plain

mBotに無線対応マイコンESP8266をUARTで接続するだけの構成。
ただし、mBotは5V系マイコン、ESP8266は3.3V系マイコンのため、両者間にレベル変換ICをはさむ必要があります。
レベル変換ICは以下のものを使用。
NTB0104 レベルシフタ ピッチ変換済みモジュール - スイッチサイエンス

そして、ESP8266を使ったもう一つの理由、
それは……頑張ってmBotマイコン筐体内に収める事
これにより子供のおもちゃとしての堅牢性を確保します!(大事)
そのため、具体的な回路図/レイアウトを検討してみた。

まず、接続方法から。

github.com

上記サイトからmBot側の回路図を見てみる。当初は、UART信号としてmBot側はDO端子(RXD)/D1端子(TXD)を使う予定だったが、これだとデバッグのために使うSerial(USB)とバッティングしてしまい開発上面倒くさい事になる。
私の開発スタイルは基本的に速度重視なので、Software SerialでUART信号をソフト対応する事にするため、任意のデジタル端子2系統に変更する事にした。

レイアウトを見ていると、mBot中央部にESP8266モジュールサイズ程度なら入りそうなスペースと、その隣接に5V+GND+デジタル2Pinが出力されていてピンヘッダを手付けすれば使えそうな端子があるので、ここに収まる基板サイズを作ればよさそうです。

f:id:motokiinfinity8:20181220234606j:plain
f:id:motokiinfinity8:20181220005328j:plainf:id:motokiinfinity8:20181220005344j:plain

作ったらこんな感じです。結構コンパクトに収める事ができました!
二つの基板間はピンヘッダメスで直接基板を刺す事で、配線をなくし、堅牢性を上げました。

外装は自由にカスタマイズで!

私はmBotの前後突起物が息子の目に刺さらないためのカバーに遊び心を加えたものをfusion360モデリング3Dプリンタで作りました

f:id:motokiinfinity8:20181220235322p:plain
f:id:motokiinfinity8:20181220071058j:plain


ソフトウェア構成

ソフトウェアに関して、ジョイスティックからの操作命令は先日に記載したモビリティと同じ仕様にしています。
ogimotokin.hatenablog.com

また、mBotのライブラリについては公式で用意されているので、以下を参考にしました。
github.com

一つ方針に悩んだのは、mBot / ESP8266 のどちらを本体頭脳にするのか、です。
ESP8266の方が処理性能は高いのですが、mBotの方が他のデバイス(ブザー/LED等)が接続されている事、今後の拡張性、メンテナンス性を考えると、mBotを本体頭脳に設定しました。(こうすれば別のロボットを作った際でも、ESP8266側のソフトはそのまま使いまわせるメリットを選択しました)

なので、ESP8266は、コントローラから無線経由で送信されたコマンド情報をUARTに変換するだけのシンプルな作りにしました。

また、mBot側は、今回
 ・ジョイスティック方向に応じてモータ制御を実施 (MeDCMotor.run関数)
 ・ブザー音で行動予告 (MeBuzzer.tone関数)
 ・カラーLEDで方向と色を紐づけ (MeRGBLed.setColor関数)

の機能も加えています。

全ソースは下記GitHubにも置いています。
(コード自体は汚いですが、あくまでラピッド開発的に開発しているので、とにかく自分での分かりやすさと使いまわしやすさを優先している感じです)
github.com

■mBot側(mBot_arduino.ino)

#include <Arduino.h>
#include <Wire.h>
#include <SoftwareSerial.h>
#include <MeMCore.h>

#define M_SPEED_L 6
#define M_DIR_L 7
#define M_SPEED_R 5
#define M_DIR_R 4
#define SW_UART_TX 11
#define SW_UART_RX 12

#define CMD_SIZE  12
#define STEP_PWM_1MS   80  //1秒ごとに増加するPWM値

SoftwareSerial softSerial(SW_UART_RX, SW_UART_TX);

double angle_rad = PI/180.0;
double angle_deg = 180.0/PI;

String recv_buffer;//
int i = 0;        // 文字数のカウンタ
bool inst_flg = 0;

String cmd_msg, vel_msg;
int vel_linear=0, vel_argular=0;
int motor_inst, motor_state ;
int running_mode; //0: 事前通知モード、1:事前予告なし(通常)モード
int cmd_start_time = 0;


MeBuzzer buzzer;
MeRGBLed rgbled_7(7, 7==7?2:4);
MeDCMotor motor_9(9);
MeDCMotor motor_10(10);
int Speed = 0;

void setup(){
  Serial.begin(19200);
  softSerial.begin(9600);
  motor_inst = 0;
  motor_state = -3;
  running_mode = 0;
  recv_buffer = "";
}

void loop(){
loop_start: 
  String cmdmsg = "";

  // Serial受信(内部バッファへのコピー)
  rcv_buf_update();
  
  // Serialバッファからコマンド取得
  cmdmsg = getCmdmsg();
  if(cmdmsg != ""){
    //Serial.print(cmdmsg);
    cmd_msg = (cmdmsg.substring(0,3));
    vel_msg = (cmdmsg.substring(3.7));
    running_mode = (cmdmsg.substring(7,8)).toInt();
  
    // MDC (Mobility Direction Control)の場合
    int motor_inst_tmp = motor_inst;
          
    if(cmd_msg.equals("MDC")){
        if((vel_msg.substring(0,2)).equals("-9")) motor_inst = -4;
        if((vel_msg.substring(0,2)).equals("+0")) motor_inst =  0;
        if((vel_msg.substring(0,2)).equals("+9")) motor_inst =  4;
        if((vel_msg.substring(2,4)).equals("-9")) motor_inst -= 1;
        if((vel_msg.substring(2,4)).equals("+9")) motor_inst += 1;
    }

    // レバー操作のチャタ防止対策 2回連続で同じコマンドの場合のみ有効にする
    if(motor_inst_tmp != motor_inst && motor_inst != 0 && running_mode == 0 ){
         delay(100);
         goto loop_start;
    }

    Serial.print("Motor Instructiont:"); 
    Serial.println(motor_inst);   
  }

  //モータ制御
  if(Time_Mesure(cmd_start_time) < 1000){
    Speed = 96;
  }else{
    Speed = (int)(96 + STEP_PWM_1MS * (float)(Time_Mesure(cmd_start_time)-1000) / 1000);
  }

  if(running_mode ==2){
    Speed = 96;
  }

  if(Speed > 255) Speed = 255;

  Serial.print("  ");
  Serial.println(Time_Mesure(cmd_start_time));
  Serial.print("  ");
  Serial.println(Speed);

  // Motor制御命令に変更があった場合に停止 & 音源再生
  if((motor_inst != motor_state) && motor_inst != 0 && running_mode == 0){
    Serial.println("Motor Instruction input!"); 
    // Motor停止
    motor_9.run(0);
    motor_10.run(0);
     switch(motor_inst){
          case  -5: //右後ろ
            (省略)
          case -4:  //後ろ
            (省略)
          case  -3: //左後ろ
            (省略)
          case  -1: //右 
           rgbled_7.setColor(1,0,255,255); //水色
           rgbled_7.setColor(2,0,0,0); //無色
           rgbled_7.show();
           buzzer.tone(659, 500);   //E5音階を500ms 
           break;
          case  1:  //左
           rgbled_7.setColor(1,0,0,0); //無色
           rgbled_7.setColor(2,0,255,0); //緑色
           rgbled_7.show();  
           buzzer.tone(440, 500);   //A4音階を500ms  
           break;
          case  3:  //右前 
            (省略)
          case  4:  //前
           rgbled_7.setColor(0,255,255,0); //黄色
           rgbled_7.show();  
           buzzer.tone(523, 500);   //C5音階を500ms 
           break;
          case  5:  //左前
            (省略)
        }  
  }
  
  int leftSpeed, rightSpeed;
  switch(motor_inst){
      case  -5: //右後ろ
        (省略)
      case -4:  //後ろ
        (省略)
      case  -3: //左後ろ 
        (省略)
      case  -1: //右
        leftSpeed = Speed;
        rightSpeed = -1*Speed;
        rgbled_7.setColor(1,0,255,255); //水色
        rgbled_7.setColor(2,0,0,0); //無色
        rgbled_7.show();  
        break;
      case  1:  //左
        leftSpeed = -1*Speed;
        rightSpeed = Speed;
        rgbled_7.setColor(1,0,0,0); //無色
        rgbled_7.setColor(2,0,255,0); //緑色
        rgbled_7.show();  
       break;
      case  3:  //右前
        (省略)
     case  4:  //前
        leftSpeed = Speed;
        rightSpeed = Speed;
        rgbled_7.setColor(0,255,255,0); //黄色
        rgbled_7.show();  
        break;
      case  5:  //左前
        (省略)
      case  0:  //なし
        leftSpeed = 0;
        rightSpeed = 0;
        rgbled_7.setColor(0,0,0,0); //無色
        rgbled_7.show();  
        break;
    }
    motor_9.run((9)==M1?-(leftSpeed):(leftSpeed));
    motor_10.run((10)==M1?-(rightSpeed):(rightSpeed));



  //情報更新
  delay(10);
  if(running_mode != 1){
    if(motor_inst != motor_state || motor_inst ==0 ) cmd_start_time = millis();  // 命令情報が変わった場合、時間リセット
  }else{
    if(abs(motor_inst-motor_state) >1 || motor_inst ==0 ) cmd_start_time = millis();  // 命令情報が前後で変わった場合、時間リセット
  }
  motor_state = motor_inst; //状態更新    
}


//受信用内部バッファの更新
void rcv_buf_update(){
  int size = softSerial.available();
  char c;
  int i;

  for(i=0; i<size; i++){
    c = softSerial.read();
    recv_buffer += c;
  }
}

//内部バッファからコマンド命令の検索・抽出
String getCmdmsg(){
  String result = "";
  bool incmd = false;
  int start_pos, end_pos = 0;
  int i;
  
  for( i=0; i<recv_buffer.length(); i++){
    if(incmd == false){
      // STX捜索
      if(recv_buffer.charAt(i) == 0x2){
        incmd = true;
        start_pos = i;
      }
    }else{
      // ETX捜索
      if(recv_buffer.charAt(i) == 0x3){
        incmd = false;
        end_pos = i;
        // メッセージ抽出 & メッセージより前のフレーム破棄
        result = recv_buffer.substring( start_pos+1, end_pos);
        recv_buffer = recv_buffer.substring(end_pos+1, recv_buffer.length());
        break;
      }
    }
  }
  return result;
}


■ESP8266側(mBot_esp8266.ino)

#include <ESP8266WiFi.h>
#include <WiFiUdp.h>
#include <SoftwareSerial.h>

#define SerialPort Serial
#define SW_UART_TX 5
#define SW_UART_RX 4

#define CMD_SIZE  12

SoftwareSerial softSerial(SW_UART_RX, SW_UART_TX);


const char ssid[] = "ESP32_MOBILITY"; // SSID
const char password[] = "esp32_con";  // password
const int WifiPort = 8000;      // ポート番号

const IPAddress HostIP(192, 168, 11, 1);       // IPアドレス
const IPAddress ClientIP(192, 168, 11, 2);       // IPアドレス
const IPAddress subnet(255, 255, 255, 0); // サブネットマスク
const IPAddress gateway(192,168, 11, 0);
const IPAddress dns(192, 168, 11, 0);
bool wifi_connect;

WiFiUDP Udp;
uint8_t WiFibuff[CMD_SIZE];
int motor_inst, motor_state ;
String cmd_msg, vel_msg;
int vel_linear=0, vel_argular=0;
int running_mode; //0: 事前通知モード、1:事前予告なし(通常)モード


void setup() 
{
  //PIN設定
  Serial.begin(9600);
  softSerial.begin(9600);
  motor_inst = 0;
  running_mode = 0;
  
  // Wifi設定 (SSID&パス設定)
  wifi_connect = false;
  WiFi.mode(WIFI_AP);
  WiFi.softAP(ssid, password);
  delay(100);
  WiFi.softAPConfig(HostIP, HostIP, subnet); // IPアドレス設定

  Serial.print("AP IP address: ");
  IPAddress myAddress = WiFi.softAPIP();
  Serial.println(myAddress);

  Udp.begin(WifiPort);  // UDP通信開始
  Serial.println("Starting UDP");
  
  Serial.print("Local port: ");
  Serial.println(WifiPort);

}

void loop() {

loop_start:

  int packet_size = Udp.parsePacket();

  if (packet_size > 0) {
    //Serial.println("Serial Received");   
    rcvWiFi();    
    if(WiFibuff[0] == 0x2){
          String s_buf = String((char*)WiFibuff);
          Serial.write(WiFibuff, CMD_SIZE);
          softSerial.write(WiFibuff, CMD_SIZE);   
      }
  }

void rcvWiFi() {
  Udp.read(WiFibuff, CMD_SIZE);
  //Serial.print(WiFibuff);
  Udp.flush();
}

おわりに

今回頑張って mBotの筐体内に無線モジュールを入れ込んだおかげで、頑丈になっており、息子の療育園への運搬などで壊れる心配もなく
かなり安心して使ってもらえる様になりました! 子供って、本当に色々な使い方をするので、出来る限り既製品の外装部をうまく利用していくやり方がよいのかなぁ、と感じてます!

モビリティMottoyと同じ通信IFにしてありますので、これから新しいコントローラを作ったら「モビリティ」も「小型ロボット」もどちらも動かす事ができるので、色々と応用が広がりそうです!

療育園のお母さんからも「是非欲しい!」というお声を頂いたのですが、残念ながら現状だとなかなか高いお値段になってしまいます。(mBotだけで1万円以上しますので…)
ただ、mBotだと今回の用途にはオーバースペックなので、療育園リハビリ用にはもっと簡単に&安く作れる手段を検討していきたいと思います。

ESP32(M5Stack)+LED:Neopixelを使ってみた② (ライブラリ見直し)

はじめに


f:id:motokiinfinity8:20181201020813j:plain


以前の記事で、無線対応マイコンESP32モジュールに、お馴染みのマイコン搭載LED “NeoPixel" を搭載して、子供が楽しくリハビリするための操作ジョイスティックを作ってみた。

ogimotokin.hatenablog.com

そう、この2つのデバイス組み合わせは、光りモノが大好きな『子供向けのIoTおもちゃ』の作成には必須ともいえる組み合わせなのです!
・Alexaから操作できるIoTイルミネーション
スマホから簡単におもちゃのLED色を操作

など、特にこれから冬にかけてイルミネーションの季節という事もあり、可能性がどんどん広がる!
スマホや無線からLEDを光らせるのに、良い組み合わせ)


しかし、現状使っている NeoPixelの標準となっているAdafruitのライブラリだとどうも動作が安定しない。

github.com

特に、無線通信を行っている時、点灯させていないはずのLEDが時々ノイズが発生した様に一瞬光ってしまう事があるフリッカー現象)
無線が駆動しているか分かって良いともいえるのですが、これでは流石にカッコ悪い。
(これがまた、目立つんだな。。。)

最初はシリアル信号に入る物理ノイズの可能性を疑いましたが、オシロで信号を観測したところ、ノイズ発生時はデジタル信号的に出力を発しているので、どちらかというソフトバグくさいなぁ…

じわじわ進む: ESP32で33個以上のNeoPixelを使うと制御が乱れる
で紹介されていたDisable Interrupや1ms delayも効果なし。

今後に向けて難儀していたところ、別のライブラリが素性が良さそうとの情報を得たので試したみたいと思う。

ライブラリ変更 (NeoPixelBus) への置き換え

今回置き換えるライブラリは以下の「NeoPixelBus」になります。

github.com

Adafruitベースなので、ほとんど関数の使い方が同じなのでありがたい!
なので、テキストエディタの置換機能のみで変更可能。

以下が変更イメージ

#include

#include

Adafruit_NeoPixel strip = Adafruit_NeoPixel(PIXEL_COUNT, PIXEL_PIN, NEO_GRB + NEO_KHZ800);

NeoPixelBus strip(PIXEL_COUNT, PIXEL_PIN);

strip.begin();

strip.Begin();

strip.show();

strip.Show();

strip.setPixelColor(i, strip.Color(0, 0, 0));

strip.SetPixelColor(i, RgbColor(0, 0, 0));


変更はたったこれだけ(笑)


そして、実際に動作を試してみたら、、、
「フリッカー課題が解決されてる!!」

無線通信が行われていようが全くチラつきがおこらず、安定している!
これなら問題なしなので、今度から積極的にNeoPixelBusライブラリを使っていこう!

修正したソースは GitHuに置きましたよー
github.com


今日は、細かい技術話になりましたが、こんなところで!

息子向けポータブルモリビリティを改造してみた② (デバイス間通信ソフトなど)

前回のおさらい

前回記事で記載した通り、息子などの電動乗り物への搭乗機会を増やすため、子供用電動乗り物「mottoy」を改造してみました。
ogimotokin.hatenablog.com

今回は前回で書ききれなかったソフトウェアの話を簡単にします。

ソフト構成の考え方

今回のMottoy改造の様に「コントロール部」と「モータ部」に異なるマイコンに分かれている場合、そのマイコン間で通信のやりとりが必要になってきます。そのため、その通信メッセージのルールを決めてやる必要があります。この検討で大事なのは、「今後に接続デバイスを増やす事を想定して拡張しやすくしておくか」という拡張性と、「いかに複雑にしすぎず分かりやすいルールにするか」という開発速度とのバランスです。
f:id:motokiinfinity8:20181104043251j:plain

業務や商用で検討する場合、出来る限り通信エラー耐性を上げるために
フッタに「チェックサム(伝送エラー)」やら「信頼性を上げるためのハンドシェイク」等様々な耐性強化の工夫を入れるものですが、
開発時間の限られる個人製作物においてその細かい箇所に時間をかける事自体が本質ではないため、必要最低限の絞り込みを実施した感じです。
「あれこれ悩む前にまずはざっと作ってみる」
こういう判断も、ラピットプロト開発ならではかな、と思います。

この辺りは色々と賛否両論あると思いますし、私も自分のやり方が正解とは思っていないので、是非とも有識者・玄人の皆様のやり方をご教示頂けるとありがたいです( ^^) _



独自通信メッセージルールの例

という訳で、今回のやりたい事を実現するための「独自伝送メッセージルール」を定義してみました。

今回は、操作デバイスから「モビリティの移動したい方向」を伝える独自ルールになります。

■モビリティ移動方向
(コマンド名:MDC/Mobility Direction Control)

型は Byte (unsigned char)型

1byte   : 0x2 (STX:テキスト開始文字 )
2-4byte : 命令固有文字 "MDC" ASCIIで表示
5-6byte : 前方方向の速度 "+9": 前進 ~ "+0":停止 ~ "-9":後退
7-8byte : 旋回方向の速度 "+9": 左旋回 ~ "+0":停止 ~ "-9":右旋回
9byte  : 選択ジョイコン ("0" : ジョイコン、"1": バイクコントローラ)
10-11byte  : 予備 ("0x0")
12byte : 0x3 (ETX:テキスト終了文字)

これだけでは分かりにくいので、一例をあげます。

例) コントローラから、『最大速度で前進』する場合、以下の12byteを転送します。

[1byte] 2 : STX
[2byte] 77 : M
[3byte] 68 : D
[4byte] 67 : C
[5byte] 43 : +
[6byte] 57 : 9 (前進)
[7byte] 43 : +
[8byte] 48 : 0 (旋回なし)
[9byte] 48 : 0 (ジョイスティック)
[10byte] 48 : 0
[11byte] 48 : 0
[12byte] 3 : ETX


英語&数字&記号はASCIIコードで表現できるので、その値を伝送するイメージです。
変換表は以下のサイトを参考ください。

ASCII文字コード - IT用語辞典


独自ルールを決める上で気にすべきは、「メッセージの先頭&終わりをどうやって判別させるか」です。
バイスの送信側はかならず一定間隔でメッセージ先頭からデータを投げますが、受信側は必ずしもきっちりメッセージ先頭から取れるとは限りません。受信側が別の処理をするのを頑張っていた場合、受信バッファ(郵便受けのポストの様なもの)が溢れてしまいメッセージが途中で切れてしまう事は実使用上でありえます。
なので、「どこまでが一つのメッセージなのか判別できる様にする仕組み」は最低限必要かなぁ、と思います。
判別のさせ方は色々ありますが、私は『普通の文字通信では絶対に使わない値を先頭/末尾に置く』というやり方がよいかな、と思い、ASCII制御コードであるSTX/ETXを使いました。

伝送メッセージルールの応用

上記のメッセージルールを共通にしておけば、モビリティ側とコントローラ側を自由に組み合わせる事が可能です!
例えば、今は『モビリティ』を 『ジョイスティック』&『段ボールトイコン』から操作できますが、これに別の第三のコントローラを作ってもすぐにモビリティが動かせるし、
また別の新しい二輪ロボットを作った場合でも、すぐにこの3つのコントローラがすべて対応してくれる訳です!リソースのない個人開発に置いては、なかなかの効率化ですね♪
f:id:motokiinfinity8:20181123200648j:plain

ちなみに、無線の場合はWifi(UDP)通信、有線の場合はUART通信(Serial)で、同じプロトコルが使いまわせられるので、超便利!!

ソースコード

以下のGithubに置きました。色々と汚いコードですが、ご参考までに。

① モビリティ(Mottoy)側
github.com

ジョイスティックコントローラ側
github.com

Nintendo Labo バイクトイコン側
github.com

更に参考まで、UDP送信側とUDP受信側のコードを抜粋

UDP送信側
#define CMD_SIZE  12

loop{
    省略
    
    String  m_buf = "";
    m_buf.concat("MDC");  // CMD命令設定

     //前方方向速度
     if(g_stick_pos >= 3) m_buf.concat("+9");
     else if(g_stick_pos >= -1) m_buf.concat("+0");
     else m_buf.concat("-9");

      //旋回方向速度
      if((g_stick_pos+8) % 4 == 1) m_buf.concat("+9");
      else if((g_stick_pos+8) % 4 == 0) m_buf.concat("+0");
      else m_buf.concat("-9");  
  
      m_buf.concat("0");  // ジョイコントローラモード 

     byte wifi_buf[CMD_SIZE-2];
     m_buf.getBytes(wifi_buf, CMD_SIZE-2);
     sendWiFi(wifi_buf);
}

/////////////////////////////
// Wifi送信処理
/////////////////////////////
void sendWiFi(uint8_t byteData[]) {
  uint8_t stx = 0x2;
  uint8_t etx = 0x3;
  if (Udp.beginPacket(HostIP,  WifiPort)) {
    Udp.write(&stx,1); //STX送信  
    Udp.write(byteData,CMD_SIZE-2);
    Udp.write(&etx,1); //ETX送信
    Udp.endPacket();
  }
}
UDP受信側
#define CMD_SIZE  12

uint8_t WiFibuff[CMD_SIZE];

loop(){
  中略
    Udp.read(WiFibuff, CMD_SIZE);
    Udp.flush();

    if(WiFibuff[0] == 0x2){
          String s_buf = String((char*)WiFibuff);
          cmd_msg = (s_buf.substring(1,4));
          vel_msg = (s_buf.substring(4.8));
          running_mode = (s_buf.substring(8,9)).toInt();
   }
}

まとめ

今回は、「モビリティ部」と「コントローラ部」の通信メッセージについて、少し書いてみました。今回は「移動させたい方向を指示するだけ」のシンプルなメッセージですが、当然ながら細かい速度量等を設定できるメッセージも別途設定しようと思っています。(アナログジョイスティックの傾き量に応じてモビリティの速度を変える等)
この様に、個人製作物で設計思想を統一しておくのは、今後モノをたくさん作っていった時に威力を発揮するかなぁ、と思います。自分が作ったモノ同士での新たなコラボレーションが簡単にできたり、色々と楽しそう!

息子向けポータブルモリビリティを改造してみた① (ジョイスティック操作)

今回の学び技術: 電動カート改造、回路ハッキング

はじめに(作成動機)

f:id:motokiinfinity8:20181028180440j:plain

歩けない息子が自分の意志で自由に動ける様になるための練習用電動移動機器を作りたいというのが動機です。
電動移動機器を小さな子供のうちから慣れる事は子供の好奇心・成長に大きな効果があるとの研究もあり、息子の将来の可能性を考えると少しでも早く試したい!
本当は電動車椅子でやりたかったのですが、お値段が高く購入には踏み切れないので、まずは子供用おもちゃで操作練習をする事にしました。
しかし、自分で一から作るとなると時間がかかってしまう…(特に子供の体重に耐える強度のものを作ろうとすると金属加工等のノウハウがいるため手をこまねいていました)
という事で、まずは既製品をベースにちょい変更。

目を付けたのが、
子供用電動移動乗り物「mottoy」!!e-akros.com


本機の特徴は、子供用乗り物としては珍しい左右のタイヤが別々に動く独立二輪 
つまり、前後だけでなく左右旋回も出来るので小回りも効き、車タイプの乗り物に比べて移動自由度が高い! 電動車椅子の移動感に近いので、これは練習にうってつけ!

という事で、さっそく購入!

しかし、、、残念ながら、操作レバーが左右2つ別々に付いている。例えば、右側旋回する場合、左レバーは前、右レバーは後ろに倒すイメージですが、、、知的発達も遅れている息子にはまずこの操作インターフェースは扱いきれない……


という事で、このMottoyをベースに、息子に適した操作インターフェースに変更できる様なモビリティベースに改造したいと思う!

要求仕様① 息子が簡単に操作できるジョイスティックでモビリティを操る
 (コントローラの置き場所は自由に変更できる事)
要求仕様② ジョイスティック以外の手段でも操作可能。簡単に交換できる事


※改造はあくまで自己責任ですので、ご注意ください(^^;

完成品

完成動画はこちら

既製品を改造したおかげで、開発期間は約10日間

なお、コントローラ側は以前の記事で開発した
「LEDが光るジョイスティックコントローラ」です。
ogimotokin.hatenablog.com

ちなみに、ジョイスティック以外にも簡単に置き換えれる様にしてみた応用例として、Nintendo LABOのバイクToy-Conで操作してみました。

なお、Nintendo LABOのバイクToy-Conでの操作方法は過去記事を参照ください。
(過去記事の対応から、今回向けに通信ルールを変更したのみです)
ogimotokin.hatenablog.com

ベース機体分析 (分解/ハッキング検討)

まずは制御マイコン部を見てみます。座席部分のフタをあけてみると、制御回路&バッテリーが収まっていました。

f:id:motokiinfinity8:20181028182347j:plain

意外とスカス…もといシンプルに収まっている。これなら何とか回路構成を追えそうだ。
ネジを外して、基板部分を取り出してみます。

f:id:motokiinfinity8:20181104053254j:plainf:id:motokiinfinity8:20181102010016j:plainf:id:motokiinfinity8:20181104052850j:plain

これもまた結構シンプル。両面基板なので配線の流れが追いやすいね。
そんな感じで、簡易回路図+ブロック図に起こしたのが下記図

f:id:motokiinfinity8:20181102010543j:plain


一部間違いなどあるかもしれませんので、その場合はご容赦を ^^) _

ハック構想検討

どの様にハックするか? オリィ研究所で製作された「Nintendo LABOで動く車椅子」を先日体験させて頂きました。
https://twitter.com/Takeru_FTX/status/991267031520460800
それは電動車椅子のハンドルを物理サーボモータ×2で外部から物理ハックするものでした。同様の方法で、本Mottoyもハンドル部分を物理ハックしようと企んでましたが、残念ながら息子が触って壊す可能性があるため、今回はボツ。
なので、少し考え方を変えて、ハンドル部分に配線されているスイッチ自体をハックする「スイッチ回路ハック方式」に変更しました。

注目したのは、マイコンボードとスイッチ間を接続している10Pinのハーネス
ここを分離/延長して、別途ハッキングマイコンに経由させて必要な信号のみをハッキングする方式を考えました。
ハックするのは、左右のレバー操作と繋がっている4本の信号(黄/緑/橙/青)。どうやら、モーターON時は L(GND)に、OFF時はOpenにする仕様なので、
トランジスタによるオープンドレイン回路を構築して、マイコンからモータ制御方向情報に応じたON/OFF情報を通知してもらえばOKそうですね。


必要部品

① M5stack : ESP32 Dev Kitでもよかったが、「動作事前通知(動きを事前に音声でお知らせ)」をやりたかったので簡単にスピーカ再生ができるM5stackを選択
② M5stack プロトタイプモジュール
③ 圧着コネクタ XHP-10 & ハウジング (分岐用ハーネスを作成するため)
④ M5stack電源用 6V→5Vレギュレータ
  L7805CV STMicroelectronics | Mouser 日本
⑤ NPNトランジスタ×4個 

ハードウェア改造

改造後の全体イメージは以下。
f:id:motokiinfinity8:20181102013530j:plain

元の基板とハーネスの間に、自作追加延長ハーネスを追加して、その一部の信号をM5stackにつなげます。
こうすれば、もし将来的に誰かに譲渡する場合、元の市販品の状態に戻す事が出来ます。(Mottoy自体のハードウェアには手を加えていないため)

f:id:motokiinfinity8:20181104044911j:plainf:id:motokiinfinity8:20181104044820j:plainf:id:motokiinfinity8:20181104044849j:plainf:id:motokiinfinity8:20181104044915j:plain

苦戦したのはモータ左後回転。他信号同様にNPNトランジスタ経由でGPIO制御する改造を試みましたが、何故かここだけ制御できませんでした。原因を追っていくと、黄色信号の電圧が特殊であった事でした。 他の端子はOpen時は3.3V電位なのですが、黄色信号のみなぜか 1.0Vのため、M5stackからのGPIO(3.3V)をトランジスタ ベースに接続してもONできない。
ここから、ベース電圧 > コレクタ電圧 になり ベース→コレクタ方向に電流が流れる(トランジスタがPNダイオードとして動作してしまう)事が原因と推定しました。
しかし、トランジシタVf特性上 ベース電圧は0.7V以上である必要がある。悩んだ結果、ベース電圧を抵抗分圧により コレクタ電圧と同程度(1.0V)にする事で無事にON/OFF出来る様になりました。

f:id:motokiinfinity8:20181104052659j:plain

ソフトウェア制作

ソフトウェアについては、ざっと以下の図の思想で作成しています。
f:id:motokiinfinity8:20181104043251j:plain

① コントローラ部とモビリティ部を無線で接続 (どちらかをAP設定にする)
② コントローラ部で得られた操作命令(「前」とか「右」とか)をUDP通信でモビリティに通知。この時、独自で通信コマンドルール(プロトコル)を決めておく
③ ②に応じて、制御モータ量を調整


②のコマンドルールを合わせておけば、モビリティ側は変更する事なく好きなコントローラをどんどん開発&入れ替えする事が出来る、という訳ですね♪
Nintendo LABOのバイクToy-Conによるモビリティ操作も、実は以前に作ったものからコマンドルールのみ改良する作業のみ(1時間程度)で開発できたので、さっと作るには便利な考え方です!

コマンドルール等ソフト設計の詳細はまた別記事に置きたいと思います。
(現状のコードがあまりに突貫すぎて、一部上記思想からずれているので、今後に向けて少しメンテナンスしたいと思います)

まとめと今後

今回息子向けにモビリティをひとまず作ってみましたが、残念ながら搭乗しながらモビリティを動かすとおびえて泣き出してしまうので、幣息子に向けてという観点では、まだまだ改良が必要という事が分かりました。

・安定して座席に座れないので座位固定方法が必要
 →専用シート&ベルトを作る予定)
・コントローラも筐体固定が必要
・動き出しのスピードが速いので、ゆっくり動き出す制御を追加
 → 残念ながらベース機体の回路では不可なので、モータドライバから作り替えが必要そう


ただ、今回の取り組みで、
電動車椅子を購入しなくても簡単に当人に応じたカスタマイズ電動カートを改造作成できるという事が分かりましたので、これから色々と応用可能性が期待できそうです!


引き続き、改良していきたいと思います!