ことの経緯

以前、raspberry piにて猫カメラ(にゃんCAM)を構築しました。
普段はTapoを使っていたので全然出番が無かったのですが、久々に起動しようとしたら起動を繰り返す変な動作をしていました(デスクトップ表示→左上にアンダーバー点滅→再起動の繰り返し)
直そうと適当にやっていたらSDカードを割ってしまいました(物理的に)。
というわけで、再構築しようとしたのですが環境が変わってつまずいたので新たに記事にしました。

教訓

SDカードは色んな意味ですぐ死ぬ。クローンを作っておくこと

ハード

  • raspberry pi 3B(3B+ではない)
  • 32GBのSDカード
  • 5.1V出力のACアダプタ
    • これはlow voltage errorの対策です

機能、ファイルの置き場など

OpenCVでUSBカメラ(webカメラ等)2台を制御し、30分ごとにLINEで撮影画像を送信します
2台用でプログラムを書いてあるため、2台以外(1台or3台〜)ではプログラムがエラーになり、動作しません。(具体的にはプログラムエラー→再起動のループ))
1台分にコメントアウトすれば1台でも動作します
saikidou.serviceとsaikidou.timerによってラズパイを定期再起動させますが、ラズパイの安定性向上やUSBが死んでカメラが認識しなくなることが偶にあり、それを復帰させるために実施します
USBが死ぬ(例えば、カメラが1台しか繋がってないような状態になります)と上記した通りnyancam.pyがエラーになり、nyancam.serviceによってプログラムの再起動を繰り返しますが、少なくとも定期再起動のタイミング(6時間ごと)で復帰するため長期にカメラが機能しなくなることはなくなります
死亡したときはプログラムの再起動が繰り返されるため、USBカメラのLEDが点滅します(LEDは撮影時に点灯)
ラズパイのそばで対応できるなら再起動を待たずともUSBの抜き差しで再認識します(USBが復帰し、nyancam.serviceによる再起動でカメラが2台認識します) GPIO26に接続したスイッチを単押しするとテスト撮影、5秒以上長押しするとラズパイをシャットダウンします

FilenamePathNote
nyancam.py/home/USERNAME/Desktop/プログラム本体
nyancam.service/lib/systemd/system/nyancam.pyを自動起動する エラー時にnyancam.pyを再起動する
saikidou.service/lib/systemd/system/ラズパイを定期再起動する
saikidou.timerlib/systemd/system/saikidou.serviceとセット

githubにあげてあります

プログラムの実行結果

nyancam.pyを実行した結果例を載せます
カメラ検索でワーニングが出ますが、動作に問題ないためそのままにしています。

camera number 0 Find!
[ WARN:0] global /tmp/pip-wheel-a8gfdc_n/opencv-python_13563f08137a4b20bc4dfee05bcbf854/opencv/modules/videoio/src/cap_v4l.cpp (893) open VIDEOIO(V4L2:/dev/video1): can't open camera by index
camera number 1 None
camera number 2 Find!
[ WARN:0] global /tmp/pip-wheel-a8gfdc_n/opencv-python_13563f08137a4b20bc4dfee05bcbf854/opencv/modules/videoio/src/cap_v4l.cpp (893) open VIDEOIO(V4L2:/dev/video3): can't open camera by index
camera number 3 None
[ WARN:0] global /tmp/pip-wheel-a8gfdc_n/opencv-python_13563f08137a4b20bc4dfee05bcbf854/opencv/modules/videoio/src/cap_v4l.cpp (893) open VIDEOIO(V4L2:/dev/video4): can't open camera by index
camera number 4 None
[ WARN:0] global /tmp/pip-wheel-a8gfdc_n/opencv-python_13563f08137a4b20bc4dfee05bcbf854/opencv/modules/videoio/src/cap_v4l.cpp (893) open VIDEOIO(V4L2:/dev/video5): can't open camera by index
camera number 5 None
[ WARN:0] global /tmp/pip-wheel-a8gfdc_n/opencv-python_13563f08137a4b20bc4dfee05bcbf854/opencv/modules/videoio/src/cap_v4l.cpp (893) open VIDEOIO(V4L2:/dev/video6): can't open camera by index
camera number 6 None
[ WARN:0] global /tmp/pip-wheel-a8gfdc_n/opencv-python_13563f08137a4b20bc4dfee05bcbf854/opencv/modules/videoio/src/cap_v4l.cpp (893) open VIDEOIO(V4L2:/dev/video7): can't open camera by index
camera number 7 None
[ WARN:0] global /tmp/pip-wheel-a8gfdc_n/opencv-python_13563f08137a4b20bc4dfee05bcbf854/opencv/modules/videoio/src/cap_v4l.cpp (893) open VIDEOIO(V4L2:/dev/video8): can't open camera by index
camera number 8 None
[ WARN:0] global /tmp/pip-wheel-a8gfdc_n/opencv-python_13563f08137a4b20bc4dfee05bcbf854/opencv/modules/videoio/src/cap_v4l.cpp (893) open VIDEOIO(V4L2:/dev/video9): can't open camera by index
camera number 9 None
接続されているカメラは 2 台です
カメラ0 : 0カメラ1 : 2CamNum0 : 0
CamNum1 : 2


### にゃんCAM 起動 ###
rev:7

30.0分ごとに撮影しまーす!
接続カメラ : 22023-09-09 11:38:42.525746

カメラ0
うp残 : 46 / 50

カメラ1

3BはUSBブートがめんどいらしい

SDカードを割ってしまったので、もっと(物理的に)丈夫なUSBメモリでbootさせることを考えました
しかし、3B+はデフォルトでUSBブートができるのですが、3Bはできないようです
正確には色々変更すればできるようですが面倒なので見送りました

SDカードに書き込みから

まずは環境構築していきます
Raspberry Pi Imager v1.7.5を使い、Raspberry Pi OS(32-bit)を書き込みます
SSHとVNCで操作するので、“SSHを有効にする"をチェックしておきます
他の設定はお好みで

SSHで繋いで、VNCを有効化する

VNCはデフォルトでは無効化されているので、セットアップ時のraspi-configで有効化させます
まず、SSH接続します

ssh username@ip-address

VNCはrealvncが元々入っていますのでバージョンを確認してみます

$ dpkg -l real*
realvnc-vnc-server 7.0.1.49073

raspi-configからVNCを有効化します

sudo raspi-config

3 → VNC を有効化でオッケ

SSHで繋いでいても、VNCは繋がるようです

最初にやるアプデとか

色々乱立していますが、現在まとめたのは以下です
上から順に実行します
どのラズパイでも共通です

パッケージリストの更新

sudo apt update

アップグレード

sudo apt full-upgrade

ここまではguiでやるアプデなどと内容は一緒らしいです

続いて、要らないファイルの削除

sudo apt autoremove

これは sudo apt full-upgrade を実行した後に表示されます

キャッシュの削除

sudo apt clean

再起動

sudo reboot

ここで注意したいのは sudo rpi-updateはやらないことです

OpenCVのインストール

これは調べるとたくさん記事が出てきますが、みんな苦労しているようです
numpyのバージョンが古いとエラーが出るようで、私も同じパターンでした
今回OSに同封されていたpython3はpython 3.9.2でした(前回は3.7.3)

$ python3 -VV
Python 3.9.2 (default, Mar 12 2021, 04:06:34) 
[GCC 10.2.1 20210110]

Raspberry Pi3 ModeB+ Python3 OpenCV構築 - Qiita

上記を参考にして、まずライブラリのインストールをします

sudo apt-get install libhdf5-dev libhdf5-serial-dev libatlas-base-dev -y

続いて、OpenCVをバージョン指定でインストールします
のちほどroot権限でsystemdを実行するので、sudoでインストールします
python3にインストールするため、pip3と明示します
バージョンは以前の記事で紹介したものと同じです

sudo pip3 install opencv-python==4.5.1.48

バージョン指定しないでインストールは最初にやってみたのですが、インストールにとても時間がかかり止まっているようだったので中断しました
バージョン指定したらすぐ終わったのでやはり止まっていたようです

numpyがエラー

OpenCVはnumpyが必要です
numpyはデフォルトでインストールされていますが、OpenCVの要求に対してバージョンが古いためimport cv2がエラーになり動きません
そこで、numpyをアップグレードします
こちらもrootでsystemdを実行するため、sudoでアップグレードします

sudo pip3 install numpy --upgrade

更新前は 1.19.5 でしたが、更新後は 1.25.0 となりました

$ python3
Python 3.9.2 (default, Mar 12 2021, 04:06:34) 
[GCC 10.2.1 20210110] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import numpy
>>> numpy.__version__
'1.25.0'
>>> numpy.__file__
'/usr/local/lib/python3.9/dist-packages/numpy/__init__.py'

numpyを更新したら、OpenCVもインポートできるようになります

>>> import cv2
>>> cv2.__version__
'4.5.1'
>>> cv2.__file__
'/usr/local/lib/python3.9/dist-packages/cv2/cv2.cpython-39-arm-linux-gnueabihf.so'

systemdを使ったプログラムの自動起動、再起動、ラズパイの定期再起動

.serviceファイルを今回は /lib 以下に作成しました
/lib 以下はシステム関連が多いため、汚したくないという理由で /etc 以下に作るのがお作法らしいです
それは後から知ったので、今回はそのまま /lib 以下にしています

まず、nyancam.pyを自動起動、再起動するサービスをsudoで作成します

sudo nano /lib/systemd/system/nyancam.service

続いて、ラズパイを定期再起動するサービスを作成します

sudo nano /lib/systemd/system/saikidou.service
sudo nano /lib/systemd/system/saikidou.timer

次に、サービスをスタート、有効化します
enableで有効化することで、次回起動時に自動でサービスが開始されます

sudo systemctl start nyancam.service
sudo systemctl enable nyancam.service
sudo systemctl enable saikidou.timer

saikidou.serviceenableする必要はありません

正常にサービスが起動しているか、ステータスを確認します
例えば定期再起動のサービスの確認は以下です

sudo systemctl status saikidou.timer -l

より詳細なログは以下で確認できます

journalctl -u nyancam.service -r | less

Wi-Fiのスリープ回避

ラズパイのパワーセーブ機能でWi-Fiのスリープ機能があります。
30分ごとに動作する間にスリープしてしまうとエラーになりますので機能を停止します。
ただ、実際にどのくらいでスリープするかは確認していないので一応やっただけです。

以下でパワー・マネージメントモードの状態を確認します
デフォルトではオンです。

$ iw dev wlan0 get power_save
Power save: on

パワー・マネージメントの無効化をします

sudo nano /etc/dhcpcd.exit-hook

dhcpcd.exit-hookの中身はGitHubにあげています。

ここまでで一応終わりです
以下は詰まった点などを記載します

ターミナルでは動くのに、systemdでは動かない

nyancam.pyはターミナルやThonnyでは動くのに、systemdでは動きませんでした

原因

numpyをアップデートする際にsudoをつけなかった

pip3 install numpy --upgrade

対策

sudoをつける

sudo pip3 install numpy --upgrade

sudoありなしでは、pythonパッケージのインストール先が異なる

sudoありの場合は/usr/local/lib/python3.9/dist-packagesにインストールされますが、sudoなしの場合は/home/USERNAME/.local/lib/python3.9/site-packagesにインストールされます
systemdはsudoで実行しますが、デフォルトでは/home/USERNAME/.local/lib/python3.9/site-packagesを見に行きません
OpenCVはsudoでインストールしたため/usr/local/lib/python3.9/dist-packagesにインストールされたので問題無かったのですが、numpyはsudoなしでアップグレードしたため、/home/USERNAME/.local/lib/python3.9/site-packagesにインストールされていました(ver 1.25.0)
numpyはプリインストールされており、そちらは/usr/local/lib/python3.9/dist-packagesにあります(ver 1.19.5)
よって、バージョン違いのnumpyが2つあることになります
これらより、systemdで実行した場合はOpenCVが古いnumpy(ver 1.19.5)を見に行ったためエラーとなりました
numpyの更新をsudoで実施すれば、プリインストールされたnumpyが更新されます
ターミナルやThonnyで実行する場合は/home/USERNAME/.local/lib/python3.9/site-packagesも見に行くためエラーにならず、気づきづらいです

以下はターミナルで実行した場合の、インポートするpythonパッケージのロケーション一覧です。/home/USERNAME/.local/lib/python3.9/site-packagesが含まれていることがわかります
systemdをsudoで実行すると含まれません

$ python3
>>> import sys
>>> sys.path
 ['',
 '/usr/lib/python39.zip',
 '/usr/lib/python3.9', 
 '/usr/lib/python3.9/lib-dynload',
 '/home/USERNAME/.local/lib/python3.9/site-packages',
 '/usr/local/lib/python3.9/dist-packages',
 '/usr/lib/python3/dist-packages',
 '/usr/lib/python3.9/dist-packages']

以前のにゃんCAMではなぜ問題なかったのか

以前はOpenCVはsudoでインストールし、numpyはプリインストールされたものを使用しています
numpyはアップグレードせずともOpenCVは動作していたため、今回のようなインストール場所の問題が起きず使用できていたと考えられます

対策の別案

新しいnumpyがインストールされている場所を示す方法があります
これはsys.path.insert(0, "/home/USERNAME/.local/lib/python3.9/site-packages")で、パッケージリストの先頭に/home/USERNAME/.local/lib/python3.9/site-packagesを追加しています
これによりsystemdをsudoで実行しても/home/USERNAME/.local/lib/python3.9/site-packages内を探してくれるようになります
また、pythonはパッケージリストの上から検索していき、同じパッケージが複数ある場合は先に見つかったものから適用するらしいので、先頭に追加して古いnumpyが適用されることを防いでいます

import sys
import pprint
import os
print(sys.executable)
print("before path add")
pprint.pprint(sys.path) # before path add
print(os.getenv('PYTHONPATH'))
sys.path.insert(0, "/home/USERNAME/.local/lib/python3.9/site-packages")
print("after path added")
pprint.pprint(sys.path) # after path added

結果↓

/usr/bin/python
before path add
['/home/USERNAME/Desktop',
 '/usr/lib/python39.zip',
 '/usr/lib/python3.9',
 '/usr/lib/python3.9/lib-dynload',
 '/home/USERNAME/.local/lib/python3.9/site-packages',
 '/usr/local/lib/python3.9/dist-packages',
 '/usr/lib/python3/dist-packages',
 '/usr/lib/python3.9/dist-packages']
None
after path added
['/home/USERNAME/.local/lib/python3.9/site-packages',
 '/home/USERNAME/Desktop',
 '/usr/lib/python39.zip',
 '/usr/lib/python3.9',
 '/usr/lib/python3.9/lib-dynload',
 '/home/USERNAME/.local/lib/python3.9/site-packages',
 '/usr/local/lib/python3.9/dist-packages',
 '/usr/lib/python3/dist-packages',
 '/usr/lib/python3.9/dist-packages']

after path added以降は/home/USERNAME/.local/lib/python3.9/site-packagesが2箇所記載されていますが、これはターミナルで実行したからであり、systemdで実行すれば先頭に1箇所だけになります。
この方法は一応は動きましたが、別の動作でエラーが発生したため、より簡単なsudo pip3 install numpy --upgradeで対応しました
本来はsudo pip3 installはシステムを汚すためタブーらしいのですが、OpenCVもsudoで入れてしまったし、numpyもプリインストールされてるものなのでまぁ良いかなと思っています
他にも、OpenCV、numpyをsudoなしでインストールし、systemdをユーザで実行する方法もありますが、そちらは未着手です

以上