概要
家庭菜園の成長記録用カメラのソフトウェア部分の製作を行い、完成させた。
背景と目的
前回、ハードウェア製作を行った。今回は、ソフトウェア製作を行って完成させた。
詳細
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週間くらい稼働させ、サーバーが指示した定刻に、撮影して送信するという動作をしてくれた。ということで、無事完成。
まとめと今後の課題
家庭菜園の成長記録用カメラが完成した。