はじめに
ずっと我が家で埃をかぶっていた Raspberry Pi Zero W で、突然ラジコン作りたくなったのでつくりました
これはその備忘録的なやつです
- はじめに
- 完成品
- 環境構築
- Raspberry Pi の準備
- Raspberry Pi をアクセスポイントにする
- Bluetooth コントローラーを接続する
- カメラを接続する
- カメラの映像をストリーミングする
- ストリーミング表示用ページを作る
- ラジコンのプログラムを作る
- 基板を作る
- 組み立てる
- おわりに
完成品
どんなことができるの?
- Xbox コントローラーで操作するラジコン
- カメラの映像をリアルタイムでストリーミングして見れる(レイテンシはある)
- ライトが点いて暗いところでも走れる(気がする)
使ったもの
名前 | 説明 | 参考リンクなど |
---|---|---|
ツイストクローラー工作セット(2chリモコン) | ベースとなる筐体 |
タミヤ公式 |
連結式クローラー&スプロケットセット | 樹脂製のクローラーから置き換え(使わなくてもいい) | タミヤ公式 |
ユニバーサルプレートセット | Raspberry Pi やカメラを固定する | タミヤ公式 |
SG90 用カメラパンチルトキット | うちに転がってたやつ | アマゾン検索結果 |
Raspberry Pi Zero 2 W | FFC コネクタ壊した Raspberry Pi Zero W の代わり | スイッチサイエンス |
Raspberry Pi Camera V2 | うちに転がってたやつ | スイッチサイエンス |
Raspberry Pi Zero カメラケーブル | いろんな長さが入ってて比較的安い(使ってるのは30cm) | アマゾン |
Raspberry Pi マウント用ネジセット | Raspberry Pi 本体や HAT を固定するのにちょうどいいスペーサーとかが揃ってるセット | アマゾン |
7.4V 2S Li-Po バッテリー | ラジコンのバッテリーとしてちょうどいい大きさで、しかも2つ入り | アマゾン |
Xbox ワイヤレス コントローラー | うちに転がってたやつ | Microsoft 公式 |
カメラ照明基板 | カメラをパンチルトキットに取り付けられるようにするのと、暗所でもカメラで見るための基板 |
自作 |
電源・モータードライバ基板 | バッテリーから Raspberry Pi Zero の 5V 電源を取り出すのと、モーターやサーボを駆動するための基板 | 自作 |
その他 | 導線とかネジとか必要なものを適宜 |
プログラム
作ったプログラムはここにあります
環境構築
WSL2 から作業したいので、 WSL2 と、ついでに Windows Terminal をインストールします
Windows Terminal は最高なんで、みんな入れましょう
管理者権限でコマンドプロンプトか Power Shell を起動して、以下を実行
wsl --install
Microsoft Store で Ubuntu と Windows Terminal をインストール
Windows Terminal の設定とかは各自調べていい感じにしてください
これ以降は基本的に WSL2 (Ubuntu) 上で作業します
Raspberry Pi の準備
SD カードにイメージを書き込む
やり方は検索すれば無限に転がってるので割愛
今回はヘッドレス(画面とか表示しない)で運用するので、そのイメージ(Lite
って書いてあるやつ)を選択してください
とりあえず公式の通りにやればできます
注意点としては、 SSH を有効にして、認証方法をパスワードにしておくくらいです
https://www.raspberrypi.com/documentation/computers/getting-started.html#raspberry-pi-imagerwww.raspberrypi.com
今回使ってる OS のバージョンとかはこんな感じです(Raspberry Pi OS (Legacy) Lite)
$ cat /etc/os-release PRETTY_NAME="Raspbian GNU/Linux 11 (bullseye)" NAME="Raspbian GNU/Linux" VERSION_ID="11" VERSION="11 (bullseye)" VERSION_CODENAME=bullseye ID=raspbian ID_LIKE=debian HOME_URL="http://www.raspbian.org/" SUPPORT_URL="http://www.raspbian.org/RaspbianForums" BUG_REPORT_URL="http://www.raspbian.org/RaspbianBugs"
WSL2 から SSH 接続して作業できるようにする
WSL2 から Raspberry Pi に SSH 接続して作業していくので、そのための設定をしていきます
Raspberry Pi に周辺機器を一切接続せず、完全にリモートでやっていきます
やってるうちにエラーが出てきても、エラー文でググればどうすればいいのか説明してくれてる人がいるので、頑張ってください
Raspberry Pi の IP を調べる
Raspberry Pi に SSH するには Raspberry Pi の IP アドレスがわからないとどうしようもありません
まあ、 Raspberry Pi にキーボードやらディスプレイ繋いで直接見れば簡単ですが、ここは意地でもやりません
nmap をインストール
ローカル IP をスキャンするために nmap
をインストールします
sudo apt install -y nmap
ローカル IP をスキャンして Raspberry Pi の IP を見つける
ローカル IP は基本的に 192.168.1.XXX
なので、それをスキャンします
ルーターの設定によっては 192.168.2.XXX
とか 192.168.10.XXX
とかかもしれないので、それぞれ読み替えてください
まずは Raspberry Pi の電源を 入れていない 状態で nmap
のスキャンを実行します(スキャンが終わるまで時間がかかるので、焦らず待ちましょう)
sudo nmap -sn 192.168.1.0/24
次に、 Raspberry Pi の電源を入れて(起動が完了して電源 LED が点灯している状態)でスキャンを実行します
すると、 IP アドレスが増えてると思うので、それが Raspberry Pi の IP アドレスです
初回 SSH 接続
Raspberry Pi の IP アドレスがわかったので、 SSH 接続します
ssh <USER>@<IP>
<USER>
はイメージを書き込んだ時に設定したユーザー名、 <IP>
は先ほど確認した Raspberry Pi の IP アドレスです
パスワード入力を求められるので、イメージを書き込んだ時に設定したパスワードを入力します
これで Raspberry Pi に入れました
IP アドレスを固定
今後 SSH 接続するとき Raspberry Pi の IP アドレスが変わると面倒なので固定します
/etc/dhcpcd.conf
を編集します
sudo vi /etc/dhcpcd.conf
ファイルの最後に以下を追加します
interface wlan0 static ip_address=<IP>/24 static routers=192.168.1.1 static domain_name_servers=192.168.1.1
<IP>
は何でもいいですが、先ほど確認した Raspberry Pi の IP アドレスを入れておけばいいと思います
routers
と domain_name_servers
は、自分が使用しているルーターの設定に合わせて変えてください
確認
まずは適当に ping
を打って、ちゃんと設定できたか確認します
ping -c 5 www.google.com
Network is unreachable
とか出てきたら、どこか設定が間違ってるので確認してください
問題なければ再起動して、設定した IP アドレスに対して SSH 接続できるか確認しましょう
sudo reboot
もう一度 SSH 接続できれば成功です
ssh <USER>@<IP>
SSH キーで接続できるようにする
https://www.raspberrypi.com/documentation/computers/remote-access.html#passwordless-ssh-accesswww.raspberrypi.com
毎回 SSH 接続するときにパスワード入力するのは面倒なので SSH キーで認証するようにします
Raspberry Pi に入っていたら WSL2 に戻ります
以降は WSL2 上での作業です
SSH キーの確認
以下のコマンドを実行します
ls ~/.ssh
これで id_rsa.pub
などのファイルがあれば、すでに SSH キーがあるので、生成の必要はありません
SSH キーの生成
SSH キーがなかった場合、この手順を実行します
以下のコマンドで SSH キーを生成します
ssh-keygen
デフォルトでは id_rsa
という名前になりますが、違う名前にしたければ以下のようにします
ssh-keygen -f <NAME>
これで SSH キー(秘密鍵 id_rsa
、公開鍵 id_rsa.pub
)やその他必要なものが生成されました
以下のコマンドで生成されたものが確認できます
ls ~/.ssh
公開鍵を Raspberry Pi に送る
以下のコマンドを実行します(途中でパスワードの入力が求められる)
ssh-copy-id <USER>@<IP>
名前を変えた場合は以下です(絶対パスで公開鍵を指定しないとダメっぽい)
ssh-copy-id -i ~/.ssh/<NAME>.pub <USER>@<IP>
これで SSH 接続(ssh <USER>@<IP>
)すると、パスワード入力を求められることなく Raspberry Pi に入れると思います
Raspberry Pi の基本的な設定は終了です
パッケージを最新にアップデートする
いつものおまじないです
sudo apt update sudo apt upgrade -y
ついでに pip
も入れます
sudo apt install -y python3-pip
Raspberry Pi をアクセスポイントにする
最終的にカメラの映像をストリーミングするのですが、 Raspberry Pi に直接接続して見られるようにしたほうが便利です(Wi-Fi が飛んでない屋外で遊んだりするときとか)
そこで、 Raspberry Pi をアクセスポイントにします
アクセスポイントとして設定するのに hostapd
と dnsmasq
というパッケージを使います
以下の記事を参考にしました
ちなみに、 Raspberry Pi OS (bookworm) で NetworkManager を使った場合の方法もまとめてます
現在のネットワークインターフェースと MAC アドレスを確認
まずは現在設定されているネットワークインターフェースを確認してみましょう
iw dev
実行結果
phy#0 Unnamed/non-netdev interface wdev 0x2 addr xx:xx:xx:xx:xx:xx type P2P-device txpower 31.00 dBm Interface wlan0 ifindex 2 wdev 0x1 addr xx:xx:xx:xx:xx:xx ssid <SSID 名> type managed channel 10 (2457 MHz), width: 20 MHz, center1: 2457 MHz txpower 31.00 dBm
wlan0
の addr
の値(MAC アドレス)は今後も使うので、どこかにメモしておいてください
仮想ネットワークインターフェースを作る
まずは ap0
という名前でネットワークインターフェースを作ります
sudo iw phy phy0 interface add ap0 type __ap
次に、 ap0
に MAC アドレスを設定します
さっき確認した addr
と同じで大丈夫です
sudo ip link set ap0 address xx:xx:xx:xx:xx:xx
確認すると ap0
が新たにできています
phy#0 Interface ap0 ifindex 3 wdev 0x3 addr xx:xx:xx:xx:xx:xx type AP channel 10 (2457 MHz), width: 20 MHz, center1: 2457 MHz txpower 31.00 dBm Unnamed/non-netdev interface wdev 0x2 addr xx:xx:xx:xx:xx:xx type P2P-device txpower 31.00 dBm Interface wlan0 ifindex 2 wdev 0x1 addr xx:xx:xx:xx:xx:xx ssid <SSID 名> type managed channel 10 (2457 MHz), width: 20 MHz, center1: 2457 MHz txpower 31.00 dBm
udev でアクセスポイントの設定を永続化
Raspberry Pi を再起動すると ap0
は消えてしまうので、 udev
で起動時に ap0
を作成するようにします
まずはファイルを作ります
sudo vi /etc/udev/rules.d/99-ap0.rules
以下の内容を書き込みます
SUBSYSTEM=="ieee80211", ACTION=="add|change", ATTR{macaddress}=="xx:xx:xx:xx:xx:xx", KERNEL=="phy0", \ RUN+="/sbin/iw phy phy0 interface add ap0 type __ap", \ RUN+="/bin/ip link set ap0 address xx:xx:xx:xx:xx:xx"
xx:xx:xx:xx:xx:xx
は先ほどの MAC アドレスです
パッケージのインストール
アクセスポイントとして動作させるのに必要なパッケージ、 hostapd
と dnsmasq
をインストールします
sudo apt install -y hostapd dnsmasq
DHCP サーバの設定
IP アドレス中の XXX
は適宜決めてください
/etc/dnsmasq.conf
を編集します
sudo vi /etc/dnsmasq.conf
ファイルの最後に以下を追加します
interface=ap0 dhcp-range=192.168.XXX.100,192.168.XXX.150,255.255.255.0,12h
これで ap0
で DHCP サーバ機能が有効となります
この設定の場合、 ap0
に繋がった機器へ 192.168.XXX.100
から 192.168.XXX.150
の間のどれかの IP アドレスが割り当てられます
DHCP クライアントの設定
/etc/dhcpcd.conf
を編集して ap0
の IP アドレスを固定します
ルーター本体の IP アドレス(192.168.1.1
とか 192.168.2.1
とか)に相当するものですね
sudo vi /etc/dhcpcd.conf
ファイルの最後に以下を追加します
interface ap0 static ip_address=192.168.XXX.1/24 nohook wpa_supplicant
IP アドレス中の XXX
は DHCP サーバの設定 で設定したものに合わせてください
hostapd の設定
/etc/hostapd/hostapd.conf
を作成、編集します(このファイルは自動で作られないらしい)
sudo vi /etc/hostapd/hostapd.conf
ファイル内容は以下のようにします
アクセスポイントのパスワードは 8文字以上16文字以下 にしてください
ctrl_interface=/var/run/hostapd ctrl_interface_group=0 interface=ap0 driver=nl80211 ssid=<アクセスポイントの SSID 名> hw_mode=g country_code=JP channel=11 ieee80211d=1 wmm_enabled=0 macaddr_acl=0 auth_algs=1 wpa=2 wpa_passphrase=<アクセスポイントのパスワード> wpa_key_mgmt=WPA-PSK rsn_pairwise=CCMP
最後にサービスに登録して、 Raspberry Pi の起動時に hostapd
が実行されるようにします
sudo systemctl unmask hostapd.service sudo systemctl enable hostapd.service
各サービスの起動と確認
各サービスを再起動します
sudo systemctl restart dhcpcd.service sudo systemctl restart hostapd.service sudo systemctl restart dnsmasq.service
スマホなどでアクセスポイントを探すと、先ほど設定した SSID が表示されるはずです
先ほど設定したパスワードで接続できるはずです
なお、このアクセスポイントは独立しているため、インターネットや宅内ネットワークにアクセスすることはできません
今回はカメラ映像をストリーミングする用途なので、これで問題ありません
最後に再起動して、アクセスポイントが再び表示されるか確認しましょう
sudo reboot
Bluetooth コントローラーを接続する
ラジコンなのでコントローラーが必要です
今回は Xbox コントローラーを Bluetooth で接続して使います(他のコントローラーを使う場合は、適宜読み替えてください)
ヘッドレスなので、 bluetoothctl
を使ってコマンドラインから設定します
Raspberry Pi OS には最初から入っているので、特になにもしなくても使えるはずです
bluetoothctl
についてはこちらに詳しくまとめられています
bluetoothctl 起動
まずは bluetoothctl を起動して、 bluetoothctl の CLI に入ります
bluetoothctl
実行結果
Agent registered [CHG] Controller XX:XX:XX:XX:XX:XX Pairable: yes [bluetooth]#
Controller
は、 Bluetooth の送受信機みたいなものと考えてください(Raspberry Pi には Controller
が1つしか載ってない)
今後はこの Controller
に対して設定していきます
Controller の現在の設定を表示
show
実行結果
Controller XX:XX:XX:XX:XX:XX (public) Name: raspberrypi Alias: raspberrypi Class: 0x00000000 Powered: yes Discoverable: no DiscoverableTimeout: 0x000000b4 Pairable: yes UUID: Generic Attribute Profile (XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX) UUID: Generic Access Profile (XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX) UUID: PnP Information (XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX) UUID: A/V Remote Control Target (XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX) UUID: A/V Remote Control (XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX) UUID: Device Information (XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX) Modalias: usb:XXXXXXXXXXXXXXX Discovering: no Roles: central Roles: peripheral Advertising Features: ActiveInstances: 0x00 (0) SupportedInstances: 0x05 (5) SupportedIncludes: tx-power SupportedIncludes: appearance SupportedIncludes: local-name
ペアリング時の認証方法の変更
パソコンに Bluetooth 機器を接続する時、6桁の認証コードの入力が求められたりします
今回の場合、そういうのは面倒なので全部なしにします
agent off
実行結果
Agent unregistered
Bluetooth デバイスをスキャンする
Bluetooth デバイスをスキャンします
これを実行すると、ペアリング・接続可能な Bluetooth デバイスの名前と MAC アドレスが表示されます
実行する前に Xbox コントローラーの電源を入れておきましょう
scan on
実行結果
Discovery started [CHG] Controller XX:XX:XX:XX:XX:XX Discovering: yes [NEW] Device XX:XX:XX:XX:XX:XX <Bluetooth デバイス1> [NEW] Device XX:XX:XX:XX:XX:XX <Bluetooth デバイス2> [NEW] Device XX:XX:XX:XX:XX:XX <Bluetooth デバイス3> [NEW] Device XX:XX:XX:XX:XX:XX Xbox Wireless Controller ... ...
Xbox Wireless Controller
が見つかりましたね
Xbox コントローラーとペアリングする
まずは Controller をペアリング可能にします(show
コマンドで Pairable: yes
になってたら不要)
pairable on
実行結果
Changing pairable on succeeded [CHG] Controller XX:XX:XX:XX:XX:XX Pairable: yes
先ほど確認した Xbox コントローラーの MAC アドレスを指定してペアリングを実行します
pair <MAC アドレス>
実行結果
Attempting to pair with XX:XX:XX:XX:XX:XX [CHG] Device XX:XX:XX:XX:XX:XX Connected: yes [CHG] Device XX:XX:XX:XX:XX:XX Bonded: yes [CHG] Device XX:XX:XX:XX:XX:XX UUIDs: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX [CHG] Device XX:XX:XX:XX:XX:XX UUIDs: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX [CHG] Device XX:XX:XX:XX:XX:XX UUIDs: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX [CHG] Device XX:XX:XX:XX:XX:XX UUIDs: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX [CHG] Device XX:XX:XX:XX:XX:XX UUIDs: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX [CHG] Device XX:XX:XX:XX:XX:XX UUIDs: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX [CHG] Device XX:XX:XX:XX:XX:XX ServicesResolved: yes [CHG] Device XX:XX:XX:XX:XX:XX Paired: yes ... 中略 ... Pairing successful
ペアリングされているか確認
最後に、 Xbox コントローラーがちゃんとペアリングされているか確認します
bluetoothctl v5.55 (Legacy 標準)
paired-devices
bluetoothctl v5.66 (bookworm 標準)
devices Paired
実行結果
Device XX:XX:XX:XX:XX:XX Xbox Wireless Controller
Xbox Wireless Controller
が表示されれば OK です
bluetoothctl 終了
ペアリングが終わったので bluetoothctl を終了します
exit
カメラを接続する
公式の手順に従ってケーブルを繋ぎます
https://www.raspberrypi.com/documentation/accessories/camera.html#installing-a-raspberry-pi-camerawww.raspberrypi.com
動作確認
試しにカメラで画像を撮ります
まずコンフィグからカメラを使えるようにします
sudo raspi-config
3 Interface Options
を選択します
I1 Legacy Camera
を選択します
Yes
を選択します
「今後サポートされなくなるかもしれないよ」という警告が出てくるので、 OK
します
Finish
を選択して終了します
再起動を勧めてくるので Yes
で再起動します
これでカメラを使う準備ができたので、試しに撮ります
raspistill -o test.jpg
WSL2 で画像をコピーしてきて確認してみましょう
scp <USER>@<IP>:~/test.jpg /mnt/<コピー先のパス>
確認すると撮れてるはずです
カメラの映像をストリーミングする
ストリーミングする方法は様々あるようですが、軽量で遅延が少ないらしい mjpg-streamer を使います
こちらを参考にしました
必要なパッケージをインストール
mjpg-streamer は GitHub からソースコードを引っ張ってきてビルドする必要があるので、そのためのパッケージをインストールします
sudo apt install -y git cmake libjpeg-dev
mjpg-streamer をインストール
mjpg-streamer を clone し、ビルドしてインストールします
今回使用したソースコードはこちらです
git clone https://github.com/neuralassembly/mjpg-streamer.git cd mjpg-streamer/mjpg-streamer-experimental make sudo make install
mjpg-streamer の動作確認
mjpg-streamer/mjpg-streamer-experimental
に移動して以下のコマンドを実行します
mjpg_streamer -o './output_http.so -w ./www -p 8080' -i './input_raspicam.so -x 1280 -y 720 -fps 30 -q 10'
オプションの意味などは参考記事や公式などで確認してください
ブラウザで <ラズパイの IP>:8080
に接続すると、 mjpg-streamer のメインページが表示されます
左の Stream
を選択するとリアルタイムのストリーミング映像が見れます
ストリーミング映像だけを見たい場合は <ラズパイの IP>:8080/?action=stream
にアクセスします
終了は Ctrl + C
です
mjpg-streamer 起動用スクリプトの作成
いちいちオプションを指定して起動するのは面倒なので、シェルスクリプトを作って起動するようにします
cd /usr/local/bin vi start_mjpg_streamer.sh
中身
#!/bin/bash export LD_LIBRARY_PATH=/usr/local/lib # mjpg-streamer config WIDTH=1280 HEIGHT=720 FPS=30 QUALITY=10 PORT=8080 # Kill process that is already running pkill mjpg_streamer # Run mjpg-streamer /usr/local/bin/mjpg_streamer \ -i "input_raspicam.so -x $WIDTH -y $HEIGHT -fps $FPS -quality $QUALITY" \ -o "output_http.so -w /usr/local/share/mjpg-streamer/www -p $PORT" exit 0
実行権限とかを変更しておきます
chmod 755 start_mjpg_streamer.sh
スクリプトを実行して、 mjpg-streamer が問題なく起動し、ストリーミングされていることを確認しましょう
sh start_mjpg_streamer.sh
ここまで出来たら、ソースコードはディレクトリごと削除しても問題ありません
mjpg-streamer 起動用スクリプトをサービス化する
Raspberry Pi が起動したときに mjpg-streamer が起動するように、サービス登録します
sudo vi /etc/systemd/system/mjpg_streamer.service
中身
[Unit] Description=MJPG-Streamer service After=network.target [Service] Type=simple KillMode=process Restart=always WorkingDirectory=/usr/local/bin ExecStart=/usr/local/bin/start_mjpg_streamer.sh [Install] WantedBy=multi-user.target
After=network.target
を指定することで、ネットワーク周りの起動が終わってからサービスを起動するようにしています
systemctl
に登録して起動します
sudo systemctl enable mjpg_streamer sudo systemctl start mjpg_streamer
ストリーミング映像が見られれば OK です
ストリーミング表示用ページを作る
直接 <ラズパイの IP>:8080/?action=stream
にアクセスするだけでも十分ですが、映像を画面全体に表示したいので、専用のページを作ります
あと、こういうページを作っておけば、ブラウザからラジコンの設定弄ったりするページを追加したりしやすい気がする(現状のソースコードではそういうのができるようになってないので、やりたい人は頑張ってください)
面倒ならやらなくてもいいです
サクッと flask
で構築します
flask をインストール
pip install flask
pip
がインストールされてなければ先にインストールしてください
sudo apt install -y python3-pip
コードを書く
最終的なファイル構造はこうなります
camera_streaming ├── streaming.py └── templates └── index.html
streaming.py (メインとなるプログラム)を作成
ディレクトリを作ってファイルを作ります
sudo mkdir /usr/local/bin/camera_streaming sudo cd /usr/local/bin/camera_streaming sudo vi streaming.py
中身
from flask import Flask, render_template app = Flask(__name__) @app.route("/") def index(): return render_template("index.html") if __name__ == "__main__": app.run(host="0.0.0.0", port=8000, debug=False, threaded=True)
index.html (ストリーミング映像を表示するページ)を作成
templates/index.html
を作ります
sudo mkdir templates sudo cd templates sudo vi index.html
中身
<!DOCTYPE html> <html> <head> <title>RasPi Zero Camera Stream</title> </head> <body style="padding:0; margin:0; background-color:#333;"> <div style="height: 100vh; display: flex; justify-content: center;"> <img style="height:100%; margin:0;" src="http://<ap0 の ip_adress>:8080/?action=stream" > </div> </body> </html>
ap0 の ip_adress
を指定しているので、 ap0
に繋いだ状態でないとストリーミング映像は表示されません(ページ自体は表示される)
こんな風にページに直接埋め込んで表示できるのがいいですね
動作確認
streaming.py
を実行します
python3 /usr/local/bin/camera_streaming/streaming.py
ap0
に繋いだスマホなどのブラウザから DHCP クライアントの設定 で設定した IP アドレスにアクセスします
<ap0 の ip_address>:8000
カメラのストリーミング映像が見られたら成功です
確認が終わったら Ctrl + C
で終了しましょう
サービス化する
とりあえずカメラのストリーミングはラジコン操作と独立で動いてて構わないので、ストリーミング機能だけでサービス化します
サービスファイルを作ります
sudo vi /etc/systemd/system/camera_streaming.service
中身
[Unit] Description=Camera streaming service After=network.target [Service] User=<Raspberry Pi のユーザー名> Group=<Raspberry Pi のユーザー名> Type=simple Restart=always WorkingDirectory=/usr/local/bin/camera_streaming ExecStart=/usr/bin/python3 streaming.py [Install] WantedBy=multi-user.target
After=network.target
を指定することで、ネットワーク周りの起動が終わってからサービスを起動するようにしています
systemctl
に登録して起動します
sudo systemctl enable camera_streaming sudo systemctl start camera_streaming
同じようにブラウザからアクセスして問題なければ OK です
ラジコンのプログラムを作る
本丸のラジコンのプログラムです
とはいっても、あんまり詳しく説明はしないので、ソースコードを読んで感じてください
あと、これが python 初挑戦みたいなもんなので、微妙な部分がいっぱいあると思います
パッケージのインストール
pygame のインストール
コントローラーの入力を取得するために pygame
をインストールします
python3 -m pip install -U pygame --user
pygame
には libsdl2-2.0-0
というパッケージも必要なのでインストールします
sudo apt install -y libsdl2-2.0-0
pigpio のインストール
GPIO を制御するための pigpio
をインストールします
sudo apt install -y python3-pigpio
pigpio
はデーモンを起動しないと使えないので起動します
sudo pigpiod
また、 Raspberry Pi が起動したときに自動的に起動するように /etc/rc.local
を編集します
sudo vi /etc/rc.local
最後の exit 0
の前に pigpiod
を追加します
pigpiod # ここに追加 exit 0
コントローラーの入力を取得する
やってることは公式のやつほとんどそのまんまです
# 一部抜粋 def _run(self) -> None: current_input = ControllerInput() while True: self._running_event.wait() event = pygame.event.wait() if event.type == pygame.QUIT: break if not self._controller_connected: continue if ( event.type == pygame.JOYDEVICEREMOVED and event.instance_id == self._controller.get_instance_id() ): self._controller_connected = False self.data.clear() self._controller_data.event = ControllerEventType.Disconnected self._controller_data.controller_input.reset() current_input.reset() self.data.put(self._controller_data) self._logger.warn("Controller disconnected.") self._running_event.clear() self._reconnect_controller() continue current_input.reset() for i in range(self._controller.get_numaxes()): axis = self._controller.get_axis(i) current_input.axes.append(self._axis_converter.convert(i, axis)) for i in range(self._controller.get_numbuttons()): button = self._controller.get_button(i) current_input.buttons.append(button) for i in range(self._controller.get_numhats()): hat = self._controller.get_hat(i) current_input.hats.append(hat) if self._controller_data.controller_input == current_input: continue self._controller_data.event = ControllerEventType.InputChanged self._controller_data.controller_input.copy(current_input) self._logger.debug("Controller input:%s", self._get_controller_input()) self.data.put(self._controller_data)
_run()
自体は、別スレッドで無限ループしてます
工夫したところと言えば
- コントローラーと接続が切れたら、ループを一時停止して再接続ルーチンを起動する
_axis_converter
でアナログスティックの入力を丸める- 入力が変わった時だけ通知する
とかですかね
_axis_converter
での丸め方は analogaxis.py
とかで定義してます
最終的には Speed
の5段階にしてます
class Speed(IntEnum): Zero = 0 Slow = 1 Middle = 2 High = 3 Max = 4
そこら辺を変えたいなら、 analogaxis.py
とか、関係するところを編集すれば OK です
詳しくはソースを見てください
モーターの制御
motordriver.py
でそこら辺を書いてます
サーボモーターの制御
「右スティックを倒している間だけその方向に動かしたい」ので、サーボへの信号出力も別スレッドでループさせています(_run()
が別スレッドで動いてる)
# 一部抜粋 def _run(self) -> None: while True: self._running_event.wait() if self._exit: break if self._clockwise: self._current_duty += self._step if self._current_duty > self._max_duty: self._current_duty = self._max_duty self.stop() continue else: self._current_duty -= self._step if self._current_duty < self._min_duty: self._current_duty = self._min_duty self.stop() continue self._servo.hardware_PWM(self._pin, self._freq, self._current_duty) time.sleep(self._interval)
「ボタンを押している間だけ」とかもできます
モーターの制御
こっちは特に別スレッドで無限ループとかはしてません(不要なので)
# 一部抜粋 def move(self, dutycycle: float, forward: bool = True) -> None: """ Move the motor. Args: dutycycle (float): Parcentage of dutycycle (from 0 to 1). forward (bool): True for forward movement, False for backward. """ dutycycle_ = int(self._max_dutycycle * dutycycle) if dutycycle_ > self._max_dutycycle: dutycycle_ = self._max_dutycycle self._logger.debug( "Move motor to %s in %d", "foward" if forward else "backward", dutycycle_, ) if forward: self._motor.set_PWM_dutycycle(self._backward_pin, 0) self._motor.set_PWM_dutycycle(self._forward_pin, dutycycle_) else: self._motor.set_PWM_dutycycle(self._forward_pin, 0) self._motor.set_PWM_dutycycle(self._backward_pin, dutycycle_)
メインループ
これらのパーツを組み合わせてゴリっと作ったのがこちら
全体は radiocontrol.py
を見てください
# 一部抜粋 def main(): logger.info("Start initializing.") controller = Controller(controller_type=ControllerType.Xbox, name=CONTROLLER_NAME) led = LED(LED_PIN, LED_NAME) pan_servo = ThreadedServo( PAN_SERVO_PIN, SG90_MIN_PULSE_WIDTH, SG90_MAX_PULSE_WIDTH, SG90_FRAME_WIDTH, SERVO_INITIAL_PULSE, name=PAN_SERVO_NAME, ) tilt_servo = ThreadedServo( TILT_SERVO_PIN, SG90_MIN_PULSE_WIDTH, SG90_MAX_PULSE_WIDTH, SG90_FRAME_WIDTH, SERVO_INITIAL_PULSE, name=TILT_SERVO_NAME, ) left_motor = Motor(LEFT_FORWARD_PIN, LEFT_BACKWARD_PIN, name=LEFT_MOTOR_NAME) right_motor = Motor(RIGHT_FORWARD_PIN, RIGHT_BACKWARD_PIN, name=RIGHT_MOTOR_NAME) pi = pigpiohelper.get_instance() pi.set_mode(MOTOR_DRIVER_STATE_PIN, pigpio.INPUT) pi.set_pull_up_down(MOTOR_DRIVER_STATE_PIN, pigpio.PUD_DOWN) logger.info("End initializing.") try: controller.connect() # Wait for the controller to be connected before calling controller.run(). while True: state = controller.data.get() if state.event == ControllerEventType.Connected: break if state.event == ControllerEventType.Quit: raise Exception("Faild to connect controller.") controller.run() logger.info("Start main loop of radio control.") while True: state = controller.data.get() if state.event == ControllerEventType.Quit: break if state.event == ControllerEventType.Disconnected: pan_servo.stop() tilt_servo.stop() left_motor.stop() right_motor.stop() if state.event != ControllerEventType.InputChanged: continue input = state.controller_input # LED lighting if XboxControllerInputHelper.HatX(input) > 0: # LED on if Hat Right is pushed. led.on() elif XboxControllerInputHelper.HatX(input) < 0: # LED off if Hat Left is pushed. led.off() if XboxControllerInputHelper.HatY(input) > 0: # Increment brightness of LED if Hat Up is pushed. led.increment_brightness() elif XboxControllerInputHelper.HatY(input) < 0: # Decrement brightness of LED if Had Down is pushed. led.decrement_brightness() # Control camera servo if XboxControllerInputHelper.StickRightX(input).value == 0: # Stop servo if Right Stick's X axis is neutral position. pan_servo.stop() else: # Run the servo clockwise or counterclockwise based on the tilt direction of the Left Stick's X axis. speed = Speed(XboxControllerInputHelper.StickRightX(input).value) clockwise = XboxControllerInputHelper.StickRightX(input).positive run_servo(pan_servo, speed, not clockwise) if XboxControllerInputHelper.StickRightY(input).value == 0: # Stop servo if Right Stick's Y axis is neutral position. tilt_servo.stop() else: # Run the servo clockwise or counterclockwise based on the tilt direction of the Left Stick's Y axis. speed = Speed(XboxControllerInputHelper.StickRightY(input).value) clockwise = XboxControllerInputHelper.StickRightY(input).positive run_servo(tilt_servo, speed, clockwise) if XboxControllerInputHelper.ButtonStickRight(input): # Move to initial position if Right Stick is pushed. pan_servo.initial() tilt_servo.initial() # Check motor driver chip's state if pi.read(MOTOR_DRIVER_STATE_PIN): logger.warn("Motor driver chip down. Waiting restart.") continue # Spin turn if ( XboxControllerInputHelper.TriggerLeft(input).value != 0 and XboxControllerInputHelper.TriggerRight(input).value == 0 ): # Spin turn to left if only Trigger Left is pushed. speed = Speed(XboxControllerInputHelper.TriggerLeft(input).value) run_motor(left_motor, speed, False) run_motor(right_motor, speed) continue elif ( XboxControllerInputHelper.TriggerLeft(input).value == 0 and XboxControllerInputHelper.TriggerRight(input).value != 0 ): # Spin turn to right if only Trigger Right is pushed. speed = Speed(XboxControllerInputHelper.TriggerRight(input).value) run_motor(left_motor, speed) run_motor(right_motor, speed, False) continue # Running motor if ( XboxControllerInputHelper.StickLeftY(input).value == 0 and XboxControllerInputHelper.StickLeftX(input).value == 0 ): # Stop motor if Left Stick is neutral position. left_motor.stop() right_motor.stop() elif ( XboxControllerInputHelper.StickLeftY(input).value != 0 and XboxControllerInputHelper.StickLeftX(input).value == 0 ): # Run forward or backward if Left Stick is tilted only Y axis. speed = Speed(XboxControllerInputHelper.StickLeftY(input).value) forward = not XboxControllerInputHelper.StickLeftY(input).positive run_motor(left_motor, speed, forward) run_motor(right_motor, speed, forward) elif ( XboxControllerInputHelper.StickLeftY(input).value == 0 and XboxControllerInputHelper.StickLeftX(input).value != 0 ): # Turn left or right if Left Stick is tilted only X axis. speed = Speed(XboxControllerInputHelper.StickLeftX(input).value) if XboxControllerInputHelper.StickLeftX(input).positive: right_motor.stop() run_motor(left_motor, speed) else: left_motor.stop() run_motor(right_motor, speed) else: # Turn left or right # # ex) Turn left -> # RUNNING SPEED is based on Left Stick's Y axis value. # Right motor runs at RUNNING SPEED. # Left motor's speed is (RUNNING SPEED - Left Stick's X axis value * 0.15). # So, left motor runs 15-60% slower than RUNNING SPEED (slowest is 0). speed = Speed(XboxControllerInputHelper.StickLeftY(input).value) dutycycle = get_dutycycle(speed) - ( 0.15 * XboxControllerInputHelper.StickLeftX(input).value ) forward = not XboxControllerInputHelper.StickLeftY(input).positive if dutycycle < 0: dutycycle = 0 if XboxControllerInputHelper.StickLeftX(input).positive: run_motor(left_motor, speed, forward) run_motor_raw(right_motor, dutycycle, forward) else: run_motor(right_motor, speed, forward) run_motor_raw(left_motor, dutycycle, forward) if ( XboxControllerInputHelper.ButtonBack(input) and XboxControllerInputHelper.ButtonStart(input) ): # Quit when Back button and Start button pushed. logger.info("End main loop of radio control.") break except Exception as e: logger.exception(e) finally: controller.quit() pan_servo.quit() tilt_servo.quit() left_motor.quit() right_motor.quit() led.quit() logger.info("End program.")
全体的に、本当はもう少し Factory パターンとか使っていい感じにしたかったんですが、 python の型ヒントが思ったようにいかなかったので諦めました
Controller().data.get()
で返ってくるやつを XboxControllerData
(ControllerData
から継承)とかにしたかったけど、型ヒントが想定した感じにならなくてインテリセンスの恩恵が受けられないので、わざわざ XboxControllerInputHelper
とかいう静的メソッドだけのクラス作って取得したりとか微妙すぎる
XboxControllerInputHelper
はインテリセンスでいい感じに補完してもらうために作った、マジで意味のないクラス
python つよつよの人、いい解決法あったら教えてくれ~~
サービス化する
ソースコードを /usr/local/bin/radio_control/
に配置して、サービス登録してしまいます
sudo vi /etc/systemd/system/radio_control.service
中身
[Unit] Description=Radio Control service After=network.target [Service] User=<Raspberry Pi のユーザー名> Group=<Raspberry Pi のユーザー名> Type=simple WorkingDirectory=/usr/local/bin/radio_control ExecStart=/usr/bin/python3 radiocontrol.py [Install] WantedBy=multi-user.target
systemctl
に登録して起動します
sudo systemctl enable radio_control sudo systemctl start radio_control
基板を作る
ゴリっと基板を作ります
まあ、ユニバーサル基板とかに作ればいいんですけど、やっぱちゃんと PCB にした方がカッコいいんで
出来上がったのがこちら
左側が、カメラマウントおよび照明基板、右側が、電源・モータードライバ基板です
電源スイッチも付ければよかったんですけど忘れちゃいました
組み立てる
タミヤのユニバーサルプレートを頑張って加工して、カメラとか Raspberry Pi が固定できるようにします
基板にも必要な部品(導線とかピンヘッダとか)を半田付けします
そして、出来上がったのがこちら(再掲)
走破性はいいけど、常に接地してるシーソー型の2つの転輪部分を軸に、車体が激しく前傾後傾するので、カメラを通して操作するのがちょっと難しい
実際、遊んでるとめちゃくちゃ楽しいです
普通(?)のバージョンも作りました
重心が後ろに寄ってるので登坂能力がちょっと弱いけど、車体は安定しているのでカメラを通しての操作はしやすい
おわりに
めちゃくちゃ長くなりました
でも、これを見ればみんな Raspberry Pi Zero W を使ってラジコンが作れるようになるんじゃないかと思います
作った基板はあと4枚あるんですが、どうしましょうかね……
こいつで遊びながら考えます