光る!鳴る!ガシャポン『ディスプレイレジェンド変身ベルト』の台座をつくる

犬を飼い始めたり引越しがあったりでなかなか時間がとれなかったのですが、ようやく落ち着いてきましたので工作再開。今回はガシャポン『ディスプレイレジェンド変身ベルト』を100倍(?)楽しく遊ぶための専用台座を作ってみました。

開発背景

元々、ミニチュア、でも機能は本格派、みたいなものをつくるのは好きで、過去に2作品ほど作っています。

特に前者は、私の仮面ライダー玩具改造の処女作とも言えるもので、これが結構評判良くて気を良くした結果、その後延々とライダー玩具の改造を続けていくことになりました。

この2作品、とても気に入っているのですが、ネックは作るのがとても大変、ということです。それぞれの独自ギミックを再現するように作り込もうとすると、それぞれに専用の設計が必要になります。

 

ということで、増やそうにもなかなか増やせない状況だったのですが、そんな折にガシャポンで『ディスプレイレジェンド変身ベルト』というアイテムが出ました。

いうなればギミックのないミニチュアCSMみたいなもので、タイフーン、電王ベルト、ダブルドライバーの3種展開。私に言わせれば一気に改造素材が3つも出てきたようなものなのですが、一個一個作り込む時間はとてもじゃないですがありません。

そこでちょっと発想を変えてみて、一個一個を作り込むのではなく、発光と音声再生を担う共通台座のようなものを作って、その上に変身ベルトをディスプレイ台座ごと載せることで、ベルトごとの「光る!鳴る!」を楽しめる、というふうにしてみてはどうかと考えてみました。こうすれば、ベルトごとのアクションギミックについては再現できなくなるものの、音声と発光パターンのプログラムを追加するだけで簡単にバリエーションを増やすことができます。また、ベルトを取り替えれば音声&発光パターンが変わる、というのも、シンプルに玩具として面白そうです。

ということで、現行作品(『ギーツ』)向けの改造はそっちのけで、台座の台座を開発することにしました。

特徴

遊び方は以下になります。

1. ベルトを台座ごとセットして認識開始

2. 認識完了するとベルト名が表示&読み上げられ、変身待機状態に移行

3. ベルト台座をプッシュして変身!

タイフーン、ダブルドライバー、電王ベルトで、それぞれ異なる音声&発光エフェクトが楽しめます。

また、電王ベルトだけはちょっと特殊で、セットすることでベルト自体も発光するようになっています。

ハードウェア解説

ここからは仕組みのご説明です。まずはいつものように使用した具材から。

ガシャポン『ディスプレイレジェンド変身ベルト』

過去にもこの手の変身ベルトのミニチュア玩具はガシャポンや食玩で展開されていましたが、今回はベルト台座も重要な位置付けになっているのがポイントです。今回の作品はこれがあるおかげで非常に遊びやすく、また全体的にまとまりの良い玩具になりました。

ちなみにベルトと台座はセットではないので、全てのベルトに個別で台座を用意しようと思うと台座を3回引き当てる必要があり、最低でも500円×6回で3,000円必要です。私は幸い、電王ベルトが1個重複しただけで全種コンプリートできました。

Seeeduino XIAO

今回は無線みたいな高級機能は不要なので、シンプルなマイコンを使用しました。後述するRFIDリーダーを既存のライブラリで簡単に使いたかったので、Arduino系から選んでいます。

M5Stack用WS1850S搭載 RFID 2ユニット

今回の作品の肝です。ベルトの台座の底にNFCタグを貼り付け、それをこれで読み取ることで個別認識を実現しています。

こんな感じ。

NFCリーダーを電子工作に使おうと思うと、選択肢は長らくソニー製のRC-S620Sしか選択肢がなかったのですが、遂に新たな手段が現れて、個人的には大喜びしてました。過去に何度か苦労しながら作品を作っていたので(これとかこれとか)。商品自体は少し前から発売されていたみたいですが、商品説明欄には『NFC』という言葉が明示されていなかったので、気づくのが遅れてしまいました。ともあれ、RC-S620Sより遥かに安価でかつ簡単に扱えるので、これからNFCリーダーを電子工作で扱いたい人にはこれが本当にオススメです(※なお、RC-S620Sは生産終了予定とのことなので、今度はこれ一択になりそうな気がしています)。

NFCタグ

リーダーと違ってタグはメチャメチャ選択肢があり、正直使えれば何でも良いのですが、今回はベルト台座の底に貼り付けて使用する関係で、底面のサイズ以下でかつ黒色、という観点でこれにしました。メーカーは全然知りませんが、普通に使えました。

NeoPixelRGB RING WS2812B

昔の作品(これとかこれとか)ではNeoPixelを自分で円形に並べてはんだ付けするという大変面倒なことをやっていたのですが、今はこういう既製品を売ってくださっていて本当にありがたいです。とてもラクだし仕上がりもキレイになります。いくつか種類がありますが、私が今回使用したのは「流れ方向: 時計回り, サイズ: 外径 : 60mm」のものです。

STEMMA QT/Qwiic互換 128×32 OLEDモノクロディスプレイ

実は本質的にはあってもなくても良い部品(←「光る!鳴る!」とは関係ない)なのですが、これでドライバー名が表示される方が玩具としては面白い(←認識してる感が高まる)かなと思いましたので、採用してみました。出来上がりを見ると、正解だったかなと思います。

MP3プレイヤー

私の作品では使い倒してきたMP3プレイヤーです。今回、試しにDFPlayer Proとかも試してみたのですが、何かどうにも扱い辛い…ということで、結局いつものこれに戻ってきてしまいます。SDカードも忘れずに。

マイクロスピーカー 赤/黒リード付 8Ω

今回はこれを2個使用していますが、DFPlayerの出力自体はモノラルなので、2個ある意味は正直ないです。見た目のバランスだけ。ちなみに前出のDFPlayer Proを使うとステレオ再生ができるのですが、玩具でそこまで頑張らんでも良いかな、という感じです。

単4電池ボックス

リチウムイオンポリマー充電池を使う方がコンパクトにはできるのですが、安全面を考えるとあまり使いたくはない部品なので、スペースの制限があまりない作品作りにおいては、最近はできるだけ乾電池を使うようにしています。今回は部品の配置上、単四電池2本用1本用を組み合わせて使用しています。

あとは、タクトスイッチとかバネとかネジももちろん要りますが、私はこの辺りの部品は、要らなくなった玩具の部品を再利用することが多いです。なので、玩具を捨てるときには部品の分解・ストックで毎度時間がかかります。

 

筐体は3Dプリンタで作成しています。使用している3Dプリンタは記事執筆時点で一世代前のものになりますが、Adventurer3 Liteです。ホビー用途ならこれで必要十分です。

使用フィラメントの大部分はPLAのブラックで、発光部分のみPETGのクリアを使用しています。

クリアフィラメントは一巻き持っておくと何かと便利です。

 

筐体部品はこんな感じで、5つの部品に分けて設計・出力しています。

 

部品の回路図はこんな感じで、

実際に配線するとこんな感じ。

最近はんだ付けがかなりやりづらくなってきた感じがするのですが、まさか老眼…?

 

これを筐体に組み付けてみるとこんな感じになります。

NFCリーダーの下にバネとタクトスイッチを配置することで、ベルト台座を押し込む→リーダーが押し込まれる→スイッチが押される、というアクションを実現しています。

 

本体の仕組みはこんな感じですが、あと一つ、電王ベルトについて補足します。これだけ他のベルトと異なり、ベルト台座をセットするとベルト自体が発光するようになっています。

これはどうやって実現しているかというと、

DX版のウィザードリングの仕組みを応用しています。このリングは中を開けると、

こんな感じでアンテナとLED付きの基板のみが入っていて、電池は見当たりません。が、ウィザードライバーに近づけると発光します。これは、ざっくり言うと、NFCリーダーがタグを読み取るために出している電波からアンテナで電力を取り出すことによって実現しています。

ということで、アンテナから基板を外して、

こんな感じで使いたいLEDを繋ぎ、

穴を開けたベルト台座の中に仕込めば、電池不要で発光するベルト台座の出来上がりです。LEDがちょっと奥まった位置にあるのは、あんまり前に出し過ぎると発光が点になってしまってあんまり綺麗に光らなかったためです。

合わせて、電王ベルトの方も光を通すように穴開け加工しています。

 

鋭い方は「わざわざアンテナにLEDを付け直してNFCタグ付きのベルト台座に仕込まなくても、ウィザードリングの中身をそのまま中に入れてしまって認識も発光もさせてしまえば良いのでは?」と思われたかもしれません。それは私も試してみたのですが、

  • ウィザードリングの基板は、今回使用したNFCリーダーでは情報を読めなかった(←おそらく標準ではなく独自通信プロトコル)
  • NFCタグ付きの台座にウィザードリングの中身をそのまま突っ込むと、NFCタグの読取に失敗する(←ウィザードリングの基板が何らか読取に干渉してくる)
  • NFCタグとアンテナの距離を離せば読取はできるが、発光も弱くなってしまう

という結果になりましたので、読取干渉の原因となる基板を外してしまって、純粋に電力を取り出すためだけにアンテナを使用するため、別でLEDだけを繋ぐやり方にしました。

ソフトウェア解説

ソースコードはちょっと長くなってしまったので最後にまとめて掲載します。ここではポイントだけ、箇条書きでいくつかご説明します。

  • 状態遷移図は非常にシンプルで、以下のようになります。
    ベルト台座を押し込むたびにCHANGE_AとCHANGE_Bを行き来していますが、これは変身状態が2種類あるわけではなく、ボタンを押すたびに状態遷移を起こして、それをトリガーに音声再生と発光を開始させるための対応です。
  • NFCリーダーは、ここから入手できる”MFRC522_I2C.cpp”と”MFRC522_I2C.h”を”.ino”ファイルと同じ階層に置くことで利用できるようになります。モジュールがM5Stack向けのものなのでSeeeduino XIAOで使用できるかやや不安でしたが、問題なく使用できました。
  • NFCタグのIDはもちろんタグ毎に異なるので、ご自身が用意されたタグのものに書き換えてください。
  • 玩具上ではタグの認識に時間がかかっているように見えますが、実際はベルト台座を置いた瞬間に認識しています。ディスプレイに”.”を少しずつ表示しているのは、ただの演出です。
  • LEDリングの発光パターンのところのプログラムは、すみません、若干イマイチ感はあるのですが、「動けばいいんだ動けば」の精神でそのままにしてしまっています

最後に音声データについて少しだけ補足です。今回、ベルトを認識するとベルト名を読み上げてくれますが、このときの音声はデザイアドライバーから録音しています。ライダー名の音声はディケイド以降のベルト(orアイテム)にはたくさん収録されていますが、ベルト名の音声というのはかなり珍しい気がします。ある意味、デザイアドライバーがあってこその今回の作品かもしれません。

まとめ

以上、ガシャポン『ディスプレイレジェンド変身ベルト』を100倍(?)楽しむための台座の台座のご紹介でした。前二作のミニチュア変身ベルトとはまた違うベクトルで遊んで楽しいミニチュアベルトが出来たかなと思います。

この後の作品作りの予定としては、今回の仕組みをベースに思いっきり簡略化したもの(NOT ライダー玩具)を一つと、それから、ちょっと毛色の違うアイテムを一つ、作れればと思っています。相変わらず『ギーツ』のアイテムを作る計画はゼロなのですが、特に後者のアイテムは久々に作ってみたくてウズウズしているようものなので、個人的には無問題です。

 

ソースコード

最後にソースコードの全文を掲載します。

#include <SPI.h>
#include <Wire.h>

#define PIN_MP3_RX 0
#define PIN_MP3_TX 1
#define PIN_LED    2
#define PIN_SW     3

#define ON  LOW
#define OFF HIGH

uint8_t sw = OFF;
uint8_t prev_sw = OFF; 

#define STATE_INIT     0
#define STATE_READING  1
#define STATE_READY    2
#define STATE_CHANGE_A 3
#define STATE_CHANGE_B 4

uint8_t state = STATE_INIT;
uint8_t prev_state = STATE_INIT;

#define READING_TIME_MS 3000
unsigned long reading_start_point_ms = 0;

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

#include "MFRC522_I2C.h"
#define  RELEASE_COUNT 3

uint8_t release_counter = 0;

MFRC522 mfrc522(0x28);

#define RIDER_ID_DENO    8
#define RIDER_ID_W      11
#define RIDER_ID_1     101

typedef struct {
  byte tag_id[7];
  uint8_t rider_id;
  char* belt_name_1; // およそ10文字以内ならここだけ指定して1行表示
  char* belt_name_2; // 不要ならNULL
} rider_belt;

rider_belt rider_belts[] = {
  {{0x04, 0x28, 0xD1, 0x78, 0xB6, 0x2A, 0x81}, RIDER_ID_DENO, "DEN-O BELT", NULL},
  {{0x04, 0x17, 0xD1, 0x78, 0xB6, 0x2A, 0x81}, RIDER_ID_W, "DOUBLE",    "DRIVER"},
  {{0x04, 0x27, 0xD1, 0x78, 0xB6, 0x2A, 0x81}, RIDER_ID_1, "TYPHOON",    NULL}
};

uint8_t num_riders = sizeof(rider_belts) / sizeof(rider_belt);

boolean is_belt_on = false;
uint8_t rider_id = 0;
char* belt_name_1 = NULL;
char* belt_name_2 = NULL;

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 32 // OLED display height, in pixels

#define OLED_RESET     -1 // Reset pin # (or -1 if sharing Arduino reset pin)
#define SCREEN_ADDRESS 0x3C ///< See datasheet for Address; 0x3D for 128x64, 0x3C for 128x32
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

#define DOTS_WRITING_START_POINT_MS  800
#define DOTS_WRITING_END_POINT_MS   2400

#define DOT_INTERVAL_MS 50
#define DOT_WIDTH  12
#define DOT_HEIGHT 16
#define DOTS_IN_LINE 10
unsigned long display_start_point_ms = 0;
uint8_t dot_counter = 0;

void draw_text_center(char* text_1, char* text_2){

  display.clearDisplay();

  int16_t x1, y1;
  uint16_t w, h;

  if(text_2 == NULL){
    // スペースが含まれない場合、1行で表示
    display.getTextBounds(text_1, 0, 0, &x1, &y1, &w, &h);
    display.setCursor((SCREEN_WIDTH - w)/2, (SCREEN_HEIGHT - h)/2);
    display.print(text_1);
  }else{
     // スペースが含まれる場合、2行で表示
    display.getTextBounds(text_1, 0, 0, &x1, &y1, &w, &h);
    display.setCursor((SCREEN_WIDTH - w)/2, 0);
    display.print(text_1);
    display.getTextBounds(text_2, 0, 0, &x1, &y1, &w, &h);
    display.setCursor((SCREEN_WIDTH - w)/2, 16);
    display.print(text_2);
  }

  display.display();
}

void clear_display(){
  display.clearDisplay();
  display.display();
}

void control_display(unsigned long now_ms){

  // 状態変化時処理
  if(prev_state != STATE_INIT && state == STATE_INIT){
    clear_display();
  }else if(prev_state == STATE_INIT && state == STATE_READING){
    dot_counter = 0;
  }else if(prev_state == STATE_READING && state == STATE_READY){
    draw_text_center(belt_name_1, belt_name_2);
  }

  // 定常処理
  unsigned long passed_ms = now_ms - reading_start_point_ms;

  switch(state){
  case STATE_INIT:
    break;
  case STATE_READING:
    if(passed_ms < DOTS_WRITING_START_POINT_MS){
      ;
    }else if(DOTS_WRITING_START_POINT_MS <= passed_ms && passed_ms < DOTS_WRITING_END_POINT_MS){
      if(dot_counter < DOTS_IN_LINE){
        display.setCursor(DOT_WIDTH * dot_counter, 0);
        display.print(".");
      }else{
        display.setCursor(DOT_WIDTH * (dot_counter-DOTS_IN_LINE), DOT_HEIGHT);
        display.print(".");
      }
      display.display();
      dot_counter++;
    }else if(DOTS_WRITING_END_POINT_MS <= passed_ms){
      ;
    }
    break;
  case STATE_READY:
    break;
  case STATE_CHANGE_A:
  case STATE_CHANGE_B:
    break;
  default:
    ;
  }
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

#include <DFPlayerMini_Fast.h>
#include <SoftwareSerial.h>
SoftwareSerial ss_mp3_player(PIN_MP3_RX, PIN_MP3_TX);

DFPlayerMini_Fast mp3_player;

// 01フォルダの再生が上手くいかなかったので、02フォルダから
#define SOUND_FOLDER_COMMON 2
#define SOUND_FOLDER_READY  3
#define SOUND_FOLDER_CHANGE 4

#define SOUND_TRACK_ON      1
#define SOUND_TRACK_RELEASE 2

#define SOUND_VOLUME_DEFAULT 14 // 0〜30

void play_sound(uint8_t folder_num, uint8_t track_num){
  mp3_player.playFolder(folder_num, track_num);
  Serial.print(F("Play Folder: "));
  Serial.print(folder_num);
  Serial.print(F(", Track: "));
  Serial.println(track_num);
}

void pause_sound(){
  mp3_player.pause();
}

void control_sound(){
  if(prev_state != STATE_INIT && state == STATE_INIT){
    play_sound(SOUND_FOLDER_COMMON, SOUND_TRACK_RELEASE);
  }else if(prev_state == STATE_INIT && state == STATE_READING){
    play_sound(SOUND_FOLDER_READY, rider_id);
  }else if(prev_state == STATE_READY && state == STATE_CHANGE_A){
    play_sound(SOUND_FOLDER_CHANGE, rider_id);
  }else if(prev_state == STATE_CHANGE_A && state == STATE_CHANGE_B){
    play_sound(SOUND_FOLDER_CHANGE, rider_id);
  }else if(prev_state == STATE_CHANGE_B && state == STATE_CHANGE_A){
    play_sound(SOUND_FOLDER_CHANGE, rider_id);
  }
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

#include <Adafruit_NeoPixel.h>
#define N_LED      20
#define N_HALF_LED 10
#define CLOCKWISE        0
#define COUNTERCLOCKWISE 1

struct color_rgb {
  uint8_t r;
  uint8_t g;
  uint8_t b;
};

struct color_rgb COLOR_BLACK  = {  0,  0,  0};
struct color_rgb COLOR_WHITE  = {127,127,127};
struct color_rgb COLOR_RED    = {127,  0,  0};
struct color_rgb COLOR_BLUE   = {  0,127,127}; // 1号のベルトエフェクトに近づけるため、意図的に{0,0,127}指定にしていない
struct color_rgb COLOR_GREEN  = {  0,127,  0};
struct color_rgb COLOR_PURPLE = {127,  0,127};

int8_t led_index   = 0;
int8_t led_index_r = 0;
int8_t led_index_l = 0;
unsigned long led_pattern_start_point_ms = 0;
int prev_interval_ms = 0;
uint8_t prev_steps = 0;
unsigned long prev_action_point_ms = 0;
boolean is_blink_on = false;
boolean is_circling = false;
boolean is_inc = true;
unsigned long inc_dim_start_point_ms = 0;

// 本来はNEO_RGB指定が正しいはずだが、Color(r,g,b)の色指定でなぜかRとGが入れ替わってしまうため、NEO_GRB指定にしている
Adafruit_NeoPixel pixels = Adafruit_NeoPixel(N_LED, PIN_LED, NEO_GRB);

void led_base_pattern_on(struct color_rgb *color){
  for(uint8_t i=0;i<N_LED;i++){
    pixels.setPixelColor(i, pixels.Color(color->r,color->g,color->b));
  }
}

void led_base_pattern_on_2(struct color_rgb *color_1, struct color_rgb *color_2){
  for(uint8_t i=0;i<N_HALF_LED;i++){
    pixels.setPixelColor(i, pixels.Color(color_1->r,color_1->g,color_1->b));
  }
  for(uint8_t i=N_HALF_LED;i<N_LED;i++){
    pixels.setPixelColor(i, pixels.Color(color_2->r,color_2->g,color_2->b));
  }
}

void led_base_pattern_off(){
  for(uint8_t i=0;i<N_LED;i++){
    pixels.setPixelColor(i, pixels.Color(0,0,0));
  }
}

void led_base_pattern_blink(struct color_rgb *color, unsigned long now_ms, int inverval_ms){
  if(now_ms - prev_action_point_ms >= inverval_ms){
    if(is_blink_on){
      for(uint8_t i=0;i<N_LED;i++){
        pixels.setPixelColor(i, pixels.Color(0,0,0));
      }
    }else{
      for(uint8_t i=0;i<N_LED;i++){
        pixels.setPixelColor(i, pixels.Color(color->r,color->g,color->b));
      }
    }
    is_blink_on = !is_blink_on;
    prev_action_point_ms = now_ms;
  }
}

void led_base_pattern_asym_blink(struct color_rgb *color, unsigned long now_ms, int on_ms, int off_ms){
  if(is_blink_on){
    if(now_ms - prev_action_point_ms >= on_ms){
      for(uint8_t i=0;i<N_LED;i++){
        pixels.setPixelColor(i, pixels.Color(0,0,0));
      }
      is_blink_on = false;
      prev_action_point_ms = now_ms;
    }
  }else{
    if(now_ms - prev_action_point_ms >= off_ms){
      for(uint8_t i=0;i<N_LED;i++){
        pixels.setPixelColor(i, pixels.Color(color->r,color->g,color->b));
      }
      is_blink_on = true;
      prev_action_point_ms = now_ms;
    }
  }
}

void led_base_pattern_inc(struct color_rgb *color, unsigned long now_ms, int interval_ms, uint8_t steps){
  if(inc_dim_start_point_ms == 0 || interval_ms != prev_interval_ms || steps != prev_steps){
    inc_dim_start_point_ms = now_ms;
    prev_interval_ms = interval_ms;
    prev_steps = steps;
  }

  int ms_per_step = interval_ms / steps;
  int current_step = (now_ms - inc_dim_start_point_ms) / ms_per_step;
  if(current_step > steps){
    current_step = steps;
  }
  uint8_t r_step = color->r/steps;
  uint8_t g_step = color->g/steps;
  uint8_t b_step = color->b/steps;

  for(uint8_t i=0;i<N_LED;i++){
    pixels.setPixelColor(i, pixels.Color(r_step*current_step, g_step*current_step, b_step*current_step));
  }
}

void led_base_pattern_inc_2(struct color_rgb *color_1, struct color_rgb *color_2, unsigned long now_ms, int interval_ms, uint8_t steps){
  if(inc_dim_start_point_ms == 0 || interval_ms != prev_interval_ms || steps != prev_steps){
    inc_dim_start_point_ms = now_ms;
    prev_interval_ms = interval_ms;
    prev_steps = steps;
  }

  int ms_per_step = interval_ms / steps;
  int current_step = (now_ms - inc_dim_start_point_ms) / ms_per_step;
  if(current_step > steps){
    current_step = steps;
  }
  uint8_t r_1_step = color_1->r/steps;
  uint8_t g_1_step = color_1->g/steps;
  uint8_t b_1_step = color_1->b/steps;
  uint8_t r_2_step = color_2->r/steps;
  uint8_t g_2_step = color_2->g/steps;
  uint8_t b_2_step = color_2->b/steps;
  for(uint8_t i=0;i<N_HALF_LED;i++){
    pixels.setPixelColor(i, pixels.Color(r_1_step*current_step, g_1_step*current_step, b_1_step*current_step));
  }
  for(uint8_t i=N_HALF_LED;i<N_LED;i++){
    pixels.setPixelColor(i, pixels.Color(r_2_step*current_step, g_2_step*current_step, b_2_step*current_step));
  }
}

void led_base_pattern_dim(struct color_rgb *color, unsigned long now_ms, int interval_ms, uint8_t steps){
  if(inc_dim_start_point_ms == 0 || interval_ms != prev_interval_ms || steps != prev_steps){
    inc_dim_start_point_ms = now_ms;
    prev_interval_ms = interval_ms;
    prev_steps = steps;
  }

  int ms_per_step = interval_ms / steps;
  int current_step = (now_ms - inc_dim_start_point_ms) / ms_per_step;
  if(current_step > steps){
    current_step = steps;
  }
  uint8_t r_step = color->r/steps;
  uint8_t g_step = color->g/steps;
  uint8_t b_step = color->b/steps;
  for(uint8_t i=0;i<N_LED;i++){
    pixels.setPixelColor(i, pixels.Color(r_step*(steps-current_step), g_step*(steps-current_step), b_step*(steps-current_step)));
  }
}

void led_base_pattern_dim_2(struct color_rgb *color_1, struct color_rgb *color_2, unsigned long now_ms, int interval_ms, uint8_t steps){
  if(inc_dim_start_point_ms == 0 || interval_ms != prev_interval_ms || steps != prev_steps){
    inc_dim_start_point_ms = now_ms;
    prev_interval_ms = interval_ms;
    prev_steps = steps;
  }

  int ms_per_step = interval_ms / steps;
  int current_step = (now_ms - inc_dim_start_point_ms) / ms_per_step;
  if(current_step > steps){
    current_step = steps;
  }
  uint8_t r_1_step = color_1->r/steps;
  uint8_t g_1_step = color_1->g/steps;
  uint8_t b_1_step = color_1->b/steps;
  uint8_t r_2_step = color_2->r/steps;
  uint8_t g_2_step = color_2->g/steps;
  uint8_t b_2_step = color_2->b/steps;
  for(uint8_t i=0;i<N_HALF_LED;i++){
    pixels.setPixelColor(i, pixels.Color(r_1_step*(steps-current_step), g_1_step*(steps-current_step), b_1_step*(steps-current_step)));
  }
  for(uint8_t i=N_HALF_LED;i<N_LED;i++){
    pixels.setPixelColor(i, pixels.Color(r_2_step*(steps-current_step), g_2_step*(steps-current_step), b_2_step*(steps-current_step)));
  }
}

void led_base_pattern_blink_slowly(struct color_rgb *color, unsigned long now_ms, int interval_ms, uint8_t steps){
  if(inc_dim_start_point_ms == 0 || interval_ms != prev_interval_ms || steps != prev_steps){
    inc_dim_start_point_ms = now_ms;
    prev_interval_ms = interval_ms;
    prev_steps = steps;
  }

  int ms_per_step = interval_ms / steps;
  int current_step = (now_ms - inc_dim_start_point_ms) / ms_per_step;
  if(current_step > steps){
    current_step = steps;
  }
  uint8_t r_step = color->r/steps;
  uint8_t g_step = color->g/steps;
  uint8_t b_step = color->b/steps;

  if(is_inc){
    for(uint8_t i=0;i<N_LED;i++){
      pixels.setPixelColor(i, pixels.Color(r_step*current_step, g_step*current_step, b_step*current_step));
    }
  }else{
    for(uint8_t i=0;i<N_LED;i++){
      pixels.setPixelColor(i, pixels.Color(r_step*(steps-current_step), g_step*(steps-current_step), b_step*(steps-current_step)));
    }
  }

  if(now_ms - inc_dim_start_point_ms >= interval_ms){
    is_inc = !is_inc;
    inc_dim_start_point_ms = 0;
  }
}

void led_base_pattern_blink_slowly_2(struct color_rgb *color_1, struct color_rgb *color_2, unsigned long now_ms, int interval_ms, uint8_t steps){
  if(inc_dim_start_point_ms == 0 || interval_ms != prev_interval_ms || steps != prev_steps){
    inc_dim_start_point_ms = now_ms;
    prev_interval_ms = interval_ms;
    prev_steps = steps;
  }

  int ms_per_step = interval_ms / steps;
  int current_step = (now_ms - inc_dim_start_point_ms) / ms_per_step;
  if(current_step > steps){
    current_step = steps;
  }
  uint8_t r_1_step = color_1->r/steps;
  uint8_t g_1_step = color_1->g/steps;
  uint8_t b_1_step = color_1->b/steps;
  uint8_t r_2_step = color_2->r/steps;
  uint8_t g_2_step = color_2->g/steps;
  uint8_t b_2_step = color_2->b/steps;

  if(is_inc){
    for(uint8_t i=0;i<N_HALF_LED;i++){
      pixels.setPixelColor(i, pixels.Color(r_1_step*current_step, g_1_step*current_step, b_1_step*current_step));
    }
    for(uint8_t i=N_HALF_LED;i<N_LED;i++){
      pixels.setPixelColor(i, pixels.Color(r_2_step*current_step, g_2_step*current_step, b_2_step*current_step));
    }
  }else{
    for(uint8_t i=0;i<N_HALF_LED;i++){
      pixels.setPixelColor(i, pixels.Color(r_1_step*(steps-current_step), g_1_step*(steps-current_step), b_1_step*(steps-current_step)));
    }
    for(uint8_t i=N_HALF_LED;i<N_LED;i++){
      pixels.setPixelColor(i, pixels.Color(r_2_step*(steps-current_step), g_2_step*(steps-current_step), b_2_step*(steps-current_step)));
    }
  }

  if(now_ms - inc_dim_start_point_ms >= interval_ms){
    is_inc = !is_inc;
    inc_dim_start_point_ms = 0;
  }
}

void led_base_pattern_circle(struct color_rgb *color, unsigned long now_ms, int interval_ms, uint8_t v){
  if(now_ms - prev_action_point_ms >= interval_ms){

    pixels.setPixelColor(led_index, pixels.Color(color->r, color->g, color->b));
    int8_t prev_led_index = led_index - v;
    if(prev_led_index >= 0){
      pixels.setPixelColor(prev_led_index, pixels.Color(0,0,0));
    }else{
      pixels.setPixelColor(N_LED + prev_led_index, pixels.Color(0,0,0));
    }

    led_index = led_index + v;
    if(led_index >= N_LED){
      led_index = led_index - N_LED;
    }

    prev_action_point_ms = now_ms;
  }
}

void led_base_pattern_tri_circle(struct color_rgb *color_1, struct color_rgb *color_2, unsigned long now_ms, int interval_ms, uint8_t v){
  if(now_ms - prev_action_point_ms >= interval_ms){
    int8_t led_index_1 = led_index;
    int8_t led_index_2 = led_index_1 + 7;
    int8_t led_index_3 = led_index_2 + 6;
    if(led_index_2 >= N_LED){
      led_index_2 = led_index_2 - N_LED;
    }
    if(led_index_3 >= N_LED){
      led_index_3 = led_index_3 - N_LED;
    }
    pixels.setPixelColor(led_index_1, pixels.Color(color_1->r, color_1->g, color_1->b));
    pixels.setPixelColor(led_index_2, pixels.Color(color_2->r, color_2->g, color_2->b));
    pixels.setPixelColor(led_index_3, pixels.Color(color_2->r, color_2->g, color_2->b));

    int8_t prev_led_index   = led_index - v;
    int8_t prev_led_index_1 = prev_led_index;
    int8_t prev_led_index_2 = prev_led_index_1 + 7;
    int8_t prev_led_index_3 = prev_led_index_2 + 6;
    if(prev_led_index_1 < 0){
      prev_led_index_1 = N_LED + prev_led_index_1;
    }
    if(prev_led_index_2 >= N_LED){
      prev_led_index_2 = prev_led_index_2 - N_LED;
    }
    if(prev_led_index_3 >= N_LED){
      prev_led_index_3 = prev_led_index_3 - N_LED;
    }
    pixels.setPixelColor(prev_led_index_1, pixels.Color(0,0,0));
    pixels.setPixelColor(prev_led_index_2, pixels.Color(0,0,0));
    pixels.setPixelColor(prev_led_index_3, pixels.Color(0,0,0));    

    led_index = led_index + v;
    if(led_index >= N_LED){
      led_index = led_index - N_LED;
    }

    prev_action_point_ms = now_ms;
  }
}

void led_base_pattern_fill_circle(struct color_rgb *color, uint8_t start_led_num, uint8_t direction ,unsigned long now_ms, uint16_t period_ms){
  if(!is_circling){
    led_index = start_led_num;
    is_circling = true;
  }

  uint16_t interval_ms = period_ms / N_LED;

  if(now_ms - prev_action_point_ms >= interval_ms){
    pixels.setPixelColor(led_index, pixels.Color(color->r, color->g, color->b));
    if(direction == CLOCKWISE){
      led_index++;
      if(led_index == N_LED){
        led_index = 0;
      }
    }else{
      led_index--;
      if(led_index < 0){
        led_index = N_LED - 1;
      }
    }

    prev_action_point_ms = now_ms;
  }
}

void led_base_pattern_fill_circle_2(struct color_rgb *color_r, struct color_rgb *color_l, unsigned long now_ms, uint16_t period_ms){
  if(!is_circling){
    led_index_r =  9;
    led_index_l = 10;
    is_circling = true;
  }

  uint16_t interval_ms = period_ms / N_HALF_LED;

  if(now_ms - prev_action_point_ms >= interval_ms){
    pixels.setPixelColor(led_index_r, pixels.Color(color_r->r, color_r->g, color_r->b));
    pixels.setPixelColor(led_index_l, pixels.Color(color_l->r, color_l->g, color_l->b));
    led_index_r--;
    if(led_index_r < 0){
      led_index_r = 0;
    }
    led_index_l++;
    if(led_index_l >= N_LED){
      led_index_l = N_LED - 1;
    }

    prev_action_point_ms = now_ms;
  }
}

void led_pattern_init(unsigned long now_ms){
  led_base_pattern_blink_slowly(&COLOR_WHITE, now_ms, 1000, 20);
}

void led_pattern_reading(unsigned long passed_ms, unsigned long now_ms){
  if(passed_ms < 500){
    led_base_pattern_dim(&COLOR_WHITE, now_ms, 500, 10);
  }else if(500 <= passed_ms && passed_ms < 550){
    led_base_pattern_off();
  }else{
    led_base_pattern_circle (&COLOR_WHITE, now_ms, 20, 1);
  }
}

void led_pattern_ready(unsigned long passed_ms, unsigned long now_ms){
  switch(rider_id){
  case RIDER_ID_DENO:
    if(passed_ms < 1000){
      led_base_pattern_on(&COLOR_RED);
    }else if(1000 <= passed_ms && passed_ms < 1200){
      led_base_pattern_off();
    }else if(1200 <= passed_ms){
      led_base_pattern_asym_blink(&COLOR_RED, now_ms, 250, 1300);
    }
    break;
  case RIDER_ID_W:
    if(passed_ms < 1200){
      led_base_pattern_on_2(&COLOR_PURPLE, &COLOR_GREEN);
    }else if(1200 <= passed_ms){
      led_base_pattern_blink_slowly_2(&COLOR_PURPLE, &COLOR_GREEN, now_ms, 490, 10);
    }
    break;
  case RIDER_ID_1:
    if(passed_ms < 800){
      led_base_pattern_on(&COLOR_RED);
    }else if(800 <= passed_ms && passed_ms < 1000){
      led_base_pattern_off();
    }else if(1000 <= passed_ms && passed_ms < 1500){
      led_base_pattern_dim(&COLOR_RED, now_ms, 400, 10);
    }else if(1500 <= passed_ms && passed_ms < 2800){
      led_base_pattern_fill_circle(&COLOR_RED, 9, COUNTERCLOCKWISE , now_ms, 1300);
    }else if(2800 <= passed_ms){
      led_base_pattern_on(&COLOR_RED);
    }
    break;
  default:
    ;
  }
}

void led_pattern_change(unsigned long passed_ms, unsigned long now_ms){
  switch(rider_id){
  case RIDER_ID_DENO:
    if(passed_ms < 1000){
      led_base_pattern_dim(&COLOR_RED, now_ms, 900, 10);
    }else if(1000 <= passed_ms && passed_ms < 1600){
      led_base_pattern_dim(&COLOR_RED, now_ms, 500, 10);
    }else if(1600 <= passed_ms && passed_ms < 2300){
      led_base_pattern_dim(&COLOR_RED, now_ms, 600, 10);
    }else if(2300 <= passed_ms && passed_ms < 3500){
      led_base_pattern_on(&COLOR_RED);
    }else if(3500 <= passed_ms && passed_ms < 4600){
      led_base_pattern_dim(&COLOR_RED, now_ms, 1000, 20);
    }else if(4600 <= passed_ms && passed_ms < 6500){
      led_base_pattern_dim(&COLOR_RED, now_ms, 1800, 10);
    }else if(6500 <= passed_ms && passed_ms < 7000){
      led_base_pattern_dim(&COLOR_RED, now_ms, 400, 10);
    }else if(7000 <= passed_ms && passed_ms < 7800){
      led_base_pattern_dim(&COLOR_RED, now_ms, 700, 10);
    }else if(7800 <= passed_ms && passed_ms < 8500){
      led_base_pattern_blink(&COLOR_RED, now_ms, 50);
    }else if(8500 <= passed_ms && passed_ms < 9000){
      led_base_pattern_on(&COLOR_RED);
    }else if(9000 <= passed_ms && passed_ms < 10000){
      led_base_pattern_dim(&COLOR_RED, now_ms, 1000, 20);
    }else if(10000 <= passed_ms){
      led_base_pattern_off();
    }
    break;
  case RIDER_ID_W:
    if(passed_ms < 400){
      led_base_pattern_fill_circle_2(&COLOR_PURPLE, &COLOR_GREEN, now_ms, 400);
    }else if(400 <= passed_ms && passed_ms < 1200){
      led_base_pattern_on_2(&COLOR_PURPLE, &COLOR_GREEN);
    }else if(1200 <= passed_ms && passed_ms < 2200){
      led_base_pattern_blink_slowly_2(&COLOR_BLACK, &COLOR_GREEN, now_ms, 200, 10);
    }else if(2200 <= passed_ms && passed_ms < 3200){
      led_base_pattern_blink_slowly_2(&COLOR_PURPLE, &COLOR_BLACK, now_ms, 201, 10);
    }else if(3200 <= passed_ms && passed_ms < 3400){
      led_base_pattern_off();
    }else if(3400 <= passed_ms && passed_ms < 5200){
      led_base_pattern_blink_slowly_2(&COLOR_BLACK, &COLOR_GREEN, now_ms, 200, 10);
    }else if(5200 <= passed_ms && passed_ms < 7200){
      led_base_pattern_blink_slowly_2(&COLOR_PURPLE, &COLOR_BLACK, now_ms, 201, 10);
    }else if(7200 <= passed_ms && passed_ms < 8200){
      led_base_pattern_inc_2(&COLOR_PURPLE, &COLOR_GREEN, now_ms, 1000, 20);
    }else if(8200 <= passed_ms && passed_ms < 10200){
      led_base_pattern_on_2(&COLOR_PURPLE, &COLOR_GREEN);
    }else if(10200 <= passed_ms && passed_ms < 12200){
      led_base_pattern_dim_2(&COLOR_PURPLE, &COLOR_GREEN, now_ms, 2000, 20);
    }else if(12200 <= passed_ms){
      led_base_pattern_off();
    }
    break;
  case RIDER_ID_1:
    if(passed_ms < 3000){
      led_base_pattern_tri_circle(&COLOR_RED, &COLOR_BLUE, now_ms, 10, 2);
    }else if(3000 <= passed_ms && passed_ms < 6000){
      led_base_pattern_tri_circle(&COLOR_RED, &COLOR_BLUE, now_ms, 10, 4);
    }else if(6000 <= passed_ms && passed_ms < 8500){
      led_base_pattern_tri_circle(&COLOR_RED, &COLOR_BLUE, now_ms, 10, 6);
    }else if(8500 <= passed_ms){
      led_base_pattern_off();
    }
    break;
  default:
    ;
  }
}

void control_led(unsigned long now_ms){
  if(prev_state != state){
    led_pattern_start_point_ms = now_ms;
    led_index = 0;
    inc_dim_start_point_ms= 0;
    is_circling = false;
    is_inc = true;
    is_blink_on = false;
    led_base_pattern_off();
  }

  unsigned long passed_ms = now_ms - led_pattern_start_point_ms;

  switch(state){
  case STATE_INIT:
    led_pattern_init(now_ms);
    break;
  case STATE_READING:
    led_pattern_reading(passed_ms, now_ms);
    break;
  case STATE_READY:
    led_pattern_ready(passed_ms, now_ms);
    break;
  case STATE_CHANGE_A:
  case STATE_CHANGE_B:
    led_pattern_change(passed_ms, now_ms);
    break;
  default:
    ;
  }

  pixels.show();
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void setup(void){
  //M5.begin();

  Serial.begin(115200);
  pinMode(PIN_LED, OUTPUT);
  pinMode(PIN_SW, INPUT_PULLUP);

  Wire.begin();
  mfrc522.PCD_Init();

  // フルカラーLED初期化
  pixels.begin();
  pixels.clear();
  led_base_pattern_off();
  pixels.show();

  //---------- ディスプレイ ----------
  if(!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
    Serial.println(F("SSD1306 allocation failed"));
    for(;;); // Don't proceed, loop forever
  }

  display.setTextSize(2);
  display.setTextColor(SSD1306_WHITE); // Draw white text
  display.cp437(true); // Use full 256 char 'Code Page 437' font
  clear_display();

  //---------- MP3プレイヤー ----------
  ss_mp3_player.begin(9600);

  //if(!mp3_player.begin(hs_mp3_player)) {
  if(!mp3_player.begin(ss_mp3_player)) {
    Serial.println(F("Unable to begin music_player:"));
    Serial.println(F("1.Please recheck the connection!"));
    Serial.println(F("2.Please insert the SD card!"));
    while(true);
  }
  Serial.println(F("mp3_player online."));
  mp3_player.volume(SOUND_VOLUME_DEFAULT);

  play_sound(SOUND_FOLDER_COMMON, SOUND_TRACK_ON);
}

void loop(){
  unsigned long now_ms = millis();

  if(mfrc522.PICC_IsNewCardPresent() && mfrc522.PICC_ReadCardSerial()){
    // NFCタグがあり、かつ、そのUIDが読めた
    if(state == STATE_INIT){
       // ベルトが置かれていない状態からベルトが置かれた
      for(uint8_t i=0; i<mfrc522.uid.size; i++) {  // Output the stored UID data.
        Serial.print(mfrc522.uid.uidByte[i] < 0x10 ? " 0" : " ");
        Serial.print(mfrc522.uid.uidByte[i], HEX);
      }
      Serial.println("");

      for(uint8_t i=0; i<num_riders; i++){
        if (memcmp(rider_belts[i].tag_id, mfrc522.uid.uidByte, mfrc522.uid.size) == 0){
          Serial.println(rider_belts[i].belt_name_1);
          rider_id = rider_belts[i].rider_id;
          belt_name_1 = rider_belts[i].belt_name_1;
          belt_name_2 = rider_belts[i].belt_name_2;
          break;
        }
      }

      state = STATE_READING;
      reading_start_point_ms = now_ms;
    }

    release_counter = 0;

  }else{
    // NFCタグを読めなかった
    if(state != STATE_INIT){
      release_counter++;
      if(release_counter >= RELEASE_COUNT){
        // NFCタグが近接してる状態だとFindとLostを繰り返すので、
        // Lostが所定回数続けばベルトが置かれている状態からベルトが外されたと見なす
        state = STATE_INIT;
      }
    }
  }

  // -------- ボタン処理 --------
  sw = digitalRead(PIN_SW);

  if(prev_sw == OFF && sw == ON){
    if(state == STATE_READY || state == STATE_CHANGE_B){
      state = STATE_CHANGE_A;
    }else if(state == STATE_CHANGE_A){
      state = STATE_CHANGE_B;
    }
  }

  // -------- 時間経過処理 --------
  if(now_ms - reading_start_point_ms >= READING_TIME_MS){
    if(state == STATE_READING){
      state = STATE_READY;
    }
  } 

  // -------- 音声処理 --------
  control_sound();

  // -------- ディスプレイ処理 --------
  control_display(now_ms);

  // ---------- 発光処理 ----------
  control_led(now_ms);

  prev_sw    = sw;
  prev_state = state;

  delay(10);
}