概要
ESP32-WROVER-Eを使って、OV2640使用200万画素カメラで撮ったUXGAの画像をクラウドストレージに送信できた。
背景と目的
以前、ESP-WROOM-32を使って、OV2640使用200万画素カメラを動かしたのだが、ESP-WROOM-32のRAM不足により最大解像度UXGAでの撮影がうまくいかなかった。そこで、RAMが増強されたESP32=WROVER-Eを使って、UXGAでの撮影を試す。
詳細
0. やること
1. ESP32-WROVER-E
ESP32-WROVER-Eは、こちらに書いた通り、ESP-WROOM-32にPSRAMと呼ばれる外付けのRAMが備わったもの。
2. 撮影
2.1 ピン設定で間違えてハマる
今回の撮影部分のプログラムは、以前の記事に挙げてあるesp_cameraのサンプルを少し修正したプログラムをそのまま使ったのだが、実はピン設定がおかしかったことに気づいたので修正。PWDN_GPIO_NUM、RESET_GPIO_NUMは接続なしとする必要があった。実は、この間違いに全く気付くまで、プログラムの途中でおかしな再起動が多発し、かなりの時間を費やしてしまった。再起動が起こったある箇所を修正すると、今度は影響がないはずの前方の箇所で再起動が発生するという不思議な現象が起きていた。結局、このあたりからヒントを得て、どうにか修正にたどり着いた。
// ピン設定 #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 -1 // 変更
2.2 カメラ設定
UXGAで撮るには、config.frame_sizeを変更。
// if PSRAM IC present, init with UXGA resolution and higher JPEG quality // for larger pre-allocated frame buffer. if (psramFound()) { Serial.println("PSRAM found."); config.frame_size = FRAMESIZE_UXGA; // UXGAを選択 config.jpeg_quality = 5; config.fb_count = 1; } else { :
2.3 撮影部分
esp_camera_fb_get関数で実行するだけ。pixel_formatをPIXFORMAT_JPEG(サンプルそのまま)なので、fb->bufには、jpgのバイナリデータが格納される。
// 撮影 camera_fb_t * fb = esp_camera_fb_get(); if (!fb) { Serial.println("Camera capture failed"); return fb; }
3. アップロード
今回の送信先は、AWSのAPIGatewayのエンドポイント。JSONで受けて、バックエンドのLambdaでS3に保存する。なので、送信元はjpgバイナリデータをいったんbase64エンコードしてテキストに変換しJSONに格納する。(本当はapplication/octet-streamでバイナリを送った方がデータ量が少なくて済むが、クラウド側ができてしまっているので今回は不採用)
3.1 base64エンコード
extern "C" { #include "libb64/cdecode.h" #include "libb64/cencode.h" }
以下のような関数を用意して呼び出して使う。
// base64エンコード void base64_encode(uint8_t * packet, int packet_len, char * encoded) { base64_encodestate _state; base64_init_encodestate(&_state); int len = base64_encode_block((const char *) &packet[0], packet_len, &encoded[0], &_state); len = base64_encode_blockend((encoded + len), &_state); }
なお、呼び出し側で用意するencodedというエンコード結果格納用配列は、ps_callocを使ってPSRAM上にメモリ確保している。(またはps_malloc)UXGAのjpgバイナリは200kB程度があり、ESP32内部のRAMでは確保できないためだ。PSRAMがあると、できることが増えるというのを実感した。
char * encoded = (char *) ps_calloc(size, sizeof(char)); if (encoded == NULL) { Serial.printf("encoded=NULL!\n"); while (1) {} } encoded[0] = '\0'; Serial.printf("ps_calloc encoded at %x, size=%lu\n", &encoded[0], (unsigned long) size);
3.2 POST
実は、ここでもかなり悩まされてしまった。 ESP32でJSONをPOSTするために以前より自作して使用していた関数を今回も使ったところ、JSON BodyをWiFiClientSecure.printlnした直後に固まってしまった。いろいろ試行錯誤した結果、どうやらBodyのサイズが大きいと固まるらしく、Bodyを分割して複数回WiFiClientSecure->writeするとうまくいった。(POST POST自体を複数回行うという手もやってみたが、あまりに送信に時間がかかるのでボツ)
なので、以下のように、Bodyを分割してwriteするように関数を修正した。 なお、WiFiClientSecureのインスタンス化は、インスタンスをPSRAM上に確保するためにややこしい処理になっている。clientという変数は、WiFiClientSecureインスタンスではなくそのポインタ。そのせいで、各呼び出しが、client.***ではなく、client->になっている。(上述のBody送信後フリーズはが、WiFiClientSecureのインスタンスがRAM不足に起因すると思ってPSRAM上に確保してみたのだが、WiFiClientSecureのインスタンス自体はInternal RAMでも実は問題ない気もする。。。あとで余裕があれば試す。)
#include <WiFiClientSecure.h> // Wi-Fiに接続して、HTTPS通信するためのライブラリ const int TIMEOUT_IN_MSEC = 60000; // タイムアウト[msec] // POSTリクエストを送信する // payload: 送信したいJSON Bodyのテキスト void post_request(const char* host, const char* path, const char* payload) { // PSRAM上にインスタンスを確保するためのps_mallocとnew char * ptr = (char *) ps_malloc(sizeof(WiFiClientSecure)); WiFiClientSecure* client = new (ptr) WiFiClientSecure; Serial.printf("client at %x\n", client); Serial.printf("Trying to connect to %s...\n", host); if (!client->connect(host, 443)) { // 接続にトライ // 接続失敗 Serial.println("Connection failed!"); } else { // 接続成功 Serial.println("Connection success!"); size_t contentLength = strlen(payload); Serial.printf("Content-Length: %d, Body=\"%c ... %c\"\n", contentLength, payload[0], payload[contentLength - 1]); // ヘッダ client->printf("POST %s HTTP/1.1\n", path); // POSTメソッド client->printf("Host: %s\n", host); // HTTP1.1で必須のヘッダ, アクセス先サーバーとしておく(そうしないとクロスドメインアクセスになる) client->println("Connection: close"); client->println("Content-Type: application/json"); client->printf("Content-Length: %d\n", contentLength); client->println(); // ヘッダの最後は空行 // ボディ // 一度に大きなデータを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(); client->println(); // 空行を最後に // レスポンス待ち Serial.println("Waiting for reponse..."); unsigned long curtime = millis(); while (client->available() == 0) { if (millis() - curtime > TIMEOUT_IN_MSEC) { // タイムアウトのときは終了 Serial.println(">>> Client Timeout !"); client->stop(); return; } } // レスポンス受信 if (client->connected()) { while (client->available()) { char c = client->read(); Serial.write(c); // 1文字ずつ出力 } Serial.println(""); } // 接続終了 client->stop(); } }
4. 動作確認
無事、S3上に保存できた。
まとめと今後の課題
とにかく、ピン設定のミスと、Bodyの分割writeでハマってしまい非常に苦労した。ESP32-CAMのような出来上がっている基板を買えばもっとずっと楽だっただろう。とはいえ、ここまでたどり着けたので良しとする。