M5StickCでスマホのカメラシャッターを作ろうとしたら、意外と沼だった話

みなさん、こんにちは。

先日、あるプロジェクトで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の低レイヤー部分は一筋縄ではいきません。

  1. 接続状態管理が激ムズ
    サーバーコールバック、アドバタイジングの再開、MTUネゴシエーションなど、考慮すべき事項が多すぎます。
  2. 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点です。

  1. 車輪の再発明は慎重に
    BLEのような複雑なプロトコルは、素直にメンテナンスされたライブラリを使うのが吉です。
  2. 接続管理は意外と深い
    「繋がった後」よりも「切れた後」の処理の方がコード量が多くなりがちです。
  3. プラットフォームの壁
    AndroidとWindows、iOSでは挙動が違うことが多いので、複数端末でのテストは必須です。

単純な機能に見えても、無線通信が絡むと一気に奥が深くなりますね。

M5StickCでリモートシャッターを作りたい方、BLEキーボードの開発を行いたい方の参考になれば嬉しいです。

 

本日も最後までお読みいただきありがとうございました。

それでは、よい IoT ライフを!

コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

上部へスクロール