Raspberry Pi + AWS + LINENotifyAPIでサーバーレスな植物の成長を毎日観察できるシステムを作ってみた

Raspberry Pi + AWS + LINENotifyAPIでサーバーレスな植物の成長を毎日観察できるシステムを作ってみた

目次

    はじめに

    はじめまして、アジアクエストの伏貫と申します。 普段はJava,PHP,JavaScriptなどを使いフルスタックに開発をやっています。 さて今回、以前より注目されているIT×農業につながる試作を行ったので記事にしてみました。

    ゴール

    ・Raspberry Pi + カメラモジュールで撮影した野菜の画像をタイムラプスにして毎日LINEで動画が送られてくるシステム
    を作成することが今回の目的です。

    plant_monitoring

    必要なもの

    ・Raspberry Pi
    カメラモジュールを付けられるものが必須です。
    自分は「ResberryPI 3 A+」を使いました。

    ・マウス/キーボード
    Raspberry Piの初期設定時、必要になります。
    ノートPCなどしか持っておらず、外部キーボードやマウスが無い方などは注意です。

    ・カメラモジュール
    Raspberry Piに付けられる簡易カメラです。
    例えばこのようなものです。
    https://www.amazon.co.jp/dp/B08Q34FKFY

    ・AWSアカウント
    画像の保存、動画の作成、LINEへの送信などをAWS上で行います。
    https://aws.amazon.com/jp/

    ・LINE Developerアカウント
    LINEでBOTを作る際、登録が必要となります。
    https://developers.line.biz/ja/

    ※今回、事前にLINEでBOTを作成しておく必要があります。
    詳しくは以下の記事を参照してください。(私が執筆しました)
     https://qiita.com/oimo23/items/11a0c38bbe1e297fb611#collision-line%E3%81%AEdevelopers%E8%A8%AD%E5%AE%9A%E7%B7%A8

    ・監視する植物
    自分はきゅうりにしました。

    この記事で扱わないもの

    ・Raspberry Piの基本的な使い方
    ・Raspberry Piの初期設定
    ・LINE BOTの作成の仕方
    ・AWSアカウントの作成方法・基本的な使い方

    この辺りは長くなりそうなのと、ググると情報がたくさん出てくるので割愛します。

    Raspberry Pi → S3への画像アップロード

    Raspberry Piで、PythonとPiCameraというライブラリを用いて、 写真を撮影するコードを書きます。

    まず、必要なライブラリをインストールします。

    $ sudo apt-get update
    $ pip3 install boto3 picamera

    Pythonでカメラモジュールを使って写真を撮るコードを書きます。 具体的には以下のようなものになります。

    import os
    import time
    from picamera import PiCamera
    import boto3

    def take_photo():
    if not os.path.exists('images'):
    os.makedirs('images')

    timestamp = time.strftime("%Y%m%d-%H%M%S")
    date = time.strftime("%Y%m%d")
    img_path = f'images/{timestamp}.jpg'

    with PiCamera() as camera:
    camera.resolution = (1024, 768)
    camera.start_preview()
    time.sleep(2) # Camera warm-up time
    camera.capture(img_path)
    print(f'Photo taken and saved as {img_path}')
    upload_to_s3(img_path, date)

    def upload_to_s3(file_path, date):
    # name of the bucket
    BUCKET_NAME = '***'

    # name of the file on S3, set as 'images/YYYYMMDD/{timestamp}.jpg'
    S3_NAME = f'images/{date}/' + os.path.basename(file_path)

    s3 = boto3.client('s3')
    s3.upload_file(file_path, BUCKET_NAME, S3_NAME)

    print(f'Uploaded {file_path} to {BUCKET_NAME}/{S3_NAME}')

    if __name__ == '__main__':
    take_photo()

    ここで「BUCKET_NAME」に指定したのと同名のS3バケットを自分のAWSアカウントで作成してください。

    また、Pythonのコードから上記のS3バケットが置いてある自身のAWSアカウントにアクセスするには、
    基本的にはAWSのシークレットキーが必要になります。

    しかし、このコードの中にはシークレットキーに関する記述を書いていません。
    これは「~/.aws/credentials」内の「default」という名前のプロファイルを事前に設定することで、 自動でそれを読みに行ってくれるというAWS SDKの仕組みがあり省略することが可能となっています。

    私はこの設定を事前に行なっているため、このコードで自分のバケットにアクセス出来ます。

    詳しくはAWSの公式ドキュメントをご覧ください。
    https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/cli-configure-files.html

    コード内にシークレットキーをハードコードする方法もありますが、 間違ってGitHubにpushしてしまったりすると危ないのでオススメしません。

    cronにより定期的に実行する

    cronを使ってこのプログラムを8:00~19:00まで15分ごとに実行することにします。

    crontab -e

    するとcrontabの設定ファイルが開かれるので、
    末尾に以下のような形で追記します。

    ...
    */15 8-19 * * * cd ${path/to/code} && /usr/bin/python3 take_photo.py >> /home/${ユーザー名}/cron.log 2>&1

    ${path/to/code}と${ユーザー名}は適宜読み替えてください。

    S3にアップされた画像をタイムラプス動画にする

    問題なく上記のcronが動くと、S3バケット内の「images/YYYYMMDD/」内に8:00~19:00の間、
    15分ごとに画像が追加されていきます。

    この後、方針としては以下のLambda関数2つを作成します。

    1.その日のS3内(images/YYYYMMDD/**)の画像をつなげてタイムラプス動画を作成する関数
    2.タイムラプス動画をLINEに送信する関数

    作成時は以下のような流れになります。
    なお、ここからはRaspberry Piではなく自分の作業用のPCで構いません。

    今回は、ServerlessFrameworkを使用しました。
    Lambda/EventBridge/API Gateway/DynamoDBなどのサーバーレスアーキテクチャの構築をコードベースですることが出来ます。(今回使うのはLambdaとEventBridgeだけです)

    まだ入っていない場合はインストールします。

    npm install --location=global serverless

    プロジェクトの作成

    serverless create --template aws-python3 --path plant-monitoring-generate-movie

    すると、色々とファイルが自動的に作成されるはずです。
    多分、こんな感じになります。

    handler.py
    node_modules/
    package.json
    package-lock.json
    serverless.yml

    続いて、
    「serverless-python-requirements」をインストール。

    $ cd generate-movie
    $ serverless plugin install -n serverless-python-requirements

    これを入れると、requirements.txt にインストールしたいライブラリを書くことでデプロイ時にまとめて入れてくれます。
    Lambdaでライブラリを使用するのが意外と面倒なので助かります。

    requirements.txt

    opencv-python-headless==4.6.0.66
    numpy==1.23.5

    serveless.ymlを編集して、定期的にLambda関数が自動実行されるようにします。
    また、環境変数の設定もここでします。

    service: plant-monitoring-generate-movie

    frameworkVersion: '3'

    provider:
    name: aws
    runtime: python3.10
    region: ap-northeast-1
    timeout: 180
    environment:
    BUCKET_NAME: ${env:BUCKET_NAME}

    iam:
    role:
    statements:
    - Effect: Allow
    Action:
    - s3:PutObject
    - s3:GetObject
    - s3:DeleteObject
    - s3:ListBucket
    Resource:
    - arn:aws:s3:::${self:provider.environment.BUCKET_NAME}
    - arn:aws:s3:::${self:provider.environment.BUCKET_NAME}/*
    - Effect: Allow
    Action:
    - logs:CreateLogGroup
    - logs:CreateLogStream
    - logs:PutLogEvents
    Resource:
    - '*'

    functions:
    create:
    handler: handler.lambda_handler
    events:
    - schedule: cron(50 9 * * ? *) # JST 18:50 (UTC 09:50) daily

    plugins:
    - serverless-python-requirements

    custom:
    pythonRequirements:
    dockerizePip: non-linux
    noDeploy: []

    環境変数の内容を定義しておきます。

    export BUCKET_NAME=${自分のバケット名}

    Serverless Frameworkプロジェクト内にある handler.pyを上書きしてコードを書いていきます。

    import boto3
    import os
    import tempfile
    import cv2
    from datetime import datetime

    def create_video(bucket_name, folder_name, video_name, frame_rate=6):
    s3 = boto3.resource('s3')
    bucket = s3.Bucket(bucket_name)

    # S3からオブジェクトのリストを取得
    objects = bucket.objects.filter(Prefix=folder_name)

    image_files = []
    for obj in objects:
    # .jpgで終わるものののみを処理
    if obj.key.endswith(".jpg"):
    tmp = tempfile.NamedTemporaryFile(delete=False)
    bucket.download_file(obj.key, tmp.name)
    image_files.append(tmp.name)

    if not image_files:
    print("No image files found.")
    return

    frame = cv2.imread(image_files[0])
    height, width, channels = frame.shape

    # コーデックを決定し VideoWriter オブジェクトを作成する
    video_name_path = os.path.join('/tmp', video_name)
    video = cv2.VideoWriter(video_name_path, cv2.VideoWriter_fourcc(*'MP4V'), frame_rate, (width, height))

    for image_file in image_files:
    frame = cv2.imread(image_file)
    video.write(frame) # Write out frame to video

    video.release()

    print(f"Video saved as {video_name}")

    # VideoをS3へアップロード
    bucket.upload_file(video_name_path, f'{folder_name}/{video_name}')
    print(f"Video uploaded to S3 as {folder_name}/{video_name}")

    def lambda_handler(event, context):
    today = datetime.now().strftime('%Y%m%d')
    folder_name = f'images/{today}'
    bucket_name = os.environ['BUCKET_NAME']

    create_video(bucket_name, folder_name, 'output.mp4')

    作成できたらデプロイします。

    serverless deploy

    正常に終了すると、AWSでLambdaを確認すると関数が増えています。

    同じ流れで「2.タイムラプス動画をLINEに送信する関数」も作成します。
    ほぼ同じ流れなのでコマンドは割愛します。

    requirements.txt

    requests==2.29.0
    service: send-movie-to-line

    frameworkVersion: '3'

    provider:
    name: aws
    runtime: python3.10
    region: ap-northeast-1
    timeout: 180
    environment:
    BUCKET_NAME: ${env:BUCKET_NAME}
    LINE_ACCESS_TOKEN: ${env:LINE_ACCESS_TOKEN}
    LINE_TO: ${env:LINE_ACCESS_TOKEN}

    iam:
    role:
    statements:
    - Effect: Allow
    Action:
    - s3:PutObject
    - s3:GetObject
    - s3:DeleteObject
    - s3:ListBucket
    Resource:
    - arn:aws:s3:::${self:provider.environment.BUCKET_NAME}
    - arn:aws:s3:::${self:provider.environment.BUCKET_NAME}/*
    - Effect: Allow
    Action:
    - logs:CreateLogGroup
    - logs:CreateLogStream
    - logs:PutLogEvents
    Resource:
    - '*'

    functions:
    create:
    handler: handler.lambda_handler
    events:
    - schedule: cron(0 10 * * ? *) # JST 19:00 (UTC 10:00)

    plugins:
    - serverless-python-requirements

    custom:
    pythonRequirements:
    dockerizePip: non-linux
    noDeploy: []
    import boto3
    import requests
    import json
    from datetime import datetime

    def lambda_handler(event, context):
    url = "https://api.line.me/v2/bot/message/push"
    access_token = os.environ['LINE_ACCESS_TOKEN']
    line_to = os.environ['LINE_TO']

    # Generate a presigned URL for the S3 object
    s3 = boto3.client('s3')
    today = datetime.now().strftime('%Y%m%d')
    object_name = f'images/{today}/output.mp4'
    bucket_name = os.environ['BUCKET_NAME']
    response = s3.generate_presigned_url('get_object',
    Params={'Bucket': bucket_name,
    'Key': object_name},
    ExpiresIn=604800) # 7 days in seconds

    headers = {
    'Content-Type': 'application/json; charset=UTF-8',
    'Authorization': 'Bearer {}'.format(access_token),
    }

    # Create a Flex Message with video preview
    flex_message = {
    "type": "flex",
    "altText": "動画プレビュー",
    "contents": {
    "type": "bubble",
    "hero": {
    "type": "video",
    "url": response,
    "previewUrl": "https://example.com/video_preview.jpg", # サムネイル画像が欲しい時は正しいURLに
    "altContent": {
    "type": "image",
    "size": "full",
    "aspectRatio": "16:9",
    "aspectMode": "cover",
    "url": "https://example.com/video_preview.jpg", # サムネイル画像が欲しい時は正しいURLに
    },
    "aspectRatio": "16:9",
    },
    "body": {
    "type": "box",
    "layout": "vertical",
    "contents": [
    {
    "type": "text",
    "text": "新しい動画が利用可能です",
    "weight": "bold",
    "size": "md",
    "wrap": True
    }
    ]
    }
    }
    }

    data = {
    'to': line_to,
    'messages': [flex_message],
    }

    response = requests.post(url, headers=headers, data=json.dumps(data))

    # check the response
    if response.status_code == 200:
    return {
    'statusCode': 200,
    'body': json.dumps('Request was successful')
    }
    else:
    return {
    'statusCode': response.status_code,
    'body': response.text # レスポンス本文(エラーメッセージなど)を返す
    }

    push APIを使用することで特定の相手にBOTからメッセージを送ることが可能になります。
    冒頭でも紹介しましたが、LINEのBOTの作成とアクセストークン、自分のIDが必要になります。

    こちらを参照。

    結果

    冒頭の動画の繰り返しですが、 毎日指定時刻に野菜の成長が動画で届くようになりました。

    plant_monitoring

    さいごに

    Raspberry Piは手頃な価格で手に入り、色々な実験が出来て面白いです。
    特にカメラと連携すると色々と夢が広がるなと思いました。

    また、個人ではコストの面からあまりAWSサービスを存分に活用できないこともあると思いますが、
    Lambdaなどサーバーレスな構成であればそこまで気にせずに色々な実験や個人開発が出来るのがいいですね。

    ちなみにソースコードはこちらに置いてあります。

    最後までお読みいただきありがとうございました。

    アジアクエスト株式会社では一緒に働いていただける方を募集しています。
    興味のある方は以下のURLを御覧ください。