ESP32で自作マウス〜その5〜
前回のあらすじ
前回はESP-IDFを導入して、電源管理などができるようにしました。
今回はGihubのレポジトリごとESP-IDFのプロジェクトに移行しつつ、機能の改善をしていこうと思います。Githubのコミットはこれです。
レポジトリの移行
前回の記事を書いた時点では、「フォルダの.gitさえ移動すれば良いんじゃないか?」とか軽く考えてたんですが、結構苦労しました。
何が大変かと言うと、BLEのライブラリを使うのにArduinoのライブラリがあるとかなり実装が楽なんですが、Arduinoのライブラリの中からBLEだけ抜き出そうと思っても、依存関係のせいで上手く行かず、結局arduino-esp32というコンポーネントを追加して、完全にArduinoの環境をESP-IDFに持ってこなくてはいけなくなりました。
結果必要になったことはGithubのREADME.mdに書いた手順のとおりなんですけど、もう少し詳しく書きます。
ESP-IDFの最新バージョンは5.3なんですが、非常に遺憾なことにarduino-esp32はESP-IDFv5.1にしか対応していません!なのでこれを一致させるためにESP-IDFのディレクトリでGitコマンドを使ってv5.1.4に戻したうえで、install.shの実行等を済ませます。更に、arduino-esp32を使うのにFreeRTOS_HZを100じゃなくて1000にしろって言われたりするのでちゃんと変更して、初めてビルドが再び通るようになります。
あと、追加で注意が必要なこととして、Arduino IDEではsetup関数とloop関数が自動で呼び出されていましたが、ESP-IDFではapp_mainを使用しているので、以下のように関数の定義を冒頭を変更する必要があります。
extern "C" void app_main()
{
// Initialize Arduino
initArduino();
...
それともちろんmain.cppの先頭に#include <Arduino.h>を含める必要があります。更に、CMakelists.txtのSRCについても落とし穴があって、ヘッダーファイルをインクルードするんなら、INCLUDE_DIRSにフォルダーを追加すればいいと思うじゃないですか?でもこれだけだと大量のエラーがldから吐かれます。なので、ライブラリのために書いたファイルも以下のようにSRCに追加する必要があります
idf_component_register(SRCS "main.cpp" "./libraries/BLE_Mouse/BleConnectionStatus.cpp" "./libraries/BLE_Mouse/BleMouse.cpp" "./libraries/Madgwick/MadgwickAHRS.cpp" "./libraries/RotaryEncoder/RotaryEncoder.cpp"
以上でESP-IDFでArduinoのライブラリが使えるようになり、開発環境が整いました。というわけで早速マルチペアリングを実装していきましょう!
マルチペアリング実装
実装の難易度
一言で感想を述べるなら「難易度激ムズ」です。なにが難しいってそもそも情報が皆無なわけです。確かにペアリングしてる記事はありますけど、マルチペアリングとなると事情が違います。OS自作もそうですが、プログラミングにおいて最も大きな壁は情報が少ないAPIなんですが、ESP-IDFも公式の簡素な(見にくい)ドキュメントがあるだけで、あとは断片的な情報を探しながら書くしかありません。
処理の大まかな流れ
これを考えるのが結構大変だったんですが、こんな感じになってます。
まずはBLEをパスコード付きで起動
一台目のデバイスを接続し、ペアリング(ボンディング)
タッチ割り込み(変更予定)でデバイス番号を変更
切断後2台目のデバイスとペアリング(ボンディング)
この時点でこれ以上のペアリングをできないよう設定
移行接続を切断すると、ホワイトリストを使用して自動接続
そもそもボンディングってなんじゃいと思うわけですが、これはお互いの情報を保持することで、再起動しても自動接続できるようにすることを言います。そしてホワイトリストもなんとなくは分かると思いますが、これに登録したデバイスのみ接続するようになります。つまり、簡潔にまとめるとボンディング+ホワイトリストでマルチペアリングを実現しているわけです。
ではなぜこんな回りくどいやり方をしているかといいますと、ホントは僕もボンディング情報を編集して、接続先を変えたかったんです。でも、ESP32のAPI上ボンディング情報にユーザー側がアクセスできないようになっていて、これは諦めるしかありませんでした(一回APIのコード読んで無理やりアクセスできるよう改変しようか迷ったけどやめた)。
というわけでもう少し詳しい解説を書いていきます。
BLEをパスコード付きで起動
これはまあもとのコードから大きく変えては無いんですが、ちょっとやってみたくて認証にパスコードをつけてみました。コードはこうなってます:
int passkey = 999983;
pSecurity->setAuthenticationMode(ESP_LE_AUTH_BOND);
pSecurity->setCapability(ESP_IO_CAP_OUT);
pSecurity->setKeySize(16);
pSecurity->setInitEncryptionKey(ESP_BLE_ENC_KEY_MASK | ESP_BLE_ID_KEY_MASK);
esp_ble_gap_set_security_param(ESP_BLE_SM_SET_STATIC_PASSKEY, &passkey, sizeof(uint32_t));
もとは2行目だけだったところに、パスコード等のセキュリティ強化のためのコードを加えてます。ここは別に言うことはないですね。
一台目のペアリング後
ここで少し問題がありまして、実は今回の実装では完全にマルチペアリングを再現できておりません!なぜかと言うと、2台目のペアリング時は1台目とボンディングされた状態なので、1台目との接続を切断したくても、ESP側からはできません。なので1台目のデバイス側でBluetoothを切った状態で接続することでごまかすことにしました()非常に不本意ですが、一回ペアリングすれば後述する処理で上手く行くので我慢です。
2台目のペアリング
というわけでとりあえず2台目をペアリングしてやるんですが、Bluetoothを切っているとはいえ、一応仮に切り忘れたとき用の処理と、3台以上繋がないようにする処理が必要なので、以下のようなコードを書いています。
if (!devices_loaded) {
ESP_LOGI(LOG_TAG, "%d", esp_ble_get_bond_device_num());
ESP_LOGI(LOG_TAG, "Loading bonded devices");
ESP_ERROR_CHECK(esp_ble_get_bond_device_list(&list_size, bonded_devices));
devices_loaded = true;
}
char macStr[18];
sprintf(macStr, "%02X:%02X:%02X:%02X:%02X:%02X",
param->connect.remote_bda[0], param->connect.remote_bda[1], param->connect.remote_bda[2],
param->connect.remote_bda[3], param->connect.remote_bda[4], param->connect.remote_bda[5]);
if (!device_full) {
if (device_num == 1 && memcmp(param->connect.remote_bda, bonded_devices[0].bd_addr, sizeof(param->connect.remote_bda)) != 0) {
ESP_LOGI(LOG_TAG, "Bonding device with MAC address: %18s", macStr);
pServer->getAdvertising()->stop();
if (esp_ble_get_bond_device_num() == 2) {
device_full = true;
}
} else if (device_num==1) {
ESP_LOGI(LOG_TAG, "Disconnect Bonded device");
esp_ble_gap_disconnect(param->connect.remote_bda);
}
} else {
ESP_LOGI(LOG_TAG, "Connected to device with MAC address: %18s", macStr);
pServer->getAdvertising()->stop();
}
変数の定義とかふっとばしてるせいで正確にはわからないと思いますが、if分岐見てもらえればなんとな〜く前述した処理をやっていることが伝わると思います。
タッチ割り込み
これはあくまで仮の処理なので、後日ちゃんとGPIOピンの割り込みに変更するつもりなんですけど、今回はタッチセンサーで割り込みを発生させてデバイスを切り替えます。
もうコード書くまでもなく普通にタッチ割り込みなのでそこは省略するんですが、一点だけ注意点があって、割り込の中にprintやdelay系の関数を書くと、クラッシュすることがあります。誠に遺憾なんですが、割り込みが長引くとタイムアウトを起こしてクラッシュすることがあるので気をつけましょう。この手のやつはデバッグ手段が無いので頭に入れておかないと死にます。
ホワイトリストによる自動接続(切り替え)
最初僕はこれの存在を知らず、一度マルチペアリングの実装を諦めかけたんですが、ESP32 Forumでふと見たところで知って、公式ドキュメント見たりしながら実装しました。
これを使うと、ホワイトリストに登録したデバイス以外を無視できるようになるので、ボンディングされたデバイスが2台あっても片方だけに接続できるので、擬似的にマルチペアリングを実装できるのです!(別に本来どうやって実装するのかも知りませんが)
コードは以下の様になってます。
if (device_full) {
if (memcmp(param->connect.remote_bda, bonded_devices[device_num].bd_addr, sizeof(param->connect.remote_bda)) != 0) {
esp_ble_gap_update_whitelist(false, param->connect.remote_bda, BLE_WL_ADDR_TYPE_PUBLIC);
esp_ble_gap_update_whitelist(true, bonded_devices[device_num].bd_addr, BLE_WL_ADDR_TYPE_PUBLIC);
} else {
uint16_t len;
esp_ble_gap_get_whitelist_size(&len);
if (len == 0) {
esp_ble_gap_update_whitelist(true, param->connect.remote_bda, BLE_WL_ADDR_TYPE_PUBLIC);
}
}
}
device_fullはペアリングしたデバイスが2台になるとtrueになるので、ここでは2台ペアリングされた状態で接続が切断されると、ホワイトリストにもう片方のデバイスを入れるようにしています。
実はホワイトリストの更新を切断時にしかしていないので、一度切断したら逆のデバイスに接続する縛りが発生したりしていますが(原因は前述した割り込み内のクラッシュ問題)、後日治そうかなと思います。
余談:Github Copilot
ついでにGithub Copilotについても感想とかを書こうと思うんですが、無事学生登録が承認されて使用できるようになったんですが、補完性能は驚くほど高いです。マルチペアリングの実装でも結構お世話になりました。
ただ、長いコードを書く能力はClaude 3.5 SonnetのArtifactが便利かつ強力なので負けてるかなと感じました。o1-previewが使えてない(と思う)のでなんとも言えませんが、今のところは大枠がClaude、実際に書く時のまさにCopilotがGithub Copilotて感じですかね。
まとめ
というわけで今回はESP-IDFの環境をととのえて、マルチペアリングを実装したわけですが、いやーどちらも大変でしたが、その分達成感はすごかったです。まあだいぶ時間溶かしたのでしばらく開発は休みますけど、まだやりたいことは残っているので少しずつ進めたいと思います。それではまた。