逢魔降臨暦ワンダーライド電子ブックをつくる

今回は自分にしては珍しく、一度作成したもののリファイン版になります。ということで、前作『逢魔降臨暦ワンダーライドブック』のVer.2.0、『逢魔降臨暦ワンダーライド電子ブック』のご紹介です。

製作経緯

前作『逢魔降臨暦ワンダーライドブック』ですが、平成ライダーごとに固有のリード必殺技やプッシュ必殺技が発動したり、また火炎剣烈火のエンブレムが連動して発光したりとかなりのボリュームの作品だったのですが、残念ながら世間の評価的にはイマイチな結果(←YouTubeの再生回数が少ない)となりました。要因は色々あるかと思いますが、自分としても確かに課題がいくつか残る作品ではありました。不満点とは、具体的には以下になります。

  1. 表紙が開くギミックが繊細過ぎてとても遊びづらい
  2. 部品の詰め込みがかなり無茶していて今にも壊れそう
  3. アギト〜キバはストーリーページに何もギミックがない
  4. 抜刀してもライダーの絵が出てこない
  5. 特殊聖剣(土豪剣激土、風双剣翠風、音銃剣錫音、光剛剣最光)に対応していない

特に大きいのが1番で、とにかく遊びづらい。マグネットでロックする仕組みにしたせいで、磁力の調整がとにかく大変でした。そんなわけで、これら課題を解消するためにVer.2.0を作ることにしました…と言っても良いのですが、実際には『元々考えていた構想を諦めた結果、上記のような課題が顕在化してしまった』という方が正しいかもしれません。

では『元々考えていた構想』とは何だったのかというと、それは『電子ペーパーを使った、電子書籍としてワンダーライドブックを作る』というものです。実際、制作を開始した時点では、電子ペーパーデバイスを使うための試行錯誤をしていました。

こんな感じで(※2020年11月2日撮影)。

ただ、このときのデバイスは(自分の使い方が悪かったのかもしれませんが)画像のに更新に数十秒かかってしまい、残念ながら実用に耐え得るものではありませんでした。仕方がないので、前作では『普通のディスプレイを使って、本のページにライダーズクレストが浮かび上がるような演出にする』という方式で作り始めたのですが、その後にタイミングが良いのか悪いのか、M5Stackから電子ペーパーデバイスである『M5Stack CoreInk』と『M5Paper』が発売されました。作品自体はもう作り始めてしまっていたので、入手だけしておいてしばらく放置していたのですが、作品作りがひと段落して『M5Paper』を試しに触ってみると、これがとても良い。白黒16階調で表示できるし、画像の更新に1秒もかからない。これなら自分の当初の構想を実現できそう、ということで、前作のリベンジも兼ねて、改めて『逢魔降臨暦ワンダーライドブック』を作り直してみることにしました。

 

特徴

コンセプトは前作『逢魔降臨暦ワンダーライドブック』を踏襲しているので大筋は変わりませんが、色々変わっているところがあります。まず見た目。

0

M5Paperをそのまま丸ごと突っ込んでいるので、非常に大きいです。ジャオウドラゴン、ドラゴニックナイト、キングライオン大戦記、エックスソードマンらに連なる『大型本』扱いですが、

高さはジャオウドラゴン以上です。

厚みはだいぶマシかな。本当はもう少し薄くしたかったのですが、

ソードライバーの抜刀時に前に迫り出してくるパーツとの干渉の問題で、泣く泣く分厚くしました。これがなければ後7mmは薄くできたのに。

 

そして中身ですが、まず各平成ライダーのストーリーページに対応しています。

 

前作では「ライダーズクレストの発光+音声読み上げ(DX版のワンダーライドブックが存在するもののみ)」という形での表現だったのですが、今回はもう少し本っぽく、「ライダーズクレスト+古の文字でのストーリー表記+音声読み上げ(DX版のワンダーライドブックが存在するもののみ)」という形での表現になりました。おかげで、DX版のワンダーライドブックが存在しないライダーも、相変わらず音声読み上げはないものの、ギミックとしてより充実したものになったと思います。

DX版が存在しないライダー達もストーリー自体は設定されていて、こちらのページで確認できます。上図のように、古の文字に翻訳したものを表示するようにしています。

 

なお、今回の素体となっているM5Paperにはデフォルトで上図の多機能ボタン(上・押し込み・下)が搭載されているため、

 

前作では一方向にページを「送る」(=平成ライダーを順に切り替える)しかできなかったのが、「戻る」の操作もできるようになりました。おかげで電子書籍感を更に増すことができました。ワンダーライドブックはただでさえ「これは本と言っていいのか?」と言いたくなるような存在なので、少しでも「本」感(?)を出せるに越したことはないのです。

 

続いて、抜刀時の動作です。前作だと通常の変身音が鳴った後に「レジェンド共通変身音」+「ライダー名」+「各ライダーの変身音」が鳴る、ということで、音声面では(長いですが)豪華な演出となっていたのですが、本体としてはストーリーページと同じページが開いてライダーズクレストが発光しているだけでした。それが、

 

こんな感じで、各ライダーの肖像が表示されるようになりました。これが前作からの一番大きな変化点かなと思います。M5Paper本体を丸ごと使ったおかげで、かなり迫力があります。M5Paperの制約でグレースケール16階調での表示になっているのですが、その制約が却って、本に印刷された油絵のような印象を与えてくれている(?)気がします。元絵として、バンダイナムコエンターテインメントから発売されたゲーム『仮面ライダー クライマックスファイターズ』及び『仮面ライダー クライマックススクランブル ジオウ』のキャラクタービジュアルを使用させて頂いたのですが、これが本当にカッコ良いです。カッコ良すぎて、抜刀時にしか見れないのはもったいないと思いましたので、通常時にサイドの多機能ボタンを長押しすることでも、裏技的に肖像を表示できるようにしました。

 

電子ペーパーなので、この状態で電源を切れば、ライダーの肖像が表示されたまま電源がOFFになります。そのまま飾ればポートレートのようです。なお、これらの画像は個人が私的に楽しむ範囲で使用させて頂いているものの、バンダイナムコ様に怒られてしまったら動画や記事内の写真は削除しようと思いますので、予めご了承ください。

 

ちなみに、ソードライバーにセットしてライダーの肖像が表示された状態でブックの横のボタンを押し込むと、各ライダーの固有の戦闘BGMが再生されます。

 

前作の「リード必殺技待機時に、各ライダーの必殺技BGMを鳴らす」という機能が個人的には気に入っていたのですが、同時に「待機時にしかBGMが聞けない」というところは不満点でもあって、それを解消した形になります。これで、よりレジェンドの力を宿した感じ(?)でなりきり遊びを楽しむことができます。

なお、前作にあったギミックである「リード必殺技待機時に、各ライダーの必殺技BGMを鳴らす」と「火炎剣烈火のエンブレムにライダーズクレストが出現する」は、今作でも健在です。

 

また、闇暗剣月闇で使用すると、音声がアナザーライダー仕様になるのは前作と同じですが、今回は更にライダーのビジュアルもアナザーライダーになります。

 

この機能のために改めてアナザーライダーの画像をじっくり眺めることになったのですが、基本的に全員怖過ぎですね。特にジオウとダブル。当時観ていたチビッコたちは大丈夫だったんでしょうか。

 

さらに、前作では本が開いた状態でプッシュすることで、各ライダーの固有技(ゴウラム召喚やクロックアップなど)を発動できるようにしていたのですが、この機能はオミットして、代わりにブックの画面をタッチすることで、各ライダーの使用した『剣』を召喚する機能を入れることにしました。

 

元々のレジェンドライダーのワンダーライドブックでも変身ページの右側は各ライダーの使用した剣のビジュアルになっていたので、元々の玩具の仕様に沿った形での機能修正になります。前作のような音声しか使えない状況では、この武器召喚をライダー別に表現することが難しかったため実現できませんでしたが、今回、剣のビジュアルを表示できるようになったことで、実現できるようになりました。

剣の名前の下にある説明文章は、実際にレジェンドライダーのワンダーライドブックの中に記載されている古の文字の文章をそのまま転記したものになっています。

なお、各ライダーの「剣」のビジュアルは、Webの『仮面ライダー図鑑』に掲載されているものを使用させて頂いています。そのため、まだ図鑑に掲載されていないライダーの「剣」については、表示されません。公開され次第、順次追加して私的に楽しむ予定です。

 

最後の特徴として、今回晴れて特殊聖剣(土豪剣激土、風双剣翠風、音銃剣錫音、光剛剣最光)に正式対応しました。

わざわざ「正式」対応としているのは、実は前作でもプログラム上は対応していたのですが、実際動かしてみると中々うまく動作してくれない、ということで、非公式機能扱いしていたためです。今回、筐体が大きくなったことでスペースに余裕ができたため、スイッチを一つ追加することで、ブックをソードライバー/カリバードライバー/特殊聖剣に装填したときに、どれに装填されたかをブック自身が正確に自動判断できるようになりました(※ただし「光剛剣最光」を除く)。これにより、音声の再生タイミングを聖剣ごとに最適化することができるようなりました。このあたりのことは、後の「ハード解説/ソフト解説」のところでもう少し詳しく説明します。

ちなみに、「他のライダーは何となくわかるけど、最光はなぜビルド?」と思われた方もいるかもしれません。桐生戦兎の口癖にかけた単なる駄洒落、みたいなものです。

 

ハードウェア解説

作品作りとしての今回の1番のポイントは、3Dモデリング&プリントの本格導入です。3Dプリンタを活用した作品は過去にもあるにはあったのですが、比較的シンプル&コンパクトなものに留まっていました。今回、今後の作品作りの幅を広げる意味もあって、GW休暇をフルに使って3Dモデリングを勉強して、やや複雑な造形にチャレンジしてみることにしました。

モデリングに使用したソフトは定番のFusion 360で、学習に使用した参考書は以下になります。

一冊目の前半、Chapter 1〜5を一通り手順通りにこなして、その後一応二冊目も通したぐらいで、あとは習うより慣れろで実際にモデリングを始めてみました。

 

今回作成した部品は以下の4つです。

前面カバーパーツ。

背面カバーパーツ。

特殊聖剣認識用スイッチ。

電源OFF用スイッチ。

 

通常のワンダーライドブックの背面、実際にソードライバーに装填する部分のパーツについては、ちゃんと作ろうとするとモールドの作り込みが大変なので、ここについては既存の玩具の部品をそのまま流用することにしました。

成形色が黒いものが良かったので、DX版のオーズのワンダーライドブックを使用しています。

ちなみに上記写真の部品の他、内部のスイッチ系のパーツも一部流用しています。

 

そして、実際に3Dプリントしたものが以下になります。

上図は3Dプリントしたものに、サーフェイサーをかける → 400番の紙やすりをかける → もう一回サーフェイサーをかける → スプレーで塗装する、までを行った状態です。

3Dプリントの積層痕まで消したいなら表面処理をもっと丁寧にやる必要がありますが、上記の処理ぐらいでも触った感じは全然変わるので、自分としてはこれぐらいでOKです。

ちなみに3Dプリンタには、これまで使用していた自作のBS01に代わって、FLASHFORGE製のAdventurer3 Liteを使用しています。本業での昇進祝いに自分で自分にプレゼントしました。

 

エントリーモデルですが、このレベルのホビー用途であれば必要十分な性能です。先代のBS01は7年程前の3Dプリンタだったので、それに比べれば遥かに使いやすくて感動しました。なお、造形可能サイズは150x150x150mmでそれほど大きくはないので、今回はギリギリのサイズでした。使用したフィラメントはAdventurer 3 LiteにセットでついてきたMODELA:PLA Standardです。

 

これで外見は出来上がったので、あとは中に部品を詰め込んでいきます。まずは何はともあれ、M5Paperです。

比較的高額ではあるのですが、電子ペーパーの開発機として非常に優秀です。大容量(1150mAh)のバッテリーを内蔵してくれているのも素晴らしい。おかげでとても遊びやすいし、何より動画撮影が捗ります(←いつも小容量のバッテリーを無理やり詰め込んでいるので、こまめに充電しないといけない)。

続いて、いつものMP3プレイヤー、DFPlayerです。

マイクロSDカードも忘れずに。私は以下のものを使用しました。

スピーカーはDX付属のものを使用しても良いのですが、私はいつも以下のものに交換しています。

それから、M5PaperのIOポートはGROVEシステム規格なので、専用のケーブルがある方が配線の加工がラクになると思います。

あとは、「抜刀(本開き)検出スイッチ」、「リード検出/カリバードライバー認識用スイッチ」、「特殊聖剣認識用スイッチ」を実装するためのディテクタスイッチです。

必要な部品は大体こんな感じです。これらを以下のように配線します。

まずはM5Paper側のコネクタ部分から、Groveケーブルを加工して必要な線を伸ばします。

これを前面カバーパーツにはめ込みます。ピッタリ。トライ&エラーの4回目のプリントですけども。

続いて背面側です。

まずはDX玩具に入っているスイッチ類のパーツやバネを先に組み込みます。併せて、ワンダーライドブックを特殊聖剣にセットしたときにロックがかかる部分に、特殊聖剣認識用にプリントしたスイッチをセットしています。

そして、配線しておいた部品を、

組み込みます。

自分で設計しているので、ディテクタスイッチが必要なところにピッタリ収まるようになっています。

あとは、背面パーツを裏返して電源OFF用にプリントしたパーツをはめ込んだ後、先程のM5Paperをはめ込んだケーブルと接続して、前面と背面を合わせてネジ留め。

さらに、DX玩具の背面パーツで蓋をして、

完成です。いつもはDX玩具の筐体に部品を埋め込むところで四苦八苦するのですが、今回はややこしいところ(ディスプレイや電源)の配線が既にM5Paper本体に組み込まれているのと、筐体を自分で設計したのとで、比較的すんなり組み上げることができました。

 

ソフトウェア解説

ソースコードの全文はいつもどおりこの記事の最後に掲載しますので、ここでは全体設計と、ちょっとしたポイントについてのみご説明します。

まず、全体設計=状態遷移図は以下のようになります。

基本の考え方は前作を踏襲していますが、今回M5Paperを採用したことで、電子ペーパーによる描画処理の他、多機能ボタンを使えるようになったことでちょっと遷移の仕方が変わっています。あと、前作のコードから大きく改善されたのは、聖剣のモード判定処理です。前作では特殊聖剣を認識させるための処理が「何秒以内に本の開閉動作が完了する」みたいな確実性の低いものになってしまい、結果、実際に動かしてみると全然特殊聖剣を認識しない、ということになってしまいました。

今回は特殊聖剣認識用のスイッチを追加したおかげで、通常のワンダーライドブックでいうところのライダーページが開く操作が発生した瞬間に「どのスイッチが押されているか」の組み合わせを見るというシンプルな形で、ソードライバー/カリバードライバー/特殊聖剣を自動認識できるようになりました。

ただ一点、これだとカリバードライバーと光剛剣最光(サイコウドライバー)の区別ができません。バックルの形状がどちらも同じであり、カリバードライバーの動作優先のため、何もしなければ光剛剣最光でこのブックを使うと『ジャアクリード アナザーライダー』となってしまいます。

ということで、ちょっとズルいですが、光剛剣最光で使用するときのみ、ワンダーライドブックに専用の補助パーツを取り付けます。こちらも3Dプリンタで作成しました。これによって、サイコウドライバーにセットしていても特殊聖剣にセットしたと認識されるため、セットしたときに流れる音声が通常の特殊聖剣と同一になります。

あと書いておくことがあるとすれば、今回初めて扱った電子ペーパーの描画処理です。表示を更新した際、前回表示していた内容の残像?みたいなものが次の表示内容に残ってしまう、ということが起こりましたので、あんまり短時間に連続して書き換えるのは良くないかなと思うものの、表示内容を更新する際には、一度画面全体を白く塗りつぶして(fillCanvas(0)して)から、新しい画像を表示するようにしています。

 

まとめ

以上、『逢魔降臨暦ワンダーライド電子ブック』のご紹介でした。『ワンダーライドブック』は「これは本なのか?」と言いたくなるような存在ではあるのですが、今回作ってみて、『電子書籍』としてなら(個人的には)納得できる気がしました。

前作の『逢魔降臨暦ワンダーライドブック』が、おそらく自他共に不満が残る結果になったかなと思うのですが、個人的には今回の『逢魔降臨暦ワンダーライド電子ブック』で前作でやれなかったことを全てやり切ることができたので、どういう評価になろうと個人的には満足です。それより、今後の作品作りに向けて3Dモデリングの基礎を習得できたのが非常に大きいと思っています。

『セイバー』の玩具改造はまだ2作目ではあるのですが、やりたいことは大体やれたので、おそらくこれが最後になると思います。番組ももう終盤ですし。

さて、次は何を作りましょう。今のところはノーアイデアです。

 

ソースコード

////////// 基本定義 ////////////////////////////////////////////////////////////
#include <M5EPD.h>
// Macでコンパイルできなかったが、以下で解決
// sudo pip3 install --target /Library/Python/2.7/site-packages pyserial
// 参考:https://qiita.com/Nabeshin/items/b429675faa918f12c415

#include "BLEDevice.h"
#include "BLEServer.h"
#include "BLEUtils.h"

#define LOOP_DELAY_MS 5

#define PIN_MP3_RX       18
#define PIN_MP3_TX       19
#define PIN_BOTTOM_SW    25
#define PIN_DRIVER_SW    32
#define PIN_BACK_SW      26
#define PIN_TOP_SW       33
#define PIN_SIDE_SW_UP   37
#define PIN_SIDE_SW_PUSH 38
#define PIN_SIDE_SW_DOWN 39 

#define ON  LOW
#define OFF HIGH

uint8_t bottom_sw    = OFF;
uint8_t driver_sw    = OFF;
uint8_t back_sw      = OFF;
uint8_t top_sw       = OFF;
uint8_t side_sw_up   = OFF;
uint8_t side_sw_push = OFF;
uint8_t side_sw_down = OFF;
uint8_t prev_bottom_sw    = OFF;
uint8_t prev_driver_sw    = OFF;
uint8_t prev_back_sw      = OFF;
uint8_t prev_top_sw       = OFF;
uint8_t prev_side_sw_up   = OFF;
uint8_t prev_side_sw_push = OFF;
uint8_t prev_side_sw_down = OFF;

#define SIDE_SW_LONG_PRESS_MS 1000
unsigned long side_sw_long_press_start_time = 0;
boolean is_side_sw_pressing = false;

#define BACK_SW_LONG_PRESS_MS 1000
unsigned long back_sw_long_press_start_time = 0;
boolean is_back_sw_pressing = false;

#define SUMMON_WEAPON_MS 5000
unsigned long summon_weapon_start_time = 0;
boolean is_summoning_weapon = false;

boolean is_touching= false;
boolean prev_is_touching= false;

#define STATE_INIT          0
#define STATE_MOVE          1
#define STATE_STORY         2
#define STATE_PORTRAIT      3
#define STATE_DRIVER_IN     4
#define STATE_DRIVER_OPEN   5
#define STATE_DRIVER_MOVE   6
#define STATE_BGM           7
#define STATE_WEAPON        8
#define STATE_READING       9
#define STATE_READING_MOVE 10

uint8_t state      = STATE_INIT;
uint8_t prev_state = STATE_INIT;

void print_state(){
  if(prev_state != state){
    switch(state){
    case STATE_INIT:         Serial.println(F("STATE_INIT"));         break;
    case STATE_MOVE:         Serial.println(F("STATE_MOVE"));         break;
    case STATE_STORY:        Serial.println(F("STATE_STORY"));        break;
    case STATE_PORTRAIT:     Serial.println(F("STATE_PORTRAIT"));     break;
    case STATE_DRIVER_IN:    Serial.println(F("STATE_DRIVER_IN"));    break;
    case STATE_DRIVER_OPEN:  Serial.println(F("STATE_DRIVER_OPEN"));  break;
    case STATE_DRIVER_MOVE:  Serial.println(F("STATE_DRIVER_MOVE"));  break;
    case STATE_BGM:          Serial.println(F("STATE_BGM"));          break;
    case STATE_WEAPON:       Serial.println(F("STATE_WEAPON"));       break;
    case STATE_READING:      Serial.println(F("STATE_READING"));      break;
    case STATE_READING_MOVE: Serial.println(F("STATE_READING_MOVE")); break;
    default: ;
    }
  }
}

#define SWORD_MODE_HOLY    0 // 聖剣ソードライバー
#define SWORD_MODE_EVIL    1 // 邪剣カリバードライバー
#define SWORD_MODE_SPECIAL 2 // 特殊聖剣
uint8_t sword_mode = SWORD_MODE_HOLY;

#define NUM_HEISEI_RIDERS 20
uint8_t num_rider = 0;

boolean legend_read[NUM_HEISEI_RIDERS];

void reset_legend_read(){
  for(uint8_t i=0;i<NUM_HEISEI_RIDERS;i++){
    legend_read[i] = false;
  }
}

boolean check_read_all_legend(){
  boolean flag_read_all = true;
  for(uint8_t i=0;i<NUM_HEISEI_RIDERS;i++){
    if(legend_read[i] == false){
      flag_read_all = false;
      break;
    }
  }
  return flag_read_all;
}

////////// 音声処理 /////////////////////////////////////////////////////////////

#include <DFPlayerMini_Fast.h>

HardwareSerial hs_mp3_player(1);
DFPlayerMini_Fast mp3_player;

#define SOUND_VOLUME_DEFAULT 20 // 0〜30 20

#define SOUND_BUTTON                       1
#define SOUND_PUSH                         2
#define SOUND_INSERT                       3
#define SOUND_EJECT                        4
#define SOUND_PUSH_STORY_LEGEND            5
#define SOUND_ADD_LEGEND                   6
#define SOUND_WEAPON                       7
#define SOUND_READ_HOLY                    8
#define SOUND_LEARNED_HOLY                 9
#define SOUND_OHMA_ZIO_UNDERSTANDING_HOLY 10
#define SOUND_LEARNED_OHMA_ZIO_HOLY       11
#define SOUND_EVIL_READ_ANOTHER_RIDER     12
#define SOUND_READ_EVIL                   13
#define SOUND_LEARNED_EVIL                14
#define SOUND_OHMA_ZIO_UNDERSTANDING_EVIL 15
#define SOUND_LEARNED_OHMA_ZIO_EVIL       16

#define SOUND_OFFSET_RIDER_TIME_CHANGE                 20
#define SOUND_OFFSET_PUSH_STORY                        40
#define SOUND_OFFSET_CHANGE_WAITING                    60
#define SOUND_OFFSET_RIDER_NAME_CHANGE                 80
#define SOUND_OFFSET_RIDER_TIME_UNDERSTANDING         100
#define SOUND_OFFSET_RIDER_NAME_CRITICAL              120
#define SOUND_OFFSET_ANOTHER_RIDER_NAME_CHANGE        140
#define SOUND_OFFSET_ANOTHER_RIDER_TIME_UNDERSTANDING 160
#define SOUND_OFFSET_ANOTHER_RIDER_NAME_CRITICAL      180
#define SOUND_OFFSET_BGM_HOLY                         200
#define SOUND_OFFSET_BGM_EVIL                         220

#define WAIT_SOUND_STATE_INIT                              0
#define WAIT_SOUND_STATE_RIDER_TIME_CHANGE                 1
#define WAIT_SOUND_STATE_RIDER_NAME_CRITICAL               2
#define WAIT_SOUND_STATE_ANOTHER_RIDER_NAME_CRITICAL       3
#define WAIT_SOUND_STATE_CHANGE_WAITING                    4
#define WAIT_SOUND_STATE_ADD_LEGEND                        5
#define WAIT_SOUND_STATE_RIDER_NAME_CHANGE_LONG            6
#define WAIT_SOUND_STATE_RIDER_NAME_CHANGE_SHORT           7
#define WAIT_SOUND_STATE_EVIL_READ                         8
#define WAIT_SOUND_STATE_ANOTHER_RIDER_NAME_CHANGE         9
#define WAIT_SOUND_STATE_RIDER_TIME_UNDERSTANDING         10
#define WAIT_SOUND_STATE_ANOTHER_RIDER_TIME_UNDERSTANDING 11

#define WAIT_SOUND_RIDER_TIME_CHANGE_MS                1800
#define WAIT_SOUND_RIDER_NAME_CRITICAL_MS              2000
#define WAIT_SOUND_ANOTHER_RIDER_NAME_CRITICAL_MS      2000
#define WAIT_SOUND_CHANGE_WAITING_MS                   1200
#define WAIT_SOUND_ADD_LEGENGD_MS                     19000
#define WAIT_SOUND_RIDER_NAME_CHANGE_LONG_MS          15500
#define WAIT_SOUND_RIDER_NAME_CHANGE_SHORT_MS          1800
#define WAIT_SOUND_EVIL_READ_MS                        1200
#define WAIT_SOUND_ANOTHER_RIDER_NAME_CHANGE_MS        4500
#define WAIT_SOUND_RIDER_TIME_UNDERSTANDING_MS         1500
#define WAIT_SOUND_ANOTHER_RIDER_TIME_UNDERSTANDING_MS 4300

uint8_t wait_sound_state = WAIT_SOUND_STATE_INIT;
unsigned long sound_wait_start_time = 0;
boolean is_playing_bgm = false;

void play_sound(uint8_t sound_num){
  mp3_player.playFromMP3Folder(sound_num);
  Serial.print(F("Play sound: "));
  Serial.println(sound_num);
}

void advertise_sound(uint8_t sound_num){
  mp3_player.playAdvertisement(sound_num);
  Serial.print(F("Advertise sound: "));
  Serial.println(sound_num);
}

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

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

  if(prev_state != state){ // 状態遷移直後の処理

    // 状態変化が起きれば、待ちのステータスはリセットするのが基本。一部の状態変化ではリセットさせない
    if(prev_state == STATE_MOVE && state == STATE_INIT){
      ;
    }else if(prev_state == STATE_DRIVER_MOVE && state == STATE_DRIVER_OPEN){
      ;
    }else if(prev_state == STATE_READING_MOVE && state == STATE_READING){
      ;
    }else{
      wait_sound_state = WAIT_SOUND_STATE_INIT;
    }

    switch(state){
    case STATE_INIT:
      switch(prev_state){
      case STATE_STORY:
        if(num_rider == 0){
          play_sound(SOUND_PUSH_STORY_LEGEND);
        }else{
          play_sound(SOUND_OFFSET_PUSH_STORY + num_rider);
        }
        break;
      case STATE_DRIVER_IN:
      case STATE_DRIVER_OPEN:
      case STATE_DRIVER_MOVE:
      case STATE_BGM:
      case STATE_WEAPON:
        play_sound(SOUND_EJECT);
        is_playing_bgm = false;
        break;
      case STATE_READING:
      case STATE_READING_MOVE:
        if(sword_mode == SWORD_MODE_HOLY || sword_mode == SWORD_MODE_SPECIAL){
          if(check_read_all_legend()){
            play_sound(SOUND_LEARNED_OHMA_ZIO_HOLY);
          }else{
            if(num_rider != 0){
              play_sound(SOUND_LEARNED_HOLY);
            }
            wait_sound_state = WAIT_SOUND_STATE_RIDER_NAME_CRITICAL;
            sound_wait_start_time = now_ms;
          }
        }else{
          if(check_read_all_legend()){
            play_sound(SOUND_LEARNED_OHMA_ZIO_EVIL);
          }else{
            if(num_rider != 0){
              play_sound(SOUND_LEARNED_EVIL);
            }
            wait_sound_state = WAIT_SOUND_STATE_ANOTHER_RIDER_NAME_CRITICAL;
            sound_wait_start_time = now_ms;
          }
        }
        break;
      default:
        ;
      }
      break;
    case STATE_MOVE:
      switch(prev_state){
      case STATE_INIT:
        play_sound(SOUND_BUTTON);
        wait_sound_state = WAIT_SOUND_STATE_RIDER_TIME_CHANGE;
        sound_wait_start_time = now_ms;
        break;
      default:
        ;
      }
      break;
    case STATE_STORY:
      break;
    case STATE_PORTRAIT:
      break;
    case STATE_DRIVER_IN:
      switch(prev_state){
      case STATE_INIT:
        play_sound(SOUND_INSERT);
        wait_sound_state = WAIT_SOUND_STATE_CHANGE_WAITING;
        sound_wait_start_time = now_ms;
        break;
      default:
        ;
      }
      break;
    case STATE_DRIVER_OPEN:
      switch(prev_state){
      case STATE_DRIVER_IN:
        if(sword_mode == SWORD_MODE_HOLY){
          pause_sound();
          wait_sound_state = WAIT_SOUND_STATE_ADD_LEGEND;
        }else if(sword_mode == SWORD_MODE_EVIL){
          wait_sound_state = WAIT_SOUND_STATE_EVIL_READ;
          pause_sound();
        }else{ // 特殊聖剣
          play_sound(SOUND_BUTTON);
          wait_sound_state = WAIT_SOUND_STATE_RIDER_NAME_CHANGE_SHORT;
        }
        sound_wait_start_time = now_ms;
        break;
      default:
        ;
      }
      break;
    case STATE_DRIVER_MOVE:
      switch(prev_state){
      case STATE_DRIVER_OPEN:
        play_sound(SOUND_BUTTON);
        wait_sound_state = WAIT_SOUND_STATE_RIDER_NAME_CHANGE_SHORT;
        sound_wait_start_time = now_ms;
        break;
      default:
        ;
      }
      break;
    case STATE_BGM:
      switch(prev_state){
      case STATE_DRIVER_OPEN:
        if(!is_playing_bgm){
          if(sword_mode == SWORD_MODE_HOLY || sword_mode == SWORD_MODE_SPECIAL){
            play_sound(SOUND_OFFSET_BGM_HOLY + num_rider);
          }else{
            play_sound(SOUND_OFFSET_BGM_EVIL + num_rider);
          }
        }else{
          pause_sound();
        }
        is_playing_bgm = !is_playing_bgm;
        break;
      default:
        ;
      }
      break;
    case STATE_WEAPON:
      switch(prev_state){
      case STATE_DRIVER_OPEN:
        if(is_playing_bgm){
          advertise_sound(SOUND_WEAPON);
        }else{
          play_sound(SOUND_WEAPON);
        }
        break;
      default:
        ;
      }
      break;
    case STATE_READING:
      switch(prev_state){
      case STATE_INIT:
        if(sword_mode == SWORD_MODE_HOLY || sword_mode == SWORD_MODE_SPECIAL){
          play_sound(SOUND_READ_HOLY);
          wait_sound_state = WAIT_SOUND_STATE_RIDER_TIME_UNDERSTANDING;
        }else{
          play_sound(SOUND_READ_EVIL);
          wait_sound_state = WAIT_SOUND_STATE_ANOTHER_RIDER_TIME_UNDERSTANDING;
        }
        sound_wait_start_time = now_ms;
        break;
      default:
        ;
      }
      break;
    case STATE_READING_MOVE:
      switch(prev_state){
      case STATE_READING:
        if(sword_mode == SWORD_MODE_HOLY || sword_mode == SWORD_MODE_SPECIAL){
          play_sound(SOUND_READ_HOLY);
          wait_sound_state = WAIT_SOUND_STATE_RIDER_TIME_UNDERSTANDING;
        }else{
          play_sound(SOUND_READ_EVIL);
          wait_sound_state = WAIT_SOUND_STATE_ANOTHER_RIDER_TIME_UNDERSTANDING;
        }
        sound_wait_start_time = now_ms;
        break;
      default:
        ;
      }
      break;
    default:
      ;
    }
  }else{ // 状態遷移後の時間経過処理
    switch(wait_sound_state){
    case WAIT_SOUND_STATE_INIT:
      break;
    case WAIT_SOUND_STATE_RIDER_TIME_CHANGE:
      if(now_ms - sound_wait_start_time >= WAIT_SOUND_RIDER_TIME_CHANGE_MS){
        if(num_rider != 0){
          play_sound(SOUND_OFFSET_RIDER_TIME_CHANGE + num_rider);
        }
        wait_sound_state = WAIT_SOUND_STATE_INIT;
      }
      break;
    case WAIT_SOUND_STATE_RIDER_NAME_CRITICAL:
      if(now_ms - sound_wait_start_time >= WAIT_SOUND_RIDER_NAME_CRITICAL_MS){
        if(num_rider != 0){
          play_sound(SOUND_OFFSET_RIDER_NAME_CRITICAL + num_rider);
        }
        wait_sound_state = WAIT_SOUND_STATE_INIT;
      }
      break;
    case WAIT_SOUND_STATE_ANOTHER_RIDER_NAME_CRITICAL:
      if(now_ms - sound_wait_start_time >= WAIT_SOUND_ANOTHER_RIDER_NAME_CRITICAL_MS){
        if(num_rider != 0){
          play_sound(SOUND_OFFSET_ANOTHER_RIDER_NAME_CRITICAL + num_rider);
        }
        wait_sound_state = WAIT_SOUND_STATE_INIT;
      }
      break;
    case WAIT_SOUND_STATE_CHANGE_WAITING:
      if(now_ms - sound_wait_start_time >= WAIT_SOUND_CHANGE_WAITING_MS){
        if(num_rider != 0){
          play_sound(SOUND_OFFSET_CHANGE_WAITING + num_rider);
        }
        wait_sound_state = WAIT_SOUND_STATE_INIT;
      }
      break;
    case WAIT_SOUND_STATE_ADD_LEGEND:
      if(now_ms - sound_wait_start_time >= WAIT_SOUND_ADD_LEGENGD_MS){
        play_sound(SOUND_ADD_LEGEND);
        wait_sound_state = WAIT_SOUND_STATE_RIDER_NAME_CHANGE_LONG;
        sound_wait_start_time = now_ms;
      }
      break;
    case WAIT_SOUND_STATE_RIDER_NAME_CHANGE_LONG:
      if(now_ms - sound_wait_start_time >= WAIT_SOUND_RIDER_NAME_CHANGE_LONG_MS){
        if(num_rider != 0){
          play_sound(SOUND_OFFSET_RIDER_NAME_CHANGE + num_rider);
        }
        wait_sound_state = WAIT_SOUND_STATE_INIT;
      }
      break;
    case WAIT_SOUND_STATE_RIDER_NAME_CHANGE_SHORT:
      if(now_ms - sound_wait_start_time >= WAIT_SOUND_RIDER_NAME_CHANGE_SHORT_MS){
        if(num_rider != 0){
          if(sword_mode == SWORD_MODE_HOLY || sword_mode == SWORD_MODE_SPECIAL){
            play_sound(SOUND_OFFSET_RIDER_NAME_CHANGE + num_rider);
          }else{
            play_sound(SOUND_OFFSET_ANOTHER_RIDER_NAME_CHANGE + num_rider);
          }
        }
        wait_sound_state = WAIT_SOUND_STATE_INIT;
      }
      break;
    case WAIT_SOUND_STATE_EVIL_READ:
      if(now_ms - sound_wait_start_time >= WAIT_SOUND_EVIL_READ_MS){
        play_sound(SOUND_EVIL_READ_ANOTHER_RIDER);
        wait_sound_state = WAIT_SOUND_STATE_ANOTHER_RIDER_NAME_CHANGE;
        sound_wait_start_time = now_ms;
      }
      break;
    case WAIT_SOUND_STATE_ANOTHER_RIDER_NAME_CHANGE:
      if(now_ms - sound_wait_start_time >= WAIT_SOUND_ANOTHER_RIDER_NAME_CHANGE_MS){
        if(num_rider != 0){
          play_sound(SOUND_OFFSET_ANOTHER_RIDER_NAME_CHANGE + num_rider);
        }
        wait_sound_state = WAIT_SOUND_STATE_INIT;
      }
      break;
    case WAIT_SOUND_STATE_RIDER_TIME_UNDERSTANDING:
      if(now_ms - sound_wait_start_time >= WAIT_SOUND_RIDER_TIME_UNDERSTANDING_MS){
        if(check_read_all_legend()){
          play_sound(SOUND_OHMA_ZIO_UNDERSTANDING_HOLY);
        }else{
          if(num_rider != 0){
            play_sound(SOUND_OFFSET_RIDER_TIME_UNDERSTANDING + num_rider);
          }
        }
        wait_sound_state = WAIT_SOUND_STATE_INIT;
      }
      break;
    case WAIT_SOUND_STATE_ANOTHER_RIDER_TIME_UNDERSTANDING:
      if(now_ms - sound_wait_start_time >= WAIT_SOUND_ANOTHER_RIDER_TIME_UNDERSTANDING_MS){
        if(check_read_all_legend()){
          if(num_rider != 0){
            play_sound(SOUND_OHMA_ZIO_UNDERSTANDING_EVIL);
          }
        }else{
          play_sound(SOUND_OFFSET_ANOTHER_RIDER_TIME_UNDERSTANDING + num_rider);
        }
        wait_sound_state = WAIT_SOUND_STATE_INIT;
      }
      break;
    default :
      ;
    }
  }
}

////////// 描画処理 //////////////////////////////////////////////////////

#define PIC_TITLE_OFFSET          0
#define PIC_RIDER_OFFSET         20
#define PIC_WEAPON_OFFSET        40
#define PIC_ANOTHER_RIDER_OFFSET 60

String PICTURE_DIR = "/picture/";

#define PIC_STATE_NORMAL              0
#define PIC_STATE_PORTRAIT            1
#define PIC_STATE_WAITING_RIDER_LONG  2
#define PIC_STATE_WAITING_RIDER_SHORT 3
#define PIC_STATE_WAITING_RIDER_EVIL  4

unsigned long const WAIT_PIC_RIDER_LONG_MS  = WAIT_SOUND_ADD_LEGENGD_MS + WAIT_SOUND_RIDER_NAME_CHANGE_LONG_MS;
unsigned long const WAIT_PIC_RIDER_SHORT_MS = WAIT_SOUND_RIDER_NAME_CHANGE_SHORT_MS;
unsigned long const WAIT_PIC_RIDER_EVIL_MS  = WAIT_SOUND_EVIL_READ_MS + WAIT_SOUND_ANOTHER_RIDER_NAME_CHANGE_MS;
uint8_t pic_state = PIC_STATE_NORMAL;
unsigned long pic_wait_start_time = 0;

M5EPD_Canvas canvas(&M5.EPD);

void draw_picture(uint8_t page){
  // 残像が残らないように、表示を一度リセットしてから描画する
  canvas.fillCanvas(0);
  canvas.pushCanvas(0,0,UPDATE_MODE_GC16);
  //delay(500);

  String file_path_str = PICTURE_DIR + String(page) + ".jpeg";
  uint8_t file_path_len = file_path_str.length()+1;
  char file_path_char[file_path_len];
  file_path_str.toCharArray(file_path_char, file_path_len);
  Serial.println(file_path_char);
  canvas.drawJpgFile(SD, file_path_char);
  canvas.pushCanvas(0,0,UPDATE_MODE_GC16);
  //delay(500);
}

void control_display(){

  unsigned long now_ms = millis();

  if(prev_state != state){ // 状態遷移直後の処理
    switch(state){
    case STATE_INIT:
      switch(prev_state){
      case STATE_DRIVER_OPEN:
      case STATE_DRIVER_MOVE:
      case STATE_BGM:
        draw_picture(PIC_TITLE_OFFSET + num_rider);
        pic_state = PIC_STATE_NORMAL;
        break;
      default:
        ;
      }
      break;
    case STATE_MOVE:
      switch(prev_state){
      case STATE_INIT:
        draw_picture(PIC_TITLE_OFFSET + num_rider);
        pic_state = PIC_STATE_NORMAL;
        break;
      default:
        ;
      }
      break;
    case STATE_STORY:
      break;
    case STATE_PORTRAIT:
      switch(prev_state){
      case STATE_STORY:
        if(num_rider != 0){
          draw_picture(PIC_RIDER_OFFSET + num_rider);
          pic_state = PIC_STATE_PORTRAIT;
        }
        break;
      default:
        ;
      }
      break;
    case STATE_DRIVER_IN:
      switch(prev_state){
      case STATE_INIT:
        if(pic_state != PIC_STATE_NORMAL){
          draw_picture(PIC_TITLE_OFFSET + num_rider);
          pic_state = PIC_STATE_NORMAL;
        }
        break;
      default:
        ;
      }
      break;
    case STATE_DRIVER_OPEN:
      switch(prev_state){
      case STATE_DRIVER_IN:
        if(sword_mode == SWORD_MODE_HOLY){
          pic_state = PIC_STATE_WAITING_RIDER_LONG;
        }else if(sword_mode == SWORD_MODE_EVIL){
          pic_state = PIC_STATE_WAITING_RIDER_EVIL;
        }else{ // 特殊聖剣
          pic_state = PIC_STATE_WAITING_RIDER_SHORT;
        }
        pic_wait_start_time = now_ms;
        break;
      case STATE_WEAPON:
        if(sword_mode == SWORD_MODE_HOLY || sword_mode == SWORD_MODE_SPECIAL){
          draw_picture(PIC_RIDER_OFFSET + num_rider);
        }else{
          draw_picture(PIC_ANOTHER_RIDER_OFFSET + num_rider);
        }
        break;
      default:
        ;
      }
      break;
    case STATE_DRIVER_MOVE:
      switch(prev_state){
      case STATE_DRIVER_OPEN:
        //if(num_rider != 0){
        //  draw_picture(PIC_TITLE_OFFSET + num_rider);
        //}
        pic_state = PIC_STATE_WAITING_RIDER_SHORT;
        pic_wait_start_time = now_ms;
        break;
      default:
        ;
      }
      break;
    case STATE_BGM:
      break;
    case STATE_WEAPON:
      switch(prev_state){
      case STATE_DRIVER_OPEN:
        if(num_rider != 0){
          draw_picture(PIC_WEAPON_OFFSET + num_rider);
        }
        break;
      default:
        ;
      }
      break;
    case STATE_READING:
      switch(prev_state){
      case STATE_INIT:
        if(pic_state != PIC_STATE_NORMAL){
          draw_picture(PIC_TITLE_OFFSET + num_rider);
          pic_state = PIC_STATE_NORMAL;
        }
        break;
      default:
        ;
      }
      break;
    case STATE_READING_MOVE:
      switch(prev_state){
      case STATE_READING:
        draw_picture(PIC_TITLE_OFFSET + num_rider);
        break;
      default:
        ;
      }
      break;
    default:
      ;
    }
  }else{ // 状態遷移後の時間経過処理
    switch(pic_state){
    case PIC_STATE_NORMAL:
      break;
    case PIC_STATE_PORTRAIT:
      break;
    case PIC_STATE_WAITING_RIDER_LONG:
      if(now_ms - pic_wait_start_time >= WAIT_PIC_RIDER_LONG_MS){
        if(num_rider != 0){
          draw_picture(PIC_RIDER_OFFSET + num_rider);
        }
        pic_state = PIC_STATE_NORMAL;
      }
      break;
    case PIC_STATE_WAITING_RIDER_SHORT:
      if(now_ms - pic_wait_start_time >= WAIT_PIC_RIDER_SHORT_MS){
        if(num_rider != 0){
          if(sword_mode == SWORD_MODE_HOLY || sword_mode == SWORD_MODE_SPECIAL){
            draw_picture(PIC_RIDER_OFFSET + num_rider);
          }else{
            draw_picture(PIC_ANOTHER_RIDER_OFFSET + num_rider);
          }
        }
        pic_state = PIC_STATE_NORMAL;
      }
      break;
    case PIC_STATE_WAITING_RIDER_EVIL:
      if(now_ms - pic_wait_start_time >= WAIT_PIC_RIDER_EVIL_MS){
        if(num_rider != 0){
          draw_picture(PIC_ANOTHER_RIDER_OFFSET + num_rider);
        }
        pic_state = PIC_STATE_NORMAL;
      }
      break;
    default:
      ;
    }
  }
}

////////// BLE処理 ////////////////////////////////////////////////////////////

// Service UUID
// Macのターミナルの"uuidgen"コマンドで生成
#define SERVICE_UUID "9D264561-039A-4EE9-B1B9-52116066B59C"

#define WAIT_BLE_ADVERTISE_STATE_NONE                    0
#define WAIT_BLE_ADVERTISE_STATE_READY_NAME_CHANGE_LONG  1
#define WAIT_BLE_ADVERTISE_STATE_READY_NAME_CHANGE_SHORT 2
#define WAIT_BLE_ADVERTISE_STATE_READY_READING           3
#define WAIT_BLE_ADVERTISE_STATE_ADVERTISING             4
uint8_t wait_ble_advertise_state = WAIT_BLE_ADVERTISE_STATE_NONE;

#define WAIT_BLE_ADVERTISE_FOR_END_MS 200
unsigned long const WAIT_BLE_ADVERTISE_FOR_NAME_CHANGE_LONG_MS  = WAIT_SOUND_ADD_LEGENGD_MS + WAIT_SOUND_RIDER_NAME_CHANGE_LONG_MS;
unsigned long const WAIT_BLE_ADVERTISE_FOR_NAME_CHANGE_SHORT_MS = WAIT_SOUND_RIDER_NAME_CHANGE_SHORT_MS;
unsigned long const WAIT_BLE_ADVERTISE_FOR_READING_MS = WAIT_SOUND_RIDER_TIME_UNDERSTANDING_MS;
unsigned long ble_advertise_wait_start_time = 0;

BLEServer *pServer;
BLEAdvertising *pAdvertising;

void setAdvData(BLEAdvertising *pAdvertising, uint8_t value){
  BLEAdvertisementData oAdvertisementData = BLEAdvertisementData();

  // アドバタイズはペイロードとして全体で31バイトまでデータを詰め込める。
  // 「長さ(1 byte)+AD Type(1 byte)+AD Data(X byte)」(=AD structure)を繰り返す形で格納する。
  // 「長さ」は、AD TypeとAD Dataを足した長さ。
  // 以下は1行で一つのAD structureを設定している

  // Flags ... AD Type:0x01 デバイスの発見や機能についての設定
  //  ESP_BLE_ADV_FLAG_LIMIT_DISC         ... ビット0 ... LE Limited Discoverable Mode(デバイスの発見に時間制限がある場合)
  //  ESP_BLE_ADV_FLAG_GEN_DISC           ... ビット1 ... LE General Discoverable Mode(通常はこのモードを使用)
  //  ESP_BLE_ADV_FLAG_BREDR_NOT_SPT      ... ビット2 ... BR/EDR Not Supported(BLEのみをサポートする場合)
  //  ESP_BLE_ADV_FLAG_DMT_CONTROLLER_SPT ... ビット3 ... Simultaneous LE and BR/EDR to Same Device Capable (Controller)
  //  ESP_BLE_ADV_FLAG_DMT_HOST_SPT       ... ビット4 ... Simultaneous LE and BR/EDR to Same Device Capble (Host)
  oAdvertisementData.setFlags(ESP_BLE_ADV_FLAG_BREDR_NOT_SPT || ESP_BLE_ADV_FLAG_GEN_DISC); // 1+1+1=3 byte

  // Complete local name ... AD Type:0x09 デバイスの完全な名前
  // 完全な名前が長くなりそうなときは、AD Type:0x08のShortened local nameを使う(setShortName)
  oAdvertisementData.setName("WRB"); // 1+1+3=5 byte

  // ServiceUUIDの設定(※長さとAD Typeの値は関数内で自動設定される)
  oAdvertisementData.setCompleteServices(BLEUUID(SERVICE_UUID)); // 1+1+16=18 byte

  // Manufacture specific dataの設定 ... AD Type:0xFF
  //  先頭2バイトは企業の識別子の設定が必要。テスト用には0xFFFFを設定して良い
  std::string strManufacturerData = "";
  strManufacturerData += (char)0xff; // Manufaucture ID
  strManufacturerData += (char)0xff; // Manufaucture ID
  strManufacturerData += (char)value;
  oAdvertisementData.setManufacturerData(strManufacturerData); // 1+1+3=5 byte

  // 計 3+5+18+5=31
  pAdvertising->setAdvertisementData(oAdvertisementData);
}

void ble_advertise_start(uint8_t value){
  Serial.println(F("BLE Advertise Start."));
  setAdvData(pAdvertising, value);
  pAdvertising->start();
}

void ble_advertise_stop(){
  pAdvertising->stop();
  Serial.println(F("BLE Advertise Stop."));
}

// アドバタイズをするのは、以下の条件のとき
// 1. STATE_INITからSTATE_DRIVER_INに変化したとき(表示クリア)
// 2. STATE_DRIVER_INからSTATE_DRIVER_OPENに変化して、「ライダー名+変身音」が鳴るとき
// 3. STATE_DRIVER_OPENからSTATE_DRIVER_MOVEに変化して、ライダー名+変身音」が鳴るとき
// 4. STATE_INITからSTATE_READINGに変化して、「〜の刻+ふむふむ+待機音」が鳴るとき
// 5. STATE_READING_MOVEからSTATE_READINGに変化して、「〜の刻+ふむふむ+待機音」が鳴るとき

void control_ble(){

  unsigned long now_ms = millis();

  if(prev_state != state){
    // 状態遷移直後の処理
    switch(state){
    case STATE_DRIVER_IN:
      ble_advertise_start(0);
      wait_ble_advertise_state = WAIT_BLE_ADVERTISE_STATE_ADVERTISING;
      ble_advertise_wait_start_time = now_ms;
      break;
    case STATE_DRIVER_OPEN:
      if(prev_state == STATE_DRIVER_IN){
        ble_advertise_wait_start_time = now_ms;
        wait_ble_advertise_state = WAIT_BLE_ADVERTISE_STATE_READY_NAME_CHANGE_LONG;
      }
      break;
    case STATE_DRIVER_MOVE:
      wait_ble_advertise_state = WAIT_BLE_ADVERTISE_STATE_READY_NAME_CHANGE_SHORT;
      ble_advertise_wait_start_time = now_ms;
      break;
    case STATE_READING:
      ble_advertise_wait_start_time = now_ms;
      wait_ble_advertise_state = WAIT_BLE_ADVERTISE_STATE_READY_READING;
      break;
    default:
      ;
    }
  }else{
    switch(wait_ble_advertise_state){
    case WAIT_BLE_ADVERTISE_STATE_NONE:
      break;
    case WAIT_BLE_ADVERTISE_STATE_READY_NAME_CHANGE_LONG:
      if(now_ms - ble_advertise_wait_start_time >= WAIT_BLE_ADVERTISE_FOR_NAME_CHANGE_LONG_MS){
        ble_advertise_start(num_rider);
        wait_ble_advertise_state = WAIT_BLE_ADVERTISE_STATE_ADVERTISING;
        ble_advertise_wait_start_time = now_ms;
      }
      break;
    case WAIT_BLE_ADVERTISE_STATE_READY_NAME_CHANGE_SHORT:
      if(now_ms - ble_advertise_wait_start_time >= WAIT_BLE_ADVERTISE_FOR_NAME_CHANGE_SHORT_MS){
        ble_advertise_start(num_rider);
        wait_ble_advertise_state = WAIT_BLE_ADVERTISE_STATE_ADVERTISING;
        ble_advertise_wait_start_time = now_ms;
      }
      break;
    case WAIT_BLE_ADVERTISE_STATE_READY_READING:
      if(now_ms - ble_advertise_wait_start_time >= WAIT_BLE_ADVERTISE_FOR_READING_MS){
        ble_advertise_start(num_rider);
        wait_ble_advertise_state = WAIT_BLE_ADVERTISE_STATE_ADVERTISING;
        ble_advertise_wait_start_time = now_ms;
      }
      break;
    case WAIT_BLE_ADVERTISE_STATE_ADVERTISING:
      if(now_ms - ble_advertise_wait_start_time >= WAIT_BLE_ADVERTISE_FOR_END_MS){
        ble_advertise_stop();
        wait_ble_advertise_state = WAIT_BLE_ADVERTISE_STATE_NONE;
      }
      break;
    default:
      ;
    }
  }
}

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

void setup(){
  M5.begin();

  //Serial.begin(115200); // M5 Paperではこの宣言は不要でSerial利用可(逆に書くと動作がおかしくなる)
  pinMode(PIN_BOTTOM_SW,    INPUT_PULLUP);
  pinMode(PIN_DRIVER_SW,    INPUT_PULLUP);
  pinMode(PIN_BACK_SW,      INPUT_PULLUP);
  pinMode(PIN_TOP_SW,       INPUT_PULLUP);
  pinMode(PIN_SIDE_SW_UP,   INPUT_PULLUP);
  pinMode(PIN_SIDE_SW_PUSH, INPUT_PULLUP);
  pinMode(PIN_SIDE_SW_DOWN, INPUT_PULLUP);

  //---------- MP3プレイヤー ----------
  hs_mp3_player.begin(9600, SERIAL_8N1, PIN_MP3_RX, PIN_MP3_TX);

  if(!mp3_player.begin(hs_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."));
  delay(100);
  mp3_player.volume(SOUND_VOLUME_DEFAULT); 

  //---------- 描画処理 ----------
  M5.EPD.SetRotation(90);
  M5.TP.SetRotation(90);  // タッチパネル設定
  M5.EPD.Clear(true);
  canvas.createCanvas(540, 960);
  draw_picture(0);

  //---------- BLEアドバタイズ準備 ----------
  BLEDevice::init("WRB");
  pServer = BLEDevice::createServer();
  pAdvertising = pServer->getAdvertising();
}

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

  //////////////////// 状態遷移管理 ////////////////////

  // スイッチ処理
  bottom_sw    = digitalRead(PIN_BOTTOM_SW);
  driver_sw    = digitalRead(PIN_DRIVER_SW);
  back_sw      = digitalRead(PIN_BACK_SW);
  top_sw       = digitalRead(PIN_TOP_SW);
  side_sw_down = digitalRead(PIN_SIDE_SW_DOWN);
  side_sw_up   = digitalRead(PIN_SIDE_SW_UP);
  side_sw_push = digitalRead(PIN_SIDE_SW_PUSH);
  side_sw_down = digitalRead(PIN_SIDE_SW_DOWN);

  // タッチ処理
  if(M5.TP.avaliable()){
    M5.TP.update();
    if(M5.TP.getFingerNum() > 0){
      is_touching = true;
    }else{
      is_touching = false;
    }
    M5.TP.flush();
  }

  // スイッチ&タッチによる状態遷移
  switch(state){
  case STATE_INIT:
    if(prev_side_sw_push == OFF && side_sw_push == ON){
      state = STATE_STORY;
      is_side_sw_pressing = true; // 横スイッチ長押し認識開始
      side_sw_long_press_start_time = now_ms;
    }else if(prev_side_sw_up == OFF && side_sw_up == ON){
      if(num_rider > 0){
        num_rider--;
      }
      state = STATE_MOVE;
    }else if(prev_side_sw_down == OFF && side_sw_down == ON){
      if(num_rider < NUM_HEISEI_RIDERS){
        num_rider++;
      }
      state = STATE_MOVE;
    }else if(prev_bottom_sw == OFF && bottom_sw == ON){
      state = STATE_DRIVER_IN;
    }else if(prev_back_sw == OFF && back_sw == ON){
      is_back_sw_pressing = true; // 背面スイッチ長押し認識開始
      back_sw_long_press_start_time = now_ms;
    }
    break;
  case STATE_MOVE:
    if(prev_side_sw_down == ON && side_sw_down == OFF){
      state = STATE_INIT;
    }else if(prev_side_sw_up == ON && side_sw_up == OFF){
      state = STATE_INIT;
    }
    break;
  case STATE_STORY:
    if(prev_side_sw_push == ON && side_sw_push == OFF){
      state = STATE_INIT;
      is_side_sw_pressing = false; // 横スイッチ長押し認識終了
    }
    break;
  case STATE_PORTRAIT:
    if(prev_side_sw_push == ON && side_sw_push == OFF){
      state = STATE_INIT;
    }
    break;
  case STATE_DRIVER_IN:
    if(prev_bottom_sw == ON && bottom_sw == OFF){
      state = STATE_INIT;
    }else if(prev_driver_sw == OFF && driver_sw == ON){
      state = STATE_DRIVER_OPEN;
    }
    break;
  case STATE_DRIVER_OPEN:
    if(prev_side_sw_up == OFF && side_sw_up == ON){
      if(num_rider > 0){
        num_rider--;
      }
      state = STATE_DRIVER_MOVE;
    }else if(prev_side_sw_down == OFF && side_sw_down == ON){
      if(num_rider < NUM_HEISEI_RIDERS){
        num_rider++;
      }
      state = STATE_DRIVER_MOVE;
    }else if(prev_bottom_sw == ON && bottom_sw == OFF){
      state = STATE_INIT;
    }else if(prev_side_sw_push == OFF && side_sw_push == ON){
      state = STATE_BGM;
    }else if(prev_is_touching == false && is_touching == true){
      state = STATE_WEAPON;
      is_summoning_weapon = true;
      summon_weapon_start_time = now_ms;
    }
    break;
  case STATE_DRIVER_MOVE:
    if(prev_side_sw_down == ON && side_sw_down == OFF){
      state = STATE_DRIVER_OPEN;
    }else if(prev_side_sw_up == ON && side_sw_up == OFF){
      state = STATE_DRIVER_OPEN;
    }else if(prev_bottom_sw == ON && bottom_sw == OFF){
      state = STATE_INIT;
    }
    break;
  case STATE_BGM:
    if(prev_side_sw_push == ON && side_sw_push == OFF){
      state = STATE_DRIVER_OPEN;
    }else if(prev_bottom_sw == ON && bottom_sw == OFF){
      state = STATE_INIT;
    }
    break;
  case STATE_WEAPON:
    if(prev_bottom_sw == ON && bottom_sw == OFF){
      is_summoning_weapon = false;
      state = STATE_INIT;
    }
    break;
  case STATE_READING:
    if(prev_back_sw == ON && back_sw == OFF){
      state = STATE_INIT;
    }else if(prev_side_sw_up == OFF && side_sw_up == ON){
      if(num_rider > 0){
        num_rider--;
      }
      state = STATE_READING_MOVE;
    }else if(prev_side_sw_down == OFF && side_sw_down == ON){
      if(num_rider < NUM_HEISEI_RIDERS){
        num_rider++;
      }
      state = STATE_READING_MOVE;
    }
    break;
  case STATE_READING_MOVE:
    if(prev_back_sw == ON && back_sw == OFF){
      state = STATE_INIT;
    }else if(prev_side_sw_up == ON && side_sw_up == OFF){
      state = STATE_READING;
    }else if(prev_side_sw_down == ON && side_sw_down == OFF){
      state = STATE_READING;
    }
    break;
  default:
    ;
  }

  // 時間経過による状態遷移
  if(is_side_sw_pressing == true && now_ms - side_sw_long_press_start_time >= SIDE_SW_LONG_PRESS_MS){
    if(state == STATE_STORY){
      state = STATE_PORTRAIT;
    }
    is_side_sw_pressing = false; // 横スイッチ長押し認識終了
  }

  if(is_summoning_weapon == true && now_ms - summon_weapon_start_time >= SUMMON_WEAPON_MS){
    if(state == STATE_WEAPON){
      state = STATE_DRIVER_OPEN;
    }
    is_summoning_weapon = false;
  }

  if(prev_back_sw == ON && back_sw == OFF){
    is_back_sw_pressing = false; // 背面スイッチ長押し認識終了
  }

  if(is_back_sw_pressing && now_ms - back_sw_long_press_start_time >= BACK_SW_LONG_PRESS_MS){
    if(state == STATE_INIT){
      state = STATE_READING;
    }
    is_back_sw_pressing = false; // 背面スイッチ長押し認識終了
  }

  ////////// 聖剣判定 /////////////
  if(prev_state == STATE_DRIVER_IN && state == STATE_DRIVER_OPEN){
    if(back_sw == OFF){
      sword_mode = SWORD_MODE_HOLY;
    }else{
      if(top_sw == OFF){
        sword_mode = SWORD_MODE_EVIL;
      }else{
        sword_mode = SWORD_MODE_SPECIAL;
      }
    }
  }

  ////////// 必殺技用ページ読取管理1 /////////////
  if((prev_state == STATE_INIT || prev_state == STATE_READING_MOVE) && state == STATE_READING){
    if(num_rider != 0){
      legend_read[num_rider - 1] = true;
    }
  }

  ////////// 音声再生処理 ////////////////////
  control_sound();

  ////////// 必殺技ページ読取管理2(リセット) /////////////
  if((prev_state == STATE_READING || prev_state == STATE_READING_MOVE) && state == STATE_INIT){
    reset_legend_read();
  }

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

  ////////// BLE処理 ///////////////////////
  control_ble();

  ////////// デバッグ処理 ////////////////////
  print_state();

  ////////// 処理状態の保持 //////////////////
  prev_bottom_sw    = bottom_sw;
  prev_driver_sw    = driver_sw;
  prev_back_sw      = back_sw;
  prev_top_sw       = top_sw;
  prev_side_sw_up   = side_sw_up;
  prev_side_sw_push = side_sw_push;
  prev_side_sw_down = side_sw_down;
  prev_is_touching  = is_touching;
  prev_state        = state;

  delay(LOOP_DELAY_MS);
}