バッテリレス超簡易監視カメラ
概要
SPRESENSEの省エネ特性を利用してバッテリを用いない簡易の監視カメラを製作しました。
SPRESENSEカメラボードで定期的に画像撮影し、LTE-M 通信モジュールで携帯通信網を介してサーバに画像を送信します。
システム全体を省電力で実現できたためソーラパネルのみで給電します。
構成
部品
- SPRESENSE
- カメラボード CXD5602PWBCAM1
- 電気二重層コンデンサ
- LTE-M 通信モジュール SIM7080G
- データSIM
- 昇降圧DC-DCコンバータ SC8721
- ソーラパネル
画像送信動作
撮影した画像の送信には低消費電力であるSIM7080G搭載のLTE-Mモジュールを採用しました。
データSIMを契約して携帯通信網を用いて通信します。
LTE-MモジュールはATコマンドで各種設定や制御が可能です。
LTE-Mモジュールのhttp通信を使用してサーバに撮影したJPEG画像 (QVGA : 320×240)を送信します。
1度に送信できるデータ量に限りがあるため、ここでは画像を4KBづつに分割して送信しています。
サーバ側で送信されたデータを結合して画像を保存します。
消費電力
ソーラパネルの代わりに安定化電源を接続して本監視カメラの消費電力を測定しました。
1時間当たり5回画像を送信して消費電力は116mWhと非常に低い結果となりました。
撮影と撮影の間はSPRESENSEをスリープさせて消費を抑えています。
LTE-MモジュールもSPRESENSEの3.3V出力から給電しているのでスリープ時には停止します。
消費電流のピークはデータ送信時で入力電圧が10Vで約140mA、5.5Vで約220mAでした。
ソーラパネル
本監視カメラはソーラパネルとSPRESENSE電源入力の間に昇降圧DC-DCコンバータを使用しています。ソーラパネルから供給される電圧を4Vに変換してSPRESENSEに給電します。
使用したコンバータ SC8721は入力電圧範囲が2.7V~22Vと非常に広く、幅広い種類のソーラパネルの採用が可能です。
カメラシステムの消費電力は非常に小さいためデータ送信時のピーク電流をまかなうことができれば小さなサイズのソーラパネルでも駆動可能です。
1W~5Wの色々なパネルを購入して実験してみました。
1W ソーラは標準電圧:5.5 V、標準電流:170 mAと若干能力が足りずSPRESENSEは動作しましたがLTE-Mモジュールの通信までは駆動できませんでした。
2Wソーラでは問題なく画像送信動作を確認することができました。
この超簡易監視カメラは2W以上のソーラパネルを用いれば動作可能で、それ以上の出力のパネルを用いれば多少日照が弱くても駆動できることが分かりました。
撮影画像
撮影画像はLTE-Mモジュールが大きなデータを取り扱えないことと消費電力低減を考慮しQVGA(320×240)サイズとしました。
1枚画像を送るのに1分と少々ほどかかり10分スリープ後に起床して撮影・送信を繰り返します。
1時間に約5枚の画像が送られてきます。
ソースコード
SPRESENSE超簡易監視カメラ
#include <LowPower.h>
#include <RTC.h>
#include <Camera.h>
int state = 100;
int retry = 5;
String reply;
unsigned long startTime;
CamImage img;
byte pic[100000];
int n;
int cnt = 0, last;
void setup() {
LowPower.clockMode(CLOCK_MODE_32MHz);
Serial.begin(115200); //シリアルモニタ用
Serial2.begin(230400);
Serial.println("Prepare camera");
theCamera.begin();
Serial.println("Set Auto white balance parameter");
theCamera.setAutoWhiteBalanceMode(CAM_WHITE_BALANCE_DAYLIGHT);
Serial.println("Set still picture format");
theCamera.setStillPictureImageFormat(
CAM_IMGSIZE_QVGA_H,
CAM_IMGSIZE_QVGA_V,
CAM_IMAGE_PIX_FMT_JPG);
RTC.begin();
LowPower.begin();
pinMode(21, OUTPUT);
digitalWrite(21, HIGH);
Serial.println("start");
}
void loop() {
int num = 0;
Serial.println(state);
//ATコマンド
switch (state) {
case 0: //モジュール-SPRESENSE間の通信確認
Serial2.write("AT\r\n");
break;
case 2: //モジュール通信ON
Serial2.write("AT+CNACT=0,1\r\n");
break;
case 30:
if(cnt == 0) Serial2.write("AT\r\n");
else Serial2.write("AT+SHDISC\r\n");
break;
case 31: //HTTP設定
Serial2.write("AT+SHCONF=\"URL\",\"[サーバURL]\"\r\n");
break;
case 32:
Serial2.write("AT+SHCONF=\"BODYLEN\",4096\r\n");
break;
case 33:
Serial2.write("AT+SHCONF=\"HEADERLEN\",350\r\n");
break;
case 34:
Serial2.write("AT+SHCONN\r\n");
break;
case 4: //HTTP接続確認
Serial2.write("AT+SHSTATE?\r\n");
break;
case 5:
Serial2.write("AT+SHAHEAD=\"Content-Type\",\"image/jpeg\"\r\n");
break;
case 6: //画像 4KB分セット
if(n == cnt){
Serial2.print("AT+SHBOD=");
Serial2.print(last);
Serial2.print(",10000\r\n");
}else{
Serial2.write("AT+SHBOD=4000,10000\r\n");
}
break;
case 7: //画像データPUTコマンド送信
Serial2.write("AT+SHREQ=\"/cam\", 3\r\n");
break;
case 80: //画像送信終了コマンド
if(cnt == 0) Serial2.write("AT\r\n");
else Serial2.write("AT+SHDISC\r\n");
break;
case 81:
Serial2.write("AT+SHCONF=\"URL\",\"[サーバURL]\"\r\n");
break;
case 82:
Serial2.write("AT+SHCONF=\"BODYLEN\",4096\r\n");
break;
case 83:
Serial2.write("AT+SHCONF=\"HEADERLEN\",350\r\n");
break;
case 84:
Serial2.write("AT+SHCONN\r\n");
break;
case 9: //画像送信終了 GETコマンド送信
Serial2.write("AT+SHREQ=\"/camend\", 1\r\n");
break;
}
if(state == 100){ //LTEモジュール起動
delay(500);
digitalWrite(21, LOW);
delay(500);
digitalWrite(21, HIGH);
startTime = millis();
state = 0;
}else if(state == 0){ //AT導通確認
while(1){
reply = "";
if(Serial2.available()) {
reply = Serial2.readStringUntil('\n');
}
num++;
if(reply.indexOf("OK") >= 0) {
Serial.println(reply);
state = 1;
break;
}else if(num >= retry){
break;
}
delay(300);
}
}else if(state == 1){
Serial2.write("AT+CNACT=0,1\r\n"); //通信開始
//カメラ撮影
Serial.println("call takePicture()");
img = theCamera.takePicture();
Serial.print("ImgSize: "); Serial.println(img.getImgSize());
n = img.getImgSize() / 4000;
Serial.print("n: "); Serial.println(n + 1);
last = img.getImgSize() - 4000 * n;
Serial.print("last: "); Serial.println(last);
memcpy(pic, img.getImgBuff(), img.getImgSize());
state = 2;
}else if(state == 2){ //通信開始確認
while(1){
reply = "";
if(Serial2.available()) {
reply = Serial2.readStringUntil('\n');
}
num++;
if(reply.indexOf(",ACTIVE") >= 0) {
Serial.println(reply);
state = 30;
break;
}else if(num >= retry){
break;
}
delay(300);
}
}else if(state == 30 || state == 31 || state == 32 || state == 33 || state == 34){ //HTTP設定
while(1){
reply = "";
if(Serial2.available()) {
reply = Serial2.readStringUntil('\n');
}
num++;
if(reply.indexOf("OK") >= 0) {
Serial.println(reply);
if(state == 30) state = 31;
else if(state == 31) state = 32;
else if(state == 32) state = 33;
else if(state == 33) state = 34;
else if(state == 34) state = 4;
break;
}else if(num >= retry){
break;
}
delay(300);
}
}else if(state == 4){ //HTTPステート確認
while(1){
reply = "";
if(Serial2.available()) {
reply = Serial2.readStringUntil('\n');
}
num++;
if(reply.indexOf("SHSTATE: 1") >= 0) {
Serial.println(reply);
state = 5;
break;
}else if(num >= retry){
break;
}
delay(300);
}
}else if(state == 5){ //HTTP body設定
while(1){
reply = "";
if(Serial2.available()) {
reply = Serial2.readStringUntil('\n');
}
num++;
if(reply.indexOf("OK") >= 0) {
Serial.println(reply);
state = 6;
break;
}else if(num >= retry){
break;
}
delay(300);
}
}else if(state == 6){ //画像送信
if(n == cnt){
for(int i = 4000 * cnt ; i <= img.getImgSize(); i++){
Serial2.write(pic[i]);
}
}else{
for(int i = 4000 * cnt ; i < (4000 + 4000 * cnt); i++){
Serial2.write(pic[i]);
}
}
delay(1000);
state = 7;
}else if(state == 7){ //HTTP POSTリクエスト
while(1){
reply = "";
if(Serial2.available()) {
reply = Serial2.readStringUntil('\n');
}
num++;
if(reply.indexOf("SHREQ: \"POST\",200") >= 0) {
Serial.println(reply);
if(cnt < n) state = 30;
else state = 80;
cnt++;
break;
}else if(num >= retry * 3){
break;
}
delay(300);
}
}else if(state == 80 || state == 81 || state == 82 || state == 83 || state == 84){ //終了信号用HTTP設定
while(1){
reply = "";
if(Serial2.available()) {
reply = Serial2.readStringUntil('\n');
}
num++;
if(reply.indexOf("OK") >= 0) {
Serial.println(reply);
if(state == 80) state = 81;
else if(state == 81) state = 82;
else if(state == 82) state = 83;
else if(state == 83) state = 84;
else if(state == 84) state = 9;
break;
}else if(num >= retry){
break;
}
delay(300);
}
}else if(state == 9){ //画像送信終了信号HTTP GETリクエスト
while(1){
reply = "";
if(Serial2.available()) {
reply = Serial2.readStringUntil('\n');
}
num++;
if(reply.indexOf("SHREQ: \"GET\",200") >= 0) {
Serial.println(reply);
state = 10;
cnt++;
break;
}else if(num >= retry){
break;
}
delay(300);
}
}else if(state == 10){ //通信後スリープ
state = 100;
cnt = 0;
//sleep
Serial.println("sleep");
LowPower.deepSleep(600); //10分スリープ
}
//エラー処理 3分リミット
if(millis() - startTime > 300000){
state = 10;
Serial.println("time limit");
}
//delay(1000);
}
SPRESENSEのCPUクロックを32MHzにおとして消費電力を低下させています。
SPRESENSEからLTE-MモジュールにATコマンドを入力して画像データ送信制御を実施しています。
SPRESENSE-LTE-Mモジュール間のボーレートは230400bpsとしました。
SPRESENSEの大まかな動作は以下の通りです。
- 起床後にLTE-Mモジュールを起動(モジュールのPWRピンにLOWパルス入力)
- カメラ撮影
- モジュールとのコマンド通信確立後にHTTP通信設定
- 撮影画像をサーバに4KBづつHTTP PUT送信
- 画像データ送信終了後にデータ送信終了信号をGET送信
- 10分スリープ
LTE-Mモジュール起動後 3分経ってもデータ送信が終了していない場合はエラーとみなして強制的にスリープさせています。
サーバは画像データを4KBづつ受け取り、データ送信終了信号がきたらデータをまとめてJPEGファイルとして保存します。
まとめ
ここではSPRESENSEを用いてバッテリレス超簡易監視カメラを製作しました。
SPRESENSEの省エネ特性により太陽光発電のみで非常にコンパクトな監視カメラの実現ができました。
システム自体が軽量なのでどこにでも持ち込むことができて、設置も容易です。
日照があれば画像を送信し続けます。
山奥のアナログメータの監視や平地の少ない険しい土地の環境監視などに応用可能であると考えます。
本監視カメラは日照がある限り画像を送り続けるという発想で製作し、AIなどによる画像認識を行う際にはサーバ側での実施を想定しています。
しかし、SPRESENSE自体の性能も高く、専用の夜間撮影可能な専用高感度カメラも販売されておりますのでバッテリ搭載の本格的監視カメラへの応用も可能です。
エッジAIを用いて画像認識を現場で行う高機能動作もできるでしょう。
例えば野生動物を検知したり川の水位を画像から判断して緊急時には撮影頻度を上げるなど。
SPRESENSEの応用は多岐で大きな利益があると今回の製作を通じて実感できました。
投稿者の人気記事
-
HomeMadeGarbage
さんが
2022/09/09
に
編集
をしました。
(メッセージ: 初版)
-
HomeMadeGarbage
さんが
2022/09/09
に
編集
をしました。
-
HomeMadeGarbage
さんが
2022/09/19
に
編集
をしました。
ログインしてコメントを投稿する