ケモミミPJT:MiMi-Drive NNK製作記録
- 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プリンタでリンク構造を作成します。

リンクはサーボを見えづらくするため採用。
ちなみに4節リンクを使用するんですが、グラスホフの定理(グリューアーの公式)ってのを知りました。リンクを考えるとき参考になります。
少ない動きでぺたんとすることができます。
今回はミミの反り形状にこだわり新しいコマンド、加算ロフトをやっとマスターして作製しました。
ミミ部分はシール付きベロアを使用。黒、白、オレンジを重ねて作成しました。

ミミ表 
ミミ裏

ユニットは帽子で隠したいところ。 組み立て:
直接動作用にボタン2つと、サーボを2つ接続するための基板を作製。

こんな感じ、アンテナが別付けですが無くても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と同じ外観を新設
4. 動作の紹介
現状、MiMi Drive は以下の4種類の動きを実現しています。
bluetoothの操作番号とも一致しています。電源投入時、右、左、ぺターンとなります。
起動 :起動時に片耳ずつ倒し、両耳を倒します。
ピコン :20度耳を立て2秒キープ 信号1
ビビクーン :前にビビビ後ピーんとなる 信号2
ぺターン :前にぺターンと倒す 信号3
通常 :前にぴくぴく2回 操作しなくても15秒程度に1回動く
5. Bluetooth 操作の説明
操作手順: 各bluetoothシリアル通信アプリとbluetooth接続したのち、UTF-8形式で数字を送信すると動作します。
6. 今後の展望
改良点
耳の動きのバリエーション追加
2軸化により横の動き追加
リアル系ケモミミ
追加したい点
センサーを使用した自動反応機能
まとめ
MiMi Driveは、実際作ってみてそれっぽい動きをするので気に入っています。ひとまずベース作製は完了です。次の機会には、リアル系ケモミミ完成披露したいところです。
あとしっぽも作りたいレゲット向けに作っていたので、テイルズをモチーフ予定でした。MEGA SP(しっぽ)も今後作成予定。
このプロジェクトを見て、ちょっと使ってみたいのだけど~とか、自分にも作って~という方いらっしゃいましたら是非声かけ下さいませ。ケモキャラクターの耳を現実世界に再現し、Bluetooth で操作できるこのプロジェクトをぜひ試してみてください。
あと地味にケモミミカバー作ってくれる方募集中です。
感想コメントがやる気につながるので簡単に一言お願いしまーす。





コメント