Raspberry Piでラジコンをつくってみる

[東京理科大学 Advent Calendar 2016](http://qiita.com/advent-calendar/2016/tokyo_university_of_science) 12/11の記事になります。

が、遅刻しました。現在12/12です。すみません……。写真とか後で追加すると思います。


###皆さんRaspberry Piを知っていますか？
教育用に開発された小型パソコンで、普通のパソコンとは違いgpioピンが搭載されていたりとIOTにはうってつけの代物です。これで色々遊べるのじゃないかと、とりあえず何かやってみようと、そう思いました。
#Raspberry Piを使ってラジコンを組む
今回はこのRaspberry Pi(ラズパイ)を使ってサクッと遠隔操作できるカメラ搭載ラジコンのようなものを作って見ようと思います。

どうやら既に同じようなものをつくっている方が結構いらっしゃったようで、検索すれば他にも色々情報があると思います。
#仕様を考える
###やりたいこと
戦車道したい

###最初に考えた仕様
- モーターで動くようにする
    　→ラズパイのGPIOピンを使ってモータドライバICを介してモータを操作、ハード側はタミヤ工作キットを使う

- ブラウザさえあればネット越しにモーターその他を操作できるようにする
    　→WebIopiでネット越しにGPIOピンを操作

- せっかくだからブラウザ操作のときにゲームパッドを使いたい
    　→html5のgamepad apiが使えそう？

- カメラを搭載してネット越しに見られるようにする
    　→webカメラを指してmotionで通信できるかな？

最初に考えたのはこのあたりでした。少し変わっていますがだいたいこのままです。
#ハード部分
ラズパイの型番は`Raspberry Pi 3 Model B`です。既に無線LAN機能が内蔵されているので、余計なものを付ける必要がありません。

側はタミヤの工作キットを使用しました。みんな大好き[タンク工作基本セット](http://www.tamiya.com/japan/products/70108tracked_vehicle/index.htm)と[ダブルギヤボックス](http://www.tamiya.com/japan/products/70168double_gearbox/index.htm)です。鉄板の組み合わせなので迷いませんでした。

電源供給はモバイルバッテリーから行います。

#gpioピンでモーターを動かす
Raspberry Piのピンは最大電圧3.3Vまで出力できますが、電流はあまり出ないので、電力が足りず、モーターを直接動作させることはできません。

そこで、モータドライバを使用してモーターを動かします。モータドライバは有名なTA7291Pを使用しました。他に作例がたくさんあるのと、IN1、IN2がpwmでも動作しそうなので採用しました。秋月で2個入り300円です。データシートは以下のようになっています。

| 端子記号 | 端子番号 |
|:---:|:---:|
| Vcc  | 7  |
| Vs  | 8  |
| Vref  | 4  |
| GND  | 1  |
| IN1  | 5  |
| IN2  | 6  |
| OUT1  | 2  |
| OUT2  | 10  |

これを見て配線していきます。

ラズパイは標準でpwmが出せるピンがあるのですが、一つしか無いので、今回はソフトウェアpwmを使用します。ソフトウェアpwmならどのピンでも出せるので、良さなそうピンに配線します。今回はGPIO17、18、22、27の4つのピンを出力に使用することにしました。


ピンを操作するには[WebIOPi](http://webiopi.trouch.com/)を使います。これを使うことで、ブラウザからかんたんにGPIOピンが操作できます。また、ソフトウェアpwmに対応しています。


インストールは、[http://webiopi.trouch.com/INSTALL.html](http://webiopi.trouch.com/INSTALL.html)にある通り、

```
$ wget http://sourceforge.net/projects/webiopi/files/WebIOPi-0.7.1.tar.gz
$ tar xvzf WebIOPi-0.7.1.tar.gz
$ cd WebIOPi-0.7.1
```

このままでは動かないので、[https://github.com/doublebind/raspi](https://github.com/doublebind/raspi)のパッチを当てます。

```
$ wget https://raw.githubusercontent.com/doublebind/raspi/master/webiopi-pi2bplus.patch
$ patch -p1 -i webiopi-pi2bplus.patch
```
そしてセットアップ。

```
$ sudo ./setup.sh
```

起動と停止は、

```
$ sudo /etc/init.d/webiopi start
$ sudo /etc/init.d/webiopi stop
```
です。

WebIOPiはこのままでも簡単な操作を行えますが、HTMLとPythonを書くことで複雑な操作も可能となります。今回は、ゲームパッドを使用するということで、それらのコードを書く必要があります。

適当な場所に`script.py`と`index.html`のファイルを作成し、`/etc/webiopi/config`のコンフィグファイルを編集します。

```:/etc/webiopi/config
[SCRIPTS]
myscript = "script.pyのディレクトリ"/script.py

[HTTP]
doc-root = "index.htmlのディレクトリ"
```
以上を追記します。`http://ラズパイのアドレス:8000`に接続すると作成した`index.html`が読み込まれるようになっているはずです。

少しWebIOPiで扱われる`.py`ファイルについて例を上げて解説しておきます。

```py:example.py
import webiopi

GPIO = webiopi.GPIO

led = 17

def setup():
    GPIO.setFunction(led, GPIO.PWM)

def loop():
    webiopi.sleep(0.1)
    GPIO.pwmWrite(led, 0.5)
    webiopi.sleep(0.1)
    GPIO.pwmWrite(led, 1.0)

def destroy():
    GPIO.pwmWrite(led, 0)
```

まず、`led = 17`で`led`にGPIOピン17番を定義しています。`def setup():`はWebIOPi起動時、`def loop():`は実行中、`def destroy():`は終了時読み込まれる関数です。また、`GPIO.setFunction(led, GPIO.PWM)`でGPIOピンの種類を指定し（今回はPWM）、`GPIO.pwmWrite(led, 0.5)`で出力を変えます。

`index.html`については、ボタンやフォームから、javascriptで直接ピンを制御したり、`script.py`に値を投げるということになります。

#ゲームパッドで操作する
ブラウザでゲームパッドの入力を判定するには、GamePad APIというHTML5で実装されたAPIを使います。

GamePad→ブラウザ→Webipopoi→モーターという風にしてモーターを操作します。

index.htmlは以下のようにしました。GamePad APIのことは私もあまり良くわかりません。[Subterranean Flower blogさんのページ](https://sbfl.net/blog/2016/05/11/gamepad-api-on-browsers/)を参考にして書きました。

```html: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">
    <title>WebIOPi</title>
    <script type="text/javascript" src="/webiopi.js"></script>
    <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.0/jquery.min.js"></script>
    <script type="text/javascript">             //Gamepad APIを使う
        let connectedGamepadIndex;
        let loopID;
        loopID = requestAnimationFrame(loop);
        connectedGamepadIndex = null;
        cancelAnimationFrame(loopID);

        window.addEventListener("gamepadconnected", function(e) {
            connectedGamepadIndex = e.gamepad.index;
            loopID = requestAnimationFrame(loop);
        });

        window.addEventListener("gamepaddisconnected", function(e) {
            connectedGamepadIndex = null;
            cancelAnimationFrame(loopID);
        });

        const AXIS_L_VERTICAL_INDEX   = 1;
        const AXIS_R_VERTICAL_INDEX   = 3;

        $(function() {
            loop();
            setInterval("loop()", 100);              //100ms毎にloop関数を実行
        });

        function loop() {                            //ゲームパッドのスティック情報を取得する関数
            let gamepads = navigator.getGamepads();
            let gp = gamepads[connectedGamepadIndex];

            if(gp != undefined){
                let leftAxis = gp.axes[AXIS_L_VERTICAL_INDEX];
                let rightAxis = gp.axes[AXIS_R_VERTICAL_INDEX];
                var rightvalue= Math.round(rightAxis *1000);
                var leftvalue = Math.round(leftAxis *1000);
                var verticalvalue =  [leftvalue,rightvalue];

                webiopi().callMacro( "ChangeSpeed",verticalvalue);   //"changeSpeed"関数に"verticalvalue"を引数として渡す
            }
        }
    </script>
</head>
<body>
</body>
</html>
```
`webiopi().callMacro( "ChangeSpeed",verticalvalue);`はGamePad APIで取得したスティックの値"verticalvalue"を`script.py`にあるChangeSpeed関数に渡しています。これで`script.py`に0.1秒毎に情報が送られてくるので、次はそれを受け取って処理するコードを書きます。前述の通り、GPIO17、18、22、27の4つのピンに配線したので、

```py:script.py
mport webiopi

webiopi.setDebug()

GPIO = webiopi.GPIO

rightf = 17
rightb = 18
leftf = 22
leftb = 27
g_speed_RF = g_speed_RB = g_speed_LF = g_speed_LB = 0

def setup():
    GPIO.setFunction(rightf, GPIO.PWM)
    GPIO.setFunction(rightb, GPIO.PWM)
    GPIO.setFunction(leftf, GPIO.PWM)
    GPIO.setFunction(leftb, GPIO.PWM)

def loop():
    webiopi.sleep(0.1)
    webiopi.debug("%s | %s | %s | %s" % (g_speed_RF, g_speed_RB, g_speed_LF, g_speed_LB) )

def destroy():
    GPIO.pwmWrite(rightf, 0)
    GPIO.pwmWrite(rightb, 0)
    GPIO.pwmWrite(leftf, 0)
    GPIO.pwmWrite(leftb, 0)

@webiopi.macro
def ChangeSpeed( speed_R, speed_L):
    global g_speed_RF, g_speed_RB, g_speed_LF, g_speed_LB
    g_speed_RF = g_speed_RB = g_speed_LF = g_speed_LB = 0
    if int(speed_R) < -10:
       g_speed_RF = (int(speed_R) / 1000) * -0.8 +0.2
       g_speed_RB = 0
    elif int(speed_R) > 10:
       g_speed_RB = (int(speed_R) / 1000) * 0.8 +0.2
       g_speed_RF = 0
    if int(speed_L) < -10:
       g_speed_LF = (int(speed_L) / 1000) * -0.8 +0.2
       g_speed_LB = 0
    elif int(speed_L) > 10:
       g_speed_LB = (int(speed_L) / 1000) * 0.8 +0.2
       g_speed_LF = 0
    GPIO.pwmWrite(rightf, g_speed_RF)
    GPIO.pwmWrite(rightb, g_speed_RB)
    GPIO.pwmWrite(leftf, g_speed_LF)
    GPIO.pwmWrite(leftb, g_speed_LB)
```
関数の前に`@webiopi.macro`と書くことで送られてきたデータを受け取れます。`def ChangeSpeed( speed_R, speed_L):`で、送られてきた要素数2の配列`"verticalvalue"  ( = [leftvalue, rightvalue] )`を`speed_R, speed_L`に入れています。この値を編集し、GPIO.pwmWriteでピンの出力を指定します。

`if`文あたりは私のゲームパッドでいい感じに操縦できるよう合わせたものなので変更してもらっていいと思います。また、`def loop():`内では0.1秒毎にコンソールに各値を表示するようにしていますが、必要ないなら消しても構いません。

これでうまく行っていれば、モーターの操作自体はできるはずです。注意点としては、

- Gamepad APIがうまく動かない環境がある
　　そもそもIEでは動かないとかなんとか

- ゲームパッドは抜き差ししたりボタンを押したりしないと認識してくれない
　　私の環境だと一度抜き差ししないと認識しませんでした。

その他何故か動かないこともあります。ラズパイを再起動すると治っていたり、よくわかりません。


#カメラの映像をブラウザに送る
[motion](http://www.lavrsen.dk/foswiki/bin/view/Motion/WebHome)というソフトウェアを使って映像を送信します。ちょうど使っていないWebカメラがあったのでそれを使うことにしました。ラズベリーパイはカメラモジュールもWebカメラも扱えるのでいいですね。ラズパイはいいぞ。

###motionをインストールする
apt-getでインストールする

```
sudo apt-get install motion
```
コンフィグファイルは`/etc/motion/motion.conf`にあるので、これを編集します。とりあえず以下のようにしました。

```
output_pictures off         //イメージを出力するかどうか
stream_motion off           //モーション検出でFPSを変えるか
stream_maxrate 15           //ストリーミングのフレームレート(高くしすぎると通信量を食うのでローカル以外で使う場合は注意)
offstream_port 8081         //サーバーのポート番号
stream_quality 50           //映像の画質
stream_localhost off        //ストリーミングサーバーへの接続をローカルホストからのみにするか
stream_authentication off   //ストリーミングサーバーへの接続に認証を必要とするか
```

`ラズパイのアドレス:8081`に接続すると映像を見ることが出来ます。ですが、このままではWebIopiと同時に使えないので、WebIOPiのページにmotionのストリーミングを埋め込みます。`<img src="http://ラズパイのアドレス:8081/">`のタグを入力することで埋め込みが出来ます。

```html
<img id="stream" src="">
<script type="text/javascript">
   document.getElementById('stream').setAttribute("src", "http://" + location.hostname + ":8081");
</script>
```

ラズパイのIPは固定していなかったのと、違う環境でも使えるようにホストネームを取得して接続するようにしました。ということでcssも多少追加して`index.html`は以下のようになります。

```html: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">
    <title>WebIOPi | Light Control</title>
    <script type="text/javascript" src="/webiopi.js"></script>
    <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.0/jquery.min.js"></script>
    <link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.11.4/themes/smoothness/jquery-ui.css">
    <script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.11.4/jquery-ui.min.js"></script>
    <script type="text/javascript">
        let connectedGamepadIndex;
        let loopID;
        loopID = requestAnimationFrame(loop);
        connectedGamepadIndex = null;
        cancelAnimationFrame(loopID);

        window.addEventListener("gamepadconnected", function(e) {
            connectedGamepadIndex = e.gamepad.index;
            loopID = requestAnimationFrame(loop);
        });

        window.addEventListener("gamepaddisconnected", function(e) {
            connectedGamepadIndex = null;
            cancelAnimationFrame(loopID);
        });

        const AXIS_L_VERTICAL_INDEX   = 1;
        const AXIS_R_VERTICAL_INDEX   = 3;

        $(function() {
            loop();
            setInterval("loop()", 100);
        });

        function loop() {
            let gamepads = navigator.getGamepads();
            let gp = gamepads[connectedGamepadIndex];

            if(gp != undefined){
                let leftAxis = gp.axes[AXIS_L_VERTICAL_INDEX];
                let rightAxis = gp.axes[AXIS_R_VERTICAL_INDEX];
                console.log("L:" + rightAxis + "　　　　　　R" + leftAxis);
                var rightvalue= Math.round(rightAxis *1000);
                var leftvalue = Math.round(leftAxis *1000);
                var verticalvalue =  [leftvalue,rightvalue];
                webiopi().callMacro( "ChangeSpeed",verticalvalue);
            }
        }
    </script>
    <style type="text/css">
        body {
            padding: 0;
            margin: 0;
        }
        img {
            width: 100%;
            height: auto;
            position: fixed;
            bottom: 0;
        }
    </style>
</head>
<body>
    <div><img id="stream" src=""></div>
    <script type="text/javascript">
        document.getElementById('stream').setAttribute("src", "http://" + location.hostname + ":8081");
    </script>
</body>
</html>
```
これで終わりです。htmlも表示やボタンを追加したり、いくらでもやりようはありますが、最低限としてここまでにしておきます。

```
$ sudo motion /etc/motion/motion.conf & sudo webiopi -d -c /etc/webiopi/config
```
でWebIOPiとmotionを起動。これで、`http://ラズパイのアドレス:8000`にアクセスした時カメラの映像が表示されたのではないかと思います。

#まとめ
うちの子は元気に動いてます。
![12.JPG](https://qiita-image-store.s3.amazonaws.com/0/145589/51e6f5c4-2742-43a7-5ea3-5365d53b7cb3.jpeg)

とりあえず自宅のLAN内ではうまく動作しました。後はカメラ増やしたりスピーカー付け足したり、ラズパイなので色々いじり甲斐がありそうです。

HTCviveでも対応してもっと戦車道したい……。