【自動変形】ライジングヴァレンバスターをつくる

あけましておめでとうございます。本年もよろしくお願いいたします。今回はヴァレンバスターを自動変形させたかっただけの改造です。

開発経緯

 

レバーのダイナミックな開閉アクションが売りの『DX変身銃ヴァレンバスター』。玩具の都合上、上部のハンドルでレバーを持ち上げる前に下部のロックを外すワンアクションが必要であり、SNSを見ていた感じ、劇中再現のためにこの部分に手を加えている人が結構いる印象でした。

でも個人的には、もう少し手を入れて「自動開閉になったらカッコ良いのでは?」と思っていました。自動変形の変身アイテムといえばCSMのサンドライバーとムーンドライバーが有名ですが、これらは残念ながら(財布の事情で)手が出せず。ただ、欲しいものではあったので、今回のヴァレンバスターを自動変形に改造してしまえば、欲しかった自動変形アイテムも手に入るし一石二鳥では、と思い、チャレンジしてみることにしました。

とはいえ、今回はこれまでのような中身総入れ替えみたいな形ではなく、極力、元のヴァレンバスターに手を加えない形で実現する方法を考えることにしました。理由は単純で、難しそうだから。

見た目に拘るなら、モーターやマイコン、電池を中に仕込んでいくことになるのですが、スペース的に、モーターを仕込むとなると、おそらくギアから考えて作っていかないといけなくなりそうです。残念ながら私にそのスキルはありません。ということで、この方針は早々に諦めて、多少不細工になっても自動開閉さえできればOK、ということにして、外付けタイプで自動化する方向で考えることにしました。これなら、いずれ元のヴァレンバスターに戻したくなったときにも、すぐに戻すことができます。

…という方向性でスタートしてみましたが、結局は細かいところで色々ヴァレンバスター自体に手を加えることにはなりました。無念。

 

特徴

自動開閉ということでハンドル操作が不要になったので、代わりに金色の装甲パーツを追加。

他の場所にもチラホラ。この金の装甲は、『クウガ』のライジングフォームでの武器強化のオマージュです。なので、名前も『ライジングヴァレンバスター』です。

今回のキモ、自動開閉を可能にするためのパワーユニットです。塗装はミスりましたがそこは深追いしない。

発光部分を展開してヴァレンバスターの背面にセット。

磁石でくっつきます。

ヴァレンバスターで元々はベルトに固定するパーツがあったところは、今回のパワーユニットを取り付けるための磁石入りパーツに換装しています。

 

ユニットの電源を入れてスイッチを押すと、レバーが自動で閉じます。レバーが動いている間は発光部が赤く発光し、閉じ終わるとしばらく緑に発光します。

レバーが閉じた状態で再度スイッチを押すと、レバーが自動で開きます。発光については同様です。

また、開閉時に、本来はハンドルがあった部分の金の装甲内も緑に発光します。この部分はパワーユニットとは独立した仕組みで発光しますが、詳細は後ほど。

 

以上。内容としてはシンプルですが、うまくレバーを動かすために色々試行錯誤したり、3Dプリントする部品の点数がだいぶ多くなってしまった結果、思った以上に時間がかかってしまいました。

 

ハードウェア解説

まずは具材から。何はともあれヴァレンバスター。

続いて、レバーの開閉に使用するモーター。モーター単体だとパワー(トルク)が足りないので、低速ギヤボックスを使用。

上下レバーは内部で連動するので、強いモーターなら1個でもいけるのかもしれませんが、今回使用したものでは厳しく、今回は上下レバーに個別に用意しました。計2個。

モータードライバ。これ一つで2個のモーターを制御できます。先のギヤボックスで使用されているのはミニモーターなので、供給電流の性能もこれで充分です。

メインのマイコン。無線通信とかは不要なので、扱いやすいSeeeduino XIAOを使用。

レバーの開閉を検知するためのスイッチ×4個。プログラムによってはもう少しシンプルな構成にもできるかもしれませんが、確実性重視でこうしました。

NeoPixel。自動開閉だけだと見た目の変化がちょっと乏しいかなと思い、ムーンドライバーとかを念頭に、発光をつけてより豪華にすることにしました。おかげでだいぶ手間が増えましたが。

単四電池3本用ボックス。安全性の観点で、リチウムイオン充電池ではなく普通の電池を使用しています。

ネオジム磁石。今回はパワーユニットの固定だったりレバーの固定だったり、いろんなところで使用しています。手持ちのものも使用しましたが、今回はこちらを買い足しました。

プラ棒。レバーとパワーユニットの接点に使用。ひょっとしたら3Dプリンタの出力部品でいけたかもしれませんが、強度面が不安だったので既製品を使用しました。

振動LED。電池交換できない使い切りですが、これがあればマイコンなしでお手軽に発光させられます。レバーの先端部にこれを仕込むことで、配線不要で開閉時に発光させることができます。同じ寸法で生活防水タイプもありますが、発光の強い5気圧防水タイプを使用。

あとは3Dプリンタ&フィラメント。今回、パワーユニットが完全自作のためトライ&エラーしまくったので大量に消費しました…

それから、電源用のスライドスイッチと、開閉操作指示用のタクトスイッチ。これは、手持ちのものを使用しました。リード線とかも。具材に関しては以上になります。

 

今回の改造は、

  1. パワーユニットの作成
  2. ヴァレンバスター本体の改造

の大きく2つに分かれます。まずは前者から。

 

考え方としては、ハンドルの代わりに、

この部分でレバーとパワーユニットを接続して、外からモーターの力でレバーを開閉させてしまおう、ということです。

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

3Dプリントした筐体に部品を組み込んでみた様子がこちら。

なかなかの密度になってしまいました。

モーターのギヤ比については、パワーと開閉速度のバランスを考えた結果、149.9:1にしています。

上下独立してレバーの開閉状態を把握できるように、スイッチは4箇所に配置。割り切ればもうちょいシンプルにはできるかもしれません。

発光部分を折りたためるようにしたのは、妄想ですが、もし実際にヴァレンバスターの強化アイテムを作るとしたら、パワーユニットをヴァレンバスターに取り付けると金色の装甲部分が現れる…みたいなイメージにしたくて、パワーユニットを単独で成立する別アイテムとして持ち歩きやすい形にしたかったからです。持ち歩きのイメージ的には、

こんな感じですかね。接続部は見なかったことにしてください。

今回とにかく時間がかかったのは、パワーユニットのディテールです。

デザインセンスがないので、とにかくのっぺらぼうになるのを避けるためだけに色々付け足しました。

結果として、3Dプリント後の表面処理が大変だったり、その後の塗装もだいぶイマイチな感じにはなってしまったのですが、そこは本筋ではないのでそこそこで良しとしました。

 

 

続いてヴァレンバスター側の改造です。極力手を入れないようにしたい、と言いながら、いざパワーユニットを試作して取り付けてみると、これがなかなかいい感じにレバーが開閉せず下側がうまくロックするように頑張った結果、今度はレバーが閉じたときに先端がぶらぶらするようになったり。

中を開けてギアの位置を変えてみたり色々試したのですが、最終的にはデフォルトの状態が一番マシ、と言う結論になって、元に戻しました。

その試行錯誤の過程の中で、先端のチョコレートのロック部分をちょっと削ってロックが甘くなってしまったので、

中にネオジム磁石を仕込んだこういう部品をプリントしまして、

ここに、

こう。

逆側も同様にして、磁石でロックがかかるようにしました。ちなみに、ネオジム磁石が近づき過ぎるとモーターの力でも剥がせなくなってしまうため、ロックはかかりつつ、モーターの力でロックが解除される、という位置に磁石を仕込むのに結構試行錯誤しました。

続いて、ハンドルがあった部分。ここをなるべく簡単に発光させるために、「衝撃で光るLED」を仕込みます。

 

こういうやつ。

ハンドルと置き換える形で、3Dプリントした部品をセットして、

こんな感じでセット。あんまり高さを出したくなかったので、発光はやや弱まりますが上下逆さにセットするようにしました。

発光部を少し隠蔽するためにポリプロピレンの板を切ったものを貼り付けて、

蓋をして完成。

蓋が飛んでいかないように、金色のネジでねじ止めしています。

 

最後に、パワーユニットを取り付ける部分。ここは簡単で、

3Dプリントした部品にネオジム磁石を仕込んで、

ベルトにセットするための部品と交換するかたちでセット。

パワーユニット側にも底にネオジム磁石を仕込んであるので、これでしっかり固定されます。

 

ソフトウェア解説

プログラムについては、そんなに特筆すべきところはないかなあと思います。ボタンを押せばモーターが回る。スイッチが押されればモーターを止める。基本はそれだけです。それに発光の制御を加えているのみ。

メチャメチャ長いわけではないですが、ここに掲載するにはちょっと長いので、最後に全文掲載しておきます。もう少し短く整理して書くこともできた気がしますが、そこはあんまり頑張らなくても良いかな、と思いましたので、そのままにしています。

 

まとめ

以上、自動変形・ライジングヴァレンバスターのご紹介でした。外付け型にして簡単に済ませるはずが、思いのほか時間がかかってしまいました。内容はシンプルですが、遊んでいる分には楽しいので満足しています。

『ガヴ』関連の改造アイデアは現状、これでひと段落しましたので、次はまた別のを題材に何か作ろうと思います。

 

ソースコード

全文は以下になります。

////////// 基本定義 ////////////////////////////////////////////////////////////

#define LOOP_DELAY_MS 20

#define PIN_LED_UP          0
#define PIN_LED_DOWN        1
#define PIN_SW_UP_OPEND     2
#define PIN_SW_UP_CLOSED    3
#define PIN_SW_DOWN_OPEND   4
#define PIN_SW_DOWN_CLOSED  5
#define PIN_SW_MAIN         6
#define PIN_MOTOR_UP_1      7
#define PIN_MOTOR_UP_2      8
#define PIN_MOTOR_DOWN_1    9
#define PIN_MOTOR_DOWN_2   10

#define ON  LOW
#define OFF HIGH

uint8_t sw_up_open    = OFF;
uint8_t sw_up_close   = OFF;
uint8_t sw_down_open  = OFF;
uint8_t sw_down_close = OFF;
uint8_t sw_main       = OFF;

uint8_t prev_sw_up_open    = OFF;
uint8_t prev_sw_up_close   = OFF;
uint8_t prev_sw_down_open  = OFF;
uint8_t prev_sw_down_close = OFF;
uint8_t prev_sw_main       = OFF;

#define STATE_OPEND   0
#define STATE_CLOSING 1
#define STATE_CLOSED  2
#define STATE_OPENING 3
uint8_t state_up        = STATE_OPEND;
uint8_t state_down      = STATE_OPEND;
uint8_t prev_state_up   = STATE_OPEND;
uint8_t prev_state_down = STATE_OPEND;

void change_state_up(uint8_t new_state){
  state_up = new_state;
  Serial.print(F("UP: "));
  switch(state_up){
  case STATE_OPEND:   Serial.println(F("STATE_OPEND"));   break;
  case STATE_CLOSING: Serial.println(F("STATE_CLOSING")); break;
  case STATE_CLOSED:  Serial.println(F("STATE_CLOSED"));  break;
  case STATE_OPENING: Serial.println(F("STATE_OPENING")); break;
  default: ;
  }
}

void change_state_down(uint8_t new_state){
  state_down = new_state;
  Serial.print(F("DOWN: "));
  switch(state_down){
  case STATE_OPEND:   Serial.println(F("STATE_OPEND"));   break;
  case STATE_CLOSING: Serial.println(F("STATE_CLOSING")); break;
  case STATE_CLOSED:  Serial.println(F("STATE_CLOSED"));  break;
  case STATE_OPENING: Serial.println(F("STATE_OPENING")); break;
  default: ;
  }
}

 ////////////////////////////////////////////////////////////
#include <Adafruit_NeoPixel.h>
#define N_LED 6

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

struct color_rgb COLOR_RED   = {255, 0, 0};
struct color_rgb COLOR_GREEN = {0, 255, 0};

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

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;
unsigned long inc_dim_start_point_ms = 0;

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

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

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_up.setPixelColor(i, pixels_up.Color(r_step*current_step, g_step*current_step, b_step*current_step));
    pixels_down.setPixelColor(i, pixels_down.Color(r_step*current_step, g_step*current_step, b_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_up.setPixelColor(i, pixels_up.Color(r_step*(steps-current_step), g_step*(steps-current_step), b_step*(steps-current_step)));
    pixels_down.setPixelColor(i, pixels_down.Color(r_step*(steps-current_step), g_step*(steps-current_step), b_step*(steps-current_step)));
  }
}

void control_led(unsigned long now_ms){
  if(prev_state_up != state_up){
    led_pattern_start_point_ms = now_ms;
    inc_dim_start_point_ms= 0;
    led_base_pattern_off();
  }

  unsigned long passed_ms = now_ms - led_pattern_start_point_ms;

  switch(state_up){
  case STATE_OPEND:
    if(passed_ms < 2000){
      led_base_pattern_on(&COLOR_GREEN);
    }else if(2000 <= passed_ms && passed_ms <= 3000){
      led_base_pattern_dim(&COLOR_GREEN, now_ms, 1000, 20);
    }else if(3000 <= passed_ms){
      led_base_pattern_off();
    }
    break;
  case STATE_CLOSING:
    led_base_pattern_on(&COLOR_RED);
    break;
  case STATE_CLOSED:
    if(passed_ms < 500){
      led_base_pattern_on(&COLOR_RED);
    }else if(500 <= passed_ms && passed_ms < 8000){
      led_base_pattern_on(&COLOR_GREEN);
    }else if(8000 <= passed_ms && passed_ms <= 10000){
      led_base_pattern_dim(&COLOR_GREEN, now_ms, 2000, 20);
    }else if(10000 <= passed_ms){
      led_base_pattern_off();
    }
    break;
  case STATE_OPENING:
    led_base_pattern_on(&COLOR_RED);
    break;
  default:
    ;
  }

  pixels_up.show();
  pixels_down.show();
}

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

#define MOTOR_UP_SPEED   255
#define MOTOR_DOWN_SPEED 255

#define MOTOR_UP_DELAY_MS   1500
#define MOTOR_DOWN_DELAY_MS 1500

unsigned long motor_up_wait_start_point_ms   = 0;
unsigned long motor_down_wait_start_point_ms = 0;
bool is_motor_up_ready   = false;
bool is_motor_down_ready = false;

void control_motor(unsigned long now_ms){
  // 閉じ始める
  if(prev_state_up == STATE_OPEND && state_up == STATE_CLOSING){
    // 状態が変化してからXXXms後にモーターを稼働させる
    is_motor_up_ready = true;
    motor_up_wait_start_point_ms = now_ms;
  }

  if(prev_state_down == STATE_OPEND && state_down == STATE_CLOSING){
    // 状態が変化してからXXXms後にモーターを稼働させる
    is_motor_down_ready = true;
    motor_down_wait_start_point_ms = now_ms;
  }

  // 閉じ終わる
  if(prev_state_up == STATE_CLOSING && state_up == STATE_CLOSED){
    analogWrite(PIN_MOTOR_UP_1, 0);
    analogWrite(PIN_MOTOR_UP_2, 0);
  }

  if(prev_state_down == STATE_CLOSING && state_down == STATE_CLOSED){
    analogWrite(PIN_MOTOR_DOWN_1, 0);
    analogWrite(PIN_MOTOR_DOWN_2, 0);
  }

  // 開き始める
  if(prev_state_up == STATE_CLOSED && state_up == STATE_OPENING){
    // 状態が変化してからXXXms後にモーターを稼働させる
    is_motor_up_ready = true;
    motor_up_wait_start_point_ms = now_ms;
  }

  if(prev_state_down == STATE_CLOSED && state_down == STATE_OPENING){
    // 状態が変化してからXXXms後にモーターを稼働させる
    is_motor_down_ready = true;
    motor_down_wait_start_point_ms = now_ms;
  }

  // 開き終わる
  if(prev_state_up == STATE_OPENING && state_up == STATE_OPEND){
    analogWrite(PIN_MOTOR_UP_1, 0);
    analogWrite(PIN_MOTOR_UP_2, 0);
  }

  if(prev_state_down == STATE_OPENING && state_down == STATE_OPEND){
    analogWrite(PIN_MOTOR_DOWN_1, 0);
    analogWrite(PIN_MOTOR_DOWN_2, 0);
  }

  ///// 時間経過処理 /////

  // 閉じる
  if(state_up == STATE_CLOSING && is_motor_up_ready && now_ms - motor_up_wait_start_point_ms >= MOTOR_UP_DELAY_MS){
    analogWrite(PIN_MOTOR_UP_1, MOTOR_UP_SPEED);
    analogWrite(PIN_MOTOR_UP_2, 0);
    is_motor_up_ready = false;
  }
  if(state_down == STATE_CLOSING && is_motor_down_ready && now_ms - motor_down_wait_start_point_ms >= MOTOR_DOWN_DELAY_MS){
    analogWrite(PIN_MOTOR_DOWN_1, MOTOR_DOWN_SPEED);
    analogWrite(PIN_MOTOR_DOWN_2, 0);
    is_motor_down_ready = false;
  }

  // 開く
  if(state_up == STATE_OPENING && is_motor_up_ready && now_ms - motor_up_wait_start_point_ms >= MOTOR_UP_DELAY_MS){
    analogWrite(PIN_MOTOR_UP_1, 0);
    analogWrite(PIN_MOTOR_UP_2, MOTOR_UP_SPEED);
    is_motor_up_ready = false;
  }
  if(state_down == STATE_OPENING && is_motor_down_ready && now_ms - motor_down_wait_start_point_ms >= MOTOR_DOWN_DELAY_MS){
    analogWrite(PIN_MOTOR_DOWN_1, 0);
    analogWrite(PIN_MOTOR_DOWN_2, MOTOR_DOWN_SPEED);
    is_motor_down_ready = false;
  }

}

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

void setup(){
  Serial.begin(115200);
  pinMode(PIN_SW_UP_OPEND, INPUT_PULLUP);
  pinMode(PIN_SW_UP_CLOSED, INPUT_PULLUP);
  pinMode(PIN_SW_DOWN_OPEND, INPUT_PULLUP);
  pinMode(PIN_SW_DOWN_CLOSED, INPUT_PULLUP);
  pinMode(PIN_SW_MAIN, INPUT_PULLUP);
  pinMode(PIN_MOTOR_UP_1, OUTPUT);
  pinMode(PIN_MOTOR_UP_2, OUTPUT);
  pinMode(PIN_MOTOR_DOWN_1, OUTPUT);
  pinMode(PIN_MOTOR_DOWN_2, OUTPUT);

  delay(500);

  pixels_up.begin();
  pixels_up.clear();
  pixels_down.begin();
  pixels_down.clear();

  led_base_pattern_off();
  pixels_up.show();
  pixels_down.show();

  if(digitalRead(PIN_SW_UP_CLOSED) == ON){
    change_state_up(STATE_CLOSED);
  }

  if(digitalRead(PIN_SW_DOWN_CLOSED) == ON){
    change_state_down(STATE_CLOSED);
  }
}

void loop(){
  sw_up_open    = digitalRead(PIN_SW_UP_OPEND);
  sw_up_close   = digitalRead(PIN_SW_UP_CLOSED);
  sw_down_open  = digitalRead(PIN_SW_DOWN_OPEND);
  sw_down_close = digitalRead(PIN_SW_DOWN_CLOSED);
  sw_main       = digitalRead(PIN_SW_MAIN);

  unsigned long now_ms = millis();

  if(prev_sw_main == OFF && sw_main == ON){
    // 閉じ始める
    if(state_up == STATE_OPEND){
      change_state_up(STATE_CLOSING);
    }
    if(state_down == STATE_OPEND){
      change_state_down(STATE_CLOSING);
    }
    // 開き始める
    if(state_up == STATE_CLOSED){
      change_state_up(STATE_OPENING);
    }
    if(state_down == STATE_CLOSED){
      change_state_down(STATE_OPENING);
    }
  }

  // 閉じる
  if(prev_sw_up_close == OFF && sw_up_close == ON){
    change_state_up(STATE_CLOSED);
  }

  if(prev_sw_down_close == OFF && sw_down_close == ON){
    change_state_down(STATE_CLOSED);
  }

  // 開く
  if(prev_sw_up_open == OFF && sw_up_open == ON){
    change_state_up(STATE_OPEND);
  }

  if(prev_sw_down_open == OFF && sw_down_open == ON){
    change_state_down(STATE_OPEND);
  }

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

  control_motor(now_ms);

  control_led(now_ms);

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

  prev_sw_up_open    = sw_up_open;
  prev_sw_up_close   = sw_up_close;
  prev_sw_down_open  = sw_down_open;
  prev_sw_down_close = sw_down_close;
  prev_sw_main       = sw_main;

  prev_state_up   = state_up;
  prev_state_down = state_down;

  delay(LOOP_DELAY_MS);
}