5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2019-11-24
// [実装機能]
// スマホとカメラを直接接続とルータにカメラを接続を#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 // フレームバッファ数(2つあれば2倍で処理できる?)
  };  
  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; // 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);
}
5
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?