ADVENT CALENDAR 2019

SwitchのJoyConをプログラムから使う

By argonism

Amusement Creators 2019 アドベントカレンダー 23日目(12/23)の記事です。Nintendo_Switch_Reverse_Engineeringを参考に、ほとんどわからないなりにボタンとスティックの入力を取るくらいのことはできたので、その辺の知識をまとめていきます。

Joycon 基本情報

Joyconとは、Nintendo Switchのコントローラーです。
JoyconにはLとRがあり、Switchを買うと二つ付いてきます。

このコントローラー、結構な高性能で、

  • HD振動
  • 加速度センサー
  • ジャイロセンサー
  • nfcの読みとり
  • モーションIRカメラ

こんなにたくさんの機能が!。
(nfcの読みとり,モーションIRカメラはRのみ。)

接続はBluetooth 3.0により行われています。そのため、パソコンと接続するのも簡単できちゃいます!

Switch Reverse Engineering

Nintendo_Switch_Reverse_Engineering

これはdekuNukemという方のリポジトリで、主にJoyconをリバースエンジニアリングした結果とその解説を載せてくれています。

markdownでまとめてくれているので、それをよく参照することになるかと思います。
以下各 .md ファイルの概要(笑)です。

・bluetooth_hid_notes.md
Joyconとのやりとりをするために必要な、Output reports と Input reports 、 Feature reportsについて解説しています。

・bluetooth_hid_subcommands_notes.md
Output reportsで送信するサブコマンドの列挙と説明が乗っています。これを通して、Joyconに各種命令を送信します。

・imu_sensor_notes.md
今回僕はそんなにみていないですが、加速度センサー&ジャイロセンサー周りの説明がされています。

・rumble_data_table.md
joyconを振動させる際に送信する周波数と振幅周りの表です。 表形式になっていますが、表になっている意味がいまいちよくわかりません。 rumbleとはvibrationのことのようです。

・spi_flash_notes.md
今回一回も参照しなかったので、文字通り何もわからないです。spiって何…

まずは接続してみる。

これは普通にイヤホンを接続する感覚でJoyconを接続できます。 接続するとき、Joyconのシンクロボタンを長押しすることに注意しましょう。

hidapi

では、接続されたJoyconをプログラムで読み取ってみます。

そのために、hidapiを使用します。

hidapiは USB もしくは Bluetooth で接続された HID device を取得し、各種レポートを送信するクロスプラットフォームのC++ライブラリです。

このライブラリの説明をこれ以上すると長くなりすぎるので、割愛します。

Joyconを取得してみる。

hidapiをセットアップしたら、実際に取得してみましょう。

#define JOYCON_L_PRODUCT_ID 8198
#define JOYCON_R_PRODUCT_ID 8199
#include <hidapi/hidapi.h>

int main()
{
    // 接続されているHIDデバイスの連結リストを取得。
    hid_device_info *device = hid_enumerate(0, 0);

    while (device)
    {
        // プロダクトID等を指定して、HID deviceをopenする。そうすると、そのhidデバイスの情報が載ったhid_deviceが帰ってくる。
        hid_device *dev = hid_open(device->vendor_id, device->product_id, device->serial_number);
        // 今開いているデバイスのプロダクト名の取得。
        printf("\nproduct_id: %ls", device->product_string);
        // 次のデバイスへ。  
        device = device->next;
    }

    hid_free_enumeration(device);
}

これを実行すると、こういう感じの出力が得られます。
コンパイルのコマンドはmac用です。

~/hidapi $ gcc -I ../hidapi/hidapi ../hidapi/mac/hid.c -framework IOKit -framework CoreFoundation test.cpp -o test 
~/hidapi $ ./test

product_id: Keyboard Backlight
product_id: USB Keyboard           
product_id: Apple T1 Controller
product_id: Apple T1 Controller
product_id: Apple Internal Keyboard / Trackpad
product_id: Joy-Con (L)
product_id: Apple Internal Keyboard / Trackpad
product_id: Apple Internal Keyboard / Trackpad
product_id: Apple Internal Keyboard / Trackpad
product_id: Apple Internal Keyboard / Trackpad
product_id: ~/hidapi $ 

product_id: Joy-Con (L)

しっかりとJoyconを取得できてますね。 他にも色々なものがhidデバイスとして認識されているのがわかります。 (キーボードのバックライトもhidデバイスなのか…??)

Joyconのランプを操作する。

Joyconも認識できたということで、ここから実際に色々とJoyconとやりとりしていきます。 まずは、Joyconのランプを操作するところから初めてみます。

Joyconをパソコンに接続すると、下のgifのようになりますね。 パソコンに接続するとこうなる

これを変更するには、joyconにsubcommandを送信する必要がありますので、早速やっていきます。

SendSubcommand()は、渡されたデバイスにサブコマンドを送る関数です。
これを実行すると、joyconの一番上のランプが光ります。

光った!

色々遊んでみることも。

こんなのとか こんなのとか

こんなのとか 例えば

ボタンの入力を取る。

ボタンについては、ここにフォーマットが掲載されています。

ボタンの情報は、input report の id が 0x3F のものか、Standard input reports から取得できます。 今回は、Standard input reportsから取得しようと思います。 Standard input reportsは、最初にライトのサブコマンドを送信したときに受け取るみたいですので、 今回は特にサブコマンド等送信せず、ただ読み込みます。

    // プロダクトID等を指定して、HID deviceをopenする。そうすると、そのhidデバイスの情報が載ったhid_deviceが帰ってくる。
    hid_device *dev = hid_open(device->vendor_id, device->product_id, device->serial_number);
    // 今開いているデバイスのプロダクト名の取得。
    printf("\nproduct_id: %ls", device->product_string);

    uint8_t data[0x01];
    
    data[0] = 0x01;
    SendSubcommand(dev, 0x30, data, 1, &globalCount);

//            read input report
    uint8_t buff[0x40]; memset(buff, 0x40, size_t(0x40));
    // 読み込むサイズを指定。
    size_t size = 49;
    // buff に input report が入る。
    int ret = hid_read(dev, buff, size);
    printf("\ninput report id: %d\n", *buff);
    // ボタンの押し込みがビットフラグで表現されている。
    printf("input report id: %d\n", buff[5]);

buff には input report が入ります。 このレポートの詳細は、bluetooth_hid_notes.md の Standard input reportを参照してください。 Standard input report - buttons を見ると、

  • buff[3] -> Joycon R
  • buff[4] -> common
  • buff[5] -> Joycon L

となっていることは分かりますね。 つまり、上記のソースコードでは、Joycon L のボタン入力を取得しています。
buff[5]では、押し込まれているボタンが、ビットフラグで表現されます。 buff[5] で 4 と帰ってきたなら、押し込まれているボタンは「右」です。 もし 7 なら、「上」「右」「下」が押し込まれています。

こんな感じで取得できますが、任意のタイミングでボタンの状況を取得したいことがほとんどだと思います。 そんな時におあつらえむきなのが、id 0x3F の input reports です。

これは、ボタンが押されたときにJoyconから送信されるレポートです。 standard input report とは情報の内容と配列が異なっているので、情報を取り出すには異なる実装が必要です。

またhid_set_nonblocking(dev, 1)も必要です。 今まではのhid_read()は、何かしらの input report を受け取るまで待っていました。 しかし、毎回hid_read()したときに、何かしらの input report を受け取るまで処理を止まるというのは色々問題がありますね。 これをいい感じにしてくれるのが、hid_set_nonblocking(dev, 1)です。 これを呼び出すことで、nonblocking mode が on になります。 nonblocking mode では、hid_read()を呼び出した時、毎回値をすぐに返してくれるようになります。 これを利用することで、新しい情報が入っていた場合は、それを読み込むということが可能になります。 では、実装してみます。

上記のコードでは、hid_set_nonblocking(dev, 1)をコメントアウトしています。 これは、実際に実行するとわかると思いますが、実際にhid_set_nonblockingのコメントアウトを外して実行すると、標準出力が止まらなくなります。

これは見辛いので、とりあえずコメントアウトしています。
実行して違いを感じてみてください。

スティック入力

まず、大体はボタンの入力と同じです。
上記のjoycon nonblocking のコードに、スティックの入力を表示するコードを追加してみます。

while (true) 
{
    // input report を受けとる。
    int ret = hid_read(dev, buff, size);
    // input report の id が 0x3F のものに絞る。
    if (*buff != 0x3F)
    {
        continue;
    }
    // input report の id を表示。
    printf("\ninput report id: %d\n", *buff);
    // ボタンのビットビットフィールドを表示。
    printf("button byte 1: %d\n", buff[1]);
    printf("button byte 2: %d\n", buff[1]);
    // スティックの状態を表示。
    printf("stick  byte 3: %d\n", buff[3]);
}

buff[3] にstickの情報が格納されています。
これは、左上から始まって、時計回りに 1, 2, 3, …7(上) となり、真ん中が 8 になります。

振動について

joyconを振動させることもできます。
しかし、残念ながら、振動についてはあまりよくわかっていません。 きちんと実装できたとこがないので、これについては省略しようと思います。
ここに詳細が記されていますが、うーん、よくわからない。

最後に

今回のソースコードは、ほとんどAltseed2と同じものですので、より具体的な方法を知りたい方はこちらを参照してみると良いかもしれません。

「このボタンを取得したい!」 や 「加速度とかジャイロ取りたい!」 という方、

僕はまだ試したことがないので、 ここ を参考に色々試してみてください!。(そしてよければ僕にも教えてください!)

SHARE THIS POST