M5Cameraのわかりやすいサンプル

```
// [実装機能]
// スマホとカメラを直接接続とルータにカメラを接続を#defineで切り替え可能
// LED制御、パン・チルト用サーボ制御、静止画・動画取得対応
// ルータにカメラを接続した際、LINEにカメラのIPアドレスを通知（アクセストークン取り扱い注意）
// [todo]
// はじめはカメラに直接接続し、ルータのSSIDとパスワーを設定するとカメラがルータにつながる
// 動画再生中の静止画取得
// 画面に動きがあったときに画像をLineに投稿

#include <WiFi.h>
#include <esp_camera.h>
#include <esp_http_server.h>
#include <soc/rtc_cntl_reg.h>
#include <HTTPClient.h>
#include <base64.h>

// パン用チャネル
#define PAN 14
// チルト用チャネル
#define TILT 15

// カメラに直接接続、未定義でカメラをルータにつなげる
#define WIFI_DIRECT
// カメラに設定する、またはルータのSSID
#define SSID          "webcam"
// カメラに設定する、またはルータのパスワード（未定義でパスワードなし）
//#define PASSWORD      "webcam1234"

//LINE通知用トークン(未定義の場合は通知しない)
//#define LINE_TOKEN "https://notify-bot.line.me/ja/で取得"

// 区切り用のランダム文字列
#define BOUNDARY_KEY  "123456789000000000000987654321"
// 区切り
#define BOUNDARY      "\r\n--" BOUNDARY_KEY "\r\n"

String urlencode(const String &s) {
  static const char lookup[]= "0123456789abcdef";
  String result;
  size_t len = s.length();
  for(size_t i = 0; i < len; i++) {
    const char c = s[i];
    if(('0' <= c && c <= '9') || ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || (c=='-' || c=='_' || c=='.' || c=='~')) {
      result += c;
    } else {
      result += "%" + String(lookup[(c & 0xf0) >>4]) + String(lookup[c & 0x0f]);
    }
  }
  return result;
}

// 写真撮影
struct Shot {

  // カメラフレーム
  camera_fb_t* frame;

  // カメラ画像取得
  Shot() : frame(esp_camera_fb_get()) {
    if (!ok()) {
      Serial.println("esp_camera_fb_get failed");
    }
  }

  // 取得成功かどうか
  inline bool ok() {
    return frame != NULL;
  }
  // バイナリデータ
  inline uint8_t* data() {
    return frame->buf;
  }
  
  // バイト数
  inline size_t size() {
    return frame->len;
  }

  // リソース開放
  ~Shot() {
    if (frame != NULL) {
      esp_camera_fb_return(frame);
    }
  }
};

// トップページ
static char html[] = "<div><img/></div>"
"<input type=\"button\" value=\"up\">"
"<input type=\"button\" value=\"down\">"
"<input type=\"button\" value=\"left\">"
"<input type=\"button\" value=\"right\">"
"<input type=\"button\" value=\"shot\">"
"<input type=\"button\" value=\"led\">"
"<input type=\"button\" value=\"stop\">"
"<script>"
"document.querySelector('[value=up]').addEventListener('click', function() { fetch('/up') });"
"document.querySelector('[value=down]').addEventListener('click', function() { fetch('/down') });"
"document.querySelector('[value=left]').addEventListener('click', function() { fetch('/left') });"
"document.querySelector('[value=right]').addEventListener('click', function() { fetch('/right') });"
"document.querySelector('[value=shot]').addEventListener('click', function() { location.href = '/shot' });"
"document.querySelector('[value=led]').addEventListener('click', function() { fetch('/led') });"
"document.querySelector('[value=stop]').addEventListener('click', function() { fetch('/stop') });"
"setTimeout(function() { document.querySelector('img').src = 'http://' + location.host + ':81/stream' }, 1000);"
"</script>";
static esp_err_t index_handler(httpd_req_t *request) {
  Serial.println("index_handler");

  esp_err_t result = httpd_resp_set_type(request, "text/html; charset=UTF-8");
  if (result == ESP_OK) {
    result = httpd_resp_send(request, html, sizeof(html));
  }

  if (result != ESP_OK) {
    Serial.printf("httpd_resp_send(html) error 0x%x\n", result);
    return httpd_resp_send_500(request);
  }
  return httpd_resp_send(request, NULL, 0);
}

// LED制御
static uint8_t led_state = HIGH;
static esp_err_t led_handler(httpd_req_t *request) {
  Serial.println("led_handler");

  led_state = led_state == LOW ? HIGH : LOW;
  digitalWrite(GPIO_NUM_14, led_state);
  return httpd_resp_send(request, NULL, 0);
}

// パン用のサーボを動かす
static int pan_angle = 0;
static esp_err_t pan_handler(httpd_req_t *request) {
  Serial.println("pan_handler");

  pan_angle += (int)request->user_ctx;
  if (pan_angle > 90) {
    pan_angle = -90;
  }
  ledcWrite(PAN, map(pan_angle, -90, 90, 26, 123));  // -90°〜90°を26〜123に比例計算
  return httpd_resp_send(request, NULL, 0);
}

// チルト用のサーボを動かす
static int tilt_angle = 0;
static esp_err_t tilt_handler(httpd_req_t *request) {
  Serial.println("tilt_handler");

  tilt_angle += (int)request->user_ctx;
  if (tilt_angle > 90) {
    tilt_angle = -90;
  }
  ledcWrite(TILT, map(tilt_angle, -90, 90, 26, 123));  // -90°〜90°を26〜123に比例計算
  return httpd_resp_send(request, NULL, 0);
}

// 写真撮影
static esp_err_t shot_handler(httpd_req_t *request) {
  Serial.println("shot_handler");

  Shot shot;
  if(shot.ok()) {

    // レスポンスの種類を指定
    esp_err_t result = httpd_resp_set_type(request, "image/jpeg");

    // ダウンロード用ヘッダ追加
    if (result == ESP_OK) {
      result = httpd_resp_set_hdr(request, "Content-Disposition", "attachment; filename=\"shot.jpg\"");
    }

    // 画像を送信
    if (result == ESP_OK) {
      result = httpd_resp_send(request, (char*)shot.data(), shot.size());
    }

    if (result != ESP_OK) {
      Serial.printf("shot_handler error 0x%x\n", result);
      return httpd_resp_send_500(request);
    }
  }
  return httpd_resp_send(request, NULL, 0);
}

// 動画撮影
static bool playing = true;
static esp_err_t stream_handler(httpd_req_t *request) {
  Serial.println("stream_handler");

  // レスポンスの種類を指定
  esp_err_t result = httpd_resp_set_type(request, "multipart/x-mixed-replace;boundary=" BOUNDARY_KEY);
  if (result != ESP_OK) {
    Serial.printf("httpd_resp_set_type(multipart) error 0x%x\n", result);
  }

  // 繰り返し画像を送信（動画のように見える）
  playing = true;
  while (result == ESP_OK && playing) {

    //撮影
    Shot shot;
    if (!shot.ok()) break;

    // ヘッダ部分の送信
    char buffer[64];
    size_t length = snprintf(buffer, sizeof(buffer), "Content-Type: image/jpeg\r\nContent-Length: %u\r\n\r\n", shot.size());
    result = httpd_resp_send_chunk(request, buffer, length);
    if (result != ESP_OK) {
      Serial.printf("httpd_resp_send_chunk(header) error 0x%x\n", result);
      continue;
    }

    // JPEGの送信
    result = httpd_resp_send_chunk(request, (char*)shot.data(), shot.size());
    if (result != ESP_OK) {
      Serial.printf("httpd_resp_send_chunk(image) error 0x%x\n", result);
      continue;
    }

    // 区切り送信
    result = httpd_resp_send_chunk(request, BOUNDARY, strlen(BOUNDARY));
    if (result != ESP_OK) {
      Serial.printf("httpd_resp_send_chunk(boundary) error 0x%x\n", result);
      continue;
    }
  }
  if (result != ESP_OK) {
    Serial.printf("stream_handler error 0x%x\n", result);
    return httpd_resp_send_500(request);
  }
  return httpd_resp_send(request, NULL, 0);
}

// 動画停止
static esp_err_t stop_handler(httpd_req_t *request) {
  Serial.println("stop_handler");

  playing = false;

  return httpd_resp_send(request, NULL, 0);
}

// 初期設定
void setup() {
  // 電圧低下検出無効化
  WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0);
 
  // シリアル通信開始
  Serial.begin(115200);
  Serial.setDebugOutput(true);

  //LED初期化
  Serial.println("led begin");
  pinMode(GPIO_NUM_14, OUTPUT); //GPIO14番を出力モードに設定
  delay(300);
  digitalWrite(GPIO_NUM_14, HIGH);
  delay(300);
  digitalWrite(GPIO_NUM_14, LOW);
  delay(300);
  digitalWrite(GPIO_NUM_14, HIGH);
  delay(300);
  digitalWrite(GPIO_NUM_14, LOW);

  // サーボ初期化
  Serial.println("servo begin");
  ledcSetup(PAN, 50, 10);  // (チャネル, 周波数, 分解能)
  ledcAttachPin(GPIO_NUM_4, PAN);   // (ピン番号, チャネル)
  ledcSetup(TILT, 50, 10);  // (チャネル, 周波数, 分解能)
  ledcAttachPin(GPIO_NUM_13, TILT);   // (ピン番号, チャネル)

  // カメラ接続
  Serial.println("camera begin");
  camera_config_t config = {
    .pin_pwdn = -1,
    .pin_reset = 15,
    .pin_xclk = 27,
    .pin_sscb_sda = 22,
    .pin_sscb_scl = 23,
    .pin_d7 = 19,
    .pin_d6 = 36,
    .pin_d5 = 18,
    .pin_d4 = 39,
    .pin_d3 = 5,
    .pin_d2 = 34,
    .pin_d1 = 35,
    .pin_d0 = 32,
    .pin_vsync = 25,
    .pin_href = 26,
    .pin_pclk = 21,
    .xclk_freq_hz = 20000000, // 20MHz
    .ledc_timer = LEDC_TIMER_0, // 0番のタイマー使用
    .ledc_channel = LEDC_CHANNEL_0, // 0番のチャネル使用
    .pixel_format = PIXFORMAT_JPEG, // JPEG
    .frame_size = FRAMESIZE_VGA, // 解像度
    .jpeg_quality = 12, // JPGE画質（小さいほど高画質）
    .fb_count = 1 // フレームバッファ数（２つあれば２倍で処理できる？）
  };  
  esp_err_t result = esp_camera_init(&config);
  if (result != ESP_OK) {
    Serial.printf("esp_camera_init error 0x%x\n", result);
    return;
  }

  Serial.println("wifi begin");

#ifdef WIFI_DIRECT
  // アクセスポイントモード
  WiFi.mode(WIFI_AP);
  // アクセスポイント開始
#  ifdef PASSWORD
  WiFi.softAP(SSID, PASSWORD);
#  else
  WiFi.softAP(SSID);
#  endif
  Serial.println("Please Connect to SSID: " SSID);
  Serial.print("Browse http://");
  Serial.println(WiFi.softAPIP());
#else
  // ステーションモード
  WiFi.mode(WIFI_STA);
#  ifdef PASSWORD
  WiFi.begin(SSID, PASSWORD);
#  else
  WiFi.begin(SSID);
#  endif
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("WiFi connected");
  Serial.print("Please browse http://");
  Serial.println(WiFi.localIP());

#  ifdef LINE_TOKEN
  // LINEにカメラのIPアドレスを通知
  HTTPClient http;
  http.begin("https://notify-api.line.me/api/notify");
  http.addHeader("Authorization", "Bearer " LINE_TOKEN);
  http.addHeader("Content-Type", "application/x-www-form-urlencoded");
  http.POST("message=" + urlencode("http://" + WiFi.localIP().toString()));
#  endif
#endif

  // HTTPサーバ開始
  Serial.println("httpd begin");
  httpd_handle_t httpd;
  httpd_config_t httpd_config = HTTPD_DEFAULT_CONFIG();
  httpd_config.server_port = 80; // ８０番ポートで待ち受け
  result = httpd_start(&httpd, &httpd_config);
  if (result != ESP_OK) {
    Serial.printf("httpd_start(%d) error 0x%x\n", httpd_config.server_port, result);
    return;
  }
  Serial.printf("lisen port %d\n", httpd_config.server_port);

  // ストリーミング用サーバ開始
  httpd_handle_t httpd_stream;
  httpd_config.server_port += 1;
  httpd_config.ctrl_port += 1;
  result = httpd_start(&httpd_stream, &httpd_config);
  if (result != ESP_OK) {
    Serial.printf("httpd_start(%d) error 0x%x\n", httpd_config.server_port, result);
    return;
  }
  Serial.printf("lisen port %d\n", httpd_config.server_port);

  // トップページリクエストハンドラ登録
  httpd_uri_t index = {
    .uri       = "/",
    .method    = HTTP_GET,
    .handler   = index_handler,
    .user_ctx  = NULL
  };
  httpd_register_uri_handler(httpd, &index);
  if (result != ESP_OK) {
    Serial.printf("httpd_register_uri_handler(index) error 0x%x\n", result);
    return;
  }

  // LED制御リクエストハンドラ登録
  httpd_uri_t led = {
    .uri       = "/led",
    .method    = HTTP_GET,
    .handler   = led_handler,
    .user_ctx  = NULL
  };
  httpd_register_uri_handler(httpd, &led);
  if (result != ESP_OK) {
    Serial.printf("httpd_register_uri_handler(led) error 0x%x\n", result);
    return;
  }

  // 画像取得リクエストハンドラ登録
  httpd_uri_t shot = {
    .uri       = "/shot",
    .method    = HTTP_GET,
    .handler   = shot_handler,
    .user_ctx  = NULL
  };
  httpd_register_uri_handler(httpd, &shot);
  if (result != ESP_OK) {
    Serial.printf("httpd_register_uri_handler(shot) error 0x%x\n", result);
    return;
  }

  // 動画取得リクエストハンドラ登録
  httpd_uri_t stream = {
    .uri       = "/stream",
    .method    = HTTP_GET,
    .handler   = stream_handler,
    .user_ctx  = NULL
  };
  httpd_register_uri_handler(httpd_stream, &stream);
  if (result != ESP_OK) {
    Serial.printf("httpd_register_uri_handler(stream) error 0x%x\n", result);
    return;
  }

  // 動画停止リクエストハンドラ登録
  httpd_uri_t stop = {
    .uri       = "/stop",
    .method    = HTTP_GET,
    .handler   = stop_handler,
    .user_ctx  = NULL
  };
  httpd_register_uri_handler(httpd, &stop);
  if (result != ESP_OK) {
    Serial.printf("httpd_register_uri_handler(stop) error 0x%x\n", result);
    return;
  }

  // パン左用サーボ制御リクエストハンドラ登録
  httpd_uri_t left = {
    .uri       = "/left",
    .method    = HTTP_GET,
    .handler   = pan_handler,
    .user_ctx  = (void*)5
  };
  httpd_register_uri_handler(httpd, &left);
  if (result != ESP_OK) {
    Serial.printf("httpd_register_uri_handler(left) error 0x%x\n", result);
    return;
  }

  // パン右用サーボ制御リクエストハンドラ登録
  httpd_uri_t right = {
    .uri       = "/right",
    .method    = HTTP_GET,
    .handler   = pan_handler,
    .user_ctx  = (void*)-5
  };
  httpd_register_uri_handler(httpd, &right);
  if (result != ESP_OK) {
    Serial.printf("httpd_register_uri_handler(right) error 0x%x\n", result);
    return;
  }

  // チルトUP用サーボ制御リクエストハンドラ登録
  httpd_uri_t up = {
    .uri       = "/up",
    .method    = HTTP_GET,
    .handler   = tilt_handler,
    .user_ctx  = (void*)5
  };
  httpd_register_uri_handler(httpd, &up);
  if (result != ESP_OK) {
    Serial.printf("httpd_register_uri_handler(up) error 0x%x\n", result);
    return;
  }

  // チルトDOWN用サーボ制御リクエストハンドラ登録
  httpd_uri_t down = {
    .uri       = "/down",
    .method    = HTTP_GET,
    .handler   = tilt_handler,
    .user_ctx  = (void*)-5
  };
  httpd_register_uri_handler(httpd, &down);
  if (result != ESP_OK) {
    Serial.printf("httpd_register_uri_handler(down) error 0x%x\n", result);
    return;
  }

  digitalWrite(GPIO_NUM_14, led_state);
  Serial.println("setup complete");
}

// 繰り返し処理
void loop() {
  delay(1000);
}
```