ESP32 PSRAM Timer Camera で簡易カメラ撮影サーバーを作ったメモ
この記事は IoTLT Advent Calendar 2024 の 3 日目の記事です。
ESP32 PSRAM Timer Camera X で簡易カメラ撮影サーバーを作ったメモです。
Timer Camera たのしい
最近、こちらの Timer Camera で AI 画像認識の入り口として使うなど色々やっていて楽しいです。
自分が使っている簡易カメラ撮影サーバーの仕組みがだいぶ整ってきたので、今回はメモしておこうかなと思います。
参考資料
参考資料としては、構築方法や大まかな特性を掴むなど、以下の記事がとても参考になりました。感謝いたします!
- M5Stack ESP32 PSRAM Timer Camera X (OV3660) | Lang-ship
- macOS上に、ESP32 PSRAM Timer Camera Fの開発環境を構築する方法について
- Timer Cameraでサンプルプログラムを動かす | さとやまノート
どんな仕組みを目指したか

実際に簡易カメラ撮影サーバーを作るうえでは、公式ドキュメントを参考にしました。
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 が取得できて、いろいろな仕掛けに連携できます!