2022年12月4日日曜日

ESP32からPS5のコントローラーに接続する


ライブラリの進捗

ps5-esp32に出したペアリング対応のpull requestが受理されたので、ps5-esp32を使えばESP32とps5のコントローラーをペアリングできます。

背景

PS5のコントローラー(dualsense)はbluetooth classicで通信します。(BLEじゃないのどうして…)

ESP32向けのPS5コントローラーライブラリはありますが、PS3のコントローラーのようにコントローラー内部で接続対象を指定済み(ペアリング済み)となった想定の処理になっており、ペアリングの処理は記事を書いている時点ではありません。
スマホやPCならPS5のコントローラーと接続(ペアリング)できるのでESP32でも同じようなことが出来るだろうと期待して取り組んだところ、できました。
(ライブラリ化はまだです。)

ESP32でbluetooth classicの装置を検索や、L2capでの接続方法の把握に戸惑ったので、備忘録を兼ねて記事に取り組んだ内容を残します。

使った物


検索

esp32_btのapiを呼んでも良いですがSerialBTの検索で同等のことをしてくれるので、それに任せるのが楽です。

参考: https://github.com/espressif/arduino-esp32/blob/master/libraries/BluetoothSerial/examples/bt_classic_device_discovery/bt_classic_device_discovery.ino

下記のコードで検索と見つかったものの表示を繰り返してくれます。

#include <Arduino.h>
#include <BluetoothSerial.h>

#if !defined(CONFIG_BT_ENABLED) || !defined(CONFIG_BLUEDROID_ENABLED)
#error Bluetooth is not enabled! Please run `make menuconfig` to and enable it
#endif

#if !defined(CONFIG_BT_SPP_ENABLED)
#error Serial Bluetooth not available or not enabled. It is only available for the ESP32 chip.
#endif

BluetoothSerial SerialBT;

#define BT_DISCOVER_TIME 10000
esp_spp_sec_t sec_mask =
ESP_SPP_SEC_NONE; // or ESP_SPP_SEC_ENCRYPT|ESP_SPP_SEC_AUTHENTICATE to
// request pincode confirmation
esp_spp_role_t role = ESP_SPP_ROLE_SLAVE; // or ESP_SPP_ROLE_MASTER

void setup() {
Serial.begin(115200);
if (!SerialBT.begin("ESP32test", true)) {
Serial.println("========== serialBT failed!");
abort();
}
// SerialBT.setPin("1234"); // doesn't seem to change anything
// SerialBT.enableSSP(); // doesn't seem to change anything
}

void loop() {
Serial.println("Starting discoverAsync...");
BTScanResults* btDeviceList =
SerialBT.getScanResults(); // maybe accessing from different threads!
if (SerialBT.discoverAsync([](BTAdvertisedDevice* pDevice) {
// BTAdvertisedDeviceSet*set =
// reinterpret_cast<BTAdvertisedDeviceSet*>(pDevice);
// btDeviceList[pDevice->getAddress()] = * set;
Serial.printf(">>>>>>>>>>>Found a new device asynchronously: %s\n",
pDevice->toString().c_str());
})) {
delay(BT_DISCOVER_TIME);
Serial.print("Stopping discoverAsync... ");
SerialBT.discoverAsyncStop();
Serial.println("discoverAsync stopped");
delay(5000);
if (btDeviceList->getCount() > 0) {
BTAddress addr;
int channel = 0;
Serial.println("Found devices:");
for (int i = 0; i < btDeviceList->getCount(); i++) {
BTAdvertisedDevice* device = btDeviceList->getDevice(i);
Serial.printf(" ----- %s %s %d\n",
device->getAddress().toString().c_str(),
device->getName().c_str(), device->getRSSI());
std::map<int, std::string> channels =
SerialBT.getChannels(device->getAddress());
Serial.printf("scanned for services, found %d\n", channels.size());
for (auto const& entry : channels) {
Serial.printf(" channel %d (%s)\n", entry.first,
entry.second.c_str());
}
if (channels.size() > 0) {
addr = device->getAddress();
channel = channels.begin()->first;
}
}
if (addr) {
Serial.printf("connecting to %s - %d\n", addr.toString().c_str(),
channel);
SerialBT.connect(addr, channel, sec_mask, role);
}
} else {
Serial.println("Didn't find any devices");
}
} else {
Serial.println(
"Error on discoverAsync f.e. not workin after a \"connect\"");
}
}

手持ちのPS5のコントローラーの検索結果がこちら。
コントローラー左上のペアリングボタンとプレステボタンを同時に長押ししてペアリングモードにすると、検索結果に出てきます。
Starting discoverAsync...
>>>>>>>>>>>Found a new device asynchronously: Name: Wireless Controller, Address: 48:18:8d:a0:be:c0, cod: 9480, rssi: -66
Stopping discoverAsync... discoverAsync stopped
Found devices:
----- 48:18:8d:a0:be:c0 Wireless Controller -66
[ 45761][E][BluetoothSerial.cpp:378] esp_spp_cb(): ESP_SPP_DISCOVERY_COMP_EVT failed!, status:1
scanned for services, found 0
Starting discoverAsync...

上記のログからこれらが分かります。
デバイス名: Wireless Controller
アドレス: 48:18:8d:a0:be:c0
cod: 9480
rssi: -66
sppサービス数: 0(bluetoothのシリアル接続は対応していないようです。)

PS5のコントローラーと分かるデバイス名にして欲しかったです。

検索して判明したcodはclass of deviceの略であり、デバイスの役割が分かります。
bluetooth sigが子飼している資料(Assigned Numbers (PDF))の「2.8 Class of Device」を参照するとcodから役割を読み取れます。
9480(int)はbitに変換すると0b0010_0101_0000_1000です。
各ビットの役割を読み解くと、このようになります。
0b..1._...._...._.... -> Limited discoverable mode
0b...0_0101_...._.... -> Major device class: peripheral
0b...._...._0000_10.. -> peripheral -> Minor device class: Game pad

ということで、見つかったPS5のコントローラーのcodは「限定的に発見可能除隊で動いているゲームパッド」を意味すると分かりました。

検索でゲームパッドであることは確認できますが、PS5のコントローラーであることの確証はできないようです。
判別方法をご存知でしたら、コメントなどで教えていただけると嬉しいです。

L2capで繋ぐ: bluedroidのstackを利用

ゲームパッドはHID(人による入力装置)に分類されます。
esp-idfでのbluetooth classicのHIDのサンプルコードは自分が探した範囲ではマウスのslaveになれるものしか見つけられなかったので、HIDの親機として使うにはHIDで利用されるL2capで通信を組み立てる必要があります。
hidのホスト側として動いてくれそうなesp_hidh_api.hを見つけはしましたが、使い方や利用例が見つからないので、参考情報の多いL2capを今回は利用しました。

ESP32のbluetooth classic実装はarduino環境でビルド済みの機能が呼び出し可能状態ではあるもののesp32arduinoに必要なヘッダファイルが含まれていないので、自分でプロジェクトにコピーします。

bluetooth classic実装はbluedroidというandroid4と5で使われていたライブラリが取り入れられているので、それを利用します。
コピー対象 bluedroidのstack
https://github.com/espressif/esp-idf/tree/495d35949d50033ebcb89def98f107aa267388c0/components/bt/host/bluedroid/stack/include/stack

ESP32からキーボードに接続した例esp32向けps3ライブラリesp32向けps5ライブラリを参考にしてL2capの利用に必要な下記の6ファイルをプロジェクトにコピーしました。
src
|- osi
| |- allocator.h
|- stack
|- bt_types.h
|- btm_api.h
|- hcidefs.h
|- l2c_api.h
|- l2cdefs.h


コピーしたファイルはgithubにも上げています。
https://github.com/asukiaaa/esp32-ps5-controller-connect-practice/tree/master/src/

上記のbluedroid関連ファイルを配置した上で下記の3ファイルを作成します。
長いファイルがあるのとgithubの方が見やすいと思うので、中身は表示せずリンクを貼り概要を説明します。

src
|- hid_l2cap.c
|- hid_l2cap.h
|- main.cpp


hid_l2cap.cpp
PS5への接続処理を行うl2capの実装です。
必要な関数を定義してtL2CAP_APPL_INFO dyn_infoとしてまとめ、hid_l2cap_initializeが呼ばれた際にL2CA_Registerで登録してL2capの動作をさせます。
ESP32からキーボードに接続した例ps5ライブラリの実装を参考にしました。

hid_l2cap.h
main.cppで呼びたいhid_l2capの関数を定義します。

main.cpp
hid_l2capを呼び出してPS5のコントローラーへの接続を実行します。
接続対象となるコントローラーのアドレスもmainからhid_l2capに渡します。
試す場合は持っているPS5のコントローラーのアドレスにTARGET_BT_ADDRを書き換えてください。
l2capの処理の他に、esp_bt_gap_set_scan_modeを量して外部から接続可能なbluetooth装置として動かしています。
というのも、PS5のコントローラーはペアリング時はESP32から接続するのですが、再接続時はコントローラーから接続が始まるようなので、再接続の実現にはESP32をbluetooth機器として外部から接続可能な状態にする必要がありました。

上記のファイルを配置した状態でビルドして実行すると、指定したアドレスのコントローラーとペアリングや再接続が可能です。
 

ログ
実行の度に変わるcid以外は、ペアリング時も再接続時も同じログです。

connecting
status 2
connecting
hid_l2cap_connect_ind_cb
[hid_l2cap_config_ind_cback] l2cap_cid: 0x40
p_cfg->result: 0
p_cfg->mtu_present: 1
p_cfg->mtu: 672
[hid_l2cap_config_cfm_cback] l2cap_cid: 0x40
p_cfg->result: 0
hid_l2cap_connect_ind_cb
[hid_l2cap_config_ind_cback] l2cap_cid: 0x42
p_cfg->result: 0
p_cfg->mtu_present: 1
p_cfg->mtu: 672
[hid_l2cap_config_cfm_cback] l2cap_cid: 0x42
p_cfg->result: 0
Hid Connected
[hid_l2cap_data_ind_cback] l2cap_cid: 0x42
event=4352 len=11 offset=9 layer_specific=0
data=a1 01 81 81 81 7d 08 00 00 00 00

終わり

PS5のコントローラーとESP32のペアリングと再接続ができました。
既存のライブラリに変更依頼をするか自分でライブラリを作るかして、ESP32でPS5のコントローラーを手軽に使えるようにしたいです。

参考

ESP32からキーボードに接続した例
esp32向けps3ライブラリ
esp32向けps5ライブラリ
Assigned Numbers (PDF)
https://github.com/asukiaaa/esp32-ps5-controller-connect-practice
https://github.com/espressif/esp-idf/tree/495d35949d50033ebcb89def98f107aa267388c0/components/bt/host/bluedroid/stack/include/stack

変更履歴

2022.12.07 ライブラリ化の進捗を追加しました。

0 件のコメント :