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);
}
Comments