工作と競馬2

電子工作、プログラミング、木工といった工作の記録記事、競馬に関する考察記事を掲載するブログ

家庭菜園の成長記録用カメラの製作(3) --- ソフトウェア製作、完成 ---

概要

家庭菜園の成長記録用カメラのソフトウェア部分の製作を行い、完成させた。



背景と目的

前回、ハードウェア製作を行った。今回は、ソフトウェア製作を行って完成させた。



詳細

1. 全体構成

arduino-esp32のesp-cameraサンプルを基に、撮影したデータをWi-Fiクラウドストレージ(Amazon S3)へ送信する。大まかな流れは、

  • カメラの初期化
  • 撮影
  • 送信
  • スリープ

という感じ。以下、すべてではないが工夫が必要だった点を中心にメモ。

1.1 インクルードファイル

  • HttpRequest.hはjsonをHTTPでpostするための自作ライブラリ(後述)。
  • personは、json用ライブラリ。送信後のレスポンス処理のため。
  • libb64は、arduino-esp32同梱のbase64ライブラリ。送信前のbase64エンコード処理用(後述)。
#include <WiFiClientSecure.h>
#include "esp_camera.h"
#include "Esp.h"
#include "HttpRequest.h"
#include "esp_sleep.h"
#include "parson.h"

extern "C" { // base64エンコード用ライブラリ
#include "libb64/cdecode.h"
#include "libb64/cencode.h"
}


2. カメラの初期化

ピンの設定は以下。

// ピン設定
#define Y2_GPIO_NUM 32
#define Y3_GPIO_NUM 35
#define Y4_GPIO_NUM 34
#define Y5_GPIO_NUM 5
#define Y6_GPIO_NUM 39
#define Y7_GPIO_NUM 18
#define Y8_GPIO_NUM 36
#define Y9_GPIO_NUM 19
#define XCLK_GPIO_NUM 27
#define PCLK_GPIO_NUM 23
#define VSYNC_GPIO_NUM 25
#define HREF_GPIO_NUM 26
#define SIOD_GPIO_NUM 21
#define SIOC_GPIO_NUM 22
#define PWDN_GPIO_NUM -1 // esp-camera.hのマクロは使用しなかった
#define RESET_GPIO_NUM 4
#define PWDN_GPIO_NUM_EXT 33 // PWDNをesp-camera.hとは別に制御するため

まず、PWDNを解除。

  pinMode(PWDN_GPIO_NUM_EXT, OUTPUT);
  digitalWrite(PWDN_GPIO_NUM_EXT, LOW);
  delay(1000);

カメラの初期化は、以下に注意。

  • config.xclk_freq_hzは20MHzだと、通信がうまくいかず画像がうまく撮れない場合があるので10MHzとした。ユニバーサル基板に実装したせいだと思う。ちゃんとしたプリント基板しないと性能が出なそうだ。
  • config.frame_sizeはOV2640で使える最大のUXGAを指定。
  • config.jpeg_qualityは、こちらで検証した結果を基に10とした。
  • config.fb_countは、静止画しかとらないので1でも2でもよさそう。
  // camera_config_t
  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; // 20MHzだとUXGAがうまく撮れないのでクロックを下げてフレームレートを多少犠牲にする
  config.pixel_format = PIXFORMAT_JPEG;
  config.frame_size = FRAMESIZE_UXGA;
  config.jpeg_quality = 10;
  config.fb_count = 1;

  // 初期化
  esp_err_t err = esp_camera_init(&config);
  if (err != ESP_OK) {
    // エラーの時は適宜終了させる
  }

  // カメラコンテキスト取得
  sensor_t * s = esp_camera_sensor_get();

  // カメラ設定
  s->set_framesize(s, FRAMESIZE_UXGA); 


3. 撮影

こちらで検証した結果を基に、予備撮影と本撮影の2段階とした。

3.1 撮影用関数

camera_fb_t* capture(int64_t * fr_time) {
  int64_t fr_start = esp_timer_get_time();
  camera_fb_t * fb = esp_camera_fb_get();
  if (!fb) {
    Serial.println("Camera capture failed");
    return fb;
  }  
  int64_t fr_end = esp_timer_get_time();  
  *fr_time = fr_end - fr_start;
  return fb;
}

3.2 予備撮影

こちらで検証した結果を基に、30回予備撮影を行う。ちょっと多すぎるが、多い分には問題はない。

#define PRE_CAPTURING 30 // 事前撮影回数

:

  int k = 0;
  int64_t fr_time;
  while (k < PRE_CAPTURING) {
    camera_fb_t * fb2 = capture(&fr_time);
    esp_camera_fb_return(fb2);
    k++;
  }

3.3 本撮影

こちらで検証した結果を基に、最低データサイズ75kBを超えるまで最大70回行う。

#define CAPTURE_RETRY_LIMIT 70 // リトライ回数
#define JPG_MINSIZE 75 * 1024 // 最小サイズ[bytes]

:

  camera_fb_t * fb;
  int captureRetry = 0;
  while (1) {
    fb = capture(&fr_time);
    if (fb->len >= JPG_MINSIZE || captureRetry >= CAPTURE_RETRY_LIMIT - 1) {
      // 最低サイズ以上の大きさがあるかリトライ制限到達でループ脱出
      break;
    }
    esp_camera_fb_return(fb);
    captureRetry++;
  }

  // PWDNを有効化
  digitalWrite(PWDN_GPIO_NUM_EXT, HIGH);

4. 送信

4.1 base64エンコード

今回使用している送信先エンドポイントがJSON=文字列しか受け付けない仕様のため、jpg画像のバイナリデータをbase64エンコードする。処理には、arduino-esp32同梱のbase64ライブラリlibb64がを利用した。また、画像データはサイズが大きく、base64エンコードするとさらに1.3倍くらいに大きくなるので、ESP32-WROVERのPSRAM上で作業する必要がある。そこで、以下のように、ps_calloc関数を使ってエンコード後の文字列格納先encodedを確保している。

// PSRAM上で確保
char * encoded = (char *) ps_calloc(JPG_MAXSIZE, sizeof(char));
encoded[0] = '\0';

4.2 リクエストボディの組み立て

リクエストボディには、base64エンコードされた画像データが含まれるので、組み立てをESP32-WROVERのPSRAM上でやる必要があるのだが、残念ながらperson.hを使用した場合にうまくいかなかったので、4.1と同様、変数をPSRAMに確保してstrcatで地道にJSON文字列を組み立てた。こういうときは、面倒だが泥臭い方法が役立つ。

4.3 送信

ここでもリクエストボディが大きいことが原因で躓いたため、いくつか工夫している。

まず、WiFiClientSecureクラスのインスタンスを作成するときに、ps_mallocでWiFiClientSecureクラスのサイズのメモリを確保し、そのポインタを取得。

  char * ptr = (char *) ps_malloc(sizeof(WiFiClientSecure));
  WiFiClientSecure* client = new (ptr) WiFiClientSecure;

この場合、WiFiClientSecureのサンプルのようなclient.メソッド名ではなく、->を使って

client->connect(host, 443)

となる。 そして、ここが一番重要だが、大きなbodyを出力する場合には、1kB程度で分割して送信しないとうまくいかないことがわかった。そこで、以下のように分割して、繰り返しclient->write、client->flushをしている。

    // 一度に大きなデータをprintfしたりwriteするとうまくいかない
    // mバイトに分割してwriteする
    int m = 1024;
    int i, j = 0;
    uint8_t * p = (uint8_t *) ps_malloc(m);
    while (true) {
      for (i = 0; i < m; i++) {
        p[i] = payload[i + j * m];
        if (i + j * m == contentLength - 1) break;
      }
      //Serial.printf("j=%d, length=%d\n", j, i);
      client->write(p, m);
      client->flush();
      if (i + j * m == contentLength - 1) break;
      j++;
    }
    client->println(); // 空行を最後に

5. スリープ

最後に、カメラ非稼働の間消費電力を下げるため、ESP32-WROVERをディープスリープさせる。今までesp_deep_sleepを使用できていたが、廃止になる旨の警告が出るようになったので、esp_sleep.hで実装する。

  // sleepの設定
  esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_OFF);
  esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_SLOW_MEM, ESP_PD_OPTION_OFF);
  esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_FAST_MEM, ESP_PD_OPTION_OFF);
  esp_sleep_pd_config(ESP_PD_DOMAIN_MAX, ESP_PD_OPTION_OFF);

ディープスリープで気を付けなければならないのは、uint64_t型の数値を使って計算したものをesp_sleep_enable_timer_wakeupに渡すこと。esp_sleep_enable_timer_wakeupの引数は、microsecond単位の値なので、32ビット整数だとたったの4,5時間しか表現できない。(1日は86400*106[us]≒36ビットなので、4ビット分程度はみ出る。) なので、秒数sleepTimeInSec、係数1000000をuint64_t型でキャストしてから掛け算する。なお、sleepTimeInSecは、画像送信時に次回起動時刻を考慮してサーバ側が計算して送り返してきている。これにより、カメラ側で時刻を管理するハードを持ったり、NTPサーバへのアクセスをしなくても、決まった時刻に起動して撮影することができる。

  // スリープ時間を算出(usec)
  uint64_t us = (uint64_t) sleepTimeInSec * (uint64_t) 1000000; // 待ち時間[us]を計算

  // スリープを実行
  esp_sleep_enable_timer_wakeup(us); // esp_sleep_enable_timer_wakeupの戻り値は役に立たないので使用しない
  esp_deep_sleep_start();

6. 動作確認

1週間くらい稼働させ、サーバーが指示した定刻に、撮影して送信するという動作をしてくれた。ということで、無事完成。


まとめと今後の課題

家庭菜園の成長記録用カメラが完成した。