M5Cameraで定点カメラを立ててみる

![m5camera.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61404/2db7a17d-aa69-89b8-e8a3-25dddfa784f7.jpeg)

M5Cameraから一定間隔でWebサーバーに画像をアップロードする手順を紹介します。M5CameraはWi-Fi接続が可能なESP32を搭載したカメラデバイスです。連続稼働させるとオーバーヒートしてしまうため監視には向きませんが、間隔を空けて撮影する用途では使えると思いWebサーバーに画像をアップロードするまでの手順をまとめました

# Arduino IDE

Arduino IDEがPCにインストールされていて、必要なライブラリが使えるように設定等が済んでいることを前提とします

## ボードの設定

Arduino IDEをM5Cameraに合わせた設定にします

[ツール] > [ボード:"M5Stack-Core-ESP32"] > [ESP32 Arduino] > [ESP32 Wrover Module] を選択します

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61404/8f565d82-a4ab-c440-ddfa-e1571c74e792.png)

## 参考) サンプルコード

本記事はArduino IDEに用意されているサンプルスケッチを参考にしています。何か分からないことがあった時の補足として紹介します

[ファイル] > [スケッチ例]  > [ESP32] > [Camera] > [CameraWebServer]

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61404/b73febaa-1ec3-9923-19f1-37fd2c2cddff.png)

### サンプルコードからの変更点

上記サンプルコードをM5Cameraで動かすには以下の変更を加えます

```cpp:CameraWebServer
//#define CAMERA_MODEL_WROVER_KIT // コメントを付ける
 #define CAMERA_MODEL_M5STACK_PSRAM // コメントを外す
```

camera_pins.h の `defined(CAMERA_MODEL_M5STACK_PSRAM)` の下にある `SIOD_GPIO_NUM` と `VSYNC_GPIO_NUM` の値を変更します

```cpp:camera_pins.h
#elif defined(CAMERA_MODEL_M5STACK_PSRAM)

        :

#define SIOD_GPIO_NUM 22 // 25 から 22に変更
#define VSYNC_GPIO_NUM 25 // 22 から 25に変更

        :

```

# コード

新規にスケッチを作成して、Webサーバーにアップロードするコードを書いていきます。なお、私の癖なのですがソースコードを機能別に分けて(タブを追加して)書くようにしています

今回のケースでは Camera 周りの処理と Wi-Fi 通信周りの処理で、以下のように分けるようにしました

* Camera周り
  * camera_pins.h
  * Camera.h
  * Camera.cpp
* Wi-Fi通信周り
  * WifiClient.h 
  * WifiClient.cpp

タブの追加(ソースコードの追加)はArduino IDEの画面右側に下向きの ▼ をクリックして[新規タブ]をクリックします

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61404/c97245ed-ecfb-8186-3c09-97b8de381daf.png)

下部にファイル名入力欄が表示されますので、作成したいファイルの名前を入れて[OK]をクリックすると、新しいタブが作成されます

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61404/091a6bf9-a763-479f-6b2d-456c3a8573a5.png)

最終的には、以下のように6つのタブになります

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61404/8376b4d4-9eb4-2ad9-2284-633a9a7aab37.png)


## Camera周りの実装

### camera_pins.h

ここは、上に挙げたサンプルコードから抜粋しています。M5Cameraは `Aモデル` と `Bモデル` があるようで、モデルによってカメラのピンが変わるようです。最近購入したならば `Bモデル` らしいのですが、見分け方は軽く探してみましたが分かりませんでした。公式Webでは見分け方のページはリンク切れになっており、同じモデルなのにこういった罠が潜んでいるのはM5マイコンの闇だと思います

Bモデルならば以下のコードになります

```cpp:camera_pins.h
#define PWDN_GPIO_NUM     -1
#define RESET_GPIO_NUM    15
#define XCLK_GPIO_NUM     27
#define SIOD_GPIO_NUM     22
#define SIOC_GPIO_NUM     23

#define Y9_GPIO_NUM       19
#define Y8_GPIO_NUM       36
#define Y7_GPIO_NUM       18
#define Y6_GPIO_NUM       39
#define Y5_GPIO_NUM        5
#define Y4_GPIO_NUM       34
#define Y3_GPIO_NUM       35
#define Y2_GPIO_NUM       32
#define VSYNC_GPIO_NUM    25
#define HREF_GPIO_NUM     26
#define PCLK_GPIO_NUM     21
```

Aモデルならば以下のように `SIOD_GPIO_NUM` と `VSYNC_GPIO_NUM` の値を入れ替えてください。他はBモデルと共通です

```cpp:Aモデル用
#define SIOD_GPIO_NUM     25
#define VSYNC_GPIO_NUM    22
```

### Camera.h / Camera.cpp

カメラ周りは大まかはサンプルコードから画像取得の所だけを抜き出した物です

```cpp:camera.h
#pragma once

#include "esp_camera.h"

class Camera {
  private:
    camera_fb_t *fb;
  public:
    Camera();
    void begin();
    esp_err_t capture(const char **fb_buf, size_t *fb_len);
    void free();
};
```

```cpp:camera.cpp
#include <M5Stack.h>

#include "Camera.h"
#include "camera_pins.h"

Camera::Camera()
{
  fb = NULL;
}

void
Camera::begin()
{
  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;
  //init with high specs to pre-allocate larger buffers
  if(psramFound()){
    config.frame_size = FRAMESIZE_UXGA;
    config.jpeg_quality = 10;
    config.fb_count = 2;
  } else {
    config.frame_size = FRAMESIZE_SVGA;
    config.jpeg_quality = 12;
    config.fb_count = 1;
  }

  // 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
  if (s->id.PID == OV3660_PID) {
    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
  }

  // 好きなようにカメラの設定を変更します
  s->set_framesize(s, FRAMESIZE_SVGA);  // 大きさを800x600にします
  s->set_ae_level(s, 1);  // 少し明るくします

  Serial.println("Camera init Successful");
  delay(1500);  // waiting for Camera Sensor stabilize
}

// 引数に指定されたバッファポインタをJPEG画像の領域に当てるようにします
esp_err_t
Camera::capture(const char **fb_buf, size_t *fb_len)
{
    esp_err_t res = ESP_OK;

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

    if(fb->format == PIXFORMAT_JPEG){
        *fb_len = fb->len;
        *fb_buf = (const char *)fb->buf;
        Serial.println("JPEG captured");
    }

    return res;
}

void
Camera::free()
{
  if (fb) {
    esp_camera_fb_return(fb);
    fb = NULL;
  }
}
```

## Wi-Fi通信周りの実装

Wi-Fiのコードは ESP-WROOM などと共通なので、特に難しい所はないと思います

### WifiClient.h / WifiClient.cpp

```cpp:WifiClient.h
#pragma once

class WifiClient {
  private:
  public:
    void begin(const char *ssid, const char *password);
    void post(const char *host, const char *buffer, const size_t length);
};
```

```cpp:WifiClient.cpp
#include <WiFi.h>
#include <HTTPClient.h>
#include "WifiClient.h"

void
WifiClient::begin(const char *ssid, const char *password)
{
  WiFi.begin(ssid, password);

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

void
WifiClient::post(const char *host, const char *buffer, const size_t length)
{
  HTTPClient http;
  http.begin(host);
  http.addHeader("Content-Type", "image/jpeg");
  const int status_code = http.POST((uint8_t*)buffer, length);

  Serial.printf("Wi-Fi POST -> %s\nSize=%zu, Status=%d", host, length, status_code);

  http.end();
}
```

## スケッチのメイン処理

省電力化もありますが、オーバーヒートを防止するために、1回送信する度にDeepSleepするようにしています。撮影間隔を変えたい場合は `SLEEP_INTERVAL_SEC` (秒単位)を変更してください。間隔を短くしすぎるとWi-Fiの再接続やIPアドレスの再割り当てが頻繁になり、Wi-Fiルーターに負荷をかけてしまいます。1分より短くしたいならばDeepSleepは行わないのが良いと思います

環境に合わせて、 `ssid` (Wi-Fiアクセスポイント名)、 `password` (Wi-Fiのパスワード)、 `upload_endpoint` (送信先のWebサーバーのURL)を変更します

```cpp:m5camera_post
#include "Camera.h"
#include "WifiClient.h"

// DeepSleep
#define SLEEP_INTERVAL_SEC 60

// 環境に合わせて適宜変更します
const char* ssid = "Wi-Fi access point name";
const char* password = "Wi-Fi password";
const char* upload_endpoint = "http://192.168.1.1/m5camera/upload.php";

uint64_t starttime;
Camera *camera;
WifiClient *wifi;

void deep_sleep(const uint64_t interval_sec)
{
    uint64_t sleeptime = interval_sec * 1000000 - (millis() - starttime) * 1000;
    esp_deep_sleep(sleeptime);  // DeepSleepモードに移行
}

void setup()
{
  camera = new Camera();
  wifi = new WifiClient();

  starttime = millis();

  // put your setup code here, to run once:
  Serial.begin(115200);
  Serial.setDebugOutput(true);
  Serial.println();

  wifi->begin(ssid, password);
  camera->begin();
}

void loop() {
  // put your main code here, to run repeatedly:
  const char *fb_buf;
  size_t fb_len;

  camera->capture(&fb_buf, &fb_len);
  wifi->post(upload_endpoint, fb_buf, fb_len);
  camera->free();

  deep_sleep(SLEEP_INTERVAL_SEC);
  // deep sleepするとこの先は処理されません。loop()も一度だけです
}
```

## Webサーバーのコード

簡単にphpで書きました。送信側は単純にJPEG画像のバイナリをPOSTで送信してるだけなので、php側は標準入力で受け取ったデータをファイルに書き出すだけです。書き出し中にブラウザからのアクセスがあると中途半端なデータを表示させてしまうので、一時ファイルに書き出してから本来の名前に置き換えるようにしています

```php:upload.php
<?php
    $filename = 'image.jpg';
    $temp_file = 'tmp_image.jpg';

    $fd = fopen($temp_file, 'w');
    if ($fd) {
        fwrite($fd, file_get_contents('php://input'));
        fclose($fd);
        rename($temp_file, $filename);
    }

    echo '200 OK';
?>
```

以上で、Webサーバーに保存された image.jpg をブラウザからアクセスすると、カメラが撮影した画像を見ることができます

本記事のプログラムはシンプルにするため認証などは行っていません。M5CameraとWebサーバーが同じLAN内であればこのままでも良いですが、外に公開する場合はセキュリティを考慮して関係ない所からデータをアップロードされないようにしてください