サマターゲの顔
Stack-chan ESP32-S3 IoT

サマターゲをStack-chanへ
— 顔・音声・赤外線を壊さず追加する

既存機能を残したまま、机の上のキャラクターを家電操作端末へ。
変更範囲を小さく保つための設計記録です。

← Articles 一覧へ戻る

作ったもの

M5StackのStack-chanを土台に、サマターゲの顔で反応し、話しかけると会話やエアコン操作ができるデスクトップロボットを作っています。

重要だったのは、公式ファームを別物へ置き換えないことでした。マイク、スピーカー、タッチ、カメラ、サーボ、Wi-Fi、瞬き、視線、発話アニメーションの経路を残し、サマターゲ固有の処理を薄い統合層として追加しています。

基準: 2026年7月30日時点の開発ブランチ。遠隔操作の実URL、認証トークン、Wi-Fi情報は公開していません。

顔だけを置き換える

Stack-chanの画面は320×240です。Web版の絵を縮小すると細部が読めなくなるため、サマターゲの特徴を次の四つへ絞りました。

  • 縦長の白い目
  • 目の中だけを移動する小さな瞳
  • 感情を示す独立した眉
  • 発話量に応じて幅・高さ・丸みが変わる口

公式の小さな目全体を動かす処理から、固定した目の中で瞳だけを動かす構成へ変更しました。ただし、呼吸、瞬き、頭をなでた反応、IMUによる傾きは既存のmodifierをそのまま登録しています。

感情を切り替えた直後、瞬き処理が変更前の目の状態へ戻す問題がありました。感情変更時に瞬き側の基準値も同期することで、二つのアニメーションが同じ部品を奪い合わないようにしています。

ウェイクワードを必須にしない音声経路

現在の構成では、「サマターゲ」と毎回呼ぶ必要はありません。本体はローカルの音声フロントエンドで、一定時間続く発話だけを検出します。発話があったときだけ所有者のVPSへ一つのセッションを開き、処理後はローカル検出へ戻ります。部屋の音を常時VPSへ流し続ける構成ではありません。

日本語の文字起こしはVPS上の faster-whisper で行います。処理先を区別しました。

  • 固定の家電命令:ルールで判定し、外部の文章生成APIを呼ばない
  • 固定の返答音声:VPSへ事前キャッシュし、操作ごとの生成を避ける
  • 自由な質問:この場合だけテキストAPIへ渡す
  • 音声合成:初回生成後はモデル・声・本文の組み合わせでキャッシュする

APIキーは本体にもGitHubにも置かず、VPSのサービス用環境ファイルだけに保存します。

Wi-Fi処理中でも赤外線を崩さない

エアコンの赤外線信号は、短いオン・オフを正しい長さで並べる必要があります。CPUの待機だけで波形を作ると、Wi-Fiや音声タスクの割り込みでタイミングが伸びる可能性があります。

そこで、DAIKIN312形式のフレームをESP32-S3のRMT symbolへ先に変換し、搬送波を含む一つのDMAバッファとして送ります。実装した操作は、自動、冷房、暖房、停止、温度上げ、温度下げの6種類です。

RMTの1 symbolに入らない長い区間は、CPUで待たず、Lowのsymbolを複数へ分けます。1回の命令につき赤外線フレームは1回だけ送信し、同じ操作が二重に実行されないようコマンドIDも確認します。

赤外線の送信成功は、家電が実際に状態を変更したことの確認ではありません。内部状態は「要求した」「送信した」「確認できた」を分け、現在のセンサー未接続状態では「送信した」までを扱います。

自宅ルーターのポートを開けない

スマートフォンから操作するときも、Stack-chanや自宅ルーターへ外部から直接接続しません。認証付きのVPSキューへスマートフォンが命令を登録し、本体が外向きHTTPSで定期確認します。

管理画面、本体ポーリング、音声セッションには別々のトークンを使います。命令にはUUIDと作成時刻を付け、本体は古すぎる命令と直前に実行したIDを拒否します。これにより、再読込や通信再試行で同じ赤外線が繰り返し出る可能性を下げます。

音声処理中にTLS接続を始めると音が途切れる可能性があるため、遠隔操作のポーリングは本体がidleになってから行います。

上流を残して分かったこと

全面的に書き直す方が、最初は速く見えます。しかし、カメラ、タッチ、サーボ、音声、OTAなどを同じ品質で作り直す必要が生まれます。今回は次の境界を決めたことで、公式機能を利用しながら独自部分を追えました。

  • 顔の見た目はskin側へ閉じ込める
  • 家電操作は一回で完了するMCP toolにする
  • 秘密情報は追跡外のローカルヘッダーとVPS環境へ置く
  • 汎用OTAは独自拡張を消すため無効にし、利用者が明示した更新だけを許可する

キャラクターを追加することより、既存のライフサイクルを壊さない境界を探すことの方が、この実装では大きな作業でした。