ほとんどのデスクガジェットは情報を表示するだけです。私が欲しかったのは、人に反応するものです。話しかけるとこちらを見て、答えてくれ、誰もいないときには何か面白いことをするロボットです。Espressif ESP-VoCat v1.2にはそのためのハードウェアが備わっています。ESP32-S3 SoC、360×360の丸いタッチスクリーン、2つのマイク、スピーカー、バッテリーを猫耳付きのケースに収めています。
VoCat Eyesは私がそのために書いた顔であり、Cute Desktop Robotというシリーズの最初のロボットです。主な特徴は次のとおりです。
- 56種類の表情と47種類の短いアニメーション
- ロボットの行動を決める小さなステートマシン — 遊ぶ、反応する、退屈する、眠る
- 話している人の方向に目を向ける
- ウェイクワード「Hi ESP」またはタップで始まる、Gemini Live APIを通じた音声会話
- ロボット自身の画面で設定できるWi-Fi、音量、明るさ、音声の設定
- 同じコードをPC上およびこのページ内で実行するエミュレータ
ファームウェア、エミュレータ、ツールはcute-desktop-robotリポジトリでオープンソース公開しています。

上の画像はエミュレータの3Dビューです。Espressifが公開した筐体モデルの中に、エミュレートされたロボットのライブスクリーンが表示されています。
そしてこちらが、同じファームウェアを実行している実際のデスク上のロボットです。
実機上のロボット — YouTubeで見る。
ブラウザでロボットを試す
下のエミュレータは、WebAssemblyにコンパイルされたロボットのC++コードを実行します。ESP32-S3上で動くのと同じ顔エンジン、ステートマシン、画面が動作します。ハードウェア層だけがシミュレートされた部品に置き換わっています。アカウントもインストールも不要です。エミュレータにはネットワークがないため、音声会話だけは実機が必要です。
フレームが小さすぎる場合は、エミュレータを全画面で開いてください。
ウィンドウは3つの部分に分かれています。左側は3Dのロボット、その下がステートマシン、右側が感情グラフとセンサーのパネルです。試すには次のようにします。
- ロボットの画面をタップする。 3Dロボットの丸いスクリーンを一度クリックします。ロボットが次のアニメーションを再生します。
- 設定を開く。 時計とバッテリーが表示されるまで画面をクリックし続け、次に時計を一度クリックします。設定メニューが開きます。行の左半分をクリックすると値が下がり、右半分をクリックすると上がります。メニューを閉じるにはクリックし続けます。
- 片側から話しかける。 右側のパネルのSENSORSの下で、fromの隣のスライダーをある角度までドラッグし、**talk [V]**をクリックします。床の紫の点がシミュレートされた声の発生源を示し、ロボットの目がそちらを向きます。
- ステートマシンを観察する。 ロボットが行動を変えるたびに、ステートマシン内の対応するボックスが点灯し、下部のバーが次にそれを変えうるタイマーを示します。
- ロボットを回転させる。 画面の外側のどこかをドラッグして3Dビューを回転させ、マウスホイールでズームします。
Mを押すと画面のフラットビューに切り替わります。
エミュレータを一度クリックすればキーボードも使えます。1から9で表情を表示し、Yでうなずき、Uで首を振り、Hで吹き出しを表示します。タップと設定は実機でも同じように動作します。
次のスクリーンショットは、デスクトップPC上でのエミュレータウィンドウ全体の様子です。

ハードウェア
ESP-VoCatはケース、バッテリー、スクリーンが組み付けられた状態で届きます。ESP-VoCatユーザーガイドにボードの詳細が記載されています。1つのESP32-S3モジュールがすべての処理を担います。16 MBのフラッシュメモリと16 MBのPSRAM(外部RAM)を搭載しています。PSRAMがあるからこそ360×360の顔が実現できます。16ビットカラーの1フレームは253 KBで、SoCの内部RAMには収まりません。
次の図は、ファームウェアが使用する部品と、それぞれをESP32-S3に接続するバスを示しています。
2つの部品には追加の作業が必要でした。
- スクリーンはバックライトが点灯するものの、黒いままでした。ベンダードライバから適応した初期化シーケンスに、最後の2つのコマンドが欠けていました。ピクセルフォーマット(
0x3A)とディスプレイオン(0x29)です。 - マイクは最初、ゼロしか返しませんでした。マイクADC(ES7210)とスピーカーDAC(ES8311)は同じI2Sクロックラインを共有しています。そのため、I2Sバスは両方向で同じフレームフォーマットを使う必要があります。4つの16ビットスロットを持つTDM(時分割多重)です。この変更により、2つのマイクはスロット0と2に現れ、スロット1はエコー参照用のスピーカー信号のコピーを運びます。
ロボットが顔を描く仕組み
ロボットは33 msごと、つまり1秒間に約30回、顔を再描画します。次の図は、そのうちの1フレームをセンサーからスクリーンまで追跡したものです。
図の各ブロックはファームウェアの1モジュールです。
- Sensorsはタッチ、バッテリー残量、時刻、音イベントを1つの構造体に収集します。
- Brainはawake、show、reacting、dozing、sleeping、info、manualという状態を持つステートマシンです。各状態は、タッチ、大きな音、時刻、長時間の孤独といった特定のイベントで抜けます。Brainはゆっくりとした気分も追跡します。よく撫でられるロボットは幸せな顔に傾き、あまりにつつかれすぎると不機嫌になります。
- FaceEngineは表情間をグラフに沿って移動します。56種類の表情がノードであり、顔の変更は最短経路をたどるため、ロボットは幸せから悲しみへと中間の表情を経て移行します。各目はサイズ、角の丸み、傾き、まぶた、色といったいくつかのパラメータで記述され、エンジンがそれらを補間します。
- CanvasはPSRAMにフレームを描画し、変更された24×24タイルを記録します。まばたきはいくつかのタイルを変えるだけなので、ディスプレイバスは253 KB全体ではなく数キロバイトだけを送信します。
- VocatLcdは変更されたタイルをQSPI(4線式SPIバス)経由でST77916ディスプレイに送信します。
すべての表情とアニメーション
エミュレータには facetool というコマンドラインツールが含まれており、ウィンドウを開かずにロボットの画面をレンダリングできます。以下の動画はこのツールから生成されたものです。各動画では、あるグループのすべての顔が同時に再生されます。まず各顔への変化、次にその顔自体が表示されます。
表情
穏やか・考え中(16種類)— 通常、好奇心、集中、懐疑、退屈、困惑、傾聴など。
幸せ(15種類)— 小さな微笑みから大笑い、ラブラブ、目が星になる表情まで。
悲しい(8種類)— 心配、懇願、泣く、孤独、罪悪感など。
怒り(6種類)— イライラから激怒、そして嫌悪まで。
驚き・警戒(7種類)— 驚き、怖がる、目を回す、気絶、催眠など。
眠い(4種類)— 眠い、あくび、眠っている、疲れた。
アニメーション
アニメーションはコード内では「アクト」と呼ばれ、短いシーンです。多くは目の横にオブジェクトが描かれます。雨雲、コーヒーカップ、レーダースクリーンなどです。Brainはロボットがアイドル状態のときにこれらを再生し、画面をタップすると次のアクトが再生されます。
ボディランゲージ(9種類)— うなずき、首振り、目を回す、くしゃみ。
画面エフェクト(5種類)— グリッチ、再起動、スキャナーライン、落下するコード。
オブジェクト付き(18種類)— ハート、雨、花火、泡、ボール、ダンス。
日常のもの(9種類)— 本、ヘッドフォン、コーヒー、ロケット、雪。
ゲーム・丸型画面エフェクト(6種類)— Pac-Man、Pong、虹、レーダー、水族館。
同じグリッドのアニメーションGIFはリポジトリのREADMEにあります。
ロボットが話す仕組み
会話にはGemini Live APIを使用しており、1つのWebSocket接続で音声ストリームを受信し、音声ストリームで応答します。執筆時点では、ロボットはgemini-2.5-flash-native-audioモデルを使用しています。以下の図は音声の経路を示しています。
ロボットは1つのマイクの音を16 kHzのPCM音声として、100 msのチャンクで送信します。Geminiは24 kHzの音声で応答し、ロボットはそれを16 kHzに変換して再生します。
会話は半二重方式です。ロボットは聞くか話すかのどちらかで、同時に両方は行いません。スピーカーはマイクから数センチしか離れていないため、ロボットは応答の再生中にマイクストリームをミュートします。そうしないとGeminiが自分の声を聞いてしまいます。相手が話し終えたかどうかはGemini側が判断します。
Geminiはまた、会話中に呼び出せる4つのツール(関数)を受け取ります。set_face、play_act、look、show_textです。会話が悲しい方向に進むと、Geminiはset_face("sad")を呼び出せます。ロボットに同意を求めると、play_act("yes")を呼び出してロボットがうなずきます。各セッションはローカルの時刻と日付を含む短いテキストメッセージで始まるため、Geminiはそれらについての質問に答えられます。
開発時間の大部分は3つの問題に費やされました。
- セキュア接続。 ロボットはTLS経由でGeminiに接続します。ArduinoWebsocketsライブラリの
setInsecure()関数はESP32-S3では効果がないため、TLSハンドシェイクが失敗しました。ファームウェアは現在、Googleの2つのルート証明書(GTS Root R1とR4、2036年まで有効)を組み込んでいます。また、証明書の検証には現在の日付が必要なため、NTPによる時計の同期を待機します。 - 内部RAM。 「Hi ESP」ウェイクワードはEspressifの音声認識ライブラリであるESP-SR上で動作し、約100 KBの内部RAMを占有します。TLSライブラリはハンドシェイクに内部RAMを必要とし、最初の試みは
SSL - Memory allocation failedで失敗しました。ロボットは現在、会話が始まるとESP-SRを停止して107 KBを解放し、会話が終わると再び読み込みます。ESP-SRの読み込みには約1.2秒かかります。 - マイクストリームの欠落。 ある日、ロボットはユーザーに挨拶した後、その後のすべてを無視しました。目は依然として私の声を追っていたので、マイクは明らかに動作していました。原因は、ファームウェアがGeminiへの音声ストリームを有効にしていなかったことでした。シリアルコンソールがGeminiによる聞き取り内容の文字起こしを表示するようになったため、同じ問題はすぐに発見できるでしょう。
Geminiを使用するにはGoogle AI StudioのAPIキーが必要で、APIはGoogleのGemini API料金に従って使用量に応じて課金されます。キーはローカルのsecrets.hファイルに保存され、リポジトリにコミットされることはありません。
ロボットの画面での設定
一度セットアップすれば、ロボットにコンピュータは不要です。画面を長押しすると、時計、バッテリー残量、Wi-Fiの状態が表示されます。時計をタップすると設定が開き、以下を変更できます。
- 音量と画面の明るさ
- Geminiの声(8種類から選択)
- 「Hi ESP」ウェイクワードのオン・オフ
- 表情に伴う効果音
- ロボットが自発的に再生・スリープするかどうか
- 会話を終了する無音時間(秒)
Wi-Fiの行をタップすると、範囲内のネットワークの一覧と、大文字、数字、記号を備えたオンスクリーンキーボードが開きます。ESP32-S3はネットワークへの接続中にスキャンできないため、ファームウェアはスキャン中に接続試行を一時停止し、その後再開します。パスワードが間違っている場合は、時計画面にその理由が表示されます。ESP32-S3は2.4 GHzネットワークのみをサポートしています。
ロボット、PC、ブラウザで共通のコードベース
ロボットのコードは、ディスプレイ、タッチ、オーディオのドライバを直接呼び出しません。顔、Brain、画面はsrc/hwとsrc/audioにある薄いハードウェア層を経由します。エミュレータはその層を置き換えるため、残りのコードは3つのターゲット向けに変更なしでビルドされます。
エミュレータでは、Arduino.h、Wire.h、SPI.hの小さな代替バージョンが、ハードウェアの代わりとなる構造体に書き込みます。この構造体は、ディスプレイメモリ、タッチコントローラのレジスタ、バッテリーの読み取り値を保持します。raylibグラフィックスライブラリで書かれたエミュレータウィンドウがその構造体を描画し、センサー値を変更できるようにします。
ブラウザ版は、C++をWebAssemblyに変換するEmscriptenでコンパイルされた同じエミュレータプログラムです。これを可能にしたのは3つの変更です。
- Web向けraylib。 raylibはEmscriptenでビルドすると、WebGL経由で描画する静的ライブラリになります。
- 1回の呼び出しで1フレーム。 デスクトップ版は無限ループで動作していました。そのループは1フレームを描画する関数になりました。デスクトップ版はそれをループ内で呼び出し、ブラウザは画面のリフレッシュごとに1回呼び出します。
- 固定ウィンドウサイズ。 ブラウザでは、エミュレータは1380×900のキャンバスに描画し、ページがキャンバスを拡大縮小してフィットさせます。サイズ変更可能なキャンバスは、マウス座標とキャンバス座標の対応を壊してしまいました。
3D筐体はEspressifによるものです。ESP-CollegeはESP-VoCat(EchoEar)のケースをSTLファイルとしてOSHWHubでGPL-3.0ライセンスの下に公開しています。短いPythonスクリプトがSTLファイルを1つの3Dモデルファイルに変換し、ブラウザが素早く読み込めるように詳細度を下げます。エミュレータはライブ画面をケースの丸い窓の中の円盤に描画します。3D画面をクリックすると、エミュレータはその円盤上のクリックされた点を見つけ、対応するピクセルへのタッチに変換します。
自分で作る
VoCat Eyesをビルドするには、ESP-VoCat v1.2ボードと、執筆時点ではESP32用Arduinoコアのバージョン3.3が必要です。VoCat Eyes READMEにライブラリと正確なコマンドが記載されています。要約すると、以下のとおりです。
secrets.example.hをsecrets.hにコピーし、Gemini APIキーを追加します。Wi-Fiネットワークもそこで入力できますし、後からロボット上で設定することもできます。esp_sr_16パーティションスキームとスケッチフォルダ内のpartitions.csvファイルを使ってスケッチをコンパイルします。このレイアウトではアプリケーションに6 MBのフラッシュが割り当てられます。- ファームウェアを「Hi ESP」ウェイクワード用のモデルファイル
srmodels.binと一緒にアップロードします。 - ロボット上で画面を長押しし、時計をタップし、Wi-Fiを選択してネットワークを選び、パスワードを入力します。
- 「Hi ESP」と言うか、画面下部の小さなマイクをタップして、話し始めます。
ESP32-S3や他のボード向けのファームウェアを設計しているなら、Velxioでハードウェアに書き込む前にブラウザでシミュレーションできます。