//------------------------------------------------------------------------ // '22.04.29 naka // LINEに写真を送付する防犯カメラ(ESP32-CAM) // // 1. 機能と動作フロー // (1) 起動すると監視開始メッセージをLINE notifyに送る // (2) 焦電センサが人を検知すると写真を撮り、microSDに保存、LINE notityに送る // (3) LINE notifyに写真を送れる枚数制限(50枚/H)から、送る間隔は75秒以上。 // ただし、焦電センサが検知したら撮影とmicroSDへの保存は最短1秒ごとに行う。 // (4) 75秒経過すると(2)を待つ。 // // 2. 使い方 // ・WiFiのアクセスポイントSSID, パスワード、LINEトークンは暗号化してmicroSDに格納 // ファイル名:setup.txt // // 以下を1行ずつ順に記載(日本語メッセージはUTF-8) // 1行目 暗号化したWiFi ssid // 2行目 暗号化したpassword // 3行目 暗号化したLINE notify token // 4行目 センサが検知してから撮影までのdelayをmsecで記載 // 5行目 監視開始メッセージ // 6行目 検知したときに送るメッセージを記載 // (例) // &D/)%X/C04S+#T'?+USM // *%/O-J_F$##5)CK`(% // #VGN-Z_*-FCG+VK1/E;M<47E1UCJ.C/`-U(S#F?C06CV)%C_0$$E8W$89(0[5(U // 500 // 監視開始 // 侵入者検知 // // ・処理のログファイル出力 // microSDの/log.txtに追加モードで出力 // // ・写真保存 // microSDの/photo/日付フォルダ内にタイムスタンプがファイル名になったjpg形式で保存 // // 3. その他 // 当初、deep sleepを試したがPIRセンサが検出しwake upしてから写真撮影までの // タイムラグが大きく、シャッタチャンスを逃すので断念。 // その後、light sleepを試したが、wake up後の写真撮影に失敗することが多く、 // これも断念。結果としてsleepはしないで動作し続ける。WiFi接続は必要なときのみ接続。 // // WiFi接続やLINEに写真を送るのは以下のスケッチを流用させて頂いた // https://github.com/fustyles/Arduino/tree/master/ESP32-CAM_Linenotify // // LINE notifyサービスは以下参照 // https://notify-bot.line.me/ja/ // // 上記URLからLINEにログインし、マイページからアクセストークンを発行する // (1) LINEのマイページでNotifyを設定 // (2) 設定したトークルームに LINE Notify を招待 // トークンを暗号化してmicroSDのsetup.txt3行目に書く //------------------------------------------------------------------------ #include #include #include "esp_camera.h" #include "SD.h" // ESP32-CAM #define PWDN_GPIO_NUM 32 #define RESET_GPIO_NUM -1 #define XCLK_GPIO_NUM 0 #define SIOD_GPIO_NUM 26 #define SIOC_GPIO_NUM 27 #define Y9_GPIO_NUM 35 #define Y8_GPIO_NUM 34 #define Y7_GPIO_NUM 39 #define Y6_GPIO_NUM 36 #define Y5_GPIO_NUM 21 #define Y4_GPIO_NUM 19 #define Y3_GPIO_NUM 18 #define Y2_GPIO_NUM 5 #define VSYNC_GPIO_NUM 25 #define HREF_GPIO_NUM 23 #define PCLK_GPIO_NUM 22 // 焦電型赤外線センサ #define PIR GPIO_NUM_33 // microSD #define sd_sck 14 #define sd_mosi 15 #define sd_ss 13 #define sd_miso 2 const char* host = "notify-api.line.me"; char* crypt_key = "OpenSesame"; // SSID,PW,LINE tokenを暗号化したときのkey char ssid[64],password[64],token[64],message1[128],message2[128]; int shutter_delay; #define SetupFile "/setup.txt" #define LogFile "/log.txt" #define PhotoPath "/photo" unsigned long previous_time,current_time; unsigned long wakeup_millis; File lfp; // log file ptr // デバッグでSerialに出力するときにコメントを外す //#define DEBUG_SERIAL void setup() { char msg[128]; pinMode(PIR, INPUT); // 焦電センサ pinMode(4, OUTPUT); // フラッシュLED digitalWrite(4, LOW); // フラッシュLED off #ifdef DEBUG_SERIAL Serial.begin(115200); #endif // カメラの初期化 camera_init(); // microSD初期化 SPI.begin(sd_sck, sd_miso, sd_mosi, sd_ss); SD.begin(sd_ss); // setupファイル読み込み if (read_setup_file(SetupFile)){ error_flash(10); // エラー時にFlash用LED点滅 while(1); } // logファイルオープン lfp = SD.open(LogFile, FILE_APPEND); if(!lfp){ #ifdef DEBUG_SERIAL Serial.printf("log file open eror %s\n",LogFile); #endif error_flash(5); // エラー時にFlash用LED点滅 while(1); } if (wifi_connect()) { // NTPサーバに接続し、時刻設定 configTime(9 * 3600L, 0, "ntp.nict.jp", "time.google.com", "ntp.jst.mfeed.ad.jp"); } else{ message2log("WiFi connection error occurred."); // この時点では時刻設定ができていないのでlog出力の時刻は正しくない } message2log("Power On"); message2log("Wait 30sec until PIR stable."); delay(30000); // PIRセンサが安定するまで30秒待つ while(digitalRead(PIR)); // 念のためPIRセンサがLowになるまで待つ // 監視開始の通知 send_msg2line(message1); // LINEにメッセージ送付 message2log(message1); // 監視時はWiFi未接続にする wifi_disconnect(); previous_time = 0; } void loop() { camera_fb_t* fb; char msg[128]; if (digitalRead(PIR)) { // センサがオンしたとき delay(shutter_delay); // センサ検知と撮影までの待ち時間 for (int i=0;i<3;i++) { // 時々、撮影に失敗したこと(最新版では大丈夫そう)があったので最大3回トライする fb = NULL; fb = esp_camera_fb_get(); // 写真撮影 sprintf(msg,"Took photo.",i+1); message2log(msg); if (fb!=NULL && fb->len>1000) break; // 撮影に成功したら抜ける。通常120KB前後なので1KB未満は失敗と判断 esp_camera_fb_return(fb); delay(500); // 500ms待ってからもう一度撮影 sprintf(msg,"Retry(%d) take photo.",i+1); message2log(msg); } if(fb) { savePhoto(fb); // microSDに写真を保存 current_time = millis(); if ( previous_time==0 || (current_time - previous_time) > 75000) { // 最初のLINE送信か、前回LINEに送信してから75秒以上経過したか? // LINEへ通知 if(wifi_connect()) { message2log("WiFi Connected to send photo."); if (fb->len>1000) { // 写真サイズが妥当ならLINEへ送付 sendCapturedImage2LineNotify(token,message2,fb); sprintf(msg,"Send photo to LINE notify."); message2log(msg); } else { // 写真撮影失敗時はテキストメッセージのみ sprintf(msg,"message=%s(text only)",message2); send_msg2line(msg); message2log(msg); } wifi_disconnect(); previous_time = current_time; } else { message2log("WiFi connection error occurred at send photo."); } } //return the frame buffer back to the driver for reuse esp_camera_fb_return(fb); } else { strcpy(msg,"Camera capture failed."); message2log(msg); } } delay(1000); // 次の検出まで最短1秒(実際には色々と処理に時間がかかるので1秒間隔以上) } void camera_init(){ char msg[128]; camera_config_t config; config.ledc_channel = LEDC_CHANNEL_0; config.ledc_timer = LEDC_TIMER_0; config.pin_d0 = Y2_GPIO_NUM; config.pin_d1 = Y3_GPIO_NUM; config.pin_d2 = Y4_GPIO_NUM; config.pin_d3 = Y5_GPIO_NUM; config.pin_d4 = Y6_GPIO_NUM; config.pin_d5 = Y7_GPIO_NUM; config.pin_d6 = Y8_GPIO_NUM; config.pin_d7 = Y9_GPIO_NUM; config.pin_xclk = XCLK_GPIO_NUM; config.pin_pclk = PCLK_GPIO_NUM; config.pin_vsync = VSYNC_GPIO_NUM; config.pin_href = HREF_GPIO_NUM; config.pin_sscb_sda = SIOD_GPIO_NUM; config.pin_sscb_scl = SIOC_GPIO_NUM; config.pin_pwdn = PWDN_GPIO_NUM; config.pin_reset = RESET_GPIO_NUM; config.xclk_freq_hz = 20000000; config.pixel_format = PIXFORMAT_JPEG; // Frame parameters config.frame_size = FRAMESIZE_UXGA; config.jpeg_quality = 12; config.fb_count = 1; // camera init esp_err_t err = esp_camera_init(&config); if (err != ESP_OK) { sprintf(msg,"CAMERA init failed with error 0x%x", err); message2log(msg); } sensor_t *sensor = esp_camera_sensor_get(); // sensor->id.PID は OV2640_PID (0x26) だった。 // 以下、デフォルトでonのものもあるかも知れないが一応設定 sensor->set_wb_mode(sensor,3); // white balance 0:sunny,1:cloudy,2:office,3:home sensor->set_gain_ctrl(sensor,1); // automatic gain control sensor->set_exposure_ctrl(sensor,1); // Automatic Exposure Control sensor->set_ae_level(sensor,1); // +1 で少し明るい? sensor->set_aec2(sensor,1); // Automatic Exposure Control } void error_flash(int count) { // エラー時などにFlash用LED点滅 for (int i=0;i 8bit j = 0; for(i=0; j>4; j++; break; case 1: a[i] = (x[j]&0x0F)<<4 | (x[j+1]&0x3C)>>2; j++; break; case 2: a[i] = (x[j]&0x03)<<6 | x[j+1]&0x3F; j += 2; break; } } m = i; k = 0; int n = strlen(key); for (i=0; ilen < 1000) { message2log("Photo did not save because size too small."); return; } getLocalTime(&timeinfo); sprintf(yymmdd, "%02d%02d%02d",(timeinfo.tm_year + 1900)%100, timeinfo.tm_mon + 1, timeinfo.tm_mday); sprintf(filename, "%s_%02d%02d%02d.jpg", yymmdd,timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec); // 日付のdirが無ければ作成 sprintf(path,"%s/%s",PhotoPath,yymmdd); if (!SD.exists(path)) { if (SD.mkdir(path)) { sprintf(msg,"make dir error. %s",path); message2log(msg); } } // 写真保存 sprintf(path_file,"%s/%s",path,filename); if (File fp = SD.open(path_file, FILE_WRITE)) { fp.write(fb->buf, fb->len); fp.close(); sprintf(msg,"Photo saved to %s",path_file); } else { sprintf(msg,"Photo save error %s",path); } message2log(msg); } void message2log(const char* message) { char log_message[220]; char timestamp[20]; struct tm timeinfo; getLocalTime(&timeinfo); sprintf(timestamp, "%02d.%02d.%02d %02d:%02d:%02d", (timeinfo.tm_year + 1900)%100, timeinfo.tm_mon + 1, timeinfo.tm_mday, timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec); // log書き出し sprintf(log_message,"%s %.200s\n",timestamp,message); #ifdef DEBUG_SERIAL Serial.printf("%s",log_message); #endif lfp.print(log_message); lfp.flush(); } String sendCapturedImage2LineNotify(String token,String message,camera_fb_t* fb) { WiFiClientSecure client_tcp; char msg[128]; client_tcp.setInsecure(); //run version 1.0.5 or above sprintf(msg,"Connect to %s",host); message2log(msg); if (client_tcp.connect(host, 443)) { message2log("Connection successful"); String head = "--ESP32-CAM\r\nContent-Disposition: form-data; name=\"message\"; \r\n\r\n" + message + "\r\n--ESP32-CAM\r\nContent-Disposition: form-data; name=\"imageFile\"; filename=\"esp32-cam.jpg\"\r\nContent-Type: image/jpeg\r\n\r\n"; String tail = "\r\n--ESP32-CAM--\r\n"; size_t extraLen = head.length() + tail.length(); size_t totalLen = fb->len + extraLen; client_tcp.println("POST /api/notify HTTP/1.1"); client_tcp.println("Connection: close"); client_tcp.println("Host: " + String(host)); client_tcp.println("Authorization: Bearer " + token); client_tcp.println("Content-Length: " + String(totalLen)); client_tcp.println("Content-Type: multipart/form-data; boundary=ESP32-CAM"); client_tcp.println(); client_tcp.print(head); uint8_t *fbBuf = fb->buf; size_t fbLen = fb->len; for (size_t n=0;n0) { size_t remainder = fbLen%1024; client_tcp.write(fbBuf, remainder); } } client_tcp.print(tail); String getResponse="",Feedback=""; int waitTime = 10000; // timeout 10 seconds long startTime = millis(); boolean state = false; while ((startTime + waitTime) > millis()) { delay(100); while (client_tcp.available()) { char c = client_tcp.read(); if (state==true) Feedback += String(c); if (c == '\n') { if (getResponse.length()==0) state=true; getResponse = ""; } else if (c != '\r') getResponse += String(c); startTime = millis(); } if (Feedback.length()>0) break; } client_tcp.stop(); message2log(Feedback.c_str()); return Feedback; } else { message2log("Connecttion failed."); } } void send_msg2line(char* message) { WiFiClientSecure client; client.setInsecure(); if (client.connect(host, 443)) { message2log("Connection successful."); String query = String("message=") + String(message); String request = String("") + "POST /api/notify HTTP/1.1\r\n" + "Host: " + host + "\r\n" + "Authorization: Bearer " + token + "\r\n" + "Content-Length: " + String(query.length()) + "\r\n" + "Content-Type: application/x-www-form-urlencoded\r\n\r\n" + query + "\r\n"; client.print(request); while (client.connected()) { String line = client.readStringUntil('\n'); if (line == "\r") { break; } } String line = client.readStringUntil('\n'); client.stop(); } else { message2log("Connecttion failed."); return; } } //------------------------------------------------------------------------------- // EOF //-------------------------------------------------------------------------------