Ketunorobio が 2022年08月03日23時17分02秒 に編集
初版
タイトルの変更
外輪船ラジコン
タグの変更
船
RaspberryPiZero2W
ブラウザ操作
DCモーター
メイン画像の変更
記事種類の変更
製作品
本文の変更
先日、家族で奈良県にある生駒山上遊園地に行ってまいりました。 そこで少し風変りな船の乗り物を見かけたので、子供と一緒に乗ってみました。形状はこのような感じです。↓ @[twitter](https://twitter.com/ketunorobio/status/1520563222603988992) 船体の横に水車が付いているので、「外輪船」と呼ばれているみたいです。中にあるクランクを回すと水車も回る仕組みで、組み合わせによって前進、後退、左旋回、右旋回、前右左折、後右左折が出来ます。 これが意外とスムーズに進むものでして、少し関心していた時に「あ、これ作れそうだな」に至った次第です。 ## 外輪船の構成 2つの水車はDCモーター2個を使い、マイコンを使って制御させます。今回、マイコンボードにはRaspberry pi ZERO2 W を使用しました。 Raspberry Pi ⇒ DCモータードライバ ⇒ DCモーター×2 バッテリはマイコン用とドライバ用で2つ使用します。 カメラも搭載させたいと思ったので、とりあえずブラウザから操作する仕組みにしました。 スマホからRaspberry PiのWEBサーバーにアクセスし、そこの画面にカメラ画像を反映、配置されたボタンを押せばモーターが回転するようにします。 スマホ ⇒ wifi ⇒ raspberry Pi(html ⇒ javascript ⇒ pyrhon(cgi)) ## 使用部品 ・Raspberry Pi ZERO2 W ・タミヤツインモーターギアボックス ・DCモータードライバ(2CH) ・リード線 ・リポバッテリー × 2 ## 船体作成 @[twitter](https://twitter.com/ketunorobio/status/1522189465447403525?ref_src=twsrc%5Etfw%7Ctwcamp%5Etweetembed%7Ctwterm%5E1522189465447403525%7Ctwgr%5Eb8213dac6a88d37e1c3f471a383a1157116a9902%7Ctwcon%5Es1_&ref_url=https%3A%2F%2Fdenkenmusic.com%2Fraspberry-pie381a7e4bd9ce3828be5a496e8bcaae888b9e383a9e382b8e382b3e383b3e291a13d-cade381aee4bd9ce68890e38081e58db0e588b7e38081e6b5aee9818a%2F) 水車とモーターを繋ぐのにシャフトを通す訳ですが、船体にシャフト穴を空けるので、どうしてもそこから少量ながらも水が入り込んでしまいます。 浸水を防ぐ方法として、シャフトの上にもう一つ筒を作って、その中にグリスを注入するってのがあるみたいですが、面倒なのでとりあえずシャフト穴に直でグリスを塗りたくってやりました。。 今のところ、なんとか浸水は防げています。(長時間はやっていないからあれですが。。) ## プログラム ``` <!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="jquery-3.6.0.js"></script> <script src="main.js"></script> </body> </html> ``` jqueryはCDN読み込みだとボタン操作に不具合が生じたので、ローカルに落として使用しています。 カメラのストリーミングはmjpg-streamerを使用しています。 ``` * { margin: 0px; padding: 0px; } body { max-width: 600px; font-size: 25px; width: 100%; -webkit-touch-callout: none; -webkit-user-select: none; } img { width: 100%; } .cam { transform: scale(1, -1); } 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; background: skyblue; } .b { margin-left: auto; } .c { margin-right: auto; } ul li { text-align: center; } .a li, .b li, .c li, .d li { border: solid 1px; } #r, #l { border: none; } .ledon { background: #f88888; } .n li { background: skyblue; } a:active { color: #ff2020; } ``` ``` $(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 == "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'); }); }); ``` ``` #!/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; GPIO.setup(MOTOR_A1, GPIO.OUT) GPIO.setup(MOTOR_A2, GPIO.OUT) GPIO.setup(MOTOR_B1, GPIO.OUT) GPIO.setup(MOTOR_B2, 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) 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) 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) 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) 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) 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) 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) 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) 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.H ``` ## ハードウェアを収納 マイコンボードとDCモータードライバ、モーターギアボックス、バッテリの配線を繋いで、船体に収納します。 マイコンボードはピンソケットがあるとかなり場所を取るので、全て取っ払いました。リード線は半田で直付けします。 @[twitter](https://twitter.com/ketunorobio/status/1526313270998552576?ref_src=twsrc%5Etfw%7Ctwcamp%5Etweetembed%7Ctwterm%5E1526313270998552576%7Ctwgr%5E532604960652605c464a9de2ae17717eec110750%7Ctwcon%5Es1_&ref_url=https%3A%2F%2Fdenkenmusic.com%2Fraspberry-pie381a7e4bd9ce3828be5a496e8bcaae888b9e383a9e382b8e382b3e383b3e291a3e794b2e69dbfe381aee4bd9ce68890e38081e382abe383a1e383a9%2F) ## カメラ無しバージョン走行 @[twitter](https://twitter.com/ketunorobio/status/1526699242391080962?ref_src=twsrc%5Etfw%7Ctwcamp%5Etweetembed%7Ctwterm%5E1526699242391080962%7Ctwgr%5E532604960652605c464a9de2ae17717eec110750%7Ctwcon%5Es1_&ref_url=https%3A%2F%2Fdenkenmusic.com%2Fraspberry-pie381a7e4bd9ce3828be5a496e8bcaae888b9e383a9e382b8e382b3e383b3e291a3e794b2e69dbfe381aee4bd9ce68890e38081e382abe383a1e383a9%2F) まあまあ、ちゃんと動いております。 続いてカメラを実装します。 ## カメラ収納部の作成 @[twitter](https://twitter.com/ketunorobio/status/1527041601008041984?ref_src=twsrc%5Etfw%7Ctwcamp%5Etweetembed%7Ctwterm%5E1527041601008041984%7Ctwgr%5E532604960652605c464a9de2ae17717eec110750%7Ctwcon%5Es1_&ref_url=https%3A%2F%2Fdenkenmusic.com%2Fraspberry-pie381a7e4bd9ce3828be5a496e8bcaae888b9e383a9e382b8e382b3e383b3e291a3e794b2e69dbfe381aee4bd9ce68890e38081e382abe383a1e383a9%2F) もう修正する気力がなくなったので、このまま押し通す事に決めました。 それではこれにて完成です! ## カメラ有りバージョン走行 @[twitter](https://twitter.com/ketunorobio/status/1527394059886555137?ref_src=twsrc%5Etfw%7Ctwcamp%5Etweetembed%7Ctwterm%5E1527394059886555137%7Ctwgr%5E532604960652605c464a9de2ae17717eec110750%7Ctwcon%5Es1_&ref_url=https%3A%2F%2Fdenkenmusic.com%2Fraspberry-pie381a7e4bd9ce3828be5a496e8bcaae888b9e383a9e382b8e382b3e383b3e291a3e794b2e69dbfe381aee4bd9ce68890e38081e382abe383a1e383a9%2F)