【ドラレコ】Raspberry pi Zere2 W を起動したら、前方カメラ、後方カメラの録画を開始する【スマートバイク】

前後カメラの録画プログラムを考える

ドラレコの要件としては、

  1. 前後カメラの録画
  2. 録画は最高画質で行う(もしもの時の資料)
  3. コーディックはH264(データ量が少ないので長い時間記録を残せる)
  4. 録画データは1分毎に別ファイルにする。
  5. メモリが一杯になったら、古いファイルから消していく
  6. 本体とは別のSDカードに記録する。SDカードは差し替え可能であること。

となります。

1. 前後カメラの録画は、カメラ2台用意したのでOK。Raspberry pi Zero 2 W次第ですが、パフォーマンス的にもなんとかなりそうです。

2. 録画は最高画質で行うも、大丈夫かな?ここもRaspberry pi Zero 2 W次第な部分ですね。とりあえずプログラム中で解像度を切り替え出来るようにしときましょう。

3. コーディックはH264は、カメラのハードウェアでH264にしてくれるので、2台のカメラともH264でできそう

4. 録画データは1分毎に別ファイルは、ffmpegの機能で出来ます。

5番目の要件「SDカードのメモリが一杯になったら古いファイル消す」の部分はプログラムを書かないと実現できなさそうです。これが一番大変かも?

でははずは、6. 本体とは別のSDカードに記録するから対応していきましょう。

動画保存用のSDカードをどうするか?

まずは、

6. 本体とは別のSDカードに記録する。SDカードは差し替え可能であること。

を行うために、SDカードリーダーとSDカードを用意します。

ドラレコに使用するSDカードは、消耗品です。常に古いデータを消して新しいデータを書き込み続けるのですが、そもそもSDカードやUSBメモリもですが、これらは書き換え可能回数はきまっていて、書き換えているとそのうち書き込めなくなります。MLCチップを使ったものが書き換え可能回数が多いので、ドラレコにはMLCチップのmicroSDカードを選択します。 万全を期すなら、SDカードは1年ぐらいで交換してあげるのが良いらしいです。

簡単に交換できるように、ラズパイのOSが入っているSDカードとは別に、SDカードを用意することにします。

USBハブにカードリーダーを入れます。製品はコレにしました。

耐久性の高いMLCのSDカードとして、これを選択しました。

USBメモリという手もあるのですが、通常ドラレコmicroSDを採用しておりSDカードの方が入手しやすいと思うので、SDカードリーダーとSDカードという構成としました。

動画で見るとわかりやすいですが物凄くピッタリ収まってます。 【ドラレコ】microSDリーダーを用意する - YouTube

USBメモリの自動マウント

SDカードが認識されているか確認

sudo blkid /dev/sda1

中略
Device     Boot Start       End   Sectors  Size Id Type
/dev/sda1       32768 249526271 249493504  119G  7 HPFS/NTFS/exFAT

/dev/sda1 として認識されていますね。

USBメモリバイスの情報を表示します。

sudo blkid /dev/sda1

/dev/sda1: UUID="9C33-6BBD" BLOCK_SIZE="512" TYPE="exfat"

あとでマウントするためにUUIDとTYPEの情報を使用します。

マウントするフォルダを作成します。

sudo mkdir /mnt/usb1

/etc/fstabを編集します。 UUID="9C33-6BBD...の1行を追加して、リブートします。自動でマウントされていれば、成功です。

sudo vim /etc/fstab

UUID="9C33-6BBD"  /mnt/usb1             exfat    defaults,noatime,nofail  0       0       #←この行を追加

再起動したらdfコマンドでマウントされたか確認します。

pi@raspberrypi:~ $ df
Filesystem     1K-blocks    Used Available Use% Mounted on
/dev/root       14900440 9836872   4424900  69% /
devtmpfs          185492       0    185492   0% /dev
tmpfs             218772       0    218772   0% /dev/shm
tmpfs              87512     992     86520   2% /run
tmpfs               5120       4      5116   1% /run/lock
/dev/mmcblk0p1    261108   52102    209006  20% /boot
/dev/sda1      124730368     384 124729984   1% /mnt/usb1
tmpfs              43752       0     43752   0% /run/user/1000

ただし、この方法だと、新しいメモリカードを差し替えるたびに、この作業をしないといけません。

なぜなら、UUIDはメモリカード毎に異なるからです。これは不便すぎですね。不採用です。

/etc/fstabを編集して元に戻すことにしました。

UUID="9C33-6BBD...の1行をコメントアウトしてリブートします。

作成したフォルダも消しておきます。

sudo rmdir /mnt/usb1

usbmountで自動マウント

SDカードを別のものにするたびにfstabを編集するのは不便なので、差し替えて再起動すれば自動マウントしてくれるように構成しましょう。

usbmountというパッケージを使うと手軽に自動マウントできるようになります。

インストール及び自動起動設定

sudo apt install usbmount -y
sudo mkdir /etc/systemd/system/systemd-udevd.service.d

設定ファイルをつくます。

sudo vim /etc/systemd/system/systemd-udevd.service.d/00-my-custom-mountflags.conf
[Service]
PrivateMounts=no

保存して再起動。dfでマウントされていることを確認します。

パーテーションを2つ持つSDカードだと、usb0,usb1としてそれぞれのパーティションがマウントされているのがわかります。

$ df
Filesystem     1K-blocks    Used Available Use% Mounted on
省略
/dev/sda1         261108   52108    209000  20% /media/usb0
/dev/sda2       14585536 3782972  10151892  28% /media/usb1
省略

電源をOFFし、SDカードを入れ替えてみます。今度は1パーテーションのみのSDカードに差し替えました。

設定ファイルを確認します。

sudo vim /etc/usbmount/usbmount.conf

FILESYSTEMS= の行を探して、exfatがなければ追記します。

FILESYSTEMS="exfat vfat ext2 ext3 ext4 hfsplu xfs"

FS_MOUNTOPTIONS= の行を探して、次のように設定します。これをしておかないと、いちいちrootにならないと書き込み出来ないためです。

#FS_MOUNTOPTIONS=""
FS_MOUNTOPTIONS="-fstype=exfat,iocharset=utf8,codepage=932,uid=pi,gid=pi,umask=000,dmask=000,fmask=011"

保存して再起動します。再起動したらdfで確認します。

pi@raspberrypi:~ $ df
Filesystem     1K-blocks    Used Available Use% Mounted on
省略
/dev/sda1      124730368     384 124729984   1% /media/usb0

無事ラズパイでSDカードを自動マウント出来るようになりました。

書き込みできるか確認します

pi@raspberrypi:/media/usb0 $ cd /media/usb0
pi@raspberrypi:/media/usb0 $ touch hoge
pi@raspberrypi:/media/usb0 $ ls
hoge
pi@raspberrypi:/media/usb0 $ rm hoge
pi@raspberrypi:/media/usb0 $ ls
pi@raspberrypi:/media/usb0 $ 

できました。

プログラム作る。

それでは、本題です。「2台のカメラでH264最高画質で録画し、1分毎に動画ファイルを作る」というプログラムを作っていきます。

ffmpegでファイル分割

次のように設定すると、60秒毎に1ファイルのファイルに分割してくれます。これを使えばファイル分割の要件もクリアですね。

まずは、最高画質 h264 mp4形式で1分ごとに保存します。

ffmpeg -rtbufsize 30M \
-f v4l2 -input_format h264 -video_size 1920x1080 -framerate 30 \
-i /dev/video0 -c:v copy \
-f segment -strftime 1 -segment_time 60 \
-segment_format_options movflags=+faststart -segment_format mp4 \
-reset_timestamps 1 \
/media/usb0/front_%Y-%m-%d_%H-%M-%S.mp4

ファイルの頭が途切れる。困った。aviにしてみようか。

最高画質 h264 avi形式で1分ごとに保存します。1280x720

ffmpeg -rtbufsize 300M \
-use_wallclock_as_timestamps 1 \
-f v4l2 -input_format h264 -video_size 1920x1080 -framerate 30 \
-i /dev/video2 -c:v h264_v4l2m2m \
-f segment -strftime 1 -segment_time 60 \
-reset_timestamps 1 \
-pix_fmt yuv420p \
/media/usb0/rear_%Y-%m-%d_%H-%M-%S.avi

よしよし、aviは良いみたいです。

h264も試してみる。うまく分割されるかな?

ffmpeg \
-f v4l2 -input_format h264 -video_size 1920x1080 -framerate 30 \
-i /dev/video0 -c:v copy \
-f segment -strftime 1 -segment_time 60 \
-reset_timestamps 1 \
/media/usb0/front_%Y-%m-%d_%H-%M-%S.h264

生の画像データをそのまま格納するだけなので、一番安定している。ただ一番再生が大変そう。

mp4はファイルの切替時に崩れる事が多い感じだが、aviは安定している。.h264は再生ソフトがないと再生できない。ファイルフォーマットはaviで決定ですね。

では、お手軽にpythonスクリプトを組んで行きましょう。

ファイル名はdashcam.pyとします。dashcam=英語でドラレコの事だそうです。ダッシュボードに置くカメラだからかな?

#!/usr/bin/env python3

from usbVideoDevice import UsbVideoDevice  # https://smartphone-zine.hatenablog.com/entry/2023/02/25/065957
import subprocess
import psutil  # フォルダ残量確認
import os  # ファイル操作に使う
import sys  # プログラムを途中で終了させるのに使う
from operator import itemgetter  # イテラブルから任意の要素を抜き出す
import signal  # 非同期イベントにハンドラを設定する
import threading
import time  # スリープ用


# データを保存するフォルダ EX) folder = '/media/usb0/'
folder = '/media/usb0/'

# 保存形式
extension = ".avi"

# デスク使用率がこの%を超えたら古いファイルを削除
dsk_usage_ratio = 90.0

# 前方カメラのUSBポート番号と画像サイズ
front_cam_port = 1
front_cam_size = '1920x1080'

# 後方カメラのUSBポート番号
rear_cam_port = 4
rear_cam_size = '1280x720'

video_front_p = None  # 録画用のプロセスです
video_rear_p = None  # 録画用のプロセスです

# カメラのVideoポート判定
usbVideoDevice = UsbVideoDevice()
video_front = "/dev/video" + str(usbVideoDevice.getId(front_cam_port))
video_rear = "/dev/video" + str(usbVideoDevice.getId(rear_cam_port))


# Ctrl+C or KILL で止められた時の処理
def sig_handler(signum, frame) -> None:
    print('\n\n\n------------sig_handler()\n\n\n')
    global video_front_p
    global video_rear_p

    if not video_front_p is None:
        #video_front_p.terminate()
        video_front_p.kill()

    if not video_rear_p is None:
        #video_rear_p.terminate()
        video_rear_p.kill()

    sys.exit(1)  # プログラムを途中で終了させる


# 録画処理
def record():
    global video_front_p
    global video_rear_p
    video_front_p = subprocess.Popen(
        ['ffmpeg',
         '-rtbufsize', '300M',
         '-f', 'v4l2', '-input_format', 'h264', '-video_size', front_cam_size, '-framerate', '30',
         '-i', video_front, '-c:v', 'copy',
         '-f', 'segment', '-strftime', '1', '-segment_time', '60',
         '-reset_timestamps', '1',
         folder + '%Y-%m-%d_%H-%M-%S_f' + extension])
    video_rear_p = subprocess.Popen(
        ['ffmpeg',
         '-rtbufsize', '30M',
         '-f', 'v4l2', '-input_format', 'h264', '-video_size', rear_cam_size, '-framerate', '30',
         '-i', video_rear, '-c:v', 'copy',
         '-f', 'segment', '-strftime', '1', '-segment_time', '60',
         '-reset_timestamps', '1',
         folder + '%Y-%m-%d_%H-%M-%S_r' + extension])

def diskfree():
    file_list = []
    # ディスク使用率を取得
    dsk = psutil.disk_usage(folder)
    print('\n------------usage:' + str(dsk.percent))
    if dsk.percent <= dsk_usage_ratio:
        return

    # ファイルデータ取得
    for file in os.listdir(folder):
        file_info = os.stat(folder + file)
        file_list.append([folder + file, file_info.st_mtime, int(file_info.st_size / 1024)])

    # 古い順にソート
    file_list.sort(key=itemgetter(1))

    # ファイルの削除
    for file in file_list:
        if dsk.percent > dsk_usage_ratio:
            print('\n------------[DELETE]' + file[0])
            os.remove(file[0])
            dsk = psutil.disk_usage(folder)
            print('\n------------usage:' + str(dsk.percent))
        else:
            break


def main():
    # スクリプトが止められたらプロセスを停止する
    signal.signal(signal.SIGTERM, sig_handler) # KILLされたとき
    signal.signal(signal.SIGINT, sig_handler) # Ctrl+Cされたとき

    # 録画開始、メインスレッド
    record()

    while True:
        # ディスクが使用率を超えたら古いファイルを削除する
        thread = threading.Thread(target=diskfree)
        thread.start()

        # 暫く待つ
        time.sleep(60)


if __name__ == '__main__':
    main()

実行可能ファイルにする

chmod 755 dashcam.py

実行してみる

./dashcam.py

さてここでまた問題発生、前後とも1920x1080で同時に撮影すると処理が追いつかず片方のカメラがfpsが5くらいまで落ちてしまう。

仕方ありません。泣く泣く、後方のカメラを1280x720に変更します。これで安定して30fps出ています。流石にラズパイZero2Wの限界でしょうか?USBカメラ2台ともフルHD録画とsdカード保存はゼロには荷が重いようです。

ただ、ラズパイZero2WでH264で前後カメラで録画出来るのは快挙ですよね!

ラズパイ起動時にドラレコの録画を開始する。

スクリプトも完成しましたので、サービス登録して起動時にスクリプトを起動するように設定しましょう。dashcam.serviceという名前のユニットファイルを作成します。

vim dashcam.service

次の内容で保存します。

# dash cam unit file
[Unit]
Description=dashcam
# 保存先USBメディアが使用可能になるのを待つ
RequiresMountsFor=/media/usb0

[Service]
# Type=simpleはコマンドを実行したタイミングで起動完了と判断します
Type=simple
User=pi
Group=pi

# 作業ディレクトリ指定
WorkingDirectory=/home/pi/

# サービスの起動コマンド
ExecStart=/home/pi/dashcam.py

# Pythonのprint 関数の内容をログに残す設定
Environment=PYTHONUNBUFFERED=1

# 停止完了までに待機する時間
TimeoutStopSec=5

# /bin/sleepを使うと起動時にWaitかけることができる
ExecStartPre=/bin/sleep 5

# ログ出力関連
StandardOutput=syslog
StandardError=syslog
SyslogIdentifier=dashcam

[Install]
WantedBy = multi-user.target

SyslogIdentifier=dashcam の名前を使って使ってログを設定。

sudo vim /etc/rsyslog.d/dashcam.conf
if $programname == 'dashcam' then {
    action(type="omfile" file="/var/log/dashcam.log")
}

syslog再起動

sudo systemctl restart rsyslog

ユニットファイルは、/etc/systemd/systemに配置します。ここは管理者が変更した設定ファイルが配置されるディレクトリです。

ユニットファイルを配置したらdaemon-reloadで反映し、dashcamサービスの起動・終了のテストを行い、dashcamサービスを自動起動するように設定しましょう。

sudo cp dashcam.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl start dashcam
sudo systemctl stop dashcam
sudo systemctl enable dashcam

ログを確認してみましょう。

tail -f /var/log/dashcam.log

エラーが出てきます。

ModuleNotFoundError: No module named 'psutil'

あ、これは多分psutilがsudoで実行出来ないからですね。sudoで実行できるようにインストールします。

sudo pip3 install psutil

これで解決しました。

さて、これでサービスとしてdashcamを実行出来るようになりました。ドラレコスクリプトも完成ですね。

おっと、まだスクリプトにバグが有るようです。削除処理ですね。

Feb 27 08:41:51 raspberrypi dashcam[1009]: ------------usage:90.4
Feb 27 08:41:51 raspberrypi dashcam[1014]: frame=1159046 fps= 36 q=-1.0 size=N/A time=08:49:25.13 bitrate=N/A speed=   1x
Feb 27 08:41:51 raspberrypi dashcam[1009]: Exception in thread Thread-529:
Feb 27 08:41:51 raspberrypi dashcam[1009]: Traceback (most recent call last):
Feb 27 08:41:52 raspberrypi dashcam[1009]:   File "/usr/lib/python3.9/threading.py", line 954, in _bootstrap_inner
Feb 27 08:41:52 raspberrypi dashcam[1009]:     self.run()
Feb 27 08:41:52 raspberrypi dashcam[1009]:   File "/usr/lib/python3.9/threading.py", line 892, in run
Feb 27 08:41:52 raspberrypi dashcam[1009]:     self._target(*self._args, **self._kwargs)
Feb 27 08:41:52 raspberrypi dashcam[1009]:   File "/home/pi/dashcam.py", line 88, in diskfree
Feb 27 08:41:52 raspberrypi dashcam[1009]:     file_info = os.stat(folder + file)
Feb 27 08:41:52 raspberrypi dashcam[1009]: FileNotFoundError: [Errno 2] No such file or directory: '/media/usb02023-02-25_16-17-45_r.avi'

フォルダとファイル名の間にスラッシュが抜けていました。(上のdashcam.pyはバグ修正済みです)

今日はここまでです。

長時間稼働試験

丸一日稼働させてみてffmpegが安定稼働するか見ていますが、たまに

Conversion failed!

というエラーでffmpegが終了してしまします。

Google検索すると、-max_muxing_queue_size 512すると良いとか書かれていたので試してみます。

しかしmax_muxing_queue_sizeを付けると負荷が高くなり、FPSが7くらいまで落ちてしまい安定しませんでした。もう少し様子を見て、ダメなら拡張子を.AVIから.H465に変更してみようかと思います。