【電子工作】ゲームパッドの入力信号をBluetoothでラズパイに飛ばしてモーターを駆動させる

今回の電子工作は「PC接続したゲームパッドのスティック入力をBluetoothでラズパイに飛ばしてDCモーターおよびサーボモーターを駆動させる」というものです。

やりたいこと

そもそものきっかけは、「脳波コントロールできるラジコンカーを作りたい!」でした。でも、いきなりそこに行くのはかなり大変そうだ。そこでまずはゲームパッドでコントロールできるラジコンカーを作ることにしました。ただし、単純にラズパイにゲームパッドを接続して動かすのではそのあとの開発が大変そうだ、と考え、こんな構成を考えました。

f:id:wanko_sato:20220103104703p:plain

ゲームパッドや脳波などからの入力信号はPCで処理します。ゲームパッドの場合、左スティックの上下をDCモーターの回転に、右スティックの左右をサーボモーターの駆動に割り当てます(詳しくはコードのパートで)。処理された信号をBluetoothでラズパイに送信し、モーター類の駆動のためのデータとします。
こうすることで、入力信号の処理系と、モーター類の駆動系を別々にできるので、入力デバイスを変えたいと思ったときにはPC側の処理系だけを変更すれば良い、ということになるわけです。

さあ、工作しよう

というわけで、工作します。

使うもの

後ははんだごて、割りばし、瞬間接着剤、カッターを使います。
ジャンパワイヤはこんなに必要ないです。

オス-メスピン 8本

  • DCモーター用 2本(DCモーターのケーブルにはんだづけ)
  • サーボモーター用 3本
  • モータードライバ用 3本
  • ブレッドボード-ラズパイ接続用 2本

オス-オスピン 1本

  • ブレッドボード上で使用

それ以外はブレッドボードキットに付属しているホチキス針みたいなやつがあればOK。ゲームパッドXBoxのやつを使いました。
ワニ口クリップのやつは電池をつなぐのに使います。はんだ付けしちゃっても良い場合はワニ口じゃなくても大丈夫です。
車体にはタミヤのバギーを使います。ステアリングとモーターボックスが別になっており、加工が簡単なためです。ただし、このままでは使えない箇所もあるので、一部ちょこっとだけ加工が必要です。

なお、ラズパイは3B+を使っていますが、バージョンはこれでなくても大丈夫だとは思います。また、今回はラズパイを電源に挿したままの状態で使用しているので、ラズパイ用のバッテリーは購入していません。本格的にラジコンにする場合はバッテリーも必要ですので、購入しておいてください。

バギー本体をくみ上げる

タミヤのバギーにはDCモーターが付属しているので、基本的に説明書に沿って作っちゃってOKです。ただし、電池ボックスの箇所にサーボモーターを配置するため、電池ボックスはつけないでください。
こんな感じになります。
f:id:wanko_sato:20220103122342j:plain

二つほど工夫があります。まずはモーターから。
f:id:wanko_sato:20220103122451j:plain
モーターの電極部分にコンデンサをはんだ付けしています。これをしておくと電流が安定し、ラズパイ本体を傷つけにくくなるとのこと。

もう一つはサーボモーター。
f:id:wanko_sato:20220103122628j:plain
いろいろと模索してみた結果、割りばしを裁断して積み上げる形式におちつきました。今回は5本、積み上げています。使う割りばしの太さによって積み上げる数は変わると思います。百均で売っている瞬間接着剤で割りばし同士および割りばしとサーボモーターを接着しています。ねじうちするとねじの太さによっては割りばしが壊れてしまうので、細い釘で補強するなりすると動作が安定するでしょう。
また、プロペラ部は穴同士をつなげています。ステアリングを左右に振ったときに位置が前後するため、プロペラの隙間を動けるようにするためです。これは実際に自分で動かしてみると、こうする理由がはっきりすると思います。
あとは、ステアリングバーの中心にプロペラの穴に入るくらいの太さの針金をはんだ付けしています。これでステアリング部分は完了。
なお、注意としてここまでくみ上げる前にサーボモーターの位置を中心にしておいてください。ここまで組んでしまうと取り外しできない仕様になっているためです。

配線する

f:id:wanko_sato:20220103123347p:plain

ものすごくわかりにくくて恐縮です。

DCモーター、モータードライバ、ラズパイの接続

DCモーターとモータードライバの接続は付属の説明書に記載があるので、そちらを確認してください。この配線では、モータードライバのin1を20に、in2を21に、pwmを16につないでいます。

サーボモーターとラズパイの接続

サーボモーターは茶、赤、黄のラインが出ています。以下のような接続にします。今回はラズパイと直接接続していますが、間にドライバを挟んでも良いかもしれないです。

  • 茶:GND
  • 赤:電源
  • 黄:3ピン
配線後

配線が全部完了するとこんな感じ。眼鏡ケースに乗せて動作確認しています。
このまま走らせると内〇ぶちまけながら走ってるみたいな感じになっちゃいますので要注意。
f:id:wanko_sato:20220103124848j:plain
f:id:wanko_sato:20220103125009j:plain

コードを書こう

コードはpython3系を使用します。pygame, serial, RPi.GPIOの三つのライブラリを使います。そんなに難しいことはしていません。ポイントはPC側で取得したデータをいったんテキストにしてBluetoothのシリアル通信に乗せ、ラズパイ側で受信したらそれを数値に復元する、というところでしょうか。
PC側、ラズパイ側のいずれも入力信号を確認するためのウィンドウをpygameの関数を使って表示させています。これはあってもなくてもかまいません。あった方がどんな数値をやりとりしているのかがわかりやすくなる、くらいです。

PC側のコード

まずはPC側のコードです。細かいところはコードを解読してください。

# pygameでPCにUSB接続されたコントローラーのスティック入力を検知し数値を返す
# 取得したスティック入力をBluetoothでRaspberryPiに送信する
import pygame
import serial

s = serial.Serial('COM3', 9600) # 名称はBluetooth接続したときの名称にする

BLACK = (0, 0, 0)
WHITE = (255,255, 255)
WIDTH = 320
HEIGHT = 240

def main():
	# 初期化
	pygame.init()
	surface = pygame.display.set_mode((WIDTH, HEIGHT))
	pygame.joystick.init()
	print('get_count:{}'.format(str(pygame.joystick.get_count())))

	# コントローラーの情報を出力
	joystick_id = 0
	joystick = pygame.joystick.Joystick(joystick_id)
	joystick.init()
	print('get_name:{}'.format(joystick.get_name()))
	print('get_numaxes:{}'.format(str(joystick.get_numaxes())))
	print('get_numbuttons:{}'.format(str(joystick.get_numbuttons())))
	print('get_numhats:{}'.format(str(joystick.get_numhats())))

	# 画面表示用の設定
	myfont = pygame.font.Font(None, 30)
	myclock = pygame.time.Clock()
	endflag = 0

	while endflag == 0:

		# ウィンドウが閉じられるまでwhileループを実行する
		for event in pygame.event.get():
			if event.type == pygame.QUIT: endflag = 1

		# スティックの入力方向を数値で取得する
		ax_0 = round(joystick.get_axis(1), 3) # 左スティックの縦
		ax_1 = round(joystick.get_axis(2), 3) # 右スティックの横

		# そのままだとわずかにスティックが倒れていると検知されるので、閾値を設けて0にする処理が必要
		ax_0 = 0 if abs(ax_0) < 0.1 else ax_0
		ax_1 = 0 if abs(ax_1) < 0.1 else ax_1

		m = str(ax_0) + ',' + str(ax_1) + '\r\n'
		s.write(m.encode('utf-8'))

		# 取得した入力値を画面表示
		bitmaptext_0 = myfont.render('axis_0:{}'.format(str(ax_0)), True, BLACK)
		bitmaptext_1 = myfont.render('axis_1:{}'.format(str(ax_1)), True, BLACK)
		surface.fill(WHITE)
		surface.blit(bitmaptext_0, (50, 100))
		surface.blit(bitmaptext_1, (50, 150))
		pygame.display.update()
		myclock.tick(60)

	pygame.quit()


if __name__ == '__main__':
	main()

ラズパイ側のコード

次にラズパイ側のコードです。同じく細かいところは解読してください。

import serial
import pygame
import RPi.GPIO as GPIO

# シリアル通信開始
s = serial.Serial('/dev/rfcomm0', 9600)
flg = 0

# コントローラー入力表示ウィンドウ
BLACK = (0,0,0)
WHITE = (255, 255, 255)
WIDTH = 320
HEIGHT = 240

# GPIOセット
GPIO.setmode(GPIO.BCM)
hertz = 50

# モータードライバ初期設定
in1 = 20
in2 = 21
pwm = 16
standbyPin = 12

GPIO.setup(in1, GPIO.OUT)
GPIO.setup(in2, GPIO.OUT)
GPIO.setup(pwm, GPIO.OUT)
GPIO.setup(standbyPin, GPIO.OUT)
GPIO.output(standbyPin, GPIO.HIGH)
p_motor= GPIO.PWM(pwm, hertz)
p_motor.start(0)

duty = [0, 0]

# サーボモーター初期設定
GPIO.setmode(GPIO.BCM)
servo = 3
GPIO.setup(servo, GPIO.OUT) # 3ピンを制御用とする

p_servo = GPIO.PWM(servo, hertz)
p_servo.start(0)

# モーター駆動用関数
def motor_drive(speed):
    dutyCycle = speed
    if(speed < 0):
        dutyCycle = dutyCycle * -1
    if(speed > 10):
        GPIO.output(in1, GPIO.HIGH)
        GPIO.output(in2, GPIO.LOW)
    elif(speed < -10):
        GPIO.output(in1, GPIO.LOW)
        GPIO.output(in2, GPIO.HIGH)
    else:
        GPIO.output(in1, GPIO.LOW)
        GPIO.output(in2, GPIO.LOW)
    p_motor.ChangeDutyCycle(dutyCycle)

# サーボモーター駆動用関数
def servo_drive(angle):
    p_servo.ChangeDutyCycle(angle)

# 実行用関数
def main():
    # データ表示用
    pygame.init()
    surface = pygame.display.set_mode((WIDTH, HEIGHT))
    myfont = pygame.font.Font(None, 30)
    myclock = pygame.time.Clock()
    
    stick_data = [0, 7]
    while flg == 0:
        for event in pygame.event.get():
            if event.type == pygame.QUIT: end == 1
        
        # 受信したデータを数値に変換する
        m = s.readline().decode('utf-8').replace('\r\n','').split(',')
        m = [float(m[0]), float(m[1])] 
        #print(float(m[0]), float(m[1]))
        
        # DCモーター駆動
        motor_drive(stick_data[0])
        
        # サーボモーター駆動
        servo_drive(stick_data[1])
        
        stick_data = [0 if abs(m[0])<0.1 else m[0]*50, # 右スティック入力が0.1未満の場合は駆動しない
                      (-m[1]*1.5)+7] # サーボモーターは最小5.5, 最大8.5、中央7とする
        print(stick_data)
        
        surface.fill(WHITE)
        bitmaptext_0 = myfont.render('axis0:{}'.format(float(m[0])), True, BLACK)
        bitmaptext_1 = myfont.render('axis1:{}'.format(float(m[1])), True, BLACK)
   
        surface.blit(bitmaptext_0, (50, 50))
        surface.blit(bitmaptext_1, (50, 100))
        
        pygame.display.update()
        
    pygame.quit()
    
if __name__ == '__main__':
    main()
    
GPIO.cleanup()

PCとラズパイをBluetooth接続しよう

こちらのリンク先に詳細があるのでこちらを参照ください。
qiita.com
Bluetoothのペアリングが終わったら、以下のコマンドをラズパイ側で実行するのを忘れずに。

sudo sdptool add --channel=22 SP
sudo rfcomm listen /dev/rfcomm0 22

これでラズパイ側で受信可能な状態になったはずです。

動かしてみよう

上のコマンドでラズパイ側をリスン状態にした上で、PC、ラズパイそれぞれでpythonコードを実行します。ゲームパッドをPCにUSB接続しておくのを忘れずに。

youtu.be

こんな感じで動くわけです。

課題と今後

課題点

比較的単純な構成で作れるBluetooth接続としました。しかし、Bluetoothは近距離通信なため、本格的な遠隔操作、例えば東京のPCから大阪のロボットを遠隔操作する、みたいな用途には向きません。その場合はSSH接続するのが良いでしょう。SSH接続の場合、Bluetoothのシリアル通信のような単純なデータの送信だけではだめで、Webインターフェイスなりなんなりを作る必要がありそうです。そうした場合にどうするのか、はまだ考えていません。

今後

次は脳波コントロールだーと思っていたのですが、もう少し間をはさんでも良さそうな気がしてきました。たとえば筋電位系を使って、腕の動きを検出してロボットアームを動かす、などなど。いろいろ面白そうなことができると思うので、あれこれ調べてみたいと思います。

ちなみに、脳波関係だとOpenBCIというところで各種デバイスを販売しているのでオススメです。

openbci.com

いじょ。