koktoh の雑記帳

気ままに書いていきます

RaspberryPi Zero W でラジコン作った

はじめに

ずっと我が家で埃をかぶっていた Raspberry Pi Zero W で、突然ラジコン作りたくなったのでつくりました

これはその備忘録的なやつです

完成品

どんなことができるの?

  • Xbox コントローラーで操作するラジコン
  • カメラの映像をリアルタイムでストリーミングして見れる(レイテンシはある)
  • ライトが点いて暗いところでも走れる(気がする)

使ったもの

名前 説明 参考リンクなど
ツイストクローラー工作セット(2chリモコン) ベースとなる筐体 (本体が激しく上下に振れるのでカメラ搭載にはあんまり向かなかった) タミヤ公式
連結式クローラー&スプロケットセット 樹脂製のクローラーから置き換え(使わなくてもいい) タミヤ公式
ユニバーサルプレートセット Raspberry Pi やカメラを固定する タミヤ公式
SG90 用カメラパンチルトキット うちに転がってたやつ アマゾン検索結果
Raspberry Pi Zero W うちに転がってたやつ(作ってるうちにカメラ用 FFC コネクタ壊しちゃったので引退) スイッチサイエンス
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 電源を取り出すのと、モーターやサーボを駆動するための基板 自作
その他 導線とかネジとか必要なものを適宜

プログラム

作ったプログラムはここにあります

github.com

環境構築

WSL2 から作業したいので、 WSL2 と、ついでに Windows Terminal をインストールします
Windows Terminal は最高なんで、みんな入れましょう

learn.microsoft.com

管理者権限でコマンドプロンプトか Power Shell を起動して、以下を実行

wsl --install

Microsoft Store で UbuntuWindows Terminal をインストール

apps.microsoft.com

apps.microsoft.com

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 PiSSH 接続して作業していくので、そのための設定をしていきます
Raspberry Pi に周辺機器を一切接続せず、完全にリモートでやっていきます

やってるうちにエラーが出てきても、エラー文でググればどうすればいいのか説明してくれてる人がいるので、頑張ってください

Raspberry Pi の IP を調べる

Raspberry PiSSH するには 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 アドレスを入れておけばいいと思います

routersdomain_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 をアクセスポイントにします
アクセスポイントとして設定するのに hostapddnsmasq というパッケージを使います

以下の記事を参考にしました

www.mikan-tech.net

ちなみに、 Raspberry Pi OS (bookworm) で NetworkManager を使った場合の方法もまとめてます

koktoh.hatenablog.com

現在のネットワークインターフェースと 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

wlan0addr の値(MAC アドレス)は今後も使うので、どこかにメモしておいてください

仮想ネットワークインターフェースを作る

まずは ap0 という名前でネットワークインターフェースを作ります

sudo iw phy phy0 interface add ap0 type __ap

次に、 ap0MAC アドレスを設定します
さっき確認した 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 アドレスです

パッケージのインストール

アクセスポイントとして動作させるのに必要なパッケージ、 hostapddnsmasq をインストールします

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

これで ap0DHCP サーバ機能が有効となります
この設定の場合、 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 アドレス中の XXXDHCP サーバの設定 で設定したものに合わせてください

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 についてはこちらに詳しくまとめられています

qiita.com

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 を使います

こちらを参考にしました

ponkichi.blog

必要なパッケージをインストール

mjpg-streamer は GitHub からソースコードを引っ張ってきてビルドする必要があるので、そのためのパッケージをインストールします

sudo apt install -y git cmake libjpeg-dev

mjpg-streamer をインストール

mjpg-streamer を clone し、ビルドしてインストールします

今回使用したソースコードはこちらです

github.com

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

コントローラーの入力を取得する

やってることは公式のやつほとんどそのまんまです

www.pygame.org

    # 一部抜粋
    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() で返ってくるやつを XboxControllerDataControllerData から継承)とかにしたかったけど、型ヒントが想定した感じにならなくてインテリセンスの恩恵が受けられないので、わざわざ 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枚あるんですが、どうしましょうかね……

こいつで遊びながら考えます

NetworkManager で Wi-Fi クライアントを維持しつつアクセスポイントにする(RPi OS (bookworm))

はじめに

Raspberry Pi OS (bookworm) から、ネットワーク周りが NetworkManager で管理されるようになったみたいです

Raspberry Pi 4 とか、有線と無線両方載ってるボードなら、有線で宅内ネットワークからのアクセス(ssh とか)を確保できるので、無線をアクセスポイント利用で潰してもそんなに不便はないです

しかし、 Raspberry Pi Zero W/2 W は有線の口がないので、そうしてしまうと不便です(USB On-The-Go とか使えばいいけど)

ここでは、 Raspberry Pi Zero には電源供給の USB しか繋がない縛りプレイでいきます

なお、 Raspberry Pi の IP 固定とか ssh 接続の設定とかは終わっている前提でいきます
そこらへんが終わってない人は、ググったりして終わらせておいてください

Wi-Fi クライアントを維持してアクセスポイント化するには

方針としてはこうです

  • アクセスポイント用の仮想ネットワークインターフェースを作る(ap0
  • NetworkManager で ap0 を利用したアクセスポイント用の設定を作る

なんでそんなことするの?

Raspberry Pi Zero にはデフォルトで wlan0 というネットワークインターフェースがあります

これをそのままアクセスポイントに流用すると、 Wi-Fi クライアント機能はなくなり、アクセスポイントとしての動きしかしなくなります
Raspberry Pi Zero が家の中に飛んでる Wi-Fi とかを掴まなくなるわけですね

これは、 Raspberry Pi Zero に繋がった機器だけの閉じたネットワークが構成され、外に出ることも、外からアクセスすることもできなくなるということです
外(宅内ネットワークとか)から Raspberry Pi Zero に ssh したり、 Raspberry Pi Zero やそこに繋がってる機器からインターネットに出ていくことができません
当然 sudo apt update とかできません(めっちゃ困る)

なので、 wlan0 にはこれまで通り、家の中を飛んでる Wi-Fi を掴んでもらえるようにしておいて、新たに ap0 を作ってアクセスポイントになってもらうわけです

アクセスポイント用のネットワークインターフェースを作る

第一段階としてネットワークインターフェースを作ります

現在のネットワークインターフェースと 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

wlan0addr の値(MAC アドレス)は今後も使うので、どこかにメモしておいてください

仮想ネットワークインターフェースを作る

まずは ap0 という名前でネットワークインターフェースを作ります

sudo iw phy phy0 interface add ap0 type __ap

次に、 ap0MAC アドレスを設定します
さっき確認した 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"

再起動して ap0 がちゃんと作られているか確認しましょう

NetworkManager でアクセスポイントの設定をする

NetworkManager には nmclinmtui という2つのユーザーインターフェースがあります

nmcliコマンドラインに直接打ち込むコマンド、 nmtuiraspi-config のようなテキストユーザーインターフェースです

どちらでも好きな方を使ってください

nmcli で設定する

この一行で終わりです(参考にしたサイトほぼそのまま)

rpi_ap という名前の connection を作成し、もろもろの設定を一気に適用します

sudo nmcli connection add type wifi ifname ap0 con-name rpi_ap autoconnect yes ssid SSID_AS_YOU_LIKE 802-11-wireless.mode ap 802-11-wireless.band bg ipv4.method shared ipv4.address 192.168.XXX.1/24 ipv4.never-default yes wifi-sec.key-mgmt wpa-psk wifi-sec.pairwise ccmp wifi-sec.proto rsn wifi-sec.psk "password_as_you_like"

SSID_AS_YOU_LIKEpassword_as_you_like は好きに決めてください
192.168.XXX.1/24XXX1 ~ 255 の間の好きな数字にしてください(すでにあるものと被らないように)

ポイントはここです

ipv4.never-default yes

これを設定しないと、今回作った rpi_ap がルートになって Raspberry PiWi-Fi を掴まなくなることがあります(この説明が正しいかは疑問だけど 、ネットワークや NetworkManager を完全に理解してないので、「たぶんそんな感じなんだろうな」くらいの感覚)
起動のたびに rpi_ap がルートになったりならなかったりするので、「再起動すると ssh できたりできなかったりする」みたいな現象が起きます

他のオプションや、 nmcli についての詳細は公式とかを見てください(「こうしとけばいいのね」くらいの理解度なので解説とかムリ)

最後に、 NetworkManager の設定を再度読み込んで、 rpi_ap を起動します(やらなくても作成時点で起動してるかも)

sudo nmcli connection reload
sudo nmcli connection up rpi_ap

nmtui で設定する

まずは nmtui を起動します

sudo nmtui

Edit a connection を選択して Enter

<Add> を選択して Enter

Wi-Fi を選択して Enter

設定は以下のようにします


IPv4 CONFIGURATION の内容は最初は隠れているので、 <Show> で表示します

赤くなっているの項目名は、正しくない(設定が必要なのに空欄、使えない文字が使われている、など)ので、適宜修正してください

SSID_AS_YOU_LIKEpassword_as_you_like は好きに決めてください
Deviceap0 を作るときに指定した MAC アドレスを大文字で入力してください(XX:XX:XX:XX:XX:XX の部分)
Addresses はアクセスポイントのアドレスです(ルーター192.168.1.1 的なやつ)
192.168.000.1/240001 ~ 255 の間の好きな数字にしてください(すでにあるものと被らないように)

nmcli の項で説明したように、 Never use this network for default route にチェックを入れることを忘れないようにしましょう

<OK> で作成を終了すると、設定した connection ができています

これで設定は終了したので、 <Back><Quit> を選択して nmtui を出ましょう

確認

nmcli でも connection を確認してみましょう

nmcli connection show

実行結果

NAME           UUID                                  TYPE      DEVICE
rpi_ap         XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX  wifi      ap0
preconfigured  XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX  wifi      wlan0
lo             XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX  loopback  lo

rpi_ap が追加されているはずです
緑色(ターミナルの設定によるかも)になっていれば起動しています

この時点で、スマホやノート PC から、設定したアクセスポイントに繋ぐことができます
しかも、このアクセスポイントを介してインターネットに出ていくこともできます

一応、再起動しても問題なく ssh やアクセスポイントへの接続ができるか確認しましょう

sudo reboot

参考

以下の記事を参考にしました

www.mikan-tech.net

raspida.com

おわりに

以上、 Raspberry Pi OS (bookworm) で(NetworkManager で) Wi-Fi クライアントを維持しつつアクセスポイント化する方法でした

Legacy (bullseye) 以前では hostapddnsmasq をインストールして色々設定する必要がありましたが、 NetworkManager を設定するだけで済むのはとても楽になった気がします

現時点では、 Raspberry Pi Imager で Paspberry Pi Zero W/2 W を選択すると Legacy (bullseye) しか選択できないようになっているので、わざわざ bookworm をインストールすることはあまりないとは思いますが、将来 bookworm が Raspberry Pi Zero W/2 W のデフォルト OS になったときには役に立つと思います

というか、そうなったときの自分のために書きました

「自作キーボード初心者」考

はじめに

「自作キーボード初心者」とはどのような人々が当たるのか、個人的に思うことをつらつらと書いていきます。

なぜ自作キーボードを選ぶのか

自作キーボードとは、「自分の用途に最適化したキーボードを得ること」を目的とした手段です。あくまでも手段の一つです。
目的を達成できるのであれば、どこかしらの企業が作ったキーボードを使えばよく、わざわざ「同人ハード」である自作キーボードに手を出す必要はありません。

もちろん、「かっこいいから使いたい」という人もたくさんいます。
なかには「自作キーボードを作ること」が目的の人もたくさんいます。
しかし、初めて自作キーボードの世界の門を叩く人のほとんどは、上記の目的を達成するための手段として自作キーボードを選択したのではないでしょうか。

自作キーボード初心者とは

さて、「自作キーボード初心者」とはどんな人が当てはまるのでしょうか。
議論はあるでしょうが、上記の目的を考えると、「買ってきた自作キーボードキットを使用可能な状態まで持っていける人」といったところでしょうか。

ここで「ん?」と思った人がいるでしょう。

「買ってきた自作キーボードキットを使用可能な状態まで持っていける人」?

そうです。自作キーボードとは、そこら辺の家電量販店で売っているような、「箱から出して PC に繋げば即使える 商品」ではないのです。
使うためにいろいろなことを自分でやって、問題が出てくれば解決していく必要がある、「同人ハード」なのです。

高かろうが、「個人が趣味で作った同人ハード」なのです。趣味で作ったものをみんなに共有しているだけに過ぎないのです。
「企業が商売として販売している商品」とは全く異なるのです。

ちなみに、自作キーボードの話題の中で、海外の企業が製造販売しているキーボードについて出てくることがありますが、それらは「カスタムキーボード」と呼ばれ区別されたりします。
おおむね「箱から出して PC に繋げば即使える 商品」に近いですが、使用する上では「同人ハード」の自作キーボードとほとんど変わらないものが多いかと思います。

とにかく、買ってきた自作キーボードキットを問題なく動かすところまで持っていけるなら、それは十分に初心者のレベルに達していると言えるでしょう。

初心者になるために

自作キーボードで要求される知識

まずは以下の文を読んでみましょう。

開発環境を構築し、 GitHub からソースコードを clone あるいは pull し、コマンドを打ち込んでコンパイル、書き込みを行う。

これは、自作キーボードを作る、使用する、カスタマイズする上で、ほぼ必ず行われることです。
この文章で言及されていることが理解できるでしょうか? 何をしているのか想像できるでしょうか?

だいたいのソフトウェアエンジニアであれば引っかかることなく理解できるでしょう。

それは、この文章に書かれていることが、ソフトウェアエンジニアにとって「義務教育で習うレベルのこと」、言わば「常識」となっているからです。
そして、自作キーボードにおいても概ね同様に「常識レベル」の知識なのです。

つまり、ソフトウェアエンジニアの「常識」程度の知識は持っておきたいということです。

場合によっては、電子回路の知識も要求されます。
英文を読む能力も必要になるでしょう。(優秀な翻訳ソフトが増えているので、あまり気にする必要はないかもしれませんが)

現在の自作キーボードの難易度

昔に比べれば低くなったのは事実です。

日本語の解説記事も増え、日本人によるコミュニティで日本語で助けを得ることもできるようになりました。
設定が簡単にできるようなウェブアプリが公開されたりもしています。

しかし、これらは言わば「大学卒業程度の内容を高校卒業程度の人にも理解できるようにした」くらいの難易度の低下だと思っています。

なにも知らない人が飛び込んでも、「義務教育も終えていない幼稚園児や小学生が、高校や大学の授業を聞いている」ような状態です。

※ わかりやすいように学校教育に例えているのであって、「初心者は義務教育も終えていないバカ」というような蔑む意図はありません

多少でもソフトウェア開発を経験したことがない人にとっては、依然としてハードルが高いと言わざるを得ません。

どうすればいいのか

勉強するしかありません。

学校に行け、とか、机に教科書とノートを開いてやれ、とかではありません。

上でも言ったように、ネットにはいろんな情報が無限に転がっています。
それらを読んで、必要となる知識を身に付けるしかありません。頑張って「自作キーボードの義務教育」を修了するしかありません。

コミュニティに入って教えてもらうのもいいでしょう。
「自作キーボードの義務教育」が修了できるように手伝ってくれるはずです。

ただし、これらはすべて「善意」で成り立っていることを忘れてはいけません。
人として礼儀をわきまえた振る舞いをしなければ見捨てられます。あまりに酷ければ干されるかもしれません。
なぜなら、本来、記事を書いた人や、コミュニティの人々にあなたを助ける義務なんてないのですから。

初心者を超えて

ここまで読んで、げんなりした人もいるかもしれません。
「自作キーボードを使うのをやめとこ」と思った人もいるかもしれません。

しかし、これを「初心者」と分類できる程度には、自作キーボードの世界は、深く広いのです。
できること、それに必要な知識の量が多いのです。

初心者レベルに達したら、自作キーボードの世界を自由に泳ぎ回ることができるでしょう。

今のキーボードをもっともっと使いやすくカスタマイズするのもいい。
気に入ったキーボードを買ってコレクションするのもいい。
スイッチにこだわるのもいい。
更に勉強して、キットを買うのではなく、自分で設計してもいい。
自分で設計したキーボードを頒布してもいい。

楽しみ方は自由です。

おわりに

先日、とあるブログが炎上しました。
個人的には、「けなすことを目的として、都合の良い脚色(あるいは改変)を行い、それに基づいた個人やコミュニティへの侮辱」といった、「悪意の塊のような内容」という感想を持ちました。
とても礼儀を欠いた行動で、品性を疑うというのが正直なところです。

今回の記事を書こうと思った理由でもあります。

最近は「初心者歓迎」の流れがより大きくなり、意識としてはとても素晴らしいことです。
しかし、現在の環境は「初心者になることすら高いハードルが存在する」と言わざるを得ないと思います。(ハードルを下げる試みが日々行われているが、それでもなおハードルが高い)

この記事を読んで、自作キーボードに触れることをためらった人もいるかもしれません。
「初心者歓迎」に逆行しているかもしれません。

しかし、何をするにも勉強は必要です。礼儀は必要です。

自作キーボードは、深みに嵌れば嵌るほど、たくさんの知識が必要とされます。
それを楽しみましょう。泥臭く勉強しましょう。
得た知識を最大限利用して、「俺だけの最強のキーボード」を作りましょう。
それを笑う人はどこにもいないはずです。

自作キーボード用の KiCad フットプリントライブラリを作った

はじめに

KiCad 用フットプリントライブラリをつくりました。

その紹介と説明をします。

画像がいっぱいなので重いかもしれないけど、耐え忍んでください。

公開場所

github.com

使い方

前提

  • git が使えること

わからない場合は、頑張ってググってください。

導入手順

リポジトリをクローン

以下のコマンドを実行。

git clone git@github.com:koktoh/BrownSugar_KBD_KiCad_Library.git

git submodule を初期化

以下のコマンドを実行。

git submodule init
git submodule update

KiCad にパスを登録

KiCad の 3D ビューで 3D モデルを表示するためにパスを登録します。

名前 パス
BROWNSUGAR_KBD_3DMOD [cloned directory]\BrownSugar_KBD.3D (例 C:\foo\bar\BrownSugar_KBD.3D)


ライブラリの説明

KiCad 5.x で作成しました。たぶん KiCad 6 以降でも使えるはずです。

MCU

シンボル

ProMicro, BLE Micro Pro, RP2040 のシンボルを用意しています。

なお、 RP2040 にはデフォルトフットプリントとして Package_DFN_QFN:QFN-56-1EP_7x7mm_P0.4mm_EP5.6x5.6mm_ThermalVias を登録しています。

フットプリント

ProMicro

正位置、シルク無し、裏返しを用意しています。
裏返しはピン番号も反転しているので、同じシンボルに紐づけても問題ありません。

スルーホールの穴径は、コンスルー推奨の Φ0.85mm にしています。

BLE Micro Pro

正位置、裏返しを用意しています。
ProMicro 同様、裏返しはピン番号も反転しているので、同じシンボルに紐づけても問題ありません。

スルーホールの穴径は、コンスルー推奨の Φ0.85mm にしています。

ピンのシルクはどうしようか迷いましたが、 ProMicro と共通とすることにしました。

3D モデル

ProMicro

おおむねの見た目を再現しました。
何気にコンスルーです。

部品のほとんどは公式のモデルを登録して位置調整しています。

BLE Micro Pro

こちらもおおむね再現しました。

ProMicro と同様公式のモデルをふんだんに使っています。

余談

コンスルーのモデルを作成するために仕様書を読んだんですが、どうやら窓がない方を半田付けする前提っぽいですね。

参考

製品ページ
仕様書

キースイッチ

シンボル

SW_PUSHfoostan さんのシンボルをお借りしました。

Rotary_Encoder_LED は、 KiCad 公式のものの改変です。
ほぼ、秋月で売っている LED付ロータリーエンコーダ (通販コード P-05762, P-05767, P-05768)専用ですね。
探せば他にも同じようなものはあると思いますが。

フットプリント

MX

solder と hotswap それぞれを用意しました。

大きさは以下のものがあります。

  • 1U
  • 1.25U
  • 1.5U
  • 1.75U
  • 2U
  • 2.25U
  • 2.5U
  • 2.75U
  • 3U
  • 6U
  • 6.25U
  • 7U
  • 8U
  • 9U
  • 9.75U
  • 10U
  • ISO Enter

ほぼ網羅してるでしょう。

Choc

V1, V2 両方があります。
Choc は 1U のみ です。

V1 は、 solder と hotswap があります。
また、 Choc V1 の hotswap はソケットの大きさが 1U ぎりぎりになるので、 1U からはみ出ないようにパッドを狭めたパターンも作りました。

hotswap 用フットプリントについて

パッドの四隅に貫通 via を配置することで剥がれにくくしました。(当社比)
正直、パッド剥がれに遭遇したことがないのでどれほど効果があるかはわかりませんが、強くなってることは確かだと思います。

3D モデル

ソケットと各種スイッチのモデルを用意しました。

MX は Silent Alpaca のみです。

Choc は有名どころは押さえているはずです。
デフォルトでは、 Choc V1 は Sunset Tactile 、 Choc V2 は Red を登録しています。
好みで変えたりしてください。

キーキャップ

MX 用のみです。

R1 ~ 4 と Convex, ISO Enter があります。
デフォルトではすべてのキーキャップを登録しているので、レイアウトに合わせて削除したりして調整してください。

3D 表示したときに干渉とかが確認できたらいいなぁと思って付けました。

更に詳しくはこちら。

koktoh.hatenablog.com

スタビライザー

フットプリント

見にくいですね……

MX PCB マウント、 MX プレートマウント、 Choc V1 用を用意しています。

サイズは以下です。

  • MX
    • 2U
    • 3U
    • 6U
    • 6.25U
    • 7U
    • 10U
  • Choc V1
    • 2U
    • 5.25U

3D モデル

それぞれの 3D モデル、あります。

PCB マウントにはデフォルトで Screw-in のモデルを登録しているので、 Snap-in にしたい場合は改めて登録してください。

ロータリーエンコーダ

フットプリント

例の水平方向ロータリーエンコーダも用意しました。

3D モデル

3D モデルもあるので安心してください。

EC11 には、互換品の BOURNS PEC12R シリーズを作りました。
高さは、データシートに載っているすべてを用意しています。

  • 15mm
  • 17.5mm
  • 20mm
  • 22.5mm
  • 25mm
  • 30mm

フットプリントにはデフォルトで 20mm のモデルを登録しています。
これも設計によって変更したりしてください。

LED

シンボル

これも使いそうなものを用意しました。
形で見分けがつくようにしています。

フットプリント

見た通りですね。

3D モデル

3D モデルもちゃんと作ってるので、完成のイメージがしやすいんじゃないでしょうか。

OLED

シンボル

モジュールとしての OLED と OLED ディスプレイ自体のシンボルを用意しました。
キーボード基板に直接実装したい奇特な人向けですね。

試しに作った時は、 CxN ピンに接続するチャージポンプ用キャパシタの容量を間違えたため少し暗くなってしまいました。
でも、それ以外は問題なく動きましたよ。

フットプリント

モジュールとディスプレイ単体があります。

モジュールのフットプリントは、フットプリントのローカル原点によって3種類作りました。

  • モジュールの中心
  • ディスプレイの中心
  • ピンの中心

設計してるときにそれぞれ欲しいなと思ったので。

ディスプレイ単体は、ディスプレイから生えてる FFC を半田付けする面で複数作っています。

  • top
    • PCB の表面、ディスプレイの真裏に半田付け
  • bottom
    • PCB に穴を開けて、裏面に半田付け
  • both
    • top と bottom 両対応(半田付け時に選択)

また、 Active Area と囲われている部分が、実際の 128 x 32 px の表示エリアです。

3D モデル

3D モデルもちゃんと作ってますよ。

フットプリントと同様に、実際の 128 x 32 px の表示エリアを水色で示しています。

モジュールのモデルは、ピンソケットとピンヘッダを使って接続する状態にしています。
たぶん、この方法が一番多いと思うので。

それぞれのモデルは独立しているので、別のモデルを登録したり削除したり位置調整をすることで、いろんな付け方に対応できると思います。

USB

フットプリント

実際に使ったりした USB のフットプリントです。

PCB Edge と書かれている線に沿って外形線を引いてください。

3D モデル

USB Micro-B のプラグは生産終了予定らしいので、 3D モデルは作りませんでした。

スイッチ系

フットプリント

独断と偏見で、使われがちと判断したスイッチを用意しました。

3D モデル

3D モデルもあるね(確認)

その他

その他もろもろのシンボルとかをまとめて。

シンボル

ESD 対策用のチップとかのシンボルです。

MarkStabilizer は好きに使ってください。シルクアートのフットプリント用とか。

フットプリント

ボタン電池と TRRS ですね。

3D モデル

3D モデルもあるので、干渉確認とかに使ってください。

おわりに

説明になってましたかね。

一仕事終えたって感じです。

3D モデルは独立したリポジトリに置いて、キースイッチなどと同様 submodule で参照するようにしたいです。
まあ、しばらくはいじらないでしょうが。

なんかおかしいとか、このフットプリントが欲しいとかあったら、 Issue 立てたり、プルリクしたりしてください。
お前のプルリク待ってるぜ!!

頑張ったので差し入れください(直球) www.amazon.jp

これまで作ったキーボード関係の 3D モデルまとめ

はじめに

これまで、キーボード関係の 3D モデルをいろいろ作ってきましたが、1回まとめてみようかなと思いました。

最後に、各記事へのリンクも置いときます。

公開場所

ここで公開しています。
使用上の注意とかはそれぞれのリポジトリの README や、紹介記事を見てください。

github.com github.com

作ったモデル

キースイッチ

  • Kailh Choc V1

    • Black
    • Brown
    • Red
    • White
    • Red Pro
    • Sunset Tactile
  • Kailh Choc V2

    • Red
    • Blue
  • MX

    • Silent Alpaca

ソケット

  • Choc
  • MX

スタビライザー

  • Choc
    • 2U
    • 5.25U
  • Plate-mount
    • 2U
    • 3U
    • 6U
    • 6.25U
    • 7U
    • 10U
  • Screw-in
    • 2U
    • 3U
    • 6U
    • 6.25U
    • 7U
    • 10U
  • Snap-in
    • 2U
    • 3U
    • 6U
    • 6.25U
    • 7U
    • 10U

キーキャップ

Cherry 風モデル

1U 1.25U 1.5U 1.75U 2U 2.25U 2.5U 2.75U 3U 6U 6.25U 6.5U 7U 8U 9U 9.75U 10U
R1
R2
R3
R4
Convex
  • ISO Enter

おわりに

こうして見ると、結構作ってますね。

Kailh Choc V2 の Brown を手に入れたので、それも追加して Choc 系はコンプリートしたいなーと思ったり。
MX 系は種類が多すぎるので、流石に全部は作れないし、終わりも見えないのでたぶん追加はしないです。
また、今後新たに発売される Choc 系スイッチがあっても追加しないかも。というか、これだけあれば十分でしょ……

MX とも Choc とも違うスイッチ(ロープロとか)が出たら追加するかもしれないし、しないかもしれないです。(やる気次第)

モデル見てもらえばわかりますが、めちゃくちゃに凝ってるので、製作カロリーが高いんですよ……
Silent Alpaca とか1ヶ月近くかかったんじゃないかな。

Choc V1 のキーキャップくらいは作ってもいいかもしれない。
ただ、 1U しか持ってないし、使う予定がないキーキャップをモデルを作るためだけに買うのもなーという気も……

キーボードで使いそうなモデルは一通り作った気はするので、キーボード系の 3D モデル製作は一区切りかなって感じですね。
キーボードだけじゃない一般的に使う部品とかを作ろうかなと、ふわっと思ったりもしてます。

せっかく作ったので、よかったら使ってやってください。

記事リンク

koktoh.hatenablog.com koktoh.hatenablog.com koktoh.hatenablog.com koktoh.hatenablog.com koktoh.hatenablog.com koktoh.hatenablog.com koktoh.hatenablog.com koktoh.hatenablog.com

Snap-in のスタビライザーの 3D モデル作った

はじめに

前回はこちら。

koktoh.hatenablog.com

公開場所

github.com

ライセンス

CC BY-NC-SA で公開しています。

使用する際の注意事項はこちらもご確認ください

koktoh.hatenablog.com

内容

いつもの。

STEP と、 KiCad 用の WRL ファイルが入っています。

プリセットモデル

以下が組み立て済み(プリセット)モデルとして入っています。

  • 2U
  • 3U
  • 6U
  • 6.25U
  • 7U
  • 10U

見た目はこんな感じ(黒だから見にくいな……)

パーツモデル

はい、パーツモデルもあります。

おわりに

というわけで、 Snap-in スタビライザーでした。

いい感じに使ってください。

Plate-mount のスタビライザーの 3D モデル作った

はじめに

前回はこちら。

koktoh.hatenablog.com

公開場所

github.com

ライセンス

CC BY-NC-SA で公開しています。

使用する際の注意事項はこちらもご確認ください

koktoh.hatenablog.com

内容

いつもの。

STEP と、 KiCad 用の WRL ファイルが入っています。

プリセットモデル

以下が組み立て済み(プリセット)モデルとして入っています。

  • 2U
  • 3U
  • 6U
  • 6.25U
  • 7U
  • 10U

見た目はこんな感じ(黒だから見にくいな……)

パーツモデル

はい、パーツモデルもあります。

おわりに

というわけで、 Plate-mount スタビライザーでした。

いい感じに使ってください。