工作と競馬2

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

水耕栽培の成長記録システム(3) --- ソフトウェア実装 ---

概要

前回に引き続き、写真データをクラウドに送信して保存するためのソフトウェアを構築する。



背景と目的

前回、ハードウェアの構築ができた。今回は、Unit-CAMのソフトウェアとクラウド側で写真データ受け取り部分を実装する。



詳細

重要部分のみ記載する。

1. 撮影装置

dekuo-03.hatenablog.jp

Unit-CAMで撮影するための基本的なソフトウェアは、arduino-esp32のWiFiCameraServerを基にした上記記事で実験済みなので、これを基本として構築する。

1.1 撮像部

撮像部については、上記記事のピン配置等も同様。カメラの細かい設定は以下とし、それ以外はarduino-esp32WiFiCameraServerのデフォルトとした。SVGA(800*600)は決して解像度が高いとは言えないのだが、PSRAMが載っていないため仕方ないのと今回の用途としては十分なのでとりあえず問題ではない。

  config.pixel_format = PIXFORMAT_JPEG;
  config.frame_size = FRAMESIZE_SVGA;
  config.jpeg_quality = 10;
  config.fb_count = 1;

1.2 送信部

今回は、ESP32側のメモリ使用量が少なくて済むようにjpgバイナリをそのまま送信したい。送信側ではContent-Typeヘッダにapplication/octet-streamを設定して送信する。 また、ESP32の送信用バッファのサイズの制約か、WiFiClientSecureオブジェクトのwriteメソッドで一度に大きなデータを書いてしまうとうまく動かないので、1kbyteに分割して書き込むという工夫をしている。

#include <WiFiClientSecure.h> // Wi-Fiに接続して、HTTPS通信するためのライブラリ

const int TIMEOUT_IN_MSEC = 5000; // タイムアウト[msec]

// POSTリクエストを送信する
// respBody: レスポンス文字列
// respBodyLen: 配列大きさ
void post_request(const char* host, const char* path, const uint8_t* payload, size_t payload_len, const char* machineId, char* respBody, unsigned long respBodyLen) {

  // PSRAM用??
  char * ptr = (char *) malloc(sizeof(WiFiClientSecure));
  WiFiClientSecure* client = new (ptr) WiFiClientSecure;

  // v1.0.6
  client->setInsecure();
  
  if (!client->connect(host, 443)) { // 接続にトライ
    // 接続失敗
    client->stop();
    return;
  } else {
    // 接続成功
    size_t contentLength = payload_len; //strlen(payload);

    // ヘッダ
    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/octet-stream");
    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 *) 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(); // 空行を最後に

    // レスポンス待ち
    Serial.printf("Waiting for reponse...");
    unsigned long curtime = millis();
    while (client->available() == 0) {
      if (millis() - curtime > TIMEOUT_IN_MSEC) {
        // タイムアウトのときは終了
        client->stop();
        return;
      }
    }

    // レスポンス受信
    unsigned long ci = 0;
    bool started = false;
    while (client->available()) {
      char c = client->read();
      if (c == '{') started = true;
      if (!started) continue;
      respBody[ci] = c;
      ci++;
      if (ci == respBodyLen) break;
    }
    respBody[ci] = '\0';
    
    // 接続終了
    client->stop();
  }

}


2. クラウド

AWS

  • APIGateway
  • Lambda
  • S3

を組み合わせる。

2.1 APIGateway

APIGatewayを介してLambdaがjpgバイナリを受け取るには、APIGateway側の設定において

  • APIの設定でバイナリメディアタイプとして受け取るContent-Typeを設定
  • マッピングテンプレートでbase64エンコードされた文字列がLambdaに引き渡されるようにとして受け取るように設定

が必要。具体的には、APIの設定は

f:id:dekuo-03:20220327121336p:plain

となる。また、マッピングテンプレートでは、$input.bodyという変数にbase64エンコードされた画像データが格納されているので、テンプレートが定義されていないときのテンプレートを基に、body-json内にキー(ここでは、imgDataというもの)を定義し、$input.bodyを入れた。

f:id:dekuo-03:20220327121328p:plain

2.2 Lambda

受け取ったbase64文字列をバイナリに戻す。あとは適宜S3等に書き込めばよい。

import base64
import boto3

s3 = boto3.client("s3")

# バイナリに戻す
img_binary = base64.b64decode(event["body-json"]["imgData"])

# バイナリをBodyに入れる
s3.put_object(
    Bucket=バケット名,
    Key=保存先,
    Body=img_binary
)


3. 動作確認

動かしてみたところ、無事クラウド側に保存できた。


まとめと今後の課題

写真を撮って、クラウドに保存する仕組みが構築できた。いよいよ芽ねぎの栽培を始める。