【押せる】Pスイッチをつくる

今回は『スーパーマリオ』シリーズでお馴染みの『Pスイッチ』を実際に作ってみました。

開発経緯

きっかけは、昨年のクリスマスにサンタさんが長男にプレゼントしてくれた『スーパーマリオメーカー2』です。

ウチの長男、ゲームについてはまだ自分では上手く操作できないので、パパにアレしてコレしてという感じの関わり方だったのですが、YouTube上のいろんなプレイ動画が楽しかったようで、『スーパーマリオメーカー2』で初めて自分で操作しながらゲームを遊ぶようになりました。パパは嬉しいです。

で、長男がマリオの世界にすっかりハマったようだったので、大阪に出た際にNintendo OSAKAで何かお土産買えないかなとお店に行ってみたところ、

Pスイッチが売ってました。2,475円。

モノとしてはただの貯金箱なので、貯金箱として見たら高いです。

しかしその存在感は唯一無二。高いが安い。

あくまで貯金箱なので、押せる・光る・鳴るといったギミックはありません。ただ、こんなに見た目が立派なPスイッチがあればやっぱり押したい。

ということで、このガワを活かして、押せる・光る・鳴るPスイッチを作ることにしました。長男も喜んでくれそうです。

特徴

今回作ったものとしては、こんな感じの謎のタワーです。

これをPスイッチ貯金箱の中にセットし、

下部を外側からロックすることで、

押せる・光る・鳴るPスイッチにパワーアップします。Pスイッチ側は無改造なので、飽きたら元の貯金箱に簡単に戻すことができます。

 

電源は単四電池×3本とUSB Type-C給電のどちらでもOKです。

スライドスイッチで切り替え可。

場所を選ばず使いたいときと、場所固定で電池を気にせず使いたいときとで使い分けが可能です。

 

電源を入れてしばらくすると、出現音がして、Pスイッチが発光します。

 

この状態でPスイッチを上から押し込むと、ゲーム中と同様の効果音が流れます。

 

さすがにゲーム中みたいに本体が潰れるような表現はできなかったので、代わりに押されている間は発光が停止するようにしています。

 

さて、上の動画を観て、「あれ、なんか自分の知っているPスイッチの音と違うぞ??」と思われた方もいるかもしれません。

そう、実はPスイッチの音は時代によって異なります。ざっくり言うと、ファミコン時代、スーパーファミコン時代、DS時代以降、ぐらいな感じでしょうか。

ということで、各ユーザの馴染みの音(?)で鳴らせるように、モード切り替えできるようにしました。

底面中央のボタンがモード切り替えボタンで、押すたびに以下の順で発光&サウンドパターンが切り替わります。

  • スーパーマリオブラザーズ
  • スーパーマリオブラザーズ3
  • スーパーマリオワールド
  • New スーパーマリオブラザーズ U
  • スーパーマリオ 3Dワールド

モード(シリーズ)の選出については、『スーパーマリオメーカー2』に合わせています。本来Pスイッチは元祖『スーパーマリオブラザーズ』では出てこないのですが、『スーパーマリオメーカー2』では使用可能になっているので、選べるようにしています。

ちなみに『スーパーマリオメーカー2』ではシリーズによっては微妙に発光パターンが違っていたりするので、それぞれなるべく近い発光イメージになるようにしています。

底面のもう一つのボタンはBGM ON/OFFボタンです。ONにすると、各シリーズの地上BGMを流すことができるので、よりゲーム中でPスイッチを押す感覚を味わうことができます。一方、OFFにすれば、より自然に、我々の日常生活の中にPスイッチを置いておくことができます。

モードと発光・サウンドパターンの関係は以下のような感じになります。

発光 サウンド BGM
スーパーマリオブラザーズ 文字点滅+全体青点滅 ファミコン版 地上BGM
スーパーマリオブラザーズ3 文字点滅+全体青点滅 ファミコン版 地上BGM
スーパーマリオワールド 文字常時発光+全体白点滅 スーパーファミコン版 地上BGM
New スーパーマリオブラザーズ U 全体白点滅 DS以降版 地上BGM
スーパーマリオ 3Dワールド 発光なし DS以降版 スーパーベルの丘

「地上BGM」は、名前は全部一緒ですが、すべて、各シリーズ別の音楽になっています。

そして最後に、このPスイッチは、押すことで現実世界に何らかのアクションを起こすことができるようになっています。何を起こすかはプログラム次第。

さすがにブロックをコインに変えたり、コインをブロックに変えたりはできませんが、例えば押すことで「ただいま」のLINE通知を飛ばしたり。

 

照明をちょっとだけ点けたり、といったことができます。

 

こんな感じで「ちょっとした実用性」を持たせることで、ただのお遊び玩具じゃなくて、普段の生活の中で普通にPスイッチを使える、という姿を目指してみました。

ハードウェア解説

まずは今回使用した具材の紹介です。

何はともあれPスイッチ(の、貯金箱)。

各シリーズの地上BGMは全てこの音源に収録されているものを使用しました。

今回、部品を追加する形でいくつか部品を3Dプリンタで作っています。主に下部のシルバーの部分に追加しているので、シルバーのPLAフィラメントを使用しました。

マイコンには、WiFiを使いたかったのでATOM Liteを使用しています。

サウンドを鳴らすのには毎度のお馴染みDFPlayerを使用。マイクロSDカードも忘れずに。スピーカーはこちらを使用しました。

LEDはNeoPixel系なら何でもOKですが、今回はこちらを使用しました。エアリアルを作ったときのものが余っていたので。

Pスイッチの押し判定用に、こちらのタクトスイッチを4個使用しました。

あとは、手元にあったタクトスイッチをBGMのON/OFF切り替え用、スライドスイッチを電源切り替え用に使用し、それから単4電池×4本用の電池ケースを使用しています。

 

ここからは内部部品の作り込みです。Pスイッチ本体に穴開け加工するのはできるだけ避けたかったので、部品はできるだけ中央にタワー状に配置しました。

この形状であれば、元々の貯金箱の蓋を外せば、Pスイッチ側は無加工でセットできます。

 

これだとPスイッチが乗っかっているだけなので、ロック用パーツで外側から挟み込んで固定します。

ロック用パーツは後ろでねじ止めしています。

ロック用パーツの構造は地味に結構試行錯誤しました。

最終的にちょっと図体は大きくなりましたが、何とか許容範囲内に収まったかなと思います。

 

回路図としてはこんな感じです。

LEDの配線がめっちゃめんどくさかったです。

Pスイッチを押すときの押し心地を良くしつつきちんと押し判定をするために、Pスイッチの下部に柔らかいタクトスイッチを4つほど並列に配置しています。

ちなみに電池は4本あるように見えますが一本はダミーで、中に電源やらスピーカーやらの配線を通しています。

ちなみに、構造上、光が側面・上面でちょっと弱くなってしまうところがあったので、

光を吸収しにくくなるパーツを最後にいくつか後付けしました。

ソフトウェア解説

Pスイッチで現実世界にアクションを起こす方法ですが、具体的には自身をWiFiクライアントとして動作させて、httpリクエストを投げる形で実現しています。なので、Web APIが公開されているサービスであれば基本的に何でもござれなハズです。今回は具体例としてLINEのNotifyとHueのON/OFF制御をやってみました。

LINEのAPIからの利用方法はNotify APIとMessaging APIの2種類があり、より柔軟な通知ができるのは後者なのですが、準備が大変なので、今回は前者にしました。Messaging APIについては過去に『仮面ライダーリバイス』の作品作りの際に利用・解説してますので、興味のある方はそちらをご参照ください。

Notify APIについては、Developerサイトで自分用のトークンを発行すれば、あとはソースコードの通りにリクエストを送れば特に問題はないかなと思います。ソースコードの全文を最後に掲載しています。

Hueの制御についても、Developerサイトの手順に従って自分のHue BridgeのIPアドレス、username、keyの情報を取得すれば、あとはソースコードのとおりにリクエストを送れば特に問題はないかと思います。「とりあえず動けばいいや」の精神でコード書いてますので、ひょっとしたら不要なヘッダー情報とか混じってるかもしれません。

あと、WiFiを常時接続にするか、スイッチを押したときだけ接続にするかは、ちょっと迷いました。前者にすると、スイッチを押したときのLINEや照明ON/OFFの反応は速くなりますが、電池の消耗は速くなります。後者だと、電池の消耗は抑えられるものの、都度接続なので、接続が上手くいかなかったり遅延したりで遊びづらくなるリスクがあります。

最終的には、Pスイッチを常設するときはUSBケーブルで給電するので電池のもちはそこまで気にしなくて良いかな、と判断しましたので、遊びやすさ重視で常時接続する形で実装しました。

まとめ

以上、押せるPスイッチのご紹介でした。こういうのをやっている人って既にいそうだな、と思って探してみたら意外といなかったので今回やってみました。私的には楽しかったし、長男にも結構喜んでもらえたので満足しています。

次はまたライダー系で何かつくろうかなと思います。多分ガッチャードかファイズ。

ソースコード

以下、ソースコードの全文です。

#include "M5Atom.h"

#define LOOP_DELAY_MS 20

#define PIN_MP3_RX    32 // Groveポート
#define PIN_MP3_TX    26 // Groveポート
#define PIN_SW_MODE   39
#define PIN_SW_BGM    21
#define PIN_SW_ON     33
#define PIN_COLOR_LED 23

#define ON  LOW
#define OFF HIGH

uint8_t sw_mode = OFF;
uint8_t sw_bgm  = OFF;
uint8_t sw_on   = OFF;
uint8_t prev_sw_mode = OFF;
uint8_t prev_sw_bgm  = OFF;
uint8_t prev_sw_on   = OFF;

#define MODE_1     0
#define MODE_3     1
#define MODE_WORLD 2
#define MODE_NEW_U 3
#define MODE_3D    4 

uint8_t mode = MODE_1;
boolean is_playing_bgm = false;

#define P_SW_ON_TIME_MS 9000

boolean is_p_sw_on = false;
unsigned long p_sw_on_point_ms = 0;

////////// WiFi処理 ////////////////////////////////////////////////////////////

#include <WiFiClientSecure.h>

const char* SSID       = "xxxxxxxxxxxxxxx";
const char* PASSWORD   = "xxxxxxxxxxxxxxx";

const char* LINE_HOST  = "notify-api.line.me";
const char* LINE_TOKEN = "xxxxxxxxxxxxxxx";

const char* HUE_HOST      = "192.168.11.2";
const char* HUE_USERNAME  = "xxxxxxxxxxxxxxx";
const char* HUE_CLIENTKEY = "xxxxxxxxxxxxxxx";
const char* HUE_RID       = "xxxxxxxxxxxxxxx";

#define WIFI_CHECK_INTERVAL_MS 60000
unsigned long wifi_check_start_point_ms = 0;
uint8_t reset_counter = 0;

void line_notify(String msg){
  Serial.print("LINE connecting ... ");
  WiFiClientSecure client;
  if(!client.connect(LINE_HOST, 443)) {
    Serial.println("Failed.");
    return;
  }
  Serial.println("Connected.");

  String query = String("message=") + msg;
  String request = String("")
              + "POST /api/notify HTTP/1.1\r\n"
              + "Host: " + LINE_HOST + "\r\n"
              + "Authorization: Bearer " + LINE_TOKEN + "\r\n"
              + "Content-Length: " + String(query.length()) +  "\r\n"
              + "Content-Type: application/x-www-form-urlencoded\r\n\r\n"
              + query + "\r\n";
  client.print(request);

  // レスポンス受信確認
  while (client.connected()) {
    String line = client.readStringUntil('\n');
    Serial.println(line);
    if (line == "\r") {
      break;
    }
  }
}

void hue_control(boolean onoff){
  Serial.print("Hue connecting ... ");
  WiFiClientSecure client;
  if(!client.connect(HUE_HOST, 443)) {
    Serial.println("Failed.");
    return;
  }
  Serial.println("Connected.");

  String request = String("")
              + "PUT /clip/v2/resource/light/" + HUE_RID + " HTTP/1.1\r\n"
              + "hue-application-key: " + HUE_USERNAME + "\r\n"
              + "Host: " + HUE_HOST + "\r\n"
              + "Connection: close\r\n"
              + "Content-Type: text/plain; charset=UTF-8\r\n";
  if(onoff){
    request += "Content-Length: 18\r\n\r\n{\"on\":{\"on\":true}}";
  }else{
    request += "Content-Length: 19\r\n\r\n{\"on\":{\"on\":false}}";
  }
  client.print(request);

  // レスポンス受信確認
  while (client.connected()) {
    String line = client.readStringUntil('\n');
    Serial.println(line);
    if (line == "\r") {
      break;
    }
  }
}

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

#include <DFPlayerMini_Fast.h>

// 従来はSoftwareSerialを使ってDFPlayerやMP3ボイスモジュールと接続していたが、
// ESP32はデフォルトではソフトウェアシリアルは使用できない。
// ただし、3系統のハードウェアシリアルを使用できるので、そちらを使って通信を行う。
// SerialはUSB経由でのPCとの通信に使用するので、Serial1 または Serial2を使用する。
// 参考:M5StickCでUART(Serial)を使う https://lang-ship.com/blog/work/m5stickc-uartserial/

HardwareSerial hs_mp3_player(1);
DFPlayerMini_Fast mp3_player;

#define SOUND_POPUP_1      1
#define SOUND_POPUP_3      2
#define SOUND_POPUP_WORLD  3
#define SOUND_POPUP_NEW_U  4
#define SOUND_POPUP_3D     5
#define SOUND_SW_ON_1      6
#define SOUND_SW_ON_3      7
#define SOUND_SW_ON_WORLD  8
#define SOUND_SW_ON_NEW_U  9
#define SOUND_SW_ON_3D    10
#define SOUND_BGM_1       11
#define SOUND_BGM_3       12
#define SOUND_BGM_WORLD   13
#define SOUND_BGM_NEW_U   14
#define SOUND_BGM_3D      15

#define SOUND_VOLUME 27 // 0〜30

#define PLAY_CHECK_INTERVAL_MS 200
unsigned long play_check_point_ms = 0;

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

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

void play_popup(uint8_t mode){
  switch(mode){
  case MODE_1:     play_sound(SOUND_POPUP_1);     break;
  case MODE_3:     play_sound(SOUND_POPUP_3);     break;
  case MODE_WORLD: play_sound(SOUND_POPUP_WORLD); break;
  case MODE_NEW_U: play_sound(SOUND_POPUP_NEW_U); break;
  case MODE_3D:    play_sound(SOUND_POPUP_3D);    break;
  default: ;
  }
}

void play_sw_on(uint8_t mode){
  switch(mode){
  case MODE_1:     play_sound(SOUND_SW_ON_1);     break;
  case MODE_3:     play_sound(SOUND_SW_ON_3);     break;
  case MODE_WORLD: play_sound(SOUND_SW_ON_WORLD); break;
  case MODE_NEW_U: play_sound(SOUND_SW_ON_NEW_U); break;
  case MODE_3D:    play_sound(SOUND_SW_ON_3D);    break;
  default: ;
  }
}

void play_bgm(uint8_t mode){
  switch(mode){
  case MODE_1:     play_sound(SOUND_BGM_1);     break;
  case MODE_3:     play_sound(SOUND_BGM_3);     break;
  case MODE_WORLD: play_sound(SOUND_BGM_WORLD); break;
  case MODE_NEW_U: play_sound(SOUND_BGM_NEW_U); break;
  case MODE_3D:    play_sound(SOUND_BGM_3D);    break;
  default: ;
  }
}

////////// 発光処理 /////////////////////////////////////////////////////////////

#include <Adafruit_NeoPixel.h>
#define N_LED      15
#define N_MAIN_LED 12 // 本体部分のLED(正面3つ除く)
#define P_LED      12

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

struct color_rgb COLOR_WHITE = {  0,  64,  64}; // 白だと白くなり過ぎなのでシアンにする
struct color_rgb COLOR_BLUE  = {  0,   0, 128};

int prev_interval_ms = 0;
uint8_t prev_steps = 0;
boolean is_inc = true;
unsigned long inc_dim_start_point_ms = 0;

unsigned long prev_p_action_point_ms = 0;
boolean is_p_blink_on = false;

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

void led_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_pattern_off(){
  for(uint8_t i=0;i<N_LED;i++){
    pixels.setPixelColor(i, pixels.Color(0,0,0));
  }
}

void led_pattern_blink_slowly(struct color_rgb *color, unsigned long now_ms, int interval_ms, uint8_t steps, boolean is_p_fixed){
  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(is_p_fixed){
    pixels.setPixelColor(P_LED, pixels.Color(COLOR_WHITE.r,COLOR_WHITE.g,COLOR_WHITE.b));
  }

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

void led_pattern_asymmetry_blink_slowly(struct color_rgb *color, unsigned long now_ms, int interval_ms, uint8_t steps, int p_on_ms, int p_off_ms){
  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_MAIN_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_MAIN_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;
  }   

  if(is_p_blink_on){
    if(now_ms - prev_p_action_point_ms >= p_on_ms){
      pixels.setPixelColor(P_LED, pixels.Color(0,0,0));
      is_p_blink_on = false;
      prev_p_action_point_ms = now_ms;
    }
  }else{
    if(now_ms - prev_p_action_point_ms >= p_off_ms){
      pixels.setPixelColor(P_LED, pixels.Color(COLOR_WHITE.r,COLOR_WHITE.g,COLOR_WHITE.b));
      is_p_blink_on = true;
      prev_p_action_point_ms = now_ms;
    }
  }
}

void control_led(unsigned long now_ms){
  if(!is_p_sw_on){
    switch(mode){
    case MODE_1:     led_pattern_asymmetry_blink_slowly(&COLOR_BLUE, now_ms, 400, 20, 650, 150); break;
    case MODE_3:     led_pattern_asymmetry_blink_slowly(&COLOR_BLUE, now_ms, 400, 20, 650, 150); break;
    case MODE_WORLD: led_pattern_blink_slowly(&COLOR_WHITE, now_ms, 400, 20, true);  break;
    case MODE_NEW_U: led_pattern_blink_slowly(&COLOR_WHITE, now_ms, 300, 10, false); break;
    case MODE_3D:    led_pattern_off(); break;
    default: ;
    }
  }else{
    led_pattern_off();
  }

  pixels.show();
}

////////// メイン処理 ////////////////////////////////////////////////////////////

void setup(){
  M5.begin();

  Serial.begin(115200);
  pinMode(PIN_SW_MODE, INPUT_PULLUP);
  pinMode(PIN_SW_BGM, INPUT_PULLUP);
  pinMode(PIN_SW_ON, INPUT_PULLUP);
  pinMode(PIN_COLOR_LED, OUTPUT);

  // 省電力化のため、CPU周波数を少し落としておく。最大240MHz。80MHz未満にするとNeoPixelが動かない
  setCpuFrequencyMhz(160);

  // WiFi接続
  Serial.printf("Connecting to %s ", SSID);
  uint8_t retry_counter = 0;
  WiFi.begin(SSID, PASSWORD);
  while (WiFi.status() != WL_CONNECTED) {
    retry_counter++;
    delay(500);
    Serial.print(".");
    if(retry_counter % 6 == 0){
      // 3秒経って接続できなければ、WiFi.beginを再実行
      WiFi.disconnect();
      WiFi.begin(SSID, PASSWORD);
    }
    if(retry_counter > 18){
      // 3回やり直してもダメなら、本体そのものをリセット
      ESP.restart();
      Serial.println(" Failed. Reboot.");
    }
  }
  Serial.println(" Connected.");

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

  // 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(500); // 間を開けるのが短すぎるとコマンドが有効にならないので注意
  mp3_player.volume(SOUND_VOLUME);
  delay(200);
  play_sound(SOUND_POPUP_1);
}

void loop(){
  sw_mode = digitalRead(PIN_SW_MODE);
  sw_bgm  = digitalRead(PIN_SW_BGM);
  sw_on   = digitalRead(PIN_SW_ON);

  unsigned long now_ms = millis();

  if(prev_sw_mode == OFF && sw_mode == ON){
    switch(mode){
    case MODE_1:     mode = MODE_3;     break;
    case MODE_3:     mode = MODE_WORLD; break;
    case MODE_WORLD: mode = MODE_NEW_U; break;
    case MODE_NEW_U: mode = MODE_3D;    break;
    case MODE_3D:    mode = MODE_1;     break;
    default: ;
    }
    play_popup(mode);
    is_playing_bgm = false;
    is_p_sw_on = false;
  }

  if(prev_sw_bgm == OFF && sw_bgm == ON){
    if(is_playing_bgm){
      pause_sound();
    }else{
      play_bgm(mode);
      play_check_point_ms = now_ms;
    }
    is_playing_bgm = !is_playing_bgm;
  }

  if(prev_sw_on == OFF && sw_on == ON){
    play_sw_on(mode);
    led_pattern_off();
    pixels.show();
    p_sw_on_point_ms = now_ms;
    is_p_sw_on = true;

    //line_notify("ただいま");
    hue_control(true); // 照明ON
  }

  ////////// 時間経過処理 ////////////////////
  if(is_p_sw_on == true && now_ms - p_sw_on_point_ms > P_SW_ON_TIME_MS){
    // Pスイッチの効果が切れたら再度BGMを流す
    if(is_playing_bgm){
      play_bgm(mode);
    }
    is_p_sw_on = false;

    // 照明をつけた場合は、照明を消す
    //hue_control(false);
  }

  if(is_p_sw_on == false && is_playing_bgm == true && now_ms - play_check_point_ms > PLAY_CHECK_INTERVAL_MS){
    // BGM再生中にBGMが終了したら再度BGMを流す
    if(!mp3_player.isPlaying()){
      play_bgm(mode);
    }
    play_check_point_ms = now_ms;
  }

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

  ////////////////////// WiFiの接続が切れたときの処理(再起動) //////////////////////

  if(now_ms - wifi_check_start_point_ms >= WIFI_CHECK_INTERVAL_MS){
    if((WiFi.status() != WL_CONNECTED)){
      reset_counter++;
    }else{
      reset_counter = 0;
    }

    // WiFi未接続が3分継続したら再起動
    if(reset_counter >= 3){
      ESP.restart();
    }

    wifi_check_start_point_ms = now_ms;
  }

  ////////// 処理状態の保持 //////////////////
  prev_sw_mode = sw_mode;
  prev_sw_bgm  = sw_bgm;
  prev_sw_on   = sw_on;

  delay(LOOP_DELAY_MS);
}