バッテリレス超簡易監視カメラ
HomeMadeGarbage Advent Calendar 2022 |5日目
2022年 SPRESENSE 活用コンテストに参加するべくSPRESENSEを用いた超低消費電力の監視カメラを製作したので報告いたします。
目次
概要
カメラの構成は以下の通りです。
バッテリレスでソーラ直接駆動による監視カメラを目指します。
撮影した画像はサーバにLTE携帯網でサーバに送信します。
サーバは自宅で立ち上げているラズパイ2サーバを使用しました。
開発の流れは以下の通りです。
- SPRESENSE-LTEモジュール通信確立
- カメラ込みでの低消費電力化
- ソーラ選定
- 長期安定動作確認
LTE-Mモジュール
通信モジュールはM5Stack UnitCatMを使用しようと考えていたのですが、、、
SPRESENSEとの通信を速くしたいと欲張り、モジュールのボーレートを2900000bpsと高速設定してしまい制御通信できなくしてしまいました。。。(´;ω;`)ウゥゥ
しょうがないのでUnitCatMと同じ通信モジュール SIM7080Gが搭載された評価ボードを使用することにしました。
制御コマンド用ボーレートはマイルドに230400bpsとしました。
通信動作
カメラモジュールを接続してサーバへのhttp送信を目指します。
SIM7080Gのhttp通信では大容量データは送れないので、ここではJPEG画像 (QVGA : 320×240)を4KBづつに分割してサーバに送信することにしました。
撮影画像をサーバに4KBづつHTTP PUT送信して最後にデータ送信終了信号をGET送信してサーバ上でデータをマージして画像保存します。
消費電力
画像撮影とサーバ送信動作が確認できたので消費電力を測定します。
カメラは撮影後に10分スリープして起床して撮影を繰り返すようにしました。
1時間当たり5回画像を送信して消費電力は116mWhと非常に低い結果となりました。
LTE-MモジュールもSPRESENSEの3.3V出力から給電しているのでスリープ時には停止します。
消費電流のピークはデータ送信時で入力電圧が10Vで約140mA、5.5Vで約220mAでした。
非常に低い消費電力での動作が確認できましたのでソーラ直接駆動が実現できそうです。
ソーラパネル選定
ソーラパネルとSPRESENSE電源入力の間に昇降圧DC-DCコンバータ SC8721を使用しました。ソーラパネルから供給される電圧を4Vに変換してSPRESENSEに給電します。
SC8721は入力電圧範囲が2.7V~22Vと非常に広く、幅広い種類のソーラパネルの採用が可能となります。
カメラシステム自体の消費電力は非常に小さいのでデータ送信時のピーク電流さえまかなうことができれば小型のソーラパネルでも駆動可能のはずです。
実験に際してカメラは100均のプラケースに収めました (防水機能はゼロですww)。
DCDC出力に大きいコンデンサを接続してピーク電流をまかなおうとしましたが、あまり大きな効果はなく電解コンデンサ1000uFほどつけておけば十分な印象でした。
1W~5Wの色々なパネルを購入して実験してみました。
バッテリーレス超簡易監視カメラ
1Wソーラはさすがに無理だったけど
2Wソーラでも動いたコンパクトで良い#Spresense https://t.co/meLFlgIlZA pic.twitter.com/RugkYLmhi6
— HomeMadeGarbage (@H0meMadeGarbage) September 9, 2022
1W ソーラは標準電圧:5.5 V、標準電流:170 mAと若干能力が足りずSPRESENSEは動作しましたが
LTE-Mモジュールの通信までは駆動できませんでした。
2Wソーラでは問題なく画像送信動作を確認することができました。
2W ソーラパネルで一生懸命 通信する超簡易監視カメラ
太陽の恵み
太陽電池モジュール SY-M2Whttps://t.co/WUExABBvO5#Spresense pic.twitter.com/EsdFNwflfq
— HomeMadeGarbage (@H0meMadeGarbage) September 9, 2022
サーバに届いた画像
撮影動作
ベランダに置いて、長時間の撮影動作も確認いたしました。
お母ちゃんにサーバに届いた画像をブラウザで一覧で見れるようにしてもらいました。
我が家のベランダは東向きで午前中の短い時間でしか直射日光が当たらないのですが、日照時の撮影を確認することができました。
バッテリレス超簡易監視カメラ
本日もたくさん太陽からお恵みをいただきました。#SPRESENSE pic.twitter.com/w7K8GTRA68
— HomeMadeGarbage (@H0meMadeGarbage) September 3, 2022
ソースコード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 |
#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の応用は多岐で大きな利益があると今回の製作を通じて実感できました。