なるだけ簡単に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
完成写真はこちら
②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
完成写真はこちら
一方、操作するラジコンのコントローラ側は、ブラウザのHTML GamePadに対応したアナログコントローラを使います。
プレステやSwitchなど、世の中にはGamePad APIに対応しているのがほとんどかと思います。
ブラウザとESP32は、MQTTサーバを仲介させます。
ですので、MQTTサーバが立ち上がっている前提です。
ソースコード(ESP32側)
ESP32で動作するJavascriptで実装しています。
①2つのDCモータ駆動チャネル搭載のM5Stackの場合
GoplusがDCモータ駆動チャネルを搭載しているため、それをI2Cで制御します。
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を使います。
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つのアナログジョイスティックを使い、左右の車輪を回します。戦車のキャタピラの駆動と同じ感覚です。
そして、以下をブラウザから開きます。
<!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>
'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デバイス制御」
サポートサイト
他、参考ページ
以上



