kuroの覚え書き

96の個人的覚え書き

M5STACK Timer Camera X

これまでタイムラプスカメラはラズパイベースで作ってきたが、もっと省電力で大量にばら撒くような使い方が必要となってきたのでESP32ベースに移行を考えた。
Amazonを見ていると1000円くらいで2MピクセルのカメラがついたESP32基板が見つかるが、どれもこれも技適がついておらず、日本国内で大っぴらに電波を飛ばせない代物ばかりであった。
そんな中、M5STACKシリーズの製品で3000円程度で技適もついて、さらにバッテリーも搭載しているという製品に目が止まり、まずは試してみることにした。
ESP32 PSRAM Timer Camera X (OV3660)www.switch-science.com

ESP32搭載なのでArduinoIDEでプログラミングできるだろうとおもいつつ、色々リサーチ。

まずは
lang-ship.com
この方、おそらくM5STACKで日本から情報を発信されている第一人者なんだろうか。

ArduinoIDEのセッティングなどわかりやすく紹介されていて、参考になった。
ライブラリとしてTimer-CAMというものを入れると。
ArduinoIDE2.2.2に最新の0.0.3を入れようとするとどうも何やらエラーが出て入らない。しかたがないのでとりあえず0.0.2を入れておく。

ちなみに今回
https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
こちらをM5STACKのボード設定を取り込むために入れるためこれまで使っていたIDE1.8.19とは別に2.2.2を用意してみたんだが、これが仇となることが後で判明。

ここまでArduinoのセッティングをしてきたが、そもそも製品デモ的なファームウェアアプリがあるらしいことを知ったので、まずはそちらを試してみることにする。
3pysci.com

なるほど、M5Burnerというものでやるのね。
https://docs.m5stack.com/en/unit/timercam_xdocs.m5stack.com
こちらで該当するリンクを辿っていきアプリをダウンロードする。

しかし、ダウンロードしてきたM5Burnerは見た目がなんか違っていて、どうやらバージョンアップしているらしい。
で、ファームウェア書き込みまではうまく行くのだが、肝心の画像を見に行くtokenの発行がどうやってもうまくいかない。
なんだかバグがあってこのバージョンの M5Burnerではサーバとのやり取りがうまくいかず、tokenが取得できないというようなディスカッションが散見される。だめじゃん。

ということで次の作戦へ。

Arduinoでサンプルアプリを入れてみる。
sample.msr-r.net
こちらのかたの記述を参考に。

なるほどArduinoのexampleに入っているのか。

これね。

ArduinoでM5Stack-Timer-CAMをボードとして選び、書き込んでみる。

なるほど、カメラ自体がWEBサーバも立ち上げていて無線LAN経由でコントロールできるわけだ。

画質は・・・まあこんなもんなんでしょう。
ただ、これだと写真を撮ることはできるけどタイムラプスを自動で撮ることはできないので、目的には合致しない。

次に参照したのは
note.com
こちら。

さらにリンクされていた
twinklesmile.blog42.fc2.com
こちらを参考にGoogle Driveに撮った写真を転送することを考えてみる。

Google Driveスクリプトを仕込んでおき、アクセスがあったらファイルを格納するということなんだろうか。
言われるままにGoogleDriveにそれ用のフォルダ(timer_camera_x1)を作り、
左上の「+新規」ボタンから、その他ー>Google App Scriptとたどってスクリプトを入れる。

function doPost(e) {
var data = Utilities.base64Decode(e.parameters.data);
var nombreArchivo = Utilities.formatDate(new Date(), 'Asia/Tokyo', 'yyyyMMdd_HHmmss')+".jpg";
var blob = Utilities.newBlob(data, e.parameters.mimetype, nombreArchivo );

// Save the photo to Google Drive
var folder, folders = DriveApp.getFoldersByName("timer_camera_x1");
if (folders.hasNext()) {
folder = folders.next();
} else {
folder = DriveApp.createFolder("timer_camera_x1");
}
var file = folder.createFile(blob);
return ContentService.createTextOutput('Complete')
}

という感じ。
ファイル名を無題.gsから適当な名前に変えて、右上の「デプロイ」から「新しいデプロイ」を選び、設定する
デプロイのタイプ>ウェブアプリ
説明>なにかてきとうに
ウェブアプリ>
次のユーザーとして実行: 自分
アクセスできるユーザー: 全員

ファームウェアは先人のものを基本そのままということで。


ところがここでまた躓く。

#include "fd_forward.h"

ができないとエラーを吐く。
どうやらTimer-CAM 0.0.2はダメで、0.0.3をインストールしようとすると
先ほどと同じように

Failed to install library: 'Timer-CAM:0.0.3'. No valid dependencies solution found: dependency 'Micro-RTSP' is not available

というエラーが出てインストールできない。

ところが、IDEの1.8.19を起動して0.0.3のライブラリをインストールしてみるとこれができた。
ファームウェアの書き込みも問題なく、ちゃんと5分間隔で写真を撮って、GoogleDriveにどんどん溜まっていく。

さらに一旦1.8.19でインストールしてしまえば2.2.2の方のアプリでも使えるという不思議さ。
なんなんだ?


ついでにdeep sleepによる省電力設定を加えて一応の完成とする。

#include <WiFi.h>
#include <WiFiClientSecure.h>
#include "soc/soc.h"
#include "soc/rtc_cntl_reg.h"
#include "Base64.h"

#include "esp_camera.h"
#include "camera_pins.h"

#include "battery.h"
#include "bmm8563.h" //RTC
// #include "led.h"

const char* ssid     = "********";
const char* password = "***********";
const char* myDomain = "script.google.com";
String myScript = "/macros/s/****************/exec"; //Replace with your own url
String myFilename = "filename=M5Camera.jpg";
String mimeType = "&mimetype=image/jpeg";
String myImage = "&data=";

int waitingTime = 30000; //Wait 30 seconds to google response.

unsigned long sleepTime = 10790; //sec

void setup() {
  Serial.begin(115200);
  delay(10);

  WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0);

  // Init
  bat_init();
  bmm8563_init();
  // led_init(CAMERA_LED_GPIO);

  // disable bat output, will wake up after 5 sec, Sleep current is 1~2μA
  bat_disable_output();


  WiFi.mode(WIFI_STA);

  Serial.println("");
  Serial.print("Connecting to ");
  Serial.println(ssid);
  WiFi.begin(ssid, password);  

  while (WiFi.status() != WL_CONNECTED) {
    Serial.print(".");
    delay(500);
  }

  Serial.println("");
  Serial.println("STAIP address: ");
  Serial.println(WiFi.localIP());

  Serial.println("");

  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;
  config.frame_size = FRAMESIZE_UXGA;  // UXGA|SXGA|XGA|SVGA|VGA|CIF|QVGA|HQVGA|QQVGA
  config.jpeg_quality = 10;
  config.fb_count = 2;

  esp_err_t err = esp_camera_init(&config);

  if (err != ESP_OK) {
    Serial.printf("Camera init failed with error 0x%x", err);
    delay(1000);
    ESP.restart();
  }

  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

  //drop down frame size for higher initial frame rate
  s->set_framesize(s, FRAMESIZE_SVGA);

  saveCapturedImage(); 

 // if deep seep ここから
  esp_sleep_enable_timer_wakeup(sleepTime*1000000ULL);  
  Serial.println("Deep Sleep Start."); 
  Serial.println(" ");  
  esp_deep_sleep_start();  
 // if deep seep ここまで

}

void loop() {
  // if no deep sleep ここから
  // saveCapturedImage(); 
  // delay(sleepTime*1000ULL); 
  // if no deep sleep ここまで
}

void saveCapturedImage() {
  Serial.println("Connect to " + String(myDomain));
  WiFiClientSecure client;

  client.setInsecure();
  if (client.connect(myDomain, 443)) {
    Serial.println("Connection successful");

    camera_fb_t * fb = NULL;
    fb = esp_camera_fb_get();  
    if(!fb) {
      Serial.println("Camera capture failed");
      delay(1000);
      ESP.restart();
      return;
    }

    char *input = (char *)fb->buf;
    char output[base64_enc_len(3)];
    String imageFile = "";
    for (int i=0;i<fb->len;i++) {
      base64_encode(output, (input++), 3);
      if (i%3==0) imageFile += urlencode(String(output));
    }
    String Data = myFilename+mimeType+myImage;

    esp_camera_fb_return(fb);

    Serial.println("Send a captured image to Google Drive.");

    client.println("POST " + myScript + " HTTP/1.1");
    client.println("Host: " + String(myDomain));
    client.println("Content-Length: " + String(Data.length()+imageFile.length()));
    client.println("Content-Type: application/x-www-form-urlencoded");
    client.println();

    client.print(Data);
    int Index;
    for (Index = 0; Index < imageFile.length(); Index = Index+1000) {
      client.print(imageFile.substring(Index, Index+1000));
    }

    Serial.println("Waiting for response.");
    long int StartTime=millis();
    while (!client.available()) {
      Serial.print(".");
      delay(100);
      if ((StartTime+waitingTime) < millis()) {
        Serial.println();
        Serial.println("No response.");
        //If you have no response, maybe need a greater value of waitingTime
        break;
      }
    }
    Serial.println();   
    while (client.available()) {
      Serial.print(char(client.read()));
    }  
  } else {         
    Serial.println("Connected to " + String(myDomain) + " failed.");
  }
  client.stop();
}

//https://github.com/zenmanenergy/ESP8266-Arduino-Examples/
String urlencode(String str){
    String encodedString="";
    char c;
    char code0;
    char code1;
    char code2;
    for (int i =0; i < str.length(); i++){
      c=str.charAt(i);
      if (c == ' '){
        encodedString+= '+';
      } else if (isalnum(c)){
        encodedString+=c;
      } else{
        code1=(c & 0xf)+'0';
        if ((c & 0xf) >9){
            code1=(c & 0xf) - 10 + 'A';
        }
        c=(c>>4)&0xf;
        code0=c+'0';
        if (c > 9){
            code0=c - 10 + 'A';
        }
        code2='\0';
        encodedString+='%';
        encodedString+=code0;
        encodedString+=code1;
        //encodedString+=code2;
      }
      yield();
    }
    return encodedString;
}