オリジナルライドウォッチをつくる 〜ソフト(プログラム)編〜

お待たせ致しました、ここからオリジナルライドウォッチ改造の詳細に入ります。今回がソフト(プログラム)編、次回がハード編になります。必要な具材につきましては、前回の記事をご参照ください。

プログラムの説明ですが、私の万丈ライドウォッチのプログラムをいきなり題材にしてしまうと、フォームチェンジのためのリードスイッチの処理やらフルカラーLEDの処理やら、「シンプルにDX版相当のライドウォッチを作りたい」という方からすれば邪魔でしかないコードが含まれたものを説明することになってしまいます。そのため、ここでは「シンプルにDX版相当のライドウォッチの動作を再現するためのプログラム」を説明することにします。フォームチェンジ用のリードスイッチは使いませんし、フルカラーLEDも使いません。それらを使った私の万丈ライドウォッチのソースコードは、次回のハード編のときに参考としてご紹介します。

 

はじめに注意事項ですが、ここでご紹介するプログラムは、ジクウドライバーのLスロット側(=通常、レジェンドライダーのライドウォッチがセットされる側)にセットするライドウォッチを想定しています。Rスロット側(=通常、ジオウとゲイツのライドウォッチがセットされる側)のライドウォッチを作りたい場合は、少しソースコードの修正が必要になります。それはまた最後にご説明いたします。

 

さて、ライドウォッチのプログラムを書く上で避けて通れないのが、ライドウォッチの内部の状態遷移の理解です。オリジナルガシャットを作ったときも、初めは内部の状態遷移の調査からスタートしました

以下では、説明の便宜上、ライドウォッチの3つのスイッチに上記のような名前をつけます。これらのスイッチをどのように操作すれば、内部でどのように状態が変化し、その変化に伴い何が起こるのか?以下はビルドライドウォッチを題材に、自分なりにスイッチを色々触りながら調査した結果をまとめた状態遷移図になります。

上図は自分が独自に調査した結果であり、玩具の状態遷移を完全に再現しているものではありませんのでご注意ください。例えば、実際の玩具では「時間が経てば初期状態に戻る」という状態遷移が発生すると思いますが、上記の図ではその線は含んでいません。あくまで、自分がプログラムを作る上で必要な情報のみをまとめたものが上図になります。

スイッチの数としてはガシャットのときと同じですが、

  • 片方のスイッチを押しながら片方のスイッチを2回押すことで状態遷移が発生する
  • ある状態からある状態に遷移するのに2つのルート(ライダータイム&アーマータイム)が存在する

ということがあり、ガシャットのときよりも数段ややこしく感じました。

 

さて、以上がライドウォッチの状態遷移の基本ですが、ここから実際にプログラムを書こうと思うと、もうひと頑張りしなくてはいけません。「どの状態のときに、スイッチの組み合わせが何から何に変わると、どの状態に変わるのか」を一つ一つ確認していかないと、プログラムとして書き起こせないからです。これは相当に面倒だったので、詳細を知りたい方だけ、以下のPDFファイルを開いて拡大表示してみてください。

ライドウォッチ状態遷移整理

これは理解するのはかなりキツイ、という方は、理解しなくても大丈夫です。最終的には、以下で掲載するソースコードをまるっとコピペしてもらえば済む話になります。

 

以上でプログラムを書くための準備は終了です。お疲れ様でした。ここからは実際にソースコードを解説していきますが、最初にどこに何が書いてあるかを示しておいた方が何かと理解しやすいと思いますので、最初にプログラムの全体構造を示しておきます。

上から順にこんな感じで記述していっています。以下にソースコードを載せますが、詳細説明は上図の番号順に行なっていきます。

#include <SoftwareSerial.h>
#include <DFRobotDFPlayerMini.h>

SoftwareSerial ss_mp3_player(2, 3); // RX, TX
#define LED_PIN  5
#define SW_C_PIN 6
#define SW_1_PIN 7
#define SW_2_PIN 8

#define LOOP_INTERVAL_MS 20

#define ON  LOW
#define OFF HIGH
#define N_BUTTON 3
const uint8_t OFF_OFF_OFF[] = {OFF, OFF, OFF};
const uint8_t ON_OFF_OFF[]  = {ON,  OFF, OFF};
const uint8_t OFF_ON_OFF[]  = {OFF, ON,  OFF};
const uint8_t OFF_OFF_ON[]  = {OFF, OFF, ON };
const uint8_t ON_ON_OFF[]   = {ON,  ON,  OFF};
const uint8_t ON_OFF_ON[]   = {ON,  OFF, ON };
const uint8_t OFF_ON_ON[]   = {OFF, ON,  ON };
const uint8_t ON_ON_ON[]    = {ON,  ON,  ON };

#define STATE_SINGLE_A 1
#define STATE_SINGLE_B 2
#define STATE_READY_WP 3
#define STATE_WEAPON   4
#define STATE_READY_CH 5
#define STATE_CHANGED  6
#define STATE_READY_CR 7
#define STATE_CRITICAL 8

#define SIDE_NONE 0
#define SIDE_ARMOR_TIME 1
#define SIDE_RIDER_TIME 2

uint8_t prev_state = STATE_SINGLE_A;
uint8_t state      = STATE_SINGLE_A;
uint8_t prev_side  = SIDE_NONE;
uint8_t side       = SIDE_NONE;
uint8_t rider_time_counter = 0;
uint8_t armor_time_counter = 0;

uint8_t prev_sw[]  = {OFF, OFF, OFF};
uint8_t sw[] = {OFF, OFF, OFF};

//--------------------------------------------------------------------------//

// 効果音処理

#define SOUND_SINGLE_A       1
#define SOUND_SINGLE_B       2
#define SOUND_RIDER_TIME     3
#define SOUND_ARMOR_TIME     4
#define SOUND_WEAPON         5
#define SOUND_CRITICAL_READY 6
#define SOUND_CRITICAL       7
#define SOUND_EJECT          8

#define ARMOR_TIME_WAIT 6500
#define RIDER_TIME_WAIT 1000
#define CRITICAL_WAIT_SHORT 1000
#define CRITICAL_WAIT_LONG  2000

unsigned long sound_wait_start_time = 0;
boolean is_armor_time_waiting = false;
boolean is_rider_time_waiting = false;
boolean is_armor_time_critical_waiting = false;
boolean is_rider_time_critical_waiting = false;

DFRobotDFPlayerMini mp3_player;

void play_sound(){
  if(prev_state != state){
    switch(state){
    case STATE_SINGLE_A:
      switch(prev_state){
      case STATE_SINGLE_B:
        mp3_player.playMp3Folder(SOUND_SINGLE_A);
        break;
      case STATE_READY_WP:
      case STATE_WEAPON:
      case STATE_READY_CH:
      case STATE_CHANGED:
      case STATE_READY_CR:
      case STATE_CRITICAL:
        mp3_player.playMp3Folder(SOUND_EJECT);
        break;
      default:
        ;
      }
      break;
    case STATE_SINGLE_B:
      mp3_player.playMp3Folder(SOUND_SINGLE_B);
      break;
    case STATE_READY_WP:
      if(prev_state != STATE_WEAPON){
        mp3_player.pause();
      }
      break;
    case STATE_WEAPON:
      mp3_player.playMp3Folder(SOUND_WEAPON);
      break;
    case STATE_READY_CH:
      mp3_player.pause();
      break;
    case STATE_CHANGED:
      if(side == SIDE_RIDER_TIME){
        sound_wait_start_time = millis();
        is_rider_time_waiting = true;
      }else if(side == SIDE_ARMOR_TIME){
        sound_wait_start_time = millis();
        is_armor_time_waiting = true;
      }
      break;
    case STATE_READY_CR:
      mp3_player.playMp3Folder(SOUND_CRITICAL_READY);
      break;
    case STATE_CRITICAL:
      if(side == SIDE_RIDER_TIME){
        sound_wait_start_time = millis();
        is_rider_time_critical_waiting = true;
      }else if(side == SIDE_ARMOR_TIME){
        sound_wait_start_time = millis();
        is_armor_time_critical_waiting = true;
      }
      break;
    default:
      ;
    }
  }else{
    unsigned long now = millis();
    if(is_rider_time_waiting){
      if(now - sound_wait_start_time >= RIDER_TIME_WAIT){
          mp3_player.playMp3Folder(SOUND_RIDER_TIME);
          is_rider_time_waiting = false;
      }
    }else if(is_armor_time_waiting){
      if(now - sound_wait_start_time >= ARMOR_TIME_WAIT){
          mp3_player.playMp3Folder(SOUND_ARMOR_TIME);
          is_armor_time_waiting = false;
      }
    }else if(is_rider_time_critical_waiting){
      if(now - sound_wait_start_time >= CRITICAL_WAIT_LONG){
          mp3_player.playMp3Folder(SOUND_CRITICAL);
          is_rider_time_critical_waiting = false;
      }
    }else if(is_armor_time_critical_waiting){
      if(now - sound_wait_start_time >= CRITICAL_WAIT_SHORT){
          mp3_player.playMp3Folder(SOUND_CRITICAL);
          is_armor_time_critical_waiting = false;
      }
    }
  }
}

//--------------------------------------------------------------------------//

// 発光処理

#define LED_COUNT_MAX 1500 // LOOP_INTERVAL_MSが20msで、30sまでの発光定義を想定
#define LED_BRIGHTNESS 255 // 255が上限値

uint16_t led_counter = LED_COUNT_MAX;
unsigned long blink_time = 0;
unsigned long prev_blink_time = 0;
unsigned long inc_dim_start_time = 0;
boolean is_lighting = false;
boolean is_inc = false;

void led_base_pattern_on(){
  analogWrite(LED_PIN, LED_BRIGHTNESS);
}

void led_base_pattern_off(){
  analogWrite(LED_PIN, 0);
}

void led_base_pattern_blink(int interval_ms){
  unsigned long now = millis();
  if(now - prev_blink_time >= interval_ms){
    if(is_lighting){
      analogWrite(LED_PIN, 0);
    }else{
      analogWrite(LED_PIN, LED_BRIGHTNESS);
    }
    is_lighting = !is_lighting;
    prev_blink_time = now;
  }
}

void led_base_pattern_inc(int interval_ms, uint8_t steps){
  unsigned long now = millis();
  if(inc_dim_start_time == 0){
    inc_dim_start_time = now;
  }
  int ms_per_step = interval_ms / steps;
  int current_step = (now - inc_dim_start_time) / ms_per_step;
  uint8_t l_step = LED_BRIGHTNESS/steps;
  analogWrite(LED_PIN, l_step*current_step);
  if(now - inc_dim_start_time > interval_ms){
    inc_dim_start_time = 0;
  }
}

void led_base_pattern_dim(int interval_ms, uint8_t steps){
  unsigned long now = millis();
  if(inc_dim_start_time == 0){
    inc_dim_start_time = now;
  }
  int ms_per_step = interval_ms / steps;
  int current_step = (now - inc_dim_start_time) / ms_per_step;
  uint8_t l_step = LED_BRIGHTNESS/steps;
  analogWrite(LED_PIN, l_step*(steps-current_step));
  if(now - inc_dim_start_time > interval_ms){
    inc_dim_start_time = 0;
  }
}

void led_base_pattern_blink_slowly(int interval_ms, uint8_t steps){
  unsigned long now = millis();
  if(inc_dim_start_time == 0){
    inc_dim_start_time = now;
  }
  int ms_per_step = interval_ms / steps;
  int current_step = (now - inc_dim_start_time) / ms_per_step;
  uint8_t l_step = LED_BRIGHTNESS/steps;
  if(is_inc){
    analogWrite(LED_PIN, l_step*current_step);
  }else{
    analogWrite(LED_PIN, l_step*(steps-current_step));
  }
  if(now - inc_dim_start_time > interval_ms){
    is_inc = !is_inc;
    inc_dim_start_time = 0;
  }
}

void led_pattern_single_a(uint16_t led_counter_ms){ // 起動音+解説+「ビルドだ!」
  if(led_counter_ms <= 600){                                 led_base_pattern_blink(80);}
  else if(  600 < led_counter_ms && led_counter_ms <=  1600){ led_base_pattern_dim(1000, 10);}
  else if( 1600 < led_counter_ms && led_counter_ms <=  6200){ led_base_pattern_on();}
  else if( 6200 < led_counter_ms && led_counter_ms <=  8600){ led_base_pattern_blink(90);}
  else if( 8600 < led_counter_ms && led_counter_ms <=  9200){ led_base_pattern_off();}
  else if( 9200 < led_counter_ms && led_counter_ms <= 10700){ led_base_pattern_on();}
  else{                                                       led_base_pattern_off();}
}

void led_pattern_single_b(uint16_t led_counter_ms){ // 起動音+起動音+「ビルド!」
  if(led_counter_ms <= 600){                                led_base_pattern_blink(80);}
  else if( 600 < led_counter_ms && led_counter_ms <= 1600){ led_base_pattern_dim(1000, 10);}
  else if(1600 < led_counter_ms && led_counter_ms <= 3200){ led_base_pattern_blink(40);}
  else if(3200 < led_counter_ms && led_counter_ms <= 5000){ led_base_pattern_on();}
  else{                                                     led_base_pattern_off();}
}

void led_pattern_weapon(uint16_t led_counter_ms){ // 「ビルド!」
  if(led_counter_ms <= 300){                               led_base_pattern_blink(40);}
  else if(300 < led_counter_ms && led_counter_ms <= 1800){ led_base_pattern_on();}
  else{                                                    led_base_pattern_off();}
}

void led_pattern_rider_time(uint16_t led_counter_ms){
  if(led_counter_ms <= 1100){                               led_base_pattern_off();}
  else if(1100 < led_counter_ms && led_counter_ms <= 1900){ led_base_pattern_dim(800, 10);}
  else if(1900 < led_counter_ms && led_counter_ms <= 2800){ led_base_pattern_dim(900, 10);}
  else if(2800 < led_counter_ms && led_counter_ms <=11800){ led_base_pattern_on();}
  else{                                                     led_base_pattern_off();}
}

void led_pattern_armor_time(uint16_t led_counter_ms){
  if(led_counter_ms <= 6600){                               led_base_pattern_off();}
  else if(6600 < led_counter_ms && led_counter_ms <= 7400){ led_base_pattern_dim(800, 10);}
  else if(7400 < led_counter_ms && led_counter_ms <= 8600){ led_base_pattern_dim(1200, 10);}
  else if(8600 < led_counter_ms && led_counter_ms <=17600){ led_base_pattern_on();}
  else{                                                     led_base_pattern_off();}
}

void led_pattern_ready_critical(uint16_t led_counter_ms){ // 起動音+「ビルド!」+待機
  if(led_counter_ms <= 600){                                led_base_pattern_blink(80);}
  else if( 600 < led_counter_ms && led_counter_ms <= 1600){ led_base_pattern_dim(1000, 10);}
  else if(1600 < led_counter_ms && led_counter_ms <= 3100){ led_base_pattern_on();}
  else{                                                     led_base_pattern_blink(800);}
}

void led_pattern_critical_long(uint16_t led_counter_ms){ // 「ボルテック!」
  if(led_counter_ms <= 2100){                               led_base_pattern_off();}
  else if(2100 < led_counter_ms && led_counter_ms <= 2600){ led_base_pattern_blink(40);}
  else if(2600 < led_counter_ms && led_counter_ms <= 4100){ led_base_pattern_on();}
  else{                                                     led_base_pattern_off();}
}

void led_pattern_critical_short(uint16_t led_counter_ms){ // 「ボルテック!」
  if(led_counter_ms <= 1100){                               led_base_pattern_off();}
  else if(1100 < led_counter_ms && led_counter_ms <= 1600){ led_base_pattern_blink(40);}
  else if(1600 < led_counter_ms && led_counter_ms <= 3100){ led_base_pattern_on();}
  else{                                                     led_base_pattern_off();}
}

void flash_led(){

  if(prev_state != state){
    if(prev_state != STATE_SINGLE_B && state == STATE_SINGLE_A){
      led_counter = LED_COUNT_MAX; // 発光させないように、カウントの上限値にする
    }else if((prev_state == STATE_SINGLE_A || prev_state == STATE_SINGLE_B) && state == STATE_READY_WP){
      led_counter = LED_COUNT_MAX; // 発光させないように、カウントの上限値にする
    }else if(prev_state == STATE_WEAPON && state == STATE_READY_WP){
      ; // 発光を継続させるためにカウントをリセットしない
    }else{
      led_counter = 0; // 基本的に状態遷移が発生するとカウントをリセットする
    }
  }else if(prev_side != side){
    led_counter = 0;
  }

  uint16_t led_counter_ms = led_counter * LOOP_INTERVAL_MS; // LEDはms単位で制御

  switch(state){
  case STATE_SINGLE_A:
    led_pattern_single_a(led_counter_ms);
    break;
  case STATE_SINGLE_B:
    led_pattern_single_b(led_counter_ms);
    break;
  case STATE_READY_WP:
  case STATE_WEAPON:
    led_pattern_weapon(led_counter_ms);
    break;
  case STATE_READY_CH:
    led_base_pattern_blink(800);
    break;
  case STATE_CHANGED:
    if(side == SIDE_NONE){
      led_base_pattern_blink(800);
    }else if(side == SIDE_ARMOR_TIME){
      led_pattern_armor_time(led_counter_ms);
    }else if(side == SIDE_RIDER_TIME){
      led_pattern_rider_time(led_counter_ms);
    }
    break;
  case STATE_READY_CR:
    led_pattern_ready_critical(led_counter_ms);
    break;
  case STATE_CRITICAL:
    if(side == SIDE_ARMOR_TIME){
      led_pattern_critical_short(led_counter_ms);
    }else if(side == SIDE_RIDER_TIME){
      led_pattern_critical_long(led_counter_ms);
    }
    break;
  default:
    ;
  }

  if(led_counter < LED_COUNT_MAX){
    led_counter++;
  }
}

//--------------------------------------------------------------------------//

// リセット処理

void reset_rider_time_counter(){
  rider_time_counter = 0;
}

void reset_armor_time_counter(){
  armor_time_counter = 0;
}

void reset_all(){
  analogWrite(LED_PIN, 0);
  reset_rider_time_counter();
  reset_armor_time_counter();
  is_armor_time_waiting = false;
  is_rider_time_waiting = false;
  is_armor_time_critical_waiting = false;
  is_rider_time_critical_waiting = false;
  led_counter = LED_COUNT_MAX;
}

//--------------------------------------------------------------------------//

// 起動処理

void setup() {
  Serial.begin(115200);
  pinMode(LED_PIN, OUTPUT);
  pinMode(SW_C_PIN, INPUT_PULLUP);
  pinMode(SW_1_PIN, INPUT_PULLUP);
  pinMode(SW_2_PIN, INPUT_PULLUP);

  // ---------- MP3プレイヤーセットアップ ----------
  ss_mp3_player.begin(9600);
  if (!mp3_player.begin(ss_mp3_player)) {  //Use softwareSerial to communicate with mp3.
    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.setTimeOut(500); //Set serial communictaion time out 500ms
  mp3_player.volume(10);  //Set volume value (0~30).

  // ---------- 起動エフェクト ----------
  analogWrite(LED_PIN, LED_BRIGHTNESS);
  delay(1500);
  analogWrite(LED_PIN, 0);
}

//--------------------------------------------------------------------------//

// メイン処理

void loop() {
  sw[0] = digitalRead(SW_C_PIN);
  sw[1] = digitalRead(SW_1_PIN);
  sw[2] = digitalRead(SW_2_PIN);

  prev_state = state;
  prev_side  = side;

  if(memcmp(prev_sw, OFF_OFF_OFF, N_BUTTON) == 0){
    if(memcmp(sw, ON_OFF_OFF, N_BUTTON) == 0){
      if(prev_state == STATE_SINGLE_A){
        state = STATE_SINGLE_B;
      }else if(prev_state == STATE_SINGLE_B){
        state = STATE_SINGLE_A;
      }
    }else if(memcmp(sw, OFF_ON_OFF, N_BUTTON) == 0){
      if(prev_state == STATE_SINGLE_A || prev_state == STATE_SINGLE_B){
        state = STATE_READY_WP;
      }
    }else if(memcmp(sw, OFF_OFF_ON, N_BUTTON) == 0){
      ;
    }else if(memcmp(sw, ON_ON_OFF,  N_BUTTON) == 0){
      ;
    }else if(memcmp(sw, ON_OFF_ON,  N_BUTTON) == 0){
      ;
    }else if(memcmp(sw, OFF_ON_ON,  N_BUTTON) == 0){
      if(prev_state == STATE_SINGLE_A || prev_state == STATE_SINGLE_B){
        state = STATE_READY_CH;
      }
    }else if(memcmp(sw, ON_ON_ON,   N_BUTTON) == 0){
      ;
    }
  }else if(memcmp(prev_sw, ON_OFF_OFF, N_BUTTON) == 0){
    if(memcmp(sw, OFF_OFF_OFF, N_BUTTON) == 0){
      ;
    }else if(memcmp(sw, ON_ON_OFF,  N_BUTTON) == 0){
      if(prev_state == STATE_SINGLE_A || prev_state == STATE_SINGLE_B){
        state = STATE_READY_WP;
      }
    }else if(memcmp(sw, ON_OFF_ON,  N_BUTTON) == 0){
      ;
    }else if(memcmp(sw, OFF_ON_OFF, N_BUTTON) == 0){
      ;
    }else if(memcmp(sw, OFF_OFF_ON, N_BUTTON) == 0){
      ;
    }else if(memcmp(sw, ON_ON_ON,   N_BUTTON) == 0){
      if(prev_state == STATE_SINGLE_A || prev_state == STATE_SINGLE_B){
        state = STATE_READY_CH;
      }
    }else if(memcmp(sw, OFF_ON_ON,  N_BUTTON) == 0){
      ;
    }
  }else if(memcmp(prev_sw, OFF_ON_OFF, N_BUTTON) == 0){
    if(memcmp(sw, ON_ON_OFF, N_BUTTON) == 0){
      if(prev_state == STATE_CHANGED){
        reset_all();
        state = STATE_READY_CR;
      }else if(prev_state == STATE_READY_CR){
        reset_all();
        prev_state = STATE_CHANGED; // 擬似的に状態変化を起こす
        state = STATE_READY_CR;
      }
    }else if(memcmp(sw, OFF_OFF_OFF, N_BUTTON) == 0){
      if(prev_state == STATE_CHANGED || prev_state == STATE_READY_WP || prev_state == STATE_READY_CH || prev_state == STATE_READY_CR){
        reset_all();
        state = STATE_SINGLE_A;
      }
    }else if(memcmp(sw, OFF_ON_ON,   N_BUTTON) == 0){
      if(prev_state == STATE_READY_WP){
        state = STATE_WEAPON;
      }else if(prev_state == STATE_READY_CH || prev_state == STATE_CHANGED){
        rider_time_counter++;
        if(rider_time_counter == 2){
          prev_state = STATE_READY_CH; // 擬似的に状態変化を起こす
          state = STATE_CHANGED;
          side  = SIDE_RIDER_TIME;
        }
      }else if(prev_state == STATE_READY_CR){
        rider_time_counter++;
        if(rider_time_counter == 2){
          state = STATE_CRITICAL;
          side  = SIDE_RIDER_TIME;
        }
      }
    }else if(memcmp(sw, ON_OFF_OFF,  N_BUTTON) == 0){
      ;
    }else if(memcmp(sw, ON_ON_ON,    N_BUTTON) == 0){
      ;
    }else if(memcmp(sw, OFF_OFF_ON,  N_BUTTON) == 0){
      ;
    }else if(memcmp(sw, ON_OFF_ON,   N_BUTTON) == 0){
      ;
    }
  }else if(memcmp(prev_sw, OFF_OFF_ON, N_BUTTON) == 0){
    if(memcmp(sw, ON_OFF_ON, N_BUTTON) == 0){
      if(prev_state == STATE_SINGLE_A){
        state = STATE_SINGLE_B;
      }else if(prev_state == STATE_SINGLE_B){
        state = STATE_SINGLE_A;
      }else if(prev_state == STATE_CHANGED){
        reset_all();
        state = STATE_READY_CR;
      }else if(prev_state == STATE_READY_CR){
        reset_all();
        prev_state = STATE_CHANGED; // 擬似的に状態変化を起こす
        state = STATE_READY_CR;
      }
    }else if(memcmp(sw, OFF_ON_ON,   N_BUTTON) == 0){
      if(prev_state == STATE_SINGLE_A || prev_state == STATE_SINGLE_B){
        state = STATE_READY_CH;
      }else if(prev_state == STATE_READY_CH || prev_state == STATE_CHANGED){
        armor_time_counter++;
        if(armor_time_counter == 2){
          prev_state = STATE_READY_CH; // 擬似的に状態変化を起こす
          state = STATE_CHANGED;
          side  = SIDE_ARMOR_TIME;
        }
      }else if(prev_state == STATE_READY_CR){
        armor_time_counter++;
        if(armor_time_counter == 2){
          state = STATE_CRITICAL;
          side  = SIDE_ARMOR_TIME;
        }
      }
    }else if(memcmp(sw, OFF_OFF_OFF, N_BUTTON) == 0){
      if(prev_state == STATE_WEAPON || prev_state == STATE_CHANGED || prev_state == STATE_READY_CH || prev_state == STATE_READY_CR){
        reset_all();
        state = STATE_SINGLE_A;
      }
    }else if(memcmp(sw, ON_ON_ON,    N_BUTTON) == 0){
      ;
    }else if(memcmp(sw, ON_OFF_OFF,  N_BUTTON) == 0){
      ;
    }else if(memcmp(sw, OFF_ON_OFF,  N_BUTTON) == 0){
      ;
    }else if(memcmp(sw, ON_ON_OFF,   N_BUTTON) == 0){
      ;
    }
  }else if(memcmp(prev_sw, ON_ON_OFF,  N_BUTTON) == 0){
    if(memcmp(sw, OFF_ON_OFF, N_BUTTON) == 0){
      ;
    }else if(memcmp(sw, ON_OFF_OFF,  N_BUTTON) == 0){
      if(prev_state == STATE_CHANGED || prev_state == STATE_READY_WP || prev_state == STATE_READY_CH || prev_state == STATE_READY_CR){
        reset_all();
        state = STATE_SINGLE_A;
      }
    }else if(memcmp(sw, ON_ON_ON,    N_BUTTON) == 0){
      if(prev_state == STATE_READY_WP){
        state = STATE_WEAPON;
      }else if(prev_state == STATE_READY_CH || prev_state == STATE_CHANGED){
        rider_time_counter++;
        if(rider_time_counter == 2){
          prev_state = STATE_READY_CH; // 擬似的に状態変化を起こす
          state = STATE_CHANGED;
          side  = SIDE_RIDER_TIME;
        }
      }else if(prev_state == STATE_READY_CR){
        rider_time_counter++;
        if(rider_time_counter == 2){
          state = STATE_CRITICAL;
          side  = SIDE_RIDER_TIME;
        }
      }
    }else if(memcmp(sw, OFF_OFF_OFF, N_BUTTON) == 0){
      ;
    }else if(memcmp(sw, OFF_ON_ON,   N_BUTTON) == 0){
      ;
    }else if(memcmp(sw, ON_OFF_ON,   N_BUTTON) == 0){
      ;
    }else if(memcmp(sw, OFF_OFF_ON,  N_BUTTON) == 0){
      ;
    }
  }else if(memcmp(prev_sw, ON_OFF_ON,  N_BUTTON) == 0){
    if(memcmp(sw, OFF_OFF_ON, N_BUTTON) == 0){
      ;
    }else if(memcmp(sw, ON_ON_ON,    N_BUTTON) == 0){
      if(prev_state == STATE_SINGLE_A || prev_state == STATE_SINGLE_B){
        state = STATE_READY_CH;
      }else if(prev_state == STATE_READY_CH || prev_state == STATE_CHANGED){
        armor_time_counter++;
        if(armor_time_counter == 2){
          prev_state = STATE_READY_CH; // 擬似的に状態変化を起こす
          state = STATE_CHANGED;
          side  = SIDE_ARMOR_TIME;
        }
      }else if(prev_state == STATE_READY_CR){
        armor_time_counter++;
        if(armor_time_counter == 2){
          state = STATE_CRITICAL;
          side  = SIDE_ARMOR_TIME;
        }
      }
    }else if(memcmp(sw, ON_OFF_OFF,  N_BUTTON) == 0){
      if(prev_state == STATE_WEAPON || prev_state == STATE_CHANGED || prev_state == STATE_READY_CH || prev_state == STATE_READY_CR){
        reset_all();
        state = STATE_SINGLE_A;
      }
    }else if(memcmp(sw, OFF_ON_ON,   N_BUTTON) == 0){
      ;
    }else if(memcmp(sw, OFF_OFF_OFF, N_BUTTON) == 0){
      ;
    }else if(memcmp(sw, ON_ON_OFF,   N_BUTTON) == 0){
      ;
    }else if(memcmp(sw, OFF_ON_OFF,  N_BUTTON) == 0){
      ;
    }
  }else if(memcmp(prev_sw, OFF_ON_ON,  N_BUTTON) == 0){
    if(memcmp(sw, ON_ON_ON, N_BUTTON) == 0){
      if(prev_state == STATE_CHANGED){
        reset_all();
        state = STATE_READY_CR;
      }else if(prev_state == STATE_READY_CR || prev_state == STATE_CRITICAL){
        reset_all();
        prev_state = STATE_CHANGED; // 擬似的に状態変化を起こす
        state = STATE_READY_CR;
      }
    }else if(memcmp(sw, OFF_OFF_ON,  N_BUTTON) == 0){
      if(prev_state == STATE_CHANGED){
        side = SIDE_NONE;
        if(armor_time_counter == 2){
          reset_armor_time_counter();
        }
      }else if(prev_state == STATE_CRITICAL){
        state = STATE_CHANGED;
        side  = SIDE_NONE;
        if(armor_time_counter == 2){
          reset_armor_time_counter();
        }
      }
    }else if(memcmp(sw, OFF_ON_OFF,  N_BUTTON) == 0){
      if(prev_state == STATE_WEAPON){
        state = STATE_READY_WP;
      }else if(prev_state == STATE_CHANGED){
        side = SIDE_NONE;
        if(rider_time_counter == 2){
          reset_rider_time_counter();
        }
      }else if(prev_state == STATE_CRITICAL){
        state = STATE_CHANGED;
        side  = SIDE_NONE;
        if(rider_time_counter == 2){
          reset_rider_time_counter();
        }
      }
    }else if(memcmp(sw, ON_OFF_ON,   N_BUTTON) == 0){
      ;
    }else if(memcmp(sw, ON_ON_OFF,   N_BUTTON) == 0){
      ;
    }else if(memcmp(sw, OFF_OFF_OFF, N_BUTTON) == 0){
      if(prev_state == STATE_WEAPON || prev_state == STATE_CHANGED || prev_state == STATE_READY_CH || prev_state == STATE_READY_CR || prev_state == STATE_CRITICAL){
        reset_all();
        state = STATE_SINGLE_A;
      }
    }else if(memcmp(sw, ON_OFF_OFF,  N_BUTTON) == 0){
      ;
    }
  }else if(memcmp(prev_sw, ON_ON_ON,   N_BUTTON) == 0){
    if(memcmp(sw, OFF_ON_ON, N_BUTTON) == 0){
      ;
    }else if(memcmp(sw, ON_OFF_ON,   N_BUTTON) == 0){
      if(prev_state == STATE_CHANGED){
        side = SIDE_NONE;
        if(armor_time_counter == 2){
          reset_armor_time_counter();
        }
      }else if(prev_state == STATE_CRITICAL){
        state = STATE_CHANGED;
        side  = SIDE_NONE;
        if(armor_time_counter == 2){
          reset_armor_time_counter();
        }
      }
    }else if(memcmp(sw, ON_ON_OFF,   N_BUTTON) == 0){
      if(prev_state == STATE_WEAPON){
        state = STATE_READY_WP;
      }else if(prev_state == STATE_CHANGED){
        side = SIDE_NONE;
        if(rider_time_counter == 2){
          reset_rider_time_counter();
        }
      }else if(prev_state == STATE_CRITICAL){
        state = STATE_CHANGED;
        side  = SIDE_NONE;
        if(rider_time_counter == 2){
          reset_rider_time_counter();
        }
      }
    }else if(memcmp(sw, OFF_OFF_ON,  N_BUTTON) == 0){
      ;
    }else if(memcmp(sw, OFF_ON_OFF,  N_BUTTON) == 0){
      ;
    }else if(memcmp(sw, ON_OFF_OFF,  N_BUTTON) == 0){
      if(prev_state == STATE_WEAPON || prev_state == STATE_CHANGED || prev_state == STATE_READY_CH || prev_state == STATE_READY_CR || prev_state == STATE_CRITICAL){
        reset_all();
        state = STATE_SINGLE_A;
      }
    }else if(memcmp(sw, OFF_OFF_OFF, N_BUTTON) == 0){
      ;
    }
  }

  /*
  Serial.print("State: ");
  Serial.println(state);
  Serial.print("rider time counter: ");
  Serial.println(rider_time_counter);
  Serial.print("armor time counter: ");
  Serial.println(armor_time_counter);
  Serial.println("");
  */

  play_sound();
  flash_led();

  prev_sw[0] = sw[0];
  prev_sw[1] = sw[1];
  prev_sw[2] = sw[2];

  delay(LOOP_INTERVAL_MS);
}

⓪ メインの変数定義(1〜46行目)

Arduinoのピン定義、loop()の周期、状態管理管理のための変数などをここで定義しています。

Arduinoのピン定義で一つ注意するところとしては、このプログラムは通常の単色LEDを接続することを前提にしていて、LEDの明るさをプログラムで変更できるようにしています。これはanalogWrite()関数を使って実現しているのですが、analogWrite()関数を使うためには、LEDの接続先のピンがPWMに対応している必要があります。PWMを使えるピンはArduinoごとに違うので、事前にどのピンが使えるかを確認しておきましょう。Arduino Pro miniの場合は5番ピンが使えます。

次に、loop()の周期をLOOP_INTERVAL_MSで指定していますが、ここでは20msにしています。これを100msとかにしてしまうと、ちょっと長過ぎてうまくいかないかもしれません。というのも、このLOOP_INTERVAL_MSの周期でスイッチの状態変化をチェックしているのですが、先で述べたとおり、ライドウォッチはスイッチの2度押しで状態遷移が発生することがあります。ジクウドライバーを勢いよく回転させてしまうと、100ms以内にスイッチの2度押しが発生してしまい、2度押されたものが1度押しとして認識されてしまう可能性があります。かといって、あまり周期を短くし過ぎてしまうと、スイッチの同時押しの認識が難しくなってしまいますので、どの程度の周期に設定すべきかは、多少トライ&エラーが必要かもしれません。

続いて、状態管理のための変数ですが大きく4種類の変数を使います。

  • 以前と今の状態を管理するためのprev_stateとstate
  • 「ライダータイムとアーマータイムのどちらの経路で状態遷移が発生したか」を管理するためのprev_sideとside
  • ライダータイムとアーマータイムのためのスイッチを押された回数を管理するrider_time_counterとarmor_time_counter
  • 3つのスイッチを押されている状態を管理するためのprev_swとsw

ちょっとややこしいですが、これらを②の状態遷移管理・リセット処理の中で変更しながらライドウォッチの状態を管理していきます。

① 起動処理(386〜413行目)

ここはそんなに大したことはしていません。主にArduinoのピンのセットアップと、MP3プレイヤーの初期化処理、音量設定を行なっているだけです。

②状態遷移管理・リセット処理(415〜718行目、363〜384行目)

冒頭の方で説明した状態遷移をプログラムとして書き起こしているのが、この部分になります。興味のある方は、先で紹介した状態遷移のPDFファイルの詳細と、この部分のプログラムの内容を照らし合わせていただくと良いかと思います。

ざっくりとこの部分の説明をしますと、プログラムのこの部分を通過することで、「⓪ メインの変数定義」で紹介したprev_state, state, prev_side, side, rider_time_counter, armor_time_counterの値が変化します。続く「③効果音再生処理呼出」と「④発光処理呼出」は、ここでこれらの変数がどのように変化したのかをチェックすることで、どの音声を再生/停止させるか、どのように発光させるかを判断します。

リセット処理は状態遷移管理部分の要所要所で呼び出していて、その名の通り、状態管理用の変数をまとめて初期化するための関数です。この後で説明する効果音再生処理、発光処理で使用する変数もここで併せて初期化しています。

③効果音再生処理(48〜156行目、729行目)

loop()の729行目で呼び出されているplay_sound()関数の内容が48〜156行目で定義されています。大部分は「prev_stateの状態からstateの状態に変わったら、何番の音を再生する」という内容になっていることがわかると思います。先の状態遷移図と照らし合わせてみるとなおわかりやすいと思います。なお、DFPlayer miniの使い方ですが、このプログラムではSDカードのルートディレクトリに””mp3″という名のフォルダを作り、その中に”0001_single_A.mp3″, “0002_single_B.mp3”, … のような形でファイルを保存することを前提にしています。

ちょっとややこしいのは、ライダータイム用の音声、アーマータイム用の音声、必殺技音声の再生です。状態遷移が発生したルート(←side変数で管理)によって再生する音声が変わることもややこしくなる要因の一つなのですが、それより問題なのは、ジクウドライバーをお持ちの方はよくご存知だと思いますが、ライダータイムの場合は「ベルト回転完了音」→「ライダータイム音」の順、アーマータイムの場合は「ベルト回転完了音」→「ライダータイム音」→「アーマータイム音」の順で音声が再生されます。必殺技(タイムブレイク、タイムバースト)のときも同様です。つまり、「状態が遷移したからといって、すぐに対応する音声が再生されるわけではない」ということになります。解決方法としては、無音部分を含めてmp3ファイルを作成してしまうという方法もありますが、ここでは無音時間の長さをms単位で調整できるように、ARMOR_TIME_WAIT / RIDER_TIME_WAIT / CRITICAL_WAIT_SHORT / CRITICAL_WAIT_LONGという定数と、sound_wait_start_timeという変数を使用しています。ただ、sound_wait_start_timeという変数だけだと、どの効果音の再生待ちをしているのかがわからなくなるので、is_armor_time_waiting / is_rider_time_waiting / is_armor_time_critical_waiting / is_rider_time_critical_waitingというフラグ変数も組み合わせて、再生すべき音声ファイルを特定できるようにしています。

④発光処理(158〜361行目、730行目)

loop()の730行目で呼び出されているflash_led()関数の内容が158〜361行目で定義されています。ここは、ちょっとややこしいです。

まず、play_sound()関数のときと同様、flash_led()関数で、prev_state, state, sideの状態を見て、どの発光パターンでLEDを光らせれば良いかを選択しています。

状態ごとに選択される各発光パターンを定義しているのが、239〜300行目になります。led_pattern_xxx(uint16_t led_counter_ms)という関数が並んでいますが、中身は全て同じような構成になっていて、「何msから何msまでは、この基本発光パターンで光らせる」というのを並べて、その組み合わせで、それぞれの状態のときの固有の発光パターンを定義できるようになっています。

その「基本発光パターン」を定義しているのが、163〜238行目で、led_base_pattern_xxx()という名の関数が6つ並んでいます。

  • led_base_pattern_on … 光る
  • led_base_pattern_off … 消える
  • led_base_pattern_blink … 点滅する(何msごとに、を指定)
  • led_base_pattern_inc … 徐々に光る(何msかけて、何段階で、を指定)
  • led_base_pattern_dim … 徐々に消える(何msかけて、何段階で、を指定)
  • led_base_pattern_blink_slowly … 徐々に点いて徐々に消える(何msごとに、何段階で、を指定)

この6パターンを組み合わせれば、大体の発光パターンは作れるかなと思っています。ガシャットのときは100msごとの色・明るさ定義を地道に記述していたのですが、今回はこんな感じで発光パターンを言うなればプログラムできるようにしたので、発光タイミングの微調整がだいぶ簡単になりました。使用するArduinoのメモリの量も大きく削減されており、動作も安定しやすくなります。

 

以上が今回のオリジナルライドウォッチのプログラムの全容になります。大変おつかれさまでした。プログラムがかけましたら、実際にブレッドボード上に回路を組んで、プログラムが意図通りに動作するかを確認しましょう。必要な具材については、前回の準備編をご参照ください。

今回組む回路はこんな感じになります。

実際にブレッドボード上に組むとこんな感じで、結構シンプルになります。

 

 

無事に動作したでしょうか。「なんか発光と音声がずれるなあ」と感じる方は、発光パターン定義のmsのところをいろいろいじってみてください。

 

最後に、冒頭で述べた「Rスロット側(=通常、ジオウとゲイツのライドウォッチがセットされる側)のライドウォッチを作りたい場合どうするのか」について、簡単に説明しておきます。

ソースコードをじっくり読まれた方はお気づきかと思いますが、これまで説明してきた「Lスロット側のライドウォッチ」では、SW-1のON/OFFによってアーマータイム、SW-2のON/OFFによってライダータイムがそれぞれ発動するようになっています。これを入れ替えて、SW-1のON/OFFによってライダータイム、SW-2のON/OFFによってアーマータイムがそれぞれ発動するように変更すれば、それで「Rスロット側のライドウォッチ」として動作するようになります。言葉にするとこれだけですが、実際にソースを修正するときは途中でこんがらがってしまわないようご注意ください。

 

以上、オリジナルライドウォッチ作成のためのソフト(プログラム)の説明でした。冒頭に述べましたとおり、今回ご紹介したプログラムは、「シンプルにDX版相当のライドウォッチの動作を再現するためのプログラム」になります。私が作った万丈ライドウォッチのプログラムは、上記のプログラムに、さらに、フルカラーLEDやフォームチェンジのためのリードスイッチの認識処理を加えたものになります。ただ、今回のプログラムを理解できた方からすればそう難しい変更ではないと思われますので、プログラム自体は次回ご紹介しますが、説明は省略致します。とりあえず音声と発光を差し替えたオリジナルライドウォッチをつくりたい、あるいは自分だけの新機能を追加したライドウォッチを作りたいという方は、今回ご紹介したプログラムをベースに、色々カスタマイズを加えていってもらえればと思います。

次回でいよいよ最終回、実際に遊べる形にするためのハード改造編になります。最後までお付き合い頂けますと幸いです。