みなさん、こんにちは。
先日、あるプロジェクトでBLE通信を使うIoTボタンが必要になったのですが、手元に実物がなく調達も間に合わない……という状況になりました。「なければ作ればいいじゃない」ということで、手元にあった M5StickC を使って、IoTボタンの動作をエミュレートするプログラムを作ることにしました。
やりたいことは非常に単純です。
「M5StickC を Android スマホに Bluetooth 接続し、音量キーを送信してカメラのシャッターを切る」
これだけです。いわゆる「ダイソーのリモートシャッター」のようなものをM5StickCで再現するわけですね。
サクッと終わるだろうと高をくくっていたのですが、期待に反して数々のエラーや接続トラブルに見舞われました。今回は、その試行錯誤と解決までの道のりを共有します。
開発環境
- デバイス: M5StickC (ESP32)
- IDE: Arduino IDE
- Board Manager: ESP32 Boards 2.0.18 / ESP32 3.3.3
定番ライブラリ「BleKeyboard」の罠
まずは先人の知恵を借ります。「ESP32 BLE キーボード」などで検索すると、必ずと言っていいほど出てくるのが BleKeyboard というライブラリです。
「これを使えば一発!」と思い、GitHubからライブラリをダウンロードして手動インストールしました(ライブラリマネージャーには出てこなかったため)。
使用したライブラリ:T-vK/ESP32-BLE-Keyboard
サンプルコードを参考に、以下のようなコードを書きました。
#include <BleKeyboard.h>
BleKeyboard bleKeyboard("M5StickC Shutter", "M5Stack", 100);
void setup() {
bleKeyboard.begin();
}
void loop() {
if (bleKeyboard.isConnected()) {
bleKeyboard.write(KEY_MEDIA_VOLUME_UP);
delay(100);
}
}
意気揚々とコンパイルボタンを押したところ……。
結果:コンパイルエラー
error: cannot convert 'std::string' to 'String'
調べてみると、どうやらこのライブラリは、私が使っている ESP32 3.3.3 の新しい BLE API に対応していない ようです。ESP32 Boardsのバージョンを 2.0.18 から 2.0.13 に落として試してみても同様。いきなり出鼻をくじかれました。
AIと挑む「オレオレ HID 実装」の限界
「ライブラリがダメなら、自分で実装すればいいじゃない」
AI時代の今、私には GitHub Copilot という心強い味方がいます。「HID Over GATT でメディアキーを送るコードを書いて」と頼めば、AIがササッと書いてくれるはずです。
Copilot先生は、約200行に及ぶ立派なコードを生成してくれました。
// AIが生成したコードの一部抜粋
BLEService *hidService = pServer->createService(BLEUUID((uint16_t)0x180D));
// HID Report Mapなどの定義
uint8_t hidReportMap[] = {
0x05, 0x0C, 0x09, 0x01, ... // 複雑なバイト配列
};
「よし、これで勝つる!」と思ったのも束の間、複数のデバイスでテストしてみるとボロが出始めました。
- Android 12: 接続はできるが、キー入力が全く反応しない。
- Android 7: ペアリング直後に切断される。
- Windows: 同じく切断される。
問題点: やはりBLEの低レイヤー部分は一筋縄ではいきません。
- 接続状態管理が激ムズ
サーバーコールバック、アドバタイジングの再開、MTUネゴシエーションなど、考慮すべき事項が多すぎます。 - HIDディスクリプタの闇
各OSの実装の微妙な違いにより、汎用的なディスクリプタを書くのが困難です。
AIといえど、この辺りの「環境依存の泥臭い調整」までは完璧にカバーできませんでした。素直に負けを認めるしかなさそうです。
救世主「topcoco/ESP32-BLE-HID」
「やはりメンテナンスされているライブラリを探そう」とGitHubを放浪していたところ、topcoco/ESP32-BLE-HID というライブラリを発見しました。
発見したライブラリ:topcoco/ESP32-BLE-HID
このライブラリのいいところ
- T-vK のライブラリをベースに改良されている。
- 最近のコミットがあり、メンテナンスされている。
- メディアキーが定義済み!(これが一番大事)
使い方も非常に BleKeyboard に近く、直感的です。
// 使い方のイメージ
BleComboKeyboard keyboard("M5StickC Shutter", "M5Stack", 100);
// 中略
keyboard.write(KEY_MEDIA_VOLUME_UP);
コンパイルしてみると……成功! 実機での接続テストも、キー送信も一発でクリアしました。最初からこれに出会いたかった……。
最後の壁 – 接続状態の管理(再接続・ペアリング)
基本動作はできましたが、実運用を考えると「切断後の再接続」や「ペアリングのリセット」が必要です。ここが地味にハマるポイントでした。
問題その1. 切断後に再接続できない
スマホ側でBluetoothをオフ→オンにしても、M5StickCが再接続してくれません。
原因: 切断されたタイミングでアドバタイジング(「ここにいるよ!」という信号)が止まってしまうから。
対策: 切断を検知して、アドバタイジングを再開させる処理を追加します。
問題その2. ペアリング情報のゴミ
開発中によくあるのですが、「スマホ側でペアリング解除したのに、M5StickC側がペアリング情報を覚えていて再接続できない」という現象。
対策: M5StickCのボタン長押しなどで、明示的にペアリング情報を削除する機能が必要です。
最終的な実装コード
これらを踏まえた最終的なコード(の要点)がこちらです。
#include <M5StickC.h>
#include <BleCombo.h>
BleComboKeyboard keyboard("M5StickC Shutter", "M5Stack", 100);
bool wasConnected = false;
void setup() {
M5.begin();
keyboard.begin();
}
void loop() {
M5.update();
// --- 接続状態の管理 ---
bool isConnected = keyboard.isConnected();
if (wasConnected && !isConnected) {
// 切断されたらリセットしてアドバタイジング再開
keyboard.end();
delay(500);
keyboard.begin();
delay(500);
BLEDevice::getAdvertising()->start(); // ここ重要
}
wasConnected = isConnected;
// --- シャッター処理 ---
if (M5.BtnA.isPressed()) {
keyboard.write(KEY_MEDIA_VOLUME_UP);
delay(200); // チャタリング防止兼ねて
}
// --- ペアリングリセット処理(A+Bボタン長押し) ---
// (コード省略:BtnAとBtnBの同時長押し検知で resetPairing() を呼ぶ)
}
※全ソースコードは GitHubリポジトリ に置いてあります。
学んだこと・まとめ
今回の開発で得た教訓は以下の3点です。
- 車輪の再発明は慎重に
BLEのような複雑なプロトコルは、素直にメンテナンスされたライブラリを使うのが吉です。 - 接続管理は意外と深い
「繋がった後」よりも「切れた後」の処理の方がコード量が多くなりがちです。 - プラットフォームの壁
AndroidとWindows、iOSでは挙動が違うことが多いので、複数端末でのテストは必須です。
単純な機能に見えても、無線通信が絡むと一気に奥が深くなりますね。
M5StickCでリモートシャッターを作りたい方、BLEキーボードの開発を行いたい方の参考になれば嬉しいです。
本日も最後までお読みいただきありがとうございました。
それでは、よい IoT ライフを!



