察しの良い方はこの記事で予想をしていたと思いますが...
今回作ったものは、前の記事のプログラムにBlynkのジョイスティック操作でモータードライバを使ってモーターをコントロールする機能を付け加えたものです。
今回の完成形の写真はこれです!
真上からだとこんな感じ。
横だとこう。
ラズパイのカメラ(SONYのイメージセンサのやつ)より画質は落ちますが、ぱっと見そんなに気にならないので、コスパ的には断然M5Cameraだと思います。
使ったもの(ハードウェア)
- M5Camera
- Grove - I2C Mini Motor Driver
- 4個のプログラマブルオタクサーボモーター micro:bit Robotbit スマートカー レゴ適用 (4個赤い)
※2個だけ使用。赤はサーボモーターの形をしたギヤードモーターです。 - タミヤ TAMIYA 楽しい工作シリーズ ユニバーサルプレート(2枚セット)
※ユニバーサルプレートを1枚だけ使用 - タミヤ TAMIYA 楽しい工作シリーズ 32mm径 スプロケット&クローラーセット
- Lego Crazy Action Contraptions
※Small Wheelを2個だけ使用 - 手持ちの小型モバイルバッテリー(出力1A)
- 100均の結束バンド少々
- 100均の地震対策GEL 4枚入りの1枚を使用
- 100均の粘着タック少々
ギヤードモーター
ラズパイタンクを作った時にローテーションサーボが気に入っていたので、最初は今回もローテーションサーボを使おうと思っていたんです。
しかし、次の「ESP32Servo」など、ESP32で使えるサーボモーター用のライブラリを幾つか試したのですが、サーボモーターを使うとカメラで画像をキャプチャするコードでエラーが出るようになるんです。
エラーが出るようになるコード。(fb(フレームバッファ)に値が入らずエラーになる)
fb = esp_camera_fb_get(); if (!fb) { Serial.println("Camera capture failed"); res = ESP_FAIL; } else {
サーボモーターって制御に負荷がかかる(決まったパルスを一定周期で出す)ようで、映像ストリーミングとの共存はESP32には荷が重すぎたみたいです。
で、次に目を付けたのが今回使用したギヤードモーターなんです。
レゴ仕様のサーボモーター形ギヤードモーターとドライブスプロケット&Small Wheel
レゴ仕様のギヤードモーターは丁度いい位置に穴が開いていて、結束バンドで簡単にユニバーサルプレートに留めることができました。
問題はオレンジ色のドライブスプロケットとギヤードモーターをどう繋ぐかです。
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が現行品
また、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は問題なく動作しました。
簡単に車体を組み立てることができる2WD Mini Smart Robot Mobile Platform Kit for educationに付属していたギヤードモーターはFAULTが点灯したり、点灯しなくても動作しないときがありダメっぽいです。
使ったもの(ソフトウェア)
映像の受信には今回もChromeかFirefoxを使いました。ぼくのスマホはAndroidなのでSafariで動作確認をしていませんが、たぶん問題なく見れると思います。
URLの入力欄にM5CameraのIPアドレスを入力するだけで映像を見ることができます。
BlynkーCamTank操作用
BlynkのJoystickの設定はこうなっています。iOSだと「WRITE INTERVAL」が表示されないようですが気にする必要はありません。
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 "WiFiのSSIDで置き換えます" //#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(); }
おわり