呼ぶと応えるハロをつくる


今回はお手軽に。主にウチの子どもを喜ばせる目的で、『呼ぶと応えるハロ』を作ってみました。

開発経緯

実は私のデスクには数年前からハロが鎮座しておりまして。

『Figure-rise Mechanics ハロ』というプラモデルです。4年ぐらい前に発売されていたものですが、あるとき「これ、ゆくゆく工作に使えないかなあ」と思って購入して、とりあえず組み立てて机に飾っていたのでした。

そんなわけで、ウチの長男は乳児レベルの頃からこのハロのことを認識はしていたのですが、最近一緒に観始めた『水星の魔女』にもハロが出ているのを見たのと、あと最近「パパはぎじゅつしゃのゆーちゅーばー」ということを認識し始めたのがあって、「このハロの目を光らせて」というリクエストを受けました。

ただ、実はこのハロ、元々目を光らせられるようになっておりまして。

外装を外して。

口?を開けると。

中から『動力ユニット』という部品を取り出せるようになっています。この動力ユニットの代わりに、別売りのLEDユニットという部品を入れてあげると、ハロの目を発光状態にすることができます。なので、単に目を光らせるだけならわざわざ私が出るまでもない(?)と思ったのですが、

なんとこの口(?)、M5StickC Plusがピッタリ収まります

M5StickC Plusは、このサイトを見ているような人には説明不要かもしれませんが、この小さな筐体にESP32マイコン(WiFi&Bluetooth使用可)、ボタン、LED、赤外線LED、ディスプレイ、バッテリー、加速度センサ、マイク、ブザー等々を盛り込んだ凄いプロトタイピングツールです。これが中に仕込めるとなれば、単にLEDを光らせる以上のことは造作もなくできそうです。

が、いきなり機能盛り盛りのものを作り始めてしまうとあっという間に数ヶ月が過ぎてしまうので、まずは長男のリクエストに速やかに応えるべく、『呼べば何か反応してくれるハロ』を最初のゴールとして開発することにしました。

特徴

冒頭動画にあるように、とてもシンプル。呼びかけると、目を光らせながら、「ピピッ」みたいな声を発しながら震えます。M5StickC Plusがバッテリーを内蔵してくれているおかげで、どこでも遊ぶことができます。

「ハロ、ゲンキ」みたいなハロっぽい音声を喋れればなお良いと思いますが、それをやろうとすると遊びやすさが犠牲になる可能性が高いので、今回は要件から落としました。これについての対応は追々。

ハードウェア開発

ベースは「開発経緯」で述べたとおり、プラモデルの『Figure-rise Mechanics ハロ』です。

発売日は確か4年前のはずですが、今も再販されているかは謎です。初期出荷分がどこかの玩具屋さんに眠っていたりはするかもしれません。

それから、何はともあれM5StickC Plusです。特徴は「開発経緯」で述べたとおりです。

次の「ソフトウェア解説」でも触れますが、今回はM5StickC Plusに元々搭載されている機能のうち、「マイク」と「ブザー」を使用しています。

M5StickC Plus単体ではできない「振動」機能の実現には、以下を使用させて頂きました。

このように、綺麗にHatとして作られているのですが、素直にM5StickC Plusに装着してしまうと流石にハロの中に入らなくなってしまうので、

こんな感じでバラして中の部品を使用させて頂きました。

奥まってしまってちょっと見づらいですが、中のスペースの上段に、3Dプリンタで作った補助バーツを介してハロの中に取り付けました。

あとは、ハロの目の中に入れるLEDです。5mmの砲弾型、赤色発光のものを2つ用意しました。

目の裏にLEDを入れられるようになっているので、

こんな感じで光拡散キャップを取り付けたLEDを配線して、

突っ込みます。光拡散キャップを被せることで、発光のムラがなくなると共に、良い具合にLEDが固定されます。

後は適当に抵抗を入れて、こんな感じで配線しました。

今後の拡張性を考えて、なるべくGPIOを消費しない方向で、LEDと振動モータの制御線は同じピン(G25)に繋ぎました。従って、発光と振動は完全に同期する形になります。機能充実のM5StickC Plusですが、それゆえに自由に使えるGPIOが少ないのが弱点といえば弱点です。

なお、「ピピッ」と鳴らすためのブザーはM5StickC Plus内蔵のものを使用しています。音量調整できなかったり基本単音を鳴らすだけですが、簡単なシステム音声を鳴らす程度ならこれで充分です。また、マイクも内蔵のものを使用。これだけのものが内蔵されているとか、ホント素晴らしいです、M5StickC Plus。

ソフトウェア解説

今回のソースコードは短めなので、ここで全文掲載してしまいます。

#include "M5StickCPlus.h"

#define PIN_LED_VIB 25
#define PIN_BUZZER   2
#define PWM_CH_LED_VIB 0
#define PWM_CH_BUZZER  1

#define LED_VIB_LEVEL 128
#define LED_VIB_KEEP_MIN_MS 300 // 実際の振動時間はVOICE_LEN x 50ms。ここはこれより短ければ良い

boolean is_led_on = false;
unsigned long sound_detect_time_ms = 0;

// マルチタスク用設定
QueueHandle_t xQueueMailbox;

////////////////////////////////////////////////////////////////////////////////

#include <driver/i2s.h>

#define PIN_CLK     0
#define PIN_DATA    34
#define READ_LEN    (2 * 256)
uint8_t BUFFER[READ_LEN] = {0};
uint16_t oldy[160];
int16_t *adcBuffer = NULL;
size_t bytesread;

uint8_t volume = 0;
#define VOLUME_THRESHOLD 110

void i2sInit()
{
   i2s_config_t i2s_config = {
    .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX | I2S_MODE_PDM),
    .sample_rate =  44100,
    .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT, // is fixed at 12bit, stereo, MSB
    .channel_format = I2S_CHANNEL_FMT_ALL_RIGHT,
    .communication_format = I2S_COMM_FORMAT_I2S,
    .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
    .dma_buf_count = 2,
    .dma_buf_len = 128,
   };
   i2s_pin_config_t pin_config;
   pin_config.bck_io_num   = I2S_PIN_NO_CHANGE;
   pin_config.ws_io_num    = PIN_CLK;
   pin_config.data_out_num = I2S_PIN_NO_CHANGE;
   pin_config.data_in_num  = PIN_DATA;

   i2s_driver_install(I2S_NUM_0, &i2s_config, 0, NULL);
   i2s_set_pin(I2S_NUM_0, &pin_config);
   i2s_set_clk(I2S_NUM_0, 44100, I2S_BITS_PER_SAMPLE_16BIT, I2S_CHANNEL_MONO);
}

void mic_record_task (void* arg){
  size_t bytesread;
  while(1){
    i2s_read(I2S_NUM_0,(char*) BUFFER, READ_LEN, &bytesread, (100 / portTICK_RATE_MS));
    adcBuffer = (int16_t *)BUFFER;

    int32_t offset_sum = 0;
    for (int n = 0; n < 160; n++) {
      offset_sum += (int16_t)adcBuffer[n];
    }
    int offset_val = -( offset_sum / 160 );
    int max_val = 255;
    for (int n = 0; n < 160; n++) {
      int16_t val = (int16_t)adcBuffer[n] + offset_val;
      if ( max_val < abs(val) ) {
        max_val = abs(val);
      }
    }

    int sum = 0;
    for (int n = 0; n < 160; n++){
      sum += abs(map(adcBuffer[n] + offset_val, -max_val, max_val, -255, 255));
    }
    volume = sum/160;
    //Serial.println(volume);
    if(volume > VOLUME_THRESHOLD){
      vTaskDelay(300 / portTICK_RATE_MS);
      xQueueOverwrite(xQueueMailbox, &volume);
    }
    vTaskDelay(50 / portTICK_RATE_MS);
  }
}

////////////////////////////////////////////////////////////////////////////////

#define VOICE_NUM  2
#define VOICE_LEN 11

uint16_t HARO_VOICE[VOICE_NUM][VOICE_LEN] = {
  {1760,   0 , 1760,    0, 2093, 0,    0,    0,    0,    0, 0},
  {1760, 2093, 1760, 2093,    0, 0, 1760, 2093, 1760, 2093, 0}
}; 

////////////////////////////////////////////////////////////////////////////////

void setup() {
  Serial.begin(115200);
  M5.begin();
  pinMode(PIN_LED_VIB, OUTPUT);
  // GPIO25を使うときは、GPIO36をフローティング入力にする
  gpio_pulldown_dis(GPIO_NUM_36);
  gpio_pullup_dis(GPIO_NUM_36);
  pinMode(PIN_BUZZER, OUTPUT);

  // 圧電スピーカー用PWM設定
  ledcSetup(PWM_CH_BUZZER, 5000, 13);
  ledcAttachPin(PIN_BUZZER, PWM_CH_BUZZER);
  ledcWriteTone(PWM_CH_BUZZER, 0);

  i2sInit();

  // タスク間連携用のキュー(メールボックス)を作成
  xQueueMailbox = xQueueCreate(1, sizeof(uint8_t));
  xTaskCreate(mic_record_task, "mic_record_task", 2048, NULL, 1, NULL);
}

void loop() {
  M5.update();

  unsigned long now_ms = millis();

  if(uxQueueMessagesWaiting(xQueueMailbox) > 0){
    uint8_t volume;
    xQueueReceive(xQueueMailbox, &volume, 0);
    if(!is_led_on){
      is_led_on = true;
      sound_detect_time_ms = now_ms;
      digitalWrite(PIN_LED_VIB, 1);
    }
  }

  if(is_led_on && now_ms - sound_detect_time_ms > LED_VIB_KEEP_MIN_MS){
    is_led_on = false;
    digitalWrite(PIN_LED_VIB, 0);
  }

  if(is_led_on){
    uint8_t voice_num = random(VOICE_NUM);
    for(int i=0; i<VOICE_LEN; i++){
      ledcWriteTone(PWM_CH_BUZZER, HARO_VOICE[voice_num][i]);
      vTaskDelay(50 / portTICK_RATE_MS);
    }
  }

  vTaskDelay(50 / portTICK_RATE_MS);
}

ややこしいのはマイクの処理のところですが、ここは公式のサンプルコードだったりこちらの方の記事内容だったりをほぼほぼそのまま使用させて頂きました。変更を加えているところとしては、一回のサンプリングでの振幅の平均値が一定値以上であれば、発光&振動&ブザーの処理を行うタスクの方にお知らせを投げるようにしているところぐらいかと思います。

また、ブザーによるメロディの鳴らし方は、こちらの方の記事を参考にさせて頂きました。

まとめ

以上、『呼ぶと応えるハロ』のご紹介でした。今回は子供の遊び目的ということで、本当にただそれだけの機能なのですが、今後、もう少し実用的な機能をつけていけないかなと思っています。

その下準備として、背面に穴を開けておきました。ここからM5StickC PlusのUSBポートとGroveコネクタにアクセスできますので、いろんなペリフェラルをここに接続することができます。とりあえずいくつかセンサを購入してますので、試してみてある程度形になったら、また改めて公開しようかなと思います。

さて、2022年内の工作はおそらくこれが最後になるかなと思います。例年ならその年スタートの仮面ライダー系のアイテムを何か一つ二つ作っている頃なのですが、『ギーツ』はまだそこまでハマっておらず、また、デザイアドライバーが元々カスタマイズしやすいベルトということで、既にいろんな方々が自作バックルみたいなものを作っている状況ということもあり、個人的になかなかやる気が出ません。が、前向きに捉えると、前回のボトルマンや今回のハロのように、(おそらく自分に期待されているところではないものの)今が仮面ライダー系以外のものに手を出すチャンスということでもあるので、せっかくなので今のうちに色々試していこうかなと思っています。