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

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

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

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表現を変える様な機器に応用したいと思います!