海や川でゴミ回収できるRaspberryPiラジコンボートを作る その1

海や川でゴミ回収できるRaspberryPiラジコンボートを作る その1

Raspberry Pi搭載 水面ゴミ回収ラジコンボートの構成

今年はRaspberry Piを使って「水面ゴミ回収ラジコンボート」を制作してみようと思います。今回の構成は以下の通りです。ざっくりと考えてみた構成なので、うまくいかない部分は変更していこうと思っています。規模は小さいですが、個人レベルでこのようなラジコンが作れる時代になって嬉しいなと感じています。(2019/1/5)

 

今現在はこのような感じになっています。

こちらの記事は長くなってしまいましたので最新の記事はこちらに掲載しています↓

海や川でゴミ回収できるRaspberryPiラジコンボートを作る その2

第一章 プロトタイプを作る

まずはプロトタイプを作っていきます。プロトタイプを作ることで課題なども見えてくると思います。

必要な材料を用意する

今回は以下の材料を用意しました。

本体部分

部品名 個数
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」を使用します。 コンソールを起動し、以下のコマンドでプログラムファイルを格納するフォルダを作成しておきます。中間フォルダの名前は何でもいいです。

mkdir iot
mkdir iot/python
mkdir iot/html

フォルダとファイルは以下のような構成にします。 

htmlフォルダに移動し、「index.html」を作成します。HTMLにはMJPG-Streamerの動画を表示する部分と、ロボットを操縦する部分で構成します。ボタンが押されたらPythonのスクリプトが呼ばれてモーターが動くようになっています。 MJPG-Streamerの映像を表示するためにIPアドレスの部分は書き換えてください。アクセスポイント化していない場合は割り当てられているIPアドレスを、アクセスポイント化しているときはアクセスポイント化の手順で設定したIPアドレスを指定します。 今回はアクセスポイント化した後のIPアドレスを記載しています。

<!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&amp;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の内容を書き換えても変更内容が反映されない場合は一度リスタートしてみてください。

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カメラは設置していない状態で試してみました。

想定はしていましたが、水面に浮かべようとしたら転覆しそうになります。。

重心高いし、ロボット アーム重いしそうだよね~って思いながらすぐにロボット アームを取り外しました。

デフォルトの状態だとさすがに動かすことができました。

ラジコンボートを操縦したあとに中身の浸水状況を確認してみましたが、これについては大丈夫そうでした。でも可能性は十分あり得るので防水対策は何かしらやっておいたほうがよさそうですね。今はまったくやっていない状況です。

浮力を調整してみる

ロボットアームを設置すると重くなり、しかもバランスが悪くなるので転覆してしまうことが分かりました。そこで船の両サイドにペットボトルを設置して浮力を上げることにしました。(2019/8/18)

ボートの浮力を安定させるにあたり、こちらのサイトが分かりやすかったので参考にさせていただきました。このサイトによると両サイドに補助的な船体をもつものはアウトリガー船と呼ばれるそうです。

ペットボトルを設置したあとはちゃんと船が転覆せずに浮かんでくれたので作戦成功です。

転覆したりノーコンになったりすると岸に戻せなくなってしまい、ゴミ回収用に開発しているのにゴミを生み出しては元も子もないので、そういう場合でも回収できるようにラジコンボートには釣り糸を結び付けて操縦しています。

見た目は少しだけ不格好になりましたが、実用性を考えれば致し方無いでしょう。かなりバランスはよくなりました。

こちらが少しだけ動かしてみた様子です。

 

浮力の調整が必要だったり、ロボットアームの操作性だったりと、具体的な課題が分かってきたので少しずつ改善していきたいと思います。

第二章 プロトタイプの改善版を作る

第一章で課題などが見えてきましたので、次はその課題を解決していきたいと思います。

ロボットアームの操作性を改善する

第一章まではロボットアームがガタガタして操作性がよくなかったのでこれの改善版を作成しました。サーボモータの制御にpigpioというライブラリを使用するように変更し、これによってかなり滑らかに動かすことができるようになりました。

改善版のindex.htmlはこちらです。

<!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&amp;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が異なりますので、今回のプログラムに合わせて配線を変更しておきます。

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をインストールしておきます。私の場合はすでにインストールされていました。

sudo apt-get update
sudo apt-get install pigpio

pigpioのデーモンを設定

上記のファイルを用意できたらpigpioのデーモンを自動で起動するようにしておきます。pigpioを使用する場合、デーモンを先に起動しておかないとエラーになってしまいます。これもRaspberryPiが起動したら自動で起動するように以下の設定をしておきます。

①以下のコマンドを実行してファイルを作成する

sudo nano /etc/systemd/system/pigpiod.service

②作成したファイルの中身に以下の内容を記載して保存します

[Unit]
Description = pigpio daemon

[Service]
ExecStart = /usr/bin/pigpiod
Restart = always
Type = forking

[Install]
WantedBy = multi-user.target

③以下のコマンドを実行して自動起動するようにしておく

sudo systemctl enable pigpiod.service

こちらを実行しておくとRaspberryPiを起動したときにpigpioのデーモンが自動で起動するようになりました。

改良版ロボットアームを操作してみる

プログラムを変更して操作してみたときの様子がこちらです。改善前に比べてかなり滑らかに動いてくれるようになりました。

ロボットアームは取り外しました

ロボットアームでゴミをつかもうとしてきまししたが、なかなか操作が難しいのと関節が多くなってしまうので、今回のプロジェクトには向かないかなと判断しました。なので、ロボットアームは取り外しクローラ型に切り替えてみました。

クローラ型にしてみた形態がこちら。両サイドにサーボモータを1個ずつ配置してアームでゴミを持ち上げる作戦です。

こちらも実際に池で動かしてみました。

ピンポン玉は回収することに成功しました。500mL のペットボトルにも挑戦しましたが、風で流されたり横向きにペットボトルがならなかったりで難しかったです。もう少しアームが広くないと厳しいかもしれないですね。

サーボモータのトルクは大丈夫そうでした。まあ、そんなに重い物を持ち上げているわけでもないので。

ただ、前方のほうが重心が移ったせいで喫水線がギリギリになってしまいました。あとちょっとで浸水しそう。。

また浮力の調整が必要そうです。

プロトタイプ2を動かしてみて見えてきた課題

・ペットボトルが縦に流れてきたときはアームに乗せにくい → アームの面積を広くする、仕切り板を設置する

・アームを下げたときは水の抵抗が大きい → アームは上げた状態で走行する、肉抜きして抵抗を減らす

・ゴミ置き場のスペースが狭い → カゴを設置してスペースを広くとる

・ボートの前方の浮力が足りない(ボート前方が重い) → 結果的に単三乾電池4本を減らせた

・ボートの推進力がもうちょい欲しい → 他のモーターを検討する

・電源を入れずらい、メンテナンスしにくい → メンテしやすい回路を作り直す、電源スイッチは外に設置する

・サイズが大きくなってきて持ち運びにくい → すぐに分解できるようにする、上部と下部で切り離し?

・練習する場所がない(流れがなくて人が少ないところが理想) → 人が少ない早朝に行くしかない

・サーボの固定が弱い、できれば両軸持ちが理想 → アルミフレームを作り直す、両持ちは後々検討

サイズ感を見直す

サイズ感としてはこのような感じです。500mLペットボトルを横にするとちょうどいいぐらいのサイズになっています。

この向きになってくれないとなかなか拾えないんですよね~どうしよ。

アルミの板をもう少し広くしてみるとかはできるんですけど、全体的なバランス(見た目)が悪くなりそうなので嫌なんですよね~。サーボモータを後ろのほうにもう少し移動させてアルミ板を広くするとかならアリかもしれません。

結構重たくなるので不要な部分はニッパーとかで切り取っていきたいですね。

アルミ板を広くしてみる

仮付けでアルミ板を広くしてみました。予想通り、これならペットボトルが縦になっても拾ってくれそうな気配があります。

失敗するほうが多いので仕切り板を付けるとか工夫は必要そうですね。

電圧を測定してみる

今回のラジコンで使用しているモーターの電源を改めて測定してみようと思います。

まずはモータードライバーからの出力電圧を測定してみました。

約4Vぐらいの電圧がラジコンボートのモーターにはかかっていることが分かりました。駆動用の単三乾電池(4本→8本)を増やしてもこれに変化はありませんでした。モータードライバーの4番ピンには常時3.3vしかかけていないので、これが原因のようです。かと言って、RaspberryPiのGPIOからはMAX3.3Vしか取り出せないのでモーターの電圧もこれ以上あげられそうにありません。

電圧を上げるためには別の方法を探したほうがいいかもしれません。こちらの記事にあるMOSFETを利用した方法がよさそうです。

MOSFETとマイコン (3)  DCモータを動かす その1

上記の記事で紹介されているキットがこちら。こちらを購入するか検討中です。

TB6612使用 Dual DCモータードライブキット

 

ロボットアームのほうの電圧も測定してみます。

ロボットアームの電圧は約6Vとなっています。サーボモーターの適正電圧を考慮するとこれで大丈夫そうです。購入したラジコンボートにはもともと単三電池8本(12V)が直列接続されていたため、4本(6V)で動作するようにしています。

電池が4本少なくなると重量もかなり軽くなるので、その点は良かったかなと思います。

ラジコンボートのモーターをもう少しトルクのあるものとかに替えると動作電圧が高くなりそうなので、そのときは単三電池を8本に戻してみたいと思います。

カゴを設置する

今まではゴミなどを回収するときに後方のネットの上に置くようにしていましたが、それだけだとネット上から落ちてしまうことが多々ありました。そこでカゴを設置して保持力を高めることにしました。カゴは百均で丁度いい大きさのものを探してきてカットしました。

カゴを設置して動かしてみた様子がこちら。

両サイドで2本ずつぐらいなら500mLペットボトルを回収できそうです。まだカゴを付けてからは池で試走させてないので予想ですけど。

それにしてもかなりゴツくなってきました。笑

用途を考慮するとやっぱり大きいサイズのほうがいろいろとメリットあるんですよね。

記事が長くなってしまいましたので続きは別の記事に掲載していきます↓

海や川でゴミ回収できるRaspberryPiラジコンボートを作る その2

さらにその続きです。こちらの記事からプロポを使用しています。

海や川でゴミ回収できるロボットを作る

当ブログの関連記事

他のこともしながら少しずつ進めています。以下が当ブログの関連記事です。

RaspberryPiとサーボモータで5軸ロボットアームを動かしてみた

Raspberry PiでWebから操作できるラジコンクローラーを作る

WebIOPIをRaspberry Piにインストールする方法

Google Colaboratory上でYOLOを動かして画像認識させてみた

YOLOでペットボトルの物体検出をやってみた

カスケード分類器でペットボトルを判別してみる

OpenCVとWebカメラでリアルタイム顔認識をする

OpenCVとC++とVisualStudioで顔認識してみる

 

参考にするブログ

今後はこの方のブログを参考にさせてもらいたいと思います。

Raspberry Piによる戦艦大和プラモデル ラジコン化 完結編 – 抜錨!大和 外洋(洲原池)に向け発進!

IoTカテゴリの最新記事