1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Timer Camera Fで定点カメラを作ってTeamsに通知

Last updated at Posted at 2023-04-09

はじめに

水槽の様子を遠隔から確認したくて、遠隔カメラを設置しています。ただ、プル型で見たいときに見に行くのではなく、定期的にプッシュ型で通知があれば、確認する習慣ができるので、定期的にカメラ画像をTeamsに通知させようと思います

Teams の Incoming Webhook を使用するので、完成形はこんなイメージです。
image.png

素材

  • Timer Camera F

  • Microsoft Teams

実装の大まかな流れ

・Timer Camera F で画像データを取得
・Timer Camera F から画像データをHTTPでPOSTする
・Teamsへの投稿はIncoming Webhookを使用する
・画像データはメッセージに埋め込む方法とする

Teamsに画像データを送信する方法

今回の説明では、Incoming Webhookの作成方法やURL取得などは割愛し、Teamsに画像データを送信する方法にフォーカスして記載したいと思います。

こちらの記事にあるように、画像データを送信する方法は以下の2パターンがあるようです。
今回は、「メッセージに画像データを埋め込む方法」でやってみます。

  • データをクラウドにアップして、URL参照する方法
  • メッセージに画像データを埋め込む方法

送信するデータの形式

メッセージに画像を埋め込む場合は、Markdown形式を使用する必要があるみたいで、以下のような形式で送信します。

普通にメッセージ送信する場合は、textにそのまま文字列などを入れて送信すれば大丈夫です。

String payload = "{'text':'" + text + "'}";

画像データを埋め込む場合は、Markdown形式で埋め込む必要があるので、textの中身はMarkdown形式で画像データを渡すことになります。(この際の画像データは、base64にエンコードしたデータですが、これは後述)

String payload = "{'text':'![](data:image/jpg;base64," + text + ")'}";

画像データのエンコード

メッセージに画像を埋め込む場合は、画像データはbase64に変換する必要があります。その為、大まかな流れは以下の通りです。

  1. カメラから画像データを取得
  2. 画像データをbase64でエンコード
  3. 画像データをメッセージに埋め込んで送信する
#include <base64.h>

/* 途中は割愛 */

// カメラの撮影処理
camera_fb_t *fb = NULL;
fb = esp_camera_fb_get();

// 画像データをBASE64データに変換して送信処理
base64 base64;
String b64ImageData;
b64ImageData = base64.encode(fb->buf, fb->len);
notifyTeams(webhook, b64ImageData);    // データ送信部分は別関数にしています

esp_camera_fb_return(fb);

送信された画像データの確認(base64)

実際にbase64に変換したデータがちゃんと画像になっているかどうかの確認を行う為、まずは、base64にエンコードした内容をそのままTeamsに投稿してみました。base64の文字列をそのままTeamsに表示させると、以下のようになります。
image.png

それを以下のサイトを使って画像にデコードします。

その結果がこちら。base64に変換されたデータを、画像にデコードすることができました。
image.png

まとめ

今回の方法で、Teamsに画像を送信することができました。プッシュ型で通知が来て、そこで画像が確認できるので、かなり便利になったと思います。
あとは、画角が狭いのはレンズをちょっと考えるとして、画質に関わるデータ容量の上限はなかなか課題だと思うので、今後もう1つの方法を試してみたいと思います。

ちなみに、こんな感じで投稿されるので、カメラ画像が一目で分かります。
image.png

ポイント:送信可能な画像のデータサイズの上限

以下の参考サイトにもあるように、おそらくメッセージに埋め込める画像サイズは15KBまでです。その為、Timer Camera F を使用してもあまり送信する画像データの解像度を上げることは出来ません。また、データ形式がJPEG画像である為、画像によってデータサイズが厳密には異なる為、だいたい安定して15KB以下になるような画像サイズにする必要があります。

こちらの画像データの上限ですが、正確な情報はないのですが、ここにあるメッセージサイズの28KB制限が関係している気がします。いずれにせよ、あまり大きなデータは埋め込めないので、こちらは今後もう1つの方法でトライしてみようと思います。

参考サイト

サンプルコード

今回は、8時/12時/16時に撮影をしてTeamsに通知するようにしています。

#include "battery.h"
#include "esp_camera.h"
#include <WiFi.h>
#include "soc/soc.h"
#include "soc/rtc_cntl_reg.h"

#include "camera_pins.h"

#include <HTTPClient.h>
#include <base64.h>

#define JST     3600* 9

void notifyTeams(String webhook, String text);

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

void startCameraServer();

String webhook = "https://xxxx.webhook.office.com/webhookb2/xxxx/IncomingWebhook/xxxx/xxxx";
String text = "テスト";

void setup() {
    Serial.begin(9600);
    WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0);  // disable   detector
    bat_init();
    bat_hold_output();
    Serial.setDebugOutput(true);
    Serial.println();
    pinMode(2, OUTPUT);
    digitalWrite(2, HIGH);

    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);

    Serial.printf("Connect to %s, %s\r\n", ssid, password);

    WiFi.begin(ssid, password);

    while (WiFi.status() != WL_CONNECTED) {
        delay(500);
        Serial.print(".");
    }
    Serial.println("");
    Serial.println("WiFi connected");

    // NTP同期
    configTime( JST, 0, "ntp.nict.jp", "ntp.jst.mfeed.ad.jp");

}

void loop() {

    time_t t;
    struct tm *tm;
    static const char *wd[7] = {"Sun", "Mon", "Tue", "Wed", "Thr", "Fri", "Sat"};

    t = time(NULL);
    tm = localtime(&t);

  
    Serial.printf(" %04d/%02d/%02d(%s) %02d:%02d:%02d\n",
                  tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday,
                  wd[tm->tm_wday],
                  tm->tm_hour, tm->tm_min, tm->tm_sec);
  
    // put your main code here, to run repeatedly:
    delay(100);
    digitalWrite(2, LOW);
    delay(100);
    digitalWrite(2, HIGH);
    delay(100);
    digitalWrite(2, LOW);
    delay(100);
    digitalWrite(2, HIGH);


    // 時間になったら撮影投稿
    // 時間のチェック(24H表記)
    if (2000 < (tm->tm_year + 1900))  // サーバー同期が正しく行われているかどうかで処理を除外
    {
      // 
      if ((tm->tm_hour == 8) || (tm->tm_hour == 12) || (tm->tm_hour == 16))
      { // 8時/12時/16時
        if ((tm->tm_min == 0))
        {
          // カメラの撮影処理
          camera_fb_t *fb = NULL;
          fb = esp_camera_fb_get();

          if (!fb)
          {
            Serial.println("Camera capture failed");
            return;
          }

          Serial.println("capture complete");

          // 画像データをBASE64データに変換して送信処理
          base64 base64;
          String b64ImageData;
          b64ImageData = base64.encode(fb->buf, fb->len);
          notifyTeams(webhook, b64ImageData);
          Serial.println("send the image complete");

          esp_camera_fb_return(fb);

          delay(70000); // 余裕を見て、70秒後に変更
        }
      }
    }

    delay(1000);

}

// Teams通知
void notifyTeams(String webhook, String text)
{
  int res;
  HTTPClient https;

  Serial.print("connect url :");
  Serial.println(webhook);

  Serial.print("[HTTPS] begin...\n");
  if (https.begin(webhook))
  { // HTTPS

    Serial.print("[HTTP] POST...\n");
    // start connection and send HTTP header
    String payload = "{'text':'![](data:image/jpg;base64," + text + ")" + "'}";
    int httpCode = https.POST(payload);

    // httpCode will be negative on error
    if (httpCode > 0)
    {
      // HTTP header has been send and Server response header has been handled
      Serial.printf("[HTTPS] GET... code: %d\n", httpCode);
      //Serial.println(https.getSize());

      // file found at server
      String payload;
      if (httpCode == HTTP_CODE_OK || httpCode == HTTP_CODE_MOVED_PERMANENTLY)
      {
        payload = https.getString();
        Serial.println("HTTP_CODE_OK");
        Serial.println(payload);
      }
      res = 1;
    }
    else
    {
      Serial.printf("[HTTPS] GET... failed, error: %s\n", https.errorToString(httpCode).c_str());
      res = -1;
    }
    https.end();
  }
  else
  {
    Serial.printf("[HTTPS] Unable to connect\n");
    res = -1;
  }
}
1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?