車みたいにラジコンを運転したい
ラジコンを遠隔で車みたいに操作できたら楽しそうと思ったので作ることにしました。
${\huge \textsf{この記事では、Windows環境を前提としています}}$
MacやLinux、Win10よりも前のOSでの動作は想定していません。
1.作動の仕組み
信号の伝達図
解説
相関図は上みたいな感じ
- ハンドルコントローラのアクセルとかブレーキとかの信号をUSB経由でPCに送信する
- PCで受け取った信号をWi-Fiをとしてラズパイに送信
- ラズパイからMDに信号送信
- MDからサーボモータや、ESCに信号を送信
- ESCから540モータに信号を送信
といったような流れ。
使用するもの
- ハンドルコントローラ (GT Force Pro等 GT Forceはドライバが微妙なので推奨しない)
- PC (Windows10以上搭載 Wi-Fi,USB-Aがついているもの)
- ラジコンシャーシ (TT-02等)
- Raspberry Pi 4B (3から動くと思われるが、検証していない)
- microSDカード (ラズパイOS用 16GB以上が良い)
- モータドライバ (PCA9685 PWM信号を出すだけのもの)
- サーボモータ (TD‐8120MG等)
- ESC (amazonにある激安品でも可だが、タミヤ製のちゃんとしたものを推奨する)
- 540モータ (タミヤが売っているトルクチューンモータなど)
- ジャンパワイヤー
- モバイルバッテリー (Raspberry Pi用の電源)
- 電池ボックス (単三電池4本直列でつなぐやつ。MD用の電源)
- RCラジコン用のバッテリ (タミヤに売ってるもので良し)
2.準備
2-1 ハードウェア
ラジコンの組み立て
シャーシの組み立て
まずは操作するラジコン本体を組み立てていく。
買ったシャーシに応じて説明書通りに組み立てていく。
画像はタミヤのTT-02
ESC
大抵の場合ESCは最初から使えるものが多いが、稀にハンダ付けなどをしなければならない物があるので、必要な場合はしておく。
モータドライバ
モータドライバ(PCA9685)のGND,VCC,SDA,SCLの端子を、ジャンパ線を使ってリードしておく。オス-メスのジャンパ線を使う。オス側をモータドライバにハンダ付けし、メス側をどこかにさせるような状態にしておく。
バッテリーボックスに電池を入れて、モータドライバにつなげておく。
各コンポーネントの接続
モータドライバ
モータドライバには、ESCとサーボモータを接続する。
PWM,V+,GNDをそれぞれ間違いないように接続する。
Tips
大抵の場合、GNDが黒などの暗い色で、V+が赤などの明るい色である。
ESC
ESCは540モータを極性を間違えなく接続しておく。
Raspberry Pi
Raspberry Piには映像ケーブルと電源、操作系のインターフェース(マウスやキーボードなど)、電源用モバイルバッテリーを繋いでおく。映像はmicroHDMI、電源はUSB‐C、マウス等はUSB‐Aのコネクターに接続する。
GPIOは以下のGPIOのPINOUTの図を見てモータードライバと配線する。
GNDはお互いのGNDに、VCCは3.3V PWRに、SDAはGPIO2に、SCLはGPIO3に配線する。
2-2 ソフトウェア
PCに関する準備
各種インストール
Visual Studio Code
これからコーディングしていくにあたって、環境となるVisual Studio Codeをインストールしていく。
公式サイトからVisual Studio Codeのインストーラをダウンロードする。
ダウンロードしたインストーラを実行し、指示に従ってインストールしていく。
インストール後、起動し、Pythonや、C/C++、Github Copilot等を入れておく。
Tips
Github Copilotはいれなくても良いが、修正等をChatGPTなどを用いて自動でやってくれるので、いれておくと便利。
Python
PC側でもPythonを実行することがあるので最新版をインストールしておく。
公式サイトからPythonのインストーラをダウンロードする。
ダウンロードしたインストーラを実行して、指示に従ってインストールしていく。
ハンドルコントローラ
ハンドルコントローラとPCを接続し、PCがコントローラを認識することを確認する。
認識されたことが確認できたら、コントローラにあったドライバーをインストールする。
Tips
LogiCoolのハンドルコントローラを使用している場合はこちらからダウンロードできる。
Raspberry Piの準備
PCにRaspberry Pi Imagerをダウンロードし、インストールする。
microSDカードをPCと接続し、Raspberry Pi Imagerを起動する。
起動後、CHOOSE DEVICEは、自分の機種を選択(ここではRaspberry Pi 4を選択)し、CHOOSE OSは64bitバージョンの Raspberry PI OSを選択する。CHHOOSE STOREGEでは、先程接続したmicroSDカードを選択する。
OSがインストールされるまで待ち、インストールされたらSDカードを安全な方法で取り外す。
取り外したSDカードをRaspberry Pi本体にセットする。
初期設定
Raspberry Piに電源を接続し、起動する。
起動すると、ユーザーネームやパスワードの設定を求められる。
ユーザーネームは、RCPI
とし、パスワードは0000
としておく。
個人で使うだけなので、パスワードはこれで良い。
2-3 Raspberry Pi と PCの接続
パソコンのモバイルホットスポットを有効にする。
Tips
有効化の方法は公式ページを参照
有効化した後、Raspberry Pi のWi-Fiの接続設定より、AP化したパソコンのSSIDを選択しパスワードを入力する。
正常に接続できれば準備完了。
プログラムの作成に入っていく。
3.プログラムの作成
本格的なプログラムの作成に入る前に、下準備をしておく。
各種ツール等のインストール
コントローラの情報を変換したり、モータードライバに正しい信号を送ったりするためのツールをインストールしておく。
コマンドプロンプトを開き、以下の内容をRaspberry Piでも、PCでも実行する。
py -3 -m pip install --upgrade pip
py -3 -m pip install pygame
py -3 -m pip install adafruit-circuitpython-pca9685
py -3 -m pip install adafruit-circuitpython-motor
py -3 -m pip install RPi.GPIO
py -3 -m pip install websockets
パソコン側では、RPI.GPIOはエラーが出てインストールできないが、それで問題ない。
PC側のプログラム
Visual Studio Code を開き、File→New File→Python File の順に選択していき、win.py
という名前で新しいファイルを作成する。
以下のコードを書き込み、保存する。
#Windows PC 側のコード
import pygame
import socket
import struct
import time
UDP_IP = "xxx.xxx.xxx.xxx" # Raspberry PiのIPアドレス
UDP_PORT = 5005
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
def init_controller():
pygame.init()
pygame.joystick.init()
if pygame.joystick.get_count() == 0:
raise Exception("GT Force Proが接続されていません")
joystick = pygame.joystick.Joystick(0)
joystick.init()
return joystick
def map_range(value, in_min, in_max, out_min, out_max):
return (value - in_min) * (out_max - out_min) / (in_max - in_min) + out_min
def apply_throttle_curve(value):
return (value / 100) ** 2 * 100
def apply_deadzone(value, deadzone=0.02):
if abs(value) < deadzone:
return 0
return value
def main():
joystick = init_controller()
print("ステアリングコントローラ接続完了")
try:
while True:
pygame.event.pump()
steering = joystick.get_axis(0)
raw_throttle = -joystick.get_axis(1)
raw_brake = -joystick.get_axis(2)
print(f"Raw throttle value: {raw_throttle:.3f}, Raw brake value: {raw_brake:.3f}")
steering = apply_deadzone(steering)
throttle = apply_deadzone(raw_throttle)
brake = apply_deadzone(raw_brake)
steering_angle = map_range(steering, -1.0, 1.0, -21, 21) #ステアリングの舵角を変えたい場合は、21の値を変える。
if raw_throttle <= -0.99 and raw_brake <= -0.99:
throttle_value = 0
direction = 1 # 前進
elif raw_throttle > -0.99:
raw_throttle_value = map_range(throttle, -0.99, 1.0, 0, 50) #50の値を変えることで、前進する速度を変えられる。
throttle_value = apply_throttle_curve(raw_throttle_value)
direction = 1 # 前進
elif raw_brake > -0.99:
raw_brake_value = map_range(brake, -0.99, 1.0, 0, 30) #30の値を変えることで、後退する速度を変えられる。
throttle_value = apply_throttle_curve(raw_brake_value)
direction = -1 # 後退
print(f"Throttle value: {throttle_value:.1f}%, Direction: {'Forward' if direction == 1 else 'Reverse'}")
data = struct.pack('ffi', steering_angle, throttle_value, direction)
sock.sendto(data, (UDP_IP, UDP_PORT))
time.sleep(0.02)
except KeyboardInterrupt:
print("プログラムを終了します")
finally:
pygame.quit()
sock.close()
if __name__ == "__main__":
main()
Tips
Rasbperry PiのIPアドレスと書かれているところは、PC側のモバイルホットスポットの設定よりIPアドレスを確認して、書き換えておく。
Raspberry Pi 側のプログラム
Raspberry Pi を起動し、デスクトップ画面にする。
デスクトップ画面右上のフォルダマークのようなものをクリックし、エクスプローラもどきを起動する。
起動したそのフォルダーで、新しいファイルを作成し、raspi.py
という名前にしておく。
Geanyというソフトでraspi.py
を開き、以下のコードを書き込み保存する。
#raspberry pi 側のコード
import socket
import struct
import time
from adafruit_pca9685 import PCA9685
from adafruit_motor import servo, motor
import board
import busio
UDP_IP = "0.0.0.0"
UDP_PORT = 5005
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind((UDP_IP, UDP_PORT))
i2c = busio.I2C(board.SCL, board.SDA)
pca = PCA9685(i2c)
pca.frequency = 50 # 50Hz サーボモータに合わせて変更する
servo_channel = pca.channels[0] # サーボモータ用
esc_channel = pca.channels[1] # ESC用
servo_motor = servo.Servo(servo_channel, min_pulse=500, max_pulse=2500)
def calibrate_esc():
"""ESCのキャリブレーション手順"""
print("ESCキャリブレーション開始")
print("1. ESCの電源を切ってください")
input("2. 準備ができたらEnterを押してください...")
print("最大スロットル信号を送信中...")
esc_channel.duty_cycle = int(0.1 * 65535)
print("3. ESCに電源を接続してください")
print("4. ビープ音が鳴るまで待ってください")
input("5. ビープ音が鳴ったらEnterを押してください...")
print("ニュートラル信号を送信中...")
esc_channel.duty_cycle = int(0.075 * 65535)
time.sleep(2)
print("キャリブレーション完了")
return True
def initialize_esc():
"""ESCの初期化"""
print("ESC初期化開始")
esc_channel.duty_cycle = int(0.075 * 65535) # 7.5% duty cycle
time.sleep(2)
print("ESC初期化完了")
def set_esc_speed(throttle_percent, direction):
"""
ESCの速度を設定
direction: 1=前進, -1=後退
"""
if throttle_percent == 0:
duty_cycle = int(0.075 * 65535)
duty_percent = 7.5
else:
if direction == 1: # 前進
duty_percent = 0.075 - (throttle_percent / 100.0) * 0.025
else: # 後退
duty_percent = 0.075 + (throttle_percent / 100.0) * 0.025
duty_cycle = int(duty_percent * 65535)
esc_channel.duty_cycle = duty_cycle
return duty_percent * 100
def main():
# キャリブレーションを行うか尋ねる
if input("ESCをキャリブレーションしますか? (y/n): ").lower() == 'y':
calibrate_esc()
else:
initialize_esc()
print("UDP通信待機中...")
sock.settimeout(0.5)
try:
while True:
try:
data, addr = sock.recvfrom(1024)
steering_angle, throttle_value, direction = struct.unpack('ffi', data)
servo_motor.angle = steering_angle + 90
actual_duty = set_esc_speed(throttle_value, direction)
print(f"Throttle: {throttle_value:.1f}%, Direction: {'Forward' if direction == 1 else 'Reverse'}, Duty: {actual_duty:.2f}%")
except socket.timeout:
set_esc_speed(0, 1)
continue
except KeyboardInterrupt:
print("\nプログラムを終了します")
finally:
set_esc_speed(0, 1)
time.sleep(0.5)
pca.deinit()
sock.close()
if __name__ == "__main__":
main()
保存する場所を間違えると実行できなくなってしまうので、注意する。
4.プログラムの実行
プログラムを実行して実際に動かしてみる。
PC側
電源を接続したハンドルコントローラとPCをつないでおく。
Wi-Fiを有効にして、モバイルホットスポットをつけておく。
Raspberry PiをWi-Fi接続した後に、作っておいたwin.py
を実行する。
Raspberry PI側
モニター、マウスを接続した後に、モバイルバッテリーを接続し、電源をいれる。
電源が入りデスクトップ画面が出たら、エクスプローラもどきを起動し、Geanyを用いてraspi.py
を開き、プログラムを実行する。
プログラムが正しく実行され、ハンドルコントローラによる操作ができるようになったら、モニターやマウスなどを取り外して本体のみで動けるようにする。
5.最後に
以上の工程に沿ってできればうまくできると思うのだが、多分完全なものではないのでコメント等でたくさん質問してほしいです。初歩的なものから応用まで、答えられるものはできるだけ答えるつもりなのでよろしくお願いします。