// [実装機能]
// スマホとカメラを直接接続とルータにカメラを接続を#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);
}
More than 5 years have passed since last update.
M5Cameraのわかりやすいサンプル
Last updated at Posted at 2019-11-24
Register as a new user and use Qiita more conveniently
- You get articles that match your needs
- You can efficiently read back useful information
- You can use dark theme