知的好奇心 for IoT

IoT関連の知的好奇心を探求するブログです

CameraWebServerサンプルコードを改良してM5Cameraをネットワークカメラとして長時間使えるようにした

カメラ付きでコスパの高いESP32搭載ユニットのM5Cameraですが、自分でプログラミングをせずに出荷時の状態で使おうとすると少々問題があります。

販売代理店のM5Cameraの製品ページには、赤枠部分の不吉な文言が記載されています。

※カメラモジュールの長時間使用は、オーバーヒートしがちなため推奨しません。短時間での撮影をお勧めします。

f:id:IntellectualCuriosity:20200503165021p:plain

製品には次のカードが入っていて、赤枠部分に「Quick Start」が書いてあります。

  1. Connect Wi-Fi:"M5CAM_XXXX"
  2. Browser: 192.168.4.1

f:id:IntellectualCuriosity:20200503170331p:plain

この説明が製品紹介ページにも書かれていないので”なんのこっちゃ?”って感じになりますが、出荷時のプログラムがWiFiアクセスポイントモードで動作するようになっていて、スマホかPCから「M5CAM_XXXX」と表示されるSSIDに接続後、M5Camera上で動作しているウェブサーバー「192.168.4.1」にアクセスして映像を見るようになっているんです。

そんな使い方じゃ、M5Cameraの映像を見ている時にはインターネットにつながらないじゃん!っていう素朴な疑問を抱いてしまいました。

そして、このアクセスポイントモードで動作して映像を見せることが動作を不安定にさせている本当の原因なんです。

少し動作させただけでも映像にノイズが入ることがたびたびありました。

 

そこで、ESP32のサンプルコードを元に、普通にステーションモードで動作するプログラムを書きましたので公開します。

せっかくなのでスマートコンフィグにも対応させてあります。

/*
 * M5CAMERAをネットワークカメラとして使うプログラム
 * 
 * スケッチ例>ESP32>Camera>CameraWebServerで表示されるサンプルコードを元に
 * ビデオストリーミングに不必要な処理を外してスマートコンフィグに対応させました。
 * 
 * 作成者:伊藤浩之 (Hiroyuki ITO)
 * 作成日:2020年5月3日
 * ブログ:https://intellectualcuriosity.hatenablog.com/
 * このプログラムはパブリックドメインソフトウェアです。
 */
#include <WiFi.h>
#include "esp_camera.h"
#include "esp_http_server.h"

#define CAMERA_VFLIP    0               // カメラの設置向き(本体のM5文字の向き) 1:正常 0:逆さ
#define FRAME_SIZE      FRAMESIZE_VGA   // サイズ:FRAMESIZE_ + QVGA|CIF|VGA|SVGA|XGA|SXGA|UXGA

// スマートコンフィグが使えないときは、以下の2行のコメントを外してSSIDとパスワードを設定します
//#define WIFI_SSID       "WiFiSSIDで置き換えます"
//#define WIFI_PASS       "WiFiのパスワードで置き換えます"

#define PART_BOUNDARY "123456789000000000000987654321"
static const char* _STREAM_CONTENT_TYPE = "multipart/x-mixed-replace;boundary=" PART_BOUNDARY;
static const char* _STREAM_BOUNDARY = "\r\n--" PART_BOUNDARY "\r\n";
static const char* _STREAM_PART = "Content-Type: image/jpeg\r\nContent-Length: %u\r\n\r\n";

httpd_handle_t stream_httpd = NULL;

// カメラの初期化処理
void initCamera() {
  camera_config_t config;
  config.ledc_channel = LEDC_CHANNEL_0;
  config.ledc_timer =   LEDC_TIMER_0;
  config.pin_d0 =       32;             // Y2_GPIO_NUM
  config.pin_d1 =       35;             // Y3_GPIO_NUM
  config.pin_d2 =       34;             // Y4_GPIO_NUM
  config.pin_d3 =        5;             // Y5_GPIO_NUM
  config.pin_d4 =       39;             // Y6_GPIO_NUM
  config.pin_d5 =       18;             // Y7_GPIO_NUM
  config.pin_d6 =       36;             // Y8_GPIO_NUM
  config.pin_d7 =       19;             // Y9_GPIO_NUM
  config.pin_xclk =     27;             // XCLK_GPIO_NUM
  config.pin_pclk =     21;             // PCLK_GPIO_NUM
  config.pin_vsync =    25;             // VSYNC_GPIO_NUM
  config.pin_href =     26;             // HREF_GPIO_NUM
  config.pin_sscb_sda = 22;             // SIOD_GPIO_NUM
  config.pin_sscb_scl = 23;             // SIOC_GPIO_NUM
  config.pin_pwdn =     -1;             // PWDN_GPIO_NUM
  config.pin_reset =    15;             // RESET_GPIO_NUM
  config.xclk_freq_hz = 20000000;
  config.pixel_format = PIXFORMAT_JPEG;
  config.frame_size =   FRAME_SIZE;
  config.jpeg_quality = 10;
  config.fb_count =      2;
  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();
  s->set_vflip(s, CAMERA_VFLIP);
  s->set_hmirror(s, 0);
}

// ストリーミングハンドラー
static esp_err_t stream_handler(httpd_req_t *req) {
  camera_fb_t * fb = NULL;
  esp_err_t res = ESP_OK;
  size_t _jpg_buf_len = 0;
  uint8_t * _jpg_buf = NULL;
  char * part_buf[64];

  res = httpd_resp_set_type(req, _STREAM_CONTENT_TYPE);
  if (res != ESP_OK) {
    return res;
  }

  while (true) {
    fb = esp_camera_fb_get();
    if (!fb) {
      Serial.println("Camera capture failed");
      res = ESP_FAIL;
    } else {
      if (fb->width > 400) {
        if (fb->format != PIXFORMAT_JPEG) {
          bool jpeg_converted = frame2jpg(fb, 80, &_jpg_buf, &_jpg_buf_len);
          esp_camera_fb_return(fb);
          fb = NULL;
          if (!jpeg_converted) {
            Serial.println("JPEG compression failed");
            res = ESP_FAIL;
          }
        } else {
          _jpg_buf_len = fb->len;
          _jpg_buf = fb->buf;
        }
      }
    }
    if (res == ESP_OK) {
      size_t hlen = snprintf((char *)part_buf, 64, _STREAM_PART, _jpg_buf_len);
      res = httpd_resp_send_chunk(req, (const char *)part_buf, hlen);
    }
    if (res == ESP_OK) {
      res = httpd_resp_send_chunk(req, (const char *)_jpg_buf, _jpg_buf_len);
    }
    if (res == ESP_OK) {
      res = httpd_resp_send_chunk(req, _STREAM_BOUNDARY, strlen(_STREAM_BOUNDARY));
    }
    if (fb) {
      esp_camera_fb_return(fb);
      fb = NULL;
      _jpg_buf = NULL;
    } else if (_jpg_buf) {
      free(_jpg_buf);
      _jpg_buf = NULL;
    }
    if (res != ESP_OK) {
      break;
    }
  }
  return res;
}

// ビデオストリーミングの開始
void startCameraServer() {
  httpd_config_t config = HTTPD_DEFAULT_CONFIG();

  httpd_uri_t index_uri = {
    .uri       = "/",
    .method    = HTTP_GET,
    .handler   = stream_handler,
    .user_ctx  = NULL
  };

  Serial.printf("Starting stream server on port: '%d'\n", config.server_port);
  if (httpd_start(&stream_httpd, &config) == ESP_OK) {
    httpd_register_uri_handler(stream_httpd, &index_uri);
  }
}

// 接続処理
boolean connect() {
  if (WiFi.status() != WL_CONNECTED) {  // WiFiに繋がっているか?
    #if defined(WIFI_SSID)              // WIFI_SSIDが定義されているとき
      WiFi.begin(WIFI_SSID, WIFI_PASS); // WiFiに接続
    #else
      WiFi.begin();                     // WiFiに前回の設定で接続
    #endif
    Serial.print("Connecting");
    for (int i=0; WiFi.status() != WL_CONNECTED; i++) {
      Serial.print(".");
      delay(500);
      if (i > 20) {                     // 10秒経ったか(500ms * 20回)
        return false;
      }
    }
    Serial.println(String("\nConnected to ") + WiFi.SSID());
  }
  return true;
}

// 初期化時の接続処理
void connectInit() {
  WiFi.mode(WIFI_STA);                  // WiFiをステーションモードに設定
  if (!connect()) {                     // 接続処理
    WiFi.beginSmartConfig();            // スマートコンフィグを実行
    Serial.print("\nSmartConfig Start!");
    while(!WiFi.smartConfigDone()) {    // スマートコンフィグの完了確認
      delay(500);
      Serial.print(".");
    }
    Serial.println("\nSmartConfig received.");
    Serial.println(String("Connected to ") + WiFi.SSID());
  }
}

void setup() {
  Serial.begin(115200);
  initCamera();                         // カメラの初期化処理
  connectInit();                        // 初期化時の接続処理 

  startCameraServer();                  // ビデオストリーミングの開始
  Serial.print("Camera Ready! Use 'http://");
  Serial.print(WiFi.localIP());
  Serial.println("' to connect");
}

void loop() {
  while (!connect());                   // WiFiの接続確認
  delay(1000);
}

※2021年10月13日追記

loop()ファンクションにdelay(1000);が入っていることで、M5Cameraの温度が3〜4度下がることがわかりました。(24.5度の部屋で30分程度動作させた時の表面温度が45度ぐらいでした。)

このことも、このプログラムだと長時間使える要因になっているようです。

 

コンパイル時はArduinoで「ESP32 Wrover Module」を選択するのを忘れないように! 

f:id:IntellectualCuriosity:20200503174454p:plain

 

おわり