概要
家庭菜園の成長記録用カメラのソフトウェア部分の製作を行い、完成させた。
背景と目的
前回、ハードウェア製作を行った。今回は、ソフトウェア製作を行って完成させた。
詳細
1. 全体構成
arduino-esp32のesp-cameraサンプルを基に、撮影したデータをWi-Fiでクラウドストレージ(Amazon S3)へ送信する。大まかな流れは、
という感じ。以下、すべてではないが工夫が必要だった点を中心にメモ。
1.1 インクルードファイル
#include <WiFiClientSecure.h>
#include "esp_camera.h"
#include "Esp.h"
#include "HttpRequest.h"
#include "esp_sleep.h"
#include "parson.h"
extern "C" {
#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
#define RESET_GPIO_NUM 4
#define PWDN_GPIO_NUM_EXT 33
まず、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
:
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++;
}
digitalWrite(PWDN_GPIO_NUM_EXT, HIGH);
4. 送信
今回使用している送信先エンドポイントがJSON=文字列しか受け付けない仕様のため、jpg画像のバイナリデータをbase64にエンコードする。処理には、arduino-esp32同梱のbase64ライブラリlibb64がを利用した。また、画像データはサイズが大きく、base64エンコードするとさらに1.3倍くらいに大きくなるので、ESP32-WROVERのPSRAM上で作業する必要がある。そこで、以下のように、ps_calloc関数を使ってエンコード後の文字列格納先encodedを確保している。
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をしている。
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;
}
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で実装する。
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サーバへのアクセスをしなくても、決まった時刻に起動して撮影することができる。
uint64_t us = (uint64_t) sleepTimeInSec * (uint64_t) 1000000;
esp_sleep_enable_timer_wakeup(us);
esp_deep_sleep_start();
6. 動作確認
1週間くらい稼働させ、サーバーが指示した定刻に、撮影して送信するという動作をしてくれた。ということで、無事完成。
まとめと今後の課題
家庭菜園の成長記録用カメラが完成した。