ラズパイでラジコン作るぞ


## 書いてること

ラズパイでラジコンを作ったので、その方法をここに残します。
専用アプリ不要で手持ちのスマホがコントローラーになります。

完成品はこちら ↓

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">サーボモータをラズパイで制御して、Webサイト経由で操作するのできた！ <a href="https://t.co/7USpyYXTpx">pic.twitter.com/7USpyYXTpx</a></p>&mdash; suo-takefumi (@zsipparu) <a href="https://twitter.com/zsipparu/status/1365968329769316353?ref_src=twsrc%5Etfw">February 28, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

## 構成図

全体の構成図です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/274270/5f53a80a-d7b4-d040-3010-b6470d93df41.png)

- ラズパイで4つのサーボモーターを制御
    - `server.py`から制御信号を出す
    - ※Python3スクリプト
- `index.html`と`server.py`はSocket通信
- http://{ラズパイ}:8000/index.htmlを開いたスマホがコントローラーになる
- ＜制御の流れ概要＞
    - `index.html`から5パターンの信号(前進/後進/左回り/右回り/停止)を発信
    - `server.py`が`index.html`からの信号をもとにサーボモーターを制御

サーボモーターの配線はこちら

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/274270/35c2ff61-5bd3-3d17-580f-040d594a9f9b.png)

Raspberry Piの情報

```console
$ lsb_release -a
No LSB modules are available.
Distributor ID: Raspbian
Description:    Raspbian GNU/Linux 10 (buster)
Release:        10
Codename:       buster

$ python3 -V
Python 3.7.3
```

## スクリプト

スクリプトは全てラズパイの`/home/pi/data/web`配下に格納します。
ディレクトリ構成は以下のようになります。
ソケット通信については **[こちらのサイト](https://www.nowonbun.com/247.html)** を参考にさせて頂きました。


```console
pi@raspi:~/data/web $ tree
.
├── images
│   ├── back.png
│   ├── go.png
│   ├── left.png
│   ├── right.png
│   └── stop.png
├── index.html
└── server.py
```

コントローラーになる`index.html`のソースです。
`index.html`はSocket通信ではクライアント側になり、Socket通信のサーバー側となる`server.py`に対し5種類の信号(GO/BACK/LEFT/RIGHT/STOP)を送ります。

```html:index.html
<!DOCTYPE html>
<html>/home/pi/data/web
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
  <title>コントローラー</title>
  </head>
<body>
  <img onclick="sendMessage('GO')"    src="images/go.png" />
  <img onclick="sendMessage('BACK')"  src="images/back.png" />
  <img onclick="sendMessage('LEFT')"  src="images/left.png" />
  <img onclick="sendMessage('RIGHT')" src="images/right.png" />
  <img onclick="sendMessage('STOP')"  src="images/stop.png" />
  <br />
  <!-- 出力 area -->
  <textarea id="messageTextArea" rows="10" cols="50"></textarea>
  <script type="text/javascript">
    var webSocket = new WebSocket("ws://192.168.3.19:9998"); // ウェブサーバを接続
    var messageTextArea = document.getElementById("messageTextArea"); // ウェブサーバから受信したデータを出力するオブジェクトを取得
    // ソケット接続すれば呼び出す関数。
    webSocket.onopen = function(message){
      messageTextArea.value += "Server connect...\n";
    };
    // ソケット接続が切ると呼び出す関数。
    webSocket.onclose = function(message){
      messageTextArea.value += "Server Disconnect...\n";
    };
    // ソケット通信中でエラーが発生すれば呼び出す関数。
    webSocket.onerror = function(message){
      messageTextArea.value += "error...\n";
    };
    // ソケットサーバからメッセージが受信すれば呼び出す関数。
    webSocket.onmessage = function(message){
      // 出力areaにメッセージを表示する。
      messageTextArea.value += "Recieve From Server => "+message.data+"\n";
    };

    function sendMessage(argVal){
      messageTextArea.value += "Send to Server => "+ argVal +"\n";
      webSocket.send(argVal); // WebSocketでtextMessageのオブジェクトの値をサーバに送信
    }

    // 通信を切断する。
    function disconnect(){
      webSocket.close();
    }
  </script>
</body>
</html>
```

サーバー側でサーボモーターを制御する`server.py`のソースです。
`server.py`は`index.html`から受信した信号(GO/BACK/LEFT/RIGHT/STOP)に応じモーターを制御します。

```python:server.py
import asyncio
import websockets
import RPi.GPIO as GPIO
import time

GPIO.setmode(GPIO.BCM)

wheelPin = {
    "FrontRight": "2",
    "FrontLeft" : "3",
    "RearRight" : "17",
    "RearLeft"  : "4"
  }

wheelPwm = {
    "FrontRight": "",
    "FrontLeft" : "",
    "RearRight" : "",
    "RearLeft"  : ""
}

## 角度をラジアンに変換
def exchDegreeToRadian(argDegree):
    return 2.5 + (12.0 - 2.5) / 180 * (argDegree + 90)

## サーボモータ制御
def wheelControl(argFlg, argTerm):
    global wheelPwm
    act01 = exchDegreeToRadian( -90 )
    act02 = exchDegreeToRadian(  90 )
    act03 = exchDegreeToRadian(   0 )
    if(argFlg == "GO"):
        wheelPwm["FrontRight"].ChangeDutyCycle( act01 )
        wheelPwm["FrontLeft" ].ChangeDutyCycle( act01 )
        wheelPwm["RearRight" ].ChangeDutyCycle( act01 )
        wheelPwm["RearLeft"  ].ChangeDutyCycle( act01 )
    elif(argFlg == "BACK"):
        wheelPwm["FrontRight"].ChangeDutyCycle( act02 )
        wheelPwm["FrontLeft" ].ChangeDutyCycle( act02 )
        wheelPwm["RearRight" ].ChangeDutyCycle( act02 )
        wheelPwm["RearLeft"  ].ChangeDutyCycle( act02 )
    elif(argFlg == "STOP"):
        wheelPwm["FrontRight"].ChangeDutyCycle( act03 )
        wheelPwm["FrontLeft" ].ChangeDutyCycle( act03 )
        wheelPwm["RearRight" ].ChangeDutyCycle( act03 )
        wheelPwm["RearLeft"  ].ChangeDutyCycle( act03 )
    elif(argFlg == "LEFT"):
        wheelPwm["FrontRight"].ChangeDutyCycle( act01 )
        wheelPwm["FrontLeft" ].ChangeDutyCycle( act02 )
        wheelPwm["RearRight" ].ChangeDutyCycle( act01 )
        wheelPwm["RearLeft"  ].ChangeDutyCycle( act02 )
    elif(argFlg == "RIGHT"):
        wheelPwm["FrontRight"].ChangeDutyCycle( act02 )
        wheelPwm["FrontLeft" ].ChangeDutyCycle( act01 )
        wheelPwm["RearRight" ].ChangeDutyCycle( act02 )
        wheelPwm["RearLeft"  ].ChangeDutyCycle( act01 )

    time.sleep(argTerm)

#####################################################
# ここから処理を開始

## サーボモータの初期設定
for wheel in wheelPin:
    print("wheel={0} , pin={1}".format( wheel, wheelPin[wheel] ) )
    PinNo = int(wheelPin[wheel]) # GPIO
    GPIO.setup(PinNo, GPIO.OUT)
    wheelPwm[wheel] = GPIO.PWM(PinNo, 50)
    wheelPwm[wheel].start(0.0)

##----------------------------------------##

# クライアント接続すると呼び出す
async def accept(websocket, path):
  while True:
    # クライアントからメッセージを待機
    data = await websocket.recv()
    print("receive : " + data)
    # クライアントでechoを付けて再送信
    #await websocket.send("echo : " + data)
    if data == "GO":
      wheelControl("GO", 0.1)
    elif data == "BACK":
      wheelControl("BACK", 0.1)
    elif data == "STOP":
      wheelControl("STOP", 0.1)
    elif data == "LEFT":
      wheelControl("LEFT", 0.1)
    elif data == "RIGHT":
      wheelControl("RIGHT", 0.1)

# WebSocketサーバー生成
start_server = websockets.serve(accept, "raspi203.local", 9998)

# 非同期でサーバを待機する。
asyncio.get_event_loop().run_until_complete(start_server)
asyncio.get_event_loop().run_forever()
```

以上で全ての準備が完了しました。

## 実行方法

2つコンソールを起動し、コンソール①ではWebサーバーを起動し、コンソール②では`server.py`を実行します。

```console:コンソール①
$ cd /home/pi/data/web
$ python3 -m http.server 8000
```

```console:コンソール②
$ cd /home/pi/data/web
$ python3 server.py
```

実際の実行画面はこんな感じ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/274270/ecd9b9c9-25e8-6381-e5b2-2b7b776b54c0.png)

これでサービスが起動状態になりました。スマホなどで`http://{ラズパイIP}:8000/index.html`を開くとそのデバイスがコントローラーになります。

## おわりに

今回は、とりあえずラズパイをラジコンにするまでを紹介しました。今後は、コントローラのUIをもっとリッチにするとか、ラジコンにカメラを搭載しコントローラ側に表示するとか、機能を盛り込んでいきます。