2
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ポストに届いた荷物の様子を遠隔でモニタしたい

Last updated at Posted at 2024-09-21

はじめに

我が家のポストは、玄関から距離のあるところに設置してあり、毎度確認に行くのが億劫だ。ポスト周辺は、コンセントもなく住居からのWi-Fi電波も届かないため、安易にACから電源を取りつつWi-Fiで繋がるNW型の監視カメラを置いておけばよいという訳にもいかない。今回は、エッジ側で撮影した画像をLTE回線でAWS S3へアップロードし、アップロードされた画像を表示するWebを構えることで、安価で長時間連続稼働が可能なポストモニターを作製した。

完成物:Webから直近のポストの様子を確認できる

image.png

作ったものはこちら

1. 構成

回路は2系統に分離した。「撮影とAWSへのデータ送信に特化した消費電力の大きい部分」と「長時間待ち受けをする低消費電力の部分」の2つ。回路を分けることでシステム全体の電力消費を最小限に抑え、連続稼働時間を稼ぐ。連続稼働時間が長くなればなるほどバッテリ交換・充電の頻度を減らすことができる。

システム状態遷移

image.png

2.つくったもの

2.1 エッジ側の機器

防水効果も狙って、すべてパッキン付きの食品保存容器に押し込んだ。カメラはRaspiberryPiの純正カメラを使用した。バッテリは単三型NiH電池を8本直列に並べる形とした。電源電圧によらず安定した5Vを作り出すため、昇降圧どちらも可能な可能なDCコンバータ(SparkFunのCOM-15208)を利用した。

  • 実物
    IMG_3432.jpg
    ※検証用に乾電池を使っているが、本番ではNiH電池を使用

2.2 エッジ側 RaspberryPi Pico

基本DeepSleepで寝ている状態とし、昼間は3時間毎、夜間は12時間スパンでDeepSleepから目覚め、撮影するための回路のリレーをONする。(*1 DeepSleep状態にすれば数mAへ電流を減らすことができる) machine.deepsleep()の引数には1時間以上の引数を設定できなく、どうやって毎時カウントアップさせようか悩んだが、ファイルシステムを使って(Read/Write)、スリープ時間をカウントできるようにした。(DeepSleepではCPUが停止してしまうため、RAMを使ったカウントが実装できない)。撮影が終了後、リレーをOFFし、DeepSleepへ入る。Picoを動かすためのコードはMicroPythonで書いた。

*1 ディープスリープでRaspberry Pi Pico Wを低電力化する
https://msr-r.net/raspi-picow-deepsleep/

main.py
import time

import machine
from machine import ADC

while(1):
    startTime = time.ticks_ms()
    Out_wakeup = machine.Pin(0, machine.Pin.OUT)
    Out_wakeup.value(0)
    checkIsPowerOnReset = (machine.reset_cause() == machine.PWRON_RESET)
    filePath = "sleepTimeFile"
    with open(filePath) as f:
        s = f.read()
        if(checkIsPowerOnReset):
            print("sleepTimeFile",s)
        deepSleepHourCount_r = int((s.split(","))[0])
        deepSleepHour_r = int((s.split(","))[1])
    
    # wakeup mode
    if ((checkIsPowerOnReset) or (deepSleepHourCount_r>=deepSleepHour_r)):
        Out_wakeup.value(1)
        Adc_isTaskEnd =ADC(0)
        Adc_isNightMode = ADC(1)
        if(checkIsPowerOnReset):
            print("[log]pico wakeup!")
        for i in range(10):
            time.sleep(1)
            if(checkIsPowerOnReset):
                Adc_isTaskEndValue = Adc_isTaskEnd.read_u16()*3.3/65535
                Adc_isNightModeValue = Adc_isNightMode.read_u16()*3.3/65535
                print("Adc_isTaskEndValue",Adc_isTaskEndValue)
                print("Adc_isNightModeValue",Adc_isNightModeValue)
            
        for i in range(360):
            time.sleep(0.5)
            Adc_isTaskEndValue = Adc_isTaskEnd.read_u16()*3.3/65535
            if(checkIsPowerOnReset):
                print("Adc_isTaskEndValue",Adc_isTaskEndValue)
            if (Adc_isTaskEndValue > 2.6):
                break

        print("[log]shot finished!")
        Adc_isNightModeValue = Adc_isNightMode.read_u16()*3.3/65535

        if (Adc_isNightModeValue > 2.6):
            deepSleepHour = 12
        else:
            deepSleepHour = 3
        Out_wakeup.value(0)
        if(checkIsPowerOnReset):
            print("Adc_isNightModeValue",Adc_isNightModeValue)
            print("deepSleepTime",deepSleepHour)
            
        deepSleepHour_w = deepSleepHour
        deepSleepHourCount_w = 1

    # deepsleep mode
    else:
        deepSleepHourCount_w = deepSleepHourCount_r + 1
        deepSleepHour_w = deepSleepHour_r        

    wirteObj = str(deepSleepHourCount_w) + "," + str(deepSleepHour_w) 
    if(checkIsPowerOnReset):
        print("wirteObj",wirteObj)
    with open(filePath, mode='w') as f:
        f.write(wirteObj)
    if(checkIsPowerOnReset):
        time.sleep(5)
    deltaTime = time.ticks_diff(time.ticks_ms(), startTime)
    machine.deepsleep(60*60*1000-deltaTime)

2.3 エッジ側 RaspberryPi Zero 2W

LTE端末の起動に時間がかかるため、写真撮影を行い、NW接続の確立が確認出来たらAPI GatewayにRESTでデータを送る構成とした。画像はbodyでバイナリデータとして送信する。Pico側にはRTCの機能が無いため、Pico単体では、昼夜の判定が難しい。NWにつながるZeroの特性を生かして、Zeroのdatetime.now()による昼夜判定の結果をGPIO経由でPiCoのADCに送信し、GPIOの電圧に応じてDeepSleep時間を決められる構成とした。すべての動作が完了後、PicoのADCとつながったGPIOの電圧を立て、ジョブ完了を通知するようにした。ジョブ完了後、Picoからリレーが切られ、Zeroは電源が強制カットされる。Pythonコード自体は毎度Zeroが立ち上がる度にsystemdでトリガーされるようにした。

shot.py
#!/usr/bin/python3

import base64
import datetime
import json
import time

import gpiozero
import requests
from libcamera import controls
from picamera2 import Picamera2

pin_shutdwn = gpiozero.DigitalOutputDevice(pin=17)
pin_daynight = gpiozero.DigitalOutputDevice(pin=27)
pin_camled = gpiozero.DigitalOutputDevice(pin=19)
pin_shutdwn.off()
pin_daynight.off()
pin_camled.off()

##camera
pin_camled.on()
picam2 = Picamera2()
sizeH = int(2304)
sizeW = int(1296)
imagePath = "/home/***/PostMonitor/out.jpg"


preview_config = picam2.create_preview_configuration(main={"size": (sizeH, sizeW)})
picam2.configure(preview_config)
picam2.start()

picam2.set_controls({"AfMode":controls.AfModeEnum.Continuous})
time.sleep(2)
picam2.capture_file(imagePath)
picam2.close()
print("[log]shot success!")
pin_camled.off()

##Nwcheck
print("[log]shot.py start!")
while (True):
    statusCode = 400
    try:
        res = requests.get("https://google.com",timeout=(5,10))
        statusCode = res.status_code
    except:
        pass

    if (statusCode == 200):
        print("[log]network check end!")
        break
    else:
        time.sleep(0.1)

##send
data = open(imagePath, 'rb').read()
encoded_data = base64.b64encode(data).decode('utf-8')
wbData = base64.b64decode(encoded_data)
url = 'https://***.execute-api.ap-northeast-1.amazonaws.com/prod/ImageUpload'
payload = {'file': encoded_data, 'extension': 'jpg'}
statusCode = 400
for i in range(3):
    try:
        response = requests.post(url, data=json.dumps(payload),timeout=(5,10))
        statusCode = response.status_code
        if (statusCode == 200):
            print("[log]upload success!")
            break
    except:
        pass

dt_now = datetime.datetime.now()
if ((dt_now.hour>20) or (dt_now.hour<9)):
    pin_daynight.on()
    print("[log]night! hour:",dt_now.hour)
else:
    print("[log]daytime!")
    pin_daynight.off()
pin_shutdwn.on()
while(True):
    time.sleep(5)

2.4 Lambda

API Gatewayからバイナリデータをエンコードし、画像ファイルをS3へデータを保存する単純な処理とした。

lambda_function.py
import base64
import datetime
import json
import uuid

import boto3

s3 = boto3.client('s3')
    

def lambda_handler(event, context):
    try:
        fileExtension = event["extension"]
        fileName = str(uuid.uuid4()) + "." + fileExtension
        path_w = "/tmp/" + fileName
        
        wbData = base64.b64decode(event["file"])
        
        f = open(path_w, 'wb')
        f.write(wbData)
        f.close()
        s3.upload_file(path_w, '***', 'NewImage.jpg')
        
        # TODO implement
        return {
            'statusCode': 200,
            'body': json.dumps('Hello from Lambda!')
        }
    except:
        return {
            'statusCode': 400,
            'body': json.dumps('bad request!')
        }

最後に

ありものを使って突貫的に作ってしまい、配線がぐちゃぐちゃである。LTE端末、RaspiZero周りが電流を食うため、ジャンパ線を束ねることで電流を流せるようにしており、配線が汚い。このため電力効率も良くない気がする。PCB化するなど、もう少し回路はレベルアップさせたい。

なお、実際の運用では、電池の電圧もモニタしている。記事投稿現在、13日連続で稼働できている。電圧を見るともう1週間ぐらいは稼働できそうである。電池の数や更新頻度などを見直しながら、電池交換が月イチで済むような稼働時間を目指したい。

電池電圧の変化(単三電池1本あたりの電圧)

image.png

2
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?