Gunosy Tech Blog

Gunosy Tech Blogは株式会社Gunosyのエンジニアが知見を共有する技術ブログです。

改めてドライブレコーダーを作ってみた

こんにちは。最近Gen1のAirpodsの充電が続かなくなり困っている、広告技術部でエンジニア/マネージャをやっているサンドバーグです。 ミーティング中は片耳ずつ充電しながら話していますが、そろそろ有線か新しいAirpodsに変えようと思います。

そんな話はさておき、こちらの記事は Gunosy Advent Calendar 2021 の20日目の記事です。 昨日の記事は 田口さんの iOSエンジニアがサーバーサイドもやってみた話 でした。 Gunosyのサーバーサイド背景も端的にまとまっていて、iOSエンジニアのフレッシュな視点で書かれている記事なので、まだ読まれてない方は是非読んでみてください。

こちらの記事内容は過去に書いた ドライブレコーダを作り始めて見た Pt. 1 - Gunosy Tech Blog から数年が立っていたので、また一から作ったみた話になります。

始めに

f:id:shosundberg:20211219224517p:plain

「煽り運転」

二年前はかなりホットなトピックでしたが、皆さんは最近煽り運転を受けたことありますか?

2019年あたりに急上昇した相談件数ですが、直近だと年々減っているそうです。ただ、普段運転している中でいまだに後ろから急接近・密着してくる車は後を絶えず、ただみんな慣れてしまったのでは、、?と疑問に思うこともあります。

そこで、今回は二年前から放置していたドライブレコーダーを復活させ、ちゃんと動く物を用意しようと思います!

パーツ集め

今回パーツを集めるにあたって、実際にバイクに乗せるのに必要なパーツをすべて揃える勢いで物を購入しました。

  • Raspberry Pi Zero
    • 説明は不要だと思いますが、小型コンピューターです。Raspberry Pi Zero を選ぶ理由はその値段とサイズ、最低限のスペックです。
  • MicroSD
    • 今回は低画質で長時間録画が用途なので、32GBくらいのMicroSDカードを用意しました。
  • カメラモジュール
    • 肝になるカメラモジュールですが、値段が安いのがかなり出回っているので選び放題です。注意点としては Raspberry Pi Zero のCSIカメラコネクターのサイズが通常のものとは異なるので、必要であれば変換ケーブルが必要です。
  • モバイルバッテリー
    • Raspberry Pi を動かすためのバッテリーです。Raspberry Pi Zero 自体消費電力がかなり小さいので10,000mAhバッテリーはオーバーキルです。
  • スイッチ付きmicro-usbケーブル
    • ドライブレコーダー自体ケースの中に入れるので、電源を切るのにいちいちUSBを外したくない際はおすすめです。
  • プラスチックケース
    • 防水で加工がしやすく、大きさに余裕のあるケースならどれでもOKです。
  • 六角真鍮スペーサー + スタンドオフ
    • ケース内でコンポーネントの間に隙間を作るために使うスペーサー・スタンドオフです。
  • micro-usb to 4port USB A
    • Raspberry Pi Zero は micro-usbのポートが2つしかないので(内一つは電源用)、開発時にキーボードなどを繋げられるためのUSBハブです。
  • miniHDMI to HDMI Converter
    • Raspberry Pi Zero はHDMIではなく miniHDMIなので変換ケーブルをおすすめします。

今回誤算だった点があるとすると、Raspberry Pi Zero を選択することによって細かい変換パーツが必要になってしまったという点です。 マイコンとしての価格は安く、サイズも小さくて助かりますが、開発している段階では通常のHDMIやUSB-Aポートが備わっている Raspberry Pi 3/4 をお勧めするかもしれません。

Raspberry Pi のセットアップ

Raspberry Pi 本体のセットアップ

Raspberry Pi (Raspbian OS) 本体のセットアップは公式ホームページを参考にするか、多く存在するチュートリアルをご覧になってください!

www.raspberrypi.com

Raspberry Pi 本体のセットアップが終わった時点で以下2点まで用意ができている想定です。

  1. 画面で Raspbian OSの起動が確認できる

  2. 周辺機器をすべて接続している (画面・キーボード・カメラモジュール)

本体セットアップ後の設定

今回は最低限インターネットとカメラモジュールの起動ができていればいいのでそれらをデフォルトで入っている raspi-config というツールを使って設定します。

起動は簡単、以下のコマンドを実行すれば

$ sudo raspi-config

このような画面が出てきます。

f:id:shosundberg:20211219222359p:plain
https://www.raspberrypi.com/documentation/computers/configuration.html 公式ページ画像は少し古そう?

最初に System Settings > Wireless LAN で近くにあるWifiを設定し、 次に System Settings > Boot / Auto Login 設定を Console AutoLogin に設定してください。

Wifiに関しては特に説明はいらないと思いますが、Autologinは画面がない環境でも電源を入れたら最後まで起動させるために必要となります。

Pingなどでネットがつながっているのが確認できた段階で一度 Rasbianに乗っているパッケージなどを更新するために apt-get update/upgrade をしましょう。

$ sudo apt-get update
$ sudo apt-get upgrade

これが終わり次第 raspi-configに戻り、 Interface Options > Legacy Camera で legacy camera support を ON にしましょう。

上記問題なく進めば reboot 後に適当に以下のようなコマンドを叩けば...

$ raspivid -fps 3 -t 10 -o video.mp4

カメラの起動が確認できます!

www.youtube.com

設計

今回使うクラウドサービスはGCPへのアクセスで Google App Engine と データ保存で Google Drive API になります。 構成と処理をざっくり表すと以下のようになります。

f:id:shosundberg:20211220005812p:plain

Google App EngineにはGoogle Drive接続時に必要なトークン発行をするために OAuth での Consent を行います。

Consent の scope はGCPのコンソールでの OAuth Consent screen から Google Drive API へのアクセス権限を付与する必要があります。

f:id:shosundberg:20211220010943p:plain

その後は発行したトークンをもって事前に許可してる Drive へファイルアップロードという形です!

実装

実装しているコードは以下のレポジトリにまとめているので、興味がある方は見てください(汚いコードですが、まだ改良する予定です)! github.com

Authenticate 周り

実装の肝となるのが GCPコンソールで事前に発行した Client でのトークン発行周りになります。

コード自体はほとんどGoogleで用意しているRubyでのgoogle-auth実装を参考に書いています。 github.com

実際書いたコードは以下

def authorize
    client_id = Google::Auth::ClientId.new(
        @config['client_id'], 
        @config['client_secret']
    )
    token_store = Google::Auth::Stores::FileTokenStore.new(file: TOKEN_PATH)
    authorizer = Google::Auth::UserAuthorizer.new(client_id, SCOPE, token_store)
    credentials = authorizer.get_credentials('default')

    if credentials.nil?
        url = authorizer.get_authorization_url(base_url: OOB_URI)
        # NOTE: produce QRCode for ease of access to URL.
        #       Use fbi to view qr-code png in terminal
        qrcode = RQRCode::QRCode.new(url)
        png = qrcode.as_png(
            bit_depth: 1,
            border_modules: 4,
            color_mode: ChunkyPNG::COLOR_GRAYSCALE,
            color: "black",
            file: nil,
            fill: "white",
            module_px_size: 6,
            resize_exactly_to: false,
            resize_gte_to: false,
            size: 120
        )
        IO.binwrite('./tmp/qr-code.png', png.to_s)
        system('fbi -a ./tmp/qr-code.png')
        puts "Open the following URL in the browser and enter the " \
             "resulting code after authorization:\n" + url
        code = gets
        credentials = authorizer.get_and_store_credentials_from_code(
            user_id: 'default', 
            code: code, 
            base_url: OOB_URI
        )
    end
end

一点変な実装があるとすると、Consent をする上で発行する遷移先URLの箇所です。 普段PC/スマホでやる分には発行したURLをクリックしてブラウザに飛ばすで済みますが、Raspiberry Pi のターミナル内ではそうもいきません。 そこで、今回は単純に発行したURLをQRコードに変換し、保存した画像をターミナル上に描画させ、Consent 自体はそれを使ってスマホで操作をさせます。

動画の録画とDriveへの送信

動画の録画は最初に紹介した raspivid を使った処理になります。 内容としては至って単純、ネットに接続している場合は raspivid を停止し、保存されている mp4 ファイルをDriveへ送信。 ネットに接続できない場合は raspividを起動します。上記の処理をloopし、ネット環境があるかないかで録画開始 or Drive転送の処理を繰り返しているだけです。

def capture_or_upload
    drive = RiderCam::Drive.new
    capturing = false
    sleep(5)

    loop do
        if drive.net_connected?
            system('pkill raspivid')
            capturing = false
            drive.upload_files if drive.uploadable_files?
        else
            next if capturing
            system("raspivid -n -b 250000 \
                   -fps 3 -t 0 -w 640 -h 480 \
                   -o ./tmp/uploads/#{DateTime.now.to_s}.mp4")
            capturing = true
        end
        sleep(5)
    end
end

動画の転送も一旦マルチパートなどは使っておらず、google-drive-api で提供されている create_file で新しい動画ファイルをdriveに作っているだけになります。

def upload_files(dir = './tmp/uploads/')
    Dir[File.join(dir, '*.mp4')].each do |file_path|
        file = @service.create_file(
            { name: "#{DateTime.now.to_s}.mp4" },
            fields: 'id',
            upload_source: file_path,
            content_type: 'video/mp4'
        )
        
        puts "uploaded: #{file.id}"
        File.delete(file_path)
    end
end

組み立て

組み立てに関しては... すいません、一部パーツでサイズ違いがあり、箱に埋め込むところまでができませんでした。

結果、パーツをすべて箱に放り投げて出来上がったのが、 こちらになります。 f:id:shosundberg:20211220014359j:plain

簡易的感が半端ないものになってしまいましたが、しょうがないですね。

テスト

テストとしては、ネット接続の ON/OFF での録画・転送を試したかったので、 自分の部屋から近くの道までを歩くというのを Raspberry Pi が起動している状態で行いました。

www.youtube.com

その結果、部屋から外に出るまでは録画がされておらず、家に近づいてWifiがつながった瞬間に録画が停止、googleへファイルが転送されていました! 想定していた挙動通りには動きましたが、利便性に関してはちょっと疑問に思うところがあるので、今後の改良として普通にスイッチをつけるかもしれません。

まとめ

簡易的なもので、箱に収まる状態までは持っていくことができませんでしたが、ひとまず形にはなった時点で成功といえるでしょう。

いけていない仕様だったり、汚いコードなども残っている状態ですが、 ここまで作ったので今年こそは途中放棄をせずに、最後まで改良していきたいと思います!