はじめに
我が家のポストは、玄関から距離のあるところに設置してあり、毎度確認に行くのが億劫だ。ポスト周辺は、コンセントもなく住居からのWi-Fi電波も届かないため、安易にACから電源を取りつつWi-Fiで繋がるNW型の監視カメラを置いておけばよいという訳にもいかない。今回は、エッジ側で撮影した画像をLTE回線でAWS S3へアップロードし、アップロードされた画像を表示するWebを構えることで、安価で長時間連続稼働が可能なポストモニターを作製した。
完成物:Webから直近のポストの様子を確認できる
作ったものはこちら
1. 構成
回路は2系統に分離した。「撮影とAWSへのデータ送信に特化した消費電力の大きい部分」と「長時間待ち受けをする低消費電力の部分」の2つ。回路を分けることでシステム全体の電力消費を最小限に抑え、連続稼働時間を稼ぐ。連続稼働時間が長くなればなるほどバッテリ交換・充電の頻度を減らすことができる。
システム状態遷移
2.つくったもの
2.1 エッジ側の機器
防水効果も狙って、すべてパッキン付きの食品保存容器に押し込んだ。カメラはRaspiberryPiの純正カメラを使用した。バッテリは単三型NiH電池を8本直列に並べる形とした。電源電圧によらず安定した5Vを作り出すため、昇降圧どちらも可能な可能なDCコンバータ(SparkFunのCOM-15208)を利用した。
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/
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でトリガーされるようにした。
#!/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へデータを保存する単純な処理とした。
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週間ぐらいは稼働できそうである。電池の数や更新頻度などを見直しながら、電池交換が月イチで済むような稼働時間を目指したい。