top of page

ケモミミPJT:MiMi-Drive NNK製作記録

  • 執筆者の写真: HATAQ
    HATAQ
  • 3月17日
  • 読了時間: 7分

 ケモミミプロジェクト・MiMi-Drive NNK は、サクラ革命のキャラクター、美瑛ななこちゃんの耳型カチューシャをモチーフにした、Bluetooth 操作可能なケモミミカチューシャです。本ブログでは、その製作過程について紹介します。完成後メカMiMi-Driveもできましたので、動画はそちらになっています。


1. プロジェクトの背景と目標

  • インスピレーション: サクラ革命の美瑛ななこちゃんの公式漫画にあったぺたーん耳を再現したいと思ったのと、現状あるミミカチューシャの動きが、いまいちだったことが動機です。自分じゃ着けないのに何故造ったんだという突っ込みはなし。だって面白そうじゃん!なのです。

  • 目標: サーボ動作するミミを作成し、Bluetooth 経由でパターン動作ができること。


2. 使用する部品とツール

  • ハードウェア:

    • ESP32C3

      ESP32というマイコンを搭載したaruduino互換moduleで切手サイズでwifi BLEを搭載出力Pinがaruduinoより少ないですが、今回のプロジェクトには足りそうです。

    • サーボモーターSG90(のパクリ品)

      定番小型サーボ、本家のほうが静かにスムーズに安定して動きます。本番は本家TowerPro製を使いたい。

    • カチューシャ

      ダイ○ーの100円

    • 3Dプリンタフィラメント

    • 電子部品少々:スイッチ、ユニバーサル基板 etc

    • シール付きベロア生地

  • ソフトウェア:

    • Arduino IDE

    • FreeCAD

    • Bluetoothアプリ(LightBlue、Serial bluetooth Terminal)

  • 道具など:

    • パソコン

    • 半田ゴテ

    • 3Dプリンタ(XYZプリンティング、da Vinci Pro1.0)


3. 製作過程

  • 設計: 耳のデザインと3Dモデルの作成

    • 市販のミミは横方向に動くということで、目指すぺたーんができませんので、3Dプリンタでリンク構造を作成します。


      ree
    • リンクはサーボを見えづらくするため採用。

    • ちなみに4節リンクを使用するんですが、グラスホフの定理(グリューアーの公式)ってのを知りました。リンクを考えるとき参考になります。

    • 少ない動きでぺたんとすることができます。

    • 今回はミミの反り形状にこだわり新しいコマンド、加算ロフトをやっとマスターして作製しました。

    • ミミ部分はシール付きベロアを使用。黒、白、オレンジを重ねて作成しました。


      ミミ表
      ミミ表
      ミミ裏
      ミミ裏
    ユニットは帽子で隠したいところ。
    ユニットは帽子で隠したいところ。

  • 組み立て:

    • 直接動作用にボタン2つと、サーボを2つ接続するための基板を作製。


      こんな感じ、アンテナが別付けですが無くても2mくらいは大丈夫とのこと
      こんな感じ、アンテナが別付けですが無くても2mくらいは大丈夫とのこと
    • 貧乏人なので、ESP32C3は取り外しで別にも使えるようにしました。

    • 最初無線が使えなかったので、動作確認用ボタンを追加しています。

    • ESP32C3 用のスケッチはAIと対話を重ねてほぼ作っもらいました。

    • BLEの動作が、最後までできなくて、公式wikiのサンプルでやっと動いたので

    • あとは、動作部分のデータとAIに合わせてもらい、なんとか解決しました。

    • てか、ここまでできるAIすごくないですか。めちゃ助かります。

スケッチはこちら

 

#include <ESP32Servo.h>
#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEServer.h>
#include <string>

#define SERVICE_UUID        "4fafc201-1fb5-459e-8fcc-c5c9c331914b"
#define CHARACTERISTIC_UUID "beb5483e-36e1-4688-b7f5-ea07361b26a8"

// サーボオブジェクトを作成
Servo servo1;
Servo servo2;

// ボタンのピン設定
const int buttonA = D1;
const int buttonB = D2;

// 初期位置と角度の定義
const int initialPos = 90;
const int angleA = -10; // 反転角度
const int angleB = 15;  // 初期位置からの角度
const int angleIdle = 15;  // アイドル時の反転角度
const int angleVibrate = 5; // 振動の角度範囲
const int holdAngleA = -60; // ボタンAを2.1秒以上押したときの保持角度

// スピードの定義
const int speedA = 150; // パターン1のスピード
const int speedB = 150; // パターン2のスピード
const int speedVibrate = 150; // 振動用のスピード(増加)
const int speedIdle = 100; // アイドルパターンのスピード
const int returnSpeedA = 30; // パターン1の戻りスピード
const int returnSpeedB = 30; // パターン2の戻りスピード
const int returnSpeedLongPress = 100; // 長押し後の戻りスピード

// タイミング変数
unsigned long lastIdleMove = 0;
unsigned long buttonAPressTime = 0;

void pattern1(); // 関数の前方宣言
void pattern2();
void pattern3();
void pattern4();

// パターン実行のコールバッククラス
class PatternCallbacks : public BLECharacteristicCallbacks {
  void onWrite(BLECharacteristic *pCharacteristic) override {
     std::string value = std::string(pCharacteristic->getValue().c_str());

    if (value.length() > 0) {
      Serial.print("受信した値: ");
      Serial.println(value.c_str());
    }

    if (value == "1") {
      pattern1();
    } else if (value == "2") {
      pattern2();
    } else if (value == "3") {
      pattern3();
    } else if (value == "4") {
      pattern4();
    } else {
      Serial.println("無効なパターンが選択されました");
    }
  }
};

// サーバーのコールバッククラス
class ServerCallbacks : public BLEServerCallbacks {
  void onConnect(BLEServer* pServer) override {
    Serial.println("デバイスが接続されました");
  }

  void onDisconnect(BLEServer* pServer) override {
    Serial.println("デバイスが切断されました");
  }
};

void setup() {
  // サーボをデジタルピンに接続
  servo1.attach(D9); 
  servo2.attach(D10); 

  // ボタンピンを設定
  pinMode(buttonA, INPUT_PULLUP); // 内部プルアップ抵抗を使用
  pinMode(buttonB, INPUT_PULLUP); // 内部プルアップ抵抗を使用

  // サーボを初期位置に設定
  servo1.write(initialPos);
  servo2.write(initialPos);
  
  // 初期動作を実行
  initialServoMovement();

  // シリアル通信を初期化
  Serial.begin(115200);

  // Bluetoothを初期化
  BLEDevice::init("MiMi_Drive");
  BLEServer *pServer = BLEDevice::createServer();
  pServer->setCallbacks(new ServerCallbacks());

  BLEService *pService = pServer->createService(SERVICE_UUID);

  BLECharacteristic *pCharacteristic = pService->createCharacteristic(
                                         CHARACTERISTIC_UUID,
                                         BLECharacteristic::PROPERTY_READ |
                                         BLECharacteristic::PROPERTY_WRITE
                                       );

  pCharacteristic->setCallbacks(new PatternCallbacks());

  pCharacteristic->setValue("Please enter the command.");
  pService->start();

  BLEAdvertising *pAdvertising = pServer->getAdvertising();
  pAdvertising->addServiceUUID(SERVICE_UUID);
  pAdvertising->start();
}

void loop() {
  handleButtonA();
  handleButtonB();
  handleIdleMovement();
}

void handleButtonA() {
  if (digitalRead(buttonA) == LOW) { // ボタンはプルアップなのでLOWが押された状態
    // ボタン押下開始時間を記録
    if (buttonAPressTime == 0) {
      buttonAPressTime = millis();
    }

    if (millis() - buttonAPressTime > 2000) { // ボタンが2秒以上押された場合
      moveServo(initialPos + holdAngleA, initialPos + holdAngleA, speedA);
      return; // 位置をキープするため追加処理は不要
    } else { 
      moveServo(initialPos - angleA, initialPos - angleA, speedA);
      delay(2000); // 2秒保持
      moveServo(initialPos, initialPos, returnSpeedA);
      delay(2000); // 2秒保持
    }
    lastIdleMove = millis(); // アイドルタイマーリセット
  } else {
    if (buttonAPressTime > 0 && millis() - buttonAPressTime > 2000) { // 長押し後にボタンが離された場合
      moveServo(initialPos, initialPos, returnSpeedLongPress);
      delay(2000); // 2秒保持
    }
    buttonAPressTime = 0; // ボタン押下タイマーをリセット
  }
}

void handleButtonB() {
  if (digitalRead(buttonB) == LOW) { // ボタンはプルアップなのでLOWが押された状態
    pattern2(); 
    lastIdleMove = millis(); // アイドルタイマーリセット
  }
}

void handleIdleMovement() {
  if (millis() - lastIdleMove >= random(7000, 10000)) { // アイドル期間がランダム間隔を超えた場合、アイドル動作を実行
    pattern4();
    lastIdleMove = millis(); // アイドルタイマーリセット
  }
}

void moveServo(int targetAngle1, int targetAngle2, int speed) {
  int currentPos1 = servo1.read();
  int currentPos2 = servo2.read();
  int step1 = (targetAngle1 > currentPos1) ? 1 : -1;
  int step2 = (targetAngle2 > currentPos2) ? 1 : -1;

  while (currentPos1 != targetAngle1 || currentPos2 != targetAngle2) {
    if (currentPos1 != targetAngle1) currentPos1 += step1;
    if (currentPos2 != targetAngle2) currentPos2 += step2;
    servo1.write(currentPos1);
    servo2.write(currentPos2);
    delay(1000 / speed);
  }
}

void vibrateServo(int initialAngle, int range, int speed) {
  int angle1 = initialAngle + range;
  int angle2 = initialAngle - range;
  for (int i = 0; i < 3; i++) { // 3回振動
    moveServo(angle1, angle1, speed);
    delay(500 / speed);
    moveServo(angle2, angle2, speed);
    delay(500 / speed);
  }
}

void initialServoMovement() {
  moveServo(initialPos - 20, initialPos, 100);
  delay(1000); // 1秒保持
  moveServo(initialPos, initialPos, 100);
  delay(1000); // 1秒保持

  moveServo(initialPos, initialPos - 20, 100);
  delay(1000); // 1秒保持
  moveServo(initialPos, initialPos, 100);
  delay(1000); // 1秒保持

  moveServo(initialPos - 50, initialPos - 40, 100);
  delay(1000); // 1秒保持
  moveServo(initialPos, initialPos, 100);
  delay(1000); // 1秒保持
}

void pattern1() {
  moveServo(initialPos - angleA, initialPos - angleA, speedA);
  delay(2000); // 2秒保持
  moveServo(initialPos, initialPos, returnSpeedA);
  delay(2000); // 2秒保持
}

void pattern2() {
  moveServo(initialPos + angleA, initialPos + angleA, speedB);
  delay(500); // 0.5秒保持
  vibrateServo(initialPos + angleA, angleVibrate, speedVibrate); // 振動
  delay(500); // 0.5秒保持
  moveServo(initialPos + angleB, initialPos + angleB, speedB);
  delay(2000); // 2秒保持
  moveServo(initialPos, initialPos, returnSpeedB);
  delay(2000); // 2秒保持
}

void pattern3() {
  moveServo(initialPos + holdAngleA, initialPos + holdAngleA, speedA);
  // 位置をキープするため追加処理は不要
}

void pattern4() {
  for (int i = 0; i < 2; i++) { // 2回繰り返す
    moveServo(initialPos - angleIdle, initialPos - angleIdle, speedIdle);
    moveServo(initialPos, initialPos, speedIdle);
  }
}
  • テスト:

    • 動作状態は動画をご覧ください。メカMiMi Driveが完成。意図的ではないですが白黒にしたミミが意外といい感じ。

      機構部分はNNK Verと同じ外観を新設
      機構部分はNNK Verと同じ外観を新設

4. 動作の紹介

現状、MiMi Drive は以下の4種類の動きを実現しています。

bluetoothの操作番号とも一致しています。電源投入時、右、左、ぺターンとなります。

  1. 起動    :起動時に片耳ずつ倒し、両耳を倒します。


  2. ピコン   :20度耳を立て2秒キープ 信号1

  3. ビビクーン :前にビビビ後ピーんとなる 信号2

  4. ぺターン  :前にぺターンと倒す 信号3

  5. 通常    :前にぴくぴく2回 操作しなくても15秒程度に1回動く


5. Bluetooth 操作の説明

  • 操作手順: 各bluetoothシリアル通信アプリとbluetooth接続したのち、UTF-8形式で数字を送信すると動作します。


6. 今後の展望

  • 改良点

    • 耳の動きのバリエーション追加

    • 2軸化により横の動き追加

    • リアル系ケモミミ

  • 追加したい点

    • センサーを使用した自動反応機能

まとめ

 MiMi Driveは、実際作ってみてそれっぽい動きをするので気に入っています。ひとまずベース作製は完了です。次の機会には、リアル系ケモミミ完成披露したいところです。

 あとしっぽも作りたいレゲット向けに作っていたので、テイルズをモチーフ予定でした。MEGA SP(しっぽ)も今後作成予定。

 このプロジェクトを見て、ちょっと使ってみたいのだけど~とか、自分にも作って~という方いらっしゃいましたら是非声かけ下さいませ。ケモキャラクターの耳を現実世界に再現し、Bluetooth で操作できるこのプロジェクトをぜひ試してみてください。

 あと地味にケモミミカバー作ってくれる方募集中です。

感想コメントがやる気につながるので簡単に一言お願いしまーす。

コメント

5つ星のうち0と評価されています。
まだ評価がありません

評価を追加

@GFY_LMATRIX

​Amazonのアソシエイトとして、HQWorksは適格販売により収入を得ています。

©2023 HQWorks。Wix.com で作成されました。

bottom of page