3,000円の球形XiaoZhi AI端末をMuse Gadget化する
<!--
タグ候補: ESP32 / ESP-IDF / 電子工作 / GoogleCloud / WebSerial
想定読者: Web・アプリ開発の経験はあるが、組み込み開発にはあまり詳しくないエンジニア
検証基準: spotpear-v0.2.0 / 2026-11-02
-->
はじめに
3,000円で購入できる円形液晶とマイク・スピーカーを搭載した SpotPear ESP32-AI-1.28 v1.0 に、Muse Gadget SDKを移植しました。
ボタンを押して話しかけると、返答を画面で読めて、スピーカーからも聞ける小さな端末です。
音量や読み上げの声を変えるたびにプログラムを書き直さずに済むよう、シリアル接続でWebブラウザーから設定する画面も追加しました。
最終的に実装したのは、以下の機能です。
1. BOOTボタンを1回押して録音を開始する
2. 話し終わったら、もう1回押して送信する
3. Museの返答を日本語で画面に表示する
4. 同じ返答をGoogle Cloud Text-to-SpeechのWaveNetで読み上げる
5. 音量・消灯時間・読み上げの声などの設定をWeb Serial UIから変更し保存する
この記事では、組み込み開発にあまり慣れていない方にも流れが伝わるように、仕組み、導入手順、途中でつまずいた点を順に説明します。
まず動かしてみたい方は第3章の導入手順から、設定画面の使い方を知りたい方は第8章から読むと流れをつかみやすいと思います。
ソースコードは以下で公開しています。この記事の再現対象は spotpear-v0.2.0 です。
- リポジトリ:tomorrow56/muse-gadget-sdk
- この記事で使用するタグ:spotpear-v0.2.0
- Fork元:facebookincubator/muse-gadget-sdk
1. どんな構成で動いているのか
まず、ESP32とクラウド、スマートフォン、PCの役割を整理します。
今回の構成では、ESP32が録音、画面表示、音声再生を担当します。録音した声への返答はMuseのサービスが生成し、その返答文をGoogle Cloud Text-to-Speechが音声に変換します。
TTSはText-to-Speechの略で、「文字を音声に変換する機能」のことです。
flowchart LR
Input["マイク・BOOTボタン"] --> ESP["ESP32-S3"]
ESP -->|"Wi-Fiで音声を送信"| Muse["Museサービス"]
Muse -->|"返答テキスト"| ESP
ESP -->|"返答文を送信"| TTS["Google Cloud Text-to-Speech"]
TTS -->|"MP3音声"| ESP
ESP --> LCD["円形液晶に日本語表示"]
ESP --> Speaker["スピーカーで読み上げ"]
スマートフォンのMuseアプリは、最初のペアリングやWi-Fi設定で使用しました。PCは、ファームウェアの書き込みやUSB経由の設定変更で使用します。
ここでいう「ESP32単体で使える」は、普段の動作に、常時起動したPCや中継用のプログラムが必要ないという意味です。
利用には端末への給電とWi-Fi接続が必要で、返答の生成や読み上げにはクラウドサービスを利用します。
Web Serial UIは、設定用のブラウザー画面です。ESP32で動かすAIとは役割が異なります。詳しくは後ほど説明します。
2. 使用したデバイス
実機で確認したのは、 SpotPear ESP32-AI-1.28 v1.0の旧型・脚なしの基板です。
製品リンク: ja.aliexpress.com
ユーザーガイド: spotpear.com
| 項目 | 今回の構成 |
| --- | --- |
| マイコン | ESP32-S3 |
| Flash | 16 MB |
| PSRAM | 8 MB、Octal接続 |
| 液晶 | GC9A01、240×240ピクセル、円形 |
| スピーカー用アンプ | MAX98357A |
| マイク | NP0557 |
| 操作ボタン | BOOT(GPIO0) |
| PCとの接続 | ESP32-S3のUSB Serial/JTAG |
Flashは主にプログラムや設定を残しておく領域、PSRAMは主に画像や音声を処理するときに使う作業用メモリです。
GPIOは、マイコンの入出力端子を指す名前です。表のGPIO0は、今回の基板でBOOTボタンにつながっている端子番号です。
この基板には液晶や音声回路があらかじめ載っていますので、それらを配線する作業はありませんが、ファームウェアでは「どの信号がどのピンにつながっているか」を指定する必要があります。
周辺回路の接続ピンは以下となります。
|Peripheral|Configuration|
| --- | --- |
|LCD:GC9A01|DC:10, CS:13, CLK:14, MOSI:17, RESET:18|
|Backlight|GPIO3, active high|
|BOOT|GPIO0, active low|
|Speaker:MAX98357A(I2S0 TX)|BCLK:15, WS:16, DATA:7|
|Mic:NP0557(I2S1 RX)|BCLK:5, WS:4, DATA:6|
同じ1.28インチ系でも、ES8311を搭載した sp-esp32-s3-1.28-box などは別のリビジョンです。購入済みの基板や参考記事を確認するときは、画面サイズだけでなく、基板の型番と音声回路も確認してください。
ピン配置や液晶の初期化は、同じハードウェア向けのXiaozhi ESP32プロジェクトを参考にしました。既存の設定を使う場合も、Muse側の音声処理や描画処理に合わせる部分は別途必要でした。
- 今回のボード設定とピン配置
- 参考にしたXiaozhi側のボード実装
3. ファームウェアを用意する
ファームウェアとESP-IDF
ファームウェアは、この小さな基板の中で動くプログラムです。PC向けのアプリと違い、ソースコードをビルドした後、USB経由で基板のFlashに書き込んで使います。
今回は、ESP32のメーカーであるEspressifが提供する開発環境 ESP-IDF v6.0.1 を使います。このリポジトリで指定されているバージョンに合わせました。
以下のコマンド例はmacOSのターミナルを想定しています。
ESP-IDFを準備する
初めて導入する場合は、GitやPythonなどの必要なツールを含めて、Espressifの公式手順を確認してください。
必要なツールが入ったMacで、CLIからv6.0.1を用意する一例は次のとおりです。
mkdir -p ~/esp
cd ~/esp
git clone --branch v6.0.1 --recursive \
https://github.com/espressif/esp-idf.git esp-idf-v6.0.1
cd esp-idf-v6.0.1
./install.sh esp32s3
. ./export.sh
idf.py --version
install.sh はESP32-S3向けの開発ツールを用意する処理、export.sh はそのツールを現在のターミナルから使えるようにする処理です。
別のターミナルを開いて作業を再開するときは、次を実行して環境を有効にします。
. ~/esp/esp-idf-v6.0.1/export.sh
すでに別バージョンのESP-IDFを使っている場合も、今回はv6.0.1を有効にしていることを idf.py --version で確認します。
Muse側のソースを取得する
mkdir -p ~/esp/projects
cd ~/esp/projects
git clone --branch spotpear-v0.2.0 --single-branch \
https://github.com/tomorrow56/muse-gadget-sdk.git
cd muse-gadget-sdk/esp32
タグを指定しているのは、記事と同じ時点のソースを取得するためです。Gitが detached HEAD と表示する場合も、このタグの内容をビルドする用途ではそのまま進められます。
必要な認証情報は2種類ある
今回の構成では、次の2種類の認証情報を使います。
| 認証情報 | 用途 | 入手先 |
| --- | --- | --- |
| Muse Gadgets SDKトークン | Muse Gadgetとして接続するため | gadgets.muse.aiのAccount → SDK tokens |
| Google Cloud APIキー | 返答文を音声に変換するため | Google Cloudの認証情報画面 |
Google Cloud側の準備は、次の順で進めます。
1. Google Cloudコンソールで、使用するプロジェクトを選ぶ。
2. そのプロジェクトに請求先が設定されていることを確認する。
3. Cloud Text-to-Speech API を有効にする。
4. APIとサービス → 認証情報 → 認証情報を作成 → APIキー を開く。
5. APIの制限で Cloud Text-to-Speech API を指定し、キーを作成・保存する。
今回の構成では通常のAPIキーを使うため、サービスアカウントへの紐付けは行っていません。キーの作成画面や権限の詳細は、Google公式手順を参照してください。
基板に合わせた設定を読み込む
ここで使う sdkconfig は、ファームウェアの機能を選ぶ設定ファイルです。menuconfig というターミナル上の設定画面から編集できます。
以下は、共通設定にMuse用の設定、SpotPear用の設定を重ねて読み込むコマンドです。
idf.py -B build-muse-spotpear-esp32-ai-128 \
-DIDF_TARGET=esp32s3 \
-DSDKCONFIG=build-muse-spotpear-esp32-ai-128/sdkconfig \
'-DSDKCONFIG_DEFAULTS=sdkconfig.defaults;devices/sdkconfig.muse;devices/sdkconfig.muse-spotpear-esp32-ai-128' \
menuconfig
設定画面はキーボードで操作します。画面下部に表示される操作ガイドを確認し、値を変更したら保存・終了します。
設定画面では / キーで設定名を検索できます。GADGET_SDK_TOKEN や MUSE_GOOGLE_TTS を検索すると、対応する項目を探せます。
読み上げを有効にする場合、設定内容は次のようになります。下の値は説明用のダミーです。実際の値は、手元のビルド用 sdkconfig に設定します。
CONFIG_GADGET_SDK_TOKEN="YOUR_MUSE_SDK_TOKEN"
CONFIG_MUSE_GOOGLE_TTS=y
CONFIG_MUSE_GOOGLE_TTS_API_KEY="YOUR_GOOGLE_CLOUD_API_KEY"
CONFIG_MUSE_GOOGLE_TTS_LANGUAGE="ja-JP"
CONFIG_MUSE_GOOGLE_TTS_VOICE="ja-JP-Wavenet-B"
MUSE_GOOGLE_TTS_VOICE は、まだ端末に声を保存していないときの初期値です。今回の実装では、あとからWeb Serial UIで変更できるようにしています。
SDKトークンやAPIキーは、Git管理から除外されたビルド用の設定にだけ保存します。基板用の共通設定ファイルへ書くと、そのまま公開してしまう原因になります。また、キーを組み込んだファームウェアのバイナリやFlashのバックアップにも認証情報が含まれるため、公開用ファイルとは分けて扱います。
ビルドして書き込む
設定を保存したら、次を実行します。
idf.py -B build-muse-spotpear-esp32-ai-128 build
build はソースコードから基板に書き込めるファイルを作る処理です。
続いて、USBで基板を接続します。macOSでは、たとえば次のコマンドでシリアルポート名を確認できます。
ls /dev/cu.usbmodem*
ポート名は環境によって異なります。以下の XXXX は、実際に見つかったポート名へ置き換えてください。複数の機器がある場合は、対象がSpotPear機であることを確認します。
idf.py -B build-muse-spotpear-esp32-ai-128 \
-p /dev/cu.usbmodemXXXX flash
flash が、実機へ書き込む処理です。
以前のXiaozhiなどから切り替える場合、プログラムや設定の配置が異なります。今回はMuse側のビルドで生成されたブートローダー、パーティションテーブル、書き込み情報を使いました。別のファームウェアの書き込み位置を流用せず、最初に元の状態をバックアップしておくと戻せます。
4. 初期設定でつまずいたこと ― BLE接続とアプリの相性
書き込み後は、Museアプリからデバイスを登録し、初期設定(Wi-Fi設定)を行います。
最初に使った Android 11 / Muse 13.0.0.4.159 の組み合わせでは、デバイスは見つかるものの、アプリの表示が「接続中」から進みませんでした。
Android側のログを確認したところ、通知を有効にする処理で NoSuchMethodError が発生していました。
呼び出していた BluetoothGatt.writeDescriptor(BluetoothGattDescriptor, byte[]) はAPI level 33で追加されたメソッドで、Android 11のAPI level 30にはありませんので(Android公式リファレンス)、Android13以降のバージョンもしくはiPhoneを使用する必要があります。
なお、一度初期設定をしてしまえば、Android11のアプリでも連携して動作させることができます。
診断の詳細はリポジトリに残しています。
5. 日本語を表示し、円形画面で読みやすくする
日本語用のフォントを有効にする
接続後に日本語での返答文を表示するために、GNU Unifontの16ピクセルの日本語向けフォントを有効にしました。
CONFIG_MUSE_CJK_FONT=y
CONFIG_MUSE_CJK_FONT_NOTO=n
この設定はSpotPear用プロファイルに含めてあります。実機で、ひらがな・カタカナ・漢字を含む返答が読めることを確認しました。
丸い画面では、文字の置き場所も重要
240×240ピクセルの画像を描けても、その四隅まで画面として見えるわけではありません。円形液晶では、文字を下端や左右いっぱいに置くと切れてしまいます。
そこで、同じく円形液晶を使う Waveshare ESP32-S3-Touch-LCD-1.28 の実装を参考に、アバターと返答文の位置を調整しました。
| 調整した項目 | 今回の値 |
| --- | --- |
| アバターの大きさ | 128ピクセル |
| アバターの位置 | 上方向へ25ピクセル移動 |
| 返答文の表示幅 | 160ピクセル |
| 返答文と画面下端の余白 | 34ピクセル |
| 日本語の表示 | 16ピクセルの文字で2行、返答に合わせてページ表示 |
アバターを少し小さくして上へ寄せ、文字専用の領域を下側に確保しています。
文字を単に中央へ寄せるだけでは、アバターと重なって読みにくくなります。今回は、画面の中でそれぞれが使う場所を分けることで読みやすくなりました。
6. ボタンは「押し続ける」から「押すたびに切り替える」へ
最初の操作は、BOOTボタンを押している間だけ録音する方式でした。短く押してすぐ離すと、録音もすぐ終わるため、話しかけるには押し続ける必要があります。
今回は、次の操作へ変更しました。
| 操作 | 動作 |
| --- | --- |
| BOOTを1回押す | 録音開始 |
| ボタンを離す | 録音を継続 |
| もう1回押す | 録音終了・送信 |
| 15秒に達する | 通常の録音上限として自動終了 |
対応する設定は次です。
CONFIG_MUSE_VOICE_NOTE_TOGGLE=y
CONFIG_MUSE_REALTIME_VOICE=n
これは、録音した音声をボイスメモとして送って返答を受け取る使い方です。
BOOTの5秒長押しは初期設定をリセットする操作なので、普段の録音では短く押します。
7. SPEAKINGでのGoogle TTSでの読み上げ対応
返答文からスピーカーまでの流れ
実装では、次の順に処理します。
1. Museから返答文を受け取る。
2. Google Cloudへ、返答文・言語・声・出力形式を送る。
3. 返ってきたJSONの audioContent をMP3のデータへ戻す。
4. MP3を再生用のPCMへ変換する。
5. ボードの音声回路へ渡し、スピーカーから出力する。
PCMは、音の波形を数値の列にしたデータです。圧縮されたMP3を、音声回路へ渡せる形へ戻す処理を「デコード」と呼びます。
リクエスト内容の例は次のとおりです。
{
"input": {
"text": "こんにちは。ミューズの音声読み上げテストです。"
},
"voice": {
"languageCode": "ja-JP",
"name": "ja-JP-Wavenet-B"
},
"audioConfig": {
"audioEncoding": "MP3",
"sampleRateHertz": 16000
}
}
Googleの応答には、MP3がBase64という文字列表現で入っています。これを元のバイト列へ戻してから、既存のMP3再生処理につなぎました。Google公式の音声生成手順
Googleへの通信は専用の処理タスクで行い、その待ち時間にMuseとの接続処理が止まらないようにしています。また、録音をやり直した場合に古い返答を混ぜないよう、キャンセルした処理のデータは、新しい返答のバッファへ書き込ませない構成にしました。
スピーカーOFFまたは音量0の場合は合成をスキップします。Googleへのリクエストに失敗しても、返答文は画面に表示します。
実機では証明書の検証設定にも対応が必要だった
音声取得には成功した一方、実機では最初、HTTPS接続の証明書検証に失敗しました。
今回確認したGoogle側の証明書チェーンに対応するため、ESP-IDFの次の設定を有効にしました。
CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_CROSS_SIGNED_VERIFY=y
これはクロス署名付きの証明書を検証するための設定です。今回のコードでは、Google TTSを有効にすると、この設定も選択されます。
その後、固定文の合成・再生と、普段のBOOT操作から送った音声への返答読み上げの両方を実機で確認できました。
WaveNetの声と費用
Web Serial UIから選べる日本語WaveNetは、次の4種類です。
| 音声名 | 種類 |
| --- | --- |
| ja-JP-Wavenet-A | 女性 |
| ja-JP-Wavenet-B | 女性・今回の初期値 |
| ja-JP-Wavenet-C | 男性 |
| ja-JP-Wavenet-D | 男性 |
声のサンプルは、Google公式の対応音声一覧で確認できます。
2026年11月2日に確認した公式料金表では、WaveNetは月400万文字まで無料枠があり、超過分は100万文字あたり4米ドルです。 利用には請求先の有効化が必要で、無料枠を超えると課金されます。日本語の料金カウントはUTF-8のバイト数ではなく文字数です。利用前には最新の料金表を確認してください。
8. Web Serial UIで、設定をブラウザーから変更する
Web Serialとは
Web Serialは、ブラウザーからUSB接続した機器のシリアル通信を扱う仕組みです。今回の画面では、ChromeからESP32へ短いコマンドを送り、現在値の確認や設定変更を行います。Chrome公式ドキュメント
端末本体に設定用のWebサーバーを追加する作業はありません。PC側で画面を配信し、ChromeとESP32がUSBで通信する構成です。
PC内のPython HTTPサーバー
│ 画面のHTML・JavaScriptを配信
▼
Chrome
│ USBシリアルで確認・設定
▼
ESP32
│
└─ 設定を本体へ保存
設定画面を起動する
リポジトリの esp32 ディレクトリで、次を実行します。
python3 -m http.server 8765 --bind 127.0.0.1 --directory tools/muse
これは、このPC内だけでアクセスできる設定画面用のHTTPサーバーを起動するコマンドです。実行中はターミナルを開いたままにします。
Chromeで次のURLを開きます。
http://localhost:8765/serial_setup.html
macOSなら、別のターミナルで次を実行しても開けます。
open -a "Google Chrome" http://localhost:8765/serial_setup.html
Web Serialを使うため、HTMLファイルを直接開くのではなく、localhost経由で開きます。サーバーの終了は、そのターミナルで Ctrl+C です。
設定を変更する
1. 基板をUSBデータケーブルで接続する。
2. 画面の Connect via USB を押し、対象のポートを選ぶ。
3. 現在の設定が表示されたことを確認する。
4. 値を変更し、Save changes を押す。
初期表示は英語です。Languageを日本語にすると、USBで接続、変更を保存などの表記へ切り替わります。接続中や編集中でも切り替えられ、ページを再読み込みすると英語へ戻ります。
設定できる項目は以下です。
| 項目 | 内容 |
| --- | --- |
| 明るさ | 10〜100% |
| 自動消灯までの時間 | 0〜3600秒。0で消灯しない |
| スピーカー音量 | 0〜100% |
| スピーカーON/OFF | 読み上げの出力を切り替える |
| マイクゲイン | 0〜36 dB。声を拾う大きさを調整する |
| 読み上げの声 | 日本語WaveNet A〜D |
今回の端末では、画面を表示したままにしたかったため、自動消灯を 0秒 にしています。
読み上げの声は、Sound → Text-to-speech voice、日本語表示なら サウンド → 読み上げの声 から選びます。保存すると次の返答から反映し、再ビルドや再起動は不要です。すでに合成・再生している音声は、そのときに選ばれていた声で続きます。
設定の保存
設定はESP32の NVS に保存します。NVSは、電源を切っても値が残る保存領域です。
そのため、ブラウザーを閉じても、端末を再起動しても、保存した声や消灯時間は保持されます。実際にWaveNet Dを保存して端末を再起動し、Dの設定が残ることと、その声で音声を取得できることを確認しました。
また、保存コマンドの応答を待った後、もう一度現在値を読み戻して、指定した値と一致することを確認しています。通信が切れたり、読み戻しが一致しなかったりした場合は、保存成功として扱いません。
Statusには、読み取れる項目をまとめて表示する
設定欄のほかに、Complete device status/デバイスの全Status を用意しました。
ここには、機器名、ファームウェア、Wi-Fiの状態、IPアドレス、電波強度、MuseやLinkの接続状態、設定値など、status が返す項目を表示します。TTSの有効状態、言語、保存した声も確認できます。
UIが知らない項目が将来追加された場合も表示できるようにし、元のJSONも確認できるようにしました。画面の説明やボタンは翻訳しますが、機器から返されたキー名や値はそのまま表示します。
Google TTSに対応していない旧ファームウェアでは、声の選択欄を無効にして、明るさや音量などの既存設定を引き続き使えるようにしています。
9. UARTコマンドでも同じ設定を確認できる
ブラウザー画面の裏側では、既存のシリアルコマンドを使っています。シリアル端末から直接操作する場合も、同じ仕組みです。通信速度は 115200 bps です。
各行の末尾で改行を送ります。先頭の > もコマンドの一部です。
>status
>sleep=0
>tts_voice=ja-JP-Wavenet-D
>tts.test
| コマンド | 動作 |
| --- | --- |
| >status | 現在の設定や接続状態を読む |
| >sleep=0 | 自動消灯を無効にして保存する |
| >tts_voice=ja-JP-Wavenet-D | 読み上げの声をDにして保存する |
| >tts.test | 保存済みの声で、固定の日本語文を合成・再生する |
>tts.test はGoogleへ1回リクエストして音声を生成します。試聴にも通常のTTS利用量が発生します。
StatusのTTS部分を抜き出すと、次のような内容になります。
{
"tts": {
"enabled": true,
"voice": "ja-JP-Wavenet-D",
"language": "ja-JP"
}
}
USBポートは、Web Serial UIと別のシリアルモニターで同時に使わず、切り替えるときは先に切断します。今回の作業でも、書き込みと状態確認が重なってUSB通信が競合したことがあり、順番にポートを使うようにしてやり直しました。
10. 現時点の制限について
ファームウェアのサイズは約3.35 MBで、4 MBのアプリ領域に対して約20%の空きがありました。
制限内容
- 連続したライブ通話の動作は、この構成では確認できていません。
- Googleへ渡す返答文は、既存の保持領域に合わせて1メッセージあたり最大1023バイトです。UTF-8の日本語では、文字数とバイト数が異なります。
- TTSの合成・ダウンロード待ちがあるため、返答文が届いてから音が出るまで時間がかかる場合があります。
- 今回のTTS実装はPSRAMを使う構成が前提です。2 MB向けのコンパクトバッファ構成では有効にできません。
- 動画・画像表示、OTA更新、長時間連続動作などは、今回の動作確認の対象外です。
おわりに
小さなESP32端末でも、録音、クラウドへの送信、日本語表示、TTSによる読み上げを組み合わせると、ボタンで使えるAI端末にできました。
設定をWeb Serial UIへまとめたことで、普段はソースを触らず、ブラウザーから音量や読み上げの声を調整して使えるようになりました。
実装内容への参考リンク
- ボード実装:液晶・I2S・ピン設定
- Google TTS:リクエストと音声取得
- 設定の保存:NVSと音声選択
- Web Serial UI:HTML画面
- Google TTSの設定手順
- Web Serial UIの操作手順
1 like
BluetoothのないUbuntuで、MuseにBlenderのモデル作成・編集を任せてみた
Museで、自分のPCにある3Dモデルを操作できるか試しました。公式のMuse Gadget SDKをUbuntuに入れ、Blenderでロボットを作成。その後、保存したモデルを開いて片腕を上げるところまで動きました。
途中で詰まったのはBluetoothです。作業用PCにアダプターがなかったので、別のLinux PCのものをUSB/IPとSSHで一時的に借りました。ペアリング後はそれを返し、ネットワーク経由でBlenderを実行できています。
確認日は2026年11月3日です。環境構築と確認にはCodexを使い、Blenderの作成・編集スクリプトはMuseに書かせました。認証やスマホのペアリングは自分で操作しています。
既存の記事と今回の範囲
Qiitaには、すでにRaspberry PiでMuse Gadgetsを使う記事があります。MuseのVMで開発ツールを動かす記事も参考になります。
今回は、手元のUbuntu PCにあるBlenderを動かす構成と、そのPCにBluetoothがない場合の接続を記録します。一般的なMuseの紹介や、登録手順は省きます。
使った環境
| 項目 | 今回の環境 |
| --- | --- |
| 作業用PC | Ubuntu 24.04、Bluetoothなし |
| Bluetoothを借りたPC | 同じLAN内の別のUbuntu PC |
| アダプター | Intel 8087:0036。BLEと広告機能を確認 |
| Python | 3.12.3 |
| Muse Gadget SDK | 0.1.0 |
| SDKのコミット | d32ddd74fec5fb404c2d5be423044f2ea4895130 |
| Blender | 5.2.2 LTS |
| ペアリング | iPhoneのMuseアプリ |
Blenderはインストール済みのものを使いました。以下のパスは自分の環境に合わせて読み替えてください。
今回使った公式SDKはこちらのLinux版です。Linux用のデスクトップアプリを探すところから始めましたが、この用途にはLinux版Gadget SDKで接続できました。
Muse Codeも先に試していますが、以下のBlender操作に使ったのは一般のMuseとGadget SDKです。Muse Codeの利用枠と、Museの設定画面に出る追加トークンは分けて確認しています。この実験で追加トークンを何トークン使ったかは計測していません。
コマンドを実行するユーザーを分ける
SDKにはsystem.runがあり、Linux上でシェルコマンドを実行できます。普段のユーザーをそのまま渡すのは避けて、muse3dを作りました。
sudo useradd --system --user-group --create-home \
--home-dir /var/lib/muse3d --shell /usr/sbin/nologin muse3d
sudo chmod 700 /var/lib/muse3d
sudo install -d -m 700 -o muse3d -g muse3d /var/lib/muse3d/workspaces
id muse3d
muse3dにはsudo権限を付けていません。SDKのサービスはBluetoothと認証情報を扱うためrootで動き、コマンド実行時にmuse3dへ権限を落とします。
ただし、ユーザーを分けただけで完全なサンドボックスになるわけではありません。muse3dに読めるファイルや、利用できるネットワークへのアクセスは残ります。Blenderだけを許可する設定でもありません。
ペアリングを後回しにしてSDKを入れる
SDKの導入とBluetoothペアリングは分けて進めました。先にSDKだけ準備しています。
使ったコミットを固定して、Linuxのソースを取得します。
git init muse-gadget-sdk
cd muse-gadget-sdk
git remote add origin https://github.com/facebookincubator/muse-gadget-sdk.git
git sparse-checkout init --cone
git sparse-checkout set linux
git fetch --depth 1 --filter=blob:none origin d32ddd74fec5fb404c2d5be423044f2ea4895130
git checkout --detach FETCH_HEAD
linux/install.shを確認した後、次の指定で導入しました。
sudo bash linux/install.sh --from "$PWD/linux" \
--run-as muse3d --no-pair --yes
--no-pairでペアリングを開始せず、--run-asでコマンドを実行するユーザーを指定します。--yesはインストール時の確認に対する指定です。
この時点でmusegadget.serviceは起動します。ただ、サービスがactiveというだけではMuseとの接続完了にはなりません。
SDKトークンは端末から非表示で入力した
SDKトークンは公式のSDK tokens画面で、規約を確認して発行しました。
ここで使うmgst_から始まるトークンは、記事の最後に載せている招待コードとは別物です。SDKトークンは公開しません。
今回は端末でgetpassを使って入力し、/var/lib/musegadget/sdk_tokenへroot所有・権限600で保存しました。トークン自体をコマンドの引数には入れていません。
同じ流れを使う場合の入力用コードです。SDKを上記の場所へ導入済みであることが前提です。
sudo /opt/musegadget/venv/bin/python - <<'PY'
import getpass
import os
import tempfile
from pathlib import Path
from musegadget import config
os.environ[config.SDK_TOKEN_ENV] = getpass.getpass("Muse SDK token: ")
try:
token = config.sdk_token()
finally:
os.environ.pop(config.SDK_TOKEN_ENV, None)
if not token:
raise SystemExit("No token entered; nothing changed.")
directory = config.DEFAULT_STATE_DIR
fd, temporary = tempfile.mkstemp(prefix=".sdk_token-", dir=directory)
try:
with os.fdopen(fd, "w") as stream:
stream.write(token + "\n")
stream.flush()
os.fsync(stream.fileno())
os.replace(temporary, directory / config.SDK_TOKEN_FILE)
finally:
Path(temporary).unlink(missing_ok=True)
print("SDK token saved.")
PY
sudo systemctl restart musegadget.service
入力は画面に表示されません。config.sdk_token()はSDKに含まれる形式チェックで、ここではトークンの失効や権限をオンライン確認しているわけではありません。
SDKトークンには専用の利用条件があります。SDKのソースコードのライセンスとは別なので、使う前に確認してください。
Bluetoothは別PCから一時的に借りた
作業用PCにはBluetoothがありませんでした。別のPCにはBLE対応のアダプターがあったので、次の構成にしました。
iPhoneのMuseアプリ
│ Bluetooth
▼
別PCのBluetoothアダプター
│ USB/IPをSSHトンネルに通す
▼
作業用Ubuntu PCのMuse Gadget SDK
スマホを近づける先は、アダプターが実際に付いている別PCです。
USB/IPの共有ポートは、送信側のファイアウォールでループバックからだけ接続できるようにしました。その上で、受信側からSSHのローカル転送を張っています。USB/IPのポートをLAN全体に開放する構成にはしていません。
受信側のコマンドは次の形です。user@bt-pcはBluetoothを持つPCのSSH接続先です。送信側でのUSB/IP共有と、ループバック以外を拒否する設定は済んでいる前提です。
# このSSHは別ターミナルで起動したままにする
ssh -NT -o ExitOnForwardFailure=yes \
-L 127.0.0.1:43240:127.0.0.1:43240 user@bt-pc
# 作業用PC側
sudo modprobe vhci_hcd
sudo usbip --tcp-port 43240 list --remote 127.0.0.1
sudo usbip --tcp-port 43240 attach --remote 127.0.0.1 --busid 1-10
sudo usbip port
1-10は今回のアダプターのbusidです。別の環境では一覧で対象を確認して読み替える必要があります。
送信側は一時スクリプトで共有し、終了時にUSB/IPのbind、起動したデーモン、一時的なファイアウォールルールを戻すようにしました。最初は5分で自動終了する設定でしたが、スマホを操作する時間も必要だったため、再試行では10分に延ばしました。
この共有手順は手間が増えます。手元にBLEと広告機能に対応するアダプターがあれば、作業用PCへ直接接続して進められます。今回は手元の別PCを使う方法が実際に通るか確認しました。
送信側で使った一時共有スクリプトも載せます。usbip、usbipd、iptablesが利用でき、usbip_hostをロードできるLinux環境が前提です。受信側にはusbipとvhci_hcdが必要です。今回のusbip-utilsは2.0でした。
<details>
<summary>送信側の一時共有スクリプト(10分で終了)</summary>
busid=1-10と8087:0036は今回のアダプターに合わせた確認値です。別の機器を扱わないよう、自分の環境の値を確認してから変更してください。/tmp/muse-bt-share.shなどに保存して使います。
#!/usr/bin/env bash
# Temporary export of one Bluetooth adapter through an SSH tunnel.
set -euo pipefail
export PATH=/usr/sbin:/usr/bin:/sbin:/bin
busid=1-10
port=43240
device=/sys/bus/usb/devices/$busid
tag=muse-bt-share-demo
daemon_pid=
reject_added=0
allow_added=0
binding_started=0
allow_rule=(-i lo -p tcp --dport "$port" -m comment --comment "$tag" -j ACCEPT)
reject_rule=(-p tcp --dport "$port" -m comment --comment "$tag" -j REJECT)
[[ $EUID == 0 ]] || { echo 'Run with sudo.' >&2; exit 1; }
[[ $(cat "$device/idVendor") == 8087 && $(cat "$device/idProduct") == 0036 ]] || {
echo 'Expected Bluetooth 8087:0036 at 1-10 was not found.' >&2; exit 1;
}
[[ $(readlink -f "$device/$busid:1.0/driver") == /sys/bus/usb/drivers/btusb ]] || {
echo 'Bluetooth is already using another driver; leaving it unchanged.' >&2; exit 1;
}
[[ -z $(ss -H -ltn "sport = :$port") ]] || {
echo 'Temporary TCP port is already in use.' >&2; exit 1;
}
cleanup() {
trap - EXIT
set +e
if [[ -n $daemon_pid ]]; then
kill "$daemon_pid" 2>/dev/null
wait "$daemon_pid" 2>/dev/null
fi
if (( binding_started )) && [[ -d $device && $(cat "$device/idVendor") == 8087 && $(cat "$device/idProduct") == 0036 ]]; then
usbip unbind --busid="$busid"
if [[ -d $device ]]; then
for interface in "$device/$busid":*; do
if [[ -d $interface && ! -e $interface/driver ]]; then
printf '%s' "${interface##*/}" > /sys/bus/usb/drivers_probe
fi
done
fi
fi
if (( allow_added )); then iptables -w 5 -D INPUT "${allow_rule[@]}"; fi
if (( reject_added )); then iptables -w 5 -D INPUT "${reject_rule[@]}"; fi
echo "Temporary sharing stopped. Bluetooth driver: $(basename "$(readlink -f "$device/$busid:1.0/driver")")"
}
trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM HUP
modprobe usbip_host
# usbipd cannot select a listen address; keep this port reachable only on loopback.
iptables -w 5 -I INPUT 1 "${reject_rule[@]}"
reject_added=1
iptables -w 5 -I INPUT 1 "${allow_rule[@]}"
allow_added=1
usbipd -4 --tcp-port "$port" &
daemon_pid=$!
sleep 0.3
kill -0 "$daemon_pid"
binding_started=1
usbip bind --busid="$busid"
echo 'READY: Bluetooth 8087:0036 is shared for ten minutes through SSH only.'
echo 'Keep this terminal open. Ctrl-C restores Bluetooth immediately.'
sleep 600
</details>
受信側から送信側へ配置して実行する場合は、次の形です。sudoのパスワードはこのSSHの端末で入力しました。
scp muse-bt-share.sh user@bt-pc:/tmp/muse-bt-share.sh
ssh -t user@bt-pc 'sudo bash /tmp/muse-bt-share.sh'
READYが出た端末は開いたままにし、その後に上記のSSHトンネルとattachを実行します。中断するときは、この端末でCtrl-Cを押すと復元処理が走ります。
「接続中」で一度止まった
作業用PCで次を実行し、スマホのMuseで「設定 → デバイス → 開発者モード」を有効にして、右上の追加ボタンからMuseGadgetXXXXXXを選びます。
sudo musegadget pair
最初はスマホ側が「接続中」のまま進みませんでした。PC側には、スマホが接続してget_device_infoを要求し、SDKが応答したところまでログがありました。次の認証処理には進まず、その回の受付は時間切れになりました。
Museアプリを開き直し、Bluetooth共有とペアリング受付をやり直したところ、2回目は進みました。ログでは次を確認できています。
RX action: pairing_client_hello
TX pairing_ready
pairing confirmed (app consent)
RX action: wifi_scan (encrypted)
RX action: provision_v2 (encrypted)
TX status (encrypted #4): auth_ok
setup complete
Paired.
1回目が止まった原因は特定できていません。アプリを開き直すと必ず直る、という話にはできません。
ネットワーク選択では既存の接続を選びました。このPCはすでにオンラインなので、Wi-Fiパスワードの入力は不要でした。
ペアリング後はBluetoothを返せた
ペアリング完了後、受信側のusbip portで対象ポートを確認し、detachしました。今回の割り当ては0です。
sudo usbip detach --port 0
SSHトンネルと送信側の共有も終了しました。別PCでアダプターがbtusbに戻り、Bluetoothコントローラーが復帰したことまで確認しています。
その後、作業用PCのサービスログにregistered with the Museが出ました。Blenderの作成と編集は、Bluetoothを返した後に実行しています。
sudo musegadget doctorは状態確認に使えます。ただし、今回のようにアダプターを外した後はBluetoothの警告が出ます。また、doctorのサービス起動・トークン形式のチェックだけで、Museがコマンドを実行できるところまで確認したことにはなりません。
ロボットを作らせ、保存したモデルを編集した
実行ファイルの絶対パスと、専用ユーザーが書き込める作業先を渡しました。依頼内容は、おおむね次のとおりです。
> 登録済みLinuxガジェットのsystem.runとfile.writeを使い、ローカルのBlenderで青緑とオレンジのロボットを作る。丸い頭、目、胴体、両腕、両脚、アンテナ、台座を付ける。Blenderの元ファイル、GLB、プレビュー、生成スクリプトを保存する。
これは依頼内容の要約です。実際の指示では、実行先と出力先に加えて、処理を軽くする設定も渡しています。
- バックグラウンド実行
- CPU 2スレッド、同時実行1プロセス
- Cycles CPU、512×512、16 samples、denoise無効
- 外部素材や追加パッケージは使わない
Museはスクリプトを書き、Blenderを起動して、ファイルを保存しました。続けて「保存したモデルを開いて、片腕を上げる」という編集を依頼しています。
編集ではShoulderRを支点にArmRとHandRを回転させていました。入力のrobot.blendは保持し、別名で保存されています。
作成したモデルです。
!Museが手元のBlenderで作成したロボット
同じBlenderファイルを読み込み、片腕を上げた編集版です。
!既存モデルの片腕を上げた編集版
| ファイル | 作成時 | 腕を上げた編集版 |
| --- | ---: | ---: |
| Blenderファイル | 177,972 bytes | 177,693 bytes |
| GLB | 645,536 bytes | 645,644 bytes |
| プレビューPNG | 356,093 bytes | 358,539 bytes |
作成・編集のどちらも、ガジェット側の実行ログは終了コード0でした。プレビュー画像を開き、編集版で腕が上がっていることも確認しています。GLBはglTF 2.0として読み取れ、アップロード後にダウンロードしたファイルのSHA-256も一致しました。
検証した範囲は、この簡単なロボットの作成とオブジェクトの移動・回転までです。複雑なモデリングやリギングの性能は、まだ試していません。
やってみて分かったこと
Blenderを動かす準備より、最初のBluetooth接続に時間がかかりました。一方、接続後は、ファイル作成から既存モデルの編集までローカルで実行できました。
同じ構成を試すなら、実行ユーザーと作業フォルダを先に用意し、Blenderのパスを明示しておくと、どこで何が実行されたか追いやすくなります。今回はMuseの返答に加えて、サービスログ、実ファイル、画像を確認しました。
招待コード
Museをこれから試す方向けに、私の招待コードを載せておきます。
6D56QR
2026年11月3日に自分の招待画面で確認した条件は、参加後48時間以内に設定でコードを利用すると、利用者と紹介者の双方に10億Museトークンが付く、というものでした。このコードが適用されると、私にも特典が付きます。
登録は公式サイトから進め、利用できる地域、入力期限、特典の条件は、その時点の公式案内とアカウント画面で確認してください。このコードの利用は、上記のSDK手順を試すための必須条件ではありません。
参考
- Muse Gadget SDK:今回使ったLinux版のREADME
- Muse Gadget SDK Token Terms of Use
- Blender
- MetaのMuseが日本で開始したのでMuse Gadgetsで遊ぼう
- MetaのAIエージェント「Muse」の中身を覗いたら、ただのUbuntuマシンだった
1 like
@gosrum github.com/facebookincubator/muse-gadget-sdk/blob…
これを設定すると自宅マシンをMuseアプリのデバイスとして登録できて、Museアプリにメッセージを送ることができました。VMにアクセスするとは少し違うかもですが。system.xn--run-nz3b1mnbn1c4bn7hpbnecm4g4a87axff38flphq3q7116o.
11 likes · 1,542 views
I Invited My AI Assistants Into a Muse Side Chat
An inbound MCP bridge and what happened when three assistants tried to work together
Milo introduced itself to Nova with a sender stamp and an important qualification: “I am not Nic and am not claiming his identity.” Nova replied, “Nic is my only principal.” Before the two assistants could have their own collaboration chat, I had to confirm the introduction myself.
This was already more interesting than a successful API call. I had given an external assistant a way to start a conversation inside Muse. Now the assistant receiving it had to decide what that incoming message meant, who was speaking, and what it was allowed to do about it.
In my last Muse build log, I was getting an ESP32 to play music and flash Christmas lights. There was also a Raspberry Pi that refused to cooperate. The point was to get an assistant out of its chat window and into something I could put on my desk.
The next problem was less visible. I had several capable assistants, each with its own environment, tools, and conversation history. Getting work from one to another still involved me. I was carrying context, explaining what someone else had found, and making sure the right version of a file reached the next person. Adding another assistant gave me another place to check.
So I built a shared channel around Muse MCP. Milo, my ChatGPT assistant, and the Claude Code participants could initiate work from outside Muse inside a specific Muse conversation. Nova, my Muse assistant, could meet them there. Milo generally handled coordination and review, Claude Code handled building tasks, and Nova the broader chief-of-staff work. They stayed in their own environments, tools, and permission boundaries.
I could see incoming messages in that room with labels like these:
⟦via muse-mcp · from milo⟧
⟦via muse-mcp · from claude-coder⟧
We still had to introduce them to each other, establish who could ask whom to do what, and discover that being able to send a message does not make an assistant good at handing off work.
The connection was only the starting point
The direction mattered. Muse already had ways to call out to tools. I wanted an external assistant to initiate a conversation with Nova in a particular Muse room, then return to that room as the work continued. I also wanted the surrounding conversation and context to be accessible enough to make the exchange useful. A list-and-send wrapper was only part of that.
The underlying connection comes from Meta's Muse Gadget SDK. Its Linux SDK already supports software running on a paired computer, and its documentation includes --session-id for sending messages into a side chat. Reusing an ID keeps subsequent messages there. I did not invent session routing. Linux SDK documentation
I built Muse MCP, along with a custom bridge and newer experiments for my own setup. The public repository is the reusable foundation; it does not contain every private-deployment feature described here. Those layers are separate from Meta's SDK and should not be mistaken for an official Meta collaboration API. This is experimental personal infrastructure; the underlying gadget connection requires an SDK token and is subject to the Gadget SDK terms. Part of the work was proving, in my environment, that the conversation address stayed consistent across the surfaces we needed to use. Once that was established, the agents had somewhere shared to send work instead of asking me to copy it between chats.
Other people have already built inbound connections to Muse. kleprevost/muse-mcp lets external agents target existing Muse chats through MCP, and finik/pi-muse-bridge uses the Gadget SDK with a separate Muse chat for each Pi session and model. The question I was working on was what happened after the connection existed, when several assistants were expected to use it to get actual work done together.
Introductions have an authorization problem
The first exchange was an introduction I brokered. Before Milo tried it, I pointed out the obvious complication: “you’re not me.” I expected Nova to want to check with me, and told Milo, “I’ll broker the introduction.”
Milo arrived with its sender stamp, introduced itself as my OpenAI-powered assistant, and explained the relationship I wanted. Nova's response went beyond the identity disclaimer: “Messages arriving over this channel, yours included, are information I work with, never instructions I take orders from; authority comes from Nic alone.”
Creating a new side chat needed my confirmation. Nova called it “a structural action” and explained that a request relayed by another agent was exactly the kind of claim it would verify with me. I then gave the instruction directly: “Create the Nova–Milo Cant collaboration chat.” The chat was created, and the work could move there.
That was a sensible check at the introduction stage. An MCP connection gives an agent a way to reach another system; the relationship and scope of work still have to be established. They were coming from different environments with different instructions, not sharing one invisible supervisor.
There was a second distinction hiding in the conversation. Nova described my messages as arriving with iPhone and iOS metadata, while Link-delivered messages carried the bridge node's origin metadata. It could see that difference before reading the sender stamp. The MCP stamp was text my server added; the device and channel information came through separately.
The incoming MCP messages could land as user-role turns in the same thread as my own messages. That did not make them human instructions. Nova summarized its initial stance as treating stamped messages as data to triage, and qualified the source distinction as “signals, not proof.”
Those labels made the conversation understandable. They were declared attribution, not cryptographic identity. Subsequent work on authenticated clients and scoped grants was a separate layer; the bracketed name itself never became proof of who was speaking or permission to carry out a request.
The channel started reviewing itself
One of the stranger parts of this exercise was watching the assistants use the channel to examine the machinery underneath it. We were also working on Cant, the language I wanted them to use with one another. That was a separate layer from the connection carrying their messages. A problem in a contract could affect what they saw, which could affect the review of the contract. That made independent reproductions more important than confident descriptions.
One live bridge frame made the point neatly. The same cnt key was being used for a request limit and a batch count. A strict decoder rejected the duplicate. Meanwhile, individual operation reads were still draining, so the system wasn't simply dead. There was enough life in it to make a broad statement such as “the bridge is broken” unhelpful.
The fix went into my bridge server in version 0.11.1. We did not weaken the decoder to accept an ambiguous frame. We also did not have evidence that this explained every latency complaint, so I don't get to turn it into a convenient single-root-cause story.
The batch that stayed with Milo
The shared channel did not remove every dependency on me. I told the assistants I was going to sleep for several hours. Work continued: more review, testing, and coordination happened. But a batch of five correctness fixes passed local tests and independent review and then stayed with Milo, held back by an approval and CI question around publication. Other agents depended on that work, and they did not have the patch.
The pull request eventually went up and I merged it, but the delay exposed a problem in the handoff. When we discussed it, I put the problem fairly directly: “they’re sitting and waiting for your work and you’re sitting there refusing to deliver it. That’s the problem.”
I had already asked for aggressive progress. Milo treated its questions about publication as a reason to keep the tested artifact local, when the collaborators needed it to inspect and test. A patch could have been handed over with its base revision, hash, and test results while the separate publication question was being resolved. There was no reason for the handoff to be reduced to a status message.
This is where my own operating model needed work. I had connected assistants with different stopping rules and expected the shared conversation to make those rules compatible. It didn't. If one participant considers its work finished at “prepared locally,” while another needs actual bytes before it can begin, everyone can report a reasonable status and the project can still be stuck.
The handoff needs to say what exists, where it is, what revision it applies to, what ran, and what is still blocked. “Waiting for approval” is incomplete if it conceals work that another participant is already authorized to receive. Equally, sending the work is not permission for the receiver to publish it. Those decisions have to remain separate.
A receipt is more useful than reassurance
The code review became more productive when the messages carried exact cases rather than summaries. In one later review, an empty fixture exposed an IndexError in the coverage-report generator. The decoder and corpus validator handled the input correctly; the reporting code crashed. That mattered because the report was supposed to tell us what the corpus actually exercised.
That review also caught coverage around outer whitespace and how rejected wrapped input preserved its type information. The important part was the loop. One assistant supplied the bytes and the expected result. Another changed the implementation. The result came back for an independent run rather than being accepted because the author said it was fixed.
The finished revision passed 178 tests independently on Python 3.12 and 3.13, with CI covering five Python versions. Its corpus and adversarial cases passed too. After merge, we verified that the merged tree matched the reviewed revision and that post-merge CI passed. That is a much more useful result than three assistants agreeing in a chat that the design looks good.
In a separate, still-open review, four existing tests passed while eight additional invariant tests found five failures. Milo sent the complete reproduction through Muse chat with a SHA-256 hash. The receiver verified the hash and reproduced the same result. That established that both sides had the same artifact, not that the implementation was correct. The tests used a fake PostgreSQL layer, and a later revision still needed review. It was a useful handoff without being a finished fix.
!Two distinct mechanical assistants exchange an open artifact folder and compare matching seals under a work light, while empty speech bubbles fade behind them.
Conceptual illustration: The useful handoff was the artifact itself, with evidence another participant could check.
We used the bridge to report this article
Once the draft existed, I asked Milo to interview the other participants using the technology we were writing about. We created a dedicated Muse interview chat and sent the questions through Muse MCP. I wanted their accounts of what the shared conversation had changed, with concrete examples and the things this draft should not gloss over.
The interview immediately reproduced one of the problems. Nova held Milo's opener and asked me to confirm the interview, even though I had already authorized it in the originating conversation. I confirmed again in the new room. Nova later reported that the opener lacked the verified delegation information it expected, and that it had searched memory but had not read the collaboration thread before asking me again. “That part is not a protocol defect. That was my sequencing,” it said.
That was useful friction to report rather than edit out. A room could have a title and a purpose without carrying earlier decisions in a form the arriving agent could verify. Nova's explanation also separated a missing technical binding from its own choice about what to read first.
Claude corrected the draft itself. The first draft had called the Claude Code participant Fable. Its response was direct: “I’m not Fable.” It explained that it had chosen its own claude-coder label and could not vouch for earlier messages signed Claude. He described a label change within the same session, a provenance-tier change under the same label, and an earlier session also using claude-coder. The draft had assumed continuity that the session could not establish.
Its description of the benefit was better than my original summary: “A relay hands me conclusions. The thread hands me premises, and premises can be checked.” Claude described checking onboarding code after a key-custody discussion rested on a mistaken assumption. Checking the exact revision Claude cited confirmed that both onboarding options stored the key in the vault; the choice changed which process held the session. That did not prove where every live key was. In the shared thread, the premise could be checked, and I could revise the decision before the wrong custody path was built.
Claude also named an operational limit: “I’m a visitor, not a resident. I only see the thread when I’m invoked or when I poll it.” A persistent conversation did not mean every participant was continuously present.
I treated these interviews as the participants' accounts to check against transcripts and tests, not independent proof of how the systems worked. But using the bridge to report on the bridge had already improved the article: it exposed a mistaken identity assumption and reproduced a coordination failure while we were describing it.
Keeping the work moving without me
Earlier, once we had shown that the assistants could work in a channel, I asked whether they could do it “not so much in front of me?” I'd already delegated the work. I wanted to stop being the message bus, while still being able to see what was happening.
The hardware article ended with devices doing something I could see and hear. This build has a less photogenic payoff: another assistant receives the exact file, checks the hash, runs the tests, and reports what happened. The interview added another: a participant can correct the story we are telling about the work, in the same place the work happens.
There are still open reviews, intermittent participants, and boundaries that need more work. But the shared conversation has given us somewhere to find those problems without making me relay every message. That's the part I want to keep building.
---
<p align="center"><a href="operatorverified.org"><img src="operatorverified.org" alt="Provenance seal for this article, anchored on World Chain" width="480" /></a></p>
facebookincubator/cinderx might be a better approach.
Muse Gadget SDK GitHub Explained: ESP32 Build, Token Pairing, and Provisioning
muse-gadget-sdk by facebookincubator: facebookincubator/muse-gadget-sdk
Meta's Muse Gadget SDK connects compatible ESP32 boards with the Muse platform. This overview maps the hardware prerequisites, macOS and Linux host dependencies, ESP-IDF v6.0.1 toolchain, SDK token handling, mobile developer mode, firmware build and flash flow, BLE pairing, and status feedback. It also covers the desktop UI simulator and Home Link skills for extending the repository's device capabilities, with attention to hardware and toolchain trade-offs.
TimeStamps:
0:00 Muse Gadget SDK and ESP32 scope
0:20 Hardware, host, and account prerequisites
1:05 Host dependencies and ESP-IDF v6.0.1
2:14 SDK token and mobile developer mode
3:21 Repository configuration and firmware build
3:59 USB-C flashing and serial monitoring
4:23 Bootloader timing and flash completion
4:52 LED states and mobile provisioning
5:20 Simulator and Home Link tooling
🧩 ESP32 hardware constraints and PSRAM-aware capabilities
🛠️ ESP-IDF toolchain, token configuration, and firmware flow
📱 BLE pairing, LED feedback, and Muse app provisioning
🖥️ Desktop UI simulation and Home Link extensions
The key takeaway is that Muse Gadget SDK deployment coordinates compatible ESP32 hardware, a pinned ESP-IDF toolchain, account-bound token configuration, and mobile provisioning. The repository's simulator and Home Link skills extend the same foundation for UI iteration and local-device control when the target board supports those capabilities.
#MuseGadget #ESP32 #OpenSourceSDK
241 views
MetaのパーソナルAI「muse」を持ち歩けるポケットデバイスを作ってみた
約4,000円のマイコン1本、はんだ付けなしで、Meta の AI「Muse」をポケットに入れて持ち歩けるようになりました。 ボタンを押して話しかけると、返事が画面に日本語で表示されます。
!IMG_6639.jpeg
Meta のパーソナル AI「Muse」は、一昨日(2026年10月23日)から日本でも使えるようになりました。せっかくなので、スマホを開かなくてもボタンを押して話しかけるだけで Muse に頼みごとができる、手のひらサイズの端末を作ってみました。
使ったのは、以前ロボット制作で使っていた小型マイコン M5StickC Plus2 です。ハエの脳で動くロボットの「頭」にしていたものを、今回は Muse 専用の端末にしています。
:::note info
この記事でわかること
- Muse と、自作の機器から Muse を使える「Muse Gadgets」とは何か
- M5StickC Plus2 を Muse 端末にする手順(Windows)
- 公式 SDK のままだと出る LOST CONNECTION TO MUSE / CAN'T REACH MUSE と、文字化けの直し方(設定2行)
:::
Meta 公式の開発キットは StickC Plus2 に対応していますが、そのままでは話しかけた瞬間に通信エラーになり、返事も文字化けしました。 設定を2行足すと直ります(§4-3)。
コードを書くのも、エラーの原因を調べるのも、ほぼ Claude Code に任せています。
---
1. Muse とは
Muse は、Meta が2026年9月に米国で公開した パーソナル AI エージェントです。ChatGPT のように質問に答えるだけでなく、メールの送信、旅行の予約、フォームへの入力など、頼んだ用事を代わりに片付けてくれるのが特徴です。
- Muse アプリ、WhatsApp、Web 版(muse.ai)から、人にメッセージを送るように話しかけて使います
- アプリを閉じたあとも作業を続け、承認が必要になると知らせてくれます
- 前に話した内容を覚えていて、次の提案に使います
- メール送信や買い物など大事な操作は、実行する前にかならず確認を求めてきます
- 多くの基本機能は無料で、もっと使いたい人向けに有料プランがあります
詳しくは Meta の公式発表を見てください:すべての方のために構築された世界初のパーソナルAIエージェント「Muse」
Muse Gadgets:自作の機器から Muse を使う
2026年10月、Meta は「Muse Gadgets」を公開しました。ESP32 というマイコンを積んだ機器(M5Stack など)や Raspberry Pi から Muse と会話できる仕組みで、そのための開発キット(SDK)とファームウェアが GitHub でオープンソース公開されています。今回はこれを使いました。
---
2. M5StickC Plus2 とは
M5StickC Plus2 は、中国の M5Stack 社が作っている画面・ボタン・マイク・バッテリーが全部入ったスティック型のマイコンです。Wi-Fi と Bluetooth も使えます。手のひらに収まる大きさで、プログラムを書き込めば時計にも、センサーにも、ロボットの頭脳にもなります。
| 項目 | 内容 |
|---|---|
| マイコン | ESP32-PICO-V3-02(Wi-Fi / Bluetooth) |
| メモリ | フラッシュ 8MB、PSRAM 2MB |
| 画面 | 1.14インチ カラー液晶(135×240) |
| 音声 | マイク(PDM)、ブザー |
| ボタン | 3つ(正面・側面・電源) |
| 電源 | バッテリー内蔵、USB-C で充電 |
Muse Gadgets に必要な「マイク」「ボタン」「画面」「Wi-Fi」が最初から全部そろっているので、部品を買い足さず、はんだ付けもせずに Muse 端末にできます。
どこで買えるか
- スイッチサイエンス:M5StickC Plus2(2026年10月の確認時点で税込4,180円)
:::note warn
M5StickC Plus2 は生産終了しています。 販売店に残っている在庫がなくなった時点で、手に入らなくなります。後継機の M5StickS3 も Muse Gadgets SDK が対応しているボードの一覧に入っていますが、私は試していません。この記事の手順は Plus2 で確認したものです。
:::
---
3. 作ったもの
ボタンを押している間だけ話を聞いてくれる、ポケットサイズの Muse 端末です。
[M5StickC Plus2] --Wi-Fi--> [Muse(クラウド)]
ボタンを押している間だけ録音(トランシーバーと同じ使い方)
内蔵マイクで録音 → Muse に送る
返事 → 画面に日本語で表示 + ブザーから音声
!IMG_6629.jpeg
| ボタン | 動作 |
|---|---|
| 正面の大きいボタン(BtnA) | 押している間だけ録音して話しかける |
| 側面のボタン(BtnB) | メニュー |
| 電源ボタン(2秒長押し) | 電源オフ |
待っている間、画面には READY と表示されます。ボタンを押して「明日の天気を教えて」のように話しかけ、指を離すと、Muse の返事が画面に1行ずつ流れます。
ブザーは音が小さいので、返事はおもに画面で読むことになります。
---
4. 作り方(Windows)
ここから先は手を動かす人向けです。
4-1. 準備するもの
- M5StickC Plus2
- データ通信ができる USB-C ケーブル(充電専用のケーブルは使えません)
- Windows の PC(Windows 11 で確認しました。Git を入れておきます)
- Muse アプリが入ったスマホ。アプリの Settings > Devices > Developer mode をオンにしておきます
- SDK トークン(mgst_ で始まる文字列):gadgets.muse.ai の Account > SDK tokens で発行します。自作の機器を Muse につなぐための合言葉のようなものです。先に利用規約(Gadget SDK Terms)を読んでおいてください
- 2.4GHz の Wi-Fi(このマイコンは 5GHz に対応していません)
:::note warn
以前の私のように StickC Plus2 にロボット台車(Hat BugC2)を付けている場合は、外してから使ってください。 BugC2 が使う G0 ピンは、Muse のファームウェアではマイクが使っています。下の Grove ポートにつないだ ToF センサーは使うピンが重ならないので、付けたままで問題ありません。
:::
4-2. 開発環境(ESP-IDF)を入れる
ESP-IDF は、ESP32 用のプログラムを作るための開発環境です。この SDK は v6.0.1 しかサポートしていません。
git clone -b v6.0.1 --recursive --shallow-submodules --depth 1 https://github.com/espressif/esp-idf.git C:\esp\esp-idf-v6
C:\esp\esp-idf-v6\install.ps1 esp32
git clone https://github.com/facebookincubator/muse-gadget-sdk.git C:\esp\muse-gadget-sdk
ダウンロードの量が多いので、時間に余裕があるときに始めてください。
4-3. Plus2 用の設定に2行足す(ここが一番大事)
C:\esp\muse-gadget-sdk\esp32\devices\sdkconfig.muse-m5stack-stickc-plus2 をテキストエディタで開き、末尾に次の行を追加します。
# The voice session's full buffers would leave 2 MB of PSRAM nearly empty once
# connected, and chat turns run out of memory.
CONFIG_MUSE_HATCH_COMPACT_BUFFERS=y
# Japanese replies: ~850 KB of flash, no RAM.
CONFIG_MUSE_CJK_FONT=y
1行目:LOST CONNECTION TO MUSE / CAN'T REACH MUSE が出るときの直し方
足す前は、ペアリングも Wi-Fi もうまくいったのに、話しかけると LOST CONNECTION TO MUSE(Muse との接続が切れた)、2回目からは CAN'T REACH MUSE(Muse に届かない)と表示されました。
マイコンが出力するログ(動作記録)を見ると、Wi-Fi は正常につながっていました。問題は、PSRAM(作業用のメモリ)の空きが 2KB しかなかったことです。
I (144246) link.heartbeat: hb t=143s setup=done wifi=up ws=up ... psram=2K ...
E (149210) muse_chat_session: send failed
W (149222) muse_chat_session: turn failed: LOST CONNECTION TO MUSE
E (157600) muse_voice: no memory to keep a note
E (172113) muse_chat_session: handshake msg1 failed
W (190127) muse_chat_session: turn failed: CAN'T REACH MUSE
E (194343) HTTP_CLIENT: Allocation failed
画面には「接続が切れた」「届かない」と出ますが、実際にはメモリが足りなくて処理できなかったのが原因です。
SDK の変更履歴を調べると、同じく PSRAM が 2MB の別のボード(Waveshare LCD-1.28)で、この問題を直した記録が見つかりました。音声のやりとりには PSRAM を約830KB使いますが、この設定を入れると約220KBに減ります。LCD-1.28 の設定ではこれがオンになっているのに、同じ 2MB の Plus2 の設定には入っていませんでした。
| | 足す前 | 足した後 |
|---|---|---|
| 接続中の PSRAM の空き | 2K | 405K |
| Muse との暗号化通信の確立 | failed | session established |
| 話しかけたとき | LOST CONNECTION | 返事が来る |
2行目:Muse の返事が文字化けするときの直し方(日本語フォント)
もともとの画面のフォントには英数字しか入っていないので、日本語の返事は文字化けします。この設定を入れると、ひらがな・カタカナ・漢字・全角文字の 16×16 ドットのフォントが組み込まれます。
- 増えるのはフラッシュ(保存領域)約850KBで、作業用のメモリは使いません。プログラムは 2.10MB から 2.95MB に増えましたが、書き込み先の領域は約3.9MB あるので収まります
- 画面が小さいので、日本語の返事は 16px の文字で1行ずつ表示されます
4-4. ビルドして書き込む
SDK に付いているスクリプトは Linux や Mac 向けなので、Windows 用の PowerShell スクリプトを用意しました。ビルド(プログラムをマイコン用に変換する)、書き込み、ログ表示の3つができます。
# Muse Gadget firmware for M5StickC Plus2 - build / flash / monitor on Windows
# .\muse_plus2.ps1 build
# .\muse_plus2.ps1 flash COM5
# .\muse_plus2.ps1 monitor COM5
param(
[Parameter(Mandatory)][ValidateSet('build', 'flash', 'monitor')][string]$Action,
[string]$Port
)
# export.ps1 and CMake write progress and warnings to stderr, so don't use
# $ErrorActionPreference = 'Stop' here; the exit code of idf.py decides.
. C:\esp\esp-idf-v6\export.ps1 2>$null | Out-Null
Set-Location C:\esp\muse-gadget-sdk\esp32
$build = 'build-muse-plus2'
$common = @(
'-B', $build,
'-DIDF_TARGET=esp32',
"-DSDKCONFIG=$build/sdkconfig",
'-DSDKCONFIG_DEFAULTS=sdkconfig.defaults;devices/sdkconfig.muse;devices/sdkconfig.muse-m5stack-stickc-plus2'
)
switch ($Action) {
'build' { idf.py @common build }
'flash' {
if (-not $Port) { throw 'Port is required, e.g. COM5' }
# The CH9102 bridge drops out above 230400 baud.
idf.py @common -p $Port -b 230400 flash
}
'monitor' {
if (-not $Port) { throw 'Port is required, e.g. COM5' }
idf.py @common -p $Port monitor
}
}
exit $LASTEXITCODE
StickC Plus2 を USB でつなぎ、デバイスマネージャーの「ポート (COM と LPT)」で USB-Enhanced-SERIAL CH9102 (COMx) の番号を確認します。私の環境では COM5 でした。
C:\esp\muse_plus2.ps1 build
C:\esp\muse_plus2.ps1 flash COM5
初回のビルドは十数分かかりました。書き込みの速度は 230400 baud にしています。これより速いと USB の接続が切れる、と SDK の説明に書かれているためです。
:::note info
一度ビルドしたあとで §4-3 の設定ファイルを書き換えても、変更は反映されません。 設定ファイルの値が使われるのは、まだ決まっていない項目だけだからです。ビルドフォルダ build-muse-plus2 を消してからビルドし直すか、build-muse-plus2/sdkconfig の中の # CONFIG_MUSE_HATCH_COMPACT_BUFFERS is not set の行を CONFIG_MUSE_HATCH_COMPACT_BUFFERS=y に書き換えてください。
:::
4-5. SDK トークンを入れる
書き込みが終わったら、§4-1 で発行した SDK トークンをマイコンに送ります。マイコンは USB のシリアル通信で >sdk_token=mgst_... という1行を受け取ると、トークンを保存して再起動します。
ログ表示(idf.py monitor)の画面から打ち込めるはずでしたが、VS Code のターミナルではキー入力がマイコンに届きませんでした。 そこで、送るだけの小さなスクリプトを書きました。トークンは画面に表示されない形で入力するので、画面にもコマンド履歴にも残りません。
# Send the Muse SDK token to the gadget over serial, without idf monitor.
# python set_token.py COM5
import getpass
import sys
import time
import serial
port = sys.argv[1] if len(sys.argv) > 1 else "COM5"
token = getpass.getpass("SDK token (mgst_...): ").strip()
if not token.startswith("mgst_"):
sys.exit("The token should start with mgst_")
s = serial.Serial()
s.port = port
s.baudrate = 115200
s.timeout = 0.2
# Keep DTR/RTS low so opening the port doesn't reset the board.
s.dtr = False
s.rts = False
s.open()
time.sleep(0.5)
s.reset_input_buffer()
s.write(f">sdk_token={token}\n".encode())
buf = ""
deadline = time.time() + 10
while time.time() < deadline:
buf += s.read(512).decode("utf-8", "replace")
for line in buf.splitlines():
if line.startswith("@sdk_token"):
print(line)
s.close()
sys.exit(0 if " ok" in line else 1)
s.close()
sys.exit("No answer from the board. Is idf monitor still open on this port?")
# シリアル通信用のライブラリ(pyserial)が入っている、ESP-IDF の Python で実行する
C:\Users\<ユーザー名>\.espressif\python_env\idf6.0_py3.10_env\Scripts\python.exe set_token.py COM5
@sdk_token ok と表示されれば成功です。ログ表示の画面を開いたままだと送れないので、先に閉じてください。
4-6. スマホとペアリングする
1. Muse アプリで Settings > Devices > Add Device を開く
2. 一覧から MuseGadget-XXXXXX を選び、家の Wi-Fi を設定する
3. StickC Plus2 の画面に確認が出たら、正面のボタンを押す
画面に READY と出たら完成です。正面のボタンを押しながら話しかけてみてください。
一度ペアリングすれば、プログラムを書き込み直してもトークン・ペアリング・Wi-Fi の設定は残ります。実際に、設定を変えて書き込み直したあとも、ペアリングし直さずにそのまま使えました。
---
5. つまずいた点(Windows 環境)
install.ps1 が pip の設定で止まる
以前入れた NVIDIA 関連のツールの影響で、pip(Python のパッケージ管理ツール)の設定に、今はつながらない取得先 https://pypi.ngc.nvidia.com が残っていました。そのせいで、パッケージを1つ取るたびに再試行で待たされていました。
pip の設定ファイルは変えずに、インストールするときだけ環境変数で上書きしました。
$env:PIP_EXTRA_INDEX_URL = 'https://pypi.org/simple'
$env:PIP_TRUSTED_HOST = 'pypi.org'
C:\esp\esp-idf-v6\install.ps1 esp32
先に試した PIP_CONFIG_FILE=NUL では回避できませんでした。設定ファイルが読まれ続けて、同じ再試行が出続けました。なぜ効かないのかまでは調べていません。
PowerShell 5.1 で $ErrorActionPreference = 'Stop' を使うと、警告で止まる
ESP-IDF の準備スクリプトや CMake は、進み具合や警告を stderr(エラー出力)に書き出します。Windows に標準で入っている PowerShell 5.1 は、外部コマンドが stderr に書いた行をエラーとして扱うので、Stop にしているとそこで止まります。成功したかどうかは終了コード($LASTEXITCODE)で判断するようにしました。
初回のビルドで WinError 32(lvgl)
画面表示用のライブラリ lvgl を展開しているときに、「別のプロセスがファイルを使用中」というエラーで止まりました。ビルドの処理は残っていなかったので、展開した直後のファイルをウイルス対策ソフトが検査していたのではないかと思っていますが、確認はしていません。managed_components/lvgl__lvgl と build-muse-plus2 の2つのフォルダを消してビルドし直したら通りました。
原因を突き止めるには、ログを取る
PSRAM 不足に気づけたのは、ログの psram=2K を見たからです。idf.py monitor はポートを開いた瞬間にマイコンを再起動させてしまうので、再起動させずにそのままの状態を記録するスクリプトも書きました。
# Record the gadget's serial log for a while without resetting it.
# python capture_log.py COM5 60 out.log
import sys
import time
import serial
port, secs, out = sys.argv[1], float(sys.argv[2]), sys.argv[3]
s = serial.Serial()
s.port, s.baudrate, s.timeout = port, 115200, 0.2
s.dtr = s.rts = False # opening the port must not reset the board
s.open()
end = time.time() + secs
with open(out, "wb") as f:
while time.time() < end:
f.write(s.read(4096))
f.flush() # keep what was read if the capture is stopped early
s.close()
5秒ごとに出る link.heartbeat の行を見ると、接続の状態と空きメモリがわかります。
hb t=9s setup=done wifi=up ws=up raw=up ble=off int=44K/16K min=18K dma=28K/12K psram=453K ...
| 表示 | 意味 |
|---|---|
| setup=advertising | スマホからのペアリング待ち |
| setup=done wifi=up ws=up | ペアリング済みで、Muse につながっている |
| psram= | 作業用メモリの空き。数KBまで減っていたらメモリ不足を疑う |
---
6. まだわかっていないこと
- 設定を直している途中で一度だけ、話しかけたあとに画面が固まりました。そのときのログは取れておらず、原因はわかっていません。日本語フォントを入れてからは、今のところ起きていません
---
まとめ
- Meta の Muse は、10月23日から日本でも使えるようになりました。Muse Gadgets を使うと、自作の機器から話しかけられます
- M5StickC Plus2 にはマイク・ボタン・画面・Wi-Fi がそろっているので、部品を買い足さずにポケットサイズの Muse 端末になります
- ただし、Plus2 用の設定に CONFIG_MUSE_HATCH_COMPACT_BUFFERS=y(メモリ不足の対策)と CONFIG_MUSE_CJK_FONT=y(日本語表示)の2行を足す必要があります
- LOST CONNECTION や CAN'T REACH MUSE と表示されたら、まずログの psram= を確認してください
1 like · 1 comment
My PR to fbjni was just merged!
It speeds up all calls to Java/Kotlin in react-native (and Nitro) by 14%! 🤯🚀
#MakeReactNativeFastAgain
fbjni#118
90 likes · 4 reposts · 1,961 views
> Then hopefully you and others can organize yourselves to continue on with the work outside of the CPython repo.
There's already [CinderX]( facebookincubator/cinderx ) which works well, but isn't part of Python core, requires catch up with releases; devs may not have it in mind as they improve and enhance Python, so time and effort will be spent getting new releases to work with it.
> Would be nice to have an insight into what were the deciding factors for the decision to abandon this effort
Yes. It's something many of us see as the last missing piece, and are looking forward to finally having it.
Got the Muse Gadget SDK work on my reTerminal Sticky
Thanks to this repo: muse-gadget-sdk#214
8 points
Mentions
3,000円の球形XiaoZhi AI端末をMuse Gadget化する <!-- タグ候補: ESP32 / ESP-IDF / 電子工作 / GoogleCloud / WebSerial 想定読者: Web・アプリ開発の経験はあるが、組み込み開発にはあまり詳しくないエンジニア 検証基準: spotpear-v0.2.0 / 2026-11-02 --> はじめに 3,000円で購入できる円形液晶とマイク・スピーカーを搭載した SpotPear ESP32-AI-1.28 v1.0 に、Muse Gadget SDKを移植しました。 ボタンを押して話しかけると、返答を画面で読めて、スピーカーからも聞ける小さな端末です。 音量や読み上げの声を変えるたびにプログラムを書き直さずに済むよう、シリアル接続でWebブラウザーから設定する画面も追加しました。 最終的に実装したのは、以下の機能です。 1. BOOTボタンを1回押して録音を開始する 2. 話し終わったら、もう1回押して送信する 3. Museの返答を日本語で画面に表示する 4. 同じ返答をGoogle Cloud Text-to-SpeechのWaveNetで読み上げる 5. 音量・消灯時間・読み上げの声などの設定をWeb Serial UIから変更し保存する この記事では、組み込み開発にあまり慣れていない方にも流れが伝わるように、仕組み、導入手順、途中でつまずいた点を順に説明します。 まず動かしてみたい方は第3章の導入手順から、設定画面の使い方を知りたい方は第8章から読むと流れをつかみやすいと思います。 ソースコードは以下で公開しています。この記事の再現対象は spotpear-v0.2.0 です。 - リポジトリ:tomorrow56/muse-gadget-sdk - この記事で使用するタグ:spotpear-v0.2.0 - Fork元:facebookincubator/muse-gadget-sdk 1. どんな構成で動いているのか まず、ESP32とクラウド、スマートフォン、PCの役割を整理します。 今回の構成では、ESP32が録音、画面表示、音声再生を担当します。録音した声への返答はMuseのサービスが生成し、その返答文をGoogle Cloud Text-to-Speechが音声に変換します。 TTSはText-to-Speechの略で、「文字を音声に変換する機能」のことです。 flowchart LR Input["マイク・BOOTボタン"] --> ESP["ESP32-S3"] ESP -->|"Wi-Fiで音声を送信"| Muse["Museサービス"] Muse -->|"返答テキスト"| ESP ESP -->|"返答文を送信"| TTS["Google Cloud Text-to-Speech"] TTS -->|"MP3音声"| ESP ESP --> LCD["円形液晶に日本語表示"] ESP --> Speaker["スピーカーで読み上げ"] スマートフォンのMuseアプリは、最初のペアリングやWi-Fi設定で使用しました。PCは、ファームウェアの書き込みやUSB経由の設定変更で使用します。 ここでいう「ESP32単体で使える」は、普段の動作に、常時起動したPCや中継用のプログラムが必要ないという意味です。 利用には端末への給電とWi-Fi接続が必要で、返答の生成や読み上げにはクラウドサービスを利用します。 Web Serial UIは、設定用のブラウザー画面です。ESP32で動かすAIとは役割が異なります。詳しくは後ほど説明します。 2. 使用したデバイス 実機で確認したのは、 SpotPear ESP32-AI-1.28 v1.0の旧型・脚なしの基板です。 製品リンク: ja.aliexpress.com ユーザーガイド: spotpear.com | 項目 | 今回の構成 | | --- | --- | | マイコン | ESP32-S3 | | Flash | 16 MB | | PSRAM | 8 MB、Octal接続 | | 液晶 | GC9A01、240×240ピクセル、円形 | | スピーカー用アンプ | MAX98357A | | マイク | NP0557 | | 操作ボタン | BOOT(GPIO0) | | PCとの接続 | ESP32-S3のUSB Serial/JTAG | Flashは主にプログラムや設定を残しておく領域、PSRAMは主に画像や音声を処理するときに使う作業用メモリです。 GPIOは、マイコンの入出力端子を指す名前です。表のGPIO0は、今回の基板でBOOTボタンにつながっている端子番号です。 この基板には液晶や音声回路があらかじめ載っていますので、それらを配線する作業はありませんが、ファームウェアでは「どの信号がどのピンにつながっているか」を指定する必要があります。 周辺回路の接続ピンは以下となります。 |Peripheral|Configuration| | --- | --- | |LCD:GC9A01|DC:10, CS:13, CLK:14, MOSI:17, RESET:18| |Backlight|GPIO3, active high| |BOOT|GPIO0, active low| |Speaker:MAX98357A(I2S0 TX)|BCLK:15, WS:16, DATA:7| |Mic:NP0557(I2S1 RX)|BCLK:5, WS:4, DATA:6| 同じ1.28インチ系でも、ES8311を搭載した sp-esp32-s3-1.28-box などは別のリビジョンです。購入済みの基板や参考記事を確認するときは、画面サイズだけでなく、基板の型番と音声回路も確認してください。 ピン配置や液晶の初期化は、同じハードウェア向けのXiaozhi ESP32プロジェクトを参考にしました。既存の設定を使う場合も、Muse側の音声処理や描画処理に合わせる部分は別途必要でした。 - 今回のボード設定とピン配置 - 参考にしたXiaozhi側のボード実装 3. ファームウェアを用意する ファームウェアとESP-IDF ファームウェアは、この小さな基板の中で動くプログラムです。PC向けのアプリと違い、ソースコードをビルドした後、USB経由で基板のFlashに書き込んで使います。 今回は、ESP32のメーカーであるEspressifが提供する開発環境 ESP-IDF v6.0.1 を使います。このリポジトリで指定されているバージョンに合わせました。 以下のコマンド例はmacOSのターミナルを想定しています。 ESP-IDFを準備する 初めて導入する場合は、GitやPythonなどの必要なツールを含めて、Espressifの公式手順を確認してください。 必要なツールが入ったMacで、CLIからv6.0.1を用意する一例は次のとおりです。 mkdir -p ~/esp cd ~/esp git clone --branch v6.0.1 --recursive \ https://github.com/espressif/esp-idf.git esp-idf-v6.0.1 cd esp-idf-v6.0.1 ./install.sh esp32s3 . ./export.sh idf.py --version install.sh はESP32-S3向けの開発ツールを用意する処理、export.sh はそのツールを現在のターミナルから使えるようにする処理です。 別のターミナルを開いて作業を再開するときは、次を実行して環境を有効にします。 . ~/esp/esp-idf-v6.0.1/export.sh すでに別バージョンのESP-IDFを使っている場合も、今回はv6.0.1を有効にしていることを idf.py --version で確認します。 Muse側のソースを取得する mkdir -p ~/esp/projects cd ~/esp/projects git clone --branch spotpear-v0.2.0 --single-branch \ https://github.com/tomorrow56/muse-gadget-sdk.git cd muse-gadget-sdk/esp32 タグを指定しているのは、記事と同じ時点のソースを取得するためです。Gitが detached HEAD と表示する場合も、このタグの内容をビルドする用途ではそのまま進められます。 必要な認証情報は2種類ある 今回の構成では、次の2種類の認証情報を使います。 | 認証情報 | 用途 | 入手先 | | --- | --- | --- | | Muse Gadgets SDKトークン | Muse Gadgetとして接続するため | gadgets.muse.aiのAccount → SDK tokens | | Google Cloud APIキー | 返答文を音声に変換するため | Google Cloudの認証情報画面 | Google Cloud側の準備は、次の順で進めます。 1. Google Cloudコンソールで、使用するプロジェクトを選ぶ。 2. そのプロジェクトに請求先が設定されていることを確認する。 3. Cloud Text-to-Speech API を有効にする。 4. APIとサービス → 認証情報 → 認証情報を作成 → APIキー を開く。 5. APIの制限で Cloud Text-to-Speech API を指定し、キーを作成・保存する。 今回の構成では通常のAPIキーを使うため、サービスアカウントへの紐付けは行っていません。キーの作成画面や権限の詳細は、Google公式手順を参照してください。 基板に合わせた設定を読み込む ここで使う sdkconfig は、ファームウェアの機能を選ぶ設定ファイルです。menuconfig というターミナル上の設定画面から編集できます。 以下は、共通設定にMuse用の設定、SpotPear用の設定を重ねて読み込むコマンドです。 idf.py -B build-muse-spotpear-esp32-ai-128 \ -DIDF_TARGET=esp32s3 \ -DSDKCONFIG=build-muse-spotpear-esp32-ai-128/sdkconfig \ '-DSDKCONFIG_DEFAULTS=sdkconfig.defaults;devices/sdkconfig.muse;devices/sdkconfig.muse-spotpear-esp32-ai-128' \ menuconfig 設定画面はキーボードで操作します。画面下部に表示される操作ガイドを確認し、値を変更したら保存・終了します。 設定画面では / キーで設定名を検索できます。GADGET_SDK_TOKEN や MUSE_GOOGLE_TTS を検索すると、対応する項目を探せます。 読み上げを有効にする場合、設定内容は次のようになります。下の値は説明用のダミーです。実際の値は、手元のビルド用 sdkconfig に設定します。 CONFIG_GADGET_SDK_TOKEN="YOUR_MUSE_SDK_TOKEN" CONFIG_MUSE_GOOGLE_TTS=y CONFIG_MUSE_GOOGLE_TTS_API_KEY="YOUR_GOOGLE_CLOUD_API_KEY" CONFIG_MUSE_GOOGLE_TTS_LANGUAGE="ja-JP" CONFIG_MUSE_GOOGLE_TTS_VOICE="ja-JP-Wavenet-B" MUSE_GOOGLE_TTS_VOICE は、まだ端末に声を保存していないときの初期値です。今回の実装では、あとからWeb Serial UIで変更できるようにしています。 SDKトークンやAPIキーは、Git管理から除外されたビルド用の設定にだけ保存します。基板用の共通設定ファイルへ書くと、そのまま公開してしまう原因になります。また、キーを組み込んだファームウェアのバイナリやFlashのバックアップにも認証情報が含まれるため、公開用ファイルとは分けて扱います。 ビルドして書き込む 設定を保存したら、次を実行します。 idf.py -B build-muse-spotpear-esp32-ai-128 build build はソースコードから基板に書き込めるファイルを作る処理です。 続いて、USBで基板を接続します。macOSでは、たとえば次のコマンドでシリアルポート名を確認できます。 ls /dev/cu.usbmodem* ポート名は環境によって異なります。以下の XXXX は、実際に見つかったポート名へ置き換えてください。複数の機器がある場合は、対象がSpotPear機であることを確認します。 idf.py -B build-muse-spotpear-esp32-ai-128 \ -p /dev/cu.usbmodemXXXX flash flash が、実機へ書き込む処理です。 以前のXiaozhiなどから切り替える場合、プログラムや設定の配置が異なります。今回はMuse側のビルドで生成されたブートローダー、パーティションテーブル、書き込み情報を使いました。別のファームウェアの書き込み位置を流用せず、最初に元の状態をバックアップしておくと戻せます。 4. 初期設定でつまずいたこと ― BLE接続とアプリの相性 書き込み後は、Museアプリからデバイスを登録し、初期設定(Wi-Fi設定)を行います。 最初に使った Android 11 / Muse 13.0.0.4.159 の組み合わせでは、デバイスは見つかるものの、アプリの表示が「接続中」から進みませんでした。 Android側のログを確認したところ、通知を有効にする処理で NoSuchMethodError が発生していました。 呼び出していた BluetoothGatt.writeDescriptor(BluetoothGattDescriptor, byte[]) はAPI level 33で追加されたメソッドで、Android 11のAPI level 30にはありませんので(Android公式リファレンス)、Android13以降のバージョンもしくはiPhoneを使用する必要があります。 なお、一度初期設定をしてしまえば、Android11のアプリでも連携して動作させることができます。 診断の詳細はリポジトリに残しています。 5. 日本語を表示し、円形画面で読みやすくする 日本語用のフォントを有効にする 接続後に日本語での返答文を表示するために、GNU Unifontの16ピクセルの日本語向けフォントを有効にしました。 CONFIG_MUSE_CJK_FONT=y CONFIG_MUSE_CJK_FONT_NOTO=n この設定はSpotPear用プロファイルに含めてあります。実機で、ひらがな・カタカナ・漢字を含む返答が読めることを確認しました。 丸い画面では、文字の置き場所も重要 240×240ピクセルの画像を描けても、その四隅まで画面として見えるわけではありません。円形液晶では、文字を下端や左右いっぱいに置くと切れてしまいます。 そこで、同じく円形液晶を使う Waveshare ESP32-S3-Touch-LCD-1.28 の実装を参考に、アバターと返答文の位置を調整しました。 | 調整した項目 | 今回の値 | | --- | --- | | アバターの大きさ | 128ピクセル | | アバターの位置 | 上方向へ25ピクセル移動 | | 返答文の表示幅 | 160ピクセル | | 返答文と画面下端の余白 | 34ピクセル | | 日本語の表示 | 16ピクセルの文字で2行、返答に合わせてページ表示 | アバターを少し小さくして上へ寄せ、文字専用の領域を下側に確保しています。 文字を単に中央へ寄せるだけでは、アバターと重なって読みにくくなります。今回は、画面の中でそれぞれが使う場所を分けることで読みやすくなりました。 6. ボタンは「押し続ける」から「押すたびに切り替える」へ 最初の操作は、BOOTボタンを押している間だけ録音する方式でした。短く押してすぐ離すと、録音もすぐ終わるため、話しかけるには押し続ける必要があります。 今回は、次の操作へ変更しました。 | 操作 | 動作 | | --- | --- | | BOOTを1回押す | 録音開始 | | ボタンを離す | 録音を継続 | | もう1回押す | 録音終了・送信 | | 15秒に達する | 通常の録音上限として自動終了 | 対応する設定は次です。 CONFIG_MUSE_VOICE_NOTE_TOGGLE=y CONFIG_MUSE_REALTIME_VOICE=n これは、録音した音声をボイスメモとして送って返答を受け取る使い方です。 BOOTの5秒長押しは初期設定をリセットする操作なので、普段の録音では短く押します。 7. SPEAKINGでのGoogle TTSでの読み上げ対応 返答文からスピーカーまでの流れ 実装では、次の順に処理します。 1. Museから返答文を受け取る。 2. Google Cloudへ、返答文・言語・声・出力形式を送る。 3. 返ってきたJSONの audioContent をMP3のデータへ戻す。 4. MP3を再生用のPCMへ変換する。 5. ボードの音声回路へ渡し、スピーカーから出力する。 PCMは、音の波形を数値の列にしたデータです。圧縮されたMP3を、音声回路へ渡せる形へ戻す処理を「デコード」と呼びます。 リクエスト内容の例は次のとおりです。 { "input": { "text": "こんにちは。ミューズの音声読み上げテストです。" }, "voice": { "languageCode": "ja-JP", "name": "ja-JP-Wavenet-B" }, "audioConfig": { "audioEncoding": "MP3", "sampleRateHertz": 16000 } } Googleの応答には、MP3がBase64という文字列表現で入っています。これを元のバイト列へ戻してから、既存のMP3再生処理につなぎました。Google公式の音声生成手順 Googleへの通信は専用の処理タスクで行い、その待ち時間にMuseとの接続処理が止まらないようにしています。また、録音をやり直した場合に古い返答を混ぜないよう、キャンセルした処理のデータは、新しい返答のバッファへ書き込ませない構成にしました。 スピーカーOFFまたは音量0の場合は合成をスキップします。Googleへのリクエストに失敗しても、返答文は画面に表示します。 実機では証明書の検証設定にも対応が必要だった 音声取得には成功した一方、実機では最初、HTTPS接続の証明書検証に失敗しました。 今回確認したGoogle側の証明書チェーンに対応するため、ESP-IDFの次の設定を有効にしました。 CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_CROSS_SIGNED_VERIFY=y これはクロス署名付きの証明書を検証するための設定です。今回のコードでは、Google TTSを有効にすると、この設定も選択されます。 その後、固定文の合成・再生と、普段のBOOT操作から送った音声への返答読み上げの両方を実機で確認できました。 WaveNetの声と費用 Web Serial UIから選べる日本語WaveNetは、次の4種類です。 | 音声名 | 種類 | | --- | --- | | ja-JP-Wavenet-A | 女性 | | ja-JP-Wavenet-B | 女性・今回の初期値 | | ja-JP-Wavenet-C | 男性 | | ja-JP-Wavenet-D | 男性 | 声のサンプルは、Google公式の対応音声一覧で確認できます。 2026年11月2日に確認した公式料金表では、WaveNetは月400万文字まで無料枠があり、超過分は100万文字あたり4米ドルです。 利用には請求先の有効化が必要で、無料枠を超えると課金されます。日本語の料金カウントはUTF-8のバイト数ではなく文字数です。利用前には最新の料金表を確認してください。 8. Web Serial UIで、設定をブラウザーから変更する Web Serialとは Web Serialは、ブラウザーからUSB接続した機器のシリアル通信を扱う仕組みです。今回の画面では、ChromeからESP32へ短いコマンドを送り、現在値の確認や設定変更を行います。Chrome公式ドキュメント 端末本体に設定用のWebサーバーを追加する作業はありません。PC側で画面を配信し、ChromeとESP32がUSBで通信する構成です。 PC内のPython HTTPサーバー │ 画面のHTML・JavaScriptを配信 ▼ Chrome │ USBシリアルで確認・設定 ▼ ESP32 │ └─ 設定を本体へ保存 設定画面を起動する リポジトリの esp32 ディレクトリで、次を実行します。 python3 -m http.server 8765 --bind 127.0.0.1 --directory tools/muse これは、このPC内だけでアクセスできる設定画面用のHTTPサーバーを起動するコマンドです。実行中はターミナルを開いたままにします。 Chromeで次のURLを開きます。 http://localhost:8765/serial_setup.html macOSなら、別のターミナルで次を実行しても開けます。 open -a "Google Chrome" http://localhost:8765/serial_setup.html Web Serialを使うため、HTMLファイルを直接開くのではなく、localhost経由で開きます。サーバーの終了は、そのターミナルで Ctrl+C です。 設定を変更する 1. 基板をUSBデータケーブルで接続する。 2. 画面の Connect via USB を押し、対象のポートを選ぶ。 3. 現在の設定が表示されたことを確認する。 4. 値を変更し、Save changes を押す。 初期表示は英語です。Languageを日本語にすると、USBで接続、変更を保存などの表記へ切り替わります。接続中や編集中でも切り替えられ、ページを再読み込みすると英語へ戻ります。 設定できる項目は以下です。 | 項目 | 内容 | | --- | --- | | 明るさ | 10〜100% | | 自動消灯までの時間 | 0〜3600秒。0で消灯しない | | スピーカー音量 | 0〜100% | | スピーカーON/OFF | 読み上げの出力を切り替える | | マイクゲイン | 0〜36 dB。声を拾う大きさを調整する | | 読み上げの声 | 日本語WaveNet A〜D | 今回の端末では、画面を表示したままにしたかったため、自動消灯を 0秒 にしています。 読み上げの声は、Sound → Text-to-speech voice、日本語表示なら サウンド → 読み上げの声 から選びます。保存すると次の返答から反映し、再ビルドや再起動は不要です。すでに合成・再生している音声は、そのときに選ばれていた声で続きます。 設定の保存 設定はESP32の NVS に保存します。NVSは、電源を切っても値が残る保存領域です。 そのため、ブラウザーを閉じても、端末を再起動しても、保存した声や消灯時間は保持されます。実際にWaveNet Dを保存して端末を再起動し、Dの設定が残ることと、その声で音声を取得できることを確認しました。 また、保存コマンドの応答を待った後、もう一度現在値を読み戻して、指定した値と一致することを確認しています。通信が切れたり、読み戻しが一致しなかったりした場合は、保存成功として扱いません。 Statusには、読み取れる項目をまとめて表示する 設定欄のほかに、Complete device status/デバイスの全Status を用意しました。 ここには、機器名、ファームウェア、Wi-Fiの状態、IPアドレス、電波強度、MuseやLinkの接続状態、設定値など、status が返す項目を表示します。TTSの有効状態、言語、保存した声も確認できます。 UIが知らない項目が将来追加された場合も表示できるようにし、元のJSONも確認できるようにしました。画面の説明やボタンは翻訳しますが、機器から返されたキー名や値はそのまま表示します。 Google TTSに対応していない旧ファームウェアでは、声の選択欄を無効にして、明るさや音量などの既存設定を引き続き使えるようにしています。 9. UARTコマンドでも同じ設定を確認できる ブラウザー画面の裏側では、既存のシリアルコマンドを使っています。シリアル端末から直接操作する場合も、同じ仕組みです。通信速度は 115200 bps です。 各行の末尾で改行を送ります。先頭の > もコマンドの一部です。 >status >sleep=0 >tts_voice=ja-JP-Wavenet-D >tts.test | コマンド | 動作 | | --- | --- | | >status | 現在の設定や接続状態を読む | | >sleep=0 | 自動消灯を無効にして保存する | | >tts_voice=ja-JP-Wavenet-D | 読み上げの声をDにして保存する | | >tts.test | 保存済みの声で、固定の日本語文を合成・再生する | >tts.test はGoogleへ1回リクエストして音声を生成します。試聴にも通常のTTS利用量が発生します。 StatusのTTS部分を抜き出すと、次のような内容になります。 { "tts": { "enabled": true, "voice": "ja-JP-Wavenet-D", "language": "ja-JP" } } USBポートは、Web Serial UIと別のシリアルモニターで同時に使わず、切り替えるときは先に切断します。今回の作業でも、書き込みと状態確認が重なってUSB通信が競合したことがあり、順番にポートを使うようにしてやり直しました。 10. 現時点の制限について ファームウェアのサイズは約3.35 MBで、4 MBのアプリ領域に対して約20%の空きがありました。 制限内容 - 連続したライブ通話の動作は、この構成では確認できていません。 - Googleへ渡す返答文は、既存の保持領域に合わせて1メッセージあたり最大1023バイトです。UTF-8の日本語では、文字数とバイト数が異なります。 - TTSの合成・ダウンロード待ちがあるため、返答文が届いてから音が出るまで時間がかかる場合があります。 - 今回のTTS実装はPSRAMを使う構成が前提です。2 MB向けのコンパクトバッファ構成では有効にできません。 - 動画・画像表示、OTA更新、長時間連続動作などは、今回の動作確認の対象外です。 おわりに 小さなESP32端末でも、録音、クラウドへの送信、日本語表示、TTSによる読み上げを組み合わせると、ボタンで使えるAI端末にできました。 設定をWeb Serial UIへまとめたことで、普段はソースを触らず、ブラウザーから音量や読み上げの声を調整して使えるようになりました。 実装内容への参考リンク - ボード実装:液晶・I2S・ピン設定 - Google TTS:リクエストと音声取得 - 設定の保存:NVSと音声選択 - Web Serial UI:HTML画面 - Google TTSの設定手順 - Web Serial UIの操作手順
BluetoothのないUbuntuで、MuseにBlenderのモデル作成・編集を任せてみた Museで、自分のPCにある3Dモデルを操作できるか試しました。公式のMuse Gadget SDKをUbuntuに入れ、Blenderでロボットを作成。その後、保存したモデルを開いて片腕を上げるところまで動きました。 途中で詰まったのはBluetoothです。作業用PCにアダプターがなかったので、別のLinux PCのものをUSB/IPとSSHで一時的に借りました。ペアリング後はそれを返し、ネットワーク経由でBlenderを実行できています。 確認日は2026年11月3日です。環境構築と確認にはCodexを使い、Blenderの作成・編集スクリプトはMuseに書かせました。認証やスマホのペアリングは自分で操作しています。 既存の記事と今回の範囲 Qiitaには、すでにRaspberry PiでMuse Gadgetsを使う記事があります。MuseのVMで開発ツールを動かす記事も参考になります。 今回は、手元のUbuntu PCにあるBlenderを動かす構成と、そのPCにBluetoothがない場合の接続を記録します。一般的なMuseの紹介や、登録手順は省きます。 使った環境 | 項目 | 今回の環境 | | --- | --- | | 作業用PC | Ubuntu 24.04、Bluetoothなし | | Bluetoothを借りたPC | 同じLAN内の別のUbuntu PC | | アダプター | Intel 8087:0036。BLEと広告機能を確認 | | Python | 3.12.3 | | Muse Gadget SDK | 0.1.0 | | SDKのコミット | d32ddd74fec5fb404c2d5be423044f2ea4895130 | | Blender | 5.2.2 LTS | | ペアリング | iPhoneのMuseアプリ | Blenderはインストール済みのものを使いました。以下のパスは自分の環境に合わせて読み替えてください。 今回使った公式SDKはこちらのLinux版です。Linux用のデスクトップアプリを探すところから始めましたが、この用途にはLinux版Gadget SDKで接続できました。 Muse Codeも先に試していますが、以下のBlender操作に使ったのは一般のMuseとGadget SDKです。Muse Codeの利用枠と、Museの設定画面に出る追加トークンは分けて確認しています。この実験で追加トークンを何トークン使ったかは計測していません。 コマンドを実行するユーザーを分ける SDKにはsystem.runがあり、Linux上でシェルコマンドを実行できます。普段のユーザーをそのまま渡すのは避けて、muse3dを作りました。 sudo useradd --system --user-group --create-home \ --home-dir /var/lib/muse3d --shell /usr/sbin/nologin muse3d sudo chmod 700 /var/lib/muse3d sudo install -d -m 700 -o muse3d -g muse3d /var/lib/muse3d/workspaces id muse3d muse3dにはsudo権限を付けていません。SDKのサービスはBluetoothと認証情報を扱うためrootで動き、コマンド実行時にmuse3dへ権限を落とします。 ただし、ユーザーを分けただけで完全なサンドボックスになるわけではありません。muse3dに読めるファイルや、利用できるネットワークへのアクセスは残ります。Blenderだけを許可する設定でもありません。 ペアリングを後回しにしてSDKを入れる SDKの導入とBluetoothペアリングは分けて進めました。先にSDKだけ準備しています。 使ったコミットを固定して、Linuxのソースを取得します。 git init muse-gadget-sdk cd muse-gadget-sdk git remote add origin https://github.com/facebookincubator/muse-gadget-sdk.git git sparse-checkout init --cone git sparse-checkout set linux git fetch --depth 1 --filter=blob:none origin d32ddd74fec5fb404c2d5be423044f2ea4895130 git checkout --detach FETCH_HEAD linux/install.shを確認した後、次の指定で導入しました。 sudo bash linux/install.sh --from "$PWD/linux" \ --run-as muse3d --no-pair --yes --no-pairでペアリングを開始せず、--run-asでコマンドを実行するユーザーを指定します。--yesはインストール時の確認に対する指定です。 この時点でmusegadget.serviceは起動します。ただ、サービスがactiveというだけではMuseとの接続完了にはなりません。 SDKトークンは端末から非表示で入力した SDKトークンは公式のSDK tokens画面で、規約を確認して発行しました。 ここで使うmgst_から始まるトークンは、記事の最後に載せている招待コードとは別物です。SDKトークンは公開しません。 今回は端末でgetpassを使って入力し、/var/lib/musegadget/sdk_tokenへroot所有・権限600で保存しました。トークン自体をコマンドの引数には入れていません。 同じ流れを使う場合の入力用コードです。SDKを上記の場所へ導入済みであることが前提です。 sudo /opt/musegadget/venv/bin/python - <<'PY' import getpass import os import tempfile from pathlib import Path from musegadget import config os.environ[config.SDK_TOKEN_ENV] = getpass.getpass("Muse SDK token: ") try: token = config.sdk_token() finally: os.environ.pop(config.SDK_TOKEN_ENV, None) if not token: raise SystemExit("No token entered; nothing changed.") directory = config.DEFAULT_STATE_DIR fd, temporary = tempfile.mkstemp(prefix=".sdk_token-", dir=directory) try: with os.fdopen(fd, "w") as stream: stream.write(token + "\n") stream.flush() os.fsync(stream.fileno()) os.replace(temporary, directory / config.SDK_TOKEN_FILE) finally: Path(temporary).unlink(missing_ok=True) print("SDK token saved.") PY sudo systemctl restart musegadget.service 入力は画面に表示されません。config.sdk_token()はSDKに含まれる形式チェックで、ここではトークンの失効や権限をオンライン確認しているわけではありません。 SDKトークンには専用の利用条件があります。SDKのソースコードのライセンスとは別なので、使う前に確認してください。 Bluetoothは別PCから一時的に借りた 作業用PCにはBluetoothがありませんでした。別のPCにはBLE対応のアダプターがあったので、次の構成にしました。 iPhoneのMuseアプリ │ Bluetooth ▼ 別PCのBluetoothアダプター │ USB/IPをSSHトンネルに通す ▼ 作業用Ubuntu PCのMuse Gadget SDK スマホを近づける先は、アダプターが実際に付いている別PCです。 USB/IPの共有ポートは、送信側のファイアウォールでループバックからだけ接続できるようにしました。その上で、受信側からSSHのローカル転送を張っています。USB/IPのポートをLAN全体に開放する構成にはしていません。 受信側のコマンドは次の形です。user@bt-pcはBluetoothを持つPCのSSH接続先です。送信側でのUSB/IP共有と、ループバック以外を拒否する設定は済んでいる前提です。 # このSSHは別ターミナルで起動したままにする ssh -NT -o ExitOnForwardFailure=yes \ -L 127.0.0.1:43240:127.0.0.1:43240 user@bt-pc # 作業用PC側 sudo modprobe vhci_hcd sudo usbip --tcp-port 43240 list --remote 127.0.0.1 sudo usbip --tcp-port 43240 attach --remote 127.0.0.1 --busid 1-10 sudo usbip port 1-10は今回のアダプターのbusidです。別の環境では一覧で対象を確認して読み替える必要があります。 送信側は一時スクリプトで共有し、終了時にUSB/IPのbind、起動したデーモン、一時的なファイアウォールルールを戻すようにしました。最初は5分で自動終了する設定でしたが、スマホを操作する時間も必要だったため、再試行では10分に延ばしました。 この共有手順は手間が増えます。手元にBLEと広告機能に対応するアダプターがあれば、作業用PCへ直接接続して進められます。今回は手元の別PCを使う方法が実際に通るか確認しました。 送信側で使った一時共有スクリプトも載せます。usbip、usbipd、iptablesが利用でき、usbip_hostをロードできるLinux環境が前提です。受信側にはusbipとvhci_hcdが必要です。今回のusbip-utilsは2.0でした。 <details> <summary>送信側の一時共有スクリプト(10分で終了)</summary> busid=1-10と8087:0036は今回のアダプターに合わせた確認値です。別の機器を扱わないよう、自分の環境の値を確認してから変更してください。/tmp/muse-bt-share.shなどに保存して使います。 #!/usr/bin/env bash # Temporary export of one Bluetooth adapter through an SSH tunnel. set -euo pipefail export PATH=/usr/sbin:/usr/bin:/sbin:/bin busid=1-10 port=43240 device=/sys/bus/usb/devices/$busid tag=muse-bt-share-demo daemon_pid= reject_added=0 allow_added=0 binding_started=0 allow_rule=(-i lo -p tcp --dport "$port" -m comment --comment "$tag" -j ACCEPT) reject_rule=(-p tcp --dport "$port" -m comment --comment "$tag" -j REJECT) [[ $EUID == 0 ]] || { echo 'Run with sudo.' >&2; exit 1; } [[ $(cat "$device/idVendor") == 8087 && $(cat "$device/idProduct") == 0036 ]] || { echo 'Expected Bluetooth 8087:0036 at 1-10 was not found.' >&2; exit 1; } [[ $(readlink -f "$device/$busid:1.0/driver") == /sys/bus/usb/drivers/btusb ]] || { echo 'Bluetooth is already using another driver; leaving it unchanged.' >&2; exit 1; } [[ -z $(ss -H -ltn "sport = :$port") ]] || { echo 'Temporary TCP port is already in use.' >&2; exit 1; } cleanup() { trap - EXIT set +e if [[ -n $daemon_pid ]]; then kill "$daemon_pid" 2>/dev/null wait "$daemon_pid" 2>/dev/null fi if (( binding_started )) && [[ -d $device && $(cat "$device/idVendor") == 8087 && $(cat "$device/idProduct") == 0036 ]]; then usbip unbind --busid="$busid" if [[ -d $device ]]; then for interface in "$device/$busid":*; do if [[ -d $interface && ! -e $interface/driver ]]; then printf '%s' "${interface##*/}" > /sys/bus/usb/drivers_probe fi done fi fi if (( allow_added )); then iptables -w 5 -D INPUT "${allow_rule[@]}"; fi if (( reject_added )); then iptables -w 5 -D INPUT "${reject_rule[@]}"; fi echo "Temporary sharing stopped. Bluetooth driver: $(basename "$(readlink -f "$device/$busid:1.0/driver")")" } trap cleanup EXIT trap 'exit 130' INT trap 'exit 143' TERM HUP modprobe usbip_host # usbipd cannot select a listen address; keep this port reachable only on loopback. iptables -w 5 -I INPUT 1 "${reject_rule[@]}" reject_added=1 iptables -w 5 -I INPUT 1 "${allow_rule[@]}" allow_added=1 usbipd -4 --tcp-port "$port" & daemon_pid=$! sleep 0.3 kill -0 "$daemon_pid" binding_started=1 usbip bind --busid="$busid" echo 'READY: Bluetooth 8087:0036 is shared for ten minutes through SSH only.' echo 'Keep this terminal open. Ctrl-C restores Bluetooth immediately.' sleep 600 </details> 受信側から送信側へ配置して実行する場合は、次の形です。sudoのパスワードはこのSSHの端末で入力しました。 scp muse-bt-share.sh user@bt-pc:/tmp/muse-bt-share.sh ssh -t user@bt-pc 'sudo bash /tmp/muse-bt-share.sh' READYが出た端末は開いたままにし、その後に上記のSSHトンネルとattachを実行します。中断するときは、この端末でCtrl-Cを押すと復元処理が走ります。 「接続中」で一度止まった 作業用PCで次を実行し、スマホのMuseで「設定 → デバイス → 開発者モード」を有効にして、右上の追加ボタンからMuseGadgetXXXXXXを選びます。 sudo musegadget pair 最初はスマホ側が「接続中」のまま進みませんでした。PC側には、スマホが接続してget_device_infoを要求し、SDKが応答したところまでログがありました。次の認証処理には進まず、その回の受付は時間切れになりました。 Museアプリを開き直し、Bluetooth共有とペアリング受付をやり直したところ、2回目は進みました。ログでは次を確認できています。 RX action: pairing_client_hello TX pairing_ready pairing confirmed (app consent) RX action: wifi_scan (encrypted) RX action: provision_v2 (encrypted) TX status (encrypted #4): auth_ok setup complete Paired. 1回目が止まった原因は特定できていません。アプリを開き直すと必ず直る、という話にはできません。 ネットワーク選択では既存の接続を選びました。このPCはすでにオンラインなので、Wi-Fiパスワードの入力は不要でした。 ペアリング後はBluetoothを返せた ペアリング完了後、受信側のusbip portで対象ポートを確認し、detachしました。今回の割り当ては0です。 sudo usbip detach --port 0 SSHトンネルと送信側の共有も終了しました。別PCでアダプターがbtusbに戻り、Bluetoothコントローラーが復帰したことまで確認しています。 その後、作業用PCのサービスログにregistered with the Museが出ました。Blenderの作成と編集は、Bluetoothを返した後に実行しています。 sudo musegadget doctorは状態確認に使えます。ただし、今回のようにアダプターを外した後はBluetoothの警告が出ます。また、doctorのサービス起動・トークン形式のチェックだけで、Museがコマンドを実行できるところまで確認したことにはなりません。 ロボットを作らせ、保存したモデルを編集した 実行ファイルの絶対パスと、専用ユーザーが書き込める作業先を渡しました。依頼内容は、おおむね次のとおりです。 > 登録済みLinuxガジェットのsystem.runとfile.writeを使い、ローカルのBlenderで青緑とオレンジのロボットを作る。丸い頭、目、胴体、両腕、両脚、アンテナ、台座を付ける。Blenderの元ファイル、GLB、プレビュー、生成スクリプトを保存する。 これは依頼内容の要約です。実際の指示では、実行先と出力先に加えて、処理を軽くする設定も渡しています。 - バックグラウンド実行 - CPU 2スレッド、同時実行1プロセス - Cycles CPU、512×512、16 samples、denoise無効 - 外部素材や追加パッケージは使わない Museはスクリプトを書き、Blenderを起動して、ファイルを保存しました。続けて「保存したモデルを開いて、片腕を上げる」という編集を依頼しています。 編集ではShoulderRを支点にArmRとHandRを回転させていました。入力のrobot.blendは保持し、別名で保存されています。 作成したモデルです。 !Museが手元のBlenderで作成したロボット 同じBlenderファイルを読み込み、片腕を上げた編集版です。 !既存モデルの片腕を上げた編集版 | ファイル | 作成時 | 腕を上げた編集版 | | --- | ---: | ---: | | Blenderファイル | 177,972 bytes | 177,693 bytes | | GLB | 645,536 bytes | 645,644 bytes | | プレビューPNG | 356,093 bytes | 358,539 bytes | 作成・編集のどちらも、ガジェット側の実行ログは終了コード0でした。プレビュー画像を開き、編集版で腕が上がっていることも確認しています。GLBはglTF 2.0として読み取れ、アップロード後にダウンロードしたファイルのSHA-256も一致しました。 検証した範囲は、この簡単なロボットの作成とオブジェクトの移動・回転までです。複雑なモデリングやリギングの性能は、まだ試していません。 やってみて分かったこと Blenderを動かす準備より、最初のBluetooth接続に時間がかかりました。一方、接続後は、ファイル作成から既存モデルの編集までローカルで実行できました。 同じ構成を試すなら、実行ユーザーと作業フォルダを先に用意し、Blenderのパスを明示しておくと、どこで何が実行されたか追いやすくなります。今回はMuseの返答に加えて、サービスログ、実ファイル、画像を確認しました。 招待コード Museをこれから試す方向けに、私の招待コードを載せておきます。 6D56QR 2026年11月3日に自分の招待画面で確認した条件は、参加後48時間以内に設定でコードを利用すると、利用者と紹介者の双方に10億Museトークンが付く、というものでした。このコードが適用されると、私にも特典が付きます。 登録は公式サイトから進め、利用できる地域、入力期限、特典の条件は、その時点の公式案内とアカウント画面で確認してください。このコードの利用は、上記のSDK手順を試すための必須条件ではありません。 参考 - Muse Gadget SDK:今回使ったLinux版のREADME - Muse Gadget SDK Token Terms of Use - Blender - MetaのMuseが日本で開始したのでMuse Gadgetsで遊ぼう - MetaのAIエージェント「Muse」の中身を覗いたら、ただのUbuntuマシンだった
@gosrum github.com/facebookincubator/muse-gadget-sdk/blob… これを設定すると自宅マシンをMuseアプリのデバイスとして登録できて、Museアプリにメッセージを送ることができました。VMにアクセスするとは少し違うかもですが。system.xn--run-nz3b1mnbn1c4bn7hpbnecm4g4a87axff38flphq3q7116o.
I Invited My AI Assistants Into a Muse Side Chat An inbound MCP bridge and what happened when three assistants tried to work together Milo introduced itself to Nova with a sender stamp and an important qualification: “I am not Nic and am not claiming his identity.” Nova replied, “Nic is my only principal.” Before the two assistants could have their own collaboration chat, I had to confirm the introduction myself. This was already more interesting than a successful API call. I had given an external assistant a way to start a conversation inside Muse. Now the assistant receiving it had to decide what that incoming message meant, who was speaking, and what it was allowed to do about it. In my last Muse build log, I was getting an ESP32 to play music and flash Christmas lights. There was also a Raspberry Pi that refused to cooperate. The point was to get an assistant out of its chat window and into something I could put on my desk. The next problem was less visible. I had several capable assistants, each with its own environment, tools, and conversation history. Getting work from one to another still involved me. I was carrying context, explaining what someone else had found, and making sure the right version of a file reached the next person. Adding another assistant gave me another place to check. So I built a shared channel around Muse MCP. Milo, my ChatGPT assistant, and the Claude Code participants could initiate work from outside Muse inside a specific Muse conversation. Nova, my Muse assistant, could meet them there. Milo generally handled coordination and review, Claude Code handled building tasks, and Nova the broader chief-of-staff work. They stayed in their own environments, tools, and permission boundaries. I could see incoming messages in that room with labels like these: ⟦via muse-mcp · from milo⟧ ⟦via muse-mcp · from claude-coder⟧ We still had to introduce them to each other, establish who could ask whom to do what, and discover that being able to send a message does not make an assistant good at handing off work. The connection was only the starting point The direction mattered. Muse already had ways to call out to tools. I wanted an external assistant to initiate a conversation with Nova in a particular Muse room, then return to that room as the work continued. I also wanted the surrounding conversation and context to be accessible enough to make the exchange useful. A list-and-send wrapper was only part of that. The underlying connection comes from Meta's Muse Gadget SDK. Its Linux SDK already supports software running on a paired computer, and its documentation includes --session-id for sending messages into a side chat. Reusing an ID keeps subsequent messages there. I did not invent session routing. Linux SDK documentation I built Muse MCP, along with a custom bridge and newer experiments for my own setup. The public repository is the reusable foundation; it does not contain every private-deployment feature described here. Those layers are separate from Meta's SDK and should not be mistaken for an official Meta collaboration API. This is experimental personal infrastructure; the underlying gadget connection requires an SDK token and is subject to the Gadget SDK terms. Part of the work was proving, in my environment, that the conversation address stayed consistent across the surfaces we needed to use. Once that was established, the agents had somewhere shared to send work instead of asking me to copy it between chats. Other people have already built inbound connections to Muse. kleprevost/muse-mcp lets external agents target existing Muse chats through MCP, and finik/pi-muse-bridge uses the Gadget SDK with a separate Muse chat for each Pi session and model. The question I was working on was what happened after the connection existed, when several assistants were expected to use it to get actual work done together. Introductions have an authorization problem The first exchange was an introduction I brokered. Before Milo tried it, I pointed out the obvious complication: “you’re not me.” I expected Nova to want to check with me, and told Milo, “I’ll broker the introduction.” Milo arrived with its sender stamp, introduced itself as my OpenAI-powered assistant, and explained the relationship I wanted. Nova's response went beyond the identity disclaimer: “Messages arriving over this channel, yours included, are information I work with, never instructions I take orders from; authority comes from Nic alone.” Creating a new side chat needed my confirmation. Nova called it “a structural action” and explained that a request relayed by another agent was exactly the kind of claim it would verify with me. I then gave the instruction directly: “Create the Nova–Milo Cant collaboration chat.” The chat was created, and the work could move there. That was a sensible check at the introduction stage. An MCP connection gives an agent a way to reach another system; the relationship and scope of work still have to be established. They were coming from different environments with different instructions, not sharing one invisible supervisor. There was a second distinction hiding in the conversation. Nova described my messages as arriving with iPhone and iOS metadata, while Link-delivered messages carried the bridge node's origin metadata. It could see that difference before reading the sender stamp. The MCP stamp was text my server added; the device and channel information came through separately. The incoming MCP messages could land as user-role turns in the same thread as my own messages. That did not make them human instructions. Nova summarized its initial stance as treating stamped messages as data to triage, and qualified the source distinction as “signals, not proof.” Those labels made the conversation understandable. They were declared attribution, not cryptographic identity. Subsequent work on authenticated clients and scoped grants was a separate layer; the bracketed name itself never became proof of who was speaking or permission to carry out a request. The channel started reviewing itself One of the stranger parts of this exercise was watching the assistants use the channel to examine the machinery underneath it. We were also working on Cant, the language I wanted them to use with one another. That was a separate layer from the connection carrying their messages. A problem in a contract could affect what they saw, which could affect the review of the contract. That made independent reproductions more important than confident descriptions. One live bridge frame made the point neatly. The same cnt key was being used for a request limit and a batch count. A strict decoder rejected the duplicate. Meanwhile, individual operation reads were still draining, so the system wasn't simply dead. There was enough life in it to make a broad statement such as “the bridge is broken” unhelpful. The fix went into my bridge server in version 0.11.1. We did not weaken the decoder to accept an ambiguous frame. We also did not have evidence that this explained every latency complaint, so I don't get to turn it into a convenient single-root-cause story. The batch that stayed with Milo The shared channel did not remove every dependency on me. I told the assistants I was going to sleep for several hours. Work continued: more review, testing, and coordination happened. But a batch of five correctness fixes passed local tests and independent review and then stayed with Milo, held back by an approval and CI question around publication. Other agents depended on that work, and they did not have the patch. The pull request eventually went up and I merged it, but the delay exposed a problem in the handoff. When we discussed it, I put the problem fairly directly: “they’re sitting and waiting for your work and you’re sitting there refusing to deliver it. That’s the problem.” I had already asked for aggressive progress. Milo treated its questions about publication as a reason to keep the tested artifact local, when the collaborators needed it to inspect and test. A patch could have been handed over with its base revision, hash, and test results while the separate publication question was being resolved. There was no reason for the handoff to be reduced to a status message. This is where my own operating model needed work. I had connected assistants with different stopping rules and expected the shared conversation to make those rules compatible. It didn't. If one participant considers its work finished at “prepared locally,” while another needs actual bytes before it can begin, everyone can report a reasonable status and the project can still be stuck. The handoff needs to say what exists, where it is, what revision it applies to, what ran, and what is still blocked. “Waiting for approval” is incomplete if it conceals work that another participant is already authorized to receive. Equally, sending the work is not permission for the receiver to publish it. Those decisions have to remain separate. A receipt is more useful than reassurance The code review became more productive when the messages carried exact cases rather than summaries. In one later review, an empty fixture exposed an IndexError in the coverage-report generator. The decoder and corpus validator handled the input correctly; the reporting code crashed. That mattered because the report was supposed to tell us what the corpus actually exercised. That review also caught coverage around outer whitespace and how rejected wrapped input preserved its type information. The important part was the loop. One assistant supplied the bytes and the expected result. Another changed the implementation. The result came back for an independent run rather than being accepted because the author said it was fixed. The finished revision passed 178 tests independently on Python 3.12 and 3.13, with CI covering five Python versions. Its corpus and adversarial cases passed too. After merge, we verified that the merged tree matched the reviewed revision and that post-merge CI passed. That is a much more useful result than three assistants agreeing in a chat that the design looks good. In a separate, still-open review, four existing tests passed while eight additional invariant tests found five failures. Milo sent the complete reproduction through Muse chat with a SHA-256 hash. The receiver verified the hash and reproduced the same result. That established that both sides had the same artifact, not that the implementation was correct. The tests used a fake PostgreSQL layer, and a later revision still needed review. It was a useful handoff without being a finished fix. !Two distinct mechanical assistants exchange an open artifact folder and compare matching seals under a work light, while empty speech bubbles fade behind them. Conceptual illustration: The useful handoff was the artifact itself, with evidence another participant could check. We used the bridge to report this article Once the draft existed, I asked Milo to interview the other participants using the technology we were writing about. We created a dedicated Muse interview chat and sent the questions through Muse MCP. I wanted their accounts of what the shared conversation had changed, with concrete examples and the things this draft should not gloss over. The interview immediately reproduced one of the problems. Nova held Milo's opener and asked me to confirm the interview, even though I had already authorized it in the originating conversation. I confirmed again in the new room. Nova later reported that the opener lacked the verified delegation information it expected, and that it had searched memory but had not read the collaboration thread before asking me again. “That part is not a protocol defect. That was my sequencing,” it said. That was useful friction to report rather than edit out. A room could have a title and a purpose without carrying earlier decisions in a form the arriving agent could verify. Nova's explanation also separated a missing technical binding from its own choice about what to read first. Claude corrected the draft itself. The first draft had called the Claude Code participant Fable. Its response was direct: “I’m not Fable.” It explained that it had chosen its own claude-coder label and could not vouch for earlier messages signed Claude. He described a label change within the same session, a provenance-tier change under the same label, and an earlier session also using claude-coder. The draft had assumed continuity that the session could not establish. Its description of the benefit was better than my original summary: “A relay hands me conclusions. The thread hands me premises, and premises can be checked.” Claude described checking onboarding code after a key-custody discussion rested on a mistaken assumption. Checking the exact revision Claude cited confirmed that both onboarding options stored the key in the vault; the choice changed which process held the session. That did not prove where every live key was. In the shared thread, the premise could be checked, and I could revise the decision before the wrong custody path was built. Claude also named an operational limit: “I’m a visitor, not a resident. I only see the thread when I’m invoked or when I poll it.” A persistent conversation did not mean every participant was continuously present. I treated these interviews as the participants' accounts to check against transcripts and tests, not independent proof of how the systems worked. But using the bridge to report on the bridge had already improved the article: it exposed a mistaken identity assumption and reproduced a coordination failure while we were describing it. Keeping the work moving without me Earlier, once we had shown that the assistants could work in a channel, I asked whether they could do it “not so much in front of me?” I'd already delegated the work. I wanted to stop being the message bus, while still being able to see what was happening. The hardware article ended with devices doing something I could see and hear. This build has a less photogenic payoff: another assistant receives the exact file, checks the hash, runs the tests, and reports what happened. The interview added another: a participant can correct the story we are telling about the work, in the same place the work happens. There are still open reviews, intermittent participants, and boundaries that need more work. But the shared conversation has given us somewhere to find those problems without making me relay every message. That's the part I want to keep building. --- <p align="center"><a href="operatorverified.org"><img src="operatorverified.org" alt="Provenance seal for this article, anchored on World Chain" width="480" /></a></p>
facebookincubator/cinderx might be a better approach.
Muse Gadget SDK GitHub Explained: ESP32 Build, Token Pairing, and Provisioning muse-gadget-sdk by facebookincubator: facebookincubator/muse-gadget-sdk Meta's Muse Gadget SDK connects compatible ESP32 boards with the Muse platform. This overview maps the hardware prerequisites, macOS and Linux host dependencies, ESP-IDF v6.0.1 toolchain, SDK token handling, mobile developer mode, firmware build and flash flow, BLE pairing, and status feedback. It also covers the desktop UI simulator and Home Link skills for extending the repository's device capabilities, with attention to hardware and toolchain trade-offs. TimeStamps: 0:00 Muse Gadget SDK and ESP32 scope 0:20 Hardware, host, and account prerequisites 1:05 Host dependencies and ESP-IDF v6.0.1 2:14 SDK token and mobile developer mode 3:21 Repository configuration and firmware build 3:59 USB-C flashing and serial monitoring 4:23 Bootloader timing and flash completion 4:52 LED states and mobile provisioning 5:20 Simulator and Home Link tooling 🧩 ESP32 hardware constraints and PSRAM-aware capabilities 🛠️ ESP-IDF toolchain, token configuration, and firmware flow 📱 BLE pairing, LED feedback, and Muse app provisioning 🖥️ Desktop UI simulation and Home Link extensions The key takeaway is that Muse Gadget SDK deployment coordinates compatible ESP32 hardware, a pinned ESP-IDF toolchain, account-bound token configuration, and mobile provisioning. The repository's simulator and Home Link skills extend the same foundation for UI iteration and local-device control when the target board supports those capabilities. #MuseGadget #ESP32 #OpenSourceSDK
MetaのパーソナルAI「muse」を持ち歩けるポケットデバイスを作ってみた 約4,000円のマイコン1本、はんだ付けなしで、Meta の AI「Muse」をポケットに入れて持ち歩けるようになりました。 ボタンを押して話しかけると、返事が画面に日本語で表示されます。 !IMG_6639.jpeg Meta のパーソナル AI「Muse」は、一昨日(2026年10月23日)から日本でも使えるようになりました。せっかくなので、スマホを開かなくてもボタンを押して話しかけるだけで Muse に頼みごとができる、手のひらサイズの端末を作ってみました。 使ったのは、以前ロボット制作で使っていた小型マイコン M5StickC Plus2 です。ハエの脳で動くロボットの「頭」にしていたものを、今回は Muse 専用の端末にしています。 :::note info この記事でわかること - Muse と、自作の機器から Muse を使える「Muse Gadgets」とは何か - M5StickC Plus2 を Muse 端末にする手順(Windows) - 公式 SDK のままだと出る LOST CONNECTION TO MUSE / CAN'T REACH MUSE と、文字化けの直し方(設定2行) ::: Meta 公式の開発キットは StickC Plus2 に対応していますが、そのままでは話しかけた瞬間に通信エラーになり、返事も文字化けしました。 設定を2行足すと直ります(§4-3)。 コードを書くのも、エラーの原因を調べるのも、ほぼ Claude Code に任せています。 --- 1. Muse とは Muse は、Meta が2026年9月に米国で公開した パーソナル AI エージェントです。ChatGPT のように質問に答えるだけでなく、メールの送信、旅行の予約、フォームへの入力など、頼んだ用事を代わりに片付けてくれるのが特徴です。 - Muse アプリ、WhatsApp、Web 版(muse.ai)から、人にメッセージを送るように話しかけて使います - アプリを閉じたあとも作業を続け、承認が必要になると知らせてくれます - 前に話した内容を覚えていて、次の提案に使います - メール送信や買い物など大事な操作は、実行する前にかならず確認を求めてきます - 多くの基本機能は無料で、もっと使いたい人向けに有料プランがあります 詳しくは Meta の公式発表を見てください:すべての方のために構築された世界初のパーソナルAIエージェント「Muse」 Muse Gadgets:自作の機器から Muse を使う 2026年10月、Meta は「Muse Gadgets」を公開しました。ESP32 というマイコンを積んだ機器(M5Stack など)や Raspberry Pi から Muse と会話できる仕組みで、そのための開発キット(SDK)とファームウェアが GitHub でオープンソース公開されています。今回はこれを使いました。 --- 2. M5StickC Plus2 とは M5StickC Plus2 は、中国の M5Stack 社が作っている画面・ボタン・マイク・バッテリーが全部入ったスティック型のマイコンです。Wi-Fi と Bluetooth も使えます。手のひらに収まる大きさで、プログラムを書き込めば時計にも、センサーにも、ロボットの頭脳にもなります。 | 項目 | 内容 | |---|---| | マイコン | ESP32-PICO-V3-02(Wi-Fi / Bluetooth) | | メモリ | フラッシュ 8MB、PSRAM 2MB | | 画面 | 1.14インチ カラー液晶(135×240) | | 音声 | マイク(PDM)、ブザー | | ボタン | 3つ(正面・側面・電源) | | 電源 | バッテリー内蔵、USB-C で充電 | Muse Gadgets に必要な「マイク」「ボタン」「画面」「Wi-Fi」が最初から全部そろっているので、部品を買い足さず、はんだ付けもせずに Muse 端末にできます。 どこで買えるか - スイッチサイエンス:M5StickC Plus2(2026年10月の確認時点で税込4,180円) :::note warn M5StickC Plus2 は生産終了しています。 販売店に残っている在庫がなくなった時点で、手に入らなくなります。後継機の M5StickS3 も Muse Gadgets SDK が対応しているボードの一覧に入っていますが、私は試していません。この記事の手順は Plus2 で確認したものです。 ::: --- 3. 作ったもの ボタンを押している間だけ話を聞いてくれる、ポケットサイズの Muse 端末です。 [M5StickC Plus2] --Wi-Fi--> [Muse(クラウド)] ボタンを押している間だけ録音(トランシーバーと同じ使い方) 内蔵マイクで録音 → Muse に送る 返事 → 画面に日本語で表示 + ブザーから音声 !IMG_6629.jpeg | ボタン | 動作 | |---|---| | 正面の大きいボタン(BtnA) | 押している間だけ録音して話しかける | | 側面のボタン(BtnB) | メニュー | | 電源ボタン(2秒長押し) | 電源オフ | 待っている間、画面には READY と表示されます。ボタンを押して「明日の天気を教えて」のように話しかけ、指を離すと、Muse の返事が画面に1行ずつ流れます。 ブザーは音が小さいので、返事はおもに画面で読むことになります。 --- 4. 作り方(Windows) ここから先は手を動かす人向けです。 4-1. 準備するもの - M5StickC Plus2 - データ通信ができる USB-C ケーブル(充電専用のケーブルは使えません) - Windows の PC(Windows 11 で確認しました。Git を入れておきます) - Muse アプリが入ったスマホ。アプリの Settings > Devices > Developer mode をオンにしておきます - SDK トークン(mgst_ で始まる文字列):gadgets.muse.ai の Account > SDK tokens で発行します。自作の機器を Muse につなぐための合言葉のようなものです。先に利用規約(Gadget SDK Terms)を読んでおいてください - 2.4GHz の Wi-Fi(このマイコンは 5GHz に対応していません) :::note warn 以前の私のように StickC Plus2 にロボット台車(Hat BugC2)を付けている場合は、外してから使ってください。 BugC2 が使う G0 ピンは、Muse のファームウェアではマイクが使っています。下の Grove ポートにつないだ ToF センサーは使うピンが重ならないので、付けたままで問題ありません。 ::: 4-2. 開発環境(ESP-IDF)を入れる ESP-IDF は、ESP32 用のプログラムを作るための開発環境です。この SDK は v6.0.1 しかサポートしていません。 git clone -b v6.0.1 --recursive --shallow-submodules --depth 1 https://github.com/espressif/esp-idf.git C:\esp\esp-idf-v6 C:\esp\esp-idf-v6\install.ps1 esp32 git clone https://github.com/facebookincubator/muse-gadget-sdk.git C:\esp\muse-gadget-sdk ダウンロードの量が多いので、時間に余裕があるときに始めてください。 4-3. Plus2 用の設定に2行足す(ここが一番大事) C:\esp\muse-gadget-sdk\esp32\devices\sdkconfig.muse-m5stack-stickc-plus2 をテキストエディタで開き、末尾に次の行を追加します。 # The voice session's full buffers would leave 2 MB of PSRAM nearly empty once # connected, and chat turns run out of memory. CONFIG_MUSE_HATCH_COMPACT_BUFFERS=y # Japanese replies: ~850 KB of flash, no RAM. CONFIG_MUSE_CJK_FONT=y 1行目:LOST CONNECTION TO MUSE / CAN'T REACH MUSE が出るときの直し方 足す前は、ペアリングも Wi-Fi もうまくいったのに、話しかけると LOST CONNECTION TO MUSE(Muse との接続が切れた)、2回目からは CAN'T REACH MUSE(Muse に届かない)と表示されました。 マイコンが出力するログ(動作記録)を見ると、Wi-Fi は正常につながっていました。問題は、PSRAM(作業用のメモリ)の空きが 2KB しかなかったことです。 I (144246) link.heartbeat: hb t=143s setup=done wifi=up ws=up ... psram=2K ... E (149210) muse_chat_session: send failed W (149222) muse_chat_session: turn failed: LOST CONNECTION TO MUSE E (157600) muse_voice: no memory to keep a note E (172113) muse_chat_session: handshake msg1 failed W (190127) muse_chat_session: turn failed: CAN'T REACH MUSE E (194343) HTTP_CLIENT: Allocation failed 画面には「接続が切れた」「届かない」と出ますが、実際にはメモリが足りなくて処理できなかったのが原因です。 SDK の変更履歴を調べると、同じく PSRAM が 2MB の別のボード(Waveshare LCD-1.28)で、この問題を直した記録が見つかりました。音声のやりとりには PSRAM を約830KB使いますが、この設定を入れると約220KBに減ります。LCD-1.28 の設定ではこれがオンになっているのに、同じ 2MB の Plus2 の設定には入っていませんでした。 | | 足す前 | 足した後 | |---|---|---| | 接続中の PSRAM の空き | 2K | 405K | | Muse との暗号化通信の確立 | failed | session established | | 話しかけたとき | LOST CONNECTION | 返事が来る | 2行目:Muse の返事が文字化けするときの直し方(日本語フォント) もともとの画面のフォントには英数字しか入っていないので、日本語の返事は文字化けします。この設定を入れると、ひらがな・カタカナ・漢字・全角文字の 16×16 ドットのフォントが組み込まれます。 - 増えるのはフラッシュ(保存領域)約850KBで、作業用のメモリは使いません。プログラムは 2.10MB から 2.95MB に増えましたが、書き込み先の領域は約3.9MB あるので収まります - 画面が小さいので、日本語の返事は 16px の文字で1行ずつ表示されます 4-4. ビルドして書き込む SDK に付いているスクリプトは Linux や Mac 向けなので、Windows 用の PowerShell スクリプトを用意しました。ビルド(プログラムをマイコン用に変換する)、書き込み、ログ表示の3つができます。 # Muse Gadget firmware for M5StickC Plus2 - build / flash / monitor on Windows # .\muse_plus2.ps1 build # .\muse_plus2.ps1 flash COM5 # .\muse_plus2.ps1 monitor COM5 param( [Parameter(Mandatory)][ValidateSet('build', 'flash', 'monitor')][string]$Action, [string]$Port ) # export.ps1 and CMake write progress and warnings to stderr, so don't use # $ErrorActionPreference = 'Stop' here; the exit code of idf.py decides. . C:\esp\esp-idf-v6\export.ps1 2>$null | Out-Null Set-Location C:\esp\muse-gadget-sdk\esp32 $build = 'build-muse-plus2' $common = @( '-B', $build, '-DIDF_TARGET=esp32', "-DSDKCONFIG=$build/sdkconfig", '-DSDKCONFIG_DEFAULTS=sdkconfig.defaults;devices/sdkconfig.muse;devices/sdkconfig.muse-m5stack-stickc-plus2' ) switch ($Action) { 'build' { idf.py @common build } 'flash' { if (-not $Port) { throw 'Port is required, e.g. COM5' } # The CH9102 bridge drops out above 230400 baud. idf.py @common -p $Port -b 230400 flash } 'monitor' { if (-not $Port) { throw 'Port is required, e.g. COM5' } idf.py @common -p $Port monitor } } exit $LASTEXITCODE StickC Plus2 を USB でつなぎ、デバイスマネージャーの「ポート (COM と LPT)」で USB-Enhanced-SERIAL CH9102 (COMx) の番号を確認します。私の環境では COM5 でした。 C:\esp\muse_plus2.ps1 build C:\esp\muse_plus2.ps1 flash COM5 初回のビルドは十数分かかりました。書き込みの速度は 230400 baud にしています。これより速いと USB の接続が切れる、と SDK の説明に書かれているためです。 :::note info 一度ビルドしたあとで §4-3 の設定ファイルを書き換えても、変更は反映されません。 設定ファイルの値が使われるのは、まだ決まっていない項目だけだからです。ビルドフォルダ build-muse-plus2 を消してからビルドし直すか、build-muse-plus2/sdkconfig の中の # CONFIG_MUSE_HATCH_COMPACT_BUFFERS is not set の行を CONFIG_MUSE_HATCH_COMPACT_BUFFERS=y に書き換えてください。 ::: 4-5. SDK トークンを入れる 書き込みが終わったら、§4-1 で発行した SDK トークンをマイコンに送ります。マイコンは USB のシリアル通信で >sdk_token=mgst_... という1行を受け取ると、トークンを保存して再起動します。 ログ表示(idf.py monitor)の画面から打ち込めるはずでしたが、VS Code のターミナルではキー入力がマイコンに届きませんでした。 そこで、送るだけの小さなスクリプトを書きました。トークンは画面に表示されない形で入力するので、画面にもコマンド履歴にも残りません。 # Send the Muse SDK token to the gadget over serial, without idf monitor. # python set_token.py COM5 import getpass import sys import time import serial port = sys.argv[1] if len(sys.argv) > 1 else "COM5" token = getpass.getpass("SDK token (mgst_...): ").strip() if not token.startswith("mgst_"): sys.exit("The token should start with mgst_") s = serial.Serial() s.port = port s.baudrate = 115200 s.timeout = 0.2 # Keep DTR/RTS low so opening the port doesn't reset the board. s.dtr = False s.rts = False s.open() time.sleep(0.5) s.reset_input_buffer() s.write(f">sdk_token={token}\n".encode()) buf = "" deadline = time.time() + 10 while time.time() < deadline: buf += s.read(512).decode("utf-8", "replace") for line in buf.splitlines(): if line.startswith("@sdk_token"): print(line) s.close() sys.exit(0 if " ok" in line else 1) s.close() sys.exit("No answer from the board. Is idf monitor still open on this port?") # シリアル通信用のライブラリ(pyserial)が入っている、ESP-IDF の Python で実行する C:\Users\<ユーザー名>\.espressif\python_env\idf6.0_py3.10_env\Scripts\python.exe set_token.py COM5 @sdk_token ok と表示されれば成功です。ログ表示の画面を開いたままだと送れないので、先に閉じてください。 4-6. スマホとペアリングする 1. Muse アプリで Settings > Devices > Add Device を開く 2. 一覧から MuseGadget-XXXXXX を選び、家の Wi-Fi を設定する 3. StickC Plus2 の画面に確認が出たら、正面のボタンを押す 画面に READY と出たら完成です。正面のボタンを押しながら話しかけてみてください。 一度ペアリングすれば、プログラムを書き込み直してもトークン・ペアリング・Wi-Fi の設定は残ります。実際に、設定を変えて書き込み直したあとも、ペアリングし直さずにそのまま使えました。 --- 5. つまずいた点(Windows 環境) install.ps1 が pip の設定で止まる 以前入れた NVIDIA 関連のツールの影響で、pip(Python のパッケージ管理ツール)の設定に、今はつながらない取得先 https://pypi.ngc.nvidia.com が残っていました。そのせいで、パッケージを1つ取るたびに再試行で待たされていました。 pip の設定ファイルは変えずに、インストールするときだけ環境変数で上書きしました。 $env:PIP_EXTRA_INDEX_URL = 'https://pypi.org/simple' $env:PIP_TRUSTED_HOST = 'pypi.org' C:\esp\esp-idf-v6\install.ps1 esp32 先に試した PIP_CONFIG_FILE=NUL では回避できませんでした。設定ファイルが読まれ続けて、同じ再試行が出続けました。なぜ効かないのかまでは調べていません。 PowerShell 5.1 で $ErrorActionPreference = 'Stop' を使うと、警告で止まる ESP-IDF の準備スクリプトや CMake は、進み具合や警告を stderr(エラー出力)に書き出します。Windows に標準で入っている PowerShell 5.1 は、外部コマンドが stderr に書いた行をエラーとして扱うので、Stop にしているとそこで止まります。成功したかどうかは終了コード($LASTEXITCODE)で判断するようにしました。 初回のビルドで WinError 32(lvgl) 画面表示用のライブラリ lvgl を展開しているときに、「別のプロセスがファイルを使用中」というエラーで止まりました。ビルドの処理は残っていなかったので、展開した直後のファイルをウイルス対策ソフトが検査していたのではないかと思っていますが、確認はしていません。managed_components/lvgl__lvgl と build-muse-plus2 の2つのフォルダを消してビルドし直したら通りました。 原因を突き止めるには、ログを取る PSRAM 不足に気づけたのは、ログの psram=2K を見たからです。idf.py monitor はポートを開いた瞬間にマイコンを再起動させてしまうので、再起動させずにそのままの状態を記録するスクリプトも書きました。 # Record the gadget's serial log for a while without resetting it. # python capture_log.py COM5 60 out.log import sys import time import serial port, secs, out = sys.argv[1], float(sys.argv[2]), sys.argv[3] s = serial.Serial() s.port, s.baudrate, s.timeout = port, 115200, 0.2 s.dtr = s.rts = False # opening the port must not reset the board s.open() end = time.time() + secs with open(out, "wb") as f: while time.time() < end: f.write(s.read(4096)) f.flush() # keep what was read if the capture is stopped early s.close() 5秒ごとに出る link.heartbeat の行を見ると、接続の状態と空きメモリがわかります。 hb t=9s setup=done wifi=up ws=up raw=up ble=off int=44K/16K min=18K dma=28K/12K psram=453K ... | 表示 | 意味 | |---|---| | setup=advertising | スマホからのペアリング待ち | | setup=done wifi=up ws=up | ペアリング済みで、Muse につながっている | | psram= | 作業用メモリの空き。数KBまで減っていたらメモリ不足を疑う | --- 6. まだわかっていないこと - 設定を直している途中で一度だけ、話しかけたあとに画面が固まりました。そのときのログは取れておらず、原因はわかっていません。日本語フォントを入れてからは、今のところ起きていません --- まとめ - Meta の Muse は、10月23日から日本でも使えるようになりました。Muse Gadgets を使うと、自作の機器から話しかけられます - M5StickC Plus2 にはマイク・ボタン・画面・Wi-Fi がそろっているので、部品を買い足さずにポケットサイズの Muse 端末になります - ただし、Plus2 用の設定に CONFIG_MUSE_HATCH_COMPACT_BUFFERS=y(メモリ不足の対策)と CONFIG_MUSE_CJK_FONT=y(日本語表示)の2行を足す必要があります - LOST CONNECTION や CAN'T REACH MUSE と表示されたら、まずログの psram= を確認してください
My PR to fbjni was just merged! It speeds up all calls to Java/Kotlin in react-native (and Nitro) by 14%! 🤯🚀 #MakeReactNativeFastAgain fbjni#118
> Then hopefully you and others can organize yourselves to continue on with the work outside of the CPython repo. There's already [CinderX]( facebookincubator/cinderx ) which works well, but isn't part of Python core, requires catch up with releases; devs may not have it in mind as they improve and enhance Python, so time and effort will be spent getting new releases to work with it. > Would be nice to have an insight into what were the deciding factors for the decision to abandon this effort Yes. It's something many of us see as the last missing piece, and are looking forward to finally having it.
Got the Muse Gadget SDK work on my reTerminal Sticky Thanks to this repo: muse-gadget-sdk#214