ESP32でサーボモータをWEBサーバから動かす(はじめてのESP32)

ESP32は上海の会社espressif systemsが開発したマイコンです。製造もTSMCで完全中国製マイコンになります。

ESP32とは?

Wifi、Bluetooth(Classic + BLE)が使えて、32bitのデュアルコア(XTensa)で最大動作240MHzとハイスペックなマイコンです。Arduino IDEにも対応していて、プログラム開発も簡単にできます。

しかも、UNOよりも小型でしかも価格も安い。同じespressif社が販売している開発ボード ESP32-DevKitCなら1500円ぐらいで手に入ります。UNOが3000円ぐらいなので、およそ半額ですね。

はやくて、安い…なんか牛丼屋みたい。

唯一、UNOの方が書籍やインターネットで情報が手に入りやすいというみたいなメリットはありますが、NXPのARMマイコンで開発している自分にとっては、情報が少ない?は?十分あるじゃんって感じなレベルなので、調べれば何かしら答えが見つかります。

今回やること

はじめてということで、とりあえずチュートリアルとして良さそうな題材を探しました。

ESP32は、海外のサイトの方が情報が豊富で、開発の参考になるのがこの辺でしょうか。この記事もこの二つの記事を参考にしています。

RANDOM NERD TUTORIALS

160+ ESP32 Projects, Tutorials and Guides with Arduino IDE​ | Random Nerd Tutorials

Discover all our ESP32 Guides with easy to follow step-by-step instructions. Each tutorial includes circuit schematics, source code, images and videos.

DRONEBOT WORKSHOP

Using Servo Motors with the ESP32

Today we’ll see a few ways of interfacing servo motors to the ESP32 microcontroller and controlling them with code, with a potentiometer, and over WiFi. We have already looked at both the ESP32 microcontroller and at using analog servo motors , and today we’ll put both of them together.

手持ちでサーボ―モータがあったのと、ESP32といえば、無線機能ということで、WIFIをつかって、Webサーバからサーボモータを駆動するようなサンプルをしてみたいと思います。

基本、サンプルコードベースですが、ちょっと工夫して、オリジナリティを出していきたいと思います。

で、最終的に出来ること

こんな感じで、タブレット端末からポケモンが無線で動かせます。

かわいいポケモンたちがサーボ―モータにのっかりました
スライダーやラジオボタンでもキビキビ動きます

ちょっとオリジナリティの出す方向性を間違えてますが、作業をざっくりまとめていきます。

使用した開発ボード

マイコンの開発元であるESPRESSIFが販売している開発ボード ESP32-DevKitCを使いました。お値段は1000円前後。

ちなみにESP32というと、いったい何を指しているのかわからなくなる時があります。というのは、写真のような開発ボード(今回つかうのは、ESP32-DevKitC)を指すのか、無線機能とマイコンをセットにしたモジュール(ESP32-DevKitCでは、ESP-WROOM-32)のことか、または、コントローラ自体のことなのか(ESP-WROOM-32では、ESP32-D0WDQ6)、それぞれに種類があって、混乱するからです。

この記事ではESP32というと開発ボードを含めたシステム全体を指すようにします。

ちなみに余談ですが、WIFIとかBluetoothとかの電波をつかう機器を日本で使うには、混信を防ぐために国が許可したものしか使ってはいけません。一般的に技適というやつです。

ESP32でも使用しているモジュールによっては、技適がないものもあるので、注意が必要です。

見分け方ですが、許可されたものはこんなマークがついてるはずです。

もちろんESP32-WROOM-32には技適マークがついてます。

こんな感じに。

使用するサーボモータ

ホビー用途でよくつかわれてるSG90というのを使います。上のプロペラみたいのが180deg回転します。同じサイズで、360deg回るのもありますね。

写真のは純正ではないです。純正はTowerProという会社が製造してます。

電源は5Vで、黄色の線にPWM信号を入れて、モータを制御します。茶色はグランド。

ESP32をArduino IDEで使うための設定

espressif社が開発しているESP-IDFというのがオフィシャルな開発環境とおもわれますが、とりあえず、手っ取り早く動かすにはArduino IDEを使うのがベストです。

ESP32をArduino IDEで使うには、ボードマネージャでライブラリをまずインストールする必要があります。

Arduino IDEを起動して、ファイルから環境設定をクリックします。

下の追加のボードマネージャのURLの箇所に下のリンクをコピペします。

https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json

次に、下のツールバーからボードマネージャをクリックします。

下のテキストボックスにesp32と打つと、ライブラリがリストアップされるので、最新バージョン(この記事を書いたときは1.04が最新)をインストールします。

ツールからボードの設定をESP32 ArduinoのWrover Moduleにします。

IDE側の設定は以上です。ESPをPCに繋ぎます。デバイスマネージャを開いてみて、下図のようにデバイスがうまく認識されていない場合は、

CP210xのドライバのインストールが必要です。ESP32はUSB-UARTブリッジとしてCP2102Nというチップ利用していて、VCPというドライバをいれると、COMとして認識されるようになります。

下のサイトからダウンロードできます。

CP210x USB to UART Bridge VCP Drivers – Silicon Labs

The CP210x USB to UART Bridge Virtual COM Port (VCP) drivers are required for device operation as a Virtual COM Port to facilitate host communication with CP210x products. These devices can also interface to a host using the direct access driver.

うまくいくと下のようにCOMとして、認識されます。

Arduino IDEでビルドするときは、忘れずにポート番号の設定をここで認識した番号で設定しましょう。

Lチカで動作確認

とりあえず、Lチカで動作を確認します。どこでもいいですが、下のpinoutを確認して、今回は#18pinのGPIOにLEDをぶら下げることにします。

スケッチはArduinoと全く同じ関数が使えます。

// LED on GPIO18
int ledPin = 18;

void setup()
{
    // #18ピンをデジタル出力に設定
    pinMode(ledPin, OUTPUT);
    // シリアルモニタ設定(ボーレートは115200bps)
    Serial.begin(115200);
}

void loop()
{
    Serial.println("LED ON");
    digitalWrite(ledPin, HIGH);
    delay(500);
    Serial.println(" LED OFF");
    digitalWrite(ledPin, LOW);
    
    delay(500);
}

動作はこんな感じです。

WEBサーバ経由でLチカ

これもESP32のイントロでよく扱われていますが、トレースしていきます。WIFIを使う時には、下の二通りの方法があります。

・Wifiルータをアクセスポイントとして、ESP32をぶらさげる

・ESP32自体をアクセスポイントとして機能させる。

後者はインターネットにつなぐ必要がない場合にローカル環境で、ESP32とスマホとかPCとかと連動させるときに使えますが、今回はネット経由で制御したいので、前者の方法でいきます。一般的には、これをStation (STA)モードといいます。

STAのサンプルコードを下のように開きます。サンプル名がSimpleWifiServerというやつです。

このプログラムを実際に動作させるには、コード内で使用するwifiのSSIDとパスワードを設定しておく必要があります。

スケッチの30-31行目あたりのこれです。yourssidとyourpasswdを自分のWifiルータの環境に合わせて設定します。

const char* ssid     = "yourssid";
const char* password = "yourpasswd";

自分の場合は、#18にLEDをつないでいるので、ピン番号の設定も下のようにかえてます。

pinMode(18, OUTPUT);  //setup関数内の38行目 set the LED pin mode

if (currentLine.endsWith("GET /H")) {  //メインループの105行目あたり
   digitalWrite(18, HIGH);               
}
if (currentLine.endsWith("GET /L")) {
   digitalWrite(18, LOW);         // GET /L turns the LED off
}

プログラムを実行して、シリアルモニタを確認するとESP32のIPアドレスが表示されるので、http://xxx.xxx.xxx.xxxとして、webブラウザからアクセスすると下のような感じLEDのON/OFFを制御できるようになります。

ブラウザ上はpin5と書いてますが、ミスです。実際は18で制御してます。

サンプルコードの中身をちょっとみる

サンプルコードを見ていくと、ESP32がWEBサーバになって、スマホなどからのリクエストがあった場合にホームページの情報を返すようになってます。

いわゆるhttp通信というやつですね。

実行して、スマホでESP32に接続した時のモニター結果がこれです。

2~3行目にある GET /HTTP/1.1とHost:192.168.3.32 (ESP32のIPアドレス)が実際にブラウザからESP32に送られたリクエストメッセージになります。

内容はHTTPバージョン1.1のルールに則り、指定したIPのウェブページをGETしたいんだけど、どうかな?というものです。

次にそれを受けたESP32側は、下のコードでもって、ブラウザにこう返してます。

            client.println("HTTP/1.1 200 OK");
            client.println("Content-type:text/html");
            client.println();

ステータス200 で問題ありません。OKです。html形式で情報を今から送りますよ、と。

Arduino IDEでは、client.prinlnというメソッドをつかえば、HTTPリスポンスをこうやって、実現できます。その後、改行を一回して、次に続きます。

 client.print("Click <a href=\"/H\">here</a> to turn the LED on pin 5 on.<br>");

client.print("Click <a href=\"/L\">here</a> to turn the LED on pin 5 off.<br>");

実際、スマホから見るWEBページの正体はこれです。client.printを除けば、htmlで作成されたホームページと変わりません。

実際、中身だけコピーすると、

See the Pen
LED
by Michinori Takeuuchi (@michi1982)
on CodePen.

こんな感じで、普通にhtmlファイルとして、動作していることが分かります。つまり、EPS32がhtmlファイルをブラウザに送信する箇所をカスタマイズしていけば、自分の思い通りのページを作成することが出来るわけです。

もちろん内容によっては、html, CSS, java scriptなどの言語を知っとかないと作れませんが、その手の情報は副業ブームのせいか、無限にあるので、興味がある方は勉強してみてもいいかもしれません。

凝ったことをしなければ、そんな難しいものではないです。

一応、別記事で超基本的なものをまとめてみましたので、参考にしてください。

サーボモータを動かす。

さて、LEDはWebサーバ経由で制御が何となくできたので、次はサーボモータを動作させます。

サーボモータ用のライブラリがESP32用に公開されているので、それをまず、Arduino IDEにインストールするところから始めます。

ツールからライブラリを管理をクリック。

下のようにESP32servoと検索するとライブラリがリストされるので、これをインストールします。

サーボモータを動かすサンプルを実行します。ESP32Servoフォルダ内のsweepを開きましょう。

サンプルコードを動かす為に、下図のようにESP32とサーボモータをつなぎます。

サンプルコードは下のとおりです。

#include <ESP32Servo.h> //ESP32サーボモータのライブラリ

Servo myservo; //Servoクラスのオブジェクト化
int pos = 0;      
int servoPin = 18; //PWM信号を出力するピン

void setup() {
	// Allow allocation of all timers
	ESP32PWM::allocateTimer(0); //タイマをPWM信号に使用する
	ESP32PWM::allocateTimer(1);
	ESP32PWM::allocateTimer(2);
	ESP32PWM::allocateTimer(3);

	myservo.setPeriodHertz(50); //PWM周波数を50Hzにする
	myservo.attach(servoPin, 1000, 2000); //#18をPWM信号ピンに設定

}

void loop() {
	for (pos = 0; pos <= 180; pos += 1) {
		// in steps of 1 degree
		myservo.write(pos);
		delay(15);
	}
	for (pos = 180; pos >= 0; pos -= 1) {
		myservo.write(pos);
		delay(15);
	}
}

プログラムの動作はサーボモータを0deg~180deg間で15msec周期に1degずつ変化させていっています。サーボモータは#18pinから出力されているPWM信号で制御されています。

PWM信号で重要なのが、制御周期とDUTYですが、それはこのライブラリでは下のように定義しています。

	myservo.setPeriodHertz(50); //PWM周波数を50Hzにする
	myservo.attach(servoPin, 1000, 2000); 
//第1引数にpwm信号を出力するピン番号を指定、
//第2と第3引数に0degと180degのパルス幅をいれる(usec)

そして、writeメソッドに目標の角度をわたすことで、裏側では、attachしたパルス幅でpwm制御をしてくれるわけです。

myservo.write(pos);

ただ、パルス幅の設定は、default値だと、180degまで稼働しないので、修正が必要です。

myservo.attach(servoPin, 1000, 2000)
=>50degぐらいしか動いてない
myservo.attach(servoPin, 250, 3000);
=>180deg動いた

WEBサーバを作成する

サーボモータは動きましたので、webサーバ用のページをhtmlで作成します。詳細は別記事まとめていますので、ここでは、こんなの作ったよ程度の内容です。

See the Pen
ESP32_WebServo
by Michinori Takeuuchi (@michi1982)
on CodePen.

Ajaxっていう通信方法で、ユーザがブラウザで操作した値をESP32が拾っています。ここら辺の手法が理解できれば、ESP32で取得したセンサをブラウザでみるのも今回みたいにアクチュエータをブラウザ経由で動かすみたいなことも簡単にできます。

んで、この作ったhtmlファイルをLEDのときみたいに

client.println("<!DOCTYPE html>");
client.println("<html lang=\"ja\">");//以降、ずーっとつづく

に変換して、スケッチにコピペすれば、上のデモみたいなページがESP32で作れます。

が、これ結構面倒です。たとえば二行目とかはhtml内でダブルクオーテーションとかつかってますが、print文にするときは、エスケープ文字に変換しないといけません。

簡単に変換マクロをつくりましたので、使ってください。(すいませんがmacは動作未確認)

htmlを読み込んで>>ボタンで変換して、コピーをクリック

動かなかったらすみませんが、簡単ですので、自分で作ってください。

最終的なソース

は、以下のようになります。サーボモータのサンプルとLチカのサンプルを組み合わせています。client.printlnがガーってなってるところは上のツールでhtmlから変換したソースをコピペしている箇所になります。

#include <WiFi.h>
#include <ESP32Servo.h>

Servo myservo;  // create servo object to control a servo

// Servo GPIO pin
static const int servoPin = 18;

// Network credentials
const char* ssid     = "10B1F88E5DB9-2G";
const char* password = "5530117932294";

// Web server on port 80 (http)
WiFiServer server(80);

// Variable to store the HTTP request
String header;

// Decode HTTP GET value
String valueString = String(5);
int pos1 = 0;
int pos2 = 0;

// Current time
unsigned long currentTime = millis();
// Previous time
unsigned long previousTime = 0; 
// Define timeout time in milliseconds (example: 2000ms = 2s)
const long timeoutTime = 2000;

void setup() {
  
  // Allow allocation of all timers for servo library
  ESP32PWM::allocateTimer(0);
  ESP32PWM::allocateTimer(1);
  ESP32PWM::allocateTimer(2);
  ESP32PWM::allocateTimer(3);
  
  // Set servo PWM frequency to 50Hz
  myservo.setPeriodHertz(50);
  
  // Attach to servo and define minimum and maximum positions
  // Modify as required
  myservo.attach(servoPin,650, 2600);
  
  // Start serial
  Serial.begin(115200);


  // Connect to Wi-Fi network with SSID and password
  Serial.print("Connecting to ");
  Serial.println(ssid);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  // Print local IP address and start web server
  Serial.println("");
  Serial.println("WiFi connected.");
  Serial.println("IP address: ");
  Serial.println(WiFi.localIP());
  server.begin();
}

void loop(){
  
  // Listen for incoming clients
  WiFiClient client = server.available();   
  
  // Client Connected
  if (client) {                             
    // Set timer references
    currentTime = millis();
    previousTime = currentTime;
    
    // Print to serial port
    Serial.println("New Client."); 
    
    // String to hold data from client
    String currentLine = ""; 
    
    // Do while client is cponnected
    while (client.connected() && currentTime - previousTime <= timeoutTime) { 
      currentTime = millis();
      if (client.available()) {             // if there's bytes to read from the client,
        char c = client.read();             // read a byte, then
        Serial.write(c);                    // print it out the serial monitor
        header += c;
        if (c == '\n') {                    // if the byte is a newline character
          // if the current line is blank, you got two newline characters in a row.
          // that's the end of the client HTTP request, so send a response:
          if (currentLine.length() == 0) {
        
            // HTTP headers always start with a response code (e.g. HTTP/1.1 200 OK) and a content-type
            client.println("HTTP/1.1 200 OK");
            client.println("Content-type:text/html");
            client.println("Connection: close");
            client.println();

            // Display the HTML web page
            
client.println("<!DOCTYPE html>");
client.println("<html lang=\"ja\">");
client.println("<head>");
client.println("   <title>Wifi_Servo</title>");
client.println("   <meta charset=\"utf-8\">");
client.println("   <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"/>");
client.println("");
client.println("    <style>");
client.println("    h1 {color:black;background: lightsteelblue ;font-family:\"BIZ UDゴシック\";font-size: 150%; text-indent: 1em}");
client.println("    h2 {color:black;font-family:\"BIZ UDゴシック\";font-size: 120%; text-indent: 2em}");
client.println("    span {color:grey; font-family:\"BIZ UDゴシック\";font-size: 150%}");
client.println("    p{text-indent:2em}");
client.println("");
client.println("");
client.println("    .radio-area{text-indent:3em}");
client.println("    .angle{text-indent:10em}");
client.println("    .radio_item{display: block; color: grey}");
client.println("");
client.println("    .slider {");
client.println("    -webkit-appearance: none;");
client.println("    width: 350px;");
client.println("    height: 25px;");
client.println("    border-radius: 10px;");
client.println("    background: #008000;");
client.println("    outline: none;");
client.println("    opacity: 0.6;");
client.println("    -webkit-transition: .2s;");
client.println("    transition: opacity .2s;");
client.println("  }");
client.println("");
client.println("    </style>");
client.println("    <script src=\"https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js\"></script>");
client.println("</head>");
client.println("<body>");
client.println("<h1>ESP32をwebサーバ経由で制御するページだよ</h1>");
client.println("");
client.println("<div class=\"radio-area\">");
client.println("    <label class=\"radio_item\" id=\"pk_lb\"><input type=\"radio\" name=\"mode\" id=\"pika\" value=\"pk\" checked> ピカチュー こっちむいて</label>");
client.println("    <label class=\"radio_item\" id=\"fs_lb\"><input type=\"radio\" name=\"mode\" id =\"fusi\" value =\"fs\"> フシギダネ こっちむいて</label>");
client.println("    <label class=\"radio_item\" id=\"fr_lb\"><input type=\"radio\" name=\"mode\" id =\"free\" value =\"fr\"> ピカチューとフシギダネは自由な時間をすごします</label>");
client.println("</div>");
client.println("");
client.println("<h2>スライダーでも動かしてみよう</h2>");
client.println("  <input type=\"range\" min=\"0\" max=\"180\" class=\"slider\" id=\"servoSlider\" onchange=\"servo(this.value)\"/>");
client.println("</form>");
client.println("<h2>Current Your Target Angle is ..</h2>");
client.println("    <div class=\"angle\"><font size = \"7\"><span id=\"servoPos\"></span></font>deg</div>");
client.println("<script>");
client.println("  var slider = document.getElementById(\"servoSlider\");");
client.println("  var servoP = document.getElementById(\"servoPos\");");
client.println("  servoP.innerHTML = slider.value;");
client.println("  slider.oninput = function() {");
client.println("    slider.value = this.value;");
client.println("    servoP.innerHTML = this.value;");
client.println("  }");
client.println("");
client.println("  $(function(){");
client.println("     $( 'input[name=\"mode\"]:radio' ).change( function() {");
client.println("        var sel = $(this).val();");
client.println("        if(sel == \"pk\")");
client.println("        {");
client.println("          servoP.innerHTML = 180;");
client.println("          slider.value=180;");
client.println("          servo(slider.value);");
client.println("        }");
client.println("        if(sel == \"fs\")");
client.println("        {");
client.println("          servoP.innerHTML = 0;");
client.println("          slider.value=0;");
client.println("          servo(slider.value);");
client.println("        }");
client.println("        if(sel == \"fr\")");
client.println("        {");
client.println("");
client.println("        }");
client.println("     });");
client.println("  });");
client.println("");
client.println("  function freeControl(){");
client.println("");
client.println("    if(\"fr\"==$('input:radio[name=\"mode\"]:checked').val())");
client.println("    {");
client.println("      servoP.innerHTML = Math.round( Math.random()*180 );");
client.println("      slider.value=servoP.innerHTML;");
client.println("      servo(slider.value);");
client.println("    }");
client.println("  }");
client.println("");
client.println("    setInterval(freeControl, 1000);");
client.println("    $.ajaxSetup({timeout:1000});");
client.println("    function servo(pos) {");
client.println("      $.get(\"/?value=\" + pos + \"&\");");
client.println("      {Connection: close};");
client.println("    }");
client.println("</script>");
client.println("</body>");
client.println("</html>");

            // GET data
            if(header.indexOf("GET /?value=")>=0) {
              pos1 = header.indexOf('=');
              pos2 = header.indexOf('&');
              
              // String with motor position
              valueString = header.substring(pos1+1, pos2);
              
              // Move servo into position
              myservo.write(valueString.toInt());
              
              // Print value to serial monitor
              Serial.print("Val =");
              Serial.println(valueString); 
            }         
            // The HTTP response ends with another blank line
            client.println();
            
            // Break out of the while loop
            break;
          
          } else { 
            // New lline is received, clear currentLine
            currentLine = "";
          }
        } else if (c != '\r') {  // if you got anything else but a carriage return character,
          currentLine += c;      // add it to the end of the currentLine
        }
      }
    }
    // Clear the header variable
    header = "";
    // Close the connection
    client.stop();
    Serial.println("Client disconnected.");
    Serial.println("");
  }
}

以上です。実際、むちゃくちゃなコードですが、一応動作します。client.println()の連打で可読性が最悪です。

実は、SPIFFSというのを利用して、htmlファイルを別にESP32のフラッシュ上に保存して、実行時に読み込む方法がありますが、そっちのほうが実際には一般的です。

それはそれとして、また別の記事でまとめたいと思います。とりあえず今回は、ページがつくれたということで、ハッピーでした。

ばんざい ESP32 🙂

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です