エンジニアの大場です。

技術調査で任天堂SwitchのJoy-ConをUnityで扱う機会がありました。
まず試してみたライブラリはこちらです。
使い方は岩井 寿樹さんのブログを参考にしました。
https://tech.mof-mof.co.jp/blog/unity-joycon-introduce/
このライブラリでは加速度やボタン入力などは取れるのですがバッテリー情報が取得できません。
今回はこれを拡張してバッテリー情報を取得できないか検証しました。

1. 調査

Joy-Con ToolkitというフリーソフトがあってJoy-Conの色を変えたりIRカメラの写真を保存できたりします。
他にもいろいろできるみたいなので気になった方は調べてみてください。

このソフトはバッテリーの取得ができています。
GitHubのリポジトリを見てみるとC++で作られているみたいなのでバッテリー取得しているロジックを上記で紹介したJoyconLib(UnityのライブラリC#)に移植しようと思いました。

※ハードウェアは専門外でなんとなく理解した程度の実装なのでご注意を。

2. 移植

2-1. Joy-Con Toolkit (C++)

移植するにあたって参考にしたバッテリー実装部分です。

jctool.cpp

int get_battery(u8* test_buf) {
    int res;
    u8 buf[49];
    int error_reading = 0;
    while (1) {
        memset(buf, 0, sizeof(buf));
        auto hdr = (brcm_hdr *)buf;
        auto pkt = (brcm_cmd_01 *)(hdr + 1);
        hdr->cmd = 1;
        hdr->timer = timming_byte & 0xF;
        timming_byte++;
        pkt->subcmd = 0x50;
        res = hid_write(handle, buf, sizeof(buf));
        int retries = 0;
        while (1) {
            res = hid_read_timeout(handle, buf, sizeof(buf), 64);
            if (*(u16*)&buf[0xD] == 0x50D0)
                goto check_result;

            retries++;
            if (retries > 8 || res == 0)
                break;
        }
        error_reading++;
        if (error_reading > 20)
            break;
    }
    check_result:
    test_buf[0] = buf[0x2];
    test_buf[1] = buf[0xF];
    test_buf[2] = buf[0x10];

    return 0;
}

FromJoy.h

private: System::Void update_battery() {
    unsigned char batt_info[3];
    memset(batt_info, 0, sizeof(batt_info));

    get_battery(batt_info);

    int batt_percent = 0;
    int batt = ((u8)batt_info[0] & 0xF0) >> 4;
    
    // Calculate aproximate battery percent from regulated voltage
    u16 batt_volt = (u8)batt_info[1] + ((u8)batt_info[2] << 8);
    if (batt_volt < 0x560)
        batt_percent = 1;
    else if (batt_volt > 0x55F && batt_volt < 0x5A0) {
        batt_percent = ((batt_volt - 0x60) & 0xFF) / 7.0f + 1;
    }
    else if (batt_volt > 0x59F && batt_volt < 0x5E0) {
        batt_percent = ((batt_volt - 0xA0) & 0xFF) / 2.625f + 11;
    }
    else if (batt_volt > 0x5DF && batt_volt < 0x618) {
        batt_percent = (batt_volt - 0x5E0) / 1.8965f + 36;
    }
    else if (batt_volt > 0x617 && batt_volt < 0x658) {
        batt_percent = ((batt_volt - 0x18) & 0xFF) / 1.8529f + 66;
    }
    else if (batt_volt > 0x657)
        batt_percent = 100;

    this->toolStripLabel_batt->Text = String::Format(" {0:f2}V - {1:D}%", (batt_volt * 2.5) / 1000, batt_percent);

    // Update Battery icon from input report value.
    switch (batt) {
        case 0:
            this->toolStripBtn_batt->Image =
                (cli::safe_cast<System::Drawing::Bitmap^>(resources->GetObject(L"batt_0")));
            this->toolStripBtn_batt->ToolTipText = L"Empty\n\nDisconnected?";
            break;
        case 1:
            this->toolStripBtn_batt->Image =
                (cli::safe_cast<System::Drawing::Bitmap^>(resources->GetObject(L"batt_0_chr")));
            this->toolStripBtn_batt->ToolTipText = L"Empty, Charging.";
            break;
        case 2:
            this->toolStripBtn_batt->Image =
                (cli::safe_cast<System::Drawing::Bitmap^>(resources->GetObject(L"batt_25")));
            this->toolStripBtn_batt->ToolTipText = L"Low\n\nPlease charge your device!";
            break;
        case 3:
            this->toolStripBtn_batt->Image =
                (cli::safe_cast<System::Drawing::Bitmap^>(resources->GetObject(L"batt_25_chr")));
            this->toolStripBtn_batt->ToolTipText = L"Low\n\nCharging";
            break;
        case 4:
            this->toolStripBtn_batt->Image =
                (cli::safe_cast<System::Drawing::Bitmap^>(resources->GetObject(L"batt_50")));
            this->toolStripBtn_batt->ToolTipText = L"Medium";
            break;
        case 5:
            this->toolStripBtn_batt->Image =
                (cli::safe_cast<System::Drawing::Bitmap^>(resources->GetObject(L"batt_50_chr")));
            this->toolStripBtn_batt->ToolTipText = L"Medium\n\nCharging";
            break;
        case 6:
            this->toolStripBtn_batt->Image =
                (cli::safe_cast<System::Drawing::Bitmap^>(resources->GetObject(L"batt_75")));
            this->toolStripBtn_batt->ToolTipText = L"Good";
            break;
        case 7:
            this->toolStripBtn_batt->Image =
                (cli::safe_cast<System::Drawing::Bitmap^>(resources->GetObject(L"batt_75_chr")));
            this->toolStripBtn_batt->ToolTipText = L"Good\n\nCharging";
            break;
        case 8:
            this->toolStripBtn_batt->Image =
                (cli::safe_cast<System::Drawing::Bitmap^>(resources->GetObject(L"batt_100")));
            this->toolStripBtn_batt->ToolTipText = L"Full";
            break;
        case 9:
            this->toolStripBtn_batt->Image =
                (cli::safe_cast<System::Drawing::Bitmap^>(resources->GetObject(L"batt_100_chr")));
            this->toolStripBtn_batt->ToolTipText = L"Almost full\n\nCharging";
            break;
    }

}

jctool.cppの重要な部分をピックアップすると

pkt->subcmd = 0x50; 

このSubcommandをHIDデバイス(Joy-Con)に送った後にHIDデバイスからbyteをReadすることでバッテリーを計算するのに必要なデータを取得しているみたいです。

if (*(u16*)&buf[0xD] == 0x50D0)

またReadして得られたbyte(16bit)の値が0x50D0ならバッテリーを計算するの必要なデータみたいです。(ここら辺ちょっと曖昧です)

test_buf[0] = buf[0x2];
test_buf[1] = buf[0xF];
test_buf[2] = buf[0x10];

あとは必要なデータをここでbyte配列に代入していそうです。

FromJoy.hは得られた値を使ってバッテリーのパーセントを計算しています。 Calculate aproximate battery percent from regulated voltage と書いてある通り電圧調整から疑似的にバッテリーを計算しているみたいです。

アルゴリズムに関してはそこまで難しいことしてないので説明は省きます。

2-2. JoyconLib (Unity用のライブラリC#)

Joycon.csのvoid Poll()のwhileでループしているところに以下の一文を加えJoy-Con Toolkitと同様にSubcommandを送ります。

Subcommand(0x50, new byte[] { 0x0 }, 1);

void Update()でQueueからReadした値(report_buf)を取り出しているので、ここにreport_bufからバッテリーを計算する以下の処理を追加します。

private void UpdateBattery(byte[] test_buf)
{
    if (!imu_enabled | state < state_.IMU_DATA_OK)
        return;

    byte[] buf = test_buf;
    byte[] batt_info = new byte[3];

    if (buf[0xD] == 0xD0)
    {
        batt_info[0] = buf[0x2];
        batt_info[1] = buf[0xF];
        batt_info[2] = buf[0x10];

        int batt = ((byte)batt_info[0] & 0xF0) >> 4;
        float batt_percent = 0;

        // Calculate aproximate battery percent from regulated voltage
        int batt_volt = (byte)batt_info[1] + ((byte)batt_info[2] << 8);
        if (batt_volt < 0x560)
        {
            batt_percent = 1;
        }
        else if (batt_volt > 0x55F && batt_volt < 0x5A0)
        {
            batt_percent = ((batt_volt - 0x60) & 0xFF) / 7.0f + 1;
        }
        else if (batt_volt > 0x59F && batt_volt < 0x5E0)
        {
            batt_percent = ((batt_volt - 0xA0) & 0xFF) / 2.625f + 11;
        }
        else if (batt_volt > 0x5DF && batt_volt < 0x618)
        {
            batt_percent = (batt_volt - 0x5E0) / 1.8965f + 36;
        }
        else if (batt_volt > 0x617 && batt_volt < 0x658)
        {
            batt_percent = ((batt_volt - 0x18) & 0xFF) / 1.8529f + 66;
        }
        else if (batt_volt > 0x657 && batt_volt < 0x2710)
        {
            batt_percent = 100;
        }

        if (batt_percent == 1)
        {
            if (batt == 0 || batt == 1)
            {
                battery = (int)batt_percent;
            }
        }
        else if (batt_percent > 1 && batt_percent <= 25)
        {
            if (batt == 2 || batt == 3)
            {
                battery = (int)batt_percent;
            }
        }
        else if (batt_percent > 25 && batt_percent <= 50)
        {
            if (batt == 4 | batt == 5)
            {
                battery = (int)batt_percent;
            }
        }
        else if (batt_percent > 50 && batt_percent <= 75)
        {
            if (batt == 6 | batt == 7)
            {
                battery = (int)batt_percent;
            }
        }
        else if (batt_percent > 75 && batt_percent <= 100)
        {
            if (batt == 8 | batt == 9)
            {
                battery = (int)batt_percent;
            }
        }

        if (batt % 2 == 1)
        {
            isCharge = true;
        }
        else
        {
            isCharge = false;
        }
    }
}

基本やっていることはFormJoy.hと同じです。

bool isCharge;
int battery;

フィールド変数は上記の型です。

また

if (buf[0xD] == 0xD0) 

このif文でバッテリー情報かの判定をしています。Joy-Con Toolkitでは得られたbyteの値が16bitで0x50D0でしたが、こちらは8bitなので0xD0になっています。
ここら辺詳しい方ご教授していただきたいです。

3. 最後に

さらに詳しくJoy-Conを使いたい人はこのリポジトリを見るといいかもしれないです。

https://github.com/dekuNukem/Nintendo_Switch_Reverse_Engineering