tktk360のアイコン画像

お手軽カメラ付きタンク

tktk360 2021年01月10日に作成
お手軽カメラ付きタンク

この作品は、スマホでお手軽に操作するWifi操作のロボットタンクです。
市販のキットとマイコンと家にある使わなくなったプラモデル、そして操作するスマホで構成されています。
半田不要でIoTを実感できるものです。
動作している動画

作成の動機

  • ロボット作成としてZumoTankを貰いました。この製品は、各辺10cm以下のコンパクトな無限軌道式ロボット用プラットフォームでモーターも付属し組み立て済みです。そして、Arudinoのシールドとして使えます。しかし、Arudinoをそのまま載せて遊ぶのでは芸がないと思いカメラ付きの操作可能なタンクを作成することにしました。
    最終的には、MQTTを使い、スマートスピーカ連携まで行ったのですが、記事にするには複雑なため
    この投稿は、最初にベースとして作ったスマホからのZumoTankの操作とカメラ画像の閲覧までとしています。

パーツ

  • 作成に使用したパーツは下記となります。
品目
GR-LYCHEE
カメラ延長用フレキシブルフラットケーブル
Polou Zumo ロボット Arduino用
micro sd card 16GB(GR-LYCHEE挿入用)
プラモデル(カメラ仕込み用)

GR-LYCHEEは、カメラと無線を搭載したIoTプロトタイピングボードです。
2個のマイコン(RZ/A1LU、ESP32)が搭載されており、シキノハイテック製のカメラが付属し、OpenCVも動作可能という、非常にパワフルなマイコンボードです。

システム構成図
システム構成図

スマートフォンのアプリ画面

  • IPアドレス指定で、ZUMOのタンクに接続し、前後移動、左右回転、カメラ撮影が行えます。移動量は、スライダーで調整可能です。

筐体の作成

  • ZumoにGR-LYCHEEを接続します。GR-LYCHEEは、Arudino互換のため、そのまま刺さります。
    付属のカメラのケーブルは短いため、長いものに変更し、カメラにロボットのプラモデルの頭を覆わせて雰囲気をだしてみました。そして、プラモデルの中に配線を隠しています。
    ZumoとGR-LYCHEEの接続

使用ライブラリ

プログラムの作成方法
Githubからソースコードを取得してください。
「GR-LYCHEEとZumoを使ったタンクのプログラム開発」と「スマホで操作するアプリケーションの開発」を行います。

GR-LYCHEEとZumoを使ったタンクのプログラム開発

  • 【作成手順】
    1.IDE for Gadget Renesasで、下記プログラムを開いてください。
    2.環境に応じて、プログラムのSSIDとパスワードを書き換えてください。
    3.マイコンボードの設定は、[ツール]-[マイコンボード]-[GR-LYCHEE]に変更し、マイコンボードに書き込んでください。

  • 【補足】
    スマホとの接続はWifi経由で行います。
    タンクの操作は、下記となり、数値は移動量の大きさになります。
    ・IPアドレス/camera
    ・IPアドレス/auto/数値
    ・IPアドレス/foward/数値
    ・IPアドレス/back/数値
    ・IPアドレス/right/数値
    ・IPアドレス/left/数値
    ・IPアドレス/stop

Zumoを載せたGR-LYCHEEのプログラム(GR-LYCHEE_CAMTANK.ino)

#include <Arduino.h> #include <Camera.h> #include <SD.h> #include <HTTPServer.h> #include <mbed_rpc.h> #include <SdUsbConnect.h> #include <ESP32Interface.h> //Zumo #include <ZumoMotors.h> //Camera Image Size #define IMAGE_HW 320 #define IMAGE_VW 240 //Wifi Infomation #define WIFI_SSID "xxxxxxx" #define WIFI_PW "xxxxxxx" //_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/ //Wifi ESP32Interface wifi; // Camera Camera camera(IMAGE_HW, IMAGE_VW); SdUsbConnect storage("storage"); static char result_str[] = "success"; // Zumo #define LED_PIN 13 ZumoMotors _motors; int _speed = 0; // Program enum ZUMO_MODE_TYPE { Unknown, Auto, Foward, Back, Left, Right, Stop, }; ZUMO_MODE_TYPE _zumo_mode = ZUMO_MODE_TYPE::Unknown; static Thread httpTask(osPriorityAboveNormal, (1024 * 4)); //////////////////////////////////////////////////////// //snapshot_req static int snapshot_req(const char* rootPath, const char* path, const char ** pp_data) { if (strcmp(rootPath, "/camera") == 0) { Serial.println("snap shot"); size_t size = camera.createJpeg(); *pp_data = (const char*)camera.getJpegAdr(); return size; } else if (strcmp(rootPath, "/auto") == 0) { _speed = atoi(path+1); Serial.println("auto"); Serial.println(_speed); _zumo_mode = ZUMO_MODE_TYPE::Auto; *pp_data = (const char *)result_str; return strlen(result_str); } else if (strcmp(rootPath, "/foward") == 0) { _speed = atoi(path+1); Serial.print("foward "); Serial.println(_speed); _zumo_mode = ZUMO_MODE_TYPE::Foward; *pp_data = (const char *)result_str; return strlen(result_str); } else if (strcmp(rootPath, "/back") == 0) { _speed = atoi(path+1); Serial.print("back "); Serial.println(_speed); _zumo_mode = ZUMO_MODE_TYPE::Back; *pp_data = (const char *)result_str; return strlen(result_str); } else if (strcmp(rootPath, "/right") == 0) { _speed = atoi(path+1); Serial.print("right "); Serial.println(_speed); _zumo_mode = ZUMO_MODE_TYPE::Right; *pp_data = (const char *)result_str; return strlen(result_str); } else if (strcmp(rootPath, "/left") == 0) { _speed = atoi(path+1); Serial.print("left "); Serial.println(_speed); _zumo_mode = ZUMO_MODE_TYPE::Left; *pp_data = (const char *)result_str; return strlen(result_str); } else if (strcmp(rootPath, "/stop") == 0) { Serial.println("stop"); _speed = 0; _zumo_mode = ZUMO_MODE_TYPE::Stop; *pp_data = (const char *)result_str; return strlen(result_str); } else { Serial.println("unknown"); } return 0; } //////////////////////////////////////////////////////// //http_task void http_task(void) { // wifi Serial.print("Connecting Wi-Fi.."); if (wifi.connect(WIFI_SSID, WIFI_PW, NSAPI_SECURITY_WPA_WPA2) == 0) { Serial.println("success"); } else { Serial.println("fail"); } Serial.print("MAC Address is "); Serial.println(wifi.get_mac_address()); Serial.print("IP Address is "); Serial.println(wifi.get_ip_address()); Serial.print("NetMask is "); Serial.println(wifi.get_netmask()); Serial.print("Gateway Address is "); Serial.println(wifi.get_gateway()); Serial.println("Network Setup OK\r\n"); //req SnapshotHandler::attach_req(&snapshot_req); HTTPServerAddHandler<SnapshotHandler>("/camera"); //Camera HTTPServerAddHandler<SnapshotHandler>("/auto"); HTTPServerAddHandler<SnapshotHandler>("/foward"); HTTPServerAddHandler<SnapshotHandler>("/back"); HTTPServerAddHandler<SnapshotHandler>("/right"); HTTPServerAddHandler<SnapshotHandler>("/left"); HTTPServerAddHandler<SnapshotHandler>("/stop"); Serial.println("Handler"); FSHandler::mount("/storage", "/"); HTTPServerAddHandler<FSHandler>("/"); HTTPServerAddHandler<RPCHandler>("/rpc"); Serial.println("Server Start"); HTTPServerStart(&wifi, 80); } //Tank foward void fowardFunction(int spd) { // run left motor forward // run right motor forward for (int speed = 0; speed <= spd; speed++) { _motors.setLeftSpeed(-speed); _motors.setRightSpeed(-speed); delay(2); } for (int speed = spd; speed >= 0; speed--) { _motors.setLeftSpeed(-speed); _motors.setRightSpeed(-speed); delay(2); } } //Tank back void backFunction(int spd) { // run left motor backward // run right motor backward for (int speed = 0; speed <= spd; speed++) { _motors.setLeftSpeed(speed); _motors.setRightSpeed(speed); delay(2); } for (int speed = spd; speed >= 0; speed--) { _motors.setLeftSpeed(speed); _motors.setRightSpeed(speed); delay(2); } } //Tank right turn void rightFunction(int spd) { // run left motor forward // run right motor backward for (int speed = 0; speed <= spd; speed++) { _motors.setLeftSpeed(-speed); _motors.setRightSpeed(speed); delay(2); } for (int speed = spd; speed >= 0; speed--) { _motors.setLeftSpeed(-speed); _motors.setRightSpeed(speed); delay(2); } } //Tank left turn void leftFunction(int spd) { // run left motor backward // run right motor forward for (int speed = 0; speed <= spd; speed++) { _motors.setLeftSpeed(speed); _motors.setRightSpeed(-speed); delay(2); } for (int speed = spd; speed >= 0; speed--) { _motors.setLeftSpeed(speed); _motors.setRightSpeed(-speed); delay(2); } } //Tank stop void stopFunction() { _motors.setLeftSpeed(0); _motors.setRightSpeed(0); } //////////////////////////////////////////////////////// //setup void setup(void) { Serial.begin(9600); Serial.println("Starts."); // SD & USB Serial.print("Finding strage.."); storage.wait_connect(); Serial.println("done"); // Zumo pinMode(LED_PIN, OUTPUT); _motors.flipLeftMotor(true); _motors.flipRightMotor(true); // camera camera.begin(); // http httpTask.start(&http_task); Serial.println("start"); } //////////////////////////////////////////////////////// //loop void loop() { // Test Input while (Serial.available() > 0) { char mode = Serial.read(); Serial.print("modeNo="); Serial.println(mode); int modeNo = (mode - '0'); _zumo_mode = (ZUMO_MODE_TYPE)modeNo; } if(_zumo_mode == ZUMO_MODE_TYPE::Unknown) { return; } // Control switch(_zumo_mode) { case ZUMO_MODE_TYPE::Auto: digitalWrite(LED_PIN, HIGH); // front fowardFunction(_speed); // back backFunction(_speed); // left leftFunction(225); // front fowardFunction(_speed); // left leftFunction(225); // front fowardFunction(_speed); // left leftFunction(225); // front fowardFunction(_speed); // left leftFunction(225); // front fowardFunction(_speed); digitalWrite(LED_PIN, LOW); break; case ZUMO_MODE_TYPE::Stop: stopFunction(); break; case ZUMO_MODE_TYPE::Back: digitalWrite(LED_PIN, HIGH); backFunction(_speed); digitalWrite(LED_PIN, LOW); break; case ZUMO_MODE_TYPE::Foward: digitalWrite(LED_PIN, HIGH); fowardFunction(_speed); digitalWrite(LED_PIN, LOW); break; case ZUMO_MODE_TYPE::Right: digitalWrite(LED_PIN, HIGH); rightFunction(_speed); digitalWrite(LED_PIN, LOW); break; case ZUMO_MODE_TYPE::Left: digitalWrite(LED_PIN, HIGH); leftFunction(_speed); digitalWrite(LED_PIN, LOW); break; } _zumo_mode = ZUMO_MODE_TYPE::Unknown; }

スマホで操作するアプリケーションの開発

  • プログラムは、Unityを使用しています。
    事前にUnityがインストールされた環境での説明となります。

  • 【作成手順】
    1.Githubからソースコードをダウンロードしてください
    2.Unityを起動し、プロジェクトを読み込んでください
      フォルダ階層は、ダウンロードした「GRLycheeTank\Unity」になります
    3.Unityのプログラムシーン「Assets\Main」を開いてください
    5.Androidプロジェクトで出力してください
    6.Androidスマートフォンにインストールしてください

  • コードの説明
    ・MainシーンのManagerにアタッチしているスクリプト(ZumoControlScript.cs)が本体になります。
     uGUIの各操作ボタンを押すことで、スクリプトに対応したコードが実行されます。

スマホ用コントロールのメインスクリプト(ZumoControlScript.cs)

using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; using UnityEngine.Networking; /// <summary> /// ZumoControlScript /// </summary> public class ZumoControlScript : MonoBehaviour { public RawImage cameraImage; public InputField ipAddressText; public Slider powerSlider; public SampleWebView webView; public enum ZUMO_MODE { Front, Back, Left, Right, Round, Camera } /// <summary> /// Start /// </summary> void Start() { if (ipAddressText != null) { var address = PlayerPrefs.GetString("address"); ipAddressText.text = string.IsNullOrEmpty(address) ? "http://192.168.10.12" : address; } } /// <summary> /// OnApplicationQuit /// </summary> private void OnApplicationQuit() { if (ipAddressText != null) { PlayerPrefs.SetString("address", ipAddressText.text); } } /// <summary> /// CreateSpriteFromByte /// </summary> /// <param name="data"></param> /// <returns></returns> protected Sprite CreateSpriteFromByte(byte[] data) { var tex = new Texture2D(320, 240); tex.LoadImage(data); return Sprite.Create(tex, new Rect(0, 0, 320, 240), Vector2.zero); } public Texture readByBinary(byte[] bytes) { var texture = new Texture2D(1, 1); texture.LoadImage(bytes); return texture; } /// <summary> /// SendZumo /// </summary> protected IEnumerator SendZumo(ZUMO_MODE mode) { if (ipAddressText != null && powerSlider != null) { var url = ipAddressText.text; var power = powerSlider.value * 600f; if (power <= 10) { power = 50; } switch (mode) { case ZUMO_MODE.Front: url += string.Format("/foward/{0}", (int)power); break; case ZUMO_MODE.Back: url += string.Format("/back/{0}", (int)power); break; case ZUMO_MODE.Left: url += string.Format("/left/{0}", (int)power); break; case ZUMO_MODE.Right: url += string.Format("/right/{0}", (int)power); break; case ZUMO_MODE.Round: url += string.Format("/auto/{0}", (int)power); break; case ZUMO_MODE.Camera: url += "/camera"; break; } Debug.Log(url); // texture if (mode == ZUMO_MODE.Camera && cameraImage != null) { webView.Url = url; webView.LoadContent(); } else { WWW www = new WWW(url); yield return www; } } } public void FrontAction() { StartCoroutine(SendZumo(ZUMO_MODE.Front)); } public void BackAction() { StartCoroutine(SendZumo(ZUMO_MODE.Back)); } public void LeftAction() { StartCoroutine(SendZumo(ZUMO_MODE.Left)); } public void RightAction() { StartCoroutine(SendZumo(ZUMO_MODE.Right)); } public void RoundAction() { StartCoroutine(SendZumo(ZUMO_MODE.Round)); } public void CameraAction() { StartCoroutine(SendZumo(ZUMO_MODE.Camera)); } }

最後に
半田は極力避けたいけど、マイコンで何か面白いものを作ってみたいという方のとっかかりになればと思います。
電子工作要素は薄いですが、ここからの発展要素は満載だと思います。
LEDで発行させるも良し、スマートスピーカ連携も面白いです。
是非、試してみてください。

tktk360 さんが 2021/01/10 に 編集 をしました。 (メッセージ: 初版)
ログインしてコメントを投稿する