Twitterを拝見していると、真横や斜め、その場で旋回等自由自在に動き回る「メカナムホイールを使ったラジコン」が目に留まりまして、私も作ってみる事にしました。
過去にラズパイを使ったキャタピラ式のラジコンを作成した事があるので、プログラムはそれを応用する事にしました。↓
ブラウザで操作出来る仕組みにしたいので、ラズパイにWEBサーバーを立てる必要がありますが、python http.serverやwebiopiは少々不便でしたので今回はApacheを使用する事にしました。動作の流れとしては次の通りです。
スマホ(ブラウザ)→ラズパイ(Apache(html→js→CGI(python)))→GPIO→DCモータードライバ→DCモーター
jsからpythonスクリプトを動かしたいので、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を把握しておく。)
メカナムホイールの進行方向
4輪駆動で各タイヤに正転、後転、停止の制御をかければ、組み合わせに応じて図のような動作が再現できます。今回は全ての方向を網羅させたいので、図に左下と右下、停止を加えた「11パターン」の組み合わせを作ります。
各モーターに1~4の番号を割り振り、Aが+(正転)、Bが-(後転)、等を割り振って紙に記しておけば、後の配線やプログラム記述が楽になります。
ラズパイ、モータードライバ実装と配線
今回しようするモータードライバは1つで2ch制御出来るので、前輪と後輪に分けて実装することにします。
配線は、
ラズパイ(GPIO)→モータードライバIN→モータードライバOUT→DCモーター
となるので、モーターに接続した8本の導線はモータードライバのoutに、INにはラズパイのGPIOから8つ引っ張ってきます。
GPIOと各モーターの紐づけ(制御)については、先ほど控えた紙などを参考にすればいいと思います。
なお、電源についてはラズパイ(5V)とモータードライバ(6V程度)は別で取る事とします。
プログラム
Apacheのインストール
それではプログラムに入っていきますが、初めにWEBサーバーのApacheをインストールしておきます。また、CGIを使えるように設定の変更も行います。
この辺りの詳細については過去記事でまとめていますので、そちらを参照願います。↓
html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
<!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ストリーマーのリンクを貼っています。(mjpgストリーマーを起動後ここにアクセスすればOK)。
mjpgストリーマーについても過去記事でまとめています。↓
css
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
* { 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 |
$(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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 |
#!/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を変化させています。
それでは、これで完成ですので、動作確認をしてみます。
動作確認
いやあメカナムホイールって楽しいですネ。1日中触ってられそうです。外に持ち出してやってみたいけど近所の子供たちに潰されそうで出来ないのがネックではあります。。