知的好奇心 for IoT

IoT関連の知的好奇心を探求するブログです

M5CameraとDRV8830を2個搭載したGROVE I2CミニモータードライバとBlynkでお手軽カメラタンクを作った

察しの良い方はこの記事で予想をしていたと思いますが...

今回作ったものは、前の記事のプログラムにBlynkのジョイスティック操作でモータードライバを使ってモーターをコントロールする機能を付け加えたものです。

今回の完成形の写真はこれです!

f:id:IntellectualCuriosity:20200518213645j:plain

真上からだとこんな感じ。

f:id:IntellectualCuriosity:20200518214751j:plain

横だとこう。

f:id:IntellectualCuriosity:20200518214949j:plain

スマホスクリーンショットはこんな感じです。

f:id:IntellectualCuriosity:20200518225230j:plain

ラズパイのカメラ(SONYのイメージセンサのやつ)より画質は落ちますが、ぱっと見そんなに気にならないので、コスパ的には断然M5Cameraだと思います。

 

使ったもの(ハードウェア)

 

ギヤードモーター

ラズパイタンクを作った時にローテーションサーボが気に入っていたので、最初は今回もローテーションサーボを使おうと思っていたんです。

しかし、次の「ESP32Servo」など、ESP32で使えるサーボモーター用のライブラリを幾つか試したのですが、サーボモーターを使うとカメラで画像をキャプチャするコードでエラーが出るようになるんです。

f:id:IntellectualCuriosity:20200519183053p:plain

エラーが出るようになるコード。(fb(フレームバッファ)に値が入らずエラーになる)

fb = esp_camera_fb_get();
if (!fb) {
  Serial.println("Camera capture failed");
  res = ESP_FAIL;
} else {

サーボモーターって制御に負荷がかかる(決まったパルスを一定周期で出す)ようで、映像ストリーミングとの共存はESP32には荷が重すぎたみたいです。

で、次に目を付けたのが今回使用したギヤードモーターなんです。

レゴ仕様のサーボモーター形ギヤードモーターとドライブスプロケット&Small Wheel

f:id:IntellectualCuriosity:20200519204055j:plain

レゴ仕様のギヤードモーターは丁度いい位置に穴が開いていて、結束バンドで簡単にユニバーサルプレートに留めることができました。

問題はオレンジ色のドライブスプロケットとギヤードモーターをどう繋ぐかです。

M5Cameraもレゴ仕様になっているし、何かできるかなと思って買った「Lego Crazy Action Contraptions」に入っていたSmall Wheelが、ドライブスプロケットにピッタリハマる大きさだったんです。しかも結束バンドで留めるのにいい位置に穴が開いていて、しっかり留めることができました。

また、実際に買っていないので確証はありませんが、Brickers!というLEGOブロック専門店で1個80円で売っているのを見つけました。

レゴ テクニック パーツ ベルト ホイール [ Dark Bluish Gray / ダークグレー ] | lego

近々買ってみようと思います。

 

Grove - I2C Mini Motor Driver

これまでTB6612やDRV8835などGPIOで操作するモータードライバを使っていましたが、GPIOのピン数を多く使用するためM5Cameraでは使えません。

そこで、Groveシステムで使えるI2C対応のモータードライバを探して見つけたのがGrove - I2C Mini Motor Driverです。

DRV8835は凄くシンプルな作りでライブラリを使わなくても比較的容易に扱えるのですが、作りが古く最近売られているI2C機器とは若干使い方が違うんです。そのためメーカーが用意しているライブラリは使えず、情報を集めていないと扱いに非常に手間取るのが欠点です。

I2Cアドレス

まず最初につまづくのがI2Cアドレスです。

メーカーや販売店のホームページに掲載されている写真が2015年のv1.0のもので古く、写真や掲載されているアドレスが違っています。

メーカーの製品ページ

v1.1 05/02/2017が現行品

f:id:IntellectualCuriosity:20200519223834j:plain

また、2017年v1.1のボード記載のADDR1: 0xCA(W)、ADDR2:0xC0(W)もそのままでは使えず、1ビット右にシフトした値でアクセスする必要があります。

ADDR1(CH1): 0xCA >> 1 = 0x65
ADDR2(CH2): 0xC0 >> 1 = 0x60

モーター選び

DRV8830には電流制限機能が付いていて200mAから1Aまで設定できるようになっています。でも、この設定はソフトウェア的ではなくハードウェア的に行うもので、GROVE I2C Mini Motor Driverだと基板にチップ抵抗を付けたり外したりして設定するんです。

コネクタを差すだけで簡単に使えるのが利点のGrove対応機器で、わざわざそんな面倒なことをしようとする人がいるとは思えませんが、なぜかそういう仕様なんです。

で、デフォルトではこの電流制限値がチャンネル当たり下限の200mAとなっています。

この値を超えると電流障害状態となり基板のFAULTランプが点灯します。

また、出力電圧は0.48V~5.06Vとなっていて、これらの条件に合致するモーターを選ぶ必要があります。

手持ちのモーターではレゴ仕様のギヤードモーターの他、FEETECH FM90は問題なく動作しました。

f:id:IntellectualCuriosity:20200520005746j:plain

簡単に車体を組み立てることができる2WD Mini Smart Robot Mobile Platform Kit for educationに付属していたギヤードモーターはFAULTが点灯したり、点灯しなくても動作しないときがありダメっぽいです。

f:id:IntellectualCuriosity:20200520010356j:plain

 

 

使ったもの(ソフトウェア)

ブラウザ(ChromeFirefox)ー映像受信用

映像の受信には今回もChromeFirefoxを使いました。ぼくのスマホAndroidなのでSafariで動作確認をしていませんが、たぶん問題なく見れると思います。

URLの入力欄にM5CameraのIPアドレスを入力するだけで映像を見ることができます。

 

BlynkーCamTank操作用

BlynkのJoystickの設定はこうなっています。iOSだと「WRITE INTERVAL」が表示されないようですが気にする必要はありません。

f:id:IntellectualCuriosity:20200520214212j:plain

 

Arduinoスケッチ

コンパイル前にESP32のボード情報を取り込んで「ESP32 Wrover Module」を選び、Blynkライブラリを入れておくのを忘れないでくださいね。

/*
/*
 * CamTank2 (M5Camera Tank)
 * 
 * May 20, 2020
 * By Hiroyuki ITO
 * http://intellectualcuriosity.hatenablog.com/  
 * MIT Licensed.
 */
#define BLYNK_PRINT Serial
#include <Wire.h>
#include <WiFi.h>
#include <BlynkSimpleStream.h>
#include "esp_camera.h"
#include "esp_http_server.h"

// スマートコンフィグが使えないときは、以下の2行のコメントを外してSSIDとパスワードを設定します
//#define WIFI_SSID       "WiFiSSIDで置き換えます"
//#define WIFI_PASS       "WiFiのパスワードで置き換えます"
#define BLYNK_TOKEN     "メールで届いたAUTH TOKENで置き換えます"
#define CAMERA_VFLIP    0               // カメラの設置向き(本体のM5文字の向き) 1:正常 0:逆さ
#define FRAME_SIZE      FRAMESIZE_VGA   // サイズ:FRAMESIZE_ + QVGA|CIF|VGA|SVGA|XGA|SXGA|UXGA

// DRV8830関連
#define DRV8830_CH1 0x65                // CH1のI2Cアドレス
#define DRV8830_CH2 0x60                // CH2のI2Cアドレス
#define VSET_MIN    0x07                // 電圧設定の下限 0x07:0.56V
#define VSET_MAX    0x3f                // 電圧設定の上限 0x3f:5.06V

#define PART_BOUNDARY "123456789000000000000987654321"
static const char* _STREAM_CONTENT_TYPE = "multipart/x-mixed-replace;boundary=" PART_BOUNDARY;
static const char* _STREAM_BOUNDARY = "\r\n--" PART_BOUNDARY "\r\n";
static const char* _STREAM_PART = "Content-Type: image/jpeg\r\nContent-Length: %u\r\n\r\n";

httpd_handle_t stream_httpd = NULL;
WiFiClient wifiClient;

// カメラの初期化処理
void initCamera() {
  camera_config_t config;
  config.ledc_channel = LEDC_CHANNEL_0;
  config.ledc_timer =   LEDC_TIMER_0;
  config.pin_d0 =       32;             // Y2_GPIO_NUM
  config.pin_d1 =       35;             // Y3_GPIO_NUM
  config.pin_d2 =       34;             // Y4_GPIO_NUM
  config.pin_d3 =        5;             // Y5_GPIO_NUM
  config.pin_d4 =       39;             // Y6_GPIO_NUM
  config.pin_d5 =       18;             // Y7_GPIO_NUM
  config.pin_d6 =       36;             // Y8_GPIO_NUM
  config.pin_d7 =       19;             // Y9_GPIO_NUM
  config.pin_xclk =     27;             // XCLK_GPIO_NUM
  config.pin_pclk =     21;             // PCLK_GPIO_NUM
  config.pin_vsync =    25;             // VSYNC_GPIO_NUM
  config.pin_href =     26;             // HREF_GPIO_NUM
  config.pin_sscb_sda = 22;             // SIOD_GPIO_NUM
  config.pin_sscb_scl = 23;             // SIOC_GPIO_NUM
  config.pin_pwdn =     -1;             // PWDN_GPIO_NUM
  config.pin_reset =    15;             // RESET_GPIO_NUM
  config.xclk_freq_hz = 20000000;
  config.pixel_format = PIXFORMAT_JPEG;
  config.frame_size =   FRAME_SIZE;
  config.jpeg_quality = 10;
  config.fb_count =      2;
  esp_err_t err = esp_camera_init(&config);
  if (err != ESP_OK) {
    BLYNK_LOG2("Camera init failed with error ", err);
    return;
  }
  sensor_t *s = esp_camera_sensor_get();
  s->set_vflip(s, CAMERA_VFLIP);
  s->set_hmirror(s, 0);
}

// ストリーミングハンドラー
static esp_err_t stream_handler(httpd_req_t *req) {
  camera_fb_t * fb = NULL;
  esp_err_t res = ESP_OK;
  size_t _jpg_buf_len = 0;
  uint8_t * _jpg_buf = NULL;
  char * part_buf[64];

  res = httpd_resp_set_type(req, _STREAM_CONTENT_TYPE);
  if (res != ESP_OK) {
    return res;
  }

  while (true) {
    fb = esp_camera_fb_get();
    if (!fb) {
      BLYNK_LOG("Camera capture failed");
      res = ESP_FAIL;
    } else {
      if (fb->width > 400) {
        if (fb->format != PIXFORMAT_JPEG) {
          bool jpeg_converted = frame2jpg(fb, 80, &_jpg_buf, &_jpg_buf_len);
          esp_camera_fb_return(fb);
          fb = NULL;
          if (!jpeg_converted) {
            BLYNK_LOG("JPEG compression failed");
            res = ESP_FAIL;
          }
        } else {
          _jpg_buf_len = fb->len;
          _jpg_buf = fb->buf;
        }
      }
    }
    if (res == ESP_OK) {
      size_t hlen = snprintf((char *)part_buf, 64, _STREAM_PART, _jpg_buf_len);
      res = httpd_resp_send_chunk(req, (const char *)part_buf, hlen);
    }
    if (res == ESP_OK) {
      res = httpd_resp_send_chunk(req, (const char *)_jpg_buf, _jpg_buf_len);
    }
    if (res == ESP_OK) {
      res = httpd_resp_send_chunk(req, _STREAM_BOUNDARY, strlen(_STREAM_BOUNDARY));
    }
    if (fb) {
      esp_camera_fb_return(fb);
      fb = NULL;
      _jpg_buf = NULL;
    } else if (_jpg_buf) {
      free(_jpg_buf);
      _jpg_buf = NULL;
    }
    if (res != ESP_OK) {
      break;
    }
  }
  return res;
}

// ビデオストリーミングの開始
void startCameraServer() {
  httpd_config_t config = HTTPD_DEFAULT_CONFIG();

  httpd_uri_t index_uri = {
    .uri       = "/",
    .method    = HTTP_GET,
    .handler   = stream_handler,
    .user_ctx  = NULL
  };

  BLYNK_LOG2("Starting stream server on port: ", config.server_port);
  if (httpd_start(&stream_httpd, &config) == ESP_OK) {
    httpd_register_uri_handler(stream_httpd, &index_uri);
  }
  BLYNK_LOG3("Camera Ready! Use 'http://", WiFi.localIP(), "' to connect");
}

// 接続処理
boolean connect() {
  if (WiFi.status() != WL_CONNECTED) {
    #if defined(WIFI_SSID)              // WIFI_SSIDが定義されているとき
      WiFi.begin(WIFI_SSID, WIFI_PASS);
    #else
      WiFi.begin();                     // WiFiに前回の設定で接続
    #endif
    Serial.print("Connecting");
    for (int i=0; WiFi.status() != WL_CONNECTED; i++) { // 繋がるまで10秒間ループ
      Serial.print(".");
      delay(500);
      if (i > 20) {
        return false;
      }
    }
    BLYNK_LOG("");
    BLYNK_LOG2("Connected to ", WiFi.SSID());
    wifiClient.stop();
    wifiClient.connect(BLYNK_DEFAULT_DOMAIN, BLYNK_DEFAULT_PORT); // Blynkサーバーに接続
  }
  return true;
}

// 初期化時の接続処理
void connectInit() {
  WiFi.mode(WIFI_STA);
  if (!connect()) {
    WiFi.beginSmartConfig();            // スマートコンフィグを実行
    BLYNK_LOG("SmartConfig Start!");
    while(!WiFi.smartConfigDone()) {
      delay(500);
      Serial.print(".");
    }
    BLYNK_LOG("SmartConfig received.");
    BLYNK_LOG2("Connected to ", WiFi.SSID());
    wifiClient.connect(BLYNK_DEFAULT_DOMAIN, BLYNK_DEFAULT_PORT); // Blynkサーバーに接続
  }
  Blynk.begin(wifiClient, BLYNK_TOKEN); // Blynkの初期化
}

// DRV8830制御処理
void writeRegister(byte addr, byte vset, byte ctrl) {
  byte data = vset<<2 | ctrl;
  Wire.beginTransmission(addr);
  Wire.write(0x00);                     // CONTROL(出力の状態および出力電圧の設定)レジスタを指定
  Wire.write(data);
  Wire.endTransmission();
}

// モーター制御処理
void driveMotor(byte addr, int i) {
  int dir, spd;

  // ブリッジ制御(方向 direction)
  if (i > 0) {
    dir = 1;                            // 1:正転
  } else if (i < 0) {
    dir = 2;                            // 2:逆転
  } else {
    dir = 0;                            // 0:スタンバイ/惰走
  }
  // 電圧設定(速度 speed)
  if (i == 0) {
    spd = 0;
  } else {
    i = sqrt(i*i);
    // 100+45/2はJoystickから得られる値の最大値
    spd = map(i, 1, int(100+45/2), VSET_MIN, VSET_MAX);
  }

  writeRegister(addr, spd, dir);
}

// Joystickからの入力処理
BLYNK_WRITE(V1) {
  int r, l;
  int x = param[0].asInt();             // JoystickのX軸
  int y = param[1].asInt();             // JoystickのY軸

  if (y >= 0) {                         // 前進
    r = -x/2 + y;
    l = x/2 + y;
  } else {                              // 後退
    r = x/2 + y;
    l = -x/2 + y;
  }
  driveMotor(DRV8830_CH1, r);
  driveMotor(DRV8830_CH2, l);
}

void setup() {
  Serial.begin(115200);
  Wire.begin(4, 13);
  initCamera();                         // カメラの初期化処理
  connectInit();                        // 初期化時の接続処理 
  startCameraServer();                  // ビデオストリーミングの開始
}

void loop() {
  while (!connect());
  Blynk.run();
}

 

おわり