ESP32 PSRAM Timer Camera で簡易カメラ撮影サーバーを作ったメモ

ESP32 PSRAM Timer Camera で簡易カメラ撮影サーバーを作ったメモ

この記事は IoTLT Advent Calendar 2024 の 3 日目の記事です。

ESP32 PSRAM Timer Camera X で簡易カメラ撮影サーバーを作ったメモです。

Timer Camera たのしい

最近、こちらの Timer Camera で AI 画像認識の入り口として使うなど色々やっていて楽しいです。

自分が使っている簡易カメラ撮影サーバーの仕組みがだいぶ整ってきたので、今回はメモしておこうかなと思います。

参考資料

参考資料としては、構築方法や大まかな特性を掴むなど、以下の記事がとても参考になりました。感謝いたします!

どんな仕組みを目指したか

実際に簡易カメラ撮影サーバーを作るうえでは、公式ドキュメントを参考にしました。

GitHub のソースコードがそのまま使えるレベルで参考になりました。その中でも、STA として Wi-Fi につないでサーバーとして動く TimerCam-arduino/examples/web_cam/sta/sta.ino at master · m5stack/TimerCam-arduino が、私の使い方に結構あってたのでここから発展させました。

さらに M5 Stack Timer Cameraを使ってカメラ画像取得APIサーバーを作る | AAbrain を拝見しまして esp_http_server というサーバーのパスが簡単に作れることが分かりまして、うまく連携させました。

ソースコード

できたものがこちらです。

#include "M5TimerCAM.h"
#include <WiFi.h>
#include <WiFiMulti.h>
WiFiMulti wifiMulti;
#include "esp_http_server.h"

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

// ファームウェアバージョン
#define FW_VERSION "M5Timer 2024 1.0.5 blog"

//
#define BUTTON_PIN GPIO 38

// デバイスID
char deviceID[50];

void setup() {
  TimerCAM.begin();

  TimerCAM.Power.setLed(255);
  delay(300);
  TimerCAM.Power.setLed(0);
  delay(300);
  TimerCAM.Power.setLed(255);
  delay(300);
  TimerCAM.Power.setLed(0);
  delay(300);
  TimerCAM.Power.setLed(255);
  delay(300);
  TimerCAM.Power.setLed(0);

  if (!TimerCAM.Camera.begin()) {
    Serial.println("Camera Init Fail");
    return;
  }
  Serial.println("Camera Init Success");

  TimerCAM.Camera.sensor->set_pixformat(TimerCAM.Camera.sensor,
                                        PIXFORMAT_JPEG);
  TimerCAM.Camera.sensor->set_framesize(TimerCAM.Camera.sensor,
                                        FRAMESIZE_QVGA);
  TimerCAM.Camera.sensor->set_vflip(TimerCAM.Camera.sensor, 1);
  TimerCAM.Camera.sensor->set_hmirror(TimerCAM.Camera.sensor, 0);

  wifiMulti.addAP(ssid, password);
  

  WiFi.mode(WIFI_STA);
  // WiFi.begin(ssid, password);
  WiFi.setSleep(false);
  // Serial.println("");
  Serial.print("Connecting...");
  // Serial.println(ssid);
  // Wait for connection
  // while (WiFi.status() != WL_CONNECTED) {
  while (wifiMulti.run() != WL_CONNECTED) {
    delay(500);
    TimerCAM.Power.setLed(255);
    delay(500);
    TimerCAM.Power.setLed(0);
    Serial.print(".");
  }

  Serial.println("");
  Serial.print("Connected to ");
  Serial.println(WiFi.SSID());
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());
  Serial.print("MAC address: ");
  Serial.println(WiFi.macAddress());

  // deviceID
  String macStr = String(WiFi.macAddress());
  macStr.replace(":", "");
  sprintf(deviceID, "%s", macStr);
  
  start_webserver();
  
}

void loop() {
  
}

esp_err_t get_health_check_handler(httpd_req_t *req) {
  const char resp[] = "Alive";
  httpd_resp_send(req, resp, strlen(resp));
  return ESP_OK;
}

httpd_uri_t uri_get_health_check = {
  .uri      = "/api/healthcheck",
  .method   = HTTP_GET,
  .handler  = get_health_check_handler,
  .user_ctx = NULL
};

esp_err_t get_snapshot_handler(httpd_req_t *req) {
  camera_fb_t * fb = NULL;
  esp_err_t res = ESP_OK;
  size_t fb_len = 0;
  int64_t fr_start = esp_timer_get_time();

  TimerCAM.Power.setLed(255);
    delay(50);
    TimerCAM.Power.setLed(0);
    delay(50);
    TimerCAM.Power.setLed(255);
    delay(50);
    TimerCAM.Camera.free();
    delay(50);
    if (TimerCAM.Camera.get()) {
      TimerCAM.Power.setLed(0);
      delay(50);
      TimerCAM.Power.setLed(255);
      Serial.printf("pic size: %d\n", TimerCAM.Camera.fb->len);

      int32_t fb_len    = TimerCAM.Camera.fb->len;
      int32_t to_sends    = TimerCAM.Camera.fb->len;
      int32_t now_sends   = 0;
      uint8_t* out_buf    = TimerCAM.Camera.fb->buf;

      res = httpd_resp_set_type(req, "image/jpeg");
      if (res == ESP_OK) {
        res = httpd_resp_set_hdr(req, "Content-Disposition", "inline; filename=capture.jpg");
      }
      if (res == ESP_OK) {
        res = httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
      }
      if (res == ESP_OK) {
        res = httpd_resp_set_hdr(req, "Connection", "close");
      }
      
      if (res == ESP_OK) {
        res = httpd_resp_send(req, (const char *)out_buf, fb_len);
      }
      // esp_camera_fb_return(TimerCAM.Camera.fb);
      int64_t fr_end = esp_timer_get_time();
      Serial.printf("time: %ums\n", (uint32_t)((fr_end - fr_start) / 1000));
      
    }

    TimerCAM.Camera.free();
    TimerCAM.Power.setLed(0);

    return res;
}

httpd_uri_t uri_get_snapshot = {
  .uri      = "/api/snapshot",
  .method   = HTTP_GET,
  .handler  = get_snapshot_handler,
  .user_ctx = NULL
};

esp_err_t get_shutdown_handler(httpd_req_t *req) {
  const char resp[] = "Shutdown";
  httpd_resp_send(req, resp, strlen(resp));
  delay(500);
  TimerCAM.Power.setLed(0);
  delay(500);
  TimerCAM.Power.setLed(255);
  delay(500);
  TimerCAM.Power.setLed(0);
  delay(500);
  TimerCAM.Power.setLed(255);
  delay(500);
  TimerCAM.Power.setLed(0);
  delay(500);
  TimerCAM.Power.setLed(255);
  delay(500);
  TimerCAM.Power.setLed(0);
  TimerCAM.Power.powerOff();
  return ESP_OK;
}

httpd_uri_t uri_get_shutdown = {
  .uri      = "/api/shutdown",
  .method   = HTTP_GET,
  .handler  = get_shutdown_handler,
  .user_ctx = NULL
};

esp_err_t get_battery_handler(httpd_req_t *req) {

  Serial.printf("Bat Voltage: %dmv\r\n", TimerCAM.Power.getBatteryVoltage());
  Serial.printf("Bat Level: %d%%\r\n", TimerCAM.Power.getBatteryLevel());
  esp_err_t res = ESP_OK;
  
  res = httpd_resp_set_type(req, "application/json");
  if (res == ESP_OK) {
    String voltage = String(TimerCAM.Power.getBatteryVoltage());
    String level = String(TimerCAM.Power.getBatteryLevel());
    String resp = "{";
    resp += "\"voltage\":\"" + voltage + "\",";
    resp += "\"level\":\"" + level + "\"";
    resp += "}";
    httpd_resp_send(req, resp.c_str(), resp.length());
  }
  return ESP_OK;
}

httpd_uri_t uri_get_battery = {
  .uri      = "/api/battery",
  .method   = HTTP_GET,
  .handler  = get_battery_handler,
  .user_ctx = NULL
};

esp_err_t get_led_on_handler(httpd_req_t *req) {
  const char resp[] = "LED on";
  TimerCAM.Power.setLed(255);
  httpd_resp_send(req, resp, strlen(resp));
  return ESP_OK;
}

httpd_uri_t uri_get_led_on = {
  .uri      = "/api/led/on",
  .method   = HTTP_GET,
  .handler  = get_led_on_handler,
  .user_ctx = NULL
};

esp_err_t get_led_off_handler(httpd_req_t *req) {
  const char resp[] = "LED off";
  TimerCAM.Power.setLed(0);
  httpd_resp_send(req, resp, strlen(resp));
  return ESP_OK;
}

httpd_uri_t uri_get_led_off = {
  .uri      = "/api/led/off",
  .method   = HTTP_GET,
  .handler  = get_led_off_handler,
  .user_ctx = NULL
};

esp_err_t get_version_handler(httpd_req_t *req) {
  const char resp[] = FW_VERSION;
  httpd_resp_send(req, resp, strlen(resp));
  return ESP_OK;
}

httpd_uri_t uri_get_version = {
  .uri      = "/api/version",
  .method   = HTTP_GET,
  .handler  = get_version_handler,
  .user_ctx = NULL
};


httpd_handle_t start_webserver(void) {
  httpd_config_t config = HTTPD_DEFAULT_CONFIG();

  httpd_handle_t server = NULL;


  if (httpd_start(&server, &config) == ESP_OK) {
    httpd_register_uri_handler(server, &uri_get_health_check);
    httpd_register_uri_handler(server, &uri_get_snapshot);
    httpd_register_uri_handler(server, &uri_get_shutdown);
    httpd_register_uri_handler(server, &uri_get_battery);
    httpd_register_uri_handler(server, &uri_get_led_on);
    httpd_register_uri_handler(server, &uri_get_led_off);
    httpd_register_uri_handler(server, &uri_get_version);
  }

  return server;
}

void stop_webserver(httpd_handle_t server) {
  if (server) {
    httpd_stop(server);
  }
}

こちらを書き込むと、API としては

  • /api/snapshot
    • 撮影して JPEG を返答
    • これが一番よく使う
  • /api/version
    • バージョン情報が JSON で返答
  • /api/led/on
    • LED がつく
  • /api/led/off
    • LED が消える
  • /api/battery
    • バッテリー量が JSON で返答
  • /api/shutdown
    • シャットダウン
    • これも地味に便利
  • /api/healthcheck
    • 稼働チェックでシンプルな返答

が、動きます。

/api/snapshot は撮影して JPEG を返答。

/api/battery では、バッテリー量が JSON で返答。

/api/healthcheck は稼働チェックでシンプルな返答。

あとは Wi-Fi 側につながったら固定 IP を指定してあげてアクセスしやすくして、この Timer Camera サーバに /api/snapshot でアクセスすれば撮影したJPEG が取得できて、いろいろな仕掛けに連携できます!