はじめに
現在、方言を話すおしゃべり猫型ロボット「ミーア」を開発中。
ミーアは頭に取り付けたTTP223のタッチセンサーを介して、頭を撫でると音声を再生できる。
この仕組みを利用して、何回か連続で頭を撫でると、最初は嬉しがるが、徐々に警戒し始め、撫ですぎると、嫌がってそっぽを向くという機能を実現したい。
タッチセンサーとESP32の配線に関する記事はこちら
要件定義
下記で要件定義した。
音声再生中 or 音声再生後 7 秒未満で再度頭を触る行為を下記回数で連続で行ったら、回数に応じて固定の音声を流す。こちらは、アプリやサーバーとは連携せずに、デバイス側のみで完結の機能
4回目のタッチでのフレーズ (まだ嬉しい)
eye_expression:happy
「なでてくれて、気持ちいいニャ〜」
「こんなにかまってくれて、嬉しいにゃ〜」
「君の手はあったかいにゃ~」
「もっとナデナデして!幸せだニャ〜。」
「また触ってくれた!気持ちいいにゃん」
5回目のタッチでのフレーズ (少し警戒し始める)
eye_expression:disgust
「5回目かにゃ?ちょっと休憩しようかニャァ。」
「ねえ、ちょっとくらいは僕のプライバシー、尊重してくれるかニャ?」
「そんなに触ると照れちゃうニャ。」
「もうちょっと自分の時間がほしいニャ。」
「そろそろいい加減にしてほしいにゃ…。」
6回目のタッチでのフレーズ (明らかに嫌がる)
eye_expression:anger
「触り過ぎニャ!もう少し距離を置きたいニャ。」
「仕事してるからもう触らないでニャン!」
「さよならニャン!」
「しゃべり疲れたから、スリープモード入るニャン!」
「しゃべりすぎて口疲れたニャァー」
7回目以降:sleep
タッチ回数と音声再生ロジック
・タッチのカウント: ESP32とTTP223タッチセンサーを使用して、タッチの回数をカウント
・7秒以内のタッチ: タッチが7秒以内に連続して行われた場合、カウントを増やす
・7秒以上の間隔でのタッチ: タッチが7秒以上開いた場合、カウントをリセットする
・7回目の特別な条件: 7回目のタッチ後は、10秒間タッチがなければカウントをリセットする。これは7回タッチされた後のみ適用される条件
・音声と表情の再生: タッチ回数に応じて、事前にインストールされた固定の音声ファイルを再生し、目の表情も言葉に応じて変化させる
・音声は声優さんに依頼して新規録音
DB:phraseテーブルにtypeとkindを追加
音声や目の表情のデータを格納しているclocky.dbというデータベースのphraseテーブルに下記を追加
- phrase_type: “default” – システムのデフォルトフレーズや機能に関するフレーズを格納。これは、standard.mp3と同じく、アプリで性格や方言を切り替えるに関わらず、esp32内に残り続けるフレーズ群。現状、タッチ回数による音声やスリープモード前後のフレーズなどを想定。
- kind:フレーズのタイプを指定。今回はタッチ回数に対応するフレーズ群ということで、touch_response_4, touch_response_5, touch_response_6 を作成し、対応するvoice_pathも指定
- voice_path:実際に再生する音声ファイル。例えば、同じkind(touch_response_4)に複数のvoice_pathがあるが、これは、タッチ回数が連続4回の時に候補のvoice_pathからどれか1つをランダムに取得して再生することで、ユーザーに飽きさせないようにするため
ButtonManager.cpp:タッチ回数のロジックを作成
タッチに関する処理は、ButtonManager.cppファイルにまとめているので、既存のhandleButtonPress関数(タッチした時の処理)に、タッチ回数に関する処理を追加する。
タッチが7秒以内に連続するとカウントが増え、7秒以上の間隔があるとカウントがリセットされる。7回タッチされた後、10秒間タッチがなければカウントはリセットされる。タッチ回数に基づいて、短押しのハンドラが条件に応じて呼び出される。
// ButtonManager.cpp
ButtonManager::ButtonManager(int pin, int longPressTime) {
this->pin = pin;
this->longPressTime = longPressTime;
this->lastButtonState = HIGH;
this->isLongPressTriggered = false;
this->touchCount= 1;
this->lastTouchTime = millis();
pinMode(pin, INPUT);
}
void ButtonManager::handleButtonPress() {
int currentButtonState = digitalRead(pin);
unsigned long currentTime = millis();
if (isButtonPressed(currentButtonState)) {
buttonPressedTime = millis();
isLongPressTriggered = false;
} else if (isButtonHeldDown(currentButtonState)) {
if (!isLongPressTriggered && isLongPressed() && longPressHandler) {
longPressHandler();
isLongPressTriggered = true;
}
} else if (isButtonReleased(currentButtonState)) {
if (!isLongPressTriggered && shortPressHandler) {
if (currentTime - lastTouchTime <= 7000) {
touchCount++;
} else {
touchCount = 0;
}
// タッチ回数が7に達した場合、10秒間タッチがなければカウントをリセット
if (touchCount >= 7 && currentTime - lastTouchTime > 10000) {
touchCount = 0;
}
lastTouchTime = currentTime; // タッチ時間を更新
if (touchCount < 7) {
shortPressHandler(); // タッチ回数が6未満の場合、短押しハンドラを呼び出す
}
}
}
lastButtonState = currentButtonState;
}
int ButtonManager::getTouchCount() const {
return touchCount;
}
作成したフレーズタイプと種類をdeviceConfigにオーバーライド
ExpressionOptions
構造体を作成して kind
と phraseType
を引数として渡せるようにすることで、デバイスのデフォルト設定(AWSのDevice ShadowのdeviceConfig
で指定された設定)をオーバーライドし、外部から動的にフレーズの種類や種別を指定できるように変更。
// main.cpp
struct ExpressionOptions {
std::optional<String> kind = std::nullopt;
std::optional<String> phraseType = std::nullopt;
bool fastMode = false;
};
tl::expected<QueryResult, String> getExpressionQueryResult(sqlite3 *db, ExpressionOptions options) {
String phraseTypeToUse; // 使用するフレーズタイプ
// ExpressionOptionsからphraseTypeが指定されているかチェック
if (options.phraseType) {
phraseTypeToUse = options.phraseType.value();
} else {
// 指定されていなければconfigから取得
auto config = syncDeviceShadow->getReportedConfig();
if (!config->phrase_type)
return tl::make_unexpected("phrase_type is not set");
phraseTypeToUse = config->phrase_type.value();
}
QueryResult result;
if (options.kind) { // kindが指定されている場合は、kindに対応するフレーズからランダムに選択
auto queryResult = selectKindPhrase(db, phraseTypeToUse, options.kind.value());
if (!queryResult) {
return tl::make_unexpected("failed to select kind phrase: " + queryResult.error());
}
result = queryResult.value();
} else if (options.fastMode) {
auto queryResult = selectRandomPhrase(db, phraseTypeToUse);
if (!queryResult) {
return tl::make_unexpected("failed to select random phrase: " + queryResult.error());
}
result = queryResult.value();
} else {
auto queryResult = selectPhraseByPriority(db, phraseTypeToUse);
if (!queryResult) {
return tl::make_unexpected("failed to select priority phrase: " + queryResult.error());
}
result = queryResult.value();
}
// voiceとexpressionが取得できなかった場合はエラー
if (result.voice_path.isEmpty() || result.expression_id == 0) {
return tl::make_unexpected("null check failed for phrase result");
}
// expressionに対応する画像を取得
auto exprResult = getEyeDisplay(db, result.expression_id);
if (!exprResult) {
return exprResult;
}
result.image_left_url = exprResult->image_left_url;
result.image_right_url = exprResult->image_right_url;
result.is_animate = exprResult->is_animate;
return result;
}
handleShortPress関数で、デバイス上のボタンが短押しされた際の動作を定義
ButtonManagerからタッチ回数を取得し、タッチ回数が7未満の場合に、タッチ回数に応じたフレーズと表情を選択して再生する。タッチ回数が4、5、6の場合には、それぞれ異なるフレーズと表情が選択される。
void handleShortPress() {
// OOMになるため、BLEManager起動中はボタンを無効化
if (bleManager->isStarted()) {
Serial.println("BLEManager is started. Button is disabled.");
return;
}
// ButtonManagerからタッチ回数を取得
int touchCount = buttonManager.getTouchCount();
// タッチカウントをシリアルモニタに出力
Serial.print("Touch Count: ");
Serial.println(touchCount);
if (touchCount >= 7) {
return;
}
// タッチ回数に応じたフレーズと表情の選択
ExpressionOptions options;
if (touchCount == 4) {
options.phraseType = "default";
options.kind = "touch_response_4";
} else if (touchCount == 5) {
options.phraseType = "default";
options.kind = "touch_response_5";
} else if (touchCount == 6) {
options.phraseType = "default";
options.kind = "touch_response_6";
} else {
options.kind = std::nullopt;
}
// 選択されたフレーズと表情の再生
auto result = startExpression(options);
if (!result) {
Serial.println("handleShortPress failed: " + result.error());
}
}
最後に、handleShortPress関数を、main.cppのloop関数で呼び出す。
void loop(){
handleShortPress();
}
動作確認
最初の3回は、通常の音声再生になるが、4回目:デレ→5回目:やや不機嫌→6回目:オコの音声となり、7回目タッチすると反応しなくなるようにできた。
コメント