Kekeの日記

エンジニア、読書なんでも

Raspberry PiとImgurを使って無料で自宅監視カメラLINEボットを構築する

f:id:bobchan1915:20181217053625p:plain

本記事

本記事では、実際にRaspberry Piのカメラモジュールを使って監視カメラサービスを構築してみようと思います。

完成形は以下のようにかまると呼びかけると家の画像を返信してくれるものです。すべて無料の制限内にします。

f:id:bobchan1915:20181217073525j:plain

モチベーションとしてはカメラモジュールの使い方などを解説している記事は多くありますが、実際にCloud Nativeな技術と組み合わせて解説し、運用しているものがないと思ったからです。

使うもの、環境は以下のようです。

Raspberry Pi Model B

Raspberry Pi 3 Model B V1.2 (日本製) 国内正規代理店品

Raspberry Pi 3 Model B V1.2 (日本製) 国内正規代理店品

Raspberry Pi Camera Module V2

Imgur

imgur.com

Cloud Functions

Google Cloud Functions に関するドキュメント  |  Cloud Functions  |  Google Cloud

LINE message API

LINE Developers

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の開発者の一人の解説動画があるので、安心してつけられました。

www.youtube.com

Imgur

f:id:bobchan1915:20181216231523p:plain

Imgurとは画像シェアサービスです。主にgifなどを対象としています。

Hiddenという、いわゆるPrivateな画像を保存することができ、画像一つ一つにダイレクトリンクが渡されます。

そのリンクを知れれば、誰でも簡単にアクセスできるため気をつけてください。

最初に

迷ったらドキュメントを確認してください。バージョンアップが絶えずあるため、必要に応じて参照してください。

新しいバージョン(version 3)のAPIドキュメントは以下のリンクになっています。

apidocs.imgur.com

執筆時点(2018/12/16)で最新のバージョンとなっています。

アプリケーションを登録

まず、Imgur APIにアクセスするクライアントアプリケーションを登録する必要があります。

認証にはOAuth2.0を使っています。OAuth2.0の仕組みは、以前ブログにしました。

www.1915keke.com

簡単にいうと

  • 一旦、アプリケーションでログインして短命なコードをもらう
  • そのコードで認証を済ませて、アクセストークンをもらう
  • 今度からはそのトークンをもってサービスにアクセスする

という流れです。

まず最初にアプリケーションを登録してclient_idclient_secretをもらう必要があります。

ここでPostmanを使うと便利に登録できるので以下のサイトでダウンロードします。

www.getpostman.com

そしてドキュメントの右上のRun in Postmanを押してPostman for Macを選択します。

f:id:bobchan1915:20181216131638p:plain

すると以下のようにmacのPostmanアプリケーションでCollectionsとして開かれます。

f:id:bobchan1915:20181216152601p:plain

Collectionsとは、リクエストのまとまりです。

まず、Webページの方でSubmitして以下のようにコードをもらいます。

f:id:bobchan1915:20181216153055p:plain

ここでAuthorization callback URLにはhttps://www.getpostman.com/oauth2/callbackと入力ください。

あとはEmailを埋めるだけです。

そしてもらったclient_idclient_secretをもってして、Postmanの方で以下のようにOAuth2.0を設定します。

まずAccount-> Generate Access TokenからAuthorizationタブを選択します。

そして、Generate Access Tokenを押して以下のようにフォームを埋めます。

f:id:bobchan1915:20181216232242p:plain

そしてRequest Tokenを押すと以下のようになります。

f:id:bobchan1915:20181216153705p:plain

ここでログインするとAccessトークンを取得できます。

これでUse Tokenを選択してください。次から簡単にアクセストークンが使えるようになります。

f:id:bobchan1915:20181216233641p:plain

ここまででアクセストークンを取得することができました。

適当に画像をPostmanを使ってUploadしてみます。

以下のようにImage-> Image Uploadで使ってみます。ImageはファイルをドラッグするとBodyにつけることができます。

f:id:bobchan1915:20181216155600p:plain

無事、Hidden(Private)な画像をアップロードができました。

f:id:bobchan1915:20181216155952p:plain

このような感じで、次はスクリプトからImgurに画像をストアしていきます。

Pythonコードを書く

このセクションはアーキテクチャ図の以下の箇所に対応しています。

f:id:bobchan1915:20181217054056p:plain

1. 静止画を撮影するサーバーを立てる

f:id:bobchan1915:20181217054146p:plain

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へアップロードするスクリプト

f:id:bobchan1915:20181217054240p:plain

ここでは以下のように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を使います。

ngrok.com

以下のようにインストールすることができます。

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のサーバーを書く

f:id:bobchan1915:20181217053807p:plain

今回はコストと手軽さを考えて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に設定します。

f:id:bobchan1915:20181217063134p:plain

試しに何かメッセージを送ってみるといいでしょう。

動作確認

試しにかまると打つと以下のようにでます。

f:id:bobchan1915:20181217073525j:plain

勝手にNature Remoから取得した温度を追加してしまいました。

しかしながら、家の監視ができていると思います。

定期的にPushする

f:id:bobchan1915:20181217074027p:plain

定期的にPushするにはreplyMessagepushMessageにして、グループIDを入力します。

そしてCloud Schedulerで自分が見たいスパンでリクエストを送るようにしてください。

まとめ

ハードウェア代金がかかるものの、ソフトウェアによって自分好みの機能、デザインがすることができるので、やはり既製品を買うのとはわけが違うなと思いました。

これからも家電コントローラのNature Remoや、電子回路を組んで、楽しくやっていこうと思います。

最後まで、ありがとうございました!