Raspberry PiとImgurを使って無料で自宅監視カメラLINEボットを構築する
本記事
本記事では、実際にRaspberry Piのカメラモジュールを使って監視カメラサービスを構築してみようと思います。
完成形は以下のようにかまる
と呼びかけると家の画像を返信してくれるものです。すべて無料の制限内にします。
モチベーションとしてはカメラモジュールの使い方などを解説している記事は多くありますが、実際にCloud Nativeな技術と組み合わせて解説し、運用しているものがないと思ったからです。
使うもの、環境は以下のようです。
Raspberry Pi Model B
Raspberry Pi 3 Model B V1.2 (日本製) 国内正規代理店品
- 出版社/メーカー: Raspberry Pi
- 発売日: 2016/02/29
- メディア: Tools & Hardware
- この商品を含むブログを見る
Raspberry Pi Camera Module V2
Raspberry Pi Camera Module V2 カメラモジュール (Daylight - element14)
- 出版社/メーカー: Raspberry Pi
- メディア: Personal Computers
- この商品を含むブログを見る
Imgur
Cloud Functions
Google Cloud Functions に関するドキュメント | Cloud Functions | Google Cloud
LINE message API
Raspberry Piのセットアップ
1. Wifi設定
まず、RaspberryPiのWifiを設定します。
CUIから設定しなければならないので以下のように設定します。
sudo nano /etc/wpa_supplicant/wpa_supplicant.conf
そして以下のようにnetwork
を書き込みます。
network={ ssid="YOUR__SSID" psk="PASSWORD" }
また、これを設定してから再起動する必要があります。
sudo reboot
2. 環境構築
以下のように環境構築をします。
sudo apt-get update && sudo apt-get upgrade
そしてPython3.6.0をインストールします
sudo apt-get install build-essential tk-dev libncurses5-dev libncursesw5-dev libreadline6-dev libdb5.3-dev libgdbm-dev libsqlite3-dev libssl-dev libbz2-dev libexpat1-dev liblzma-dev zlib1g-dev wget https://www.python.org/ftp/python/3.6.0/Python-3.6.0.tar.xz tar xf Python-3.6.0.tar.xz cd Python-3.6.0 ./configure make sudo make altinstall
3. 静的IPを設定する
毎回毎回、IPアドレスが変更されると大変なので静的IPを取得します。
以下のように設定します。
sudo vim /etc/dhcpcd.conf
そして、以下のように書き換えます。
interface wlan0 static ip_address=hogehoge static routers=hogeheoge static domain_name_servers=hogehoge
そして、また再起動します。
sudo reboot
4. カメラモジュールをつける
RaspberryPiの開発者の一人の解説動画があるので、安心してつけられました。
Imgur
Imgurとは画像シェアサービスです。主にgifなどを対象としています。
Hiddenという、いわゆるPrivateな画像を保存することができ、画像一つ一つにダイレクトリンクが渡されます。
そのリンクを知れれば、誰でも簡単にアクセスできるため気をつけてください。
最初に
迷ったらドキュメントを確認してください。バージョンアップが絶えずあるため、必要に応じて参照してください。
新しいバージョン(version 3)のAPIドキュメントは以下のリンクになっています。
執筆時点(2018/12/16)で最新のバージョンとなっています。
アプリケーションを登録
まず、Imgur APIにアクセスするクライアントアプリケーションを登録する必要があります。
認証にはOAuth2.0を使っています。OAuth2.0の仕組みは、以前ブログにしました。
簡単にいうと
- 一旦、アプリケーションでログインして短命なコードをもらう
- そのコードで認証を済ませて、アクセストークンをもらう
- 今度からはそのトークンをもってサービスにアクセスする
という流れです。
まず最初にアプリケーションを登録してclient_id
とclient_secret
をもらう必要があります。
ここでPostmanを使うと便利に登録できるので以下のサイトでダウンロードします。
そしてドキュメントの右上のRun in Postman
を押してPostman for Mac
を選択します。
すると以下のようにmacのPostmanアプリケーションでCollectionsとして開かれます。
Collectionsとは、リクエストのまとまりです。
まず、Webページの方でSubmitして以下のようにコードをもらいます。
ここでAuthorization callback URL
にはhttps://www.getpostman.com/oauth2/callback
と入力ください。
あとはEmailを埋めるだけです。
そしてもらったclient_id
とclient_secret
をもってして、Postmanの方で以下のようにOAuth2.0を設定します。
まずAccount
-> Generate Access Token
からAuthorization
タブを選択します。
そして、Generate Access Token
を押して以下のようにフォームを埋めます。
そしてRequest Token
を押すと以下のようになります。
ここでログインするとAccessトークンを取得できます。
これでUse Token
を選択してください。次から簡単にアクセストークンが使えるようになります。
ここまででアクセストークンを取得することができました。
適当に画像をPostmanを使ってUploadしてみます。
以下のようにImage
-> Image Upload
で使ってみます。ImageはファイルをドラッグするとBodyにつけることができます。
無事、Hidden(Private)な画像をアップロードができました。
このような感じで、次はスクリプトからImgurに画像をストアしていきます。
Pythonコードを書く
このセクションはアーキテクチャ図の以下の箇所に対応しています。
1. 静止画を撮影するサーバーを立てる
Postmanでは手でぽちぽちとHTTPリクエストを用意して、実行していました。これからはサービスにするためにもスクリプトで書いていきます。
まず、最初にサーバを立てましょう。RaspberryPiとの相性もいいのでPython製のFlaskという軽量Webサーバーを立てていきましょう。
import flask from time import sleep from picamera import PiCamera app = flask.Flask(__name__) @app.route('/kamaru') def index(): camera = PiCamera() camera.resolution = (1024, 768) camera.start_preview() sleep(2) camera.capture('test.jpg') return "Hello, World!" if __name__ == '__main__': app.run(debug=True)
これによってtest.jpg
の名前で保存されます。
2. Imgur APIへアップロードするスクリプト
ここでは以下のようにHTTPリクエストでImgur APIへ送信します。
このスクリプトをupload.py
と名付けておきます。
import os from os.path import join, dirname import json import requests from dotenv import load_dotenv dotenv_path = join(dirname(__file__), '.env') load_dotenv(dotenv_path) url = 'https://api.imgur.com/3/image' file = {'image': open('test.jpg', 'rb')} headers = {'Authorization': 'Bearer ' + os.environ['IMGUR_ACCESS_TOKEN']} res = requests.post(url, files=file, headers=headers) data = res.json() print(json.dumps(data, indent=4))
環境変数よりアクセストークンであるIMGUR__ACCESS_TOKEN
を取得します。
また、このアクセストークンやこれからの環境変数を設定するのは.env
ファイルに記述します。
IMGUR_ACCESS_TOKEN=
の右辺に渡してあげます。
ここまででアップロードできます。
python upload.py
また、これをFlaskサーバーにこの機能を載せます。upload.py
を先ほどのFlaskサーバーに載せると以下のようになります。
import flask import os from os.path import join, dirname import json import requests from dotenv import load_dotenv from time import sleep from picamera import PiCamera dotenv_path = join(dirname(__file__), '.env') load_dotenv(dotenv_path) app = flask.Flask(__name__) def capture_image(): camera = PiCamera() camera.resolution = (1024, 768) camera.start_preview() sleep(2) camera.capture('test.jpg') def upload_image(): url = 'https://api.imgur.com/3/image' file = {'image': open('test.jpg', 'rb')} headers = {'Authorization': 'Bearer ' + os.environ['IMGUR_ACCESS_TOKEN']} res = requests.post(url, files=file, headers=headers) data = res.json() return json.dumps(data, indent=4) @app.route('/') def index(): return "Hello, World!" @app.route('/kamaru') def index(): capture_image() data = upload_image() return data if __name__ == '__main__': app.run(debug=True)
これによって/kamaru
でリクエストがあると、画像をアップロードできるようになりました。
ここで/
を残したのはヘルスチェックというもので、死活管理に使うためです。
また、ローカルホストを公開するためにはngrokを使います。
以下のようにインストールすることができます。
wget https://bin.equinox.io/c/4VmDzA7iaHb/ngrok-stable-linux-arm.zip unzip ngrok-stable-linux-arm.zip sudo mv ngrok /usr/local/bin/
Flaskサーバーをバックエンドで立てます。
python3.6 server.py &
そしてngrokで公開します。
ngrok http 5000
とするとhttps
のURLスキーマのサーバーを立てることができ、どこからでもアクセスをすることができます。
試しに
curl https://77dr8983a.ngrok.io => Hello, World!
と返ってきました。
これだとssh接続が切れるとngrokも使えなくなるため以下のようなコマンドを打ちます。
nohup ngrok http 5000 & curl localhost:4040/status | grep ngrok.io
そのURLを使ってください。
3. LineBotのサーバーを書く
今回はコストと手軽さを考えてCloud Functionsで構築してみようと思います。
最初に環境変数を使うための.env.yml
ファイルを作成します。先ほどのPythonスクリプトとは別ディレクトリか、別リポジトリにしておくと管理がしやすいでしょう。
LINE_CHANNEL_ACCESS_TOKEN: LINE_GROUP_ID: MY_RASPBERRY_PI_URL:
そして以下のようにハンドラーを作ります。
const line = require('@line/bot-sdk'); const client = new line.Client(config); function handleEvent(event) { if (event.type === 'message' && event.message.type === 'text') { if (event.message.text.match(/^かまる/g)) { // 内容を実装する } } return Promise.resolve("error") } exports.handler = function lineBot (req, res) { Promise .all(req.body.events.map(handleEvent)) .then(result => res.status(200).send(`Success: ${result}`)) .catch(err => { console.log(err.toString()) res.status(400).send(err.toString()) }); };
これはかまる
と呼びかけると反応するのものです。しかし内容はまだ実装されていません。
これからは// 内容を実装する
という項目で実装していきます。
大枠、このような実装になるでしょう。
const raspberryPiURL = process.env.MY_RASPBERRY_PI_URL const options = { url: raspberryPiURL, method: 'POST' } request(options, (err, res, body)=>{ var message if (err) { console.log(err) message = getErrorMessage(err.toString(), currentDate) return client.replyMessage(event.replyToken, message); } var message = { imageURL: body.data.url } message = getResponse(message) return client.replyMessage(event.replyToken, message); })
このようにCloud Functions`からRaspberry Piへトリガーをしています。
一応、リクエストが失敗してもLINEアプリ側にはエラーメッセージを返しています。 これによってのちにFlaskサーバーが起動していなくても、何かしらのメッセージは返せるようになっています。
getXXX
のメソッドはLINE Message APIのFlex Messageのメッセージを構築しています。ここらへんはフロントエンドの話なので好みに合わせて作成しています。
本記事では、本質的なところではないので、省略します。
Flex Messageを使ったことがなければ
response = {type: 'text', text: "レスポンス"}
などのように簡単なテキストメッセージでも構いませんのでトライしてみてください。
ここまででLineBotのレスポンスはできました。
以下のようにデプロイをします。
gcloud beta functions deploy KamaruLineBot --env-vars-file .env.yml --trigger-http --region=asia-northeast1 --entry-point handler
取得したCloud FunctionsのURLはLINE DEVELOPERSのご自身のBotのWebhookURLに設定します。
試しに何かメッセージを送ってみるといいでしょう。
動作確認
試しにかまると打つと以下のようにでます。
勝手にNature Remoから取得した温度を追加してしまいました。
しかしながら、家の監視ができていると思います。
定期的にPushする
定期的にPushするにはreplyMessage
をpushMessage
にして、グループIDを入力します。
そしてCloud Schedulerで自分が見たいスパンでリクエストを送るようにしてください。
まとめ
ハードウェア代金がかかるものの、ソフトウェアによって自分好みの機能、デザインがすることができるので、やはり既製品を買うのとはわけが違うなと思いました。
これからも家電コントローラのNature Remoや、電子回路を組んで、楽しくやっていこうと思います。
最後まで、ありがとうございました!