スキップしてメイン コンテンツに移動

走行型ロボットをESP32でブラウザから制御

今回は、タミヤのカムプログラムロボットという走行型ロボットにESP32を搭載してブラウザから制御する方法を記述していきます。
カムプログラムロボットというのはカムと呼ばれるスティックで物理的にプログラムしてロボットを制御できる、プログラミングの基礎を学べるような製品です。
以下のリンクが公式ページになっています。

www.tamiya.com

そんなカムでプログラミング制御できるロボットを、今回はESP32を用いてパソコンでプログラム・制御していこうと思います。

最終目標

・ESP32で走行型ロボットを制御
・ブラウザから制御できるようにする

学べること

・モータードライバの使い方・注意点
・簡単にESP32でWEBサーバを建てる

開発前提

・半田付けができる
・Platformioを使ったことがある
・ESP32にプログラムを書き込んだことがある

使ったもの

・カムプログラムロボット ・ESP32(node MCU

https://www.amazon.co.jp/KKHMF-ESP-32S-NodeMCU開発ボード2-4GHz-Bluetooth-デュアルコアCPU低消費電力/dp/B077ZSPKLZ/ref=asc_df_B077ZSPKLZ/?tag=jpgo-22&linkCode=df0&hvadid=221818643417&hvpos=1o1&hvnetw=g&hvrand=282099580503399153&hvpone=&hvptwo=&hvqmt=&hvdev=c&hvdvcmdl=&hvlocint=&hvlocphy=1009227&hvtargid=pla-457743594153&psc=1www.amazon.co.jp

・モータドライバ デュアルモータードライバTB6612FNG(連続最大1A)www.switch-science.com・はんだセット
・ブレッドボード
・単三2本用バッテリーボックス ・単三3本用バッテリーボック

開発順序

  1. 戦車の物理的改造・回路作り
  2. ESP32のプログラム作り

1. 戦車の物理的改造・回路作り

まずは回路を作っていきます。
カムプログラムロボットには二個のモータが搭載されているので、モータの線が合計で4本あります。
また、標準で単三電池一本が入るバッテリーケースがありますが今回は使用せずバッテリーケースは別途、単三2本用と単三3本用を使用します。 それを踏まえた上で、回路は以下のようになります。

基本回路

まずモータドライバの説明で、この回路ではモータ電源を3V電源を用意しています。
キットではバッテリーボックスが一本用で1.5V電源でしたが、モータドライバのによる電圧降下問題を解決するために3Vに強化しました。

次にバッテリーボックスについてですが、今回はバッテリーボックスをわざわざ二つ使用しています。
この理由は、もしESP32とモータ供給を一つのバッテリーボックスから行ってしまうとモータを正転と逆転に切り替えた際、バッテリーから供給される電圧が一瞬著しく降下してようで、ESP32側でBrownout Detect(電圧低下検知)が働き再起動してしまいます。
なので電源を二つに分けることでこの問題に対応しています。

ESP32の電源は今回3本の単三電池(4.5V)でまかないますが、ESP32の動作電圧は2.7~3.3VなのでNodeMCUの3.3Vピンに繋ぐと破損してしまう可能性があります。
そこで、NodeMCUに用意されている5Vピンを今回は使用しています。
このピンはNodeMCU内蔵のレギュレータに繋がっているため、内蔵回路で3.3Vにしてくれます。
また、カムプログラムロボットにはスイッチがあるのでこのスイッチを単三3本電池ボックスのプラスまたはマイナスの線とブレッドボードの間に挟むことで、ESP32の電源を管理できるようになります。

次に制御線についてで、今回はモータ1をESP32の16・17ピンでモータ2を18・19ピンで制御していきます。
モータはこれらのピンの状態で制御され、デジタル信号(電流のON・OFF)で制御されます。

これで回路作成はできたので、次はプログラムの作成になります。

2. ESP32のプログラム作り

ESP32のプログラムではWEBサーバーを立てていきます。
ESP32でWEBサーバを立ち上げる方法は、TCP接続をしてHTTPプロトコルを自分で処理していく方法と簡単にWEBサーバを立てるライブラリを使用し、簡単に実装する方法があります。
今回は後者の方法で作成しましたが、利用したのは個人的に作成していたライブラリを使用していきます。
リポジトリリンクは以下に貼っておきますが、個人で作成しているものなので仕様が変わる可能性があるのでご了承ください。

github.com

このライブラリをgithubから直接Platdformioで利用するには、platformio.iniファイルに以下を追記します。

lib_deps =
  https://github.com/TakumiShinoda/Esp32_WebServerObject

それでは早速コードを見ていきましょう。

#include <Arduino.h>
#include <ServerObject.h>
#include <WiFiConnection.h>

// ここは制作環境に応じて変更
#define MOTOR_A_PIN1 16
#define MOTOR_A_PIN2 17
#define MOTOR_B_PIN1 19
#define MOTOR_B_PIN2 18

#define STOP 0
#define ROTATE_NORMAL 1
#define ROTATE_REVERSE 2

#define TURN_STOP 0
#define TURN_LEFT 1
#define TURN_RIGHT 2

ServerObject server = ServerObject(); // サーバのインスタンス

void leftWheel(bool pin1, bool pin2){ // 左の車輪制御
  digitalWrite(MOTOR_A_PIN1, pin1);
  digitalWrite(MOTOR_A_PIN2, pin2);
}

void rightWheel(bool pin1, bool pin2){ // 右の車輪制御
  digitalWrite(MOTOR_B_PIN1, pin1);
  digitalWrite(MOTOR_B_PIN2, pin2);
}

void wheel(uint8_t left, uint8_t right){ // 車輪制御(左右)
  if(left == ROTATE_NORMAL) leftWheel(true, false);
  else if(left == ROTATE_REVERSE) leftWheel(false, true);

  if(right == ROTATE_NORMAL) rightWheel(true, false);
  else if(right == ROTATE_REVERSE) rightWheel(false, true);
}

void stop(){ // 停止
  leftWheel(false, false);
  rightWheel(false, false);
}

void turnLeft(){ wheel(false, true); } // 左に旋回
void turnRight(){ wheel(true, false); } // 右に旋回

void tankCallback(ChainArray queries, ChainArray request, String *response, WiFiClient *client){ // 'IP/tank'にアクセスされた時のコールバック
  client->println(*(response));
  client->stop();

  if(queries.exist("turn")){ // クエリにturnがあった時
    uint8_t turn = queries.get("turn").toInt();

    if(turn == TURN_LEFT) wheel(ROTATE_REVERSE, ROTATE_NORMAL);
    else if(turn == TURN_RIGHT) wheel(ROTATE_NORMAL, ROTATE_REVERSE);

    if(queries.exist("turn_delay")) delay(queries.get("turn_delay").toInt());
    else delay(1000);
    stop();
  }

  if(queries.exist("forward")){ // クエリにforwardがあった時
    uint8_t forward = queries.get("forward").toInt();

    if(forward == ROTATE_NORMAL) wheel(ROTATE_NORMAL, ROTATE_NORMAL);
    else if(forward == ROTATE_REVERSE) wheel(ROTATE_REVERSE, ROTATE_REVERSE);

    if(queries.exist("forward_delay")) delay(queries.get("forward_delay").toInt());
    else delay(1000);
    stop();
  }
}

void setup(){
  ResponseHandler tankResponse("hoge", &tankCallback); // レスポンスを生成

  Serial.begin(115200);
  if(connectAP("ssid", "pass")) Serial.println("Cannot connect to AP."); // WiFiに接続

  pinMode(MOTOR_A_PIN1, OUTPUT);
  pinMode(MOTOR_A_PIN2, OUTPUT);
  pinMode(MOTOR_B_PIN1, OUTPUT);
  pinMode(MOTOR_B_PIN2, OUTPUT);

  server.addServer(80); // WEBサーバのポートを追加
  server.setResponse(80, "/tank", &tankResponse, METHOD_GET); // ポート・パス・レスポンス内容を追加

  server.openAllServers(); // サーバー開始
}

void loop(){
  server.requestHandle(80); // アクセス確認(loop内にdelayは入れない)
} 

まずマクロで定義されているモータ制御ピンの指定は、制作環境によってモータとモータドライバの接続の関係で逆になっている可能性があるので、それぞれピンの指定を入れ変える必要があるかもしれないので注意が必要です。

今回利用したライブラリではクエリ文字列を簡単に扱えるようにしているので、戦車をクエリ文字列で制御できるようにしました。
クエリ文字列のforwardパラメータに1を渡せば前進、2を渡せば後進するにようになっています。
また、turnパラメータに1を渡せば左に旋回、2を渡せば右旋回するようになっています。
さらにデフォルトではそれぞれ1秒間動作を続けますが、動作時間を指定するために、forward_delayturn_delayパラメータでミリ秒で指定すればその秒数分動作を実行します。
動作は、コールバック関数を見てもらうとわかるように、旋回処理が先に来ていてそのあとに前進後進処理がされるのでクエリ文字列に同時に処理を記述すると旋回後に前後進します。
よって制御方法としては以下のようになります。

例1: IP/tank?forward=1&turn=1&turn_delay=3000 => 左に3秒回った後に前に1秒進む
例2: IP/tank?forward=2&turn=2&forward_delay=3000&turn_delay=1500 => 右に1.5秒回った後に後ろに3秒進む

例2の動作



おまけ

いちいちURLを編集するのは面倒なので、ブラウザから簡単に遊べるようなHTMLを作成しました。
以下のソースを適当なフォルダに保存してブラウザで開くと簡単に操作できます。

<html>
  <head>
    <title>Tank Test</title>
  </head>
  <body>
    <h1>Tank Test</h1>
    <hr>
    <label>IPアドレス</label>
    <br>
    <input type="text" value="192.168" readonly>
    <span>.</span>
    <input id="ip1" type="number" value=0>
    <span>.</span>
    <input id="ip2" type="number" value=0>
    <br>
    <br>
    <label>前後進</label>
    <input id="forward" type="range" min="0" max="2" value="1" oninput="updateDisplay()">
    <input id="forward_display" type="text">
    <br>
    <label>全後進時間</label>
    <input id="forward_delay" type="number" value="1000" required>
    <br>
    <br>
    <label>旋回</label>
    <input id="turn" type="range" min="0" max="2" value="1" oninput="updateDisplay()">
    <input id="turn_display" type="text">
    <br>
    <label>旋回時間</label>
    <input id="turn_delay" type="number" value="1000" required>
    <br>
    <br>
    <button onClick="exec()">実行</button>
    <span id="status"></span>

    <script>
      let isExec = false

      function updateDisplay(){
        let forward = document.getElementById('forward').value
        let turn = document.getElementById('turn').value

        if(forward == 0) document.getElementById('forward_display').value = "停止"
        else if(forward == 1) document.getElementById('forward_display').value = "前進"
        else if(forward == 2) document.getElementById('forward_display').value = "後進"

        if(turn == 0) document.getElementById('turn_display').value = "停止"
        else if(turn == 1) document.getElementById('turn_display').value = "左旋回"
        else if(turn == 2) document.getElementById('turn_display').value = "右旋回"
      }

      function runFetch(url, timeout){
        const controller = new AbortController()
        const signal = controller.signal

        return new Promise((res, rej) => {
          setTimeout(() => { controller.abort(); rej("Time out."); }, timeout)
          fetch(url, { signal: signal })
            .then(() => {res()})
            .catch((err) => {
              rej(err)
            })
        })
      }

      async function exec(){
        if(isExec) return 
        else{
          isExec = true
          document.getElementById('status').textContent = "実行中"
        }

        let forward = document.getElementById('forward').value
        let turn = document.getElementById('turn').value
        let forward_delay = document.getElementById('forward_delay').value
        let turn_delay = document.getElementById('turn_delay').value
        let ip1 = document.getElementById('ip1').value
        let ip2 = document.getElementById('ip2').value
        let ip = "http://192.168." + ip1 + "." + ip2
        let url = ip + "/tank?forward=" + forward + "&turn=" + turn + "&forward_delay=" + forward_delay + "&turn_delay=" + turn_delay

        if(forward != 0 && forward != 1 && forward != 2){ alert("予期しない値です。"); return; }
        if(forward != 0 && turn != 1 && turn != 2){ alert("予期しない値です。"); return; }
        if(isNaN(ip1) || isNaN(ip2)){ alert("予期しないIPです。"); return; }

        try{ await runFetch(url, 3000) }
        catch(err){ alert(err) }
        isExec = false
        document.getElementById('status').textContent = ""
      }

      updateDisplay()
    </script>
  </body>
</html> 

まとめ

・モータドライバを使用する際はESP32と電源を別にしないとマイコンが再起動する
githubからライブラリを直接使うにはplatformio.inilib_depsに追記する

コメント

このブログの人気の投稿

Phaser3 + Typescriptを使ってRPGゲームの基礎を作ろう!その2

前回の記事 に引き続きPhaser3+Typescriptを使って RPG の基礎を作っていきます。 この記事は前回の記事を呼んだ前提で説明していきますので、ぜひそちらを先に読むことをお勧めします。 また、今回の記事は前回よりも難易度と内容量が上がっていますが、記事の最後に作業後の リポジトリ のリンクがありますので、そちらを先にダウンロードしてそちらと比較しながら学習することができます。 前回まで作成した状態のプログラムが以下の リポジトリ からダウンロードできますので前回まででうまくいっていない方は参考にしてみてください。 github.com 最終目標(再掲) ・Phaser3とTypescriptで RPG ゲームの基礎を作る。 ・Phaser3をTypescriptで使う方法を学ぶ ・Phaser3の使い方を学ぶ 今回の目標 ・ゲームの作り方の断片を知る ・キャラの表示できるようにする ・キャラを移動できるようにする ・当たり判定をつける ・ NPC の追加してみる ・話しかけられるようにする 開発前提(再掲) ・Nodejsの環境・知識がある ・ Javascript ・Typescriptがある程度かける ・当ページ紹介の環境を試す場合はgit・ github の知識がある 使用した主要Nodeモジュール(再掲) ・typescript(Typescriptの コンパイル 用) ・phaser(フロントの Javascript 用ゲームライブラリ) ・live-server(ソースを監視してブラウザのページをリロード) ・ts-loader(webpackがTypescriptをバンドルする用)webpack(言わずと知れたモジュール依存をいい感じに解決しバンドルする) ・webpack- cli (webpackを コマンドライン で使用するのに必要) 注:各Nodeモジュールバージョンは後述 当ブログ仕様の画像素材の注意点 当ブログで使用する画像素材は『 ピポヤ倉庫 』より許可なしで無償再配布・改変が認められたものを改変して作成されたものです。 中には許可なく再配布・改変してはいけない素材もインターネット上には多く存在するのでそれらを使用するときは十分に規約を呼んでから使用しましょう。 1. キャラの表示 想定画面 今回は、はじめにキャラつまり操作

M5Stackで、においセンサー(TGS2450)を使ってみる。(LCDに表示編)

今回は、においセンサー(TGS2450)から取得したデータをM5Stackの LCD にグラフ表示をしていきます。 今回主に使用したもの M5Stack 10Ω抵抗 5本 Pch  MOSFET  2SJ334(スイッチとして利用) M5StackSideBB(ブレッドボード) M5Stackのピンはそのままだと配線するとき手間がかかるかと思いますので、今回はM5StackSideBBを利用しました。 ブレッドボードも付いているので、手軽に電子工作できるので個人的にオススメです。 SideBB for M5Stack www.switch-science.com 諸注意 本ブログのプログラムはArduinoIDEまたはPlatformIOでM5Stackの開発ができる状態であること前提のものですので、各自導入をお願いします。 過去に Windows のArduinoIDEで M5Stackの開発環境を構築する記事 があるので、参考にしてください。 においセンサー(TGS2450)について 今回使用したセンサーは 秋月電子通商 で購入できます。 http://akizukidenshi.com/catalog/g/gP-00989/ akizukidenshi.com TGS2450には4つのピンがあり、使用するのはそのうちの3本でヒーターを温めるピン、センサー情報を得るピン、GNDがあります。 ここで注意したいのは、ヒーター電圧は1.6Vと記述されているのでM5Stackで利用するときは分圧して3.3Vを1.6V近くまでに降圧することをお勧めします。 TGS2450のセンサーは可変抵抗になっており、データシートには5.62kΩ〜56.2kΩの間を抵抗が変化し、においが強いとより低くなると記述されていました。 よって、センサーの値をM5Stackで取得するにはセンサー抵抗と外部に接続する任意の値の抵抗による分圧回路によって求めることができます。 TGS2450の動作方法は、250ms周期でセンサーに電圧を5ミリ秒on、245ミリ秒offの状態にしてヒーターは8ミリ秒on、242ミリ秒offにすることで値を取得します。 配線 においセンサーとM5Stackの配線 上の図が配線図です。 30Ωの抵抗は10Ωの抵抗を三つ直列に接続して作成しました。 また、上部の抵抗10Ω二

Phaser3 + Typescriptを使ってRPGゲームの基礎を作ろう!その1

今回はPhaser3とTypescriptを使って簡単な RPG ゲームを作る方法を紹介していきます。 内容はPhaser3およびゲーム作りについての記事なので、Nodejsの周辺モジュールなどの説明は一部省いての説明になりますのでご了承ください。 またこの記事では Phaser2 ではなく Phaser3 を使用するので注意してください。 この記事は二部構成になりますので、この記事を読んだ際はぜひ次の記事も読むことをお勧めします。 最終目標 ・Phaser3とTypescriptで RPG ゲームの基礎を作る。 ・Phaser3をTypescriptで使う方法を学ぶ ・Phaser3の使い方を学ぶ 今回の目標 ・開発環境を整える ・Phaser3の開発構成を知る ・スタート画面を作る ・マップ表示をさせる 開発前提 ・Nodejsの環境・知識がある ・ Javascript ・Typescriptがある程度かける ・当ページ紹介の環境を試す場合はgit・ github の知識がある 使用した主要Nodeモジュール ・typescript(Typescriptの コンパイル 用) ・phaser(フロントの Javascript 用ゲームライブラリ) ・live-server(ソースを監視してブラウザのページをリロード) ・ts-loader(webpackがTypescriptをバンドルする用)webpack(言わずと知れたモジュール依存をいい感じに解決しバンドルする) ・webpack- cli (webpackを コマンドライン で使用するのに必要) 注:各Nodeモジュールバージョンは後述 1. 最低限の開発環境の準備 今回最低限の環境を整えるために、『Typescript + Phaser3』の開発テンプレートを github リポジトリ で公開しました。 以下からZIPをダウンロードするか、 git clone コマンドで各自環境に展開してみてください。 ここから先は リポジトリ のプログラムを元に説明していきます。 github.com 展開するとファイル構造は以下のようになっているかと思われます。 注: 他にもファイルやフォルダがあるかと思われますが、表記されているのは今回使うものになっています。 - src/ (これから書くプログラムの保存領域)