2022年 SPRESENSE™ 活用コンテスト 」開催中!ローパワーでハイパフォーマンスなエッジコンピューティングを、あなたの手で活用してみませんか?

Ketunorobioのアイコン画像
Ketunorobio 2022年08月03日作成
製作品 製作品 閲覧数 110
Ketunorobio 2022年08月03日作成 製作品 製作品 閲覧数 110

外輪船ラジコン

外輪船ラジコン

先日、家族で奈良県にある生駒山上遊園地に行ってまいりました。

そこで少し風変りな船の乗り物を見かけたので、子供と一緒に乗ってみました。形状はこのような感じです。↓

船体の横に水車が付いているので、「外輪船」と呼ばれているみたいです。中にあるクランクを回すと水車も回る仕組みで、組み合わせによって前進、後退、左旋回、右旋回、前右左折、後右左折が出来ます。

これが意外とスムーズに進むものでして、少し関心していた時に「あ、これ作れそうだな」に至った次第です。

外輪船の構成

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

船体作成

水車とモーターを繋ぐのにシャフトを通す訳ですが、船体にシャフト穴を空けるので、どうしてもそこから少量ながらも水が入り込んでしまいます。

浸水を防ぐ方法として、シャフトの上にもう一つ筒を作って、その中にグリスを注入するってのがあるみたいですが、面倒なのでとりあえずシャフト穴に直でグリスを塗りたくってやりました。。

今のところ、なんとか浸水は防げています。(長時間はやっていないからあれですが。。)

プログラム

<!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モータードライバ、モーターギアボックス、バッテリの配線を繋いで、船体に収納します。

マイコンボードはピンソケットがあるとかなり場所を取るので、全て取っ払いました。リード線は半田で直付けします。

カメラ無しバージョン走行

まあまあ、ちゃんと動いております。

続いてカメラを実装します。

カメラ収納部の作成

もう修正する気力がなくなったので、このまま押し通す事に決めました。

それではこれにて完成です!

カメラ有りバージョン走行

1
Ketunorobioのアイコン画像
ロビヲ@野江内代です。 ヘルニアで人生ハードモード☑️ 電子工作戦闘力3☑️ 普通の戦闘力1☑️ ブログとweb制作と電子工作しつつ四コマ漫画制作予定で人生のラビリンスに迷い込み中☑️ 都知事がストライクゾーン☑️ blogはコチラ⇒https://denkenmusic.com
ログインしてコメントを投稿する