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

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

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

簡単にスピーカ再生機能を追加! 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円程度で売ってます。


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

モジュールの説明は、こちらのサイトが詳しそうです。
DFPlayer_Mini_SKU_DFR0299-DFRobot

なお、モジュールのピン配置はこちら
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
https://www.amazon.co.jp/gp/product/B01MYT6JH9/ref=oh_aui_detailpage_o01_s00?ie=UTF8&psc=1
イルミネーションが詰まって映える様に、1m 144連の密集タイプを使用。これをクリスマスツリーに斜めに巻き付けます

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


f:id:motokiinfinity8:20181225003705j:plain



③ ESP32 開発ボード
https://www.amazon.co.jp/HiLetgo®-ESP32-ESP-32S-NodeMCU開発ボード2-4GHz-Bluetoothデュアルモード/dp/B0718T232Z/ref=sr_1_1_sspa?ie=UTF8&qid=1545661643&sr=8-1-spons&keywords=ESP32&psc=1&smid=A1XEAMF1H64GNM

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


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表示パタンがあります。
https://www.tweaking4all.com/hardware/arduino/adruino-led-strip-effects/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時間程度)で開発できたので、さっと作るには便利な考え方です!

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

まとめと今後

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

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


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


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

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

まとめ

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

ありがとう息子!

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