M5Stack-TimerCamera saves images to Google Drive on a regular basis and on time

In the Video

Japanese【日本語】

English【英語】

1. Overview

Using M5Stack-TimerCamera, acquire images with the camera at regular intervals or on time and save them to Google Drive.
TimerCamera of M5Stack is a product that uses a microcomputer ESP32 and camera OV3660.
Create a program that synchronizes time using NTP and performs periodic and scheduled startup.

2. The development environment

Arduino was developed in Italy under the philosophy of “making things easier and easier to understand”.
Currently, it is widely used for learning all over the world, and the library is also substantial.
So, if you want to start electronic work, I think this is the only development environment.
Therefore, I use Arduino at this time.

3. Google Setting

Google settings are required to use GoogleCloudAPI. Acquire the client ID, refresh token, etc. to be set in the software. Please see here for a separate post

4. Software

The software I made this time is as follows.

//*************************************************************************
//  M5TimerCAM GoogleDrive Ver2023.2.7
//  Arduino Board : M5Stack-Timer-CAM [M5Stack ver 2.0.6]
//  Written by IT-Taro
//***********************************************************************

#include <WiFi.h>
#include <WiFiClientSecure.h>
#include "esp_camera.h"

// ################ for Battery Use ###############
//#include "battery.h"
//#include "soc/rtc_cntl_reg.h" // for BrouwnoutDetector Disable

//#define BATTERY_ENABLE

// ##################### Line, Wi-Fi settings (Preferences) #####################
String clientId         = "##### CLIENT-ID #####.apps.googleusercontent.com"; // $$$ CHANGE REQUIRED $$$
String clientSecret     = "##### CLIENT-SECRET #####";                        // $$$ CHANGE REQUIRED $$$
String refreshToken     = "##### REFRESH-TOKEN #####";                        // $$$ CHANGE REQUIRED $$$
String driveFolder      = "#### GOOGLE-FOLDER-ID ###";                        // $$$ CHANGE REQUIRED $$$

const char *ssid        = "##### SSID #####"; // $$$ CHANGE REQUIRED $$$
const char *password    = "### PASSWORD ###"; // $$$ CHANGE REQUIRED $$$

const int   Interval    = 20;                 // Image save interval (minutes) [OFF:-1]
const int   SavaTime    = 15;                 // Image save time (0 to 24 hours exactly) [OFF:-1]
// ###################################################################
const char* refreshServer = "oauth2.googleapis.com";
const char* refreshUri    = "/token";
const char* apiServer     = "www.googleapis.com";
const char* apiUri        = "/upload/drive/v3/files?uploadType=multipart";
String accessToken        = "";

int preMin  = -1;     // last run time(for Interval)
int preHour = -1;     // last run time(for Time)

// LED Pin Setting
const byte LED_PIN      = 2;  // Green LED

// CAMERA_MODEL_M5_UNIT_CAM
#define PWDN_GPIO_NUM     -1
#define RESET_GPIO_NUM    15
#define XCLK_GPIO_NUM     27
#define SIOD_GPIO_NUM     25
#define SIOC_GPIO_NUM     23

#define Y9_GPIO_NUM       19
#define Y8_GPIO_NUM       36
#define Y7_GPIO_NUM       18
#define Y6_GPIO_NUM       39
#define Y5_GPIO_NUM        5
#define Y4_GPIO_NUM       34
#define Y3_GPIO_NUM       35
#define Y2_GPIO_NUM       32
#define VSYNC_GPIO_NUM    22
#define HREF_GPIO_NUM     26
#define PCLK_GPIO_NUM     21

// Global Values
WiFiClientSecure httpsClient;
bool ledFlag          = true; // LED Control Flag
int waitingTime      = 30000; // Wait 30 seconds to google response.
camera_fb_t * fb;

// Setup Function
void setup() {
  Serial.begin(115200);

// For Battery Use
#ifdef BATTERY_ENABLE
  WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0); //disable   detector
  bat_init();
  bat_hold_output();
#endif
  //Serial.setDebugOutput(true);
  //Serial.println();

  // Camera Setting
  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;
  // Image size setting:QVGA(320x240),CIF(400x296),HVGA(480x320),VGA(640x480),SVGA(800x600),XGA(1024x768)
  config.frame_size = FRAMESIZE_XGA;
  config.jpeg_quality = 10;
  config.fb_count = 2;

  // camera init
  esp_err_t err = esp_camera_init(&config);
  if (err != ESP_OK) {
    Serial.printf("Camera init failed with error 0x%x", err);
    return;
  }
  /*sensor_t *s = esp_camera_sensor_get();
  // initial sensors are flipped vertically and colors are a bit saturated
  s->set_vflip(s, 1);       // flip it back
  s->set_brightness(s, 1);  // up the blightness just a bit
  s->set_saturation(s, -2); // lower the saturation*/

  // ####### LED Pin setting #######
  pinMode ( LED_PIN, OUTPUT );

  // ####### Wireless Wi-Fi connection #######
  WiFi.begin ( ssid, password );
  while ( WiFi.status() != WL_CONNECTED ) { // infinite loop until connected
    // LED flashes every second while connected
    ledFlag = !ledFlag;
    digitalWrite(LED_PIN, ledFlag);
    delay ( 1000 );
    Serial.print ( "." );
  }
  // Wi-Fi connection completed (IP address display)
  Serial.print ( "Wi-Fi Connected! IP address: " );
  Serial.println ( WiFi.localIP() );
  Serial.println ( );
  // LED lights when Wi-Fi is connected (Wi-Fi connection status)
  digitalWrite ( LED_PIN, true );

  // ####### HTTPS certificate check setting #######
  // Skip Server certificate check (required since 1.0.5)
  httpsClient.setInsecure();//skip verification
  //httpsClient.setCACert(rootCA);// It is also possible to obtain a root certificate in advance using a web browser and set rootCA

  // ####### NTP setting #######
  configTime(9 * 3600L, 0, "ntp.nict.jp", "time.google.com", "ntp.jst.mfeed.ad.jp");

  Serial.println("Setup Completed!!!");
}

// Loop Function
void loop() {
  struct tm timeInfoLoop;
  getLocalTime(&timeInfoLoop);
  bool doFlag = false;
  // Check Interval
  int curMin = -1;
  if ( preMin <= timeInfoLoop.tm_min ) {
    curMin = timeInfoLoop.tm_min;
  // When time advances beyond 60 minute
  } else {
    curMin = timeInfoLoop.tm_min + 60;
  }
  // Check Interval time
  if ( Interval > 0 && ( (curMin - preMin) >= Interval || preMin == -1 ) ) {
    doFlag = true;
    preMin = timeInfoLoop.tm_min;
    Serial.print("Check Interval: ");
    Serial.println(preMin);
  }
  // Check Time
  if ( preHour != timeInfoLoop.tm_hour && SavaTime == timeInfoLoop.tm_hour && timeInfoLoop.tm_min == 0) {
    doFlag = true;
    preHour = timeInfoLoop.tm_hour;
    Serial.print("Check Save Time: ");
    Serial.println(preHour);
  }
  // Do Save Image to Google Drive
  if ( doFlag ) {
    // ####### Get JPEG picture #######
    Serial.println("Start get JPG");
    getCameraJPEG();
    // ####### get Access Token #######
    Serial.println("Start get AccessToken");
    getAccessToken();
    // ####### Save JPEG to GoogleDrive #######
    Serial.println("Start Post GoogleDrive");
    postGoogleDriveByAPI();
  }
  delay(1);
}

// Send JPEG by Http POST
void postGoogleDriveByAPI() {

  Serial.println("Connect to " + String(apiServer));
  if (httpsClient.connect(apiServer, 443)) {
    Serial.println("Connection successful");

    // Get Time for save file name
    struct tm timeInfo;
    char preFilename[16];
    getLocalTime(&timeInfo);
    sprintf(preFilename, "%04d%02d%02d_%02d%02d%02d",
          timeInfo.tm_year + 1900, timeInfo.tm_mon + 1, timeInfo.tm_mday,
          timeInfo.tm_hour, timeInfo.tm_min, timeInfo.tm_sec);
    String saveFilename = "M5TimerCam_" + String(preFilename) + ".jpg";

    String metadata = "--foo_bar_baz\r\n"
                      "Content-Type: application/json; charset=UTF-8\r\n\r\n"
                      "{\"name\":\"" + saveFilename + "\",\"parents\":[\"" + driveFolder + "\"]}\r\n\r\n"; // parents:save folder
                      //"{\"name\":\"" + saveFilename + "\"}\r\n\r\n"; // parerents is Optional
    String startBoundry = "--foo_bar_baz\r\n"
                          "Content-Type:image/jpeg\r\n\r\n";
    String endBoundry   = "\r\n--foo_bar_baz--";

    unsigned long contentsLength = metadata.length() + startBoundry.length() + fb->len + endBoundry.length();
    String header = "POST " + String(apiUri) + " HTTP/1.1\r\n" +
                    "HOST: " + String(apiServer) + "\r\n" +
                    "Connection: close\r\n" +
                    "content-type: multipart/related; boundary=foo_bar_baz\r\n" +
                    "content-length: " + String(contentsLength) + "\r\n" +
                    "authorization: Bearer " + accessToken + "\r\n\r\n";

    Serial.println("Send JPEG DATA by API");
    httpsClient.print(header);
    httpsClient.print(metadata);
    httpsClient.print(startBoundry);
    // JPEG data is separated into 1000 bytes and POST
    unsigned long dataLength = fb->len;
    uint8_t*      bufAddr    = fb->buf;
    for(unsigned long i = 0; i < dataLength ;i=i+1000) {
      if ( (i + 1000) < dataLength ) {
        httpsClient.write(( bufAddr + i ), 1000);
      } else if (dataLength%1000 != 0) {
        httpsClient.write(( bufAddr + i ), dataLength%1000);
      }
    }
    httpsClient.print(endBoundry);

    Serial.println("Waiting for response.");
    long int StartTime=millis();
    while (!httpsClient.available()) {
      Serial.print(".");
      delay(100);
      if ((StartTime+waitingTime) < millis()) {
        Serial.println();
        Serial.println("No response.");
        break;
      }
    }
    Serial.println();
    while (httpsClient.available()) {
      Serial.print(char(httpsClient.read()));
    }
    /*Serial.println("Recieving Reply");
    while (httpsClient.connected()) {
      String retLine = httpsClient.readStringUntil('\n');
      //Serial.println("retLine:" + retLine);
      int okStartPos = retLine.indexOf("200 OK");
      if (okStartPos >= 0) {
        Serial.println("200 OK");
        break;
      }
    }*/
  } else {
    Serial.println("Connected to " + String(refreshServer) + " failed.");
  }
  httpsClient.stop();
}

// Update access token with refresh token (access token valid time is 1 hour)
void getAccessToken() {
  accessToken = "None";
  // ####### Get Access Token #######
  Serial.println("Connect to " + String(refreshServer));
  if (httpsClient.connect(refreshServer, 443)) {
    Serial.println("Connection successful");

    String body = "client_id="     + clientId     + "&" +
                  "client_secret=" + clientSecret + "&" +
                  "refresh_token=" + refreshToken + "&" +
                  "grant_type=refresh_token";

    // Send Header
    httpsClient.println("POST "  + String(refreshUri) + " HTTP/1.1");
    httpsClient.println("Host: " + String(refreshServer));
    httpsClient.println("content-length: " + (String)body.length());
    httpsClient.println("Content-Type: application/x-www-form-urlencoded");
    httpsClient.println();
    // Send Body
    httpsClient.println(body);

    Serial.println("Recieving Token");
    while (httpsClient.connected()) {
      String retLine = httpsClient.readStringUntil('\n');
      //Serial.println("retLine:" + retLine);
      int tokenStartPos = retLine.indexOf("access_token");
      if (tokenStartPos >= 0) {
        tokenStartPos     = retLine.indexOf("\"", tokenStartPos) + 1;
        tokenStartPos     = retLine.indexOf("\"", tokenStartPos) + 1;
        int tokenEndPos   = retLine.indexOf("\"", tokenStartPos);
        accessToken       = retLine.substring(tokenStartPos, tokenEndPos); 
        Serial.println("AccessToken:"+accessToken);
        break;
      }
    }
  } else {
    Serial.println("Connected to " + String(refreshServer) + " failed.");
  }
  httpsClient.stop();
  if (accessToken == "None") {
    Serial.println("Get AccessToken Failed. Restart ESP32!");
    delay(3000);
    ESP.restart();
  }
}

// Get JPEG image with OV3660
void getCameraJPEG(){
  fb = esp_camera_fb_get();  // Get JPEG image
  if (!fb) {
    Serial.printf("Camera capture failed");
  }
  Serial.printf("JPG: %uB ", (uint32_t)(fb->len));
  Serial.println();
  // Shooting end processing
  esp_camera_fb_return(fb);
}

Document [at youtube]

Comments

Copied title and URL