Raspberry Pi搭載 水面ゴミ回収ラジコンボートの構成
今年はRaspberry Piを使って「水面ゴミ回収ラジコンボート」を制作してみようと思います。今回の構成は以下の通りです。ざっくりと考えてみた構成なので、うまくいかない部分は変更していこうと思っています。規模は小さいですが、個人レベルでこのようなラジコンが作れる時代になって嬉しいなと感じています。(2019/1/5)
RaspberryPiを搭載したボートは以下のようになりました。
第一章 プロトタイプを作る
まずはプロトタイプを作っていきます。プロトタイプを作ることで課題なども見えてくると思います。
必要な材料を用意する
今回は以下の材料を用意しました。
本体部分
部品名 | 個数 |
Raspberry Pi Zero WH スターターキット | 1 |
Webカメラ 「HD ウェブカメラ C270」 | 1 |
Goolsky Flytec HQ2011-1 ラジコンボート 27MHz 2CH 15km/h | 1 |
5自由度ロボットアーム・キット (SainSmart) | 1 |
回路部分
部品名 | 個数 |
ユニバーサル基板 | 1 |
DCモータドライブ「TA7291P」 | 2 |
ジャンパーワイヤ | 適量 |
モバイルバッテリー | 1 |
ハンダ | 1 |
ピンヘッダ | 適量 |
※DCモータドライブ「TA7291P」は生産終了予定になっています。しばらくしたら購入できなくなってるかもしれませんが今回はこれを使います。
工具
部品名 | 個数 |
+ドライバー | 1 |
ハンダごて | 1 |
電動ドリル | 1 |
ラジコンボートを分解する
ラジコンボートを購入しました!これをRaspberryPiで操作できるように改造していきます!
ボートの上部を取り外してもともと設置してあった回路を取り外しました。今回はこの部分に単三乾電池を設置していきます。中央の柱が邪魔だったりするので不要な部分はカットしていきます。
ロボットアームの組み立て(プロトタイプ2では取り外しました)
ロボットアームの組み立ては完了しました。手順については以下の記事に掲載しています。
RaspberryPiとサーボモータで5軸ロボットアームを動かしてみた
以降の手順は「Raspberry PiでWebから操作できるラジコンクローラーを作る」のときとほぼ一緒なので流用できるところはそのまま流用しています。ラジコンクローラでは「Raspberry Pi3 Model B」を使用しましたが、今回はスペースの関係上「Raspberry Pi Zero WH」を使用するようにしました。
Raspbian(Jessie)のインストール
RaspberryPiのOSにはRaspbian(Jessie)をインストールしておきます。後述で記載するRaspberry Piをアクセスポイント化して操作する場合にはRaspbianのJessieでないと方法を確立することができなかったためです。 今回は「Raspberry Pi3 Model B」もしくは「Raspberry Pi Zero WH」に「NOOBS_v2_4_1」で「Raspbian Jessie」をインストールした前提で以下の手順を実施していきます。(※Raspberry Pi3 Model B+にもNOOBS_v2_4_1を入れようとしてみましたが、インストールできませんでした。B+に上記のOSは入れられないかもしれません。。)
WebIOPIのインストール
まずはWebIOPIをインストールします。これをインストールするとブラウザからRaspberryPiのGPIOを操作することが可能となります。インストール方法については別の記事にまとめましたのでこちらをご参照ください。
WebIOPIをRaspberry Piにインストールする方法
RaspberryPiを起動するとWebIOPIも同時に起動するように設定しておきます。
Mjpg-Streamerのインストール
今回はRaspberryPiに設置したWebカメラ映像を操作画面に表示するために「Mjpg-Streamer」を使用しますので以下の記事に沿ってインストールしておきます。「Mjpg-Streamer」はRaspberryPiの起動時に同時に起動するように設定しておきましょう。
RaspberryPiとMJPG-Streamerでライブストリーミングをする方法
ラジコンの操作プログラムを作成
ブラウザから操作するようのプログラムを用意しておきます。ブラウザの表示には「HTML」、ロボットの操作には「Python」を使用します。 コンソールを起動し、以下のコマンドでプログラムファイルを格納するフォルダを作成しておきます。中間フォルダの名前は何でもいいです。
1 2 3 |
mkdir iot mkdir iot/python mkdir iot/html |
フォルダとファイルは以下のような構成にします。
htmlフォルダに移動し、「index.html」を作成します。HTMLにはMJPG-Streamerの動画を表示する部分と、ロボットを操縦する部分で構成します。ボタンが押されたらPythonのスクリプトが呼ばれてモーターが動くようになっています。 MJPG-Streamerの映像を表示するためにIPアドレスの部分は書き換えてください。アクセスポイント化していない場合は割り当てられているIPアドレスを、アクセスポイント化しているときはアクセスポイント化の手順で設定したIPアドレスを指定します。 今回はアクセスポイント化した後のIPアドレスを記載しています。
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 |
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <meta name="viewport" content="width=device-width"> <title>Controller</title> <script type="text/javascript" src="/webiopi.js"></script> <script type="text/javascript"> var imageNr = 0; // Serial number of current image var finished = new Array(); // References to img objects which have finished downloading var paused = false; function createImageLayer() { var img = new Image(); img.style.position = "absolute"; img.style.zIndex = -1; img.style.marginLeft="-160px"; img.onload = imageOnload; img.onclick = imageOnclick; img.src = "http://172.24.1.1:8080/?action=snapshot&n=" + (++imageNr); var webcam = document.getElementById("webcam"); webcam.insertBefore(img, webcam.firstChild); } // Two layers are always present (except at the very beginning), to avoid flicker function imageOnload() { this.style.zIndex = imageNr; // Image finished, bring to front! while (1 < finished.length) { var del = finished.shift(); // Delete old image(s) from document del.parentNode.removeChild(del); } finished.push(this); if (!paused) createImageLayer(); } function imageOnclick() { // Clicking on the image will pause the stream paused = !paused; if (!paused) createImageLayer(); } webiopi().ready(function() { var parts; parts =webiopi().createButton("forwardButton","GO",function() { webiopi().callMacro("forwardCrawler"); }) $("#forward").append(parts); parts =webiopi().createButton("stopButton","STOP",function() { webiopi().callMacro("stopCrawler"); }) $("#stop").append(parts); parts =webiopi().createButton("backwardButton","BACK",function() { webiopi().callMacro("backwardCrawler"); }) $("#backward").append(parts); parts =webiopi().createButton("leftButton","Left",function() { webiopi().callMacro("leftCrawler"); }) $("#left").append(parts); parts =webiopi().createButton("rightButton","Right",function() { webiopi().callMacro("rightCrawler"); }) $("#right").append(parts); $("#forwardButton").css('background-color','green'); $("#stopButton").css('background-color','green'); $("#backwardButton").css('background-color','green'); $("#leftButton").css('background-color','green'); $("#rightButton").css('background-color','green'); }); </script> <style type="text/css"> button { display: block; margin: 5px 5px 5px 5px; width: 80px; height: 35px; font-size: 17pt; font-weight: bold; color: black; } </style> </head> <body onload="createImageLayer();"> <div align="center"> <div id="webcam" style="width: 320px;height: 240px;" > <noscript> <img src="http://172.24.1.1:8080/?action=snapshot" /> </noscript> </div> </div> <div align="center"> <table> <tr> <td> <table> <tbody> <tr> <td> <div class="slidecontainer"> <input type="range" min="6" max="12" value="10" class="slider" id="myRange1"> </div> </td> <td> slider value :</td><td id="out1"></td> </tr> <tr> <td> <div class="slidecontainer"> <input type="range" min="6" max="24" value="15" class="slider" id="myRange2"> </div> </td> <td> slider value :</td><td id="out2"></td> </tr> <tr> <td> <div class="slidecontainer"> <input type="range" min="15" max="20" value="18" class="slider" id="myRange3"> </div> </td> <td> slider value :</td><td id="out3"></td> </tr> <tr> <td> <div class="slidecontainer"> <input type="range" min="11" max="19" value="15" class="slider" id="myRange4"> </div> </td> <td> slider value :</td><td id="out4"></td> </tr> <tr> <td> <div class="slidecontainer"> <input type="range" min="6" max="24" value="15" class="slider" id="myRange5"> </div> </td> <td> slider value :</td><td id="out5"></td> </tr> </tbody> </table> </td> </tr> </table> </div> <script> var slider1 = document.getElementById("myRange1"); var slider2 = document.getElementById("myRange2"); var slider3 = document.getElementById("myRange3"); var slider4 = document.getElementById("myRange4"); var slider5 = document.getElementById("myRange5"); var output1 = document.getElementById("out1"); var output2 = document.getElementById("out2"); var output3 = document.getElementById("out3"); var output4 = document.getElementById("out4"); var output5 = document.getElementById("out5"); output1.innerHTML = slider1.value; // Display the default slider value output2.innerHTML = slider2.value; // Display the default slider value output3.innerHTML = slider3.value; // Display the default slider value output4.innerHTML = slider4.value; // Display the default slider value output5.innerHTML = slider5.value; // Display the default slider value // Update the current slider value (each time you drag the slider handle) slider1.oninput = function(){ output1.innerHTML = slider1.value; var slidervalue = slider1.value; webiopi().callMacro("setHwPWMforPan1", slidervalue); }; slider2.oninput = function(){ output2.innerHTML = slider2.value; var slidervalue = slider2.value; webiopi().callMacro("setHwPWMforPan2", slidervalue); }; slider3.oninput = function(){ output3.innerHTML = slider3.value; var slidervalue = slider3.value; webiopi().callMacro("setHwPWMforPan3", slidervalue); }; slider4.oninput = function(){ output4.innerHTML = slider4.value; var slidervalue = slider4.value; webiopi().callMacro("setHwPWMforPan4", slidervalue); }; slider5.oninput = function(){ output5.innerHTML = slider5.value; var slidervalue = slider5.value; webiopi().callMacro("setHwPWMforPan5", slidervalue); }; </script> </div> <div align="center"> <table> <tr> <td> <table border=0 cellspacing="10″ cellpadding="0″> <tbody> <tr> <td></td> <td><div id="forward"></div></td> <td></td> </tr> <tr> <td><div id="left"></div></td> <td><div id="stop"></div></td> <td><div id="right"></div></td> </tr> <tr> <td></td> <td><div id="backward"></div></td> <td></td> </tr> </tbody> </table> </td> </tr> </table> </div> </div> </body> </html> |
次に「Python」フォルダに移動してラジコンボート操作用の「script.py」を作成します。HTMLでボタンが押されたら呼ばれるようになっています。動作は前進、後進、左旋回、右旋回、停止の動作となるようにしています。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 |
import webiopi import time import wiringpi as GPIO SV_PAN1 = 20 SV_PAN2 = 26 SV_PAN3 = 21 SV_PAN4 = 19 SV_PAN5 = 18 IN1 = 27 IN2 = 22 IN3 = 23 IN4 = 24 # SV_PAN (Left)90 ... 0 ... -90(Right) #val1 = [28,25,20,16,12,6,0] val2 = [28,27,26,25,24,23,22,21,19,18,17,16,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0] def getServoPanPWMvalue1(val): return val2[val] def getServoPanPWMvalue2(val): move_deg = int((9.5*val/180 + 2.5)*(1024/100)) return move_deg webiopi.setDebug() def setup(): webiopi.debug("Script with macros - Setup") GPIO.wiringPiSetupGpio() GPIO.pinMode(SV_PAN1,GPIO.OUTPUT) GPIO.softPwmCreate(SV_PAN1,0,50) GPIO.pinMode(SV_PAN2,GPIO.OUTPUT) GPIO.softPwmCreate(SV_PAN2,0,50) GPIO.pinMode(SV_PAN3,GPIO.OUTPUT) GPIO.softPwmCreate(SV_PAN3,0,50) GPIO.pinMode(SV_PAN4,GPIO.OUTPUT) # GPIO.pinMode(SV_PAN4,2) GPIO.softPwmCreate(SV_PAN4,0,50) GPIO.pinMode(SV_PAN5,GPIO.OUTPUT) # GPIO.pinMode(SV_PAN5,2) GPIO.softPwmCreate(SV_PAN5,0,50) # GPIO.pwmSetMode(0) # 0Vに指定 # GPIO.pwmSetRange(1024) # レンジを0~1024に指定 # GPIO.pwmSetClock(375) # 後述 GPIO.pinMode( IN1 , 1 ) GPIO.pinMode( IN2 , 1 ) GPIO.pinMode( IN3 , 1 ) GPIO.pinMode( IN4 , 1 ) def loop(): webiopi.sleep(5) def destroy(): webiopi.debug("Script with macros - Destroy") @webiopi.macro def setHwPWMforPan1(duty): GPIO.softPwmWrite(SV_PAN1, getServoPanPWMvalue1(int(duty))) @webiopi.macro def setHwPWMforPan2(duty): GPIO.softPwmWrite(SV_PAN2, getServoPanPWMvalue1(int(duty))) @webiopi.macro def setHwPWMforPan3(duty): GPIO.softPwmWrite(SV_PAN3, getServoPanPWMvalue1(int(duty))) @webiopi.macro def setHwPWMforPan4(duty): GPIO.softPwmWrite(SV_PAN4, getServoPanPWMvalue1(int(duty))) # GPIO.pwmWrite(SV_PAN4, getServoPanPWMvalue2(int(duty))) @webiopi.macro def setHwPWMforPan5(duty): GPIO.softPwmWrite(SV_PAN5, getServoPanPWMvalue1(int(duty))) # GPIO.pwmWrite(SV_PAN5, getServoPanPWMvalue2(int(duty))) @webiopi.macro def forwardCrawler(): GPIO.digitalWrite( IN1, 1 ) GPIO.digitalWrite( IN2, 0 ) GPIO.digitalWrite( IN3, 1 ) GPIO.digitalWrite( IN4, 0 ) @webiopi.macro def backwardCrawler(): GPIO.digitalWrite( IN1, 0 ) GPIO.digitalWrite( IN2, 1 ) GPIO.digitalWrite( IN3, 0 ) GPIO.digitalWrite( IN4, 1 ) @webiopi.macro def stopCrawler(): GPIO.digitalWrite( IN1, 0 ) GPIO.digitalWrite( IN2, 0 ) GPIO.digitalWrite( IN3, 0 ) GPIO.digitalWrite( IN4, 0 ) @webiopi.macro def leftCrawler(): GPIO.digitalWrite( IN1, 1 ) GPIO.digitalWrite( IN2, 0 ) # GPIO.digitalWrite( IN3, 0 ) # GPIO.digitalWrite( IN4, 1 ) @webiopi.macro def rightCrawler(): # GPIO.digitalWrite( IN1, 0 ) # GPIO.digitalWrite( IN2, 1 ) GPIO.digitalWrite( IN3, 1 ) GPIO.digitalWrite( IN4, 0 ) |
SDカードのバックアップを取得しておく
この段階でSDカードのバックアップを取得しておきます。RaspberryPiが壊れたりSDカードが壊れたりすることもありますので、被害が最小限になるようにバックアップを取得しておきます。とくに回路を動かすと発熱したりしてRaspberryPiが壊れたりすることが多いので、バックアップは必須だなと思いました。
回路の作成
RaspberryPiからラジコンボートを動かすために回路を作成します。ラジコンボートの中は狭いので今は以下の回路を半田付けして設置しています。ブログでは見やすいように大きいブレッドボードに配線しています。
まず、ラジコンに付属していたモーター2個はモータードライバーを経由して操作します。2本の線はモータードライバーの2番と10番のところに接続しています。次にRaspberryPiからは3.3vと5vの線をモータードライバーの4番と7番に接続しています。3.3vの部分に関してはPWM制御のときに使うことができるようなのですが、今回はPWM制御を使わずにモーターのオンとオフをするだけなのでそのまま3.3vにつないでいます。駆動用の電源としては単三乾電池4本(※変更しました)を使っています。これでラジコンボートのモーターと、ロボットアームのサーボモーターに電源を供給しています。これを間違って信号線に接続したりするとRaspberryPiが壊れてしまうことがあるので配線するときは要注意です。RaspberryPiの電源としてはモバイルバッテリーを使用しています。スイッチなどを用意していないのでモバイルバッテリーにつなげるとそのまま電源が入るようになっています。
今は設置していないのですが、今後はUSBポートにUSBカメラも接続して映像も取得できるようにしていきたいと思います。
ラジコンボートの組み立て
ラジコンボートを組み立てていきます。回路およびRaspberryPiはボートの中に格納し、ロボットアームはネジで固定します。ロボットアームを固定する用の穴と配線を通す用の穴は電動ドリルで空けておきます。駆動用の電源は当初、単三乾電池を8本セットしていましたが、4本(6V)で十分なことが分かりましたので4本だけでも動くように銅線で接続させています。
拡大するとこんな感じです。
RaspberryPiのアクセスポイント化
WiFiなどがない場所で操縦したい場合はRaspberryPiのアクセスポイント化を実施しておきます。そうすることで直接PCやスマホなどからRaspberryPiを操作することができるようになります。アクセスポイント化の方法は以下の記事にまとめてありますのでこちらをご参照ください。
Raspberry Pi3(Raspbian Jessie)をアクセスポイント化してWiFiで動画配信する
プロトタイプが完成
プロトタイプが完成したので池で操作してきました☆(2019/8/10)
まだ一度も水に浮かべたことがなかったのでWebカメラは設置していない状態で試してみました。
想定はしていましたが、水面に浮かべようとしたら転覆しそうになります。。
重心高いし、ロボット アーム重いしそうだよね~って思いながらすぐにロボット アームを取り外しました。
デフォルトの状態だとさすがに動かすことができました。
ロボットアームを取ると転覆せずに動いてくれました。笑
— Kazuki (@RoomKazuki) August 10, 2019
バランスって大事ですね。
船舶工学を学びなおしますw
ラジコンボートを操縦したあとに中身の浸水状況を確認してみましたが、これについては大丈夫そうでした。でも可能性は十分あり得るので防水対策は何かしらやっておいたほうがよさそうですね。今はまったくやっていない状況です。
浮力を調整してみる
ロボットアームを設置すると重くなり、しかもバランスが悪くなるので転覆してしまうことが分かりました。そこで船の両サイドにペットボトルを設置して浮力を上げることにしました。(2019/8/18)
ボートの浮力を安定させるにあたり、こちらのサイトが分かりやすかったので参考にさせていただきました。このサイトによると両サイドに補助的な船体をもつものはアウトリガー船と呼ばれるそうです。
ペットボトルを設置したあとはちゃんと船が転覆せずに浮かんでくれたので作戦成功です。
転覆したりノーコンになったりすると岸に戻せなくなってしまい、ゴミ回収用に開発しているのにゴミを生み出しては元も子もないので、そういう場合でも回収できるようにラジコンボートには釣り糸を結び付けて操縦しています。
見た目は少しだけ不格好になりましたが、実用性を考えれば致し方無いでしょう。かなりバランスはよくなりました。
こちらが少しだけ動かしてみた様子です。
池でロボットアーム付きラジコンボートを動かしてきました。
— Kazuki (@RoomKazuki) August 18, 2019
ペットボトルで浮力マシマシ作戦が上手くいったので転覆しないようになりました。
浮力の調整が必要だったり、ロボットアームの操作性だったりと、具体的な課題が分かってきたので少しずつ改善していきたいと思います。
第二章 プロトタイプの改善版を作る
第一章で課題などが見えてきましたので、次はその課題を解決していきたいと思います。
ロボットアームの操作性を改善する
第一章まではロボットアームがガタガタして操作性がよくなかったのでこれの改善版を作成しました。サーボモータの制御にpigpioというライブラリを使用するように変更し、これによってかなり滑らかに動かすことができるようになりました。
改善版のindex.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 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 |
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <meta name="viewport" content="width=device-width"> <title>Controller</title> <script type="text/javascript" src="/webiopi.js"></script> <script type="text/javascript"> var imageNr = 0; // Serial number of current image var finished = new Array(); // References to img objects which have finished downloading var paused = false; function createImageLayer() { var img = new Image(); img.style.position = "absolute"; img.style.zIndex = -1; img.style.marginLeft="-160px"; img.onload = imageOnload; img.onclick = imageOnclick; img.src = "http://172.24.1.1:8080/?action=snapshot&n=" + (++imageNr); var webcam = document.getElementById("webcam"); webcam.insertBefore(img, webcam.firstChild); } // Two layers are always present (except at the very beginning), to avoid flicker function imageOnload() { this.style.zIndex = imageNr; // Image finished, bring to front! while (1 < finished.length) { var del = finished.shift(); // Delete old image(s) from document del.parentNode.removeChild(del); } finished.push(this); if (!paused) createImageLayer(); } function imageOnclick() { // Clicking on the image will pause the stream paused = !paused; if (!paused) createImageLayer(); } webiopi().ready(function() { var parts; parts =webiopi().createButton("forwardButton","GO",function() { webiopi().callMacro("forwardCrawler"); }) $("#forward").append(parts); parts =webiopi().createButton("stopButton","STOP",function() { webiopi().callMacro("stopCrawler"); }) $("#stop").append(parts); parts =webiopi().createButton("backwardButton","BACK",function() { webiopi().callMacro("backwardCrawler"); }) $("#backward").append(parts); parts =webiopi().createButton("leftButton","Left",function() { webiopi().callMacro("leftCrawler"); }) $("#left").append(parts); parts =webiopi().createButton("rightButton","Right",function() { webiopi().callMacro("rightCrawler"); }) $("#right").append(parts); $("#forwardButton").css('background-color','green'); $("#stopButton").css('background-color','green'); $("#backwardButton").css('background-color','green'); $("#leftButton").css('background-color','green'); $("#rightButton").css('background-color','green'); }); </script> <style type="text/css"> button { display: block; margin: 5px 5px 5px 5px; width: 80px; height: 35px; font-size: 17pt; font-weight: bold; color: black; } </style> </head> <body onload="createImageLayer();"> <div align="center"> <div id="webcam" style="width: 320px;height: 240px;" > <noscript> <img src="http://172.24.1.1:8080/?action=snapshot" /> </noscript> </div> </div> <div align="center"> <table> <tr> <td> <table> <tbody> <tr> <td> <div class="slidecontainer"> <input type="range" min="100" max="160" value="130" class="slider" id="myRange1"> </div> </td> <td>slider value :</td> <td id="out1"></td> </tr> <tr> <td> <div class="slidecontainer"> <input type="range" min="0" max="180" value="90" class="slider" id="myRange2"> </div> </td> <td>slider value :</td> <td id="out2"></td> </tr> <tr> <td> <div class="slidecontainer"> <input type="range" min="0" max="70" value="50" class="slider" id="myRange3"> </div> </td> <td>slider value :</td> <td id="out3"></td> </tr> <tr> <td> <div class="slidecontainer"> <input type="range" min="50" max="120" value="100" class="slider" id="myRange4"> </div> </td> <td>slider value :</td> <td id="out4"></td> </tr> <tr> <td> <div class="slidecontainer"> <input type="range" min="0" max="180" value="90" class="slider" id="myRange5"> </div> </td> <td>slider value :</td> <td id="out5"></td> </tr> </tbody> </table> </td> </tr> </table> </div> <script> var slider1 = document.getElementById("myRange1"); var slider2 = document.getElementById("myRange2"); var slider3 = document.getElementById("myRange3"); var slider4 = document.getElementById("myRange4"); var slider5 = document.getElementById("myRange5"); var output1 = document.getElementById("out1"); var output2 = document.getElementById("out2"); var output3 = document.getElementById("out3"); var output4 = document.getElementById("out4"); var output5 = document.getElementById("out5"); output1.innerHTML = slider1.value; // Display the default slider value output2.innerHTML = slider2.value; // Display the default slider value output3.innerHTML = slider3.value; // Display the default slider value output4.innerHTML = slider4.value; // Display the default slider value output5.innerHTML = slider5.value; // Display the default slider value // Update the current slider value (each time you drag the slider handle) slider1.oninput = function(){ output1.innerHTML = slider1.value; var slidervalue = slider1.value; webiopi().callMacro("setServoPWM1", slidervalue); }; slider2.oninput = function(){ output2.innerHTML = slider2.value; var slidervalue = slider2.value; webiopi().callMacro("setServoPWM2", slidervalue); }; slider3.oninput = function(){ output3.innerHTML = slider3.value; var slidervalue = slider3.value; webiopi().callMacro("setServoPWM3", slidervalue); }; slider4.oninput = function(){ output4.innerHTML = slider4.value; var slidervalue = slider4.value; webiopi().callMacro("setServoPWM4", slidervalue); }; slider5.oninput = function(){ output5.innerHTML = slider5.value; var slidervalue = slider5.value; webiopi().callMacro("setServoPWM5", slidervalue); }; </script> </div> <div align="center"> <table> <tr> <td> <table border=0 cellspacing="10″ cellpadding="0″> <tbody> <tr> <td></td> <td> <div id="forward"> </div> </td> <td></td> </tr> <tr> <td> <div id="left"> </div> </td> <td> <div id="stop"> </div> </td> <td> <div id="right"> </div> </td> </tr> <tr> <td></td> <td> <div id="backward"> </div> </td> <td></td> </tr> </tbody> </table> </td> </tr> </table> </div> </div> </body> </html> |
つぎに改善版のscript.pyはこちらです。上記の回路図とは使用しているGPIOが異なりますので、今回のプログラムに合わせて配線を変更しておきます。
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 |
import webiopi import time import pigpio GPIO = webiopi.GPIO pi = pigpio.pi() SV_1 = 4 SV_2 = 13 SV_3 = 26 SV_4 = 12 SV_5 = 16 IN1 = 27 IN2 = 14 IN3 = 25 IN4 = 24 def getServoPWMvalue(val): deg = int(1900/180*val) + 500 return deg # Return 500 ~ 2400 def setup(): pi.set_mode(SV_1, pigpio.OUTPUT) pi.set_mode(SV_2, pigpio.OUTPUT) pi.set_mode(SV_3, pigpio.OUTPUT) pi.set_mode(SV_4, pigpio.OUTPUT) pi.set_mode(SV_5, pigpio.OUTPUT) pi.set_mode(IN1, pigpio.OUTPUT) pi.set_mode(IN2, pigpio.OUTPUT) pi.set_mode(IN3, pigpio.OUTPUT) pi.set_mode(IN4, pigpio.OUTPUT) def loop(): webiopi.sleep(5) def destroy(): webiopi.debug("Script with macros - Destroy") @webiopi.macro def setServoPWM1(val): pi.set_servo_pulsewidth(SV_1, getServoPWMvalue(int(val))) @webiopi.macro def setServoPWM2(val): pi.set_servo_pulsewidth(SV_2, getServoPWMvalue(int(val))) @webiopi.macro def setServoPWM3(val): pi.set_servo_pulsewidth(SV_3, getServoPWMvalue(int(val))) @webiopi.macro def setServoPWM4(val): pi.set_servo_pulsewidth(SV_4, getServoPWMvalue(int(val))) @webiopi.macro def setServoPWM5(val): pi.set_servo_pulsewidth(SV_5, getServoPWMvalue(int(val))) @webiopi.macro def forwardCrawler(): pi.write( IN1, 1 ) pi.write( IN2, 0 ) pi.write( IN3, 1 ) pi.write( IN4, 0 ) @webiopi.macro def backwardCrawler(): pi.write( IN1, 0 ) pi.write( IN2, 1 ) pi.write( IN3, 0 ) pi.write( IN4, 1 ) @webiopi.macro def stopCrawler(): pi.write( IN1, 0 ) pi.write( IN2, 0 ) pi.write( IN3, 0 ) pi.write( IN4, 0 ) @webiopi.macro def leftCrawler(): pi.write( IN1, 1 ) pi.write( IN2, 0 ) #pi.write( IN3, 0 ) #pi.write( IN4, 1 ) @webiopi.macro def rightCrawler(): #pi.write( IN1, 0 ) #pi.write( IN2, 1 ) pi.write( IN3, 1 ) pi.write( IN4, 0 ) |
pigpioのインストール
以下のコマンドでpigpioをインストールしておきます。私の場合はすでにインストールされていました。
1 2 |
sudo apt-get update sudo apt-get install pigpio |
pigpioのデーモンを設定
上記のファイルを用意できたらpigpioのデーモンを自動で起動するようにしておきます。pigpioを使用する場合、デーモンを先に起動しておかないとエラーになってしまいます。これもRaspberryPiが起動したら自動で起動するように以下の設定をしておきます。
①以下のコマンドを実行してファイルを作成する
1 |
sudo nano /etc/systemd/system/pigpiod.service |
②作成したファイルの中身に以下の内容を記載して保存します
1 2 3 4 5 6 7 8 9 10 |
[Unit] Description = pigpio daemon [Service] ExecStart = /usr/bin/pigpiod Restart = always Type = forking [Install] WantedBy = multi-user.target |
③以下のコマンドを実行して自動起動するようにしておく
1 |
sudo systemctl enable pigpiod.service |
こちらを実行しておくとRaspberryPiを起動したときにpigpioのデーモンが自動で起動するようになりました。
改良版ロボットアームを操作してみる
プログラムを変更して操作してみたときの様子がこちらです。改善前に比べてかなり滑らかに動いてくれるようになりました。
pigpioに切り替えたら滑らかに動くようになりました。
— Kazuki (@RoomKazuki) September 13, 2019
基本動作はこれで満足です。 pic.twitter.com/gRAaeUPFH2
ロボットアームは取り外しました
ロボットアームでゴミをつかもうとしてきまししたが、なかなか操作が難しいのと関節が多くなってしまうので、今回のプロジェクトには向かないかなと判断しました。なので、ロボットアームは取り外しクローラ型に切り替えてみました。
クローラ型にしてみた形態がこちら。両サイドにサーボモータを1個ずつ配置してアームでゴミを持ち上げる作戦です。
こちらも実際に池で動かしてみました。
バージョン2を操縦してみたときの動画です。
— Kazuki (@RoomKazuki) September 17, 2019
アームは持ち上げた状態にしないと抵抗になってしまいます。喫水線がギリギリになってしまいました。
ボート本体の操縦性はいい感じです。#RaspberryPi#ラジコン pic.twitter.com/OYkRAJyrSh
ピンポン玉は回収することに成功しました。500mL のペットボトルにも挑戦しましたが、風で流されたり横向きにペットボトルがならなかったりで難しかったです。もう少しアームが広くないと厳しいかもしれないですね。
サーボモータのトルクは大丈夫そうでした。まあ、そんなに重い物を持ち上げているわけでもないので。
ただ、前方のほうが重心が移ったせいで喫水線がギリギリになってしまいました。あとちょっとで浸水しそう。。
また浮力の調整が必要そうです。
プロトタイプ2を動かしてみて見えてきた課題
・ペットボトルが縦に流れてきたときはアームに乗せにくい → アームの面積を広くする、仕切り板を設置する
・アームを下げたときは水の抵抗が大きい → アームは上げた状態で走行する、肉抜きして抵抗を減らす
・ゴミ置き場のスペースが狭い → カゴを設置してスペースを広くとる
・ボートの前方の浮力が足りない(ボート前方が重い) → 結果的に単三乾電池4本を減らせた
・ボートの推進力がもうちょい欲しい → 他のモーターを検討する
・電源を入れずらい、メンテナンスしにくい → メンテしやすい回路を作り直す、電源スイッチは外に設置する
・サイズが大きくなってきて持ち運びにくい → すぐに分解できるようにする、上部と下部で切り離し?
・練習する場所がない(流れがなくて人が少ないところが理想) → 人が少ない早朝に行くしかない
・サーボの固定が弱い、できれば両軸持ちが理想 → アルミフレームを作り直す、両持ちは後々検討
サイズ感を見直す
サイズ感としてはこのような感じです。500mLペットボトルを横にするとちょうどいいぐらいのサイズになっています。
この向きになってくれないとなかなか拾えないんですよね~どうしよ。
アルミの板をもう少し広くしてみるとかはできるんですけど、全体的なバランス(見た目)が悪くなりそうなので嫌なんですよね~。サーボモータを後ろのほうにもう少し移動させてアルミ板を広くするとかならアリかもしれません。
結構重たくなるので不要な部分はニッパーとかで切り取っていきたいですね。
アルミ板を広くしてみる
仮付けでアルミ板を広くしてみました。予想通り、これならペットボトルが縦になっても拾ってくれそうな気配があります。
失敗するほうが多いので仕切り板を付けるとか工夫は必要そうですね。
理想的な拾い方。やっぱりペットボトルが横になってくれないと拾いづらい。 pic.twitter.com/Q4R5n6onlU
— Kazuki (@RoomKazuki) September 18, 2019
電圧を測定してみる
今回のラジコンで使用しているモーターの電源を改めて測定してみようと思います。
まずはモータードライバーからの出力電圧を測定してみました。
約4Vぐらいの電圧がラジコンボートのモーターにはかかっていることが分かりました。駆動用の単三乾電池(4本→8本)を増やしてもこれに変化はありませんでした。モータードライバーの4番ピンには常時3.3vしかかけていないので、これが原因のようです。かと言って、RaspberryPiのGPIOからはMAX3.3Vしか取り出せないのでモーターの電圧もこれ以上あげられそうにありません。
電圧を上げるためには別の方法を探したほうがいいかもしれません。こちらの記事にあるMOSFETを利用した方法がよさそうです。
上記の記事で紹介されているキットがこちら。こちらを購入するか検討中です。
ロボットアームのほうの電圧も測定してみます。
ロボットアームの電圧は約6Vとなっています。サーボモーターの適正電圧を考慮するとこれで大丈夫そうです。購入したラジコンボートにはもともと単三電池8本(12V)が直列接続されていたため、4本(6V)で動作するようにしています。
電池が4本少なくなると重量もかなり軽くなるので、その点は良かったかなと思います。
ラジコンボートのモーターをもう少しトルクのあるものとかに替えると動作電圧が高くなりそうなので、そのときは単三電池を8本に戻してみたいと思います。
カゴを設置する
今まではゴミなどを回収するときに後方のネットの上に置くようにしていましたが、それだけだとネット上から落ちてしまうことが多々ありました。そこでカゴを設置して保持力を高めることにしました。カゴは百均で丁度いい大きさのものを探してきてカットしました。
カゴを設置して動かしてみた様子がこちら。
カゴを付けて保持力をアップさせてみた。
— Kazuki (@RoomKazuki) September 23, 2019
これぐらいのサイズがあればペットボトルの回収も問題なさそう。 pic.twitter.com/IdPam46Ivy
両サイドで2本ずつぐらいなら500mLペットボトルを回収できそうです。まだカゴを付けてからは池で試走させてないので予想ですけど。
それにしてもかなりゴツくなってきました。笑
用途を考慮するとやっぱり大きいサイズのほうがいろいろとメリットあるんですよね。
荒川河川敷で走らせた場所
荒川でラジコンボートを走らせようとすると流れや風でラジコンボートが流されてしまう可能性があります。そこで今回は河川の「ワンド」と言われているところで走らせてきました。 こちらは河川敷で池のようになっている場所でしたので、流れも少なくていいかもと思いましてこちらのほうで実施しました。
どのような結果になったか
ラジコンボートは好調に走ってくれました。植物が生えている範囲が結構あったのでそちらのほうを避けてボートは動かすようにしました。風があるとやはり流されてしまって操縦するのが難しかったです。ペットボトルを配置して回収してみようと思いチャレンジしてみました。慎重に操縦すると回収することは可能でしたが、なかなか操縦は難しかったです。まず、旋回ができないので徐々にボートの向きを変えるしかないこと、取り込める範囲が狭いのでそれの位置調整が大変なことが挙げられます。頑張ったことでペットボトルはすべて回収することができました。
本日は荒川でラズパイボートを操縦してきました。 ゴミを拾えるようになるのはまだ難しいです。まだまだ改良していきます。#Raspberrypi #ラジコン pic.twitter.com/TWpZa22tDx
— Kazuki (@RoomKazuki) October 5, 2019
今後の課題
今回もいろいろと課題が見えてきましたのでまとめておきたいと思います。
・旋回性能が不十分すぎる
今回行ってみて思いましたが、旋回ができないと思うようにゴミを取り込むことができません。よって今後は旋回できるようにしていこうと思います。
・取り込み部分は一箇所で十分
操縦していて感じたのは、取り込み部分が二箇所あっても両方を同時に使うことはないなと思いました。人間が意識を向けられるのは一つぐらいなので二箇所あっても操作できません。それであれば一箇所に絞って行ったほうが効率的なような気がします。今後はそのような構成にしようと思います。
・モーターが弱い
ボートが極端に遅いということはないですけど、もう少しスピードがあってもいいかなと感じています。モーターはもう少し大きめのものを設置して速度とトルクを上げたいと思います。
・防水にするべき(やっとけ)
今回は防水対策を実施していなかったので、近くで船が通った際にその波であっさりと瞬殺されてしまいました。RaspberryPiとか回路とかすべて破損してしまいました。今まで壊れなかったのが不思議なぐらいなので、今後はしっかりと防水対策を実施していこうと思います。中身はすべてやられてしまいました↓
荒川を走っていたモーターボートの波に瞬殺された瞬間です。
大したことない波でも機械はやられてしまいます。(防水対策しとけ) pic.twitter.com/mlocZNLd8Y — Kazuki (@RoomKazuki) October 5, 2019
・釣り糸は自動巻取りにしたい
ラジコンが操縦不能となった際には釣り糸で回収できるようにしていたのですが、こちらを手動で行っているとラジコンボートの操縦が疎かになってしまいます。また記録用の動画などを撮影しようとすると、釣り糸・操縦用のスマホ・カメラを同時に操作することになり、水辺でそのようなことをするのも危険です。よって意識を分散させないように釣り糸は自動で巻き取れる仕組みを構築しようと思います。
・日光の下だとスマホの画面が見にくい
これは誰でも経験したことがあると思うのですが、明るいときにスマホの画面を見ると何が映っているのか全然分かりません。コントローラのボタンの位置もそうですし、ボートに設置しているWEBカメラの映像も見えません。だったらラジコンボートの操縦はプロポで行ったほうがいい気がします。また、カメラの映像を見ている余裕もないのでカメラも取り外したほうがいいかもしれません。 この項目については要検討となります。
画面にはこのような操作画面が映っています。
次はざっくりと以下のような構成にしたいと考えています。
続きはこちらの記事に書いていきます。次回からはプロポを使って操作してみたいと思います。
当ブログの関連記事
他のこともしながら少しずつ進めています。以下が当ブログの関連記事です。
RaspberryPiとサーボモータで5軸ロボットアームを動かしてみた
Raspberry PiでWebから操作できるラジコンクローラーを作る
WebIOPIをRaspberry Piにインストールする方法
Google Colaboratory上でYOLOを動かして画像認識させてみた
OpenCVとC++とVisualStudioで顔認識してみる
参考にするブログ
今後はこの方のブログを参考にさせてもらいたいと思います。
コメントを残す