ESP32でプラレールを操作してみた

前回、Webサーバからサーボモータを動かしたので、今回はDCモータを動かしてみます。モータ単体だと、つまらないので、子供が愛してやまないプラレールを動かして、電車でGO的なものをめざします。

テスト車両は、この京成スカイライナー。かっこいいぜ。

これまでの記事で、ESP32をWEBサーバ経由で制御する方法やhtmlで制御画面を作り込む方法などを紹介してきました。

今回の内容はそれらから特に新しいことはありません。

モータの制御もサーボモータがDCモータになっただけで、プログラムもほぼ変わりません。

ただ、これまでマイコンの電源をPCから、サーボモータは安定化電源からとっていたものをどうするか?この課題が今回の記事のメインテーマになります。

プラレールをWifi経由で動かすのにしたこと。

  1. リチウムイオン電池をマイコンとDCモータの電源として利用する
  2. DCモータドライバをつかって、プラレールのモータを動かす
  3. プラレールの運転画面をWEBサーバ上につくって、電車を制御する

最終的には

こんな感じで動きました。

一応動いています。改善の余地がない訳ではないですが(後述)、とにかく、子供が超よろこびました。お子さんがいる方はやってみては?

別動画。こんな感じで、実際の電車みたく、ゆっくり止まれます。

電源

プラレールは三両編成ですが、先頭車両はモータとギアボックスで占拠されていて、あまりスペースがないので、実質、後ろ二両で何とかする必要がありました。

ドライバとマイコンの電源はノイズ対策で、本当は別電源にしたかったのですが、そうすると、スペース的に苦しいので、しょうがなく同じ電源を使います。

最終車両が電源車両、真ん中車両が制御車両、先頭車両が駆動車両とします。

なので、一両に搭載できて、なるべく大容量なのを探すと、結局リチウムイオンを使うのがベストっぽいです。今回は下の単3電池より一回り大きいサイズの18650を使いました。

KeepPower社製、これで電池容量が3500mA。2980円ぐらい。

WIFIは結構電流消費が大きいです。ESP32の場合だと、モータなしでもWIFI接続等で最大で200mAぐらいは、消費します。モータをマックスDutyで動かすと合計で500mAぐらい行くので、全開走行で、トータル1Aちかくは消費しそうです。

プラレールを全開走行で、何時間も遊ぶととは思えないので、電池容量はこれでよしとして、電池ホルダに入れて、車両にセットしました。

ちなみに電池の充電なのですが、スイッチサイエンスから出ているUSBから充電できるボードを使いました(900円ぐらい)。ジャンパピンの設定で、100mAか370mAかを切り替えられます。

コネクタは日本圧着端子が出してる、PHコネクタです。この手の充電器にはメジャーに使用されてます。似たようなのにXHコネクタがありますが、ピッチが違います。XHは2.54mmピッチでPHが2.0mmピッチです。

図面だと、MicrochipのMCP7831というリチウムイオン充電用のICをつかっていて、そのICの仕様書をみると、バッテリ電圧が4.2Vで満充電になるようですね。

さらに、内部的に充電モードには、3モードあるようで、

4.2Vの66.5%つまり2.8V未満だと、ジャンパで設定した電流の10%で充電するプリコンディショニングモードになり、電圧が2.8V以上になると、設定電流で充電するファストチャージモードになるみたいです。

そして、4.2Vになると定電圧モードになって、電流は流さず、電圧が下がるとファストチャージモードに再びはいって、再充電といった感じで、満充電状態を保持しようとします。

ちょっと上の回路図がVDDとPROGが逆のような感じがするけど、まぁ気にしません。

モータドライバ

ドライバはルネサスのTB67H450FNGを使用しました。marutsuでDIP化した基板が売ってましたので、それを使います。

*DCモータを駆動するには、モータドライバというものが必要です。マイコン自体がモータを駆動する程の大電流を流せないからです。ドライバは、マイコンからのPWM信号を受けて、トランジスタをON/OFFさせます。トランジスタのソース(ここでいうとVM)は別電源で駆動されるので、大きな電流を流せます。

先ほど、紹介した電池も使って、モータを駆動する回路をつくると、こんな感じになりました。机上テストでは、心配していたノイズは影響がありませんでした。ドライバの他に二つのICをつかってますが、これはレギュレータです。マイコンの電源3.3V系とドライバの5V系をつくっています。

レギュレータ

リチウムイオンの電源が3.7Vなので、ドライバの定格5Vにやや足らないす、5Vに昇圧するためにスイッチングレギュレータを使いました。以前記事で紹介したこいつです。

5V出力で、昇圧と降圧ができるものを利用しました。やや高いですが、よくある三端子レギュレータより、高効率です。

一方、はじめは、ESP32の電源供給をレギュレータを介さずに、3V3のピンに直接供給しようとしていたのですが、リチウムイオンが満充電だと、4.2Vになるため、ESPの電源供給の3V3のピンが3.6Vまでしか許容電圧をオーバしてしまうことを後で気づき、これにもレギュレータを追加しました。

pololuの同じ製品シリーズでS9V11MACMAというのを使いました。これは出力電圧を調整することができるので、便利です。outputと書いてある方のポテンショを調整して、出力電圧を3.3Vに設定します。(下の写真からはみえませんが、肌色のboxの側面に調整ねじがあります)

ENのピンをhighにすると、カットオフ電圧を調整できるみたいですが、今回は特に調整はしてません。電池が消耗して、電圧降下してきたら出力をわざと落として、システム停止みたいなことができます。

モータ制御用画面(html)

プラレールを運転する制御画面ですが、下のソースをベースに作りました。

Making a speedometer using HTML5’s Canvas

The canvas element is part of HTML5. It allows for dynamic, scriptable rendering of 2D shapes. It allows a developer to draw just as an artist would on their canvas; albeit, without the same creative flare an artist may have. As I’m a developer I couldn’t resist having an attempt.

こんなのが完成しました。後ほど説明しますが、Arduino IDEでは、PWMのdutyを8bitの数値で指示するようで、ここでは、UP/DOWNボタンで0-255のDutyを指示できるようにしました。ラジオボタンで正転逆転の切り替えをします。

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

このページから実際はAjax通信で、目標Dutyと回転方向の設定をESP側に渡しています。

Arduino IDE ソースコード全体

とりあえず、今回のコードを貼っておきます。前回にくらべ、アニメーションによって、htmlのコードが増えたので、可読性が結構低下しました。

コメントにhtmlのコードの範囲を書いてますが、実際ほとんど、htmlです。

前回記事でも触れましたが、client.printlnという関数で、ESP32とブラウザ間のhttp通信を実現してます。この関数を使って、htmlコードを強引にソースコード内に埋め込んでいる形です。

ESP32のフラッシュにhtmlファイルを直接保存して、そこからデータを読み込むこともできます。大規模になってくるとこっちのほうがスマートですね…次回からそうします。

プログラムでモータへの出力ピンが最初の手書きの回路図と違っていますので注意してください。後述する問題に対応したため、ピンが最初の設定から変わってしまいました。

#include <WiFi.h>

// Network credentials
const char* ssid     = "******"; //ここは自分のWIFI環境に合わせて設定する
const char* password = "******";

// 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 = 20000;

// Motor制御信号用ピン
int motorpin_pos = 27; //正転 モータ端子への接続次第なので、一概に正転とはいえない
int motorpin_neg = 14; //逆転

// プラレールマスコン
int target_duty =0; //目標DUTY 8bitで指示するので0-255の値をとる
bool inv_rotation = false; //正転か逆転、真なら逆転させる

// PWM
const int freq = 10000;   //PWM周波数 Hz
const int pwmChannel_pos = 0; //channel 0
const int pwmChannel_neg = 1; //channel 1

const int resolution = 8; //分解能8bit


void setup() {
  pinMode(motorpin_pos, OUTPUT); //PWM信号を出力するピン 正転用
  pinMode(motorpin_neg, OUTPUT); //逆転用
  
  // configure LED PWM functionalitites
  ledcSetup(pwmChannel_pos, freq, resolution); //PWM信号生成用メソッド 分解能と制御周波数とチャンネル番号を指定する
  ledcSetup(pwmChannel_neg, freq, resolution);
  
  // attach the channel to the GPIO to be controlled
  ledcAttachPin(motorpin_pos, pwmChannel_pos); //PWMのチャンネルと出力ピンをひもづける
  ledcAttachPin(motorpin_neg, pwmChannel_neg);

  ledcWrite(pwmChannel_pos, 0); //duty0をだす
  digitalWrite(motorpin_neg,LOW); // 逆転側はLOWにしとく

  
  // 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(){

  if (counter%100000==0)
  {
  Serial.print("counter =");
  Serial.println(counter);
  }
  // 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();
/////////////////////////////////////////////////////////////////////////////////////////////////
// ここからhtmlファイル
/////////////////////////////////////////////////////////////////////////////////////////////////

client.println("x<!DOCTYPE html>");
client.println("<html lang=\"ja\">");
client.println("  <head>");
client.println("    <meta charset=\"utf-8\">");
client.println("    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">");
client.println("    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">");
client.println("    <title>ESP32 on the rail</title>");
client.println("");
client.println("    <style>");
client.println("    footer{font-size: 10px;background-color: #ffffff;font-family: \"Comic Sans MS\"; margin: 5em 0 0 13em;}");
client.println("");
client.println("");
client.println("    </style>");
client.println("      <link rel=\"stylesheet\" href=\"https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css\">");
client.println("");
client.println("      <script src=\"https://code.jquery.com/jquery-3.2.1.min.js\"></script>");
client.println("      <script src=\"https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js\"></script>");
client.println("");
client.println("  </head>");
client.println("");
client.println("<body onload='draw(0);'>");
client.println("");
client.println("  <div class=\"container\" >");
client.println("    <h1> ESP32 on the Rail</h1>");
client.println("      <div class=\"row\">");
client.println("        <div class=\"col-sm-6\">");
client.println("          <div class=\"card\">");
client.println("            <div class=\"card-body\">");
client.println("              <h4 class=\"card-title\">DC motor controller</h4>");
client.println("                <canvas id=\"speedometer\" width=\"400\" height=\"200\">Canvas not available.</canvas>");
client.println("");
client.println("              <p class=\"card-text\">");
client.println("                プラレールを制御するコントロール画面。このメータはモータデューティを表示していて、");
client.println("                下のボタンで増減を調整します。トグルSWで前進・後退が切り替えられます。");
client.println("              </p>");
client.println("              <br>");
client.println("              <fieldset style=\"margin:0 0 0 2em\">");
client.println("                <p> 進行方向切り替えSW </p>");
client.println("                <input id=\"item-1\" class=\"radio-inline__input\" type=\"radio\" name=\"accessible-radio\" value=\"se1\" checked=\"checked\"/>");
client.println("                <label class=\"radio-inline__label\" for=\"item-1\">");
client.println("                    Forward");
client.println("                </label>");
client.println("                <input id=\"item-2\" class=\"radio-inline__input\" type=\"radio\" name=\"accessible-radio\" value=\"se2\"/>");
client.println("                <label class=\"radio-inline__label\" for=\"item-2\">");
client.println("                    Backward");
client.println("                </label>");
client.println("                <br>");
client.println("                <button type=\"button\" id=\"upbtn\" class=\"btn btn-success\" style=\"margin : 0 0 0 5em\"><i class=\"glyphicon glyphicon-upload\"></i> UP</button>");
client.println("                <button type=\"button\" id=\"stopbtn\" class=\"btn btn-danger\"><i class=\"glyphicon glyphicon-pause\"></i>brake</button>");
client.println("                <button type=\"button\" id=\"dwnbtn\" class=\"btn btn-warning\"><i class=\"glyphicon glyphicon-download\"></i>DOWN</button>");
client.println("              </fieldset>");
client.println("              <br>");
client.println("            </div>");
client.println("          </div>");
client.println("");
client.println("");
client.println("          </div>");
client.println("          <div class=\"col-sm-6\"></div>");
client.println("      </div>");
client.println("      <footer>");
client.println("        <p> ぼくのマイコン開発のメモ Copyright 2020</p>");
client.println("      </footer>");
client.println("  </div>");
client.println("<script>");
client.println("  var DutyArr = [0, 100, 150, 200, 215, 255];");
client.println("  var mascon=0;");
client.println("  var bBackward = 0;");
client.println("  var TarDuty=DutyArr[mascon];");
client.println("");
client.println("  $( 'input[name=\"accessible-radio\"]:radio' ).change( function() {");
client.println("//      document.getElementById(\"stopbtn\").click();");
client.println("      var val = $(this).val();");
client.println("      switch (val){");
client.println("        case 'se1':");
client.println("          bBackward =0;");
client.println("          break;");
client.println("        case 'se2':");
client.println("          bBackward =1;");
client.println("          break;");
client.println("      }");
client.println("      TarDuty=0;");
client.println("      DCTar(TarDuty,bBackward);");
client.println("      delay(500);");
client.println("  });");
client.println("");
client.println("  $('#upbtn').on('click', function() {");
client.println("      var temp=mascon+1;");
client.println("      if(temp>5){mascon =5;}");
client.println("      else{mascon = temp;}");
client.println("");
client.println("      TarDuty = DutyArr[Math.abs(mascon)];");
client.println("      console.log(TarDuty);");
client.println("      drawWithInputValue(TarDuty);");
client.println("      DCTar(TarDuty,bBackward);");
client.println("");
client.println("      enableRadio(TarDuty);");
client.println("");
client.println("  });");
client.println("  $('#dwnbtn').on('click', function() {");
client.println("    var temp = mascon -1;");
client.println("    if(temp<0){mascon=0;}");
client.println("    else{mascon = temp;}");
client.println("");
client.println("    TarDuty = DutyArr[Math.abs(mascon)];");
client.println("    console.log(TarDuty);");
client.println("    drawWithInputValue(TarDuty);");
client.println("    DCTar(TarDuty,bBackward);");
client.println("");
client.println("    enableRadio(TarDuty);");
client.println("  });");
client.println("");
client.println("  $('#stopbtn').on('click', function() {");
client.println("    mascon=0;");
client.println("    TarDuty=0;");
client.println("    DCTar(TarDuty,bBackward);");
client.println("    drawWithInputValue(0);");
client.println("");
client.println("    enableRadio(TarDuty);");
client.println("  });");
client.println("");
client.println("  function enableRadio(TarDuty){");
client.println("    var dir_radio = $('input[name=\"accessible-radio\"]');");
client.println("    if(TarDuty==0){");
client.println("      dir_radio.prop('disabled', false);");
client.println("    }");
client.println("    else{");
client.println("      dir_radio.prop('disabled', true);");
client.println("    }");
client.println("  }");
client.println("");
client.println("    function DCTar(pos,bBackward) {");
client.println("      $.get(\"/?valduty=\" + pos + \"_\" + \"/?valdir=\"+ bBackward + \"&\");");
client.println("      {Connection: close};");
client.println("    }");
client.println("");
client.println("    /*jslint plusplus: true, sloppy: true, indent: 4 */");
client.println("    (function () {");
client.println("        \"use strict\";");
client.println("        // this function is strict...");
client.println("    }());");
client.println("");
client.println("    var iCurrentSpeed= 0,");
client.println("        iTargetSpeed = 0,");
client.println("        bDecrement = null,");
client.println("        job = null;");
client.println("");
client.println("    function degToRad(angle) {");
client.println("        // Degrees to radians");
client.println("        return ((angle * Math.PI) / 180);");
client.println("    }");
client.println("");
client.println("    function radToDeg(angle) {");
client.println("        // Radians to degree");
client.println("        return ((angle * 180) / Math.PI);");
client.println("    }");
client.println("");
client.println("    function drawLine(options, line) {");
client.println("        // Draw a line using the line object passed in");
client.println("        options.ctx.beginPath();");
client.println("        // Set attributes of open");
client.println("        options.ctx.globalAlpha = line.alpha;");
client.println("        options.ctx.lineWidth = line.lineWidth;");
client.println("        options.ctx.fillStyle = line.fillStyle;");
client.println("        options.ctx.strokeStyle = line.fillStyle;");
client.println("        options.ctx.moveTo(line.from.X,");
client.println("            line.from.Y);");
client.println("");
client.println("        // Plot the line");
client.println("        options.ctx.lineTo(");
client.println("            line.to.X,");
client.println("            line.to.Y");
client.println("        );");
client.println("");
client.println("        options.ctx.stroke();");
client.println("    }");
client.println("");
client.println("    function createLine(fromX, fromY, toX, toY, fillStyle, lineWidth, alpha) {");
client.println("        // Create a line object using Javascript object notation");
client.println("        return {");
client.println("            from: {");
client.println("                X: fromX,");
client.println("                Y: fromY");
client.println("            },");
client.println("            to:    {");
client.println("                X: toX,");
client.println("                Y: toY");
client.println("            },");
client.println("            fillStyle: fillStyle,");
client.println("            lineWidth: lineWidth,");
client.println("            alpha: alpha");
client.println("        };");
client.println("    }");
client.println("");
client.println("    function drawBackground(options) {");
client.println("        /* Black background with alphs transparency to");
client.println("         * blend the edges of the metallic edge and");
client.println("         * black background");
client.println("         */");
client.println("       var i = 0;");
client.println("");
client.println("        options.ctx.globalAlpha = 0.4;");
client.println("        options.ctx.fillStyle = \"rgb(0,0,0)\";");
client.println("");
client.println("        // Draw semi-transparent circles");
client.println("        for (i = 170; i < 190; i++) {");
client.println("            options.ctx.beginPath();");
client.println("");
client.println("            options.ctx.arc(options.center.X,");
client.println("                options.center.Y,");
client.println("                i,  //半径");
client.println("                0,  //スタート角度");
client.println("                Math.PI, //エンド角度");
client.println("                true); //時計周り or 反時計周り");
client.println("");
client.println("            options.ctx.fill();");
client.println("        }");
client.println("");
client.println("    }");
client.println("");
client.println("    function applyDefaultContextSettings(options) {");
client.println("        /* Helper function to revert to gauges");
client.println("         * default settings");
client.println("         */");
client.println("");
client.println("        options.ctx.lineWidth = 2;");
client.println("        options.ctx.globalAlpha = 0.5;");
client.println("        options.ctx.strokeStyle = \"rgb(255, 255, 255)\";");
client.println("        options.ctx.fillStyle = 'rgb(255,255,255)';");
client.println("    }");
client.println("");
client.println("    function drawSmallTickMarks(options) {");
client.println("        /* The small tick marks against the coloured");
client.println("         * arc drawn every 5 mph from 10 degrees to");
client.println("         * 170 degrees.");
client.println("         */");
client.println("");
client.println("        var tickvalue = options.levelRadius - 8,");
client.println("            iTick = 0,");
client.println("            gaugeOptions = options.gaugeOptions,");
client.println("            iTickRad = 0,");
client.println("            onArchX,");
client.println("            onArchY,");
client.println("            innerTickX,");
client.println("            innerTickY,");
client.println("            fromX,");
client.println("            fromY,");
client.println("            line,");
client.println("            toX,");
client.println("            toY;");
client.println("");
client.println("        applyDefaultContextSettings(options);");
client.println("");
client.println("        // Tick every 20 degrees (small ticks)");
client.println("        for (iTick = 10; iTick < 180; iTick += 20) {");
client.println("");
client.println("            iTickRad = degToRad(iTick);");
client.println("");
client.println("            /* Calculate the X and Y of both ends of the");
client.println("             * line I need to draw at angle represented at Tick.");
client.println("             * The aim is to draw the a line starting on the");
client.println("             * coloured arc and continueing towards the outer edge");
client.println("             * in the direction from the center of the gauge.");
client.println("             */");
client.println("");
client.println("            onArchX = gaugeOptions.radius - (Math.cos(iTickRad) * tickvalue);");
client.println("            onArchY = gaugeOptions.radius - (Math.sin(iTickRad) * tickvalue);");
client.println("            innerTickX = gaugeOptions.radius - (Math.cos(iTickRad) * gaugeOptions.radius);");
client.println("            innerTickY = gaugeOptions.radius - (Math.sin(iTickRad) * gaugeOptions.radius);");
client.println("");
client.println("            fromX = (options.center.X - gaugeOptions.radius) + onArchX;");
client.println("            fromY = (gaugeOptions.center.Y - gaugeOptions.radius) + onArchY;");
client.println("            toX = (options.center.X - gaugeOptions.radius) + innerTickX;");
client.println("            toY = (gaugeOptions.center.Y - gaugeOptions.radius) + innerTickY;");
client.println("");
client.println("            // Create a line expressed in JSON");
client.println("            line = createLine(fromX, fromY, toX, toY, \"rgb(255,255,255)\", 3, 0.6);");
client.println("");
client.println("            // Draw the line");
client.println("            drawLine(options, line);");
client.println("");
client.println("        }");
client.println("    }");
client.println("");
client.println("    function drawLargeTickMarks(options) {");
client.println("        /* The large tick marks against the coloured");
client.println("         * arc drawn every 10 mph from 10 degrees to");
client.println("         * 170 degrees.");
client.println("         */");
client.println("");
client.println("        var tickvalue = options.levelRadius - 8,");
client.println("            iTick = 0,");
client.println("            gaugeOptions = options.gaugeOptions,");
client.println("            iTickRad = 0,");
client.println("            innerTickY,");
client.println("            innerTickX,");
client.println("            onArchX,");
client.println("            onArchY,");
client.println("            fromX,");
client.println("            fromY,");
client.println("            toX,");
client.println("            toY,");
client.println("            line;");
client.println("");
client.println("        applyDefaultContextSettings(options);");
client.println("");
client.println("        tickvalue = options.levelRadius - 2;");
client.println("");
client.println("        // 10 units (major ticks)");
client.println("        for (iTick = 20; iTick < 180; iTick += 20) {");
client.println("");
client.println("            iTickRad = degToRad(iTick);");
client.println("");
client.println("            /* Calculate the X and Y of both ends of the");
client.println("             * line I need to draw at angle represented at Tick.");
client.println("             * The aim is to draw the a line starting on the");
client.println("             * coloured arc and continueing towards the outer edge");
client.println("             * in the direction from the center of the gauge.");
client.println("             */");
client.println("");
client.println("            onArchX = gaugeOptions.radius - (Math.cos(iTickRad) * tickvalue);");
client.println("            onArchY = gaugeOptions.radius - (Math.sin(iTickRad) * tickvalue);");
client.println("            innerTickX = gaugeOptions.radius - (Math.cos(iTickRad) * gaugeOptions.radius);");
client.println("            innerTickY = gaugeOptions.radius - (Math.sin(iTickRad) * gaugeOptions.radius);");
client.println("");
client.println("            fromX = (options.center.X - gaugeOptions.radius) + onArchX;");
client.println("            fromY = (gaugeOptions.center.Y - gaugeOptions.radius) + onArchY;");
client.println("            toX = (options.center.X - gaugeOptions.radius) + innerTickX;");
client.println("            toY = (gaugeOptions.center.Y - gaugeOptions.radius) + innerTickY;");
client.println("");
client.println("            // Create a line expressed in JSON");
client.println("            line = createLine(fromX, fromY, toX, toY, \"rgb(255,255,255)\", 3, 0.6);");
client.println("");
client.println("            // Draw the line");
client.println("            drawLine(options, line);");
client.println("        }");
client.println("    }");
client.println("");
client.println("    function drawTicks(options) {");
client.println("        /* Two tick in the coloured arc!");
client.println("         * Small ticks every 5");
client.println("         * Large ticks every 10");
client.println("         */");
client.println("        drawSmallTickMarks(options);");
client.println("        drawLargeTickMarks(options);");
client.println("    }");
client.println("");
client.println("    function drawTextMarkers(options) {");
client.println("        /* The text labels marks above the coloured");
client.println("         * arc drawn every 10 mph from 10 degrees to");
client.println("         * 170 degrees.");
client.println("         */");
client.println("        var innerTickX = 0,");
client.println("            innerTickY = 0,");
client.println("            iTick = 0,");
client.println("            gaugeOptions = options.gaugeOptions,");
client.println("            iTickToPrint = 00;");
client.println("");
client.println("        applyDefaultContextSettings(options);");
client.println("");
client.println("        // Font styling");
client.println("        options.ctx.font = 'italic 15px sans-serif';");
client.println("        options.ctx.textBaseline = 'top';");
client.println("");
client.println("        options.ctx.beginPath();");
client.println("");
client.println("        // Tick every 20 (small ticks)");
client.println("        for (iTick = 10; iTick < 180; iTick += 20) {");
client.println("");
client.println("            innerTickX = gaugeOptions.radius - (Math.cos(degToRad(iTick)) * gaugeOptions.radius);");
client.println("            innerTickY = gaugeOptions.radius - (Math.sin(degToRad(iTick)) * gaugeOptions.radius);");
client.println("");
client.println("            // Some cludging to center the values (TODO: Improve)");
client.println("            if (iTick <= 10) {");
client.println("                options.ctx.fillText(iTickToPrint, (options.center.X - gaugeOptions.radius - 12) + innerTickX,");
client.println("                        (gaugeOptions.center.Y - gaugeOptions.radius - 12) + innerTickY + 5);");
client.println("            } else if (iTick < 50) {");
client.println("                options.ctx.fillText(iTickToPrint, (options.center.X - gaugeOptions.radius - 12) + innerTickX - 5,");
client.println("                        (gaugeOptions.center.Y - gaugeOptions.radius - 12) + innerTickY + 5);");
client.println("            } else if (iTick < 90) {");
client.println("                options.ctx.fillText(iTickToPrint, (options.center.X - gaugeOptions.radius - 12) + innerTickX,");
client.println("                        (gaugeOptions.center.Y - gaugeOptions.radius - 12) + innerTickY);");
client.println("            } else if (iTick === 90) {");
client.println("                options.ctx.fillText(iTickToPrint, (options.center.X - gaugeOptions.radius - 12) + innerTickX + 4,");
client.println("                        (gaugeOptions.center.Y - gaugeOptions.radius - 12) + innerTickY);");
client.println("            } else if (iTick < 145) {");
client.println("                options.ctx.fillText(iTickToPrint, (options.center.X - gaugeOptions.radius - 12) + innerTickX + 10,");
client.println("                        (gaugeOptions.center.Y - gaugeOptions.radius - 12) + innerTickY);");
client.println("            } else {");
client.println("                options.ctx.fillText(iTickToPrint, (options.center.X - gaugeOptions.radius - 12) + innerTickX + 15,");
client.println("                        (gaugeOptions.center.Y - gaugeOptions.radius - 12) + innerTickY + 5);");
client.println("            }");
client.println("");
client.println("            // MPH increase by 10 every 20 degrees");
client.println("            //iTickToPrint += Math.round(2160 / 9);");
client.println("             iTickToPrint += 30;");
client.println("        }");
client.println("");
client.println("        options.ctx.stroke();");
client.println("    }");
client.println("");
client.println("    function drawSpeedometerPart(options, alphaValue, strokeStyle, startPos) {");
client.println("        /* Draw part of the arc that represents");
client.println("        * the colour speedometer arc");
client.println("        */");
client.println("");
client.println("        options.ctx.beginPath();");
client.println("");
client.println("        options.ctx.globalAlpha = alphaValue;");
client.println("        options.ctx.lineWidth = 5;");
client.println("        options.ctx.strokeStyle = strokeStyle;");
client.println("");
client.println("        options.ctx.arc(options.center.X,");
client.println("            options.center.Y,");
client.println("            options.levelRadius,");
client.println("            Math.PI + (Math.PI / 360 * startPos),");
client.println("            0 - (Math.PI / 360 * 10),");
client.println("            false);");
client.println("");
client.println("        options.ctx.stroke();");
client.println("    }");
client.println("");
client.println("    function drawSpeedometerColourArc(options) {");
client.println("        /* Draws the colour arc.  Three different colours");
client.println("         * used here; thus, same arc drawn 3 times with");
client.println("         * different colours.");
client.println("         * TODO: Gradient possible?");
client.println("         */");
client.println("");
client.println("        var startOfGreen = 10,");
client.println("            endOfGreen = 200,");
client.println("            endOfOrange = 280;");
client.println("");
client.println("        drawSpeedometerPart(options, 1.0, \"rgb(50,205,50)\", startOfGreen);");
client.println("        drawSpeedometerPart(options, 0.9, \"rgb(255,165,0)\", endOfGreen);");
client.println("        drawSpeedometerPart(options, 0.9, \"rgb(255,69,0) \", endOfOrange);");
client.println("");
client.println("    }");
client.println("");
client.println("    function drawNeedleDial(options, alphaValue, strokeStyle, fillStyle,bBackward) {");
client.println("        /* Draws the metallic dial that covers the base of the");
client.println("        * needle.");
client.println("        */");
client.println("        var i = 0;");
client.println("");
client.println("        options.ctx.globalAlpha = alphaValue;");
client.println("        options.ctx.lineWidth = 3;");
client.println("        options.ctx.strokeStyle = strokeStyle;");
client.println("");
client.println("        if (bBackward==1){");
client.println("          options.ctx.fillStyle = \"rgb(255,0,0)\";");
client.println("        }");
client.println("        else{");
client.println("          options.ctx.fillStyle = fillStyle;");
client.println("        }");
client.println("");
client.println("        // Draw several transparent circles with alpha");
client.println("        for (i = 0; i < 30; i++) {");
client.println("");
client.println("            options.ctx.beginPath();");
client.println("            options.ctx.arc(options.center.X,");
client.println("                options.center.Y,");
client.println("                i,");
client.println("                0,");
client.println("                Math.PI,");
client.println("                true);");
client.println("");
client.println("            options.ctx.fill();");
client.println("            options.ctx.stroke();");
client.println("        }");
client.println("    }");
client.println("");
client.println("    function convertSpeedToAngle(options) {");
client.println("        /* Helper function to convert a speed to the");
client.println("        * equivelant angle.");
client.println("        */");
client.println("        var iSpeed = options.speed,");
client.println("            iSpeedAsAngle = (iSpeed)/255 * 180+10;");
client.println("");
client.println("        // Ensure the angle is within range");
client.println("        if (iSpeedAsAngle > 180) {");
client.println("            iSpeedAsAngle = 180;");
client.println("        } else if (iSpeedAsAngle < 10) {");
client.println("            iSpeedAsAngle = 10;");
client.println("        }");
client.println("");
client.println("        return iSpeedAsAngle;");
client.println("    }");
client.println("");
client.println("    function drawNeedle(options,bBackward) {");
client.println("        /* Draw the needle in a nice read colour at the");
client.println("        * angle that represents the options.speed value.");
client.println("        */");
client.println("");
client.println("        var iSpeedAsAngle = convertSpeedToAngle(options),");
client.println("            iSpeedAsAngleRad = degToRad(iSpeedAsAngle),");
client.println("            gaugeOptions = options.gaugeOptions,");
client.println("            innerTickX = gaugeOptions.radius - (Math.cos(iSpeedAsAngleRad) * 20),");
client.println("            innerTickY = gaugeOptions.radius - (Math.sin(iSpeedAsAngleRad) * 20),");
client.println("            fromX = (options.center.X - gaugeOptions.radius) + innerTickX,");
client.println("            fromY = (gaugeOptions.center.Y - gaugeOptions.radius) + innerTickY,");
client.println("            endNeedleX = gaugeOptions.radius - (Math.cos(iSpeedAsAngleRad) * gaugeOptions.radius),");
client.println("            endNeedleY = gaugeOptions.radius - (Math.sin(iSpeedAsAngleRad) * gaugeOptions.radius),");
client.println("            toX = (options.center.X - gaugeOptions.radius) + endNeedleX,");
client.println("            toY = (gaugeOptions.center.Y - gaugeOptions.radius) + endNeedleY,");
client.println("            line = createLine(fromX, fromY, toX, toY, \"rgb(255, 0, 0)\", 3, 1);");
client.println("");
client.println("        drawLine(options, line);");
client.println("");
client.println("        // Two circle to draw the dial at the base (give its a nice effect?)");
client.println("        drawNeedleDial(options, 0.6, \"rgb(255, 255, 255)\", \"rgb(255,255,255)\",bBackward);");
client.println("        drawNeedleDial(options, 0.2, \"rgb(255, 255, 255)\", \"rgb(127,127,127)\",bBackward);");
client.println("");
client.println("    }");
client.println("");
client.println("    function buildOptionsAsJSON(canvas, iSpeed) {");
client.println("        /* Setting for the speedometer");
client.println("        * Alter these to modify its look and feel");
client.println("        */");
client.println("");
client.println("        var centerX = 210,");
client.println("            centerY = 210,");
client.println("            radius = 150,");
client.println("            outerRadius = 200;");
client.println("");
client.println("        // Create a speedometer object using Javascript object notation");
client.println("        return {");
client.println("            ctx: canvas.getContext('2d'),");
client.println("            speed: iSpeed,");
client.println("            center:    {");
client.println("                X: centerX,");
client.println("                Y: centerY");
client.println("            },");
client.println("            levelRadius: radius - 10,");
client.println("            gaugeOptions: {");
client.println("                center:    {");
client.println("                    X: centerX,");
client.println("                    Y: centerY");
client.println("                },");
client.println("                radius: radius");
client.println("            },");
client.println("            radius: outerRadius");
client.println("        };");
client.println("    }");
client.println("");
client.println("    function clearCanvas(options) {");
client.println("        options.ctx.clearRect(0, 0, 800, 600);");
client.println("        applyDefaultContextSettings(options);");
client.println("    }");
client.println("    function drawLabel(options,bBackward)");
client.println("    {");
client.println("");
client.println("        var offsetX=0;");
client.println("        var offsetY=60;");
client.println("");
client.println("        options.ctx.font = '20pt sans-serif';");
client.println("        if(bBackward==0){");
client.println("              options.ctx.strokeStyle='#ffffff';");
client.println("        }else{");
client.println("              options.ctx.strokeStyle='#ff0000';");
client.println("        }");
client.println("        var X=options.center.X;");
client.println("        var Y=options.center.Y;");
client.println("        options.ctx.textAlign = 'center';");
client.println("        //options.ctx.globalCompositeOperation = 'destination-over';");
client.println("        options.ctx.strokeText(options.speed,X-offsetX, Y-offsetY);");
client.println("        options.ctx.textAlign = 'start';");
client.println("");
client.println("    }");
client.println("    function draw() {");
client.println("        /* Main entry point for drawing the speedometer");
client.println("        * If canvas is not support alert the user.");
client.println("        */");
client.println("");
client.println("//        console.log('Target: ' + iTargetSpeed);");
client.println("//        console.log('Current: ' + iCurrentSpeed);");
client.println("");
client.println("        var canvas = document.getElementById('speedometer'),");
client.println("            options = null;");
client.println("");
client.println("        // Canvas good?");
client.println("        if (canvas !== null && canvas.getContext) {");
client.println("            options = buildOptionsAsJSON(canvas, iCurrentSpeed);");
client.println("");
client.println("            // Clear canvas");
client.println("            clearCanvas(options);");
client.println("");
client.println("            // Draw thw background");
client.println("            drawBackground(options);");
client.println("            // Draw tick marks");
client.println("            drawTicks(options);");
client.println("            // Draw labels on markers");
client.println("            drawTextMarkers(options);");
client.println("            // Draw speeometer colour arc");
client.println("            drawSpeedometerColourArc(options);");
client.println("            // Draw text of dashboard");
client.println("            drawLabel(options,bBackward);");
client.println("            // Draw the needle and base");
client.println("            drawNeedle(options,bBackward);");
client.println("");
client.println("");
client.println("");
client.println("        } else {");
client.println("            alert(\"Canvas not supported by your browser!\");");
client.println("        }");
client.println("");
client.println("        if(iTargetSpeed == iCurrentSpeed) {");
client.println("            clearTimeout(job);");
client.println("            return;");
client.println("        } else if(iTargetSpeed < iCurrentSpeed) {");
client.println("            bDecrement = true;");
client.println("        } else if(iTargetSpeed > iCurrentSpeed) {");
client.println("            bDecrement = false;");
client.println("        }");
client.println("");
client.println("        if(bDecrement) {");
client.println("            if(iCurrentSpeed - 10 < iTargetSpeed)");
client.println("                iCurrentSpeed = iCurrentSpeed - 1;");
client.println("            else");
client.println("                iCurrentSpeed = iCurrentSpeed - 1;");
client.println("        } else {");
client.println("");
client.println("            if(iCurrentSpeed + 10 > iTargetSpeed)");
client.println("                iCurrentSpeed = iCurrentSpeed + 1;");
client.println("            else");
client.println("                iCurrentSpeed = iCurrentSpeed + 1;");
client.println("        }");
client.println("");
client.println("");
client.println("        job = setTimeout(\"draw()\", 5);");
client.println("    }");
client.println("");
client.println("    function drawWithInputValue(mascon) {");
client.println("");
client.println("        iTargetSpeed = mascon;");
client.println("");
client.println("");
client.println("");
client.println("            // Sanity checks");
client.println("            if (isNaN(iTargetSpeed)) {");
client.println("                iTargetSpeed = 0;");
client.println("            } else if (iTargetSpeed < 0) {");
client.println("                iTargetSpeed = 0;");
client.println("            } else if (iTargetSpeed > 255) {");
client.println("                iTargetSpeed = 255;");
client.println("            }");
client.println("");
client.println("            job = setTimeout(\"draw()\", 5);");
client.println("");
client.println("        }");
client.println("");
client.println("");
client.println("</script>");
client.println("</body>");
client.println("</html>");

/////////////////////////////////////////////////////////////////////////////////////////////////
// ここまで
/////////////////////////////////////////////////////////////////////////////////////////////////
            // GET data

            int index_temp =0;
            
            if(header.indexOf("/?valduty=")>=0) { //GETメソッドでvaldutyをページから取得する
              pos1 = header.indexOf('=');
              pos2 = header.indexOf('_');


              // String with motor position
              valueString = header.substring(pos1+1, pos2);
              
              // Move servo into position
               target_duty=valueString.toInt();
               
              // Print value to serial monitor
              Serial.print("target duty =");
              Serial.println(target_duty); 
            }

            if(header.indexOf("/?valdir=")>=0) { //GETメソッドでvaldirをページから取得する
              pos1 = header.indexOf('&')-1;
              
              // String with motor position
              valueString = header[pos1];

              inv_rotation=valueString.toInt();
              
              Serial.print("rotational direction =");
              Serial.println(inv_rotation); 
            }

              PWMCtr(inv_rotation,target_duty); //GETした回転方向と目標DUTYでPWMを出力する
       
            // 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("");
  }
}

  void PWMCtr (bool inv, int duty)
  {
    if(inv)
      ledcWrite(pwmChannel_neg,duty);
    else
      ledcWrite(pwmChannel_pos,duty);  
  }

Arduino IDE モータ制御の部分

上のコードからモータの制御に関わる部分を切り出したものです。内容はコメントにあるとおりですので、説明は省きます。

// Motor制御信号用ピン
int motorpin_pos = 27; //正転
int motorpin_neg = 14; //逆転

// プラレールマスコン
int target_duty =0; //目標DUTY 8bitで指示するので0-255
bool inv_rotation = false; //正転か逆転、真なら逆転

// PWM
const int freq = 10000;   //PWM周波数 Hz

//ESP32は内部的にLEDのPWM信号用に16チャンネルもってる
//そのどれ使います?の設定
const int pwmChannel_pos = 0; //channel 0
const int pwmChannel_neg = 1; //channel 1

//分解能8bit 0~100%dutyを8bitで分割
const int resolution = 8;


void setup() {
  pinMode(motorpin_pos, OUTPUT); //PWM信号を出力 正転用
  pinMode(motorpin_neg, OUTPUT); //逆転用
  
 //分解能と制御周波数とチャンネル番号を指定する
 ledcSetup(pwmChannel_pos, freq, resolution);   
 ledcSetup(pwmChannel_neg, freq, resolution);
  
 //PWMのチャンネルと出力ピンをひもづける
  ledcAttachPin(motorpin_pos, pwmChannel_pos); 
  ledcAttachPin(motorpin_neg, pwmChannel_neg);

  ledcWrite(pwmChannel_pos, 0); //duty0をだす
  digitalWrite(motorpin_neg,LOW); // 逆転側はLOWにしとく
}

基本的にはledcAttachPinで使用するPWMのチャンネル(ESP32では、内部的に16チャンネルのPWM用のチャンネルを持ってる)に制御周期と分解能を指定して、

そのPWMチャンネルと実際信号を出力するピンの紐づけをledcAttachPinで実施する。

最後に実際のDutyをledcWriteで指定のPWMチャンネルに反映させるといった手順です。

完成

こんな感じでESP32に亀の子でドライバ類をつけてみました。写真はマイコン側のレギュレータをつける前のものです。それぞれ裏面と表面です。DIP基板が無駄にでかいので、シール基板やらジャンパピンとかを駆使しつつ製作しました。

それなりに綺麗にできました。いや汚いですか?

ESP32と合体させるとこんな感じです。下側のxhコネクタは赤黒線が電源ライン、緑黄線がモータの出力線になります。

車両にのせます。

カバーつけた後、青いボタンは電源ON/OFFのスイッチです。

そして、問題が発生した

低Dutyでは普通に動くんですが、高Dutyでネットワークがつながらなくなる問題が発生しました。そうなると全開で走ったまま、停車できなくなってしまいます。

ドライバの直下にWIFIのアンテナがあるので、あやしいです。配置がわるすぎましたね。

修正したのがこれです。

取付を逆にして、アンテナとモータドライバの位置を離しました。簡単な措置ですがなおりました。シールドとかする必要がでてきたらヤダな~と思ってたので、ラッキーでした。

このとき、ラインノイズかもとも思い、コンデンサ0.1uFをモータのリード線につけましが、効果があったか不明です。

そして、課題が残った

順調に動きましたが、課題が無いわけではありません。

1)速度を変更した時のレスポンスがいまいち

たぶん、ページの更新が無駄に多いからと思ってますが、原因不明です。もうちょっとタイムリーにかわると、カーブの手前で減速!みたいなことがしやすいので、改良の余地があります。

2)スピンが発生して、前進しないことがある

元々このプラレールは単三電池一本で動作するようになっていたので、電圧UPでモータの出力が大分増しました。どうもパワーウェイトレシオが崩れてしまい、特に発進時はタイヤが空転して、スムーズな発進ができません。

3)バックができない

モータ自体回るのですが、空転するだけです。最後尾の車両から車両を押すというのがプラレールの構造的に無理なことがわかりました。

まぁ、こんなところです。色々やってると、不具合が見つかって、キリないですね。動いたんでとりあえず、よしとします。

happy プラレール!!

コメントを残す

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