motoh's blog

主に趣味の電子工作やプログラミングについて書いていきます

M5Stackで作れるかわいいロボット「スタックチャン」の沼にハマった

この記事はスタックチャン(Stack-chan) Advent Calendar 2023の3日目の記事です。

今年はスタックチャンというかわいいロボットの製作を通していろいろと新しい経験ができたので、振り返りながらまとめたいと思います。(スタックチャンの詳しい作り方は割愛しますが、作る際に参考にした情報は紹介していきます。)

スタックチャンとは

スタックチャンはししかわさんが開発しオープンソースとして公開している、M5Stackで動くスーパーカワイイロボットです。Github3Dプリンタのデータ、回路/基板設計のデータ、ファームウェアといった製作に必要な情報がすべて公開されています。たくさんの方々が自分で作ったスタックチャンをSNSで公開したりイベントで展示したりしてコミュニティを盛り上げています(二次創作のガイドラインこちら)。

protopedia.net

github.com

スタックチャンとの出会い

最初は当ブログの名前で適当に作ってみたXアカウントですが、TLにちらほら現れるかわいいロボット...
何気ないツイートに対するコミュニティーの方々からの反応がうれしく、気づいたらM5Stackを購入していました(後で知りましたが、これがスタックチャン沼の入り口のようです)

初めて作ったスタックチャン。やっぱり自分で作ったスタックチャンが一番かわいいですね(親バカ)。
記事の最後に、作る際に参考にした資料をまとめました。

最新技術が詰まった「AIスタックチャン」に魅了される

スタックチャンのファームウェアは先に紹介したリポジトリの他にも、コミュニティの方々によって様々なバージョンが公開されており、私はrobo8080さんの「AIスタックチャン」を使用させていただきました。ChatGPT、Google STT(音声認識)、VoiceVox(音声合成)等のWeb APIを用いることで自然な会話ができるようになった、今大人気のスタックチャンです。

github.com

いろいろな技術が詰まったAIスタックチャンのファームウェアは、ソースコードを読んで勉強したり、自分なりにカスタマイズしたりするだけでも十分楽しめますが、カスタマイズしたものを動画でXに投稿してrobo8080さんご本人やコミュニティの方々から反応をいただけるのがとても嬉しく、SNSにあまり慣れていなかった私には新鮮な体験でした。

イベントにも参加

スタックチャン2才の誕生日会

残念ながら都合がつかず会場には行けませんでしたが、動画の生配信を見て楽しむことができました。また、事前に公開された「お祝いイラスト」にも参加してSNS上で盛り上がりました。

Maker Fair Tokyo 2023

去年もあったそうですが、今年もスタックチャンのブースが設けられました。ししかわさんからスタックチャンのブースでの展示・物販の募集があったため、思い切って展示のみで初参加しました。当日は一般客としての参加でしたが、スタッフの方々に混じって自分の作品を説明させていただいたり、ししかわさんはじめ普段Xでいいねし合っているコミュニティの方々と初めて対面でお話できたりと、初めて尽くしで緊張もしましたがとても楽しい一日となりました。

まとめ

1年間を振り返りながら、スタックチャン沼にハマっていった様子をまとめてみました。スタックチャンのおかげでいくつも新しい経験ができ、毎日が楽しくなったように思います。沼へのハマり方は人それぞれだと思うので参考になるかわかりませんが、これを見て「スタックチャンかわいい」「作ってみたい」と思っていただければとてもうれしいです(ぜひ、Xでハッシュタグ#スタックチャンでポストしましょう)。

ちなみにスタックチャン沼とM5Stack沼にはそれぞれ階層があります。私はもう引き返せないところまで来てしまったようです。

付録① スタックチャンの作り方に関する資料

スタックチャンは先に紹介したように、ししかわさんのGithubに製作に必要なデータがすべて公開されていますが、オープンソースのライセンスに従って有志の方々によりいろいろな作り方が公開されています。おきもくさんがわかりやすくまとめてくださっています。

私は以下をそれぞれ購入して作りました。M5Stackの分解なしで組み立てられるので比較的難易度が低いです。

  • M5Stack Core2
  • タカオさんのキット(ボディ+ねじ+電子パーツ)
  • SG90サーボ

タカオ版キットの組み立て方とファームウェアの書き込み方は、以下で紹介するおきもくさんが公開されているホームページや動画を参考にさせていただきました。この動画では先に紹介したrobo8080さんの「AIスタックチャン」のファームウェアを書き込みます。

また、AIスタックチャンのファームウェアの書き込み方については、ソースコードは触らずにとにかく動かしてみたいという場合はwelchmanさんのM5Burner版の動画がわかりやすくておすすめです。

■スタックチャンの組み立て方

スタックチャンとは?あなたも作れます! | おきもく工作室

www.youtube.com

■AIスタックチャンのファームウェアの書き込み方(Platformio編) www.youtube.com

■AIスタックチャンのファームウェアの書き込み方(M5Burner編) www.youtube.com

付録② AIスタックチャンのカスタマイズ版

私がカスタマイズしたAIスタックチャンのソースコードGithubで公開させていただいています。robo8080さんのAIスタックチャンが動いた後に、もしご興味があればお試しください。

https://github.com/ronron-gh/AI_StackChan2_DevCam
M5Stack CoreS3で動くAIスタックチャンです。内蔵カメラによる顔検出で会話を起動できます。

https://github.com/ronron-gh/AI_StackChan2_FuncCall
ChatGPT APIの新しい機能Function Callingを用いて、AIスタックチャンに様々な機能を追加しました。

Timer Camera Xでアゲハチョウの幼虫を観察してみた

庭の雑草にアゲハチョウがタマゴを産み付けたのを息子が発見しました。

初めはタマゴは数個しかありませんでしたが、日に日に増え、すぐに幼虫が生まれ、あっという間に雑草が幼虫のシェアハウスのようになりました。

これはよい機会だと思い、前々から構想していたTimer Camera X (M5Cameraの後継機)と防水ケースによる定点カメラを急いで組み上げて、観察を開始しました。(組み上がった頃には葉がほとんど食い尽くされていましたが^^;)

この定点カメラで1分毎に撮影した画像をつなぎ合わせて作ったタイムラプスがこちらです。ほとんど動かないと思っていた幼虫たちが活発に動く様子が見れたり、途中でバッタが登場したりと、思いの外楽しめます(息子よりも自分のほうが楽しんでいます)。

youtu.be

ここからは、どのようにこの定点カメラを作ったかを簡単に解説していきます。

システム概要

防水ケースに収めたTimer Camera Xでは、カメラ系で定番のスケッチweb_camをベースにしたプログラムを動かしています。これで、他のPCからHTTPリクエストで画像を取得できるようになります。自分はラズパイで1分毎にHTTPリクエストして画像を取得するプログラムをPythonで組みました(記事の最後にPythonのコードを掲載します)。

組み立て

防水ケースはこちら(Amazonリンク)を選びました。

あまり加工が得意ではないので、2cm厚のウレタンを切り抜いてTimer Camera Xをはめ込むようにしました。 cheeroの3200mAhのバッテリに横向きのUSBコネクタを差すと、防水ケースの寸法にぴったり収まりました。

庭の柵にタイラップで固定できるよう、裏側は両面テープ式のタイラップ固定部を貼り付けました。

Timer Camera Xの低電力化

スケッチweb_camをそのまま動かすと常に100mA程度の消費電流となるため、cheeroの3200mAhバッテリーでは32時間しか動かせない計算です。これでは毎日バッテリーを交換せねばならず、定点カメラとしてはちょっと不便なので、次の3つの対策で低電力化しました。結果、3日間連続動作できるようになりました。

対策1:CPU周波数を低くする

デフォルトだと240MHzですが、80MHzまで落とすことで電流を10mA程度下げることができました(80MHzより落とすとWifiが使えなくなる仕様のようです)。次の関数でCPU周波数を設定できます。

setCpuFrequencyMhz(80);

対策2:ディープスリープを使う

1分毎に画像を取得するので、「50秒間ディープスリープ」→「10秒間カメラサーバ動作」を繰り返すようにしました。ディープスリープへの移行は次の関数を使います。

M5.Power.deepSleep(SLEEP_SEC(50));

対策3:カメラデバイスOV3660のスタンバイモードを使う

消費電流を計測してみると、ディープスリープにしても40mAくらい消費しており、期待したより消費電流が下がりませんでした。調べるとこちらの記事にたどり着き、Timer Camera Xに搭載されているカメラデバイスOV3660をスタンバイモードにすると消費電流を数μAまで下げられることがわかりました。次のようにカメラデバイスレジスタ番地0x3008の値を「0x02」から「0x42」に書き換えることでスタンバイモードに移行できます。ディープスリープに移行する直前にこれを実施します。

sensor_t *s = esp_camera_sensor_get();
s->set_reg(s, 0x3008 , 0xff, 0x42);

ラズパイ側のPythonプログラム

1分毎にHTTPリクエストして画像を取得するプログラムを参考として掲載します。

# -*- coding: utf-8 -*-
import os
import urllib.request

if __name__ == '__main__':
    i = 0
    while True:
        try:
            response = urllib.request.urlopen('http://xxx.xxx.xxx.xxx/capture',timeout=10)
            image_b = response.read()

            #ファイル名を通番にして保存
            fname = '{:04d}'.format(i)
            f = open('YOUR_PICTURES_FOLDER/'+ fname + '.jpg', 'wb')
            f.write(image_b)
            f.close()

            i = i+1
            time.sleep(60)

        except Exception as e:
            print(e)

まとめ

Timer Camera Xを利用した定点カメラでアゲハチョウの幼虫を観察してみた様子と、定点カメラの作り方について簡単ですがご紹介しました。ウレタンやタイラップを利用することで、難しい加工を一切せずに組み立てることができました。得られた画像でタイムラプスを作成したら思いの外楽しめるものになりました。省電力化にも取り組み、バッテリーで3日間連続動作できるようになりましたが、理想はバッテリー交換を週一回にしたいので、もう少し工夫したいと思います。

参考サイト

M5Cameraをソーラー発電で動かす - MSR合同会社

Timer Camera(USB給電時)を低電力化する - MSR合同会社

防水ケースとTimer Camera Xを使った定点カメラや、Timer Cameraの低電力化について、多くを参考にさせていただきました。

M5Stack CoreS3でディープラーニング推論を実装してみた

以前、M5StackシリーズのTimer Camera X(ESP32マイコンを搭載)でディープラーニングの推論を試してみた記事を投稿しましたが、新しく発売されたM5Stack CoreS3にカメラが搭載されたため、移植して記事も刷新しました。また、以前は開発環境としてArduinoIDEを使用していたため、ライブラリを取り込むところでやや余計な手順が発生していましたが、今回はPlatformIOを使用して開発効率も上がりました。

本記事ではM5Stack CoreS3のカメラに映った手の指がさしている方向(中央、上、下、右、左)を、デバイス側のプログラムだけで推論できるようにするまでの手順を解説します。学習はGoogle Colaboratory上で行います。自分で用意した画像を用いて学習させることができます。

次の動画は、M5Stack CoreS3で実行した様子です。動画では完璧に指差した方向を認識していますが、背景の色などが変わると認識率がとても悪くなります。実用的なものを作るにはもっと勉強が必要のようです^^;

youtube.com

GitHubにPlatformIOの最終的なプロジェクトも入っていますので、ビルドしてM5Stack CoreS3に書き込めばすぐに試すことができます。

github.com

開発環境

項目 名称 バージョン
M5Stack実機 M5Stack CoreS3
開発用PC Windows 11 Pro 21H2
IDE
(実機プログラム開発用)
PlatformIO Core 6.1.7 Home 3.4.4
AI開発環境
(学習モデル生成用)
Google Colaboratory
ライブラリ : Neural Network Library (nnabla)

1.33.1

PlatformIOについて

PlatformIOはVisual Studio Code拡張機能としてインストールします。インストール手順の説明は割愛しますが、こちらの記事などが参考になります。

qiita.com

Google Colaboratoryについて

学習は自前のPC上のPython環境で行ってもよいのですが、今回はGoogle Colaboratory (以下Colab)を利用しました。Colabなら環境構築に手間をかけることなく、GPUも簡単に使うことができます(ここではColabの使い方は解説しませんが、Googleのアカウントさえ持っていれば簡単に使い始めることができます)。

機械学習フレームワーク Neural Network Library (nnabla) について

マイコンディープラーニングの推論を動かす方法はいくつかあるようですが、今回はSonyフレームワーク Neural Network Library (nnabla)を使う方法を選びました。nnablaは学習済みモデルをマイコンで実行できるCソースコードに変換する機能を備えています。

nnablaのライブラリやサンプルプログラムはGithubで公開されています。

github.com

github.com

開発手順の概要

大まかな手順は次の通りです。以降、詳しく解説していきます。

No. 作業内容 作業環境
CSVファイルでデータセットのリストを作成する(nnablaでデータセットを読み込むときに必要)。 Python (Colab)
nnablaのMNISTサンプルプログラムを、MNISTデータセットではなく自分のデータセットを読み込めるように改造する。 -
改造したサンプルプログラムを実行し学習済みモデルのファイル(.nnp)を出力する。 Python (Colab)
nnabla_cliコマンドでnnpファイルをCソースコードに変換する。 Python (Colab)
変換結果ファイルとnnablaランタイムをPlatformIOのプロジェクトに取り込む。 PlatformIO
カメラ画像から推論するコードを作成する。 PlatformIO

なお、①~④のColabでの作業については、解説のコメントを付けたColabノートブックをGitHubで公開しますので、このノートブックのコードを順番に実行すれば④までの作業が完了します。

github.com

[補足]
ColabでGPUを有効にしていないと、上記ノートブックを実行できません。もし有効になっていなければ、Colabのメニューの「ランタイムのタイプを変更」から設定してください。

手順① データセットのリストを作成する

nnablaにデータセットを読み込ませるためには、データセットのリストをCSVファイルで渡す必要があります。 CSVファイルは次のようなフォーマットで、学習用とテスト用に分けて作成します。

x:image,y
画像データのファイルパス, 分類(0,1,...,n)
画像データのファイルパス, 分類(0,1,...,n)
画像データのファイルパス, 分類(0,1,...,n)
・
・
・

今回、分類は次のようにしました。

0: 何も写っていない画像
1: 中央を指している画像
2: 上を指している画像
3: 下を指している画像
4: 右を指している画像
5: 左を指している画像

データセットGitHubのmy_datasetフォルダに、このような感じで分類毎のフォルダに分けて入っています。

今回、このようにフォルダ分けしたデータセットから、自動的にCSVファイルを作成するPythonスクリプトも作ってGitHubに入れました(make_dataset_csv.py)。データセットを8:2の割合で分けて、train.csvとtest.csvを作成してくれます。さらに、データセットの画像を28x28のグレースケールに変換してくれます(変換後の画像データはconverted_datasetsフォルダに保存されます)。

Colabノートブックでは、このデータセットスクリプトを利用してCSVファイルを作成しています。

手順② MNISTサンプルプログラムを改造する

nnablaのMNISTサンプルプログラム(classification.py)を、MNISTデータセットの代わりにCSVファイル(train.csv,、test.csv)を読み込むように改造します。

改造の仕方は次の記事がとても参考になりますのでここでの解説は割愛しますが、改造済みのプログラム(classification_mydata.py)をGitHubに置いています。Colabノートブックでもこのプログラムをダウンロードして使用しています。

cedro3.com

手順③~④ Colab上で学習し、モデルファイルを生成する

Python環境でnnablaのMNISTサンプルプログラムを実行し、学習済みモデルのファイル(.nnp)を出力します。その後、nnabla_cliコマンドでnnpファイルをCソースコードに変換します。ここまではColabノートブックで実行できます。

変換結果として以下のCソースコードが得られ、以降の手順で使用します。

  • Validation_inference.c
  • Validation_inference.h
  • Validation_parameters.c
  • Validation_parameters.h

手順⑤ 変換結果ファイルとnnablaランタイムをPlatformIOのプロジェクトに取り込む

ここからはPlatformIOでの作業になります。変換で得られたCソースコード、及びnnablaのランタイムを、下図のようにPlatformIOのプロジェクトフォルダに配置します。

nnablaのランタイムはgit cloneで入手します。

> git clone https://github.com/sony/nnabla-c-runtime.git

図中のstb_image_resize.hは、カメラ画像を前処理としてリサイズするときに使うライブラリで、こちらGitHubリポジトリで公開されているものを使用させていただきました。

また、Validation_inference.cとValidation_parameters.cは次のように修正が必要です。

■Validation_inference.c 修正内容
130行目付近の関数宣言をコメントアウト(削除)します(そうしないと、ビルド時に二重定義だと言われる)。

//void *(*rt_variable_malloc_func)(size_t size) = malloc;
//void (*rt_variable_free_func)(void *ptr) = free;

//void *(*rt_malloc_func)(size_t size) = malloc;
//void (*rt_free_func)(void *ptr) = free;

■Validation_parameters.c 修正内容
全てのfloat型配列にconstを付けます。ビルド時にRAMがオーバーフローしたというエラーが出たため、配置先をFlashに変更するためにconstを付けました。

const float Validation_parameter11[] = {      /* その他全てのfloat型配列にconstを付ける */

手順⑥ カメラ画像から推論するコードを作成する

カメラ画像から推論するコードを作成します。この部分に関しては、ほとんどこちらの記事を参考にさせていただきました。

qiita.com

ソースコードのプロジェクト一式はGitHubのPlatformioフォルダに置いてあります。手順①~⑤を実施済みのプロジェクトなので、ビルドしてM5Stack CoreS3に書き込めば動かすことができます。

■カメラ画像から推論するコード(抜粋)

    uint8_t resized_img[NNABLART_VALIDATION_INPUT0_SIZE];

    _context = nnablart_validation_allocate_context(Validation_parameters);
    float *nn_input_buffer = nnablart_validation_input_buffer(_context, 0);
    
    while(true){    

        fb = esp_camera_fb_get();
        if (!fb) {
            Serial.println("Camera capture failed");
            res = ESP_FAIL;
        } else {

              // 28x28にリサイズ
              stbir_resize_uint8(fb->buf, 160, 120, 0, resized_img, 28, 28, 0, 1);

              // 推論
              int64_t infer_time = esp_timer_get_time();
              nnablart_validation_inference(_context);

              // 推論結果をフェッチ
              float *probs = nnablart_validation_output_buffer(_context, 0);

              int top_class = 0;
              float top_probability = 0.0f;
              for (int classNo = 0; classNo < NNABLART_VALIDATION_OUTPUT0_SIZE; classNo++) {
                  if (top_probability < probs[classNo]) {
                      top_probability = probs[classNo];
                      top_class = classNo;
                  }
              }
                
              Serial.printf("Result %d ", top_class);  
        } 
    }

まとめ

新しく発売されたM5Stack CoreS3はカメラが搭載されたため、以前Timer Camera Xでやっていたディープラーニング推論を移植してみました。また、以前はArduinoIDEで開発していましたが、今回はPlatformIOで開発することで開発効率も上がっています。冒頭の動画を見るとかなり精度が高いように見えますが、実際は背景に映り込むものによってかなり精度が悪くなります。そこらへんはこれから勉強していきたいと思います。ちなみに、指差した方向を推論させた理由は、スタックチャンとあっち向いてほいをしてみたいと思ったからですw(スタックチャンって何?という方はぜひTwitter等で検索してみてください)

参考書籍・サイト

文中で紹介したものもありますが、参考書籍・サイトを記載します。

人気ブロガーからあげ先生のとにかく楽しいAI自作教室 (Amazon)

M5Stackは扱われていませんが、ディープラーニングを学び直したり今回の挑戦(エッジAI)をしたりするきっかけになった本です。Google Colaboratoryを使っているのもこの本の影響を強く受けています。

マイコンでディープラーニングした話 on ESP32 - Qiita

M5StackでNeural Network Library (nnabla)のMNISTのサンプルを動かす方法について参考にさせていただきました。

SONY Neural Network Libraries データセットの読ませ方 | cedro-blog

nnablaに独自のデータセットを読み込ませる方法について参考にさせていただきました。

プログラミングでChatGPTが欠かせなくなるかもしれないと思わされた体験談

ChatGPTにプログラミングを手伝ってもらったという記事はすでにネット上にあふれていますが、実際に自分で体験してみるまでわからなかったすごさを共有できたらと思います。

きっかけ

仕事で、Linuxでリアルタイム性が求められる組込みシステムのマルチタスク制御をする必要が出てきました。学生のとき(1●年前!!!)にLinuxのマルチスレッド(pthread)でロボット制御をしたことはありますが、最近はというと、ラズパイで遊ぶくらいしかLinuxには触れていません^^; Linuxでリアルタイム性を損なわずにマルチタスク制御できるのかどうか、真面目に調べる必要がありました。

まずは最近のトレンドについて、ChatGPTに聞いてみた

最近ようやくアカウントをつくったChatGPTに、あまり期待はせずに聞いてみました。

コルーチンって何??? ネット検索してみると、確かに最近のトレンドであることが分かります。組込みシステムについてそれなりに知っている気になっていた自分が恥ずかしくなってきました。同時に、いきなりトップクラスの技術者の友達が出来たような、不思議な感覚にとらわれました。(コルーチンなんて常識じゃんと言われてしまうとゴメンナサイですが…)

結局、仕事のチーム内でコルーチンについてプレゼンするのは面倒なので、まずは伝統的なpthreadで調査を進めることにしましたが、ChatGPTは今後無視できなくなると初めて考えさせられた体験でした。

サンプルコードも正確

さらに感動は続きます。

よくよく調べると、Linuxのマルチスレッドでリアルタイム処理をするために、スレッドのスケジューリングポリシーをデフォルトのSCHED_OTHERからSCHED_FIFOに変更する必要があることがわかりました(そうすることでリアルタイムOSと同じように優先度ベースの並列処理ができる)。

※まだ自分のネット検索力にも自信はあったので、これは自分で調べてしまいましたが、後に、ChatGPTに聞いたほうが近道だったことが分かります。

自分で調べた結果、どうやらスケジューリングポリシーを確認、変更するには sched_getscheduler() / sched_setscheduler() を使えば良いことがわかりましたが、いくら検索してもサンプルコードが出てきません。出てくるのはManページを翻訳したようなサイトばかりです(Manページの情報が不十分と言うわけではありませんが、効率を求めるとやはりサンプルコードは欲しい…)。

そこで、ようやくChatGPTに聞いてみました。

…完璧です。わかりやすい解説が付くうえ、とても読みやすいサンプルコードが出てきました。しかも、そのままコンパイルして実行することができました。

なぜ、ネット検索しても出てこないようなサンプルコードをChatGPTさんは書けるのでしょうか… もしかすると、海外サイトも含めて検索するとサンプルコードも出てくるのかもしれませんが、それを日本語の質問で出してくれるChatGPT、便利すぎます。

後で、この関数は親プロセスのスケジューリングポリシーを変更するものであって、子スレッドに対しては別の関数を使う必要があることがわかりました。ChatGPTに質問したら、初めから正しい関数を教えてくれました。そのやり取りがこちら。…ネット検索力、完敗です(汗)

まとめ

初めてChatGPTと一緒にプログラミングをして、そのすごさを目の当たりにしたという話でした。ChatGPTに聞いた内容があまり一般ウケする内容ではなかったので、記事にするかは少し悩みましたが、このような組込み技術者の中でもさらに一握りの人しか調べないような知識までChatGPTが完璧にカバーしているのが本当にすごいと思い、あえて記事にしました。

ChatGPTがプログラミングできると聞いても、結局人が色々手を加えないと使い物にならないでしょと今まであまり気にしていませんでしたが、サンプルコードを生成してくれるツールだと思うと急に身近に感じられ、ネット検索するより早いという評判も、確かに...と思えるようになりました。

ちなみに、自分が使っているのはGPT3.5です。GPT4だとさらに性能が上がってしまうなんて... GPT3.5をもう少し使いこなしたら、GPT4も使ってみたいと思います。

使わなくなったタブレットとラズパイでデジタルフォトフレーム(共有ストレージ対応)を作ってみた

きっと自分だけではないと思いますが…

  • 電子書籍を何冊か読んだ後忘れ去られていたiPad mini (2012年モデル)
  • 外付けHDDを共有ストレージ化することくらいにしか活用できていないRaspberry Pi 3B+

といったもったいない使い方をしているデバイスたちを使って、デジタルフォトフレーム(共有ストレージ対応)を作ってみました。(今回は構想メインで、ソースコード等細かいことは記載しませんが、今後追記するかもしれません...)

きっかけ

以前からRaspberry Piと使い古しの外付けHDDで共有ストレージを構成して、デジカメやスマホで撮った写真を保存していたのですが、どんどん溜まっていくだけで見る機会がほとんどなく、簡単に家族みんなで見れたら楽しいのになと思っていました。また、iPadも10年落ちとなると、新しいアプリは対応していないし、電子書籍スマホで十分読めるし…という状況で、ハード的にはまだまだ動くのに使い道がなくもったいないなと感じていました。

コンセプト

  • 共有ストレージに溜まった写真データを、タブレットにランダムで表示して家族みんなで楽しめる。表示する写真は1分毎に自動更新する。

  • 共有ストレージには家族みんながアクセスでき、スマホなどで撮った写真を簡単に保存できる。

  • フォトフレームはWEBアプリとして実装することで、WEBブラウザが動くデバイスタブレットスマホ、fireTVなど)なら何でもフォトフレーム化できる。

実現方法(かなりざっくりとした解説)

Raspberry Pi

  • SambaでUSB外付けHDDを共有ストレージ化した。
  • WEBサーバはNode.jsで開発した。WEBサーバにリクエストしてきたデバイスに対して、ランダムに選んだ写真を埋め込んだHTMLを返信する。
  • HTMLにはJavaScriptを埋め込み、1分毎に自動的にリクエスト(ページ更新)するようにした。
  • Node.jsのWebサーバを開始するときに、共有ストレージ内の特定フォルダ内の写真データをすべて探索してリストアップするようにした。(この処理はNode.jsで実装できればスマートだが、自分は慣れているPythonで作成してNode.jsから呼ぶようにした。)

iPad mini

  • Webブラウザがあればよいので、アプリ開発は不要。
  • デフォルトだと時間が経つと自動で画面オフしてしまうので、設定を「自動ロックなし」に変更した。
  • キオスクモードで、管理者以外は余計な操作(ブラウザを閉じて他のアプリを起動するなど)をできないようにした。(ここらへんはお好みで)

PC、スマホなど

アプリ開発は不要。Sambaで共有されたHDDには、標準のファイルエクスプローラでアクセスして写真データを保存できる。(スマホの場合はSambaに対応した適当なアプリを探してインストールする必要がある。)

最後に

市販のデジタルフォトフレームで共有ストレージに対応したものはほとんどないと思いますし、何より既に家にある物だけで作れたので自分的にはとても満足しています。ラズパイのWEBサーバを開発するところは、WEBプログラミング初心者の自分にはちょっと難易度が高かったですが、次の本で勉強しながら作りました。

作りながら学ぶWebプログラミング実践入門 ~一冊で理解するHTML、CSS、JavaScript、Node.js~(Amazonリンク)

お好みで、写真と一緒に日時や天気やラズパイで測った温湿度などを表示することもできると思います。このように、アイデア次第でカスタマイズできるのも自作の良いところではないでしょうか。

M5Stack (ESP32) でエッジAIを試してみた

私のAI技術レベルは数年前ディープラーニングが流行りだした頃にMNISTを動かしてみたくらいのレベルなのですが、最近、「人気ブロガーからあげ先生のとにかく楽しいAI自作教室」 (Amazonリンク)という本を見つけて数年ぶりにAIについて学び直しています。この本の中で、ラズパイでディープラーニング(推論)を動かす方法が解説されており、推論だけならばESP32のようなもっと小型なデバイスでも動かせるのではないかと考え、たまたま持っていたM5StackシリーズのTimer Camera X(ESP32マイコンを搭載)でディープラーニングの推論を試してみました。

2023.6.5追記
本記事の内容を最新のM5Stack CoreS3に移植し、記事も刷新しました。 mzmlab.hatenablog.com

Timer Camera X

Timer Camera XはESP32ベースの高性能マイコン(CPU 240MHz、PSRAM 8MB)にカメラも搭載していますが、3000円前後というお手頃価格で購入できます。スペック的にはエッジAI向きなデバイスのように思いますが、ネット上では例が少なく、想像以上に試行錯誤が必要でした。今回、自分で用意したじゃんけんの画像データセットで学習させたモデルを使って推論させることができたので、その方法について記述したいと思います。Timer Camera Xの開発環境はArduinoを用いています(実は、Arduinoで凝った開発をしようとしたことが苦労の一因になっています。本当はESP-IDFを使ったほうがよいのかもしれませんが、使ったことがないため見送りました^^;)。試行錯誤しながらここまでたどり着くまで道のりは長かったのですが、ソースコードやデータセットGitHubで公開していますので、比較的容易に再現できるのではないかと思います。今回はTimer Camera Xを使いましたが、おそらく他のM5Stack(例えばM5Camera)でもほぼ同じようにできると思います。

[補足]
上記の本でも紹介されていますが、実はM5stickVというAI開発をターゲットとしたM5Stack製品があります。サイズ感はTimer Camera Xとあまり変わりませんが処理性能はさらに高くなっています(ESP32ではありません)。こちらを使えば余計な苦労をすることなくエッジAI開発を体験できるようです(ただ、その分価格も少しお高めです)。

開発環境

項目 名称 バージョン
M5Stack実機 Timer Camera X
開発用PC Windows 10 Home 21H2
IDE
(実機プログラム開発用)
Arduino
ライブラリ : Timer-CAM
1.8.19
0.0.2
AI開発環境
(学習モデル生成用)
Google Colaboratory
ライブラリ : Neural Network Library (nnabla)

1.33.1

【STEP0】 スケッチ例”web_cam”を動かす

本記事では、Arduinoのライブラリ”Timer-CAM”内のスケッチ例”web_cam”を動かせることを前提とし、このweb_camのソースコードを改造していきます。web_camを動かすまでの手順はこちらの記事等情報があるため、本記事での説明は割愛します。

lang-ship.com

【STEP1】 MNISTのサンプルを動かす

AI開発に慣れていればいきなり自分のデータセットで学習させてもよいと思いますが、私は初心者なのでまずはMNISTのサンプルプログラムを動かしてみるところから始めました。そのため、解説もその流れで進めたいと思います。

マイコンディープラーニングの推論を動かす方法はいくつかあるようですが、今回はSonyフレームワーク Neural Network Library (nnabla)を使う方法を選びました。nnablaは学習済みモデルをマイコンで実行できるCソースコードに変換する機能を備えています。

大まかな手順は次の通りです。以降、詳しく解説していきます。

No. 作業内容 作業環境
nnablaのMNISTサンプルプログラムを実行し、学習済みモデルのファイル(.nnp)を出力する。 Python
nnabla_cliコマンドでnnpファイルをCソースコードに変換する。 Python
変換で得られたCソースコードをスケッチweb_camに取り込む。 Arduino
Arduinoのライブラリとしてnnablaのランタイム(nnabla-c-runtime)を取り込む。 Arduino
カメラ画像から推論するように、スケッチのソースコードを改造する。 Arduino

STEP1 手順①〜②

Python環境でnnablaのMNISTサンプルプログラムを実行し、学習済みモデルのファイル(.nnp)を出力します。その後、nnabla_cliコマンドでnnpファイルをCソースコードに変換します。

この作業はPC上のPython環境で行ってもよいのですが、nnablaはGoogle Colaboratory (以下Colab)でも実行することができますので、今回はColabを利用しました。Colabなら環境構築に手間をかけることなく、GPUも簡単に使うことができます(ここではColabの使い方は解説しませんが、Googleのアカウントさえ持っていれば簡単に使い始めることができます)。

解説のコメントを付けたColabノートブックをGitHubで公開しますので、詳しい手順はそちらをご覧ください。このノートブックのコードを順番に実行していけば、MNISTデータセットにより学習済みモデルを生成した後、その学習済みモデルをCソースコードに変換することができます。

github.com

変換結果として以下のCソースコードが得られ、以降の手順で使用します。

  • Validation_inference.c
  • Validation_inference.h
  • Validation_parameters.c
  • Validation_parameters.h

[補足]
ColabでGPUを有効にしていないと、上記ノートブックを実行できません。もし有効になっていなければ、Colabのメニューの「ランタイムのタイプを変更」から設定してください。

STEP1 手順③

ここからはArduinoでの作業になります。変換で得られたCソースコードをスケッチweb_camに取り込みます。

スケッチ例web_camをコピーしてweb_cam_mnistにリネームしたフォルダを用意し、その中に下図のようにファイルを配置します。図中のstb_image_resize.hは、カメラ画像を前処理としてリサイズするときに使うライブラリで、こちらGitHubリポジトリで公開されています。

また、Validation_inference.cとValidation_parameters.cは次のように修正が必要です。

■Validation_inference.c 修正①
ランタイム(後述)のヘッダファイルをインクルードしている部分を修正します。nnabla-c-runtime.hはこの後手順④で自分で作ります。

//#include <nnablart/functions.h>
#include <nnabla-c-runtime.h>

■Validation_inference.c 修正②
130行目付近の関数宣言をコメントアウトします(そうしないと、ビルド時に二重定義だと言われる)。

//void *(*rt_variable_malloc_func)(size_t size) = malloc;
//void (*rt_variable_free_func)(void *ptr) = free;

//void *(*rt_malloc_func)(size_t size) = malloc;
//void (*rt_free_func)(void *ptr) = free;

■Validation_parameters.c 修正
全てのfloat型配列にconstを付けます。ビルド時にRAMがオーバーフローしたというエラーが出たため、配置先をFlashに変更するためにconstを付けました。

const float Validation_parameter11[] = {      /* その他全てのfloat型配列にconstを付ける */

STEP1 手順④

Cソースコードに変換した学習済みモデルをマイコンで動かすには、nnablaのランタイムが必要です。ランタイムもCソースコードなので、一緒にビルドすればよいのですが、ランタイムの各ソースコードは"nnabla-c-runtime/include/nnablart"内のヘッダファイルを#include <nnablart/network.h>のような形でインクルードしているため、"nnabla-c-runtime/include"をインクルードパスとして設定しておかないとコンパイル時に「network.hが見つかりません」と怒られてしまいます。普通、IDEにインクルードパスを設定するのは当たり前の作業ですが、Arduinoの場合、どこで設定すればよいのかいくら調べてもわかりませんでした。悩んだ末たどり着いた方法が、nnabla-c-runtimeをライブラリとして取り込むという方法です。以下、その方法について説明します。ちょっと面倒ですが、このライブラリ化の作業は一回行えばその後は不要です。(インクルードパスの設定方法さえ分かればライブラリ化は不要ですので、知っている方がいたら教えてくださいm(__)m )

(1) GitHubからnnabla-c-runtimeを入手する
適当なフォルダでgit cloneしてnnabla-c-runtimeを取得します。

> git clone https://github.com/sony/nnabla-c-runtime.git

(2) 取得したnnabla-c-runtimeをArduinoのlibrariesフォルダに配置する
Arduinoではlibraries/(ライブラリ名)/srcの場所にインクルードパスが通るようなので、下図のように配置することで元のnnabla-c-runtime/includeにパスが通るようにします。

以下、自分で作成するファイルの内容です。

■library.properties
Arduinoに独自ライブラリを認識させるためにlibrary.propertiesファイルを作成します。内容はこのような感じです。

name=nnabla-c-runtime
version=1.0.0
author=*** (適当な名前)
maintainer=*** (適当な名前)
sentence=Runtime of nnabla
paragraph=See more on ...
category=Other
url=
architectures=esp32

■nnabla-c-runtime.h

#ifndef H_NNABLA_C_RUNTIME_H__
#define H_NNABLA_C_RUNTIME_H__

#include <nnablart/functions.h>

#endif

うまくいけば、このようにメニューの「スケッチ」→「ライブラリをインクルード」に独自ライブラリが出てきます。

ここまで出来たら、一旦Arduinoでビルドしてみることをお勧めします。まだweb_camのソースコードは改造していないため、ライブラリはリンクされませんが、ライブラリのソースコードコンパイルされますので、ここまでの作業に問題があればコンパイルエラーが出ます。

STEP1 手順⑤

カメラ画像から推論するように、スケッチのソースコードを改造します。この部分に関しては、ほとんどこちらの記事を参考にさせていただきました。

qiita.com

変更した部分について解説します。全体はGitHubArduino/web_cam_mnistをご覧ください。

■web_cam_mnist.ino

カメラ画像をグレースケールのQQVGA (160x120)に変更します。

  //config.pixel_format = PIXFORMAT_JPEG;
  config.pixel_format = PIXFORMAT_GRAYSCALE;
  //config.frame_size = FRAMESIZE_UXGA;
  config.frame_size = FRAMESIZE_QQVGA;
  config.jpeg_quality = 10;
  //config.fb_count = 2;
  config.fb_count = 1;

■app_httpd.cpp

stream_handler()に推論のコードを追加します。

#include "Validation_inference.h"
#include "Validation_parameters.h"
#define STB_IMAGE_RESIZE_IMPLEMENTATION
#include "stb_image_resize.h"

static esp_err_t stream_handler(httpd_req_t *req){
    ~省略~
    uint8_t resized_img[NNABLART_VALIDATION_INPUT0_SIZE];

    ~省略~
    _context = nnablart_validation_allocate_context(Validation_parameters);
    float *nn_input_buffer = nnablart_validation_input_buffer(_context, 0);
    
    while(true){    

        fb = esp_camera_fb_get();
        if (!fb) {
            Serial.println("Camera capture failed");
            res = ESP_FAIL;
        } else {

              // 28x28にリサイズ
              stbir_resize_uint8(fb->buf, 160, 120, 0, resized_img, 28, 28, 0, 1);
          
              // 白黒反転 (MNISTデータセットが黒背景に白文字であるため)
              for (int i = 0; i < NNABLART_VALIDATION_INPUT0_SIZE; i++) {
                  uint8_t p = ~(resized_img[i]);    
                  nn_input_buffer[i] = p;
              }

              // 推論
              int64_t infer_time = esp_timer_get_time();
              nnablart_validation_inference(_context);
              infer_time = (esp_timer_get_time() - infer_time) / 1000;

              // 推論結果をフェッチ
              float *probs = nnablart_validation_output_buffer(_context, 0);

              int top_class = 0;
              float top_probability = 0.0f;
              for (int classNo = 0; classNo < NNABLART_VALIDATION_OUTPUT0_SIZE; classNo++) {
                  if (top_probability < probs[classNo]) {
                      top_probability = probs[classNo];
                      top_class = classNo;
                  }
              }
                
              Serial.printf("Result %d   Inferrence-time %ums", top_class, (uint32_t)infer_time);  
            } 

    ~省略~

プログラムをコンパイルして書き込んだら、元のスケッチ(web_cam)を動かすときと同じ要領でWebブラウザで実機にアクセスし、Start Streamボタンでストリーミングを開始すると推論が動きます。推論の結果はシリアルモニタに表示されます。次の通り、若干正答率が悪い気がしますが、推論できている様子がわかります。

【STEP2】 自分で用意した画像データセットで学習したモデルで推論する

いよいよ、自分で用意したデータセットを使えるようにしていきます。手順は次の通りです。青文字の部分がSTEP1に追加となる手順です。

No. 作業内容 作業環境
CSVファイルでデータセットのリストを作成する(nnablaでデータセットを読み込むときに必要)。 Python
nnablaのMNISTサンプルプログラムを、MNISTデータセットではなく自分のデータセットを読み込めるように改造する。そのプログラムを実行し学習済みモデルのファイル(.nnp)を出力する。 Python
nnabla_cliコマンドでnnpファイルをCソースコードに変換する。 Python
変換で得られたCソースコードをスケッチweb_camに取り込む。 Arduino
Arduinoのライブラリとしてnnablaのランタイム(nnabla-c-runtime)を取り込む(STEP1ですでにライブラリ化していれば作業不要)。 Arduino
カメラ画像から推論するように、スケッチのソースコードを改造する。 Arduino

手順⓪~②はColabでの作業です。ColabノートブックをGitHubで公開します。ノートブックにコメントも記載していますが、より詳細な解説を以降に記載しますので合わせてご覧ください。

github.com

STEP2 手順⓪

nnablaにデータセットを読み込ませるためには、データセットのリストをCSVファイルで渡す必要があります。 CSVファイルは次のようなフォーマットで、学習用とテスト用に分けて作成します。

x:image,y
画像データのファイルパス, 分類(0,1,...,n)
画像データのファイルパス, 分類(0,1,...,n)
画像データのファイルパス, 分類(0,1,...,n)
・
・
・

今回、分類は次のようにしました。

0: 手が写っていない画像
1: グーの画像
2: チョキの画像
3: パーの画像

データセットGitHubのmy_datasetフォルダに、このような感じで分類毎のフォルダに分けて入っています。

今回、このようにフォルダ分けしたデータセットから、自動的にCSVファイルを作成するPythonスクリプトも作ってGitHubに入れました(make_dataset_csv.py)。データセットを8:2の割合で分けて、train.csvとtest.csvを作成してくれます。さらに、データセットの画像を28x28のグレースケールに変換してくれます(変換後の画像データはconverted_datasetsフォルダに保存されます)。

Colabノートブックでは、このデータセットスクリプトを利用してCSVファイルを作成しています。

STEP2 手順①

STEP1のColabノートブック上で使用したnnablaのMNISTサンプルプログラム(classification.py)を、MNISTデータセットの代わりにCSVファイル(train.csv,、test.csv)を読み込むように改造します。

改造の仕方はこちらの記事がとても参考になりますのでここでの解説は割愛しますが、改造済みのプログラム(classification_mydata.py)をGitHubに置いています。Colabノートブックでもこのプログラムをダウンロードして使用しています。

cedro3.com

STEP2 手順②~⑤

手順②以降はSTEP1とまったく同じです。STEP2の最終的なArduinoスケッチはGitHubArduino/web_cam_mydataに入れてあります。

以下、Timer Camera Xにスケッチを書き込んで実行した様子です。うまく推論できているのがわかりますが、影の位置も学習してしまったようで、影が違うところにくると正解率が著しく悪くなりました^^;

まとめ

M5Stack Timer Camera XというESP32ベースのデバイスを使ってディープラーニングの推論を動かしました。すぐできるだろうと軽い気持ちで始めましたが、意外と情報が少なく、まずnnablaが良さそうだという情報にたどり着くまでに相当時間がかかりました。その後もいろいろなところでハマり、やっぱりラズパイでやるべきなのか...と思ったりもしましたが、このブログに書くことをモチベーションにして何とか最後までがんばることができました...汗

今後の展望としては、こちらの記事でやったことを組み合わせて簡単なAIロボットを作れたら楽しそうだなと妄想しています。

mzmlab.hatenablog.com

参考書籍・サイト

文中で紹介したものですが、改めて参考書籍・サイトを記載します。

人気ブロガーからあげ先生のとにかく楽しいAI自作教室 (Amazon)

M5Stackは扱われていませんが、ディープラーニングを学び直したり今回の挑戦をしたりするきっかけになった本です。また、じゃんけんの判定をしたりGoogle Colaboratoryを使ったりしているのはこの本の影響を強く受けています^^;

マイコンでディープラーニングした話 on ESP32 - Qiita

M5StackでNeural Network Library (nnabla)のMNISTのサンプルを動かす方法について参考にさせていただきました。

SONY Neural Network Libraries データセットの読ませ方 | cedro-blog

nnablaに独自のデータセットを読み込ませる方法について参考にさせていただきました。

IoT乾電池MaBeeeをESP32でコントロールしてみた

MaBeeeは、スマホアプリから電圧をコントロールすることができる乾電池で、これを使えば電車などのおもちゃをラジコンのようにコントロールすることができます。今回はこのMaBeeeを、スマホではなくESP32マイコンからコントロールできるようにしてみました。これにより、例えばMaBeeeとM5Stack(ESP32を搭載した小型デバイス)をおもちゃやロボットキットに組み込めば、はんだ付け等の手間をかけずに自動制御の改造ができそうです。

MaBeeeをBLEで制御する、ESP32のプログラム

MaBeeeはBluetooth LE (BLE)で制御できるように作られています。ESP32はBLEモジュールを内蔵しており、ライブラリを利用してプログラミングすることができます。以下がプログラムの全容です。ソースコード(Arduino用)も貼ります。

  • MaBeeeがBLEサーバ(ペリフェラル)なので、ESP32がBLEクライアントになります。
  • BLEデバイスをスキャンし、MaBeeeを見つけたら接続します。接続後はUUIDによりPWM制御のサービス、キャラクタリスティックを取得します。
  • 以降は、キャラクタリスティックを使い、1秒毎にPWM値を0~100%の間で少しずつ変化させながらWriteします(100までいった後は少しずつ小さくしていきたいので、sin関数を使っています)。

動いている様子を動画で紹介します。 MaBeeeにつないだテスターの電圧測定値が少しずつ変化しています。 ESP32デバイスとしてM5Stack(Timer Camera X)を使っています。

youtu.be

■BLEClient.ino

#include "BLEDevice.h"
#include <string.h>
#include <math.h>

#define SERVICE_UUID    "b9f5ff00-d813-46c6-8b61-b453ee2c74d9"
#define CHARACTERISTIC_UUID   "b9f53006-d813-46c6-8b61-b453ee2c74d9"    //pwm-duty
#define SERVER_NAME           "MaBeee018904"
#define M_PI  (3.141592)

static BLEUUID serviceUUID(SERVICE_UUID);
static BLEUUID charUUID(CHARACTERISTIC_UUID);

static BLEAddress *pServerAddress;
static BLEAdvertisedDevice* myDevice;
static boolean doConnect = false;
static boolean connected = false;
static BLERemoteCharacteristic* pRemoteCharacteristic;

bool connectToServer(BLEAddress pAddress) {
    Serial.print("Forming a connection to ");
    Serial.println(pAddress.toString().c_str());
    BLEClient* pClient = BLEDevice::createClient();
    pClient->connect(myDevice);
    Serial.print(" - Connected to server :");
    BLERemoteService* pRemoteService = pClient->getService(serviceUUID);
    Serial.print(" - Get service :");
    Serial.println(serviceUUID.toString().c_str());
    if(pRemoteService == nullptr){
      Serial.print("Failed to find our service UUID: ");
      Serial.println(serviceUUID.toString().c_str());
      return false;
    }
    pRemoteCharacteristic = pRemoteService->getCharacteristic(charUUID);
    Serial.print(" - Found our characteristic UUID: ");
    Serial.println(charUUID.toString().c_str());
    if (pRemoteCharacteristic == nullptr) {
      Serial.print("Failed to find our characteristic UUID: ");
      return false;
    }
    Serial.println(" - Found our characteristic");
}

//
// スキャンでデバイスを見つけたときのコールバック
//
class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks {
  void onResult(BLEAdvertisedDevice advertisedDevice) {
    Serial.print("BLE Advertised Device found: ");
    Serial.println(advertisedDevice.toString().c_str());
    String serverName = advertisedDevice.getName().c_str();
    Serial.println(serverName);

    //目的のデバイスだったら接続し、スキャンを終了
    if(serverName.equals(SERVER_NAME)){ 
      Serial.println(advertisedDevice.getAddress().toString().c_str());
      advertisedDevice.getScan()->stop();
      pServerAddress = new BLEAddress(advertisedDevice.getAddress());
      myDevice = new BLEAdvertisedDevice(advertisedDevice);
      doConnect = true;
    }
  }
};

//
// 起動後一度だけ呼ばれる処理
//
void setup() {
  pinMode(5, OUTPUT);
  Serial.begin(115200);
  BLEDevice::init("");
  BLEScan* pBLEScan = BLEDevice::getScan();
  Serial.println("getScan");
  pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks());
  Serial.println("setAdvertisedDeviceCallbacks");
  pBLEScan->setActiveScan(true);
  pBLEScan->start(10);
  Serial.println("");
  Serial.println("End of setup");
}

//
// メインループ処理
//
void loop() {
  static float w = M_PI * 3 / 2;
  uint8_t duty = 0;
  uint8_t command[5];
  
  if (doConnect == true) {
    if (connectToServer(*pServerAddress)) {
      Serial.println("We are now connected to the BLE Server.");
      connected = true;
    } else {
      Serial.println("We have failed to connect to the server; there is nothin more we will do.");
      connected = false;
    }
    doConnect = false;
  }

  // PWM値を少しずつ増減させる
  if (connected) {
    w += M_PI / 10;
    if(w > M_PI * 2){
      w = 0;
    }
    duty = (sin(w) + 1.0) * 100.0 / 2.0;
    command[0] = 0x01;
    command[1] = duty;
    command[2] = 0x00;
    command[3] = 0x00;
    command[4] = 0x00;

    pRemoteCharacteristic->writeValue(command, 5);
    Serial.println(duty);
  } else{
    Serial.println("Not connected");
    doConnect = true;
  }
  delay(1000);
}

余談

MaBeeeのBLEの仕様は公開されていなかったため、自力で解析する必要がありました。 具体的には、上記プログラムで使用している以下の情報を調べる必要がありました。

  • サーバ名 (SERVER_NAME)
  • サービスUUID (SERVICE_UUID)
  • キャラクタリスティックUUID (CHARACTERISTIC_UUID)
  • PWM値をWriteする際のコマンドフォーマット "0x01 XX 00 00 00" (XX : 0x00~0x64のPWM値)

まずはこちらの記事を参考に、サーバ名とUUIDを調べました。

BLE(Bluetooth Low Energy)を人力で解析する - Qiita

しかし、コマンドフォーマットは勘を頼りにいろいろ試しましたが、まったくわからず...

そこで次に、こちらの記事を参考に、ラズパイ+micro:bitでスニファを構成し、Mabeeeとスマホアプリ「MaBeeeコントロール」の間の通信をのぞき見することで、ようやくコマンドフォーマットを推測することができました。

snifferでBLEをキャプチャする->Micro:Bitでなんとかなるかと - chakokuのブログ(rev4)

その他参考サイト

ESP32によるBLEクライアントの作成方法については、以下の記事を参考にさせていただきました。

ESP32のArduino IDEでBLEを試してみました – 計測・解析 ラボ

ESP32・BLEクライアントの開発

ESP32によるBLEスキャンとアルプスのセンサネットワークモジュール/SensorTagからのセンサーデータ取得 | TomoSoft