M5ATOMをNintendo Switchコントローラー化して自動化したやつを、デバイス書き込み不要で処理をホットスワップしてみる


# はじめに
本記事はM5ATOMをNintendo Switchコントローラー化して操作を自動化してみる記事の続きとなります。

https://qiita.com/miso_develop/items/1312d813d8fc96f26ec9

[sorasen2020/SwitchControllerESP32](https://github.com/sorasen2020/SwitchControllerESP32) によってM5ATOMをNintendo Switchコントローラー化してAボタン連打による自動レベル上げで子どものゲーム制限時間の浪費は解消できたのですが、他のゲームでも自動化を行いたくなってきました。
そしてその自動化ではAボタンを連打するだけでなく、もう少し操作を組み合わせる必要がありました。

ゲームを切り替えるたびにM5ATOMへスケッチを書き込むのは面倒ですし、キャラクター移動等を伴う自動化は何度かトライ&エラーを行う必要があり、そのたびにスケッチを書き込むのもまた面倒でした。

そこで[opniz](https://github.com/miso-develop/opniz-sdk-nodejs)を使い、デバイスへの書き込み不要で処理をホットスワップしてみることにしました。



# デモ動画
こんな感じに、デバイス書き込みせずに自動化処理を入れ替えて実行できます。

https://x.com/miso_develop/status/1778988562014470224



# opnizとは
opnizとはM5StackといったESP32デバイスをNode.jsからobnizライクに遠隔制御するための、Node.js SDKおよびArduinoライブラリです。
しくみとしてはESP32デバイスおよびNode.js SDK間にてJSON形式のRPCメッセージをやりとりし、相互に定義されたメソッドを呼び合います。

![.](https://user-images.githubusercontent.com/22117028/274111780-1f1e22ec-66ac-4cd0-bee3-1a19ee2eb65c.png)

上記のしくみのため、M5ATOM以外にopniz実行環境（PC or Server）が別途必要になってきます。



# 使うもの

* PC or Server
	* opniz実行環境です
		* 常時稼働させるならRaspberry Piといったサーバーがおすすめです
		* Node.jsをインストールしておいてください
	* ATOMS3 Liteのセットアップにも使います
		* Arduino IDEかPlatform IOをインストールしておいてください
* [ATOMS3 Lite](https://www.switch-science.com/products/8778?srsltid=AfmBOoo3jLqTt-PNTN6cQUdQWbmjbJtzNeO6ytaxOEUqyV3TPHDLz5MT)（ATOMS3等ESP32 S3系デバイスならOK）
	* Switchコントローラー代わりにするマイコンです
* USB C to Cケーブル
	* 100均とかので大丈夫です



# 作り方
ATOMS3 Liteのセットアップと、opnizプログラムをセットアップしていきます。


## ATOMS3 Liteのセットアップ

Arduino IDE、Platform IOどちらでもいけると思います。
まずは以下の依存ライブラリをインストールします。

* bblanchon/ArduinoJson@^6.20.0
* links2004/WebSockets@^2.3.6
* m5stack/M5Unified@0.1.4
* fastled/FastLED@3.5.0

加えてopniz Arduino Libraryもインストールします。
Arduinoライブラリマネージャーにはリリースされていないため、GitHubリポジトリよりZIPをダウンロードします。

[**opnizライブラリのインストール方法詳細**](https://github.com/miso-develop/opniz-arduino-m5unified?tab=readme-ov-file#arduino%E3%81%A7%E3%81%AE%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB)

## ATOMS3 Liteのソースコード
依存ライブラリが揃ったら以下のスケッチをATOMS3 Liteへ書き込みます。

以下の3つの変数はお使いの環境に沿った値を指定してください。

* ssid: Wi-FiのSSIDを指定します
* password: Wi-Fiのパスワードを指定します
* address: opniz Serverを実行するPC or ServerのIPアドレスを指定します

```cpp:main.cpp
#define M5ATOMS3_LITE_LED_ENABLE
#include "<OpnizM5Unified.h>"
#include "<lib/WiFiConnector.h>"

#include "SwitchControllerESP32.h"



const char* ssid = "<Wi-Fi SSID>";
const char* password = "<Wi-Fi Password>";

const char* address = "<opniz Server IP address>";
const uint16_t port = 3000;

WiFiConnector wifiConnector(ssid, password);
Opniz::M5Unified* opniz = new Opniz::M5Unified(address, port);



class PushButton2Handler : public BaseHandler {
public:
    String name() override { return "100:1:1"; }
    String procedure(JsonArray params) override {
        uint16_t button = (uint16_t)params[0];
        int pushing_time_msec = (int)params[1];
        pushButton2((Button)button, pushing_time_msec, 0, 1);
        return "true";
    }
};

class PushHatButtonContinuousHandler : public BaseHandler {
public:
    String name() override { return "100:1:2"; }
    String procedure(JsonArray params) override {
        uint8_t button = (uint8_t)params[0];
        int pushing_time_msec = (int)params[1];
        pushHatButtonContinuous((Hat)button, pushing_time_msec);
        return "true";
    }
};

class TiltJoystickHandler : public BaseHandler {
public:
    String name() override { return "100:1:3"; }
    String procedure(JsonArray params) override {
        int lx_per = (int)params[0];
        int ly_per = (int)params[1];
        int rx_per = (int)params[2];
        int ry_per = (int)params[3];
        int tilt_time_msec = (int)params[4];
        tiltJoystick(lx_per, ly_per, rx_per, ry_per, tilt_time_msec, 0);
        return "true";
    }
};

class UseLStickHandler : public BaseHandler {
public:
    String name() override { return "100:1:4"; }
    String procedure(JsonArray params) override {
        uint16_t Lstick = (uint16_t)params[0];
        int tilt_time_msec = (int)params[1];
        UseLStick((LS)Lstick, tilt_time_msec, 0);
        return "true";
    }
};

class UseRStickHandler : public BaseHandler {
public:
    String name() override { return "100:1:5"; }
    String procedure(JsonArray params) override {
        uint16_t Rstick = (uint16_t)params[0];
        int tilt_time_msec = (int)params[1];
        UseRStick((RS)Rstick, tilt_time_msec, 0);
        return "true";
    }
};

class TiltLeftStickHandler : public BaseHandler {
public:
    String name() override { return "100:1:6"; }
    String procedure(JsonArray params) override {
        int direction_deg = (int)params[0];
        double power = (double)params[1];
        int holdtime = (int)params[2];
        TiltLeftStick(direction_deg, power, holdtime, 0);
        return "true";
    }
};

class PushButtonAHandler : public BaseHandler {
public:
    String name() override { return "100:7:1"; }
    String procedure(JsonArray params) override {
        pushButton(Button::A, 0, 1);
        return "true";
    }
};



void setup() {
    initM5();
    
    switchcontrolleresp32_init();
    USB.begin();
    switchcontrolleresp32_reset();
    
    wifiConnector.setTimeoutCallback([]() { esp_restart(); });
    wifiConnector.setConnectingSignal(blinkBlue);
    wifiConnector.connect();
    Serial.printf("opniz server address: %s\nopniz server port: %u\n\n", opniz->getAddress(), opniz->getPort());
    
    opniz->addHandler({
        new PushButton2Handler,
        new PushHatButtonContinuousHandler,
        new TiltJoystickHandler,
        new UseLStickHandler,
        new UseRStickHandler,
        new TiltLeftStickHandler,
        new PushButtonAHandler,
    });
    opniz->connect();
}

void loop() {
    opniz->loop();
    wifiConnector.watch();
    
    if (M5.BtnA.isPressed()) pushButton2((Button)4, 40, 0, 1);
}
```



## opnizプログラムのセットアップ

Node.jsプロジェクトを新規作成し、opnizをインストールします。

```sh
$ npm init -y
$ npm install opniz tsx
```



## opnizプログラムのソースコード

そして以下のソースでindex.tsを作成します

このコードでは`loopA`、`loopB`といったボタンを0.5秒おきに連打する関数を用意しており、`main`関数にて処理を切り替えるようなつくりになっています。
自動化処理を追加したいときは`loopA`等の関数を変更してみてください。
※SwitchControllerメソッドの詳細は[前記事](https://qiita.com/miso_develop/items/1312d813d8fc96f26ec9)を参考にしてみてください

```ts:index.ts
import { Opniz } from "opniz"

class SwitchController extends Opniz.M5Unified {
	
	public async pushButton(button: SwitchController.Button, pushingTime = 40): Promise<void> {
		await this.exec("100:1:1", button, pushingTime)
	}
	
	public async pushHat(hat: SwitchController.Hat, pushingTime = 40): Promise<void> {
		await this.exec("100:1:2", hat, pushingTime)
	}
	
	public async tiltStick(lxPer: number, lyPer: number, rxPer: number, ryPer: number, tiltTime: number): Promise<void> {
		await this.exec("100:1:3", lxPer, lyPer, rxPer, ryPer, tiltTime)
	}
	
	public async lStick(stickDirection: SwitchController.StickDirection, tiltTime: number): Promise<void> {
		await this.exec("100:1:4", stickDirection, tiltTime)
	}
	
	public async rStick(stickDirection: SwitchController.StickDirection, tiltTime: number): Promise<void> {
		await this.exec("100:1:5", stickDirection, tiltTime)
	}
	
	public async tiltLStick(directionDeg: number, power: number, holdTime: number): Promise<void> {
		await this.exec("100:1:6", directionDeg, power, holdTime)
	}
	
}

namespace SwitchController {
	export const Button = {
		Y: 0x0001,
		B: 0x0002,
		A: 0x0004,
		X: 0x0008,
		L: 0x0010,
		R: 0x0020,
		ZL: 0x0040,
		ZR: 0x0080,
		MINUS: 0x0100,
		PLUS: 0x0200,
		LCLICK: 0x0400,
		RCLICK: 0x0800,
		HOME: 0x1000,
		CAPTURE: 0x2000,
	} as const
	
	export const Hat = {
		UP: 0x00,
		UP_RIGHT: 0x01,
		RIGHT: 0x02,
		RIGHT_DOWN: 0x03,
		DOWN: 0x04,
		DOWN_LEFT: 0x05,
		LEFT: 0x06,
		LEFT_UP: 0x07,
		CENTER: 0x08,
	} as const
	
	export const StickDirection = {
		CENTER: 0x0000,
		UP: 0x0001,
		UP_RIGHT: 0x0002,
		RIGHT: 0x0003,
		DOWN_RIGHT: 0x0004,
		DOWN: 0x0005,
		DOWN_LEFT: 0x0006,
		LEFT: 0x0007,
		UP_LEFT: 0x0008,
	} as const
	
	export type Button = typeof Button[keyof typeof Button]
	export type Hat = typeof Hat[keyof typeof Hat]
	export type StickDirection = typeof StickDirection[keyof typeof StickDirection]
}



const port = 3000
const opniz = new SwitchController({ port })

const loopA = async () => {
	for (;;) {
		await opniz.pushButton(SwitchController.Button.A, 100)
		await opniz.sleep(500)
	}
}

const loopB = async () => {
	for (;;) {
		await opniz.pushButton(SwitchController.Button.B, 100)
		await opniz.sleep(500)
	}
}

const loopX = async () => {
	for (;;) {
		await opniz.pushButton(SwitchController.Button.X, 100)
		await opniz.sleep(500)
	}
}

const loopY = async () => {
	for (;;) {
		await opniz.pushButton(SwitchController.Button.Y, 100)
		await opniz.sleep(500)
	}
}



const main = async () => {
	while (!(await opniz.connectWait())) { log("connect..."); await opniz.sleep(100) }
	console.log("[connected]")
	
	try {
		await opniz.Led.drawpix(0, "#00ff00")
		
		await loopA()
		// await loopB()
		// await loopX()
		// await loopY()
		
	} catch(e) {
		console.log("[error]", e.message)
		await main()
	}
}
main()
```


# 実行

## ATOMS3 LiteをNintendo Switchへ接続
USB C to CケーブルでATOMS3 LiteをNintendo Switchへ接続します。

## Node.js（opniz）プログラムの実行
続いてPC or Server側のNode.jsプログラムを実行します。

```sh
npx tsx index.ts
```

## コントローラーの接続確認
ATOMS3 LiteをNintendo Switchと接続した状態でNode.jsプログラムが実行されると、Nintendo Switchにてコントローラーの接続確認画面が開かれます。
ATOMS3 Liteのボタンを押すことで確認OKとなります。
（ATOMS3 LiteのボタンはAボタンとして認識されています）



# 自動化処理の切り替え
Node.jsソースのmain関数内で自動化したい処理の関数を切り替えてプログラムを再実行します。

tsxコマンドにて`watch`オプションを追加するとソースコードの変更を検知して自動で再起動してくれて楽ちんです。

```sh
npx tsx watch index.ts
```



# おわりに
以上でデバイスへの再書き込み不要でNintendo Switch自動化処理を簡単に切り替えられるようになりました。

しかしここでもまだ課題があります。
処理の記述がプログラムのためどうしても子どもが作るには難しく、また自動化処理の切り替えも同様で私が対応しなければいけません。

この問題を以下のツイートのようにLINE Botで解決しているのですが、その記事もいつかまた書きたいなと思います。

https://x.com/miso_develop/status/1787543193028108501