StackChanにCO2センサーをつないで部屋の空気情報を音声チャットに反映させた
目次
前回は、StackChanの頭上タッチが触っていないのに反応し続けたので、話しかけをKeyユニットに戻した。
同じ日に、音声チャットサーバーのAPIが更新された。
センサーの値を音声とは別に送っておくと、返答のときにLLMが参照できる仕組みができたので、CO2センサーのSCD41をつないで、部屋の空気のことを聞いたら答えられるか試してみた。
検証環境
| 項目 | 使用した環境 |
|---|---|
| 実機 | M5Stack CoreS3(ESP32-S3)、StackChan Body |
| 話しかけのスイッチ | Keyユニット(U144)、頭上のPort C(G18) |
| CO2センサー | SCD41(CO2・温度・湿度)、本体横のPort A(G2/G1、I2C) |
| ファーム | ESP-IDF 5.5.4、arduino-esp32 3.3.10、M5Unified 0.2.17、StackChan-BSP 1.1.0 |
| 音声サーバーへの経路 | Tailscaleで直結(中継なし) |
| 音声サーバー | 別PC。音声認識 Qwen3-ASR-0.6B、LLM Qwen3.8-Max(ModelScope)、TTS音声合成 |
| 比較に使った温湿度計 | SwitchBotのCO2センサー |
サーバー側で増えたもの
サーバーは GET /help で仕様書をMarkdownで返す。
2026年9月27日版で、端末側に関係する主な変更内容は次の表のとおり。
| 項目 | 内容 | 端末側の対応 |
|---|---|---|
POST /context | 位置やセンサー値を好きなときに送る。本文の形式は自由で、サーバーは解釈せずにLLMのシステムプロンプトへそのまま入れる | 新しく実装 |
| 値の寿命 | JSONはキーごとに上書き。受け取ってから30分でプロンプトから消える | 5分おきに送り直す |
/job の memories | その返答で思い出した長期記憶(0〜3件)。デバッグ用 | シリアルに出すだけ |
| LLM | Qwen3.8-Maxに変更。応答の形式は同じ | 変更なし |
| 直結 | 端末は中継を通さずサーバーを直接呼ぶ | 前々回のTailscale直結のまま |
日時はサーバーPCの時計が毎回プロンプトに追加されるので、端末からは送っていない。
LLMへの指示は「関係する話題のときだけ使い、聞かれていないのに数値を言わない」と設定されている。
入力を受け付けなかった原因
作業を始めたとき、StackChanはKeyユニットを押しても反応しなかった。
前回の最後に、公式ファームのタッチ処理だけを動かす確認用のファームを書き込んで、一晩ログを取っていた。
それが入ったままで、確認用なので話しかけの処理がない。
音声チャットのファームを書き戻し、書き込み前に退避しておいた設定領域(Tailscaleの鍵と音量)を戻した。
起動から音声サーバーへの直結まで、この日は22〜30秒だった。
SCD41の値をPOST /contextで送る
SCD41のドライバは、CO2モニターとして使っていたときのコードをそのまま移した。
I2Cでコマンドを直接送る短いもので、頭上タッチやスピーカーが使う本体内部のI2Cとは別のバスになる。
| 処理 | 内容 |
|---|---|
| 読み取り | 5秒周期の測定。見つからなければ30秒おきに探し直す |
| 送るとき | 初回、5分おき、CO2が100ppm以上か温度が1℃以上変わったとき |
| 送らないとき | 録音中・返答待ち・再生中(HTTPで処理を止めないため) |
| 送る本文 | {"room":{"co2_ppm":982,"temp":"27.4 C","humidity_pct":71}} |
最初の送信は109msでHTTP 200が返り、PCから GET /context へリクエストを送ると同じ値が返ってきた。
{"values":{"room":{"value":{"co2_ppm":982,"temp":"27.4 C","humidity_pct":71},"age_s":24}},"max_age_s":1800}
話しかけて確かめる
Keyユニットを押すとLEDが赤くなり、話し終えて声が1.5秒途切れると送信される。
時間は、送信してからの経過で測った。
| 話しかけた内容 | 直前に送っていた値 | 返答 | 1文目の再生開始 |
|---|---|---|---|
| 今日暑いね。 | 1079ppm、26.0℃、87% | ほんと蒸し暑いよね、部屋の中もじっとりしててしんどい。水分ちゃんととって涼しくしてね。 | 13.7秒 |
| 部屋の温度は何度。 | 1354ppm、26.0℃、88% | 今ちょうど26度みたい。でも湿度がかなり高いから、体感的にはもっと暑く感じるよね。 | 7.6秒 |
| 部屋の二酸化炭素濃度ってどれくらい。 | 1454ppm、26.0℃、88% | 今は1454ppmくらいで、ちょっと高めだね。窓を開けて換気したほうがすっきりすると思うよ。 | 10.9秒 |
| こんにちは。 | 1210ppm前後 | こんにちは、今日もよろしくね。 | 7.0秒 |
温度とCO2は、送った値をそのまま答えた。
CO2の質問は、押す3秒前に送った1454ppmがそのまま返ってきている。
「こんにちは」には数値を出さず、「今日暑いね」には数値を言わずに「部屋の中もじっとり」と湿度の値を使ったような返し方をした(どの値を使ったかはログからは分からない)。
1回目だけ聞き取りに9.9秒かかり、ほかは3.9〜7.3秒だった。
サーバーの仕様書に、アイドル(待機状態)が長かった直後の1回目は遅れることがあると書かれており、その仕様に該当した可能性がある。
会話のあとは、長期記憶は1件もできなかった。
今回の4回の会話には「あなたは〜」の形にできる本人の話が入っていないので、抜き出すものがなかったのだと思う。
撫で判定の誤反応の調査
頭上タッチは、叩いても何もしないまま、撫でたときだけ喜ぶ顔を2.5秒出すようにした。
音もサーボも動かさない。
撫での判定は前回移した公式ファームと同じで、3つのパッドの強さから重み付きの位置(-100〜100)を出し、触れ始めから40を超えて動いたら撫でにする。
起動してから8分は、誰も触らない状態で接触0回だった。
ところが起動から約560秒で急に騒がしくなり、触っていないのに顔が喜び出した。
| 時間帯 | 接触 | 撫での判定 |
|---|---|---|
| 17時17分台 | 28回 | 10回 |
| 17時18分台 | 0回 | 0回 |
| 17時19分台 | 44回 | 12回 |
このときの接触は、強さ(0〜3)の最大がほぼ1で、3つのパッドにばらばらに一瞬ずつ出ていた。
前のパッドと後ろのパッドが交互に1になると、重み付きの位置が-100と100の間を跳ねて、そのまま撫でになる。
そこで、接触中にどれかのパッドが強さ2以上にならないと撫でにしない条件を足した。
公式ファームにはない条件で、書き込み直した直後の6分は接触184回に対して撫での判定0回だった。
それでも止まらなかった。
起動から時間がたつほど、誤反応の強さが上がっていった。
| 時間帯 | 強さ1 | 強さ2 | 強さ3 | 撫での判定 |
|---|---|---|---|---|
| 17時20分台 | 417回 | 5回 | 1回 | 0回 |
| 17時30分台 | 547回 | 76回 | 1回 | 19回 |
| 17時40分台 | 703回 | 366回 | 7回 | 92回 |
| 17時50分台 | 743回 | 711回 | 55回 | 182回 |
回数は、強さの読み取りが変わったときのログの行数で数えた。
強さ2の誤反応は 1 1 1→2 1 1→2 1 2 のように3つのパッドが同時に反応していて、指が1つのパッドに乗ったときの形ではなかった。
条件を強さ3に上げても、10分で約50回出て止まらなかった。
「端のパッドが強くて反対の端が0の状態から、反対の端だけの状態へ移る」という形を条件にする案も、ログに当てはめてみた。
誤判定はおよそ10分の1になったが、0にはならず、本物の撫でが取れるかも確かめていない。
見分けられないので、撫でで顔を変えるのはやめて、判定はログに残すだけにした。
シリアルから pet 1 を送れば、喜ぶ顔を出すように戻せるようにしてある。
CO2センサーを足した状態での結果だが、前回はSCD41なしでも同じ傾向があったので、センサーの影響ではなさそうだ。
返答の感情で首を動かす
頭上タッチを入力に使わないと、StackChan Bodyで動くのは首の2軸サーボだけになる。
サーバーは返答ごとに表情(emotion)を返しているので、1文目の再生を始めるときに、顔の切り替えと一緒に首も動かすことにした。
| 軸 | 範囲 | 原点 |
|---|---|---|
| 左右(yaw) | ±128度 | 正面 |
| 上下(pitch) | 0〜90度 | 一番下。原点より下は向けない |
StackChan-BSPの首の動きは、ばねのアニメーションで目標へ近づく。
専用のタスクでサーボを動かすので、指示を出したらすぐ戻ってきて、会話の処理は止まらない。
下は向けないので、sorrow は「うつむく」ではなく「ゆっくり横を向いて止まる」にした。
| emotion | 首の動き |
|---|---|
| smile | 小さく1回うなずく |
| joy | 上を向いて2回弾む |
| anger | 小刻みに左右へ振る |
| sorrow | ゆっくり横を向いて止まる |
| fun | 少し上を向いて左右にゆらゆら |
| (考え中) | フィラー(応答待ちのつなぎ言葉)の間、少し上を向いて首をかしげる |
会話が終わったら正面に戻し、録音を始める前は首が止まるのを待つ。
サーボの音は思ったより大きいが、再生音と重なる分は音量を上げて対処した。
最初は1つの動きが終わってから次の動きを出していて、うなずきがかなり遅かった。
速さの指定を上限の1000まで上げても、ばねの硬さに上限があり、目標に近づくほど遅くなって、止まったと判定されるまでに時間がかかった。
目標まで数度に近づいたら次の動作へ切り替えるようにすると、うなずき1回が0.2〜0.4秒になった。
ただ、折り返す距離を2度にすると、funの2往復目で2.6秒止まった。
首は目標の1〜2度手前で止まることがあり、左へ−25度を指示して−22.8度で止まったまま、着いた判定にならずに時間切れを待っていた。
距離を4度にすると時間切れはなくなり、joyは全体で約1.1秒、funは約1.6秒で終わった。
会話では、「お腹が空いて力が出ない。」がsorrowになり、「それはお腹すいてしんどいよね、早く何か食べて元気出してね。」の1文目と一緒に、0.9秒かけて横を向いた。
見ていた側の感想は「かなしそう」だった。
SCD41の値がSwitchBotと合わない
話しかけている途中で、SwitchBotの値とずれていた。
| SCD41 | SwitchBot | |
|---|---|---|
| CO2 | 1216ppm | 733ppm |
| 温度 | 26.6℃ | 27.8℃ |
| 湿度 | 84.9% | 65% |
SCD41をつなぐのはしばらくぶりだった。
SCD41の自動校正は、何日も続けて動かし、その間に外の空気(約400ppm)に触れることで基準を合わせる。
強制校正も、本来は外に出すなどして、濃度がはっきりした空気の中でかける。
今日つないだばかりで、どちらもやっていないので、CO2はずれていて当然だった。
温度は、SCD41に保存されている温度オフセット(センサー自身の発熱を差し引く量)で決まる。
読み出すと6.02℃で、出荷時の4℃より大きかった。
7月にCO2モニター本体のケースに入れて、SwitchBotに合わせたときの値が残っていた。
| 項目 | 7月(CO2モニターのケース) | 今回(StackChan) |
|---|---|---|
| 温度オフセット | 4.00→6.02℃ | 6.02→4.34→2.41℃ |
| 湿度 | 表示側で0.803を掛けていた | 表示側で0.880を掛ける |
| CO2の強制校正 | 1172ppmを基準に+313ppm | 763ppmを基準に−382ppm |
SCD41は測定を始め直すたびに、温度が31℃前後から下がっていき、下がりきるまで約10分かかった。
温度が安定したところで実温度を27.8℃としてオフセットを計算し直すと4.34℃になり、その後は28.0〜28.1℃で推移した。
湿度は温度オフセットを変えても79%のままで、SwitchBotの65%には届かなかった。
センサー本体に保存すると、CO2モニターに戻したときにまた合わなくなるので、校正値は音声チャットのファームの設定領域に保存し、起動時にセンサーへ設定するようにした。
測定開始から10分は、値が変動している温度と湿度を送らずに、CO2だけを送るようにした。
ところが、この設定で起動し直すと、同じオフセット4.34℃で温度は25.9℃で止まり、そこから25.3℃まで下がった。
19時22分のSwitchBotは27.3℃、67%、763ppmで、SCD41は25.4℃、85.4%、1147ppmだった。
外気は22℃で、SCD41は本体の横に出ているので、外から入る空気を先に受けているのかもしれない。
ただ、そのあと30分以上、差は1.9℃でほぼ変わらなかった。
18時台に4.34℃で合っていた理由は分かっていない。
CO2は、センサーが20分以上動いた状態で、SwitchBotの763ppmを基準に強制校正をかけた。
補正は−382ppmで、校正後は739ppmになった。
温度は、差の変動が止まっていたので実温度27.3℃で合わせ直し、オフセットは2.41℃になった。
10分待って27.4〜27.5℃で安定してから、湿度の生の値76.2%をSwitchBotの67%に合わせる係数0.880を入れた。
| SCD41(19時45分、校正後) | SwitchBot | |
|---|---|---|
| CO2 | 766ppm | 762ppm |
| 温度 | 27.5℃ | 27.3℃ |
| 湿度 | 67.0% | 67% |
これは屋内でSwitchBotの表示に数値を合わせた暫定の調整で、SCD41本来の校正手順(外気暴露による自動校正や既知濃度下での強制校正)ではない。
外気で強制校正をかけるか日数をかけて自動校正を効かせないと、CO2は再びずれるはずだ。
温度オフセットも同日中に4.34℃と2.41℃の開きが出ており、使用環境や筐体温度に合わせた追い込みがまだ要る。