ラズパイ5でラジコンを作る

ラズパイ5でラジコンを作ります。

![radicon.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3877283/4dc5012a-cdff-f003-5cbf-5b558694b774.jpeg)

その備忘録
ちなみに電子工作的ものはほぼやったことがない状態からのスタートです。

# 必要なものとか

| 名称 | 個数 | 参考価格 | 購入先 |
|:-:|:-:|:-:|:-:|
| Raspberry Pi 5 8GB | 1 | 14,960 | 秋月電子通商 |
| Raspberry Pi 5用公式ケース(黒) | 1 | 1,700 | 秋月電子通商 |
| ブレッドボード BB-801 | 1 | 220 | 秋月電子通商 |
| DRV8835使用ステッピング&DCモータードライバーモジュール | 1 | 550 | 秋月電子通商 |
| 楽しい工作シリーズ No.97 ツインモーターギヤーボックス | 1 | 680 | ヨドバシ |
| 楽しい工作シリーズ No.98 ユニバーサルプレートセット | 1 | 356 | ヨドバシ |
| 楽しい工作シリーズ No.229 32mm径 スプロケット＆クローラーセット | 1 | 357 | ヨドバシ |
| KIOXIA microSD 128GB UHS-I Class10 KLMEA128G | 1 | 1,150 | Amazon |
| エレコム モバイルバッテリー 15W 5000mAh 5V 3A Type-C USB-A EC-C12WH | 1 | 1,690 | Amazon |
| ジャンパーワイヤー 20cm オス メス | 1 | 799 | Amazon |
| 電池ケース（単3×3個用）| 1 | 358 | Amazon |
| はんだごてセット | 1 | ? | 家にあったやつ |
| USBケーブル Type-C | 1 | ? | 家にあったやつ |
| 単3電池 SANYO eneloop | 3 | ? | 家にあったやつ |
| プレステ4コントローラー[^1] | 1 | ? | 家にあったやつ |
| Bluetoothスピーカー | 1 | ? | 家にあったやつ |
| 小汚い木片[^2] | 1 | ? | 家にあったやつ |
| 合計 |  |22,820| |

[^1]:ドリフト気味
[^2]:買い足すのが面倒で工作したため、、
楽しい工作シリーズ No.157 ユニバーサルプレート (2枚セット) がよかったかなとか
https://www.tamiya.com/japan/products/70157/index.html
楽しい工作シリーズ No.253 ユニバーサルピラーセットも欲しかったなとか
https://www.tamiya.com/japan/products/70253/index.html


# インストール

Raspberry Pi Imagerを利用します。
https://www.raspberrypi.com/software/

Raspberry Pi Imagerを起動して以下を選択します。
`Raspberry Pi OS (Other) > Raspberry Pi OS Lite (64-bit)`
Debian Bookworm のデスクトップなしですね

進めていって、設定をカスタマイズします。
設定を編集するボタンを押下
- SSHの有効化
- ユーザー名
- パスワード

必要に応じてほか項目も設定してください。

僕の環境ではWifiも設定したので、ラズパイにSDカード指して電源コードを入れれば起動、接続できます。
モニタは接続しません。
すべてSSH経由で行います。
```shell:ラズパイへ接続
# IPアドレスはお使いの環境に合わせてください。
$ ssh pi@192.168.0.131
```

## インストール後

sshでログインし以下の感じで環境を作っています。
やらなくても問題ないものがほとんどですが、Python仮想環境は作っておかないとだめな気がします。

```shell
$ echo -e "set nocompatible\nset backspace=indent,eol,start" > ~/.vimrc
$ sudo cp ~/.vimrc /root/
$ sudo apt update
$ sudo apt upgrade
$ sudo apt install python3-pip
$ python3 -m venv --system-site-packages ~/.py_venv/default
$ cp .bashrc .bashrc.bak
$ sed -i "s/#alias ll='ls -l'/alias ll='ls -l'/" .bashrc
$ echo -e '\n# python venv\nexport VIRTUAL_ENV_DISABLE_PROMPT=1\nsource ~/.py_venv/default/bin/activate' >> .bashrc
$ sudo reboot
```


# プレステ4コントローラーをBluetoothで接続

`pyPS4Controller`を利用します。

https://github.com/ArturSpirin/pyPS4Controller

```shell
$ pip3 install pyPS4Controller
```

`bluetoothctl`を使って接続します。
ちなみに`ds4drv`を利用してコントローラーを接続するのは非推奨のようです。
デュアルショック4は`SHARE + PSボタン`長押しでペアリングモード (白点滅) になります。
```:コントローラーの接続
$ bluetoothctl
[bluetooth]# scan on
### Wireless Controller を探す ###
[bluetooth]# scan off
[bluetooth]# pair **:**:**:**:**:**
### ペアリングする際に yes の入力が必要の場合もあります ###
[bluetooth]# trust **:**:**:**:**:**
[bluetooth]# connect **:**:**:**:**:**
[bluetooth]# quit
```

ちなみに2回目以降は以下で接続できます。
```shell
$ bluetoothctl -- connect **:**:**:**:**:**
```

接続確認用のpythonを作成します。
```shell:作成
$ vi ds4test1.py
```
<details><summary>内容を確認する</summary>

```python:ds4test1.py
from pyPS4Controller.controller import Controller

class MyController(Controller):
    def __init__(self, **kwargs):
        Controller.__init__(self, **kwargs)

controller = MyController(interface="/dev/input/js0", connecting_using_ds4drv=False)
controller.listen()
```
</details>

```shell:実行
$ python3 ds4test1.py
```
コントローラーをガチャガチャしてなんか表示されればOKだと思います。


# スピーカーをBluetoothで接続

スピーカーと書きましたが手持ちになかったので骨伝導イヤホン (Shokz OpenMove) です！
スピーカーをBluetoothで使えるようにする場合、必要なライブラリがあるのでインストールします。
`pair`と`trust`はできるのですが`connect`できませんでした。
```shell
$ sudo apt install pulseaudio pulseaudio-module-bluetooth
$ pulseaudio --start
```

```:スピーカーの接続
$ bluetoothctl
[bluetooth]# scan on
### 僕の環境では OpenMove by Shokz でしたがお使いのスピーカーを探してください ###
[bluetooth]# scan off
[bluetooth]# pair ++:++:++:++:++:++
### ペアリングする際に yes の入力が必要の場合もあります ###
[bluetooth]# trust ++:++:++:++:++:++
[bluetooth]# connect ++:++:++:++:++:++
### 接続されるとプロンプトが変わる ###
[OpenMove by Shokz]# quit
```

```shell:音が出るか確認
$ aplay /usr/share/sounds/alsa/Front_Center.wav
```
出ない場合再起動後もう一度`aplay`を試してみてください。
```shell:再起動
$ sudo reboot
```

実際の運用ではpython `pygame.mixer` を利用し再生します。

https://www.pygame.org/docs/ref/mixer.html

```shell
$ pip3 install pygame
```

なにか適当な音源 (mp3等) を用意してください。

```shell:作成
$ vi soundtest1.py
```
<details><summary>内容を確認する</summary>

```python:soundtest1.py
import pygame.mixer
import time

pygame.mixer.init()
pygame.mixer.music.load("適当な音源")
pygame.mixer.music.play(-1)
time.sleep(10)
pygame.mixer.music.stop()
```
</details>

```shell:実行
$ python3 soundtest1.py
```
スピーカーから音が出ればOKです。


# GPIO ピンマッピング

準備として以下をはんだごてでくっつけます。
1. DRV8835使用ステッピング&DCモータードライバーモジュール
2. モーターとジャンパーワイヤー x 2[^3]
3. 電池ボックスとジャンパーワイヤー[^3]

汚い
<img width="360" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3877283/dd7a1efc-eacf-157d-c049-023d6016bb9b.jpeg">

<img width="360" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3877283/835c33ed-8239-c219-de16-68b642bcf461.jpeg">

[^3]:ジャンパーワイヤーは片側を切って利用


以下のコマンドでラズパイのハードウェア情報？を見ることができます。
```
$ pinout
Description        : Raspberry Pi 5B rev 1.0
Revision           : d04170
SoC                : BCM2712
RAM                : 8GB
Storage            : MicroSD
USB ports          : 4 (of which 2 USB3)
Ethernet ports     : 1 (1000Mbps max. speed)
Wi-fi              : True
Bluetooth          : True
以下略
```

ブレッドボード上でつなぎました。
ラズパイからみたDCモータードライバーモジュールとの接続です。
```
| ---- | --- Raspberry pi 5  --- | ---- |
|  VCC |    3V3  (1) (2)  5V     |      |
|      |  GPIO2  (3) (4)  5V     |      |
|      |  GPIO3  (5) (6)  GND    | GND  |
|      |  GPIO4  (7) (8)  GPIO14 |      |
|      |    GND  (9) (10) GPIO15 |      |
|      | GPIO17 (11) (12) GPIO18 |      |
|      | GPIO27 (13) (14) GND    |      |
|      | GPIO22 (15) (16) GPIO23 |      |
|      |    3V3 (17) (18) GPIO24 |      |
|      | GPIO10 (19) (20) GND    |      |
|      |  GPIO9 (21) (22) GPIO25 |      |
|      | GPIO11 (23) (24) GPIO8  |      |
|      |    GND (25) (26) GPIO7  |      |
|      |  GPIO0 (27) (28) GPIO1  |      |
|      |  GPIO5 (29) (30) GND    |      |
|      |  GPIO6 (31) (32) GPIO12 |      |
|      | GPIO13 (33) (34) GND    | MODE |
| AIN1 | GPIO19 (35) (36) GPIO16 | AIN2 |
| BIN1 | GPIO26 (37) (38) GPIO20 | BIN2 |
|      |    GND (39) (40) GPIO21 |      |
```

電池ボックスからみたDCモータードライバーモジュールとの接続です。
```
| -- | -- Battery Box -- | --- |
| VM | PLUS        MINUS | GND |
```

DCモータードライバーモジュールから見た接続です。
以下のように記述します。
RP: ラズパイ
ML: モーター左
MR: モーター右
BB: 電池ボックス
```
| -------------- | -- Module -- | ---------- |
|           BB:+ |    VM   VCC  | RP:3V3     |
|             ML | AOUT1   MODE | RP:GND(34) |
|             ML | AOUT2   AIN1 | RP:GPIO19  |
|             MR | BOUT1   AIN2 | RP:GPIO16  |
|             MR | BOUT2   BIN1 | RP:GPIO26  |
| RP:GND(6) BB:- |   GND   BIN2 | RP:GPIO20  |
```

ここまで接続できたらモーターが動くはずです。
`gpiozero`というライブラリを利用しテストします。

https://gpiozero.readthedocs.io/en/latest/index.html

```shell:作成
$ vi gpiotest.py
```

<details><summary>内容を確認する</summary>

```python:gpiotest.py
from gpiozero import Motor
from time import sleep

PIN_AIN1 = 19
PIN_AIN2 = 16
PIN_BIN1 = 26
PIN_BIN2 = 20

motor_left = Motor(PIN_AIN1, PIN_AIN2)
motor_right = Motor(PIN_BIN2, PIN_BIN1)

print("forward")
motor_left.forward(0.8)
motor_right.forward(0.8)

sleep(1)

print("stop")
motor_left.stop()
motor_right.stop()
```
</details>

```shell:実行
# 電池ボックスのスイッチを入れてから実行
$ python3 gpiotest.py
```

左右のモーターが動けばOKです。

# 最終調整

部品など組み付けて完成させます。
プログラムは、コントローラーで動かして、音を出して、みたいな感じを完成にしてます。

下画像は以下を組み立てたもの (モーターは外してます)
+ 楽しい工作シリーズ No.97 ツインモーターギヤーボックス
+ 楽しい工作シリーズ No.98 ユニバーサルプレートセット
+ 楽しい工作シリーズ No.229 32mm径 スプロケット＆クローラーセット

<img width="360" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3877283/84001852-a702-749f-3d5b-b2e2f2659e74.jpeg">

プログラムの可読性が悪いので一旦以下を置きます。
```shell:作成
$ vi MyController.py
```

<details><summary>内容を確認する</summary>

```python:MyController.py
from pyPS4Controller.controller import Controller

class MyController(Controller):
    def __init__(self, **kwargs):
        Controller.__init__(self, **kwargs)
    def on_x_press(self):
        pass
    def on_x_release(self):
        pass
    def on_triangle_press(self):
        pass
    def on_triangle_release(self):
        pass
    def on_circle_press(self):
        pass
    def on_circle_release(self):
        pass
    def on_square_press(self):
        pass
    def on_square_release(self):
        pass
    def on_L1_press(self):
        pass
    def on_L1_release(self):
        pass
    def on_L2_press(self, value):
        pass
    def on_L2_release(self):
        pass
    def on_R1_press(self):
        pass
    def on_R1_release(self):
        pass
    def on_R2_press(self, value):
        pass
    def on_R2_release(self):
        pass
    def on_up_arrow_press(self):
        pass
    def on_up_down_arrow_release(self):
        pass
    def on_down_arrow_press(self):
        pass
    def on_left_arrow_press(self):
        pass
    def on_left_right_arrow_release(self):
        pass
    def on_right_arrow_press(self):
        pass
    def on_L3_up(self, value):
        pass
    def on_L3_down(self, value):
        pass
    def on_L3_left(self, value):
        pass
    def on_L3_right(self, value):
        pass
    def on_L3_y_at_rest(self):
        pass
    def on_L3_x_at_rest(self):
        pass
    def on_L3_press(self):
        pass
    def on_L3_release(self):
        pass
    def on_R3_up(self, value):
        pass
    def on_R3_down(self, value):
        pass
    def on_R3_left(self, value):
        pass
    def on_R3_right(self, value):
        pass
    def on_R3_y_at_rest(self):
        pass
    def on_R3_x_at_rest(self):
        pass
    def on_R3_press(self):
        pass
    def on_R3_release(self):
        pass
    def on_options_press(self):
        pass
    def on_options_release(self):
        pass
    def on_share_press(self):
        pass
    def on_share_release(self):
        pass
    def on_playstation_button_press(self):
        pass
    def on_playstation_button_release(self):
        pass
```
</details>

MyControllerを継承し以下を作成
```shell:作成
$ vi ctrl.py
```

<details><summary>内容を確認する</summary>

```python:ctrl.py
from MyController import MyController
from gpiozero import Motor
import pygame.mixer
import os
import sys

class Ctrl(MyController):

    PIN_AIN1 = 19
    PIN_AIN2 = 16
    PIN_BIN1 = 26
    PIN_BIN2 = 20

    BASE_SPEED = 0.3

    L3_RESTING = True
    R3_RESTING = True

    SOUND_DIR = ""
    isBacksound = False

    motor_left = None
    motor_right = None

    def __init__(self, **kwargs):
        MyController.__init__(self, **kwargs)

        self.motor_left = Motor(self.PIN_AIN1, self.PIN_AIN2)
        if isinstance(self.motor_left, Motor) == False:
            print("Motor left is Not Ready")
            sys.exit()

        self.motor_right = Motor(self.PIN_BIN2, self.PIN_BIN1)
        if isinstance(self.motor_right, Motor) == False:
            print("Motor right is Not Ready")
            sys.exit()

        self.SOUND_DIR = os.path.dirname(__file__) + "/"
        pygame.mixer.init()

    def _calcSpeed(self, value):
        speed = value / 65534 + self.BASE_SPEED
        return round(speed, 3)

    def on_L3_up(self, value):
        if value < -5000:
            self.L3_RESTING = False
            speed = self._calcSpeed(value * -1)
            self.motor_left.forward(speed)
            print("Left forward: {}".format(speed))

            if self.isBacksound == False:
                self.isBacksound = True
                pygame.mixer.music.load(self.SOUND_DIR + "sound0.mp3")
                pygame.mixer.music.play(-1)
        else:
            self.on_L3_y_at_rest()

    def on_L3_down(self, value):
        if value > 5000:
            self.L3_RESTING = False
            speed = self._calcSpeed(value)
            self.motor_left.backward(speed)
            print("Left backward: {}".format(speed))

            if self.isBacksound == False:
                self.isBacksound = True
                pygame.mixer.music.load(self.SOUND_DIR + "sound0.mp3")
                pygame.mixer.music.play(-1)
        else:
            self.on_L3_y_at_rest()

    def on_L3_y_at_rest(self):
        if self.L3_RESTING == False:
            self.L3_RESTING = True
            print("Left Speed 0")
            self.motor_left.stop()

    def on_R3_up(self, value):
        if value < -5000:
            self.R3_RESTING = False
            speed = self._calcSpeed(value * -1)
            self.motor_right.forward(speed)
            print("Right forward: {}".format(speed))
        else:
            self.on_R3_y_at_rest()

    def on_R3_down(self, value):
        if value > 5000:
            self.R3_RESTING = False
            speed = self._calcSpeed(value)
            self.motor_right.backward(speed)
            print("Right backward: {}".format(speed))
        else:
            self.on_R3_y_at_rest()

    def on_R3_y_at_rest(self):
        if self.R3_RESTING == False:
            self.R3_RESTING = True
            print("Right Speed 0")
            self.motor_right.stop()

    def on_x_release(self):
        self._doSpeak(self.SOUND_DIR + "sound1.mp3")

    def on_triangle_release(self):
        self._doSpeak(self.SOUND_DIR + "sound2.mp3")

    def on_circle_release(self):
        self._doSpeak(self.SOUND_DIR + "sound3.mp3")

    def on_square_release(self):
        self._doSpeak(self.SOUND_DIR + "sound4.mp3")

    def _doSpeak(self, path):
        self.isBacksound = False
        pygame.mixer.music.stop()
        pygame.mixer.music.load(path)
        pygame.mixer.music.play(1)

controller = Ctrl(interface="/dev/input/js0", connecting_using_ds4drv=False)
controller.listen(timeout=60)
```
</details>

実行する前に音ファイルを置きます。
仮に `sound0.mp3` `sound1.mp3` `sound2.mp3` `sound3.mp3` `sound4.mp3` として利用しています。

```shell:実行
$ python3 ctrl.py
```

コントローラー、左スティック、右スティック、で前後左右、
マル、バツ、サンカク、シカクで音がなればOKです。

ちょっとスピード感は違いますが参考までに
<img width="360" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3877283/6b8b72df-428e-851a-10bb-ec9e73eb21e6.gif">

# 実ははまった点

### 1. 電力不足

はじめに組んだ際に発生した問題。
実は最初の段階では、単3電池を2つで動かしていました。
ラズパイと電源、モーターを接続しいざスタートしたらモーターから異音が、、
`motor_left.forward(0.8) motor_right.forward(0.8)`
このあたりかと思って、値を上げたり下げたりしてもだめ、テスターを使って通電確認したりしても問題なし。
その後モーターを一つにしたら動く。
調べてみたらどうも電力が足りない？と考え

- エレクラフトシリーズ No.28 75028 低回転型 130モーター

これを注文し取り付け。
結果的に問題なく動作。
ラジコン本体の組付け、コントローラー接続、スピーカー接続、動いた！

### 2. パワー不足

低回転型に変えたその後...
動いたのですが、曲がれない。。パワー不足か？

実はこの時点では

- 楽しい工作シリーズ No.150 単3電池ボックス(1本用×2・逆転スイッチ付) 

がついていました。

で、なんかムカついたので、やっぱりモーターを最初のやつに戻した上で電池一本増やすことにした。(これが問題あるのかどうかはよくわかりませんw)
合計3Vから4.5Vへ。ただ、エネループなので公称値で3.6Vですかね。 (実測は4Vぐらい出てましたが。。)
ミニ四駆を3.7Vで動かしてるYoutubeとか見たことがあったのでいけるんじゃないのかな程度でやってみました。
結果、動いたし曲がりました。
で、今に至ります。