M5ATOMをNintendo Switchコントローラー化して操作を自動化してみる

# はじめに
きっかけは以下のツイートの内容まんまですが、Nintendo Switchのコントローラー制御を自動化したくなりました。
自動化にあたり行った調査、自動化で使用したライブラリとその使い方を紹介していきます。

https://twitter.com/miso_develop/status/1778812526731628695



# 調査

## 「Switch 自動化」でググる
Arduinoが基本のよう。
けどArduinoが手元にない…
M5StackデバイスはいっぱいあるのでESP32でいける方法がほしい…

## 生成AIに聞いてみる
ChatGPT 3.5、Claude3 Haiku、Bingに、
「ESP32というマイコンを使ってNintendo Switchのコントローラーを作りたいです。どのようなライブラリを駆使すれば実現できますか。」
と聞いてみた。

結果、プロンプトが適当すぎるからでしょうがChatGPTとClaude3はESP32のBLEライブラリを提示してきて結構な低レイヤーから実装させようとしてきました。

ただBingはESP32向けのSwitchコントローラーライブラリを提示してくれて、そこで教えてくれた[sorasen2020/SwitchControllerESP32](https://github.com/sorasen2020/SwitchControllerESP32)がサンプルコード見た感じ一番シンプルだったのでこれを使ってみました。

https://github.com/sorasen2020/SwitchControllerESP32



# 実装
[サンプルコード](https://github.com/sorasen2020/SwitchControllerESP32/blob/master/examples/AutoButton/AutoButton.ino)をそのまま[AtomS3](https://www.switch-science.com/products/8670)に書き込んでみたらSwitchに認識されなくて、コード冒頭の`#ifndef`まわりを消したらいけました。

当初目的としてはA連打だけしたかったのでこれで実装完了。
簡単すぎる…
BingとSwitchControllerESP32の作者さんありがとう…

サンプルコードの`#ifndef`まわりを削除しただけのコードがこちら。

```cpp
/*
Copyright (c) 2023 sorasen2020
released under the MIT license
https://opensource.org/licenses/mit-license.php
*/

#include "SwitchControllerESP32.h"
#include "Arduino.h"

void setup() {
  switchcontrolleresp32_init();
  USB.begin();
  switchcontrolleresp32_reset();
}

void loop() {
  pushButton(Button::A, 100, 1);
}
```

https://github.com/sorasen2020/SwitchControllerESP32/blob/master/examples/AutoButton/AutoButton.ino

https://www.switch-science.com/products/8670



# 簡単にコントローラー操作メソッドを紹介

## pushButton

```cpp
void pushButton(Button button, int delay_after_pushing_msec, int loop_num)
```

ボタンを押すメソッドです。
`button`に後述するButton型を指定します。
ここでは方向ボタンは含まれません。

単にボタンを押すだけのケースでこのメソッドを使います。

`delay_after_pushing_msec`は押したあとの待ち時間のミリ秒、`loop_num`は繰り返し回数を指定します。

`Button`型の定義は下表です。
値を足し合わせることで同時押しになります。

| ボタン | 記述 | 値 |
|:-:|:-:|:-:|
| Y | Button::Y | 0x0001 |
| B | Button::B | 0x0002 |
| A | Button::A | 0x0004 |
| X | Button::X | 0x0008 |
| L | Button::L | 0x0010 |
| R | Button::R | 0x0020 |
| ZL | Button::ZL | 0x0040 |
| ZR | Button::ZR | 0x0080 |
| - | Button::MINUS | 0x0100 |
| + | Button::PLUS | 0x0200 |
| 左スティック押し込み | Button::LCLICK | 0x0400 |
| 右スティック押し込み | Button::RCLICK | 0x0800 |
| HOME | Button::HOME | 0x1000 |
| キャプチャー | Button::CAPTURE | 0x2000 |



## pushButton2

```cpp
void pushButton2(Button button, int pushing_time_msec, int delay_after_pushing_msec, int loop_num)
```

`pushButton`の引数に加え、ボタンを押し込んでいる時間のミリ秒となる`pushing_time_msec`が追加されたメソッドです。
ちなみに`pushButton`でのデフォルトのボタン押し込み時間は40msecです。



## pushHatButton

```cpp
void pushHatButton(Hat button, int delay_after_pushing_msec, int loop_num)
```

`pushButton`と同じようなメソッドですが、こちらは方向ボタンを操作するメソッドです。

`Hat`型の定義は下表です。

| ボタン | 記述 | 値 |
|:-:|:-:|:-:|
| 上 | Hat::UP | 0x00 |
| 上右同時 | Hat::UP_RIGHT | 0x01 |
| 右 | Hat::RIGHT | 0x02 |
| 右下同時 | Hat::RIGHT_DOWN | 0x03 |
| 下 | Hat::DOWN | 0x04 |
| 下左同時 | Hat::DOWN_LEFT | 0x05 |
| 左 | Hat::LEFT | 0x06 |
| 左上同時 | Hat::LEFT_UP | 0x07 |
| ニュートラル | Hat::CENTER | 0x08 |



## pushHatButtonContinuous

```cpp
void pushHatButtonContinuous(Hat button, int pushing_time_msec)
```

方向ボタンを押し込んでいる時間のミリ秒を`pushing_time_msec`で指定します。
`pushButton2`とは異なり`delay_after_pushing_msec`、`loop_num`の引数は指定できません。



## tiltJoystick

```cpp
void tiltJoystick(int lx_per, int ly_per, int rx_per, int ry_per, int tilt_time_msec, int delay_after_tilt_msec)
```

`lx_per`、`ly_per`、`rx_per`、`ry_per`は左右スティックそれぞれのX/Y軸値を-100～100の範囲で指定します。
`tilt_time_msec`はスティックを倒している時間のミリ秒、`delay_after_tilt_msec`はメソッド実行後の待ち時間のミリ秒です。

細かなスティック操作を行いたい場合にこのメソッドを使用します。



## UseLStick / UseRStick

```cpp
void UseLStick(LS Lstick, int tilt_time_msec, int delay_after_tilt_msec)
void UseRStick(RS Rstick, int tilt_time_msec, int delay_after_tilt_msec)
```

左スティック / 右スティックを特定の方向に完全に倒すメソッドです。
`Lstick / Rstick`は下表の`LS / RS`型を指定します（割当られている値が`Hat`型と異なります）。
`tilt_time_msec`はスティックを倒している時間のミリ秒、`delay_after_tilt_msec`はメソッド実行後の待ち時間のミリ秒です。


| スティック位置 | 記述 | 値 |
|:-:|:-:|:-:|
| 中央（ニュートラル） | LS::LS_CENTER / RS::RS_CENTER | 0x0000 |
| 上 | LS::LS_UP / RS::RS_UP | 0x0001 |
| 右上 | LS::LS_UP_RIGHT / RS::RS_UP_RIGHT | 0x0002 |
| 右 | LS::LS_RIGHT / RS::RS_RIGHT | 0x0003 |
| 右下 | LS::LS_DOWN_RIGHT / RS::RS_DOWN_RIGHT | 0x0004 |
| 下 | LS::LS_DOWN / RS::RS_DOWN | 0x0005 |
| 左下 | LS::LS_DOWN_LEFT / RS::RS_DOWN_LEFT | 0x0006 |
| 左 | LS::LS_LEFT / RS::RS_LEFT | 0x0007 |
| 左上 | LS::LS_UP_LEFT / RS::RS_UP_LEFT | 0x0008 |



## TiltLeftStick

```cpp
void TiltLeftStick(int direction_deg, double power, int holdtime, int delaytime)
```

左スティックを`direction_deg`で指定した角度へ倒します。
`power`で傾き具合を0.0～1.0の間で指定できます。
`holdtime`はスティックを倒している時間のミリ秒、`delaytime`はメソッド実行後の待ち時間のミリ秒です。

なお右スティックバージョンのメソッド（`TiltRightStick`のようなメソッド）はありません。



# おわりに

先達の方々による技術の積み重ねのおかげでめちゃくちゃあっさり自動化が実現できました。

なお今回やりたかったA連打のような操作はデバイスに書き込んですぐに実用できますが、複数操作のディレイタイム調整やスティック操作が絡んでくると微妙な試行錯誤が必要になってきます。
コードを書き換えるたびにデバイス書き込みもして…と繰り返すのはかなり億劫なので、opnizを使ってデバイス書き込み不要で処理をホットスワップする方法についても記事にしたいと思います。