方言を話すおしゃべり猫型ロボット『ミーア』をリリースしました(こちらをクリック)

【PlatformIO】頭を何回か連続で撫でると最初デレから段階的に嫌がるようにする。

この記事は約12分で読めます。

はじめに

現在、方言を話すおしゃべり猫型ロボット「ミーア」を開発中。

https://mia-cat.com

ミーアは頭に取り付けた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秒間タッチがなければカウントはリセットされる。タッチ回数に基づいて、短押しのハンドラが条件に応じて呼び出される。

C++
// 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 構造体を作成して kindphraseType を引数として渡せるようにすることで、デバイスのデフォルト設定(AWSのDevice ShadowのdeviceConfig で指定された設定)をオーバーライドし、外部から動的にフレーズの種類や種別を指定できるように変更。

C++
// 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の場合には、それぞれ異なるフレーズと表情が選択される。

C++
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関数で呼び出す。

C++
void loop(){
	handleShortPress();
}

動作確認

最初の3回は、通常の音声再生になるが、4回目:デレ→5回目:やや不機嫌→6回目:オコの音声となり、7回目タッチすると反応しなくなるようにできた。

コメント

タイトルとURLをコピーしました