ニンテンドースイッチの自動化

一時期ニンテンドースイッチのポケットモンスターソードシールドにハマっていたので、そのときに「お金・ワット集めめんどくさいなあ」という一心で実装したタスク自動化の方法を説明します。ポケモン剣盾が出た当初に公開しなかったのは良心だと思ってください。

難しくはないのですが、事前に必要なものがあるので、全部集められなければ素直に諦めましょう。

あと、難しくはないのですが、それなりにコマンドラインベースのパソコンスキルが必要なので、よくわからない人は素直に諦めましょう。近くに詳しい人がいれば聞いてみましょう、めんどくさいと思われる可能性大ですが。

うまくいくとこんな感じになります。(個人情報を消すため一部編集済)(この頃のテレビは小さかった)




1.必要なもの


Arduino UNO R3



電子工作のお供です。Arduinoの他のモデルでも可能ですが、ATmega16U2チップを搭載しているものを前提に説明するので、よくわからない人は脳死でUNO R3を買いましょう。それと注意点として、公式以外にも他のメーカーからArduinoは出ていますが、必ず公式のものを買いましょう。私は友人から使ってないArduinoをもらって、嬉々として実装を始めたもののうまくいかずに嵌りました。結局公式のものを買って試してみるとすんなりいきました。

USBケーブル


USB-AとUSB-Bタイプの端子になっているケーブルです。これを使ってパソコンとArduinoを接続します。上記のArduinoにはついていません。プリンタ用のケーブルが余っている人はそれで大丈夫です。

Ubuntuがインストールされているパソコン

これは持っていない場合、余っているパソコンなどに入れてみるなど試行錯誤してください。ラズベリーパイでも可能です。私は仕事用のパソコンを弄りたくなかったので、余っていたRaspberry Pi 3B+にUbuntu Mateを入れました。試してはいないけれどデフォルトのRaspbian OSでも可能だと思います。



UnixだったらいけるだろうとMacを使おうとしているあなた、Macではなぜかコンパイラーがエラーを吐きます。新旧のOSバージョンを試しましたがダメでした。なぜ?さあ。素直にUbuntuを用意しましょう。

コントローラーライブラリ

ここからスイッチコントローラーのソースを入手します。
Ubuntu上で、ターミナルを開き、
$ git clone https://github.com/bertrandom/snowball-thrower
$ cd snowball-thrower
$ git clone https://github.com/abcminiuser/lufa.git
とタイプすれば必要なものが揃います。snowball-throwerのページには--recursiveオプションを使えと書いてますが、それは事前にフォークさせておかないと機能しないのでlufaは上記のように別に入れましょう。よくわからない人は脳死で上記のコマンドをコピペしましょう。

DFUプログラマー

次にDFUプログラマーという、Arduinoのファームウェアを更新するプログラムを入れます。公式サイトの解説を参考にしましょう。
$ sudo apt-get install dfu-programmer

以上で必要なものは全部です。あとはUbuntu上での操作が主です。ここまでで何人脱落したのだろうか。


2.Makefileの更新

まずはMakefileの設定をArduino UNO R3用に変更します。
$ nano makefile
ライン15の
MCU = at90usb1286

MCU = atmega16u2
に変更します。簡単ですね。

2.Joystick.cの更新

snowball-throwerに含まれているソースコードには、スイッチ用のボタンが足りないので
PLUS、MINUS、HOME
をライン37に追加します。

さらにそのボタン用の条件節をライン433らへんに追加します。

あとは、ライン44以下に延々と記述されているコマンドを書き直すのですが、どのボタンをどれだけの時間押して、という擬似的なコントローラー上での操作をひとつひとつ記述していきます。

このJoystick.cの変更は割と大変なので、この記事の最後に全文を載せているので、コピペして使ってください。記述されている操作は、時渡りというテクニックを利用した、ねがいのかたまりを投げ込んだ状態のポケモンの巣穴の前に主人公が立った状態から始まる、2000Wを無限に回収するスクリプトです。月の変更でループがストップするのを避けるため、ボックスの一番左上にはポケモンを置かないようにしてください。一晩くらい放置すればワットがカンストするはずです。

他にも、同じく時渡りを利用したほりだしもの市の無限購入や、ザシアン単騎でのトーナメントの無限周回などのコマンドもあったのですが、不注意で消してしまいました。まあ、簡単なので練習だと思って自分で作ってみるといいと思います。お金に関しては、無限ワットをゴージャスボールに換えてそれを換金すればカンストできます。

3.ファームウェアを書き込む

さて、操作を記述したJoystick.cを用意したあとは、その操作をコンピューター語に変換するためにコンパイルします。

$ make
問題なくコンパイルできるはずですが、失敗したら、エラーメッセージをよく読んで解決してみましょう。解決できなければ諦めましょう。
ここでコンパイルされた操作は、ファームウェアとして書き込まれているので、Arduinoをスイッチに接続すると自動で記述された操作を行います。

次は、そのファームウェアをArduinoに書き込みましょう。ArduinoをケーブルでUbuntuのパソコンに接続します。DFU Programmerが橋渡しするため、ドライバは必要ないはず。



上の画像で示されている、RESETとGNDのピンを適当な金属で一瞬つなげてショートさせます。成功すれば、ONのライトが点滅するはずです。これで、Arduinoがリセットされたので書き込む準備ができました。

$ sudo dfu-programmer atmega16u2 erase
$ sudo dfu-programmer atmega16u2 flash Joystick.hex
$ sudo dfu-programmer atmega16u2 reset

この操作で、現在入っているファームウェアは消去され、新しいファームウェアがArduinoに書き込まれました。

あとは、スイッチのUSB端子にArduinoを接続すると自動で始まります。ここでの注意点は、事前にねがいのかたまりを投げ込んだ状態のポケモンの巣穴の前に立ち、本体の電源を一度切り、再度つけた状態(以下画像参照)で、すかさずスイッチのドックにあるUSB端子の穴にArduinoを接続することです。そうするとArduinoが自動でAを連打し、ポケモンを開き、決められた操作を開始します。


4.まとめ

さて、うまくいけばいいのですが、そうでなければ諦めて地道に手作業でやりましょう。ここで諦めない人は、プログラミング系の才能があります。ひとつひとつ手順を確認し、それぞれの情報ソース(特にこれ)を確認しながら進めてみてください。



4.Joystick.c全文

以下、Joystick.c

/*
Nintendo Switch Fightstick - Proof-of-Concept

Based on the LUFA library's Low-Level Joystick Demo
 (C) Dean Camera
Based on the HORI's Pokken Tournament Pro Pad design
 (C) HORI

This project implements a modified version of HORI's Pokken Tournament Pro Pad
USB descriptors to allow for the creation of custom controllers for the
Nintendo Switch. This also works to a limited degree on the PS3.

Since System Update v3.0.0, the Nintendo Switch recognizes the Pokken
Tournament Pro Pad as a Pro Controller. Physical design limitations prevent
the Pokken Controller from functioning at the same level as the Pro
Controller. However, by default most of the descriptors are there, with the
exception of Home and Capture. Descriptor modification allows us to unlock
these buttons for our use.
*/

#include "Joystick.h"

typedef enum {
 UP,
 DOWN,
 LEFT,
 RIGHT,
 X,
 Y,
 A,
 B,
 L,
 R,
 THROW,
 NOTHING,
 TRIGGERS,
 PLUS,
 MINUS,
 HOME
} Buttons_t;

typedef struct {
 Buttons_t button;
 uint16_t duration;
} command; 

static const command step[] = {
 // Setup controller
 { NOTHING,  150 },
 { TRIGGERS,   5 },
 { NOTHING,  100 },
 { A,          5 },
 { NOTHING,  150 },
 // loop actions
 { A,          1 }, // open a den
 { NOTHING,  100 },
 { A,          1 }, // click Invite Others
 { NOTHING,  100 },
 { HOME,       1 }, // back to home
 { NOTHING,   20 },
 { DOWN,      1 }, // menu: news
 { NOTHING,   15 },
 { RIGHT,     2 }, // menu: eshop
 { NOTHING,   15 },
 { RIGHT,     2 }, // menu: album
 { NOTHING,   15 },
 { RIGHT,     2 }, // menu: controller
 { NOTHING,   15 },
 { RIGHT,     2 }, // menu: system settings
 { NOTHING,   15 },
 { A,          1 }, // click system settings
 { NOTHING,   15 },
 { DOWN,      1 }, // menu: airplane mode
 { NOTHING,   15 },
 { DOWN,     1 }, // menu: screen brightness
 { NOTHING,   15 },
 { DOWN,     1 }, // menu: screen lock
 { NOTHING,   15 },
 { DOWN,     1 }, // menu: parental control
 { NOTHING,   15 },
 { DOWN,     1 }, // menu: internet
 { NOTHING,   15 },
 { DOWN,     1 }, // menu: data management
 { NOTHING,   15 },
 { DOWN,     1 }, // menu: users
 { NOTHING,   15 },
 { DOWN,     1 }, // menu: mii
 { NOTHING,   15 },
 { DOWN,     1 }, // menu: amiibo
 { NOTHING,   15 },
 { DOWN,     1 }, // menu: themes
 { NOTHING,   15 },
 { DOWN,     1 }, // menu: notifications
 { NOTHING,   15 },
 { DOWN,     1 }, // menu: sleep mode
 { NOTHING,   15 },
 { DOWN,     1 }, // menu: controllers and sensors
 { NOTHING,   15 },
 { DOWN,     1 }, // menu: tv settings
 { NOTHING,   15 },
 { DOWN,     1 }, // menu: system
 { NOTHING,   15 },
 { A,         1 }, // menu: click system
 { NOTHING,   15 },
 { DOWN,     1 }, // menu: concole nickname
 { NOTHING,   15 },
 { DOWN,     1 }, // menu: language
 { NOTHING,   15 },
 { DOWN,     1 }, // menu: region
 { NOTHING,   15 },
 { DOWN,     1 }, // menu: date and time
 { NOTHING,   15 },
 { A,         1 }, // menu: click date and time
 { NOTHING,   15 },
 { DOWN,     1 }, // menu: time zone
 { NOTHING,   15 },
 { DOWN,     1 }, // menu: date and time
 { NOTHING,   15 },
 { A,         1 }, // menu: click date and time
 { NOTHING,   15 },
 { RIGHT,     1 }, // menu: day
 { NOTHING,   15 },
 { UP,     1 }, // menu: increment
 { NOTHING,   15 },
 { A,         1 }, // move to year
 { NOTHING,   15 },
 { A,         1 }, // move to hour
 { NOTHING,   15 },
 { A,         1 }, // move to minute
 { NOTHING,   15 },
 { A,         1 }, // move to am/pm
 { NOTHING,   15 },
 { A,         1 }, // move to ok
 { NOTHING,   15 },
 { A,         1 }, // click ok
 { NOTHING,   15 },
 { B,         1 }, // back
 { NOTHING,   15 },
 { B,         1 }, // back
 { NOTHING,   15 },
 { B,         1 }, // back
 { NOTHING,   15 },
 { LEFT,     1 }, // move to controllers
 { NOTHING,   15 },
 { LEFT,     1 }, // move to album
 { NOTHING,   15 },
 { LEFT,     1 }, // move to eshop
 { NOTHING,   15 },
 { LEFT,     1 }, // move to news
 { NOTHING,   15 },
 { UP,     1 }, // move to the game icon
 { NOTHING,   15 },
 { A,         1 }, // click the icon
 { NOTHING,   50 },
        // prevent month-end loop breaker:
        // if prev iteration was successful:
        //     click 'switch pokemon'
        // else: click empty slot (already in switch pokemon screen)
        { A,         1 },
        { NOTHING,   100 },
 { B,         1 }, // quit 'switch pokemon'
        { NOTHING,   50 },
        { B,         1}, //  quit invitation
 { NOTHING,   100 },
 { A,         1 }, // click yes
 { NOTHING,  200 },
        { A,         1 }, // open den
        { NOTHING, 50 },
        { A,         1 }, // receive 2000W
        { NOTHING, 100 },
        { A,         1 },
        { NOTHING, 100 }
};

// Main entry point.
int main(void) {
 // We'll start by performing hardware and peripheral setup.
 SetupHardware();
 // We'll then enable global interrupts for our use.
 GlobalInterruptEnable();
 // Once that's done, we'll enter an infinite loop.
 for (;;)
 {
  // We need to run our task to process and deliver data for our IN and OUT endpoints.
  HID_Task();
  // We also need to run the main USB management task.
  USB_USBTask();
 }
}

// Configures hardware and peripherals, such as the USB peripherals.
void SetupHardware(void) {
 // We need to disable watchdog if enabled by bootloader/fuses.
 MCUSR &= ~(1 << WDRF);
 wdt_disable();

 // We need to disable clock division before initializing the USB hardware.
 clock_prescale_set(clock_div_1);
 // We can then initialize our hardware and peripherals, including the USB stack.

 #ifdef ALERT_WHEN_DONE
 // Both PORTD and PORTB will be used for the optional LED flashing and buzzer.
 #warning LED and Buzzer functionality enabled. All pins on both PORTB and \
PORTD will toggle when printing is done.
 DDRD  = 0xFF; //Teensy uses PORTD
 PORTD =  0x0;
                  //We'll just flash all pins on both ports since the UNO R3
 DDRB  = 0xFF; //uses PORTB. Micro can use either or, but both give us 2 LEDs
 PORTB =  0x0; //The ATmega328P on the UNO will be resetting, so unplug it?
 #endif
 // The USB stack should be initialized last.
 USB_Init();
}

// Fired to indicate that the device is enumerating.
void EVENT_USB_Device_Connect(void) {
 // We can indicate that we're enumerating here (via status LEDs, sound, etc.).
}

// Fired to indicate that the device is no longer connected to a host.
void EVENT_USB_Device_Disconnect(void) {
 // We can indicate that our device is not ready (via status LEDs, sound, etc.).
}

// Fired when the host set the current configuration of the USB device after enumeration.
void EVENT_USB_Device_ConfigurationChanged(void) {
 bool ConfigSuccess = true;

 // We setup the HID report endpoints.
 ConfigSuccess &= Endpoint_ConfigureEndpoint(JOYSTICK_OUT_EPADDR, EP_TYPE_INTERRUPT, JOYSTICK_EPSIZE, 1);
 ConfigSuccess &= Endpoint_ConfigureEndpoint(JOYSTICK_IN_EPADDR, EP_TYPE_INTERRUPT, JOYSTICK_EPSIZE, 1);

 // We can read ConfigSuccess to indicate a success or failure at this point.
}

// Process control requests sent to the device from the USB host.
void EVENT_USB_Device_ControlRequest(void) {
 // We can handle two control requests: a GetReport and a SetReport.

 // Not used here, it looks like we don't receive control request from the Switch.
}

// Process and deliver data from IN and OUT endpoints.
void HID_Task(void) {
 // If the device isn't connected and properly configured, we can't do anything here.
 if (USB_DeviceState != DEVICE_STATE_Configured)
  return;

 // We'll start with the OUT endpoint.
 Endpoint_SelectEndpoint(JOYSTICK_OUT_EPADDR);
 // We'll check to see if we received something on the OUT endpoint.
 if (Endpoint_IsOUTReceived())
 {
  // If we did, and the packet has data, we'll react to it.
  if (Endpoint_IsReadWriteAllowed())
  {
   // We'll create a place to store our data received from the host.
   USB_JoystickReport_Output_t JoystickOutputData;
   // We'll then take in that data, setting it up in our storage.
   while(Endpoint_Read_Stream_LE(&JoystickOutputData, sizeof(JoystickOutputData), NULL) != ENDPOINT_RWSTREAM_NoError);
   // At this point, we can react to this data.

   // However, since we're not doing anything with this data, we abandon it.
  }
  // Regardless of whether we reacted to the data, we acknowledge an OUT packet on this endpoint.
  Endpoint_ClearOUT();
 }

 // We'll then move on to the IN endpoint.
 Endpoint_SelectEndpoint(JOYSTICK_IN_EPADDR);
 // We first check to see if the host is ready to accept data.
 if (Endpoint_IsINReady())
 {
  // We'll create an empty report.
  USB_JoystickReport_Input_t JoystickInputData;
  // We'll then populate this report with what we want to send to the host.
  GetNextReport(&JoystickInputData);
  // Once populated, we can output this data to the host. We do this by first writing the data to the control stream.
  while(Endpoint_Write_Stream_LE(&JoystickInputData, sizeof(JoystickInputData), NULL) != ENDPOINT_RWSTREAM_NoError);
  // We then send an IN packet on this endpoint.
  Endpoint_ClearIN();
 }
}

typedef enum {
 SYNC_CONTROLLER,
 SYNC_POSITION,
 BREATHE,
 PROCESS,
 CLEANUP,
 DONE
} State_t;
State_t state = SYNC_CONTROLLER;

#define ECHOES 2
int echoes = 0;
USB_JoystickReport_Input_t last_report;

int report_count = 0;
int xpos = 0;
int ypos = 0;
int bufindex = 0;
int duration_count = 0;
int portsval = 0;

// Prepare the next report for the host.
void GetNextReport(USB_JoystickReport_Input_t* const ReportData) {

 // Prepare an empty report
 memset(ReportData, 0, sizeof(USB_JoystickReport_Input_t));
 ReportData->LX = STICK_CENTER;
 ReportData->LY = STICK_CENTER;
 ReportData->RX = STICK_CENTER;
 ReportData->RY = STICK_CENTER;
 ReportData->HAT = HAT_CENTER;

 // Repeat ECHOES times the last report
 if (echoes > 0)
 {
  memcpy(ReportData, &last_report, sizeof(USB_JoystickReport_Input_t));
  echoes--;
  return;
 }

 // States and moves management
 switch (state)
 {

  case SYNC_CONTROLLER:
   state = BREATHE;
   break;

  // case SYNC_CONTROLLER:
  //  if (report_count > 550)
  //  {
  //   report_count = 0;
  //   state = SYNC_POSITION;
  //  }
  //  else if (report_count == 250 || report_count == 300 || report_count == 325)
  //  {
  //   ReportData->Button |= SWITCH_L | SWITCH_R;
  //  }
  //  else if (report_count == 350 || report_count == 375 || report_count == 400)
  //  {
  //   ReportData->Button |= SWITCH_A;
  //  }
  //  else
  //  {
  //   ReportData->Button = 0;
  //   ReportData->LX = STICK_CENTER;
  //   ReportData->LY = STICK_CENTER;
  //   ReportData->RX = STICK_CENTER;
  //   ReportData->RY = STICK_CENTER;
  //   ReportData->HAT = HAT_CENTER;
  //  }
  //  report_count++;
  //  break;

  case SYNC_POSITION:
   bufindex = 0;


   ReportData->Button = 0;
   ReportData->LX = STICK_CENTER;
   ReportData->LY = STICK_CENTER;
   ReportData->RX = STICK_CENTER;
   ReportData->RY = STICK_CENTER;
   ReportData->HAT = HAT_CENTER;


   state = BREATHE;
   break;

  case BREATHE:
   state = PROCESS;
   break;

  case PROCESS:

   switch (step[bufindex].button)
   {

    case UP:
     ReportData->LY = STICK_MIN;    
     break;

    case LEFT:
     ReportData->LX = STICK_MIN;    
     break;

    case DOWN:
     ReportData->LY = STICK_MAX;    
     break;

    case RIGHT:
     ReportData->LX = STICK_MAX;    
     break;

    case PLUS:
     ReportData->Button |= SWITCH_PLUS;
     break;

    case MINUS:
     ReportData->Button |= SWITCH_MINUS;
     break;

    case HOME:
     ReportData->Button |= SWITCH_HOME;
     break;

    case A:
     ReportData->Button |= SWITCH_A;
     break;

    case B:
     ReportData->Button |= SWITCH_B;
     break;

    case X:
     ReportData->Button |= SWITCH_X;
     break;

    case Y:
     ReportData->Button |= SWITCH_Y;
     break;

    case L:
     ReportData->Button |= SWITCH_L;
     break;

    case R:
     ReportData->Button |= SWITCH_R;
     break;

    case THROW:
     ReportData->LY = STICK_MIN;    
     ReportData->Button |= SWITCH_R;
     break;

    case TRIGGERS:
     ReportData->Button |= SWITCH_L | SWITCH_R;
     break;

    default:
     ReportData->LX = STICK_CENTER;
     ReportData->LY = STICK_CENTER;
     ReportData->RX = STICK_CENTER;
     ReportData->RY = STICK_CENTER;
     ReportData->HAT = HAT_CENTER;
     break;
   }

   duration_count++;

   if (duration_count > step[bufindex].duration)
   {
    bufindex++;
    duration_count = 0;    
   }


   if (bufindex > (int)( sizeof(step) / sizeof(step[0])) - 1)
   {

    // state = CLEANUP;

    bufindex = 7;
    duration_count = 0;

    state = BREATHE;

    ReportData->LX = STICK_CENTER;
    ReportData->LY = STICK_CENTER;
    ReportData->RX = STICK_CENTER;
    ReportData->RY = STICK_CENTER;
    ReportData->HAT = HAT_CENTER;


    // state = DONE;
//    state = BREATHE;

   }

   break;

  case CLEANUP:
   state = DONE;
   break;

  case DONE:
   #ifdef ALERT_WHEN_DONE
   portsval = ~portsval;
   PORTD = portsval; //flash LED(s) and sound buzzer if attached
   PORTB = portsval;
   _delay_ms(250);
   #endif
   return;
 }

 // // Inking
 // if (state != SYNC_CONTROLLER && state != SYNC_POSITION)
 //  if (pgm_read_byte(&(image_data[(xpos / 8) + (ypos * 40)])) & 1 << (xpos % 8))
 //   ReportData->Button |= SWITCH_A;

 // Prepare to echo this report
 memcpy(&last_report, ReportData, sizeof(USB_JoystickReport_Input_t));
 echoes = ECHOES;

}

コメント

このブログの人気の投稿

最強CPU(Ryzen9 3950X)で最強パソコンを自作する:実装編

SONYの新型ヘッドホンの実力は?旧モデルと比較し購入レビュー

ギター 音楽理論