ロボットダンスの効果音デバイス

ロボットダンス見るのがなぜか好きです。
なんで好きなんだろうなーって考察してみたところ、どうやら動きに合わせて稼働音が流れてるところが好きなんじゃなかろうか?という結論に至りました。

私の暮らしだとテレビでロボットダンスを見るときはほぼバラエティ番組なので大抵モータみたいなSEが流れてます。そっか、音が楽しかったのか・・

ということは自分の動きに合わせてモータ音が流れると楽しく暮らせそうです。

作るもの

手とか足とかの動く部分にセンサをつけ、動作している間効果音が流れる仕掛けを作ります。

スピーカーでモータ音を流すのもいいですが、折角なので実際にモータを回してリアルな音をあてましょう(その方が楽だからという理由も・・)

加速度センサ

動作をキャプチャしたいのでバッテリ付きで小型な M5StickCを使って加速度をモニタします。

M5StickCにはMPU6886という6軸IMUが内蔵されています。

M5StickCのIMUで加速度を取得するには M5.Imu.Init()の後M5.Imu.getAccelData()をコールするだけです。

void IMU::getAccelData(float *ax, float *ay, float *az)

XYZ軸いずれかの加速度が変化したら動作中という簡易的な判定でいけるか実験してみます。
シリアルプロッタに加速度の増分(絶対値)と増分が閾値を超えた場合をプロットしました。

gist.github.com

(オレンジ色のグラフがHighになっている区間がどれかの軸が閾値を超えた区間となります)

ある程度判定はできていますが、細かい変化を拾ってしまっています。また、動画では分かりませんが等速移動すると変動がなくなるので動作中と判定されません。

細かい変化はフィルタをかけて消してしまえば良さそうです。等速移動に関しては難しいのでタイマで一定期間は動作中と判断し続ける処理を入れて誤魔化しちゃいましょう。

モータ

M5StackのServo Kit 360°を使います。

www.switch-science.com

仕様を見ると PWMが50Hz/0.5~2.5msとあります。
duty 1.5msで停止し、0.5msと2.5msで正転・逆転の最高速になるわけです。

ESP32のPWMはLEDCというペリフェラルを使うみたいです。ledcSetupledAttachPinでセットアップし、ledcWriteでDutyを変更します。

ledcSetup(1, 50, 16);     // Channel 1を50Hz 分解能16bitで設定
ledcAttachPin(G26, 1);    // Channel 1にGPIO26をアタッチ

ledcWrite(1, 0.5f / 20.f * 65535);  // duty 0.5msで設定

動作状態の通知

動作状態の通信はESP-NOWを使います。
M5StickCはBroadcastで動作状態を送信して、M5ATOMで全てのデータを受信します。

hollyhockberry.hatenablog.com

各M5StickCが検知する動作状態はそれぞれ非同期に変化するため、どれかのセンサが動作中を検出している間はモータを動かし続けます。

M5StickCからはESP-NOWで「動作中」もしくは「停止中」を送信してくるので、受信コールバック関数内で現在動作中のM5StickCをstd::vectorで更新します。

コールバック関数内で色々やるとおかしくなるので、loop()でstd::vectorの数に応じてモータの動作・停止を切り替えます。

// 動作中のノード
std::vector<uint32_t> nodes;

// MACアドレス下位4桁をIDに変換する
uint32_t Addr2Id(const uint8_t* mac_addr) {
  union {
    uint32_t id;
    uint8_t bytes[4];
  } addr;
  for (auto i = 0; i < 4; ++i) addr.bytes[i] = mac_addr[2 + i];
  return addr.id;
}

void recvCallback(const uint8_t* mac_addr, const uint8_t *data, int data_len) {
  if (data_len <= 0) return;

  const auto id = Addr2Id(mac_addr);
  const auto move = data[0] != 0;
  const auto contain = std::find(nodes.begin(), nodes.end(), id) != nodes.end();

  // 動作中ノードのリストを更新する.
  if (move) {
    if (!contain) {
      nodes.push_back(id);
    }
  } else {
    if (contain) {
      for (auto it = nodes.begin(); it != nodes.end();) {
        if (*it == id) {
          it = nodes.erase(it);
        } else {
          ++it;
        }
      }
    }
  }
}

void loop() {
  // 動作中のノードが一つでもあればモータを回したい.
  const bool moving = nodes.size() > 0;

  if (_moving != moving) {
    _moving = moving;
    const uint32_t duty = _moving
      ? 2.0 / 20.0 * 65535
      : 1.5 / 20.0 * 65535;
    ::ledcWrite(1, duty);
    show_led(_moving ? 0xff00 : 0);
  }
}

完成!

動作状態の判定がざっくりなので精度が良くないですが、遊んでるとなかなか楽しいです。

私がやると動きがブレてるのでちょうどいい感じに検出されますが、上手な人がやったら等速移動の期間が増えて停止と判断されちゃうんでしょうか?気になります

ソースコード

加速度センサ側(M5StickC)

github.com

効果音ユニット側(M5ATOM)

github.com