0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Servo Kit 360°でラジコンを作る

0
Last updated at Posted at 2026-01-12

なるだけ簡単にESP32を使ったラジコンを作ります。
(といいつつも、ESP32とコントローラ間はMQTTを使ってます。。。)

2つのパターンで作ります。

①2つのDCモータ駆動チャネル搭載のM5Stack
②DCモータ駆動チャネル非搭載のM5Stack

ソースもろもろは以下に上げました。

必要なパーツ

それぞれの場合に用意したパーツは以下の通りです。世の中にはいろんなデバイスやユニットがありますので、これに限りません。(販売終了のものが多いですが、後継品が販売されています)

①2つのDCモータ駆動チャネル搭載のM5Stackの場合

M5Stack FIRE IoT開発キット
https://www.switch-science.com/products/7364

Servo Kit 360°
https://www.switch-science.com/products/6479

M5Stack用GOPUSモジュール
https://www.switch-science.com/products/6063

完成写真はこちら

DSC_2996.JPG

DSC_2997.JPG

②DCモータ駆動チャネル非搭載のM5Stackの場合

M5StickC
https://www.switch-science.com/products/5517

Servo Kit 360°
https://www.switch-science.com/products/6479

ATOM TailBAT
https://www.switch-science.com/products/6348

ExtPort for StickC
https://www.switch-science.com/products/10405

完成写真はこちら

DSC_2998.JPG

DSC_3000.JPG

一方、操作するラジコンのコントローラ側は、ブラウザのHTML GamePadに対応したアナログコントローラを使います。

プレステやSwitchなど、世の中にはGamePad APIに対応しているのがほとんどかと思います。

ブラウザとESP32は、MQTTサーバを仲介させます。
ですので、MQTTサーバが立ち上がっている前提です。

ソースコード(ESP32側)

ESP32で動作するJavascriptで実装しています。

①2つのDCモータ駆動チャネル搭載のM5Stackの場合

GoplusがDCモータ駆動チャネルを搭載しているため、それをI2Cで制御します。

main.js
import * as wire from "Wire";
import * as mqtt from "Mqtt";
import * as lcd from "Lcd";

const topic = "test";
const mqtt_host = "【MQTTブローカのホスト名】";
const mqtt_port = 1883;

function setup(){
	lcd.clear();

	wire.begin(21, 22);

	var ip = esp32.getIpAddress();
	mqtt.setServer(mqtt_host, mqtt_port);
	mqtt.connect(ip[0] + "." + ip[1] + "." + ip[2] + "." + ip[3]);

	mqtt.subscribe(topic, (e) =>{
		console.log(JSON.stringify(e));
		var axes = JSON.parse(e.payload);
		writeSpeedLeft(axes[1]);
		writeSpeedRight(axes[3]);	
	});

	console.log("setup finished");
	lcd.setCursor(0, 0);
	lcd.print("setup finished");
}

function writeSpeedRight(val){
	var value = Math.floor(val * 90 + 90);
	wire.beginTransmission(0x5D);
	wire.write(0x10);
	wire.write(value);
	wire.endTransmission();	
}

function writeSpeedLeft(val){
	var value = Math.floor(-val * 90 + 90);
	wire.beginTransmission(0x5D);
	wire.write(0x11);
	wire.write(value);
	wire.endTransmission();	
}

function loop(){
	esp32.update();
}

②DCモータ駆動チャネル非搭載のM5Stackの場合

DCモータ制御チャネルを持っていないため、PWMを使って自身でモータを制御します。
PWMはESP32のLEDCを使います。

main.js
import * as ledc from "Ledc";
import * as mqtt from "Mqtt";
import * as lcd from "Lcd";

const mqtt_host = "【MQTTブローカのホスト名】";
const mqtt_port = 1883;
const topic = "test";
const PIN0 = 0;
const PIN1 = 26;

function writeSpeedRight(val){
	writeServoAngle(1, 90 + 90 * val);
}

function writeSpeedLeft(val){
	writeServoAngle(0, 90 + 90 * -val);
}

function writeServoAngle(channel, angle){
	var pulse = 500 + (angle * 2000) / 180;
	var maxDuty = (1 << 16) - 1;
	var duty = Math.floor((pulse * maxDuty) / 20000);
	ledc.write(channel, duty);
}

function setup(){
	lcd.clear();

	ledc.setup(0, 50, 16);
	ledc.attachPin(PIN0, 0);
	ledc.setup(1, 50, 16);
	ledc.attachPin(PIN1, 1);

	var ip = esp32.getIpAddress();
	mqtt.setServer(mqtt_host, mqtt_port);
	mqtt.connect(ip[0] + "." + ip[1] + "." + ip[2] + "." + ip[3]);

	mqtt.subscribe(topic, (e) =>{
		console.log(JSON.stringify(e));
		var axes = JSON.parse(e.payload);
		writeSpeedLeft(axes[1]);
		writeSpeedRight(axes[3]);	
	});

	console.log("setup finished");
	lcd.setCursor(0, 0);
	lcd.print("setup finished");
}

function loop(){
	esp32.update();
}

ソースコード(コントローラ)

まず、プレステかSwitchのコントローラをUSBでPCに接続します。
コントローラにある2つのアナログジョイスティックを使い、左右の車輪を回します。戦車のキャタピラの駆動と同じ感覚です。

そして、以下をブラウザから開きます。

gamepad_controller/index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <meta http-equiv="Content-Security-Policy" content="default-src * data: gap: https://ssl.gstatic.com 'unsafe-eval' 'unsafe-inline'; style-src * 'unsafe-inline'; media-src *; img-src * data: content: blob:; worker-src 'self' blob:;">
  <meta name="format-detection" content="telephone=no">
  <meta name="msapplication-tap-highlight" content="no">
  <meta name="mobile-web-app-capable" content="yes" />
  <meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width">

  <!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
  <script src="https://code.jquery.com/jquery-1.12.4.min.js" integrity="sha384-nvAa0+6Qg9clwYCGGPpDQLVpLNn0fRaROjHqs13t4Ggj3Ez50XnGQqc/r8MhnRDZ" crossorigin="anonymous"></script>
  <!-- Latest compiled and minified CSS -->
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css" integrity="sha384-HSMxcRTRxnN+Bdg0JdbxYKrThecOKuH5zCYotlSAcp1+c8xmyTe9GYg1l9a69psu" crossorigin="anonymous">
  <!-- Optional theme -->
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap-theme.min.css" integrity="sha384-6pzBo3FDv/PJ8r2KRkGHifhEocL+1X2rVCTTkUfGk7/0pbek5mMa1upzvWbrUbOZ" crossorigin="anonymous">
  <!-- Latest compiled and minified JavaScript -->
  <script src="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js" integrity="sha384-aJ21OjlMXNL5UyIl/XNwTMqvzeRMZH2w8c5cRVpzpU8Y5bApTppSuUkhZXN0VxHd" crossorigin="anonymous"></script>

  <link rel="stylesheet" href="css/start.css">
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/spinkit/2.0.1/spinkit.min.css" />
  <script src="js/methods_bootstrap.js"></script>
  <script src="js/components_bootstrap.js"></script>
  <script src="js/components_utils.js"></script>
  <script src="js/vue_utils.js"></script>
  <script src="js/gql_utils.js"></script>

  <script src="js/remoteconsole.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/vconsole/dist/vconsole.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.x/dist/vue.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/vuex@3.x/dist/vuex.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/vue-router@3.x/dist/vue-router.min.js"></script>

  <script src="https://cdn.jsdelivr.net/npm/js-cookie@2/src/js.cookie.min.js"></script>
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/toastr.min.css">
  <script src="https://cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/toastr.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.7.7/dat.gui.min.js"></script>

  <script src="https://unpkg.com/mqtt/dist/mqtt.min.js"></script>

  <title>Gamepad Controller</title>
</head>
<body>
<!--
    <div id="loader-background">
      <div class="sk-plane sk-center"></div>
    </div>
-->
    <div id="top" class="container">
        <h1>Gamepad Controller</h1>

        <button class="btn btn-default" v-on:click="start">start</button><br>
        <label>status</label> {{mqtt_status}}<br>
        <label>gamepad</label> {{gamepad_id}}<br>

        <router-view></router-view>
      
        <!-- for progress-dialog -->
        <progress-dialog v-bind:title="progress_title"></progress-dialog>
    </div>

    <script src="js/store.js"></script>
    <script src="js/router.js"></script>
    <script src="js/start.js"></script>
</body>

gamepad_controller/js/start.js
'use strict';

//const vConsole = new VConsole();
//const remoteConsole = new RemoteConsole("http://[remote server]/logio-post");
//window.datgui = new dat.GUI();

let client;
let latest = [NaN, NaN, NaN, NaN];
const THREASHOLD = 0.1;
var gamepad_found = false;
const mqtt_url = "ws://【MQTTブローカのホスト名】:1884";
const topic = "test";

var vue_options = {
    el: "#top",
    mixins: [mixins_bootstrap],
    store: vue_store,
    router: vue_router,
    data: {
        mqtt_url: mqtt_url,
        topic: topic,
        gamepad_id: "",
        mqtt_status: ""
    },
    computed: {
    },
    methods: {
        scan_gamepad: async function(){
            var gamepadList = navigator.getGamepads();
            for(let gamepad of gamepadList){
                if( gamepad ){
                    if( !gamepad_found ){
                        this.gamepad_id = gamepad.id;
                        gamepad_found = true;
                    }
                    if( isNaN(latest[0]) ||
                        Math.abs(latest[0] - gamepad.axes[0] ) >= THREASHOLD ||
                        Math.abs(latest[1] - gamepad.axes[1] ) >= THREASHOLD || 
                        Math.abs(latest[2] - gamepad.axes[2] ) >= THREASHOLD ||
                        Math.abs(latest[3] - gamepad.axes[3] ) >= THREASHOLD ){
                        client.publish(this.topic, JSON.stringify(gamepad.axes));
                    }
                    latest[0] = gamepad.axes[0];
                    latest[1] = gamepad.axes[1];
                    latest[2] = gamepad.axes[2];
                    latest[3] = gamepad.axes[3];
                }
            }
            requestAnimationFrame(this.scan_gamepad);
        },
        start: async function(){
            client = mqtt.connect(this.mqtt_url);
            client.on('connect', () =>{
                console.log("connected");
                this.mqtt_status = "connected";
            });
            client.on('disconnect', () =>{
                console.log("disconnected");
                this.mqtt_status = "disconnected";
            });
            requestAnimationFrame(this.scan_gamepad);
        },
    },
    created: function(){
    },
    mounted: function(){
        proc_load();
    }
};
vue_add_data(vue_options, { progress_title: '' }); // for progress-dialog
vue_add_global_components(components_bootstrap);
vue_add_global_components(components_utils);

/* add additional components */
  
window.vue = new Vue( vue_options );

(参考) ESP32で動作するJavascript実行環境

ESP32で動作するJavascript実行環境を公開しています。

「電子書籍:M5StackとJavascriptではじめるIoTデバイス制御」

サポートサイト

他、参考ページ

以上

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?