Ketunorobioのアイコン画像
Ketunorobio 2022年07月27日作成 (2022年07月27日更新)
製作品 製作品 閲覧数 1874
Ketunorobio 2022年07月27日作成 (2022年07月27日更新) 製作品 製作品 閲覧数 1874

RaspberryPiで作るメカナムホイールラジコンカー

RaspberryPiで作るメカナムホイールラジコンカー

Twitterを拝見していると、「メカナムホイールを使ったラジコン」が目に留まりまして、私も作ってみました。

今回はその時の制作過程を簡単にまとめようかと思います。

なお、ブラウザで操作出来る仕組みにしたかったので、動作の流れとしては次のようにしました。

スマホ(ブラウザ)→ラズパイ(Apache(html→js→CGI(python)))→GPIO→DCモータードライバ→DCモーター

※ラズベリーパイは、ApacheなどのWEBサーバーやCGIが使える状態にある事を前提に進めます。

それでは初めに、必要な部品をまとめておきます。

使用部材

キャプションを入力できます

・ユニバーサルプレート × 1枚

・メカナムホイール(L,R) × 2セット

・TTモーター × 4つ

・ミニ接合金物アングルA-4 × 4枚

・なべ小ねじM3 10mm × 8本(ミニ接合金物アングル固定用)

・なべ小ねじM3 25mm × 4本(TTモーター固定用)

・RaspberryPi3 × 1つ

・5V電源と6V電源(乾電池やモバイルバッテリーなど)

・DRV8833 DC モーター ドライバー × 2個

・ジャンピングワイヤー × 多数

・ブレッドボード(小) × 1個

続いて組み立てます。

組立

キャプションを入力できます

モーター、メカナムホイールを写真のように組み立てます。

注意点としましては、メカナムホイールはL,Rがあり、前輪はそのままですが、後輪はL,Rが逆になります。

あと、この時点でモーターに導線をはんだ付けしておくと楽でした。その後は仮で電池を繋いで、モーターの正転、後転を確認しておきます。(導線は8本、つまりGPIOを8つ使用するので、タイヤ毎の回転方向となる電圧の向きHIGH,Lowを把握しておく。)

メカナムホイールの進行方向

キャプションを入力できます

※画像はrobotshop様より引用

4輪駆動で各タイヤに正転、後転、停止の制御をかければ、組み合わせに応じて図のような動作が再現できます。今回は全ての方向を網羅させたいので、図に左下と右下、停止を加えた「11パターン」の組み合わせを作ります。

各モーターに1~4の番号を割り振り、Aが+(正転)、Bが-(後転)、等を割り振って紙に記しておけば、後の配線やプログラム記述が楽になります。

配線

今回しようするモータードライバは1つで2ch制御出来るので、前輪と後輪に分けて実装することにします。

配線は、

ラズパイ(GPIO)→モータードライバIN→モータードライバOUT→DCモーター

となるので、モーターに接続した8本の導線はモータードライバのoutに、INにはラズパイのGPIOから8つ引っ張ってきます。

GPIOと各モーターの紐づけ(制御)については、先ほど控えた紙などを参考にすればいいと思います。

なお、電源についてはラズパイ(5V)とモータードライバ(6V程度)は別で取る事とします。

プログラム

html

<!DOCTYPE html> <html lang="ja"> <head> <meta charset="utf-8"> <title>テスト</title> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="stylesheet" href="style.css">         </head>           <body> <div class="cam"> <img src="http://ローカルIP:8080/?action=stream"/> </div> <main> <ul> <div class="bc"> <div class="b"> <li id="leftforward" class="ledoff">↖</li> </div> <div class="a"> <li id="forward" class="ledoff">↑</li> </div> <div class="c"> <li id="rightforward" class="ledoff">↗</li> </div> </div> <div class="bc"> <div class="b"> <li id="left" class="ledoff">←</li> </div> <div class="a"> <div class="z"> <div class="y"> <li id="l" class="ledoff">↰</li> </div> <div class="x"> <li id="r" class="ledoff">↱</li> </div> </div> </div> <div class="c"> <li id="right" class="ledoff">→</li> </div> </div> <div class="bc"> <div class="b"> <li id="leftbackward" class="ledoff">↙</li> </div> <div class="a"> <li id="backward" class="ledoff">↓</li> </div> <div class="c"> <li id="rightbackward" class="ledoff">↘</li> </div> </div> </ul> </main> <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js"></script> <script src="main.js"></script> </body> </html>

モーターの操作画面となるhtmlになります。jsで非同期通信を使うので、Ajaxは必須です。

また、カメラ画像も載せたいので、カメラストリーミング(mjpgストリーマー)のリンクを貼っています。

css

* { margin: 0px; padding: 0px; } body { max-width: 600px; font-size: 25px; width: 100%; -webkit-touch-callout: none; -webkit-user-select: none; } img { width: 100%; } main { height: 40vh; background: skyblue; } ul { display: block; height: 40vh; list-style: none; padding-top: 10px; } .bc { display: flex; } .z { display: flex; } .a ,.bc, .d .y, .x { height: 12vh; } li { width: 90px; height: 90% ; margin-left: 5px; background: yellow; line-height: 80px; } #l, #r { width: 45px; margin-left: 5px; } .b { margin-left: auto; } .c { margin-right: auto; } ul li { text-align: center; } .a li, .b li, .c li, .d li { border: solid 1px; } .ledon { background: #f88888; } .n li { background: skyblue; } a:active { color: #ff2020; }

javascript

$(function(){ let motor = "STOP"; // 関数:モーターを動かすマクロ呼び出し function change_motor(typee) { motor = typee; if(typee == "FOWARD") { // 前進 //w().callMacro('FW'); console.log(typee); $.ajax({ url: 'cgi-bin/recieve.py', type: 'post', data: {name: 'hhhhh' } }).done(function(data){ console.log(data); }).fail(function(){ console.log('failed'); }); } else if(typee == "BACKWARD") { //w().callMacro('BK'); console.log(typee); $.ajax({ url: 'cgi-bin/recieve.py', type: 'post', data: {name: 'iiii' } }).done(function(data){ console.log(data); }).fail(function(){ console.log('failed'); }); } else if(typee == "RIGHT") { //w().callMacro('RT'); console.log(typee); $.ajax({ url: 'cgi-bin/recieve.py', type: 'post', data: {name: 'jjjj' } }).done(function(data){ console.log(data); }).fail(function(){ console.log('failed'); }); } else if(typee == "LEFT") { //w().callMacro('LT'); console.log(typee); $.ajax({ url: 'cgi-bin/recieve.py', type: 'post', data: {name: 'kkkk' } }).done(function(data){ console.log(data); }).fail(function(){ console.log('failed'); }); } else if(typee == "STOP") { //w().callMacro('ST'); console.log(typee); $.ajax({ url: 'cgi-bin/recieve.py', type: 'post', data: {name: 'ssss' } }).done(function(data){ console.log(data); }).fail(function(){ console.log('failed'); }); } else if(typee == "LEFTFORWARD") { //w().callMacro('ST'); console.log(typee); $.ajax({ url: 'cgi-bin/recieve.py', type: 'post', data: {name: 'qqqq' } }).done(function(data){ console.log(data); }).fail(function(){ console.log('failed'); }); } else if(typee == "RIGHTFORWARD") { //w().callMacro('ST'); console.log(typee); $.ajax({ url: 'cgi-bin/recieve.py', type: 'post', data: {name: 'mmmm' } }).done(function(data){ console.log(data); }).fail(function(){ console.log('failed'); }); } else if(typee == "L") { //w().callMacro('ST'); console.log(typee); $.ajax({ url: 'cgi-bin/recieve.py', type: 'post', data: {name: 'gggg' } }).done(function(data){ console.log(data); }).fail(function(){ console.log('failed'); }); } else if(typee == "R") { //w().callMacro('ST'); console.log(typee); $.ajax({ url: 'cgi-bin/recieve.py', type: 'post', data: {name: 'dddd' } }).done(function(data){ console.log(data); }).fail(function(){ console.log('failed'); }); } else if(typee == "LEFTBACKWARD") { //w().callMacro('ST'); console.log(typee); $.ajax({ url: 'cgi-bin/recieve.py', type: 'post', data: {name: 'oooo' } }).done(function(data){ console.log(data); }).fail(function(){ console.log('failed'); }); } else if(typee == "RIGHTBACKWARD") { //w().callMacro('ST'); console.log(typee); $.ajax({ url: 'cgi-bin/recieve.py', type: 'post', data: {name: 'zzzz' } }).done(function(data){ console.log(data); }).fail(function(){ console.log('failed'); }); } } // 「前進」ボタンが押されたときのイベント処理 $('#forward').bind('touchstart', function() { // 押されたとき if(motor == 'STOP') { $(this).addClass('ledon'); change_motor('FOWARD'); } }).bind('touchend', function() { // 離したとき $(this).removeClass('ledon'); change_motor('STOP'); }); // 「後退」ボタンが押されたときのイベント処理 $('#backward').bind('touchstart', function() { if(motor == "STOP") { $(this).addClass('ledon'); change_motor('BACKWARD'); } }).bind('touchend', function() { $(this).removeClass('ledon'); change_motor('STOP'); }); // 「右」ボタンが押されたときのイベント処理 $('#right').bind('touchstart', function() { if(motor == "STOP") { $(this).addClass('ledon'); change_motor('RIGHT'); } }).bind('touchend', function() { $(this).removeClass('ledon'); change_motor('STOP'); }); // 「左」ボタンが押されたときのイベント処理 $('#left').bind('touchstart', function() { if(motor == "STOP") { $(this).addClass('ledon'); change_motor('LEFT'); } }).bind('touchend', function() { $(this).removeClass('ledon'); change_motor('STOP'); }); $('#leftforward').bind('touchstart', function() { // 押されたとき if(motor == 'STOP') { $(this).addClass('ledon'); change_motor('LEFTFORWARD'); } }).bind('touchend', function() { // 離したとき $(this).removeClass('ledon'); change_motor('STOP'); }); $('#rightforward').bind('touchstart', function() { // 押されたとき if(motor == 'STOP') { $(this).addClass('ledon'); change_motor('RIGHTFORWARD'); } }).bind('touchend', function() { // 離したとき $(this).removeClass('ledon'); change_motor('STOP'); }); $('#leftbackward').bind('touchstart', function() { // 押されたとき if(motor == 'STOP') { $(this).addClass('ledon'); change_motor('LEFTBACKWARD'); } }).bind('touchend', function() { // 離したとき $(this).removeClass('ledon'); change_motor('STOP'); }); $('#rightbackward').bind('touchstart', function() { // 押されたとき if(motor == 'STOP') { $(this).addClass('ledon'); change_motor('RIGHTBACKWARD'); } }).bind('touchend', function() { // 離したとき $(this).removeClass('ledon'); change_motor('STOP'); }); $('#l').bind('touchstart', function() { // 押されたとき if(motor == 'STOP') { $(this).addClass('ledon'); change_motor('L'); } }).bind('touchend', function() { // 離したとき $(this).removeClass('ledon'); change_motor('STOP'); }); $('#r').bind('touchstart', function() { // 押されたとき if(motor == 'STOP') { $(this).addClass('ledon'); change_motor('R'); } }).bind('touchend', function() { // 離したとき $(this).removeClass('ledon'); change_motor('STOP'); }); });

touchstart(ボタンを押している時),touchend(ボタンを離したとき)でpythonに値を渡す関数を呼び出しています。

押したボタンに応じて異なる値を、Ajaxを用いてpythonへpostしています。

python

#!/usr/bin/python3 # -*- coding: utf-8 -*- import cgi import RPi.GPIO as GPIO GPIO.setmode(GPIO.BCM) MOTOR_A1 = 5; MOTOR_A2 = 6; MOTOR_B1 = 13; MOTOR_B2 = 19; MOTOR_C1 = 26; MOTOR_C2 = 12; MOTOR_D1 = 16; MOTOR_D2 = 20; GPIO.setup(MOTOR_A1, GPIO.OUT) GPIO.setup(MOTOR_A2, GPIO.OUT) GPIO.setup(MOTOR_B1, GPIO.OUT) GPIO.setup(MOTOR_B2, GPIO.OUT) GPIO.setup(MOTOR_C1, GPIO.OUT) GPIO.setup(MOTOR_C2, GPIO.OUT) GPIO.setup(MOTOR_D1, GPIO.OUT) GPIO.setup(MOTOR_D2, GPIO.OUT) form = cgi.FieldStorage() recieve = form.getvalue('name') if recieve == 'hhhhh': GPIO.output(MOTOR_A1,GPIO.HIGH) GPIO.output(MOTOR_A2,GPIO.LOW) GPIO.output(MOTOR_B1,GPIO.HIGH) GPIO.output(MOTOR_B2,GPIO.LOW) GPIO.output(MOTOR_C1,GPIO.HIGH) GPIO.output(MOTOR_C2,GPIO.LOW) GPIO.output(MOTOR_D1,GPIO.HIGH) GPIO.output(MOTOR_D2,GPIO.LOW) print('Content-type: text/html\n') print(recieve) elif recieve == 'iiii': GPIO.output(MOTOR_A1,GPIO.LOW) GPIO.output(MOTOR_A2,GPIO.HIGH) GPIO.output(MOTOR_B1,GPIO.LOW) GPIO.output(MOTOR_B2,GPIO.HIGH) GPIO.output(MOTOR_C1,GPIO.LOW) GPIO.output(MOTOR_C2,GPIO.HIGH) GPIO.output(MOTOR_D1,GPIO.LOW) GPIO.output(MOTOR_D2,GPIO.HIGH) print('Content-type: text/html\n') print(recieve) elif recieve == 'jjjj': GPIO.output(MOTOR_A1,GPIO.HIGH) GPIO.output(MOTOR_A2,GPIO.LOW) GPIO.output(MOTOR_B1,GPIO.LOW) GPIO.output(MOTOR_B2,GPIO.HIGH) GPIO.output(MOTOR_C1,GPIO.LOW) GPIO.output(MOTOR_C2,GPIO.HIGH) GPIO.output(MOTOR_D1,GPIO.HIGH) GPIO.output(MOTOR_D2,GPIO.LOW) print('Content-type: text/html\n') print(recieve) elif recieve == 'kkkk': GPIO.output(MOTOR_A1,GPIO.LOW) GPIO.output(MOTOR_A2,GPIO.HIGH) GPIO.output(MOTOR_B1,GPIO.HIGH) GPIO.output(MOTOR_B2,GPIO.LOW) GPIO.output(MOTOR_C1,GPIO.HIGH) GPIO.output(MOTOR_C2,GPIO.LOW) GPIO.output(MOTOR_D1,GPIO.LOW) GPIO.output(MOTOR_D2,GPIO.HIGH) print('Content-type: text/html\n') print(recieve) elif recieve == 'ssss': print('Content-type: text/html\n') print(recieve) GPIO.output(MOTOR_A1,GPIO.LOW) GPIO.output(MOTOR_A2,GPIO.LOW) GPIO.output(MOTOR_B1,GPIO.LOW) GPIO.output(MOTOR_B2,GPIO.LOW) GPIO.output(MOTOR_C1,GPIO.LOW) GPIO.output(MOTOR_C2,GPIO.LOW) GPIO.output(MOTOR_D1,GPIO.LOW) GPIO.output(MOTOR_D2,GPIO.LOW) elif recieve == 'qqqq': print('Content-type: text/html\n') print(recieve) GPIO.output(MOTOR_A1,GPIO.LOW) GPIO.output(MOTOR_A2,GPIO.LOW) GPIO.output(MOTOR_B1,GPIO.HIGH) GPIO.output(MOTOR_B2,GPIO.LOW) GPIO.output(MOTOR_C1,GPIO.HIGH) GPIO.output(MOTOR_C2,GPIO.LOW) GPIO.output(MOTOR_D1,GPIO.LOW) GPIO.output(MOTOR_D2,GPIO.LOW) elif recieve == 'mmmm': print('Content-type: text/html\n') print(recieve) GPIO.output(MOTOR_A1,GPIO.HIGH) GPIO.output(MOTOR_A2,GPIO.LOW) GPIO.output(MOTOR_B1,GPIO.LOW) GPIO.output(MOTOR_B2,GPIO.LOW) GPIO.output(MOTOR_C1,GPIO.LOW) GPIO.output(MOTOR_C2,GPIO.LOW) GPIO.output(MOTOR_D1,GPIO.HIGH) GPIO.output(MOTOR_D2,GPIO.LOW) elif recieve == 'gggg': print('Content-type: text/html\n') print(recieve) GPIO.output(MOTOR_A1,GPIO.LOW) GPIO.output(MOTOR_A2,GPIO.HIGH) GPIO.output(MOTOR_B1,GPIO.HIGH) GPIO.output(MOTOR_B2,GPIO.LOW) GPIO.output(MOTOR_C1,GPIO.LOW) GPIO.output(MOTOR_C2,GPIO.HIGH) GPIO.output(MOTOR_D1,GPIO.HIGH) GPIO.output(MOTOR_D2,GPIO.LOW) elif recieve == 'dddd': print('Content-type: text/html\n') print(recieve) GPIO.output(MOTOR_A1,GPIO.HIGH) GPIO.output(MOTOR_A2,GPIO.LOW) GPIO.output(MOTOR_B1,GPIO.LOW) GPIO.output(MOTOR_B2,GPIO.HIGH) GPIO.output(MOTOR_C1,GPIO.HIGH) GPIO.output(MOTOR_C2,GPIO.LOW) GPIO.output(MOTOR_D1,GPIO.LOW) GPIO.output(MOTOR_D2,GPIO.HIGH) elif recieve == 'oooo': print('Content-type: text/html\n') print(recieve) GPIO.output(MOTOR_A1,GPIO.LOW) GPIO.output(MOTOR_A2,GPIO.HIGH) GPIO.output(MOTOR_B1,GPIO.LOW) GPIO.output(MOTOR_B2,GPIO.LOW) GPIO.output(MOTOR_C1,GPIO.LOW) GPIO.output(MOTOR_C2,GPIO.LOW) GPIO.output(MOTOR_D1,GPIO.LOW) GPIO.output(MOTOR_D2,GPIO.HIGH) elif recieve == 'zzzz': print('Content-type: text/html\n') print(recieve) GPIO.output(MOTOR_A1,GPIO.LOW) GPIO.output(MOTOR_A2,GPIO.LOW) GPIO.output(MOTOR_B1,GPIO.LOW) GPIO.output(MOTOR_B2,GPIO.HIGH) GPIO.output(MOTOR_C1,GPIO.LOW) GPIO.output(MOTOR_C2,GPIO.HIGH) GPIO.output(MOTOR_D1,GPIO.LOW) GPIO.output(MOTOR_D2,GPIO.LOW)

html,css,jsは同じ階層に置き、pythonはcgiスクリプトなので、同じ階層にcgi-binディレクトリを作成し、その配下にpythonファイルを作成します。

内容は至ってシンプルで、jsからpostされた値を受け取り、その値によってGPIOの出力HIGH,LOWを変化させています。

それでは、これで完成ですので、動作確認をしてみます。

完成

ついでにM5StickCの加速度センサーを使って制御もしてみました。

Ketunorobioのアイコン画像
ロビヲ@野江内代です。
ログインしてコメントを投稿する