読者です 読者をやめる 読者になる 読者になる

俺の備忘録

個人的な備忘録です。

Raspberry PiでWifiラジコンを作ってPS3のコントローラで操作してみた

python Raspberry Pi

やったこと

  • Raspbery Piを利用してラジコンを作った。
  • クライアントPC(ノートPC)にPS3のコントローラをつないで操作すると、Wifi経由でラジコンを制御できるようにした。
  • ラジコンにカメラを搭載し、クライアントPCからリアルタイムに映像を見れるようにした。

車体はこんな感じで、割と無理やり感満載。 2階建てにして、2階部分にモバイルバッテリーやカメラ、ラズパイを載せている。 f:id:magayengineer:20160716220217j:plain

ノートPCにPS3のコントローラをつないで、 Raspberry Piから送られてくるライブ映像を見ながら操作できる。
ちなみに、となりに置いてあるキュウリは本日収穫したもの。 f:id:magayengineer:20160716220311j:plain

システム構成

  • ライブ映像は、motionを利用。詳細は後述するが、インストールと簡単な設定だけでUSBカメラからライブ画像を取得できるようになる。

  • ラジコンの操作は、PS3のコントローラを利用。 ノートPC上で動くPythonプログラムでPS3コントローラの入力を受け取って、TCP/IPRaspberry Piに送信し、ラジコンを制御。

  • PS3のコントローラの入力をTCP/IPで受け取ったRaspberry Piは、モータドライバでモータを制御し、車体を進めたり、サーボを制御して、ステアを切ったりして動く。

  • ノートPCのOSは、Linux Mint 17.3 Mint

f:id:magayengineer:20160716220348j:plain

用意したもの

Raspberry Pi 3本体 Raspberry Pi3 Model B ボード&ケースセット (Element14版, Clear)-Physical Computing Lab

②モバイルバッテリー Raspberry Piとモータへの電源供給用として購入。

5V 2.1AでRapberry Piに電源供給し、 5V 1 AでDCモータ・サーボモータに電源供給する。
出力が2口あるため、これだけで電源がまかなえる。

(パワーアド)Poweradd Pilot 2GS 10000mAhモバイルバッテリー 急速充電 2USBポート iPhone6 / iPhone6s / iPhone5 / iPad / Xperia / Nexus等対応(シルバー)

③ブレッドボード
サンハヤト SAD-101 ニューブレッドボード サンハヤト SAD-101 ニューブレッドボード

④USBカメラ(マイク付き)
ELECOM WEBカメラ 200万画素 1/5インチCMOSセンサ マイク内蔵 コンパクトタイプ ブラック UCAM-C0220FBBK ELECOM WEBカメラ 200万画素 1/5インチCMOSセンサ マイク内蔵 コンパクトタイプ ブラック UCAM-C0220FBBK

⑤車体(モータ・ギア・タイヤ付き・ステアリング機構付き)
車体のベースとして使用する。
これを買った理由は、「ステアリング機構」が付いているため、サーボモータと、上手い感じに繋げられれば、ステアを切れると踏んだため。

タミヤ 楽しい工作シリーズ No.112 バギー工作基本セット (70112) タミヤ 楽しい工作シリーズ No.112 バギー工作基本セット (70112)

サーボモータ・モータドライバ
アマゾンで売っているキットについてたものを利用。
サーボモータ => SG-90
モータドライバ => TA7291P 

アマゾンのレビューはイマイチだが、パーツを個々に注文しなくて良いので、入門用にある程度部品をまとめて揃えたい人には便利。

Raspberry Piで学ぶ電子工作 専用 実験キット 基本部品セット スターターパック (電子部品関連) Raspberry Piで学ぶ電子工作 専用 実験キット 基本部品セット スターターパック (電子部品関連)

⑦ユニバーサルプレート
バイルバッテリーやブレッドボード、Raspberry Pi本体の固定用
楽しい工作シリーズ No.157 ユニバーサルプレート 2枚セット (70157) 楽しい工作シリーズ No.157 ユニバーサルプレート 2枚セット (70157)

ホムセンで買ったステーっぽい何か
車体を2階建てにするために使用。 2階部分にRaspberry Piやブレッドボード等を載せるために何でもいいので、それなりに硬くて高さがうまく稼げるものなら代用可能。 f:id:magayengineer:20160716220451j:plain

⑩ステンレスの針金(1.2ミリ)
家にあったものを利用。 ステアリングとサーボモータを連結するために使用。 f:id:magayengineer:20160716220458j:plain

⑪USBケーブル
バイルバッテリーから、モータ等への電源供給に使用する。
USBのケーブルを切断して、赤と黒の線だけ引っ張りだして5Vの電源として使う。 写真は赤と黒の線を引っ張りだしたあとに、ジャンパ線(オスメス)に合体したもの。

f:id:magayengineer:20160716220537j:plain

⑫その他(家にあったもの)
- SDカード
- はんだごて
- 太さ3mmのネジとボルト
- ピンドリル(3mm)
- 両面テープ
- ・ジャンパーピン(オス・オス、オス・メス)

車体組み立て

ポイントだけ説明。

次の写真のようにサーボとステアリング機構をつなげる。
- サーボは両面テーブで固定する。
- シャフトに針金を折り曲げたものを半田で固定する。
- サーボのアームに針金を無理やりとおし、シャフトに取り付けた針金の間に通す。

これでサーボのアームを動かすと、ステアが切れるようになる。 めっちゃ苦労した。

f:id:magayengineer:20160716220543j:plain

ホームセンターで買ったステーっぽいものをニコイチにして、 車体(木の部分)に固定し、2階建てにする。

2階部分はユニバーサルプレートを適当に固定する。

f:id:magayengineer:20160716220549j:plain

 回路

モータドライバは以下のように接続。

TA7291Pの1ピン:GND
TA7291Pの2ピン:DCモータと接続
TA7291Pの3ピン:未使用
TA7291Pの4ピン:10KΩの抵抗をつけてTA7291Pの7ピンと接続
TA7291Pの5ピン:GPIO25
TA7291Pの6ピン:GPIO24
TA7291Pの7ピン:10KΩの抵抗をつけてTA7291Pの4ピンと接続
TA7291Pの8ピン:未使用
TA7291Pの9ピン:電源(モバイルバッテリー)の+と接続 TA7291Pの10ピン:DCモータと接続

サーボモータは以下のように接続。

コネクタ:GND
コネクタ:電源(モバイルバッテリー)の+と接続
コネクタ:GPIO18

motionの設定

ライブ映像をストリーミングするためにRapberry PiにMotionを導入する。

インストール

sudo apt-get install motion

インストール完了後、設定ファイル(/etc/motion/motion.conf)を以下のように修正する。

stream_maxrate 30 # フレームレート
stream_localhost off # ライブ映像を他ホストに公開するか
webcontrol_localhost off 
stream_auth_method 1  # Basic認証
stream_authentication hoge:hoge # ブラウザアクセス時に使用するIDとパスワード
width 640
height 480
output_pictures off # 動体検知したときに写真を撮って自動保存するか
ffmpeg_output_movies off

設定完了後、motionを起動して、http://<ラズパイのIP>:8081/にブラウザでアクセスし、ライブ映像が見れればOK。

motionの起動方法

sudo motion

クライアント側プログラム

PS3のコントローラを使うために、ライブラリ(pygame)を導入。 PS3のコントローラで発生したイベントを受け取って、TCP/IPで送信する。 以下のフォーマットでイベントをRaspberry Pi側に送る仕様にした。  

(ボタン種別,イベント種別)&(ボタン種別,イベント種別)&

例:
(RIGHT,UP)&(CROSS,DOWN)&

#!/usr/bin/python
# -*- coding: utf-8 -*-

import pygame
from pygame.locals import *
import time
from contextlib import closing
import socket


# PS3のコントローラのイベント処理クラス
class Ps3Controller:
    # 初期化
    def __init__(self, function):
        pygame.joystick.init()
        joys = pygame.joystick.Joystick(0)
        joys.init()
        self._function = function

    # 入力受付開始
    def start(self):
        pygame.init()

        while True:
           # イベントをハンドル
           self._function(pygame.event.get())
           time.sleep(0.1)

    # TODO
    def terminate(self):
        pass

# Raspberry PiのIPアドレスまたはホスト名
RASPBERRY_HOST = "192.168.0.30"

# Raspberry側の待ち受けポート
RASPBERRY_PORT = 9999

# pygameの定数とraspberry pi側にTCP/IPで指令を送る際に使用するコマンドのマッピング
TYPE_MAP = {JOYBUTTONDOWN: "DOWN",
            JOYBUTTONUP: "UP"}

BUTTON_MAP = {0: "SELECT",
              1: "ANALOG_LEFT",
              2: "ANALOG_RIGHT",
              3: "START",
              4: "UP",
              5: "RIGHT",
              6: "DOWN",
              7: "LEFT",
              8: "L2",
              9: "R2",
              10: "L1",
              11: "R1",
              12: "TRIANGLE",
              13: "CIRCLE",
              14: "CROSS",
              15: "SQUARE"}

# 扱うイベントのフィルタ
EVENT_FILTER= [(JOYBUTTONUP, 12),
                (JOYBUTTONDOWN, 12),
                (JOYBUTTONUP, 15),
                (JOYBUTTONDOWN, 15),
                (JOYBUTTONUP, 13),
                (JOYBUTTONDOWN, 13),
                (JOYBUTTONUP, 14),
                (JOYBUTTONDOWN, 14),
                (JOYBUTTONUP, 10),
                (JOYBUTTONDOWN, 10),
                (JOYBUTTONUP, 8),
                (JOYBUTTONDOWN, 8),
                (JOYBUTTONUP, 11),
                (JOYBUTTONDOWN, 11),
                (JOYBUTTONUP, 9),
                (JOYBUTTONDOWN, 9),
                (JOYBUTTONUP, 0),
                (JOYBUTTONDOWN, 0),
                (JOYBUTTONUP, 3),
                (JOYBUTTONDOWN, 3),
                (JOYBUTTONUP, 1),
                (JOYBUTTONDOWN, 1),
                (JOYBUTTONUP, 2),
                (JOYBUTTONDOWN, 2),
                (JOYBUTTONUP, 4),
                (JOYBUTTONDOWN, 4),
                (JOYBUTTONUP, 5),
                (JOYBUTTONDOWN, 5),
                (JOYBUTTONUP, 6),
                (JOYBUTTONDOWN, 6),
                (JOYBUTTONUP, 7),
                (JOYBUTTONDOWN, 7)]

# TCP/IPでRaspberry PIにコマンドを送る
def send_command(all_events):
    # 扱いたいイベントだけにフィルタする
    # 今回はボタン関連のイベントのみを扱う(コントローラの傾き等は扱わない)
    target_events = filter(lambda e:e.dict.has_key("button") and (e.type, e.dict["button"]) in EVENT_FILTER, all_events)

    command = ""
    for e in target_events:
        command += "(" + BUTTON_MAP.get(e.dict["button"]) + "," + TYPE_MAP.get(e.type) + ")&"

    # (ボタン名,イベント種別)&(ボタン名,イベント種別)&...の形式でRaspberry Piに送るコマンドを生成
    if len(command) > 0:
        print command

        bufsize = 4096

        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

        with closing(sock):
            sock.connect((RASPBERRY_HOST, RASPBERRY_PORT))
            sock.send(command)

if __name__ == '__main__':
    try:
        # PS3コントローラ初期化
        c = Ps3Controller(send_command)
        c.start()
    except KeyboardInterrupt:
        print "terminated"

リファクタしてないから汚いコードだ...

Raspberry側プログラム

Raspberry側のプログラム。 サーボの制御のために、wiringpiを利用。 クライアントからTCP/IPで送られてきたコマンドを解釈し、ラジコンを操作する。 Duty比を変化させることで、DCモータの正転・反転・速度と、 サーボモータの角度を制御している。

#!/usr/bin/python
# -*- coding: utf-8 -*-

import RPi.GPIO as GPIO
import wiringpi2 as wiringpi
import socket
from contextlib import closing

# Wifiラジコンカー
class Car():
    # 最高スピード(Duty100%。 100%はモータに負荷がかかるので注意。)
    SPEED_MAX = 100

    # 最低スピード(Duty40%)
    SPEED_MIN = 40

    def __init__(self):
        GPIO.setmode(GPIO.BCM)
        GPIO.setup(24, GPIO.OUT)
        GPIO.setup(25, GPIO.OUT)

        wiringpi.wiringPiSetupGpio()

        # PWM出力ピンを指定
        wiringpi.pinMode(18, wiringpi.GPIO.PWM_OUTPUT)
        # 周波数を固定するための設定
        wiringpi.pwmSetMode(wiringpi.GPIO.PWM_MODE_MS)

         # 50 Hz。 <- 18750/(周波数)
        wiringpi.pwmSetClock(375)

        self._p0 = GPIO.PWM(25, 50)
        self._p1 = GPIO.PWM(24, 50)
        self._p0.start(0)
        self._p1.start(0)

        self._speed = 60

    # 前進
    def go_ahead(self):
        self._p0.ChangeDutyCycle(0)
        self._p1.ChangeDutyCycle(self._speed)

    # 後退
    def go_back(self):
        self._p0.ChangeDutyCycle(self._speed)
        self._p1.ChangeDutyCycle(0)

    # ステアリングを左に切る
    def turn_left(self):
        wiringpi.pwmWrite(18, int(8.35 * 1024 / 100))

    # ステアリングを右に切る
    def turn_right(self):
        wiringpi.pwmWrite(18, int(5.125 * 1024 / 100))

    # ステアリングをまっすぐにする
    def turn_strait(self):
        wiringpi.pwmWrite(18, int(6.75 * 1024 / 100))

    # スピードアップ
    def speed_up(self):
        if self._speed < Car.SPEED_MAX:
            self._speed += 10

    # スピードダウン
    def speed_down(self):
        if self._speed > Car.SPEED_MIN:
            self._speed -= 10

    # 停止
    def stop(self):
        self._p0.ChangeDutyCycle(0)
        self._p1.ChangeDutyCycle(0)

    # 終了処理
    def terminate(self):
        self._p0.stop()
        self._p1.stop()
        GPIO.cleanup()

# クライアントからTCP/IPで送られていたコマンドをハンドルして、
# Carクラスをコントロールするクラス
class CarController:
    def __init__(self, car):
        self._car = car

        # コマンドとCarクラスのfunctionの対応関係マップ
        self._function_map = {
            "(CROSS,UP)":car.stop,
            "(CROSS,DOWN)":car.go_ahead,
            "(CIRCLE,UP)":car.stop,
            "(CIRCLE,DOWN)":car.go_back,
            "(RIGHT,UP)":car.turn_strait,
            "(RIGHT,DOWN)":car.turn_right,
            "(LEFT,UP)":car.turn_strait,
            "(LEFT,DOWN)":car.turn_left,
            "(UP,DOWN)":car.speed_up,
            "(DOWN,DOWN)":car.speed_down,
        }

    # 送られてきたコマンドに対応するcarクラスの関数を実行する
    def control(self, command):
        if self._function_map.has_key(command):
            f = self._function_map.get(command)
            f()

    # 終了処理
    def terminate(self):
        self._car.terminate()

# メイン処理
def main():
    # Carクラスとコントローラクラスを初期化
    car = Car()
    ctrl = CarController(car)

    # TCP/IPでコマンドを受け付ける
    host = '192.168.0.30'
    port = 9999
    queue = 10
    bufsize = 4096

    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    with closing(sock):
        sock.bind((host, port))
        sock.listen(queue)
        while True:
            conn, address = sock.accept()
            with closing(conn):
                # コマンド受付
                msg = conn.recv(bufsize)
                # (ボタン名,イベント種別)&(ボタン名,イベント種別)&...の形式で送られてくるので&でスプリット
                commands = msg.split("&")
                for command in commands:
                    if len(command) > 0:
                        print command
                        # コントローラクラスにコマンドを渡す
                        ctrl.control(command)

if __name__ == '__main__':
    main()

リファクタしてないから汚いコードだ...

今後の課題

小さい車体に無理やり詰め込んだため、これ以上拡張が厳しい。 ベース車体を大きなものに変更し、いろいろつめ込んでいきたい。