見出し画像

Switch自動化~マクロモード切替、変数調整~

概要

こんにちは。じょんです。
Arduino LeonardoでのSwitch自動化にて、
マクロを変える度にプログラムを書き換えるのが面倒になりました。
そこでLeonardoに外付け回路を追加し、実行するマクロの変更や、サイクル変更を出来るようにしました。
※本記事は既にArduino LeonardoでのSwitch自動化を理解している方を対象としています。
※本記事は電気回路初心者にもわかりやすいように意識して記載しています。冗長な部分も多いかと思いますので、わかる方は適宜飛ばしてください。

この記事で実現すること

最初に結論、この記事を読むと以下の動画のようなことができます。
・DIPスイッチでマクロモードの切替
・可変抵抗で孵化サイクルを変更
・左下タクトスイッチでマクロ実行
・右上タクトスイッチでソフトウェアリセット実行

事前準備

必要なもの

・半田ごて、はんだ
・リード線(何でもいいけどAWG30程度の細さが使いやすいと思います)
・ニッパー(ハサミでも頑張ればいける???)
・部品表に記載の部品(はんだ付け失敗に備えて1,2個多めに購入)

あると便利なもの

・ピンセット(はんだ付けで使用)
・マスキングテープ
・テスター(はんだ付けでの導通チェックや、電圧確認用。配線ミスって動かないときは、これがないと厳しいです。逆に言えばミスらなければ不要。Amazonで1000円~3000円程度のもので十分。)

回路図・部品表

では早速、回路図と部品表を下記に貼ります。PDFも置いておきます。

回路図
部品表

電気回路がわからない方へ解説!

①孵化サイクル調整用・可変抵抗

XA1左側のA0~A5はLeonardoのアナログ入力ピンです。
固定抵抗R1と可変抵抗RV1の分圧をA0で読み取ります。
可変抵抗を調整し、電圧値を変化させることで、変数を調節します。

例えば可変抵抗をMAX(10kΩ)にしておくと、(10k/(10k+3k))×5[V]=3.84[V]、
MIN(0Ω)にすると(0/(0+3k))×5[V]=0[V]となります。
(部品にはそれぞれバラつきがあるため、それに近い値になっていればOKです。)

可変抵抗が2個ありますが、もう一つは今回は使ってません。必要に応じて使ってください。

②マクロモード切替用・DIPスイッチ

右側SW3はDIPスイッチです。D8~D11ピンをスケッチで内部プルアップ設定します。SW3の左端スイッチがオフのときはD8ピンの電圧は5V(Hi), スイッチオンするとR5:3kΩでプルダウンされ、Loになります。
※プルアップ、プルダウンがわからない場合はググってください。

DIPスイッチは4chあるため、(0,0,0,0)~(1,1,1,1)の16状態を設定できます。
下に記載するプログラムでは(1,1,1,1)を初期状態とし、15モード設定可能にしています。
※スイッチONが0、OFFが1になります。

DIPスイッチ

※先程の解説で「あれ、3kΩ挟んでるのにLoになるの?」と思った方。正解です。「まあ大体そんなもんか」と思った方は追加解説はスキップしてOKです。

ー追加解説ー
ATmega32U4の内部プルアップ抵抗はデータシートのP383より、「RPU:20~50kΩ」です。ワーストケースのRPU=20kで考えると、
D8電圧 = (3k/(3k+20k))×5[V] = 0.65[V]
そしてATmega32U4が確実にLoと認識する最大電圧VIL(Max)はデータシートより「0.2Vcc-0.1」、Vcc=5Vなので、VIL(Max) = 0.9[V]となります。
D8電圧 < VIL(Max) より、マイコンは確実にLoを認識できます。
(※さらに細かいことを言うと、部品精度(抵抗値バラつき)も考慮する必要がある・・・(趣味工作では気にしなくて良いよ))

まあ実際にはワーストケースには、ほぼならないし、1k~10kΩ位の値を付けとけば、問題なく動くんじゃないかな?

③割込用スイッチ

SW1, SW2は割込用スイッチです。
プログラムが動いているときに手動で割り込みを入れる、ソフトウェアリセットをかけたいなどの場合に活用できます。
今回のプログラムではマクロモード設定後の実行スイッチと、ソフトウェアリセットスイッチとして使用しています。
※正規品Leonardoについているスイッチはハードウェアリセットスイッチです。私も当初ハードウェアリセットを使っていたのですが、リセット後復帰の時間短縮のために、ソフトウェアリセットを使うことにしました。

※注意※ 割込みピンはUNOとLeonardoで異なります。
以下が割込みに使用可能なピン。

Uno: pin2(int.0) pin3(int.1)
Leonardo: pin3(int.0) pin2(int.1) pin0(int.2) pin1(int.3) pin7(int.4)

Arduino 日本語リファレンスより

④LCDディスプレイ用・I2C

回路解説の最後はI2Cです。解説は・・・特にありません。
真面目に考えると色々説明はあるのですが、今回は繋いでプログラムを書けば動いてしまうので、問題ないです。
I2Cは通常プルアップが必要ですが、Arduinoでは内部プルアップ出来るのでそれも不要です。

※注意※ LeonardoのI2C通信は、SCLがD3、SDAがD2と繋がっています。
I2Cを使うとD2, D3はデジタル入出力として使用できませんのでご注意ください。

はんだ付け・配線図

概要

配線は自由です!!好きにやってくれ!!
...と言いたい所ですが、初心者向けということで、方針を書いておきます。

まずはんだ付け初心者の方は、以下2つくらいは読んでおきましょう。
電子工作のコツ/はんだ付け | 村田製作所 技術記事 (murata.com)
小山工業高等専門学校 教育研究技術支援部 技術室 - はんだ付け講座 第3回 (oyama-ct.ac.jp)

次に部品配置と配線イメージ図はこちら。

部品配置図・表面
配線イメージ図・裏面

配線イメージ図は赤:5V、青:GND、ピンク:I2C、緑:その他です。
黄色破線は5Vの表面での配線です。

はんだ付け手順

①L字型ピンソケット6列をニッパーで4列にする。
 勢いあまって4列目も割れてしまうことがあるので、少しずつカットする。

Before
After

②部品位置を決め、全部品をはんだ付けする。
※注意※ タクトスイッチの導通方向
参考URL:タクトスイッチについて説明します。初心者の方にも最適な電子工作キットで電子回路エンジニアを目指しましょう! - いなぎ電子キット (shop-pro.jp)

全部品はんだ付け後・裏面

③GNDと5Vを抵抗の余ったリード線を使って配線する。

GND・5V配線

④その他の配線を回路図を見ながら、間違わないように配線する。

全配線終了

⑤はんだ付けが完了したら、配線を引っ掛けて外れたりしないようにマスキングテープなどで保護してあげると安心です。

マスキングテープで保護

組み立て

あとはLeonardo、シールド、LCDディスプレイを組み合わせるだけです。
①LCDディスプレイにスペーサーを付けます。

LCDディスプレイとスペーサー

②シールドをLeonardoに取り付ける。

シールド取り付け

③LCDディスプレイのI2Cピンヘッダをピンソケットに差し込み完成

完成

ちなみにLCDディスプレイはI2Cモジュール付きのものであれば、20×4行でも大丈夫です。必要に応じて使い分けてください。
さらにちなみに、LCDディスプレイは無くても動作します。
どのマクロをどのモードに設定したか覚えていればLCDディスプレイなしでOKです。

LCD2004

プログラム

最後にプログラムです。
自動化マクロはレフマーナさん(@lefmarna)のプログラムを、ほぼほぼ流用させていただきました。レフマーナさん本当にありがとうございます!!
マクロ①:ポケモンBDSPで自動孵化するコード
マクロ②、③:アルセウスで早業・力業の図鑑タスク埋め、レベル上げを自動化するコード

他にも公開されているLeonardo用の自動化コードを組み込めば書き換えなしで数種類のマクロを登録出来ます!!

また、本プログラムの実行には下記2つのライブラリのインストールが必要です。
 ・<NintendoSwitchControlLibrary.h>
 ・<LiquidCrystal_I2C.h>
<NintendoSwitchControlLibrary.h>は、上記レフマーナさんの記事に従ってインストールしてください。
<LiquidCrystal_I2C.h>は、Arduino IDEのライブラリマネージャで「LiquidCrystal I2C」と検索し、バージョン1.1.2をインストールしてください。

ライブラリマネージャ

以下が私が使用したコードです。動作は保証出来ませんので、思ったように動かない場合や、エラーが出た場合は取り合えず怪しい部分をググってみてください。
※コードの解説はコード内のコメントをご覧ください。

// ライブラリを読み込むためのコード
#include <NintendoSwitchControlLibrary.h>
#include <LiquidCrystal_I2C.h>
#include <avr/wdt.h>

//アドレス0x27 16文字2行のLCD
//I2Cモジュールによってアドレスが異なる場合があるので、LCDの取説を確認すること。
LiquidCrystal_I2C lcd(0x27, 16, 2);   

//モード設定用、DIPスイッチONが0,OFFが1で表示
  int DIP_State[4] = {1,1,1,1};   //初期設定
  int temp_State[4] = {1,1,1,1};  //比較用
  const int Mode1_State[4] = {0,1,1,1};     //マクロモード1
  const int Mode2_State[4] = {1,0,1,1};     //マクロモード2
  const int Mode3_State[4] = {0,0,1,1};     //マクロモード3
  const int Mode4_State[4] = {1,1,0,1};     //マクロモード4
  const int Mode5_State[4] = {0,1,0,1};     //マクロモード5
  const int Mode6_State[4] = {1,0,0,1};     //マクロモード6
  
    //マクロモードを増やす場合はここに追記
  
  int MacroMode = 0; //初期設定
  int start_flag = 0; //スタートボタン用
  int EGG_CYCLE = 5;  //初期設定サイクル
  int temp_CYCLE = 5; //比較用
  int j = 0;  //初回メニューカーソル合わせ用
  
//割込処理ソフトウェアリセット
void software_reset() {
  wdt_disable();
  wdt_enable(WDTO_15MS);
  while (1) {}
} 

//割込処理マクロ開始フラグ
const int PUSH_SHORT = 100;
volatile int count_low = 0;
void S_flag() {
  interrupts();
  //チャタリング対策
  count_low = 0;
  while((digitalRead(0) == LOW) && (count_low < PUSH_SHORT))  {
    count_low ++;
    delayMicroseconds(100);
  }
  if(count_low == PUSH_SHORT){
    start_flag = 1;
  }
} 

//LCDのモード、サイクル表示
void lcd_set(char Mode_name[], int Cycle_num) {
  lcd.setCursor(0, 0);
  lcd.print("Mode: ");
  lcd.print(Mode_name);
  lcd.setCursor(0, 1);
  lcd.print("Cycle: ");
  lcd.print(Cycle_num);
  lcd.print("        ");    //何も表示しない箇所はスペースで埋める
}

//サイクル数読み込み
int Cycle_Read(){
  int x = analogRead(0);
  int y = 5;
    //可変抵抗がリニアにならなかったので変更
  //  for(int i = 1; i < 8; i++ ){        
  //    if(x > 128 * i) y = 5 * (i + 1) ;
  //  }
  //可変抵抗を実際に回し、大体等間隔に調整した。個人で調節してください。
  if(x > 200)  y = 10;
  if(x > 400)  y = 15;
  if(x > 550)  y = 20;
  if(x > 640)  y = 25;
  if(x > 700)  y = 30;
  if(x > 750)  y = 35;
  if(x > 777)  y = 40;    
    return y;
}

// 孵化するまでに自転車で走り回る時間を計算する(試行錯誤の末にこのような形となったが、まだ練りきれていないところがある)
int calcTimeToHatchingSec() {
    // 孵化サイクル35では計算が合わなかったため、個別に指定している
    if (EGG_CYCLE == 35) return 345;
    return (EGG_CYCLE * 10) - 10;
}
int TIME_TO_HATCHING_SEC = calcTimeToHatchingSec();
// 空飛ぶでズイタウンに移動する関数
void moveToInitialPlayerPosition() {
    pushButton(Button::A, 2000);
    pushButton(Button::A, 1000, 2);
    delay(7000);
}

// 初期位置から育て屋さんに移動しタマゴを受け取る関数
void getEggFromBreeder() {
    // 初期位置(ズイタウンのポケモンセンター)から育て屋さんのところまで移動
    tiltLeftStick(Stick::MIN, Stick::NEUTRAL, 735);
    tiltLeftStick(Stick::NEUTRAL, Stick::MIN, 1250);
    tiltLeftStick(Stick::MIN, Stick::NEUTRAL, 735);
    // 育て屋さんから卵をもらう
    pushButton(Button::A, 500);
    pushButton(Button::B, 500, 2);
    pushButton(Button::A, 500, 2);
    pushButton(Button::B, 500, 2);
    pushButton(Button::A, 500);
    pushButton(Button::B, 500, 10);
}

// 初期位置(ズイタウンのポケモンセンター)からタマゴが孵化するまで走り回る関数
void runAround(int run_time_sec) {
    tiltLeftStick(Stick::MIN, Stick::NEUTRAL, 560);
    pushButton(Button::PLUS, 600);
    tiltLeftStick(Stick::NEUTRAL, Stick::MIN, 5900);
    // 孵化サイクル分の走行(チェック周期を考慮して-1からスタート)
    for (int i = -1; i < run_time_sec / 21; i++) {
        tiltLeftStick(Stick::NEUTRAL, Stick::MAX, 10400);
        tiltLeftStick(Stick::NEUTRAL, Stick::MIN, 10400);
    }
    tiltLeftStick(Stick::NEUTRAL, Stick::MAX, (run_time_sec % 21) * 1000);
}

// タマゴが孵化するのを待つ関数
void waitEggHatching() {
    pushButton(Button::A, 500, 30);
    delay(4000);
}

// 孵化した手持ちのポケモンをボックスに預ける関数
// box_line : 何列目にポケモンを預けるか
void sendHatchedPokemonToBox(int box_line) {
    // ボックスを開く
    pushButton(Button::X, 500);
    pushHat(Hat::RIGHT, 50);
    pushButton(Button::A, 1250);
    pushButton(Button::R, 1500);
    // 手持ちの孵化したポケモンを範囲選択
    pushHat(Hat::LEFT, 50);
    pushHat(Hat::DOWN, 50);
    pushButton(Button::Y, 50);
    pushButton(Button::Y, 50);
    pushButton(Button::A, 50);
    holdHat(Hat::DOWN, 1200);
    pushButton(Button::A, 50);
    // ボックスに移動させる
    pushHat(Hat::RIGHT, 100, box_line + 1);
    pushHat(Hat::UP, 50);
    pushButton(Button::A, 50);
    // ボックスがいっぱいになったら、次のボックスに移動させる
    if (box_line == 5) {
        pushHat(Hat::UP, 50);
        pushHat(Hat::RIGHT, 500);
    }
    // ボックスを閉じる
    pushButton(Button::B, 50);  // ボックスが空でなかった場合でも、ボックスを閉じてループを実行し続けさせるのに必要な記述
    pushButton(Button::B, 1500);
    pushButton(Button::B, 1250);
    // メニュー画面のカーソルをタウンマップに戻す
    pushHat(Hat::LEFT, 50);
}

void receiveAndHatchEggs(int box_line) {
    // 手持ちが1体の状態から、卵受け取り→孵化を繰り返していく
    for (int egg_num = 0; egg_num < 5; egg_num++) {
        moveToInitialPlayerPosition();
        getEggFromBreeder();
        pushButton(Button::X, 500);
        moveToInitialPlayerPosition();
        runAround(TIME_TO_HATCHING_SEC);
        waitEggHatching();
        // 「そらをとぶ」を使う前に、ズイタウンにいることを確定させる
        tiltLeftStick(Stick::NEUTRAL, Stick::MIN, 10400);
        tiltLeftStick(Stick::NEUTRAL, Stick::MAX, 4800);
        // 手持ちがいっぱいになったときの処理
        if (egg_num == 4) {
            // ボックスに預ける処理を呼び出す
            sendHatchedPokemonToBox(box_line);
            // 手持ちがいっぱいでない場合は、メニューを開いてからループに戻る
        } else {
            pushButton(Button::X, 500);
        }
    }
}

// マクロ① - BDSP自動孵化
void receiveAndHatch() {
    for (int box_line = 0; box_line < 6; box_line++) {
        receiveAndHatchEggs(box_line);
    }
}

// マクロ② - アルセウス早業
// ボタン入力の間隔(個人で調整してOK。早すぎると早業・力業の切り替えができないことがある)
const int INTERVAL = 400;
void Hayawaza() {
    pushButton(Button::A, INTERVAL);
    pushHat(Hat::LEFT, INTERVAL);
    pushButton(Button::A, INTERVAL);
    pushHat(Hat::LEFT, INTERVAL);
}

// マクロ③ - アルセウス力技
void Chikarawaza() {
    pushButton(Button::A, INTERVAL);
    pushHat(Hat::RIGHT, INTERVAL);  /* NOTE ポケモン交換時に右入力すると技を選択してしまうことがあるため、脱却するために力業のみの場合も左入力が必要 */
    pushButton(Button::A, INTERVAL);
    pushHat(Hat::LEFT, INTERVAL);
    pushHat(Hat::LEFT, INTERVAL);
}

// マクロ④ - A連打関数
void MashA() {
    //0.1秒毎にAボタン入力を3000回繰り返す
    pushButton(Button::A, 100, 3000);
}

// マクロ⑤ - B連打関数
void MashB() {
    //0.1秒毎にBボタン入力を3000回繰り返す
    pushButton(Button::B, 100, 3000);
}

// マクロ⑥ - X連打関数
void MashX() {
    //0.1秒毎にXボタン入力を3000回繰り返す
    pushButton(Button::X, 100, 3000);
}


// マイコンのリセット時に1度だけ行われる処理
void setup() {
    //モード切替用ピン
    pinMode(11, INPUT_PULLUP);  //モード設定用
    pinMode(10, INPUT_PULLUP);  //モード設定用
    pinMode(9, INPUT_PULLUP);   //モード設定用
    pinMode(8, INPUT_PULLUP);   //モード設定用
    pinMode(7, INPUT_PULLUP);   //割り込み用
    pinMode(0, INPUT_PULLUP);   //割り込み用
    attachInterrupt(2, S_flag, FALLING); //割り込み関数設定
    attachInterrupt(4, software_reset, FALLING); //割り込み関数設定
     //Switchがマイコンを認識するまでは信号を受け付けないため、適当な処理をさせておく
    pushButton(Button::ZL, 500, 5);   
     //LCD操作をマイコン認識よりも先に実行すると何故か上手くいかない
    lcd.init();
    lcd.backlight();
    lcd.clear();
    EGG_CYCLE = Cycle_Read();
    lcd_set("ModeSelect", EGG_CYCLE);
    delay(100);
}

// ここに記述した内容がループされ続ける
void loop() {
  if(start_flag == 0){
    //DIPスイッチ状態読み込み
    for(int i = 2; i <=5; i++ ){
      DIP_State[i-2] = digitalRead(i+6);
    }
    EGG_CYCLE = Cycle_Read();
    TIME_TO_HATCHING_SEC = calcTimeToHatchingSec();

    //(DIP_State,EGG_CYCLE)が前回読んだ値(temp_State,temp_CYCLE)から変わっていたら、
    //その時の(DIP_State,EGG_CYCLE)を(temp_State,temp_CYCLE)に保存する
    
    if((memcmp(DIP_State,temp_State,sizeof(DIP_State)) != 0) || (temp_CYCLE != EGG_CYCLE)){
          memcpy(temp_State,DIP_State,sizeof(DIP_State));  
          temp_CYCLE = EGG_CYCLE;
          if(memcmp(DIP_State,Mode1_State,sizeof(DIP_State)) == 0 ){
            lcd_set("Rcv&Hatch ", EGG_CYCLE);
            MacroMode = 1;
          }else if(memcmp(DIP_State,Mode2_State,sizeof(DIP_State)) == 0 ){
            lcd_set("Hayawaza  ", EGG_CYCLE);
            lcd.setCursor(0, 1);
            lcd.print("Cycle: None     ");
            MacroMode = 2;
          }else if(memcmp(DIP_State,Mode3_State,sizeof(DIP_State)) == 0 ){
            lcd_set("Chikara   ", EGG_CYCLE);
            lcd.setCursor(0, 1);
            lcd.print("Cycle: None     ");
            MacroMode = 3;
          }else if(memcmp(DIP_State,Mode4_State,sizeof(DIP_State)) == 0 ){
            lcd_set("MashA     ", EGG_CYCLE);
            lcd.setCursor(0, 1);
            lcd.print("Cycle: None     ");
            MacroMode = 4;
          }else if(memcmp(DIP_State,Mode5_State,sizeof(DIP_State)) == 0 ){
            lcd_set("MashB     ", EGG_CYCLE);
            lcd.setCursor(0, 1);
            lcd.print("Cycle: None     ");
            MacroMode = 5;
          }else if(memcmp(DIP_State,Mode6_State,sizeof(DIP_State)) == 0 ){
            lcd_set("MashX     ", EGG_CYCLE);
            lcd.setCursor(0, 1);
            lcd.print("Cycle: None     ");
            MacroMode = 6;

          //マクロモードを増やす場合はここに追記
          
          }else{
            lcd_set("ModeSelect", EGG_CYCLE);
            MacroMode = 0;
          }
      }

  //タクトスイッチが押され、start_flagが有効になるとマクロを実行する
  //以下に最終的に実行する関数を記載する。
  }else if(start_flag == 1){
      if(MacroMode == 1){
        if(j == 0){
          // メニューの左上にカーソルを持っていく
          pushButton(Button::X, 750);
          holdHat(Hat::UP_LEFT, 1500);
      
          // 「そらをとぶ」を使うことで、位置情報をリセットする
          moveToInitialPlayerPosition();
      
          // 初めのタマゴが出現するまで走り回る
          tiltLeftStick(Stick::MIN, Stick::NEUTRAL, 560);
          pushButton(Button::PLUS, 600);
          tiltLeftStick(Stick::NEUTRAL, Stick::MIN, 5900);
          tiltLeftStick(Stick::NEUTRAL, Stick::MAX, 10400);
          tiltLeftStick(Stick::NEUTRAL, Stick::MIN, 10400);
          tiltLeftStick(Stick::NEUTRAL, Stick::MAX, 4800);
      
          // メニューを開く動作をループに含めてしまうと、毎回メニューを閉じないといけなくなってしまうため、ループから外すことにした
          pushButton(Button::X, 500);
        
          j = j+1;
        }
        receiveAndHatch();         
      }else if(MacroMode == 2){
         Hayawaza();       
      }else if(MacroMode == 3){
        Chikarawaza();   
      }else if(MacroMode == 4){
        MashA();            
      }else if(MacroMode == 5){
        MashB();
      }else if(MacroMode == 6){
        MashX();
        //仮に1ループのみで終了したい場合は、無限ループを入れる
        //while(1);   //無限ループ
      }

      //マクロモードを増やす場合はここに追記

  }
    delay(250);
    
//  デバッグ用シリアル出力
//  Serial.print("DIP_State = ");
//  Serial.print(DIP_State[0]);
//  Serial.print(DIP_State[1]);
//  Serial.print(DIP_State[2]);
//  Serial.println(DIP_State[3]);
//  Serial.print("EGG_CYCLE = ");
//  Serial.println(EGG_CYCLE);
}

最後に

皆さんここまで読んでいただいてありがとうございました。
初めての記事で読みにくい部分も多々あったかと思いますが、誰かの役に立てれば幸いです。
電子工作としては、とても簡単な部類になると思いますので、是非チャレンジしてみてください。
本記事について、ご意見ご質問ございましたら、@john_poke222までご連絡ください。

この記事が気に入ったらサポートをしてみませんか?