StackChan Bodyの頭を叩いたら返事、撫でたら首振りを試した
目次
前回CoreS3をStackChan Bodyに載せたときに、背面に挿していたKeyユニット(U144)とCO2センサーを外した。
Keyユニットは音声チャットの話しかけボタンに使っていたメカニカルスイッチなので、代わりがいる。
StackChan Bodyの箱には「静電容量式タッチパッド」と書いてあったので、本体の頭を触ったら反応させられないか試してみた。
音声認識や返答生成のAPIまでつなぐのはちょっと重いので、今回はボディ単体で、軽く叩いたら「なんですか?」と返事をして、撫でたら「えへへ~」と言いながら首を左右に振るプログラムを作った。返事の音声を用意して、判定を書いて、叩く・撫でるの順に試した。
検証環境
| 項目 | 内容 |
|---|---|
| 本体 | M5Stack CoreS3 + StackChan Body(M5STACKCHAN) |
| 開発環境 | Windows 11 + arduino-cli 1.5.1 + esp32コア 3.3.10 |
| ボード指定(FQBN、どのボード向けにビルドするか) | esp32:esp32:m5stack_cores3 |
| ライブラリ | StackChan-BSP 1.1.0(コミット8d4d6fc)、M5Unified 0.2.17 |
| 音声 | 自宅の音声サーバーのIrodori-TTSで事前に作ったWAV 2本 |
StackChan Body用のM5Stack公式ライブラリ StackChan-BSP(BSPはボードの機能をまとめて扱うためのサポートパッケージ)は、arduino-cliのライブラリ一覧には見つからなかったので、GitHubから取ってきてライブラリフォルダーに置いた。NFCと赤外線は今回使わないが、BSPが依存しているのでM5Unit-NFCとIRremoteESP8266も入れた。このときM5Utilityが0.1.0から0.3.0に上がった。
StackChan Bodyのタッチパッド
公式ドキュメントによると、ボディの上面に3ゾーンのタッチパッドがある。ゾーンの呼び名と接続ピン、取れる値はBSPのソースで確認した。
| 項目 | 内容 |
|---|---|
| 位置 | ボディ上面。前・中・後の3ゾーン |
| チップ | Si12T(I2Cアドレス0x68) |
| 接続 | CoreS3の内部I2C(本体の中の部品同士をつなぐ通信線。SCL=G11、SDA=G12)。Groveポートは使わない |
| 取れる値 | ゾーンごとの強さ0〜3(0が触れていない) |
BSPのタッチのクラス(TouchSensor_Class)はM5Unifiedのボタンと同じ扱いになっていて、どれか1ゾーンでも触れている間は「押されている」、全ゾーンが0になると「離した」になる。
初期の感度はLow / Level 3で、BSPのコメントによると元のStackChanファームウェアに合わせてある。
前→中→後(または後→中→前)の順に3ゾーンすべてに触れて、前→中と中→後の触れ始めの差がどちらも30msより長く400msより短いと、スワイプ(撫でる動き)と判定する。叩いたか撫でたかは、このスワイプ判定が出たかどうかで分けることにした。
返事の音声を2本作った
声は、音声チャットで返事を待つ間に鳴らしているフィラー音声と同じIrodori-TTSで作った。音声サーバーのTTSにPCから直接テキストを送り、返ってきたWAVをそのまま使った。
「なんですか?」は、シード(乱数の種)を変えて3本作った1本目を使った。
「えへへ~」も同じようにシード1〜3で作ったが、3本ともいたずらっぽい言い方になった。このTTSのAPIで指定できるのはテキスト、シード、長さの倍率、生成ステップ数だけで、感情を直接指定する項目はない。なので表記を変えて、シード2種類ずつで取り直した。
| テキスト | 長さの倍率 | 使ったか |
|---|---|---|
| えへへ~(最初の3本) | 1.0 | 使わなかった(いたずらっぽい) |
| えへへ~♪ | 1.0 | 使わなかった |
| えへへ、うれしいな~ | 1.0 | 使わなかった(約2.7秒と長い) |
| わぁ、えへへ~ | 1.0 | 使わなかった |
| えへへぇ~ | 1.2(ゆっくり) | 使わなかった |
| えへへ~😊 | 1.0 | シード1を使った |
叩く・撫でるを判定するテストプログラム
触れた瞬間に反応させると、撫でたときも最初のゾーンに触れた時点で「なんですか?」が鳴ってしまうので、叩いたか撫でたかは指を離した時点で決めることにした。
| 触り方 | 判定 | 反応 |
|---|---|---|
| スワイプ判定が出た | 撫でた | 「えへへ~」+ 首を左右に振る + LEDがピンク |
| スワイプなしで、0.8秒以内に離した | 叩いた | 「なんですか?」+ LEDが青 |
| スワイプなしで、0.8秒より長く触れていた | 手を置いただけ | 何もしない |
首振りは横方向(Yaw)のサーボ(角度を指定して動かすモーター)だけを使い、BSPの Motion.moveX() で角度と速度(0〜1000)を指定した。最初は±20°を2往復して中央に戻す動きで、速度は600にした。首を振っている間と止まってから0.3秒の間に始まったタッチは、反応させずにログにだけ残すようにした。
音声2本(48kHz / 16bit / モノラル、「なんですか?」1.28秒と「えへへ~😊」1.36秒)はSDカードを使わずに、WAVをC言語の配列にしてプログラムへ埋め込んだ。2本で約250KBになり、ビルドしたプログラムは1,120,375バイトで、プログラム用の領域(3,145,728バイト)の35%だった。
タッチ1回ごとに、3ゾーンそれぞれの触れ始めた時刻、ゾーン間の間隔、強さの最大値、判定結果をシリアルに出すようにした。強さの値が変わるたびに出すモードも作った。
EP n=17 dur=550 zones=3 first=[0,121,253] gapFM=121 gapMB=132 max=[3,3,3] swipe=F class=stroke
dur が触れていた時間、first が最初に触れたゾーンから前・中・後それぞれに触れるまでの時間(ms)で、gapFM と gapMB が前→中・中→後の間隔、max が強さの最大値になる。
叩いてみる
頭の上を5回叩いた。音量は最初0〜255の160にしていたが大きすぎたので、音声チャットと同じ0〜10の段階の2(内部の値は51)に下げている。
| 回 | 触れていた時間 | 反応したゾーン | 判定 |
|---|---|---|---|
| 1 | 77ms | 前 | 叩いた |
| 2 | 77ms | 前・中 | 叩いた |
| 3 | 99ms | 前・中 | 叩いた |
| 4 | 121ms | 前・中 | 叩いた |
| 5 | 121ms | 前・中 | 叩いた |
4回は前と中の2ゾーンが同時に反応していた。
聞いていると、1つ前の「なんですか?」を言い終わった直後に叩いたときに、しゃべらないことがあった。ぽんぽん続けて叩く分には反応していた。
再生だけを繰り返して確かめる
ログに残った叩きは、どれも再生命令まで出ていた。2回目の叩きは、1回目の再生が終わった約0.1秒後だった。
スピーカー側が再生終了の直後に次の再生命令を受け付けていない可能性があるので、触らずに再生だけを繰り返すコマンドを足した。前の再生が終わってから100ms後に次を再生する、を5回流した。
ログでは5回とも再生状態に入って1.24〜1.26秒鳴っていて、耳で聞いても全部聞こえた。スピーカー側ではなかった。
タッチとして拾えていたか
表の5回はどれも記録されていた。ログを見直すと、それ以外に叩いて鳴らなかったものは、再生どころかタッチとしてもログに残っていなかった。叩いたこと自体を検出できていなかった。
頭の上の顔寄りには、組み立てのときに内部ケーブルを挿したコネクタの場所があって、その先はCoreS3本体になる。強さの値を毎回出すモードにして、コネクタのあたりから背中側まで場所を変えながら叩いた。
| 叩いた場所 | 反応したゾーン | 強さの最大値 |
|---|---|---|
| コネクタを挿すあたり | なし | 0 |
| 顔(画面)のすぐ後ろ | 前 | 2 |
| 頭の真ん中 | 前・中 | 3、3 |
| 背中側の端 | 後 | 3 |
BSPの「前」は顔側だった。外から見てどこからが前のパッドなのか分からず、適当に叩いていた。
鳴らなかったのはパッドの外を叩いていたからで、機器の不良でもプログラムの問題でもなかった。
撫でてみる
顔側から背中側へ、ふつうの速さで撫でた。
| 触れていた時間 | 前→中 | 中→後 | 強さの最大値(前,中,後) | 判定 |
|---|---|---|---|---|
| 550ms | 121ms | 132ms | 3,3,3 | 撫でた |
前→後のスワイプと判定されて画面が「撫でた」に変わり、「えへへ~」と言って首を左右に振った。
首振りのあと反応しない
撫でて首を振らせたあと、叩いても撫でても反応しなくなった。
ログを確認すると、首振りを始めて約1.5秒後に、誰も触っていないのに強さの値が3ゾーンとも3(最大)に上がり、そのまま2分近く0に戻っていなかった。プログラムは全ゾーンが0になったところでタッチ1回を区切るので、区切れないまま次のタッチを判定できなくなっていた。
静電容量式のタッチは、触れていないときの値を基準にして、そこからの変化で触れたかどうかを決めている。シリアルからBSPの recalibrate()(この基準値の取り直し)を呼ぶと、直後に0へ戻った。BSPのコメントでは、電源の切り替えなど外からの影響で基準値がずれたときに使う関数とされている。首振りのどこが影響しているのかまでは不明。
この時点の首振りは速度600で6.3秒かかっていて、1.4秒で終わる「えへへ~」のあともまだ振っていたので、首振りの長さと一緒に直した。
| 項目 | 変更前 | 変更後 |
|---|---|---|
| 首振り | 2往復 | 1往復(右→左→中央) |
| 速度 | 600 | 900 |
| 首振り後 | なし | 止まって0.3秒後に recalibrate() を呼ぶ |
| 触れっぱなし | なし | 10秒続いたら recalibrate() を呼ぶ |
首振りは2.5〜2.8秒になり、声と同時に振り始めて、声が終わって1秒ちょっとで止まるようになった。
首振りを1往復にしたあとの確認
叩くのと撫でるのを交互に詰めて繰り返した。実際に使うときはこんなに続けて叩いたり撫でたりしないが、動画ではそうしている。
| 回 | 判定 | 触れていた時間 | 前→中 | 中→後 | 直前 |
|---|---|---|---|---|---|
| 1 | 撫でた | 616ms | 132ms | 143ms | 待機中 |
| 2 | 叩いた | 88ms | - | - | 首振り停止から4.4秒 |
| 3 | 撫でた | 297ms | 77ms | 88ms | 叩いた声の1.5秒後 |
| 4 | 叩いた | 77ms | - | - | 首振り停止から1.5秒 |
| 5 | 叩いた | 110ms | - | - | 待機中 |
| 6 | 撫でた | 208ms | 66ms | 44ms | 叩いた声の再生中 |
| 7 | 叩いた | 88ms | - | - | 首振り停止から2.8秒 |
叩いた声の再生中に撫でた6回目も、撫でたと判定された。
一方、同じ動画の中で首を振っている最中に叩いたものは、1回も反応しなかった。首振り中のタッチはプログラムで反応させないようにしてあるので、反応しないこと自体は仕様どおり。ただ、そのときの強さの値を確かめると、叩いても0のまま動いていなかった。
首振り中のタッチ
首振り中はLEDもピンクに光っているので、LEDのせいかどうかを調べた。首振りをせずに、LEDだけを同じピンクで点けっぱなしにして叩いたり撫でたりすると、11回とも判定どおりに反応し、強さも3まで出ていたので、LEDは関係なさそうだ。
首振り中の強さの値は、試すたびに違っていた。
| 試した回 | 首振り中の強さの値 |
|---|---|
| 首振りを直す前 | 誰も触っていないのに3ゾーンとも3のまま下がらない |
| 首振りを1往復にしたあとの確認 | 叩いても0のまま変化しない |
| LEDを点けっぱなしにした計測 | 78msの反応のあと一部のゾーンが2秒下がらず、停止後の recalibrate() で0に戻った |
動いている最中に頭を叩くことは実際にはないし、動いていると叩く位置もずれるので、首を振っている間は触らないことにした。プログラムは、首振り中のタッチを反応させず、止まったら recalibrate() を呼ぶ形のままにしている。
撫でる速さの範囲
叩く位置が分からなかったのと同じで、撫でるときも途中から撫で始めたり最後に減速したりしそうなので、撫でる速さを変えてどこから判定されなくなるかを試した。
ゆっくり撫でる
ふつうの速さから1回ずつ遅くして5回撫でた。
| 回 | 触れていた時間 | 前→中 | 中→後 | 強さの最大値(前,中,後) | 判定 |
|---|---|---|---|---|---|
| 1 | 374ms | 99ms | 99ms | 3,3,3 | 撫でた |
| 2 | 814ms | 198ms | 231ms | 3,3,3 | 撫でた |
| 3 | 1,419ms | 407ms | 418ms | 3,3,3 | 手を置いただけ(反応なし) |
| 4 | 2,057ms | 484ms | 638ms | 3,3,3 | 手を置いただけ(反応なし) |
| 5 | 2,035ms | 528ms | 517ms | 3,3,3 | 手を置いただけ(反応なし) |
ゾーン間が231msの回は撫でたと判定され、407msの回は判定されなかった。頭の端から端までを0.8秒くらいで撫で切った回までは反応した。
遅すぎた3回は、触れていた時間が叩いた扱いの上限の0.8秒を超えているので、叩いたと誤判定されることもなく何も起きなかった。
1回目のあとの首振り中には、また3ゾーンとも3のまま下がらなくなった。止まったあとの recalibrate() で0に戻り、2回目は普通に判定された。
速く撫でる
今度は1回ずつ速くして5回撫でた。
| 回 | 触れていた時間 | 前→中 | 中→後 | 強さの最大値(前,中,後) | 判定 |
|---|---|---|---|---|---|
| 1 | 253ms | 77ms | 66ms | 3,3,3 | 撫でた |
| 2 | 220ms | 77ms | 66ms | 3,3,1 | 撫でた |
| 3 | 1,996ms | 0ms | 0ms | 3,3,3 | 首振り中のため反応させず |
| 4 | 154ms | 66ms | 後は反応なし | 3,2,0 | 叩いた(なんですか?) |
| 5 | 記録なし | - | - | - | - |
速くするほど後ろのゾーンの強さが下がり(2回目で1、4回目で0)、最後のゾーンが反応しなくなった。
4回目は前と中の2ゾーンしか反応しなくて、0.15秒で離れたので叩いたと判定され、「なんですか?」が返ってきた。一番速い5回目は、強さの値も0のままで何も記録されていない。
BSPで読み出した応答サイクルの設定値は2で、チップが一定時間続いた接触にだけ反応しているなら、指が一瞬で通過したゾーンは反応しなかったのかもしれない。
3回目は2回目のあとの首振り中に始まっていて、止まったあとの recalibrate() まで3ゾーンとも3のままだった。首振り中に撫でたのか、3のまま下がらなくなっただけなのかは、ログではどちらとも言えない。
もう2回撫でた結果と、叩きとの比較
止まった直後の再キャリブレーションと重ならないよう、首振りが止まってから間をあけて、もう2回撫でた。
| 回 | 触れていた時間 | 前→中 | 中→後 | 強さの最大値(前,中,後) | 判定 |
|---|---|---|---|---|---|
| 1 | 198ms | 88ms | 33ms | 3,3,2 | 撫でた |
| 2 | 297ms | 前は反応なし | 33ms | 0,3,3 | 叩いた(なんですか?) |
2回目は前ゾーンが最後まで0のままで、中と後の2ゾーンだけが反応した。撫で始めが前ゾーンより後ろだったようだ。
撫で損ねて叩いた扱いになったものを叩きと数字で見分けられないか、強さの値を毎回記録していた計測(叩く位置を調べたとき以降)の、首振り中を除く全部のタッチで比べた。数字は、反応したゾーンの間で触れ始めと離れた時刻がそれぞれ最大でどれだけずれたか。
| 種類 | 件数 | 触れ始めの差 | 離れた時刻の差 |
|---|---|---|---|
| 叩いた | 15 | 0〜44ms | 0〜44ms |
| 撫で損ねて叩いた扱い | 2 | 33ms、66ms | 44ms、55ms |
| 撫でたと判定 | 12 | 78〜429ms | 55〜407ms |
叩いたものの中に、LEDを点けっぱなしにした計測で前→中の順に44msずれて触れた回が1回あり、撫で損ねの33msと66msの間に入っている。2ゾーンしか反応しなかった速い撫でと叩きは、この数字では分けられなかったので、判定はBSPのままにした。
実際に使ってみて誤判定で困るようなら、そのとき調整する。