M5Stack ESP32CAMERAで簡易設置タイムラプスカメラ

定点観察用のタイムラプスカメラを簡易設置できるように検討したのでメモ。

[2024/12/25追記]
久々に触ったらいろいろ環境がわからなくなっていた。
下記で再現できました。

Board:ESP32 Wrover Module
サンプルコード：ファイル→スケッチ例→ESP32→Camera→CameraWebServer

ビルドしてサイズオーバーになったら、
https://www.yagitech.jp/esp3218-%E3%82%B9%E3%82%B1%E3%83%83%E3%83%81%E3%81%8C%E5%A4%A7%E3%81%8D%E3%81%8F%E3%81%A6%E3%83%95%E3%83%A9%E3%83%83%E3%82%B7%E3%83%A5%E3%83%A1%E3%83%A2%E3%83%AA%E3%82%92%E8%B6%85%E3%81%88/

> 最大1310720バイトのフラッシュメモリのうち、スケッチが1324862バイト（101%）を使っています。
> 最大327680バイトのRAMのうち、グローバル変数が57820バイト（17%）を使っていて、ローカル変数で269860バイト使うことができます。
> Sketch too big; see https://support.arduino.cc/hc/en-us/articles/360013825179 for tips on reducing it.
> text section exceeds available space in board

このエラーが出たらフラッシュの容量を増やす。ツールの　Partition Scheme: を　Huge APP　に。


[仕様]
・M5Stack ESP32CAMERAを5秒ごとにキャプチャしてPCに保存する。
・PC - ESP32CAMERA間はアクセスポイントなしで直接APモードで繋ぐ

[使用方法]
1. PCからWifiで"m5camera-wifi"に接続する
2. ping 192.168.4.1が接続出来たらcapture_request.pyを起動
3. capture_request.py と同じパスにimagesフォルダができ、"日付時間.jpg"でキャプチャ画像が保存される 


# M5Stack ESP32CAMERA側

CameraWebServerをベースにAPモードで接続できるようにする。

```CameraWebServer.ino.c
#include <WiFiAP.h>
・・・
#define CAMERA_MODEL_M5STACK_V2_PSRAM // M5Camera version B Has PSRAM
・・・
const char* ssid = "m5camera-wifi";
const char* password = "XXXXXXXXX";
void startCameraServer();
void setupLedFlash(int pin);

void setup() {
・・・
  // WiFi.begin(ssid, password); // 子機モード
  WiFi.softAP(ssid, password);  // 親機モード

/*
  // 子機モードの場合
  WiFi.setSleep(false);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.println("WiFi connected");  
*/

  startCameraServer();

  Serial.print("Camera Ready! Use 'http://");
  // Serial.print(WiFi.localIP()); // 子機モード
  Serial.print(WiFi.softAPIP());
  Serial.println("' to connect");
}

void loop() {
  // Do nothing. Everything is done in another task by the web server
  delay(10000);
}


```

# PC側キャプチャアプリ(Python)

```capture_request.py
import requests
import os
import time
import datetime

# 画像をダウンロードする
def download_image(url, timeout = 10):
    response = requests.get(url, allow_redirects=False, timeout=timeout)
    if response.status_code != 200:
        e = Exception("HTTP status: " + response.status_code)
        raise e

    content_type = response.headers["content-type"]
    if 'image' not in content_type:
        e = Exception("Content-Type: " + content_type)
        raise e

    return response.content

# 画像のファイル名を決める
def make_filename():
    picdir = os.path.join(os.getcwd(), 'images')
    if not os.path.exists(picdir):  # 無ければ作成
        os.makedirs(picdir)

    t_delta = datetime.timedelta(hours=9)
    JST = datetime.timezone(t_delta, 'JST')
    now = datetime.datetime.now(JST)
    d = now.strftime('%Y%m%d_%H%M%S')
    filename = d + '.jpg'

    fullpath = os.path.join(picdir, filename)
    return fullpath

# 画像を保存する
def save_image(filename, image):
    with open(filename, "wb") as fout:
        fout.write(image)

# メイン
if __name__ == "__main__":
    while True:
        url = 'http://192.168.4.1/capture'
        try:
            image = download_image(url)
            filename = make_filename()
            save_image(filename, image)
            time.sleep(5)
        except KeyboardInterrupt:
            break
        except Exception as err:
            print("%s" % (err))