はじめに
気の迷いからSwitch2のプロコンを買った(???)はいいものの、肝心の本体がないから使い道がない...(当たり前体操)
そこで今回は「Nintendo Switch 2 Proコントローラー(以下プロコン2)」をESP32と接続し、ボタンやスティックの入力を取得してみましたので共有したいと思います。
ちなみにSwitch2のプロコンは初代Switchやサードパーティ製のコントローラーとも仕様が違うらしく、普通にはPCと無線接続できないそうです。マジで使い道がないぞ
(Steamで有線接続には対応したそうです)
ざっくり解説
プロコン2はBLE(Bluetooth Low Energy)と呼ばれる規格で通信しています。
そして世の中にはBluetooth入力デバイス向けのHIDと呼ばれるプロファイルが存在しているのですが...プロコン2はそのプロファイルを採用せず、独自の規格で通信しています。その為PCや初代Switchからはプロコン2を見つけることが出来ず、接続すらできません。
そこで今回は、プロコン2独自の規格に合わせたデバイスの発見方法と接続、その後のコントローラー入力の解釈などができるESP32向けのプログラムを作成しました。
(プロコン2の仕様はまだ不明瞭な部分があり全てのプロコン2と通信できるかは分かりません。)
使用したツール・開発環境
- Nintendo Switch 2 Proコントローラー

(なぜ買ってしまったんだ...?) - M5Stack AtomS3(ESP32系でbluetoothを搭載していればいけると思います)
- PlatformIO
- フレームワーク : Arduino
- nRF Connect(スマホアプリ)
参考にしたリポジトリ
今回、以下のリポジトリを参考にプログラムを作成しました。
こちらはmacOSからプロコン2に無線接続し、キーボード入力に置き換えるプログラムです。先人の知恵は偉大なり。
コード
※BLEの通信部分はサンプルコードを元に作成しました。
#include <Arduino.h>
#include <NimBLEDevice.h>
// -- SW2ProController UUIDs --
static const char* serviceUUID = "AB7DE9BE-89FE-49AD-828F-118F09DF7FD0";
static const char* buttonNotifyCharUUID = "7492866C-EC3E-4619-8258-32755FFCC0F8";
static const NimBLEAdvertisedDevice *advDevice;
NimBLEClient *pClient = nullptr;
static bool doConnect = false;
static uint32_t scanTimeMs = 5000;
// コントローラー情報を格納する構造体
struct ControllerState
{
bool B, A, Y, X, R, ZR, Plus, RS;
bool dDown, dRight, dLeft, dUp, L, ZL, Minus, LS;
bool Home, Capture, GL, GR, Chat;
uint16_t lx, ly, rx, ry;
} cState;
volatile bool stateChanged = false;
// -- Notification Callback --
void notifyCallback(NimBLERemoteCharacteristic *pRemoteCharacteristic, uint8_t *pData, size_t length, bool isNotify)
{
if (length < 11)
return;
// ボタン状態の保存
cState.B = pData[2] & 0x01; // Bボタン
cState.A = pData[2] & 0x02; // Aボタン
cState.Y = pData[2] & 0x04; // Yボタン
cState.X = pData[2] & 0x08; // Xボタン
cState.R = pData[2] & 0x10; // Rボタン
cState.ZR = pData[2] & 0x20; // ZRボタン
cState.Plus = pData[2] & 0x40; // プラスボタン
cState.RS = pData[2] & 0x80; // Rスティック押し込み
cState.dDown = pData[3] & 0x01; // 十字下
cState.dRight = pData[3] & 0x02; // 十字右
cState.dLeft = pData[3] & 0x04; // 十字左
cState.dUp = pData[3] & 0x08; // 十字上
cState.L = pData[3] & 0x10; // Lボタン
cState.ZL = pData[3] & 0x20; //ZLボタン
cState.Minus = pData[3] & 0x40; //マイナスボタン
cState.LS = pData[3] & 0x80; // Lスティック押し込み
cState.Home = pData[4] & 0x01; // ホームボタン
cState.Capture = pData[4] &0x02; // キャプチャーボタン
cState.GR = pData[4] &0x04; // GRボタン
cState.GL = pData[4] &0x08; //GLボタン
cState.Chat = pData[4] &0x10; // Cボタン
cState.lx = pData[5] | ((pData[6] & 0x0F) << 8); // Lスティック:X
cState.ly = ((pData[6] & 0xF0) >> 4) | (pData[7] << 4); // Lスティック:Y
cState.rx = pData[8] | ((pData[9] & 0x0F) << 8); // Rスティック:X
cState.ry = ((pData[9] & 0xF0) >> 4) | (pData[10] << 4); //Rスティック:Y
stateChanged = true;
}
// -- BLE Client Callbacks --
class ClientCallbacks : public NimBLEClientCallbacks
{
void onConnect(NimBLEClient *pClient) override
{
Serial.printf("Connected to: %s. Starting service discovery...\n", pClient->getPeerAddress().toString().c_str());
}
void onDisconnect(NimBLEClient *pClient, int reason) override
{
NimBLEDevice::getScan()->start(scanTimeMs, false, true);
}
} clientCallbacks;
bool connectToServer()
{
Serial.print("Connecting to: ");
Serial.println(advDevice->getAddress().toString().c_str());
if (NimBLEDevice::getCreatedClientCount())
{
pClient = NimBLEDevice::getClientByPeerAddress(advDevice->getAddress());
if (pClient)
{
if (!pClient->connect(advDevice, false))
{
Serial.println("Reconnect failed");
return false;
}
Serial.println("Reconnected client");
}
else
{
pClient = NimBLEDevice::getDisconnectedClient();
}
}
if (!pClient)
{
pClient = NimBLEDevice::createClient();
pClient->setClientCallbacks(&clientCallbacks, false);
if (!pClient->connect(advDevice))
{
Serial.println("Failed to connect");
NimBLEDevice::deleteClient(pClient);
return false;
}
}
else if (!pClient->isConnected())
{
if (!pClient->connect(advDevice))
{
Serial.println("Failed to connect");
return false;
}
}
NimBLERemoteService *pRemoteService = pClient->getService(serviceUUID);
if (pRemoteService == nullptr)
{
Serial.println("Failed to find service UUID");
pClient->disconnect();
return false;
}
NimBLERemoteCharacteristic *pRemoteCharacteristic = pRemoteService->getCharacteristic(buttonNotifyCharUUID);
if (pRemoteCharacteristic == nullptr)
{
Serial.println("Failed to find char UUID");
pClient->disconnect();
return false;
}
if (pRemoteCharacteristic->canNotify())
{
pRemoteCharacteristic->subscribe(true, notifyCallback);
Serial.println("Notify Subscribed. Ready to receive data.");
}
return true;
}
// -- BLE Scan Callbacks --
class ScanCallbacks : public NimBLEScanCallbacks
{
void onResult(const NimBLEAdvertisedDevice *advertisedDevice) override
{
std::string mData = advertisedDevice->getManufacturerData(); //ManufactureDataを元に接続
size_t len = mData.length();
if (len < 6)
return;
const uint8_t *pData = (const uint8_t *)mData.data();
bool isNintendo = (pData[0] == 0x53 && pData[1] == 0x05); // CompanyIDを確認
if (isNintendo)
{
bool isTarget = false;
for (size_t i = 2; i < len - 1; i++)
{
if ((pData[i] == 0x7E && pData[i + 1] == 0x05) ||
(pData[i] == 0x69 && pData[i + 1] == 0x20)) // 7E05 または 6920 の並びでプロコン2を特定
{
isTarget = true;
break;
}
}
if (isTarget)
{
Serial.printf("Found Switch 2 Pro Controller (by Manufacturer Data): %s [%s]\n",
advertisedDevice->getName().c_str(),
advertisedDevice->getAddress().toString().c_str());
NimBLEDevice::getScan()->stop();
advDevice = advertisedDevice;
doConnect = true;
}
}
}
} scanCallbacks;
// ボタン状態を使用する関数
void useState()
{
String pushButtons = "";
if (cState.A)
pushButtons += "A ";
if (cState.B)
pushButtons += "B ";
if (cState.X)
pushButtons += "X ";
if (cState.Y)
pushButtons += "Y ";
if (cState.L)
pushButtons += "L ";
if (cState.R)
pushButtons += "R ";
if (cState.ZL)
pushButtons += "ZL ";
if (cState.ZR)
pushButtons += "ZR ";
if (cState.GL)
pushButtons += "GL ";
if (cState.GR)
pushButtons += "GR ";
if (cState.Minus)
pushButtons += "Minus ";
if (cState.Plus)
pushButtons += "Plus ";
if (cState.Home)
pushButtons += "Home ";
if (cState.Capture)
pushButtons += "Chapture ";
if (cState.Chat)
pushButtons += "Chat ";
if (cState.LS)
pushButtons += "L-Stick ";
if (cState.RS)
pushButtons += "R-Stick ";
if (cState.dUp)
pushButtons += "Up ";
if (cState.dDown)
pushButtons += "Down ";
if (cState.dLeft)
pushButtons += "Left ";
if (cState.dRight)
pushButtons += "Right";
if (pushButtons == "") return;
Serial.println(pushButtons);
}
void setup()
{
Serial.begin(115200);
Serial.printf("Starting NimBLE Client\n");
/** Initialize NimBLE and set the device name */
NimBLEDevice::init("SW2ProController-Client");
/** Optional: set the transmit power */
NimBLEDevice::setPower(3); /** 3dbm */
NimBLEScan *pScan = NimBLEDevice::getScan();
/** Set the callbacks to call when scan events occur, no duplicates */
pScan->setScanCallbacks(&scanCallbacks, false);
/** Set scan interval (how often) and window (how long) in milliseconds */
pScan->setInterval(100);
pScan->setWindow(100);
/**
* Active scan will gather scan response data from advertisers
* but will use more energy from both devices
*/
pScan->setActiveScan(true);
/** Start scanning for advertisers */
pScan->start(scanTimeMs);
Serial.printf("Scan Start.\n");
}
void loop()
{
delay(10);
// BLE接続確認
if (doConnect)
{
doConnect = false;
if (connectToServer())
{
Serial.printf("Success!\n");
}
else
{
Serial.printf("Failed to connect, restarting scan\n");
NimBLEDevice::getScan()->start(scanTimeMs, false, true);
}
}
// ボタンの変更に反応して実行
if (stateChanged)
{
stateChanged = false;
useState();
}
}
結果
今回のプログラムでは、押されているボタンがシリアルモニタに表示されるようになっています。(スティック座標の表示は無し)

引っかかったポイント
アドバタイズの特定
参考元のリポジトリと同じ方法でアドバタイズを特定しようとしましたが、なぜか上手くいきませんでした。データの配列で特定するという方法がもしかすると周囲の何らかの機器と一致したのかも?と推測。
そこで、色々調べてみるとBLEのManufacturerDataにはCompanyIDという企業識別IDが含まれているようだったので、先に任天堂の機器に絞り込むことに。
すると上手くいきました。
原因が本当にそうだったかは分かりませんが、とりあえず先に企業を特定しておくと安心かもしれません。
void onResult(const NimBLEAdvertisedDevice *advertiÏsedDevice) override
{
std::string mData = advertisedDevice->getManufacturerData(); //ManufactureDataを元に接続
size_t len = mData.length();
if (len < 6)
return;
const uint8_t *pData = (const uint8_t *)mData.data();
//先に企業IDが任天堂のものか確認した
bool isNintendo = (pData[0] == 0x53 && pData[1] == 0x05); // CompanyIDを確認
if (isNintendo)
{
// 以下は参考元と同じ
bool isTarget = false;
for (size_t i = 2; i < len - 1; i++)
{
if ((pData[i] == 0x7E && pData[i + 1] == 0x05) ||
(pData[i] == 0x69 && pData[i + 1] == 0x20)) // 7E05 または 6920 の並びでプロコン2を特定
{
isTarget = true;
break;
}
}
// 続く...
UUIDの不一致
先のリポジトリではNotifyのCharactaristic UUIDを7492866c-ec3e-4619-8258-32755ffcc0f9としていたのですが、私の手持ちのプロコン2では7492866c-ec3e-4619-8258-32755ffcc0f8でした。
これが個体差なのか地域差なのか、はたまたファームウェアの変更なのかは分かりませんが、もしかするとプロコン2の個体によって少し異なる可能性があります。
私はUUIDを「nRF Connect(iOS,Android,PC)」というアプリケーションを使って確認しました。上手く動かない場合は、7492から始まるCharactaristicを見つけてNotifyになっているか確認してもらうと良いと思います。

インジケーターの謎
なぜか、接続してもインジケーターが待機状態から変わりません。おそらくこちらから何かWriteして初めて接続状態になるという仕様なんだと思います。
ただ、この状態でも普通にコントローラーの入力は取れるので問題はないです。(強いていうなら接続ボタンに手が当たるとすぐに接続が切れるという程度)
終わりに
GithubにてPIOプロジェクトをそのまま公開しましたので良ければ活用ください。
わざわざプロコン2である必要があるかはさておき、私もロボットのコントローラーに活用しようかな...
