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

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

#### 完成物：Webから直近のポストの様子を確認できる
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/54100/5d790aa5-66ff-637d-8b65-5387deae11aa.png)

作ったものはこちら

https://github.com/bd8z/postMonitorOSS

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

#### システム状態遷移
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/54100/d529b54c-1521-c30b-c440-f226e5a01613.png)



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

- 実物
![IMG_3432.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/54100/7f421382-2bb2-63d3-61e5-ea83f9afb1b6.jpeg)
※検証用に乾電池を使っているが、本番では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/
```python: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でトリガーされるようにした。

```python: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へデータを保存する単純な処理とした。

```python: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/54100/fbabc449-06d6-33ae-53c7-615e286f93c7.png)