tAROの試行錯誤

技術的なことを色々試した過程を記録

Timer Camera Xで定点観測カメラを作る(その5)

前回からの続きです。

isogabamaware.hatenadiary.jp

前回、前々回で以下の4を作ったので、今度こそ5を作ります。

  1. Node-REDでhttpで画像を送受信し、表示する環境を作る
  2. TimerCameraXで撮影した画像をPC上で確認する
  3. TimerCameraXからhttpでNode-REDに画像を送信する
  4. TimerCameraXをディープスリープさせて定期的に撮影するようにする
  5. AWS部分を作り、node-redから画像を送信する
  6. TimerCameraXとAWSをつなぐ

以下の図のAWS部分を作っていきます。

f:id:tARO:20210124223326p:plain

どこから作ろうかなと思うのですが、S3は使ったことがないので、S3から作っていきます。

AWSでルートユーザーで開発作業するのは怖いので、IAMユーザーを作っています。 今回、APIGateway、Lambda、S3を使うため、そのIAMユーザーが参加しているIAMグループに以下のポリシーをアタッチします。 ポリシーをアタッチする作業自体はルートユーザーで行います。

f:id:tARO:20210131225815p:plain

アタッチしたら、次はIAMユーザーでログインします。 ほぼ、以下の参考サイト通りの設定で作っていきます。

qiita.com

まずは、S3のバケットをデフォルト設定で非公開で作成します。 S3の画面でバケットを作成を押して、バケット名を入力後、デフォルトでバケットを作成します。これでS3は完成です。

次に、LambdaでS3に画像を保存する部分を作ります。 LambdaをPython3.8で一から作成で作ります。 このままではS3にアクセスできないため、アクセス権限を変更します。 アクセス権限のタブから、実行ロールのロール名をクリックして、ロールの編集画面にいきます。 そこでポリシーをアタッチしますを押して、S3で検索して、AmazonS3FullAccessを選択し、ポリシーのアタッチを押します。 これで先程作成したLambdaからS3にアクセスできるようになりました。

次に、Python部分を実装してみます。 今回は、http postでjsonの中に'jpeg'という名前でbase64データが格納されているのを受け取ることになります。 event['jpeg']でその中身を取り出すことができます。 取り出したbase64データをデコードして、S3に書き込みます。 この辺りは上の参考サイトを参考にします。 ファイル名はひとまず、Lambdaが実行された現在時刻にします。 ここは後々撮影時刻に変更しようと思います。 参考サイトではput_objectのContentTypeが'image/png'でしたが、今回はjpegを保存するため、'image/jpg'に変更します。 あとは、例外発生時にエラーを返すように実装を追加します。 以下のように実装できました。

import boto3
import base64
import datetime

def get_now():
    now = datetime.datetime.now(datetime.timezone(datetime.timedelta(hours=9))) # 日本時刻
    return now.strftime('%Y%m%d%H%M%S')

def convert_b64_string_to_bynary(s):
    """base64をデコードする"""
    return base64.b64decode(s.encode("UTF-8"))

def lambda_handler(event, context):
    try:
        base_64ed_image = event['jpeg']
        s3 = boto3.resource('s3')
        bucket = s3.Bucket('s3-timer-camera-x')
        now = get_now()
        bucket.put_object(
                        Key=f'{now}.jpg',
                        Body=convert_b64_string_to_bynary(base_64ed_image),
                        ContentType='image/jpg')
        return {'statusCode': 200}
    except:
        return {'statusCode': 400}

次にLambdaのテストを実行して、想定通りに動作しているかを確認します。 初めに、異常系のテストをします。入力ペイロードjpegがない場合に{'statusCode':400}を返すことを確認するためにテストを作成します。 Lambdaの右上の方にあるテストイベントの設定のところから入力となるjsonを作成します。 イベントテンプレートとして、hello-worldを選択し、そのまま作成します。 作成できたらテストボタンを押してみます。 実行結果が成功となり、戻り値が以下の通り、想定通りです。

{
  "statusCode": 400
}

次に、正しい入力が来たときにS3にjpegが書き込まれているかを確認してみます。 ここで(その1)で調べたjpgのbase64変換を行い、クリップボードへコピーし、それをテストに{jpeg:base64データ}となるようにbase64データを貼り付けます。

base64 sample.jpg | pbcopy

できたら、テストボタンを押してみます。 成功となりました。

{
  "statusCode": 200
}

正しくS3に保存されたかどうか、S3の方を見てみます。 jpgファイルが保存されているのでうまくいっています。 jpgファイルをダウンロードして、元のjpgを同じかどうか確認してみます。

これで、Lambda-S3部分はできました。

次は、API Gatewayを作ります。

API Gatewayの画面から、APIの作成を選択します。 APIタイプはREST APIを選択します。 API名を入力し、作成します。 APIを作成したら、次にリソースを作成します。 アクションからリソースの作成を選択し、リソース名を入力します。 次にそのリソースを選択した状態で、メソッドを作成します。 メソッドはPOSTを選択し、チェックボタンを押します。 統合タイプはLambda関数を選択し、Lambda関数欄で上で作成したLambda関数を指定します。 これでほぼ完成です。

APIテストをしてみましょう。 以下の画面のテストボタンを押してみます。

f:id:tARO:20210131230144p:plain

リクエスト本文にLambdaのテストと同じjsonを入力し、同じ動作をすることを確認します。 正常系で200、異常系で400が返ってきたらうまくいっています。 S3を見に行くと新しく現在時刻でjpg画像が保存されています。

このままデプロイすれば、PCからAPIGatewayにPOSTできるようになるはずですが、それでは世界中の誰からでもアクセスできてしまうため、簡単な認証っぽいものを作ります。

この辺りを参考にAPIキーを設定します。

https://qiita.com/baikichiz/items/ed787c5c79059213401e

デプロイまでできたら、curlを用いてAPIの動作確認を行います。 まずは、試しにPOSTしてみます。

curl -X POST -d "{\"key\":\"value\"}" https://~~~~~~
.execute-api.ap-northeast-1.amazonaws.com/test/image
{"message":"Forbidden"}

APIキーを設定していないので、{"message":"Forbidden"}が返ってきます。

続いて、APIキーを入力してみます。

curl -X POST -d "{\"key\":\"value\"}" https://~~~~~~
.execute-api.ap-northeast-1.amazonaws.com/test/image -H "x-api-key:....."
{"statusCode": 400}

jsonが間違っているので400が返ってきます。 続いて、key:valueの部分をlambdaのテストで使ったjpeg:base64データに変えて送ってみます。

curl -X POST -d "{\"jpeg\":\"/9j/4AAQSkZJR...\"}" https://~~~~~~~~.execute-api.ap-northeast-1.amazonaws.com/test/image -H "x-api-key:......."
{"statusCode": 200}

{"statusCode": 200}が返ってきました。 S3を見てみましょう。 新しくjpg画像が保存されています。成功です。

これでAWS部分は完成です。 夜も遅くなってきたので、今回はここまでにします。

ここまでで部品はほぼできたので、次回は全て結合して、定点観測カメラを完成させたいと思います。

Timer Camera Xで定点観測カメラを作る(その4)

前回からの続きです。

isogabamaware.hatenadiary.jp

前回は以下の4を作ったので、今回は5を作ります。

  1. Node-REDでhttpで画像を送受信し、表示する環境を作る
  2. TimerCameraXで撮影した画像をPC上で確認する
  3. TimerCameraXからhttpでNode-REDに画像を送信する
  4. TimerCameraXをディープスリープさせて定期的に撮影するようにする
  5. AWS部分を作り、node-redから画像を送信する
  6. TimerCameraXとAWSをつなぐ

と思ったのですが、前回は画像をプレーンテキストで送っていたため付加情報が送れていませんでした。 そこで、今回は4の続きとして、画像をJSONで送ってみます。

4. (続き)TimerCameraXをディープスリープさせて定期的に撮影しJSONで送る

JSONをhttp POSTする方法を調べるために前々回の参考サイトをもう一度見てみます。

randomnerdtutorials.com

 // If you need an HTTP request with a content type: application/json, use the following:
 //http.addHeader("Content-Type", "application/json");
 //int httpResponseCode = http.POST("{\"api_key\":\"tPmAT5Ab3j7F9\",\"sensor\":\"BME280\",\"value1\":\"24.25\",\"value2\":\"49.54\",\"value3\":\"1005.14\"}");

この部分を参考にしてJSONで送ってみます。 せっかくJSONで送るので、画像だけでは面白くないので、バッテリーの電圧も送ってみます。 ちょっとブサイクですが、こんな感じになりました。

    http.addHeader("Content-Type", "application/json");
    String json_data = String("{\"jpeg\":\"") + String(reinterpret_cast<char*>(base64buff))
      + String("\",\"battery\":") + String(bat_get_voltage()) + "}";
    int httpResponseCode = http.POST(json_data.c_str());

受信側のNode-REDもJSONを受け取るように変更します。

f:id:tARO:20210127224625p:plain

http inノードの後ろにjsonJavaScriptオブジェクトに変換するノードを追加します。 その後、jpegを画像表示のdashboardのtemplateノードへ、batteryをグラフ表示のためのchartノードへつなぎます。

無事ダッシュボードに撮影画像とグラフが表示されたので、バッテリーがどの程度持つか、バッテリーが切れるまでグラフを描き続けてみようと思います。 60秒スリープだと半日程度時間がかかってしまうので、5秒スリープに変更し1時間程度でバッテリー切れになることを期待します。

f:id:tARO:20210127224709p:plain

PCからUSBを抜いたのがグラフの20:43:05,4089mvです。 ここからバッテリー駆動になり徐々に電圧が下がっているのがわかります。 また1時間後にみてみたいと思います。

f:id:tARO:20210127224734p:plain

22:05:32,3591mvを最後に更新されなくなりました。 スリープ時間は5秒に設定しているのですが、起動やWiFi接続に2秒程度かかり、7秒間隔で画像が送られてきていました。 1時間22分27秒(4947秒)バッテリーで駆動したので、約700枚撮影できたことになります。 ここから1時間に1回撮影した場合を計算すると、700/24で約29日とほぼ公表スペック通り1ヶ月動作させることができることがわかりました。 なかなかよくできていますね。

Timer Camera Xで定点観測カメラを作る(その3)

前回からの続きです。

isogabamaware.hatenadiary.jp

前回は以下の3を作ったので、今回は4を作ります。 これができれば1ヶ月電池駆動カメラはひとまず完成です。

  1. Node-REDでhttpで画像を送受信し、表示する環境を作る
  2. TimerCameraXで撮影した画像をPC上で確認する
  3. TimerCameraXからhttpでNode-REDに画像を送信する
  4. TimerCameraXをディープスリープさせて定期的に撮影するようにする
  5. AWS部分を作り、node-redから画像を送信する
  6. TimerCameraXとAWSをつなぐ

4. TimerCameraXをディープスリープさせて定期的に撮影するようにする

電源周りは全然わからないので、公式の二つしかないサンプルのweb_camじゃない方のwakeupの方を見てみます。

github.com

おそらく、電源が入ればLEDが点滅して、その後5秒スリープして、また電源が入る、の繰り返しをしていると思われます。

最後の2行はUSBが繋がっている時だけ通過するようです。 早速スケッチを転送してみると、思った通り、5秒に1回LEDが明るくなる、暗くなるを繰り返します。 USBを差しても抜いても同じ動作をするのは想定通りです。

ディープスリープの方法がわかったので、次は前回のカメラをディープスリープ対応します。 以下のコードで無事動きました。ただ、ディープスリープができているかはわからないので、一晩動かし続けてみようと思います。

今回は短めでここまでにします。

追記: 朝起きたらもう止まっていました。PC側のNode-REDも止めてしまっていたので、残念ながら何時に止まったかわかりませんでした。 次はNode-REDを起動したままで時計でも撮影してみようと思います。

#include "esp_camera.h"
#include <WiFi.h>
#include <HTTPClient.h>
#include <base64.hpp>
#include "camera_pins.h"
#include "battery.h"
#include "bmm8563.h"

const char* ssid = "";
const char* password = "";

void setup() {
  Serial.begin(115200);
  Serial.setDebugOutput(true);
  Serial.println();

  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 = 20000000;
  config.pixel_format = PIXFORMAT_JPEG;
  config.frame_size = FRAMESIZE_UXGA;
  config.jpeg_quality = 10;
  config.fb_count = 2;
 
  // camera init
  esp_err_t err = esp_camera_init(&config);
  if (err != ESP_OK) {
    Serial.printf("Camera init failed with error 0x%x", err);
    return;
  }

  sensor_t * s = esp_camera_sensor_get();
  //initial sensors are flipped vertically and colors are a bit saturated
  s->set_vflip(s, 1);//flip it back
  s->set_brightness(s, 1);//up the blightness just a bit
  s->set_saturation(s, -2);//lower the saturation

  //drop down frame size for higher initial frame rate
  s->set_framesize(s, FRAMESIZE_QVGA);

  if (ssid=="")
    WiFi.begin();
  else
    WiFi.begin(ssid, password);
  
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.println("WiFi connected");

  // バッテリー初期化
  bat_init();
  // タイマー初期化
  bmm8563_init();
  // 60秒後に起動設定
  bmm8563_setTimerIRQ(60);
}

void loop() {
  camera_fb_t *fb = esp_camera_fb_get();
  if ( fb ) {
    Serial.printf("width: %d, height: %d, buf: 0x%x, len: %d\n", fb->width, fb->height, fb->buf, fb->len);
    unsigned int base64_length = encode_base64_length(fb->len);
    unsigned char *base64buff = new unsigned char[base64_length+1];
    base64buff[base64_length] = '\0';
    encode_base64(fb->buf, fb->len, base64buff);
    char *serverName = "http://192.168.0.25:1880/image";
    HTTPClient http;
    http.begin(serverName);
    http.addHeader("Content-Type", "text/plain");
    int httpResponseCode = http.POST(reinterpret_cast<char*>(base64buff));
    Serial.printf("%d", httpResponseCode);
    delete [] base64buff;
    esp_camera_fb_return(fb);
  }

  rtc_date_t date;
  bmm8563_getTime(&date);
  Serial.printf("Time: %d/%d/%d %02d:%02d:%-2d\r\n", date.year, date.month, date.day, date.hour, date.minute, date.second);
  Serial.printf("volt: %d mv\r\n", bat_get_voltage());
 
  // バッテリー出力を無効化
  bat_disable_output();

  //USBが繋がっていたら通電しているのでここを通る
  esp_deep_sleep(60000000);
  esp_deep_sleep_start();
}

Timer Camera Xで定点観測カメラを作る(その2)

前回からの続きです。

isogabamaware.hatenadiary.jp

前回は以下の1、2を作ったので、今回は3を作ります。

  1. Node-REDでhttpで画像を送受信し、表示する環境を作る
  2. TimerCameraXで撮影した画像をPC上で確認する
  3. TimerCameraXからhttpでNode-REDに画像を送信する
  4. TimerCameraXをディープスリープさせて定期的に撮影するようにする
  5. AWS部分を作り、node-redから画像を送信する
  6. TimerCameraXとAWSをつなぐ

3. TimerCameraXからhttpでNode-REDに画像を送信する

撮影してbase64にしてシリアルモニタに表示するところまでは前回作ったので、今回はbase64のデータをhttp POSTします。 http post arduinoで検索します。

2番目に出てきたここが良さそうです。ちょうどNode-REDとやりとりするサンプルがあります。

randomnerdtutorials.com

http POSTの方法として以下の3通りがあるようです。

  1. URLに埋め込む
  2. JSONオブジェクトを送る
  3. プレーンテキストを送る

まずは一番簡単そうな3のプレーンテキストを試してみます。 これだけで送れそうですね。

#include <HTTPClient.h>

http.begin(serverName);
http.addHeader("Content-Type", "text/plain");
int httpResponseCode = http.POST("Hello, World!");

これの"Hello, World!"の部分をbase64の文字列に置き換えれば行けちゃいそうなので試してみます。 serverNameにはNode-REDの画像受信のURLを入れます。

char *serverName = "http://192.168.0.16:1880/image";

ということで以下がコード全体です。前回printのために終端文字を入れていたのがここでもそのまま使えました。 printしていたbase64buffをそのままhttp.POSTしたらできちゃいました。

#include "esp_camera.h"
#include <WiFi.h>
#include <HTTPClient.h>
#include <base64.hpp>
#include "camera_pins.h"

const char* ssid = "";
const char* password = "";

void setup() {
  Serial.begin(115200);
  Serial.setDebugOutput(true);
  Serial.println();

  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 = 20000000;
  config.pixel_format = PIXFORMAT_JPEG;
  config.frame_size = FRAMESIZE_UXGA;
  config.jpeg_quality = 10;
  config.fb_count = 2;
 
  // camera init
  esp_err_t err = esp_camera_init(&config);
  if (err != ESP_OK) {
    Serial.printf("Camera init failed with error 0x%x", err);
    return;
  }

  sensor_t * s = esp_camera_sensor_get();
  //initial sensors are flipped vertically and colors are a bit saturated
  s->set_vflip(s, 1);//flip it back
  s->set_brightness(s, 1);//up the blightness just a bit
  s->set_saturation(s, -2);//lower the saturation

  //drop down frame size for higher initial frame rate
  s->set_framesize(s, FRAMESIZE_QVGA);

  if (ssid=="")
    WiFi.begin();
  else
    WiFi.begin(ssid, password);
  
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.println("WiFi connected");
}

void loop() {
  camera_fb_t *fb = esp_camera_fb_get();
  if ( fb ) {
    Serial.printf("width: %d, height: %d, buf: 0x%x, len: %d\n", fb->width, fb->height, fb->buf, fb->len);
    unsigned int base64_length = encode_base64_length(fb->len);
    unsigned char *base64buff = new unsigned char[base64_length+1];
    base64buff[base64_length] = '\0';
    encode_base64(fb->buf, fb->len, base64buff);
    char *serverName = "http://192.168.0.16:1880/image";
    HTTPClient http;
    http.begin(serverName);
    http.addHeader("Content-Type", "text/plain");
    int httpResponseCode = http.POST(reinterpret_cast<char*>(base64buff));
    Serial.printf("%d", httpResponseCode);
    delete [] base64buff;
    esp_camera_fb_return(fb);
  }
  delay(100);
}

せっかくなのでカメラから送信してNode-REDのダッシュボードに表示した画面を貼ってみます。 Macではshift+command+5で範囲を指定したスクリーンキャプチャ動画が取れるみたいです。

support.apple.com

範囲を指定して、キャプチャボタンを押したのですが、止め方がわからない・・・と思ったら、タッチバーに停止ボタンがありました。 撮影完了したらgif動画に変換します。 今後のことも考えてffmpegを入れておきます。 ちょうど以下のサイトにffmpegをインストールしてmovをgifに変換するサンプルがあったので、その通り実行してgif動画を作成します。

codelab.website

というわけで、今回の成果物です。 思ったより簡単にTimerCameraXから定期的に画像を送信することができました。

f:id:tARO:20210125221631g:plain

無事やりたいことに近づいています。 次回はTimerCameraの名前の通り、定期的にディープスリープして撮影する部分を試してみます。 電源周りは全くわかっていないので、できるかどうか、、、

Timer Camera Xで定点観測カメラを作る(その1)

バッテリー内蔵で、通信できて、自由にプログラミングができて、安い(5000円以下)カメラが欲しいとずっと思っていました。 年明けにスイッチサイエンスさんからTimer Camera Xというまさにそれを叶えるものが発売されたので、お年玉気分で即購入しちゃいました。

www.switch-science.com

特に、以下のスペックがお気に入りです。

スタンバイ時の電流はわずか2 μAで、1時間に1枚のタイマー撮影をオンにしても、バッテリーは1ヶ月以上連続で動作します。

買ってからしばらく経ってしまいましたが、何か作ってみたいと思います。

1時間に1枚のタイマー撮影で1ヶ月バッテリーが持つとのことですが、そのサンプルコードが見つからなかったため、勉強がてら作ってみたいと思います。

まずは最終的に作りたいものの構成図を書いてみました。

from urllib.request import urlretrieve
from diagrams import Cluster, Diagram, Edge
from diagrams.custom import Custom
from diagrams.aws.network import APIGateway
from diagrams.aws.compute import Lambda
from diagrams.aws.storage import S3

urlretrieve("https://cdn.shopify.com/s/files/1/0056/7689/2250/products/6_8b9883c8-d556-4794-b43f-6a060b03dca5_1200x1200.jpg?v=1601857650", "timercamx.jpg")

with Diagram("システム構成図", filename="system", show=True):
  cam = Custom("TimerCameraX\n1時間に1枚撮影", "timercamx.jpg")
  with Cluster("AWS"):
    apigateway = APIGateway("APIGateway")
    awslambda = Lambda("AWSLambda")
    s3 = S3("S3")
  cam >> Edge(label='画像データ送信') >> apigateway >> awslambda
  awslambda >> Edge(label="jpg") >>s3

f:id:tARO:20210124223326p:plain

いきなりAWSに送る構成になっていますが、TimerCameraXはSDカード等の外部ストレージを持たないので、撮影した画像はクラウドに保存します。 Google Drive等に直接保存してもいいのですが、保存先は後からでも変更できる方がいいと思い、まずはAWSのAPIGatewayに送信し、そこから先はクラウド側で処理するようにします。 今回はAPIGatewayにAWSLambdaを接続し、AWSLambdaからS3に保存するようにします。

それでは、早速作っていきたいと思います。 ただ、いきなり最終構成で作っていくのはハードルが高いため、以下の6ステップで作っていきます。

  1. Node-REDでhttpで画像を送受信し、表示する環境を作る
  2. TimerCameraXで撮影した画像をPC上で確認する
  3. TimerCameraXからhttpでNode-REDに画像を送信する
  4. TimerCameraXをディープスリープさせて定期的に撮影するようにする
  5. AWS部分を作り、node-redから画像を送信する
  6. TimerCameraXとAWSをつなぐ

1. Node-REDでhttpで画像を送受信し、表示する環境を作る

ここでいきなりNode-REDが出てきます。

nodered.jp

Node-REDはブラウザ上でAPIが作成できたり、簡単なGUIが作成できたりと、IoTのプロトタイプを作るのに最適な環境で、以前から時々使っていて多少土地勘があるため使います。 MacBook Pro上のDockerで動作させているのですが、環境構築はこのあたりを参考にできている前提で進めます。

まずは、表示部分を作ります。 node-red 画像表示で検索して参考になりそうなところを探します。 上位何件かをみてみると、画像はどうやらbase64エンコードして、htmlに埋め込むのが常套手段のようです。 base64にさえしてしまえばdashboardのtemplateノードで埋め込みができそうです。

gist.github.com

上記のコードを見るとimgタグにbase64で指定すれば良さそうなので、html img base64でググって以下のページを見つけました。

edge.sincar.jp

最終的にはdashboardのtemplateノードに以下を記述すればOKでした。 <img src="data:image/jpg;base64,{{msg.payload}}"/>

動作確認するためにbase64のデータを用意します。 mac base64 encode jpgで検索して一番上に出てきたターミナルで画像をBase64化する方法(速度改善)を参考に、適当なjpgファイルをbase64エンコードして、| pbcopyクリップボードにコピーします。

base64 sample.jpg | pbcopy

コピーした文字列をinjectノードに貼り付けます。 そのinjectノードを先程のtemplateノードに接続します。 これで画像表示部分は無事完成です。 デプロイしてインジェクトして表示されることを確認します。

f:id:tARO:20210124223238p:plain

表示部分ができたので、次にhttpで送受信してみます。 上記のinjectノードをhttp inノードに置き換えます。 http inノードは以下のようにメソッドはPOST、URLは/imageと指定します。 http inノードにはhttp responseノードをステータスコード200で接続します。 これで受信部分は完成です。

f:id:tARO:20210124223302p:plain

次は、送信部分を作ります。 送信は先程のbase64をinjectするノードの後ろにhttp requestノードを接続し、以下のように設定します。 メソッドはPOST、URLはhttp://localhost:1880/imageとします。

f:id:tARO:20210124223309p:plain

これで送受信のサンプルができました。 デプロイしてinjectしたら表示されることを確認します。

送受信両方を作ったことで、今後、カメラから送信するときの受信確認や、AWSに送信するときの送信サンプルとして使えます。

2. TimerCameraXで撮影した画像をPC上で確認する

jpg画像をhttpで送受信する方法がわかったので、次はいよいよTimerCameraXを使って撮影をしてみます。 まずはサンプルを動かしてみます。 公式のQuickStartの通りにサンプルコード(web_cam)を動かして問題なく動くことを確認します。 web_camのサンプルコードでは画像を撮影するところ、配信するところが全てesp_cameraライブラリのstartCameraServer()関数に隠れてしまっていて、どうすれば撮影できるのか、どういうデータ構造を保持しているのかがよくわかりません。

そこで、esp_camera.hを使って、画像のバッファを取得しているサンプルを探します。 ちょうどいい記事が見つかりました。

qiita.com

loopの中の画像を取得して送信しているっぽいところを抜き出してコメントを書いてみます。

  // 撮影
  camera_fb_t *fb = esp_camera_fb_get();
  if ( fb ) {  // 撮影成功
    // fb->buf: データ(jpgならjpg、RAWならRAW)
    // fb->len: データの長さ(byte数)
    Serial.printf("width: %d, height: %d, buf: 0x%x, len: %d\n", fb->width, fb->height, fb->buf, fb->len);
    // MQTTで送信する
    g_pub_sub_client.publish("test", fb->buf, fb->len);
    // fbを使い終わって再利用できるようにする
    esp_camera_fb_return(fb);
  }

ということなので、MQTTで送信している部分を、http postに変更すればやりたいことができそうです。 その前に、このデータがほんとにjpgデータのなのかどうかを確認するために、base64エンコードしてシリアルモニタで確認してみようと思います。

web_camのサンプルを変更した、その実装が以下になります。今回はWifi関連の部分は使わないため削除しています。

#include "esp_camera.h"
#include <base64.hpp>
#include "camera_pins.h"

void setup() {
  Serial.begin(115200);
  Serial.setDebugOutput(true);
  Serial.println();

  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 = 20000000;
  config.pixel_format = PIXFORMAT_JPEG;
  config.frame_size = FRAMESIZE_UXGA;
  config.jpeg_quality = 10;
  config.fb_count = 2;
 
  // camera init
  esp_err_t err = esp_camera_init(&config);
  if (err != ESP_OK) {
    Serial.printf("Camera init failed with error 0x%x", err);
    return;
  }

  sensor_t * s = esp_camera_sensor_get();
  //initial sensors are flipped vertically and colors are a bit saturated
  s->set_vflip(s, 1);//flip it back
  s->set_brightness(s, 1);//up the blightness just a bit
  s->set_saturation(s, -2);//lower the saturation

  //drop down frame size for higher initial frame rate
  s->set_framesize(s, FRAMESIZE_QVGA);
}

void loop() {
  camera_fb_t *fb = esp_camera_fb_get();
  if ( fb ) {
    Serial.printf("width: %d, height: %d, buf: 0x%x, len: %d\n", fb->width, fb->height, fb->buf, fb->len);
    // base64にエンコードした場合必要なバッファサイズを計算(base64では8/7倍になる)
    unsigned int base64_length = encode_base64_length(fb->len);
    // バッファを確保、シリアルモニタにプリントするために終端文字用の1バイト追加
    unsigned char *base64buff = new unsigned char[base64_length+1];
    // 末尾に終端文字を代入
    base64buff[base64_length] = '\0';
    // base64にエンコードする
    encode_base64(fb->buf, fb->len, base64buff);
    // シリアルモニタに出力する
    Serial.printf("%s", base64buff);
    // メモリを解放する
    delete [] base64buff;
    esp_camera_fb_return(fb);
  }
  // put your main code here, to run repeatedly:
  delay(10000);
}

loopの中の以下の部分がbase64エンコードしてシリアルモニタに送信している部分です。 ここbase64ライブラリを追加してインクルードしています。

 if ( fb ) {
    Serial.printf("width: %d, height: %d, buf: 0x%x, len: %d\n", fb->width, fb->height, fb->buf, fb->len);
    // base64にエンコードした場合必要なバッファサイズを計算(base64では8/7倍になる)
    unsigned int base64_length = encode_base64_length(fb->len);
    // バッファを確保、シリアルモニタにプリントするために終端文字用の1バイト追加
    unsigned char *base64buff = new unsigned char[base64_length+1];
    // 末尾に終端文字を代入
    base64buff[base64_length] = '\0';
    // base64にエンコードする
    encode_base64(fb->buf, fb->len, base64buff);
    // シリアルモニタに出力する
    Serial.printf("%s", base64buff);
    // メモリを解放する
    delete [] base64buff;
    esp_camera_fb_return(fb);
  }

ここで引っかかったのは、base64のバッファサイズです。 バッファサイズをencode_base64_lengthで取得した必要サイズのピッタリにすると、シリアルモニタに出力する際にエラーで落ちました。 単純な話で、%sにプリントする際に、終端文字が見つかるまでメモリを参照してしまい、範囲外参照が起きているためと思われます。 そこで、サイズを1バイト余分に確保し、最後に終端文字を代入することで解決しました。

これで無事シリアルモニタに定期的にbase64の謎の文字列が送られてくるようになりました。 これがjpgかどうか、Node-REDの送信側のinjectノードにコピペしてinjectしてみます。 無事画像が出ました! 思った通り、jpgのバイナリがfb->bufに格納されていました。 これで撮影して、base64に変換するところまでできました。

ここで力尽きたので、今日はここまでにします。

作りたいものを書く

今作りたいと思っているものをブログに書こうとしたら、構成図を書く必要が出てきました。 そこでまずは描画について調べて試したことを書くことにします。 PowerPointはじめ、GUIの描画ツールは色々あるのですが、そこまで本格的に書くのはしんどいのでテキストで書いていい感じに描画してくれるものを探します。 AWSの構成図をPythonで書けたらいいと思ってaws 構成図 描画 pythonGoogle検索して最上位に出てきたDiagramsというライブラリが良さそうです。AWSの構成図をpythonで簡単に書けそうです。

diagrams.mingrammer.com

GettingStartedを見ると、GraphVizが必要とのことです。私の開発環境のMacの場合はbrew install graphvizでインストールできるようです。Homebrewはよくわからずにインストールしたはずなので早速上記コマンドでGraphvizをインストールします。 その後、python3 -m pip install diagramsでDiagramsのインストール完了です。

早速QuickStartに書いてある以下のコードを実行して動作確認をします。

from diagrams import Diagram
from diagrams.aws.compute import EC2
from diagrams.aws.database import RDS
from diagrams.aws.network import ELB

with Diagram("Web Service", show=False):
    ELB("lb") >> EC2("web") >> RDS("userdb")

ここでサンプルコードをdiagrams.pyとして実行したら以下のエラーが発生しました。

Traceback (most recent call last):
  File "diagrams.py", line 1, in <module>
    from diagrams import Diagram
  File "diagrams.py", line 1, in <module>
    from diagrams import Diagram
ImportError: cannot import name 'Diagram' from partially initialized module 'diagrams' (most likely due to a circular import) (diagrams.py)

インストールに失敗したかと思いましたが、冷静になってエラーを見てみると、most likely due to a circular importと書いてあり、most likelyなことをやっていることに思い当たりました。ファイル名(diagrams.py)がインポートするモジュール(diagrams)と同じというものすごく初歩的なミスです。こんな恥ずかしいミスもこのブログには書いておきます。

早速ファイル名をQuickStart通りにdiagram.py(末尾のsを削除)に変更して実行すると、web_service.pngというファイル名で、以下の画像ができていました。大成功です。

f:id:tARO:20210110220330p:plain

ようやく準備ができたので、作りたいものを書いてみます。 作りたいものは、カメラなどのデバイスAWSとつないでAWS上でゴニョゴニョするというもので、AWSの部分はDiagramsで問題なく書けそうです(Diagrams-nodes-aws)。しかし、問題はデバイスの方で、Diagramsにどんなものが用意されているのか、Genericにあるものをとりあえず全部書いてみます。

from diagrams import Diagram, Cluster
from diagrams.generic.blank import Blank
from diagrams.generic.compute import Rack
from diagrams.generic.database import SQL
from diagrams.generic.device import Mobile, Tablet
from diagrams.generic.network import Firewall, Router, Subnet, Switch, VPN
from diagrams.generic.os import Android, Centos, IOS, LinuxGeneral, Suse, Ubuntu, Windows
from diagrams.generic.place import Datacenter
from diagrams.generic.storage import Storage
from diagrams.generic.virtualization import Virtualbox, Vmware, XEN

with Diagram("Generic", show=True):
  with Cluster("Blank"):
    blank = Blank("Blank")
  with Cluster("Compute"):
    rack = Rack("Rack")
  with Cluster("Database"):
    sql = SQL("SQL")
  with Cluster("Device"):
    mobile = Mobile("Mobile")
    tablet = Tablet("Tablet")
  with Cluster("Network"):
    firewall = Firewall("Firewall")
    router = Router("Router")
    subnet = Subnet("Subnet")
    switch = Switch("Switch")
    vpn = VPN("VPN")
  with Cluster("OS"):
    android = Android("Android")
    centos = Centos("Centos")
    ios = IOS("IOS")
    linuxgeneral = LinuxGeneral("LinuxGeneral")
    suse = Suse("Suse")
    ubuntu = Ubuntu("Ubuntu")
    windows = Windows("Windows")
  with Cluster("Place"):
    datacenter = Datacenter("Datacenter")
  with Cluster("Storage"):
    storage = Storage("Storagte")
  with Cluster("Virtualization"):
    virtualbox = Virtualbox("Virtualbox")
    vmware = Vmware("Vmware")
    xen = XEN("XEN")

ものすごく縦長になりました。 f:id:tARO:20210110220428p:plain

Diagram("Generic", show=True)の引数にdirection="TB"を追加すると、ものすごく横長になりました。 f:id:tARO:20210110220454p:plain

deviceにはMobileとTabletしかありません。カメラを構成図に入れようと思ったら、Diagramsにないアイコンを追加する必要がありそうです。

困った時の公式ドキュメントです。Examplesを見てみると、一番下にRabbitMQ Consumers with Custom Nodesとあり、Custom Nodesという言葉がいい感じです。 サンプルコードを見てみると、ネットからpngファイルをローカルにダウンロードして、それをそのままCustomノードとして使えるようです。

ということで、独自アイコンの追加の仕方がわかったので必要なアイコンを探します。困った時のいらすとやから使えそうなアイコンを探してみようと思います。

早速アクセスしてみると、トップページのONE PIECEのイラストが気になります。カメラなどデバイスの画像を探しにきたのにONE PIECEのイラストが気になります。ONE PIECEなんて15年近く読んでないのにONE PIECEのイラストが気になります。

練習にルフィの絵を書いてみます。

from urllib.request import urlretrieve
from diagrams import Cluster, Diagram
from diagrams.custom import Custom

luffy_url = "https://1.bp.blogspot.com/-tVeC6En4e_E/X96mhDTzJNI/AAAAAAABdBo/jlD_jvZvMuk3qUcNjA_XORrA4w3lhPkdQCNcBGAsYHQ/s1048/onepiece01_luffy.png"
luffy_icon = "luffy.png"
urlretrieve(luffy_url, luffy_icon)

with Diagram("ONE PIECE", filename="onepiece", show=True):
  luffy = Custom("モンキー・D・ルフィ", luffy_icon)

書けました。 f:id:tARO:20210110220527p:plain

一人だと寂しいので増やしました。 f:id:tARO:20210110220544p:plain

関係性を書きたくなります(適当)。 f:id:tARO:20210110220604p:plain

相関図を書こうとして、相関図を書くほど詳しくなくて断念しました。 f:id:tARO:20210110220624p:plain

ということで、Diagramsを使うことで、好きな画像とAWSアイコンとを組み合わせて構成図が書けるところまでできました。

疲れたので書きたかった構成図はまた今度にします。

blog開始

最近趣味でIoTプログラミングを始めました。

色々調べながら試行錯誤しています。

せっかくなのでその過程を残しておこうと思ってブログを始めました。

Qiitaなどの技術ブログでは上手くいった例を最短で書いてあることが多いのですが、失敗にこそ価値があると思い、このブログではできるだけ失敗や、回り道の過程を記録していきます。

 

まずはM5Stack系とAWSを使って家をちょっと便利にする何かを作ってみようと思います。