kuroの覚え書き

96の個人的覚え書き

M5 Timer Camera X再び

XIAO ESP32S3 senseの運用がうまくいったのに気をよくして、M5 Timer Camera Xの方も上手く使えないかと思い、もう一度引っ張り出してきた。
そして以前作っていたファームウェアを書き込もうとしたらどういうわけかエラーが出てコンパイルができない。
以前使ってみた時と変わっているのはMacintelMacbook AirからM1のMacbook Proになっている点だけど、この環境構築がうまくいっていないんだろうかということで構築し直してone by oneで確認してみる。

まずM5 Timer Camera Xを使うためにArduino IDEのboard managerでM5のライブラリを入れる。
お作法に従って、
https://m5stack.oss-cn-shenzhen.aliyuncs.com/resource/arduino/package_m5stack_index.json


Preferenceにコピーしてやり、BOARDS MANAGERタブでM5と入れて検索し、出てくる”M5Stack by M5Stack”をインストールしてやる。

今回入ったバージョンは2.1.1であった。
次にTimer-CAMのライブラリをLIBRALY MANAGERタブでTimer-CAMで検索すると"Timer-CAM by M5Stack"が見つかる。

以前試してみていた時は最新バージョンが0.0.3だったけど最新バージョンのArduinoIDEでインストールできないというバグがあった。今回1.0.0とベータじゃなくなった感じのバージョンになっていて、インストールも問題がないようだった。

早速なんかプログラムを入れてみよう。
まずボードとしてM5Stack-Timer-CAMを選ぶ。

ここで気になったのだが、M5TimerCAMというハイフンの入っていない選択肢もあるということ。

こちらを選んでいいものかどうか、現時点では判断がつかない。M5Stackのオフィシャルページではそんなことは書かれていないのだが、これって以前にインストールしていた何かの残骸だったりするんだろうか。

とりあえずお約束のファイル>Examples>01.Basics>Blink

// the setup function runs once when you press reset or power the board
void setup() {
  // initialize digital pin LED_BUILTIN as an output.
  pinMode(LED_BUILTIN, OUTPUT);
}

// the loop function runs over and over again forever
void loop() {
  digitalWrite(LED_BUILTIN, HIGH);  // turn the LED on (HIGH is the voltage level)
  delay(1000);                      // wait for a second
  digitalWrite(LED_BUILTIN, LOW);   // turn the LED off by making the voltage LOW
  delay(1000);                      // wait for a second
}

このボードではLED_BUILTINはIO2とライブラリで定義されている。
無事LEDが1秒間隔で点滅した。とりあえずボードは認識できているね。
次にTimer-CAMライブラリで入ったexampleも試しておく。

#include "M5TimerCAM.h"

void setup() {
    TimerCAM.begin();
}

void loop() {
    for (int16_t i = 0; i < 255; i++) {
        TimerCAM.Power.setLed(i);
        vTaskDelay(pdMS_TO_TICKS(10));
    }

    for (int16_t i = 255; i >= 0; i--) {
        TimerCAM.Power.setLed(i);
        vTaskDelay(pdMS_TO_TICKS(10));
    }
}

#include "M5TimerCAM.h"
でライブラリを呼び出している。
こちらも特に問題なくLEDがpwmで明暗を繰り返している。
環境構築には問題なさそうだな。じゃあどうして以前作ったプログラムはコンパイルできないんだろうね。

どうも
# include battery.h
# include bm8563.h
が使えないらしい。どうなっているんだろうか。

M5TimerCAM.h

#ifndef _M5_TIMER_CAM_H_
#define _M5_TIMER_CAM_H_

#include "./utility/Power_Class.h"
#include "./utility/RTC8563_Class.h"
#include "./utility/Camera_Class.h"
#include "esp_camera.h"

namespace m5 {
class M5TimerCAM {
   private:
    /* data */
   public:
    void begin(bool enableRTC = false);
    Power_Class Power;
    RTC8563_Class Rtc;
    Camera_Class Camera;
};
}  // namespace m5
extern m5::M5TimerCAM TimerCAM;

#endif

なるほどbattery.h,bm8563.hともになくなって、Power_Class.h、RTC8563_Class.hに変わったというわけか。
とりあえずこの2つはincludeせず#include "M5TimerCAM.h"だけ入れてやればいけそう。

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

const char *ssid     = "your_SSID";
const char *password = "SSID_passward";

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

int waitingTime = 10; //Wait 10 seconds to google response.

int shootingIntervalSec = 600;   // 撮影間隔 (秒)

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

  WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0);

  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 = 10000000;
  config.pixel_format = PIXFORMAT_JPEG;
  config.frame_size = FRAMESIZE_UXGA;  // UXGA|SXGA|XGA|SVGA|VGA|CIF|QVGA|HQVGA|QQVGA
  config.jpeg_quality = 5;
  config.fb_count     = 1;
  config.grab_mode = CAMERA_GRAB_WHEN_EMPTY; //CAMERA_GRAB_LATEST;

  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 OV3660をOV2640に近づけるセッティング
  s->set_saturation(s, 2);//lower the saturation OV3660をOV2640に近づけるセッティング
  s->set_denoise(s, 1);//OV3660をOV2640に近づけるセッティング
  s->set_contrast(s, 1);//OV3660をOV2640に近づけるセッティング
  //drop down frame size for higher initial frame rate
  // s->set_framesize(s, FRAMESIZE_SXGA);
  delay(1000);
}

void loop() {
  saveCapturedImage();
  Serial.printf("Waiting for %u sec.\n", shootingIntervalSec);
//  delay(shootingIntervalSec * 1000);    // 次の撮影まで待つ。スリープしない時。  
  Serial.printf("esp_sleep_enable_timer_wakeup: %d\n", esp_sleep_enable_timer_wakeup((uint64_t)shootingIntervalSec * 1000ULL * 1000ULL)); //単位はマイクロ秒  スリープする時
  esp_deep_sleep_start();// スリープする時
}

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 = esp_camera_fb_get(); //最初の1枚は捨てる
    esp_camera_fb_return(fb); //最初の1枚は捨てる
    fb = esp_camera_fb_get();  //こちらを保存する
    if(!fb) {
      Serial.println("Camera capture failed");
      delay(1000);
      ESP.restart();
      return;
    }
    Serial.printf("frame buffer size: %u x %u\n", fb->width, fb->height);
    
    Serial.println("Step 1: calicurating data size...");    // Base64とurlencodeされたデータのサイズを数える。

    int index = 0;
    uint8_t *p = fb->buf;
    int rest = fb->len;
    int base64EncodedSize = 0;
    int urlencodedSize = 0;
    while (rest > 0)
    {
      char output[2048 +1];    // 一度に出力するBase64化されたデータを入れるバッファ (base64_encode()が末尾にヌルを入れるので、1バイト追加。)
      int srcLen = rest > 1536 ? 1536 : rest;   // このサイクルでエンコードする元データサイズ(最大はバッファの 3/4 のサイズ)
      int encLen = base64_encode(output, (char *)p + index, srcLen);   // Base64エンコードする。
      base64EncodedSize += encLen;
      if (encLen > 0) {
        String str = urlencode(String(output));   // URLエンコードする。
        urlencodedSize += str.length();
      }
      index += srcLen;
      rest -= srcLen;
    }
    Serial.printf("frame buffer size: %u\n", fb->len);
    Serial.printf("after Base64 encoding: %u\n", base64EncodedSize);
    Serial.printf("frame buffer size: %u\n", urlencodedSize);

    Serial.println("Step 2: Sending a captured image to Google Drive.");
    String Data = myFilename + mimeType + myImage;    // POSTで送られるデータの先頭部分。これの後に画像をBase64化したものが続く。
    client.println("POST " + myScript + " HTTP/1.1");
    client.println("Host: " + String(myDomain));
    client.println("Content-Length: " + String(Data.length() + urlencodedSize));    // ここでデータの長さを書く必要があるので、Step 1 が必要。
    client.println("Content-Type: application/x-www-form-urlencoded");
    client.println();
    client.print(Data);

    index = 0;
    p = fb->buf;
    rest = fb->len;
    Serial.printf("Estimated cycle: %u\n", rest / 1536);
    while (rest > 0 && client.connected())
    {
      char output[2048 +1];    // 一度に出力するBase64化されたデータを入れるバッファ (base64_encode()が末尾にヌルを入れるので、1バイト追加。)
      int srcLen = rest > 1536 ? 1536 : rest;   // このサイクルでエンコードする元データサイズ(最大はバッファの 3/4 のサイズ)
      int encLen = base64_encode(output, (char *)p + index, srcLen);    // Base64エンコードする。
      if (encLen > 0) {
        String str = urlencode(String(output));   // URLエンコードする。
        client.write((uint8_t *)(str.c_str()), str.length());   // データを送信する。
        index += srcLen;
        rest -= srcLen;
        Serial.print(".");
      }
    }
    Serial.println();
    client.flush();

    // 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 * 1000) < 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;
}

とりあえず超適当な画像だが取得してGoogleDriveに保存まで成功。