안녕하세요? 만들오 입니다.
아래 글에서 사용한 ESP CAM의 아두이노 코드를 공유합니다.
2021/01/24 - [레고테크닉] - [레고테크닉] 42122 지프 랭글러 구동개조 - ESP CAM
작동 영상
외부 라이브러리는 Websocket 뿐입니다. 간략히 요약한 설치 방법은 다음과 같습니다.
- 다음 주소에서 AsyncTCP 설치
https://github.com/me-no-dev/AsyncTCP - 다음 주소에서 ESPAsyncWebserver 설치
https://github.com/me-no-dev/ESPAsyncWebServer - 라이브러리 설치방법 : Github에서 zip 파일로 내려받은 후, 스케치 -> 라이브러리 포함하기 -> .ZIP 라이브러리
컨셉을 요약하면,
- ip/stream 경로에 카메라 스트리밍 서버를 만듭니다.
- ip의 최상위 경로(/)에 웹서버를 만들고, img 태그에 카메라 스트리밍 이미지를 불러옵니다.
- 웹서버의 81번 포트에 웹소켓을 뚫어, 클라이언트(접속자)의 이벤트에 따라 RC카를 제어합니다.
코드는 분할 없이 1개의 파일로 작성했습니다.
#include <WebSocketsServer.h>
#include <WiFi.h>
#include "esp_camera.h"
#include "esp_timer.h"
#include "img_converters.h"
#include "Arduino.h"
#include "fb_gfx.h"
#include "soc/soc.h"
#include "soc/rtc_cntl_reg.h"
#include "esp_http_server.h"
#define PWDN_GPIO_NUM 32
#define RESET_GPIO_NUM -1
#define XCLK_GPIO_NUM 0
#define SIOD_GPIO_NUM 26
#define SIOC_GPIO_NUM 27
#define Y9_GPIO_NUM 35
#define Y8_GPIO_NUM 34
#define Y7_GPIO_NUM 39
#define Y6_GPIO_NUM 36
#define Y5_GPIO_NUM 21
#define Y4_GPIO_NUM 19
#define Y3_GPIO_NUM 18
#define Y2_GPIO_NUM 5
#define VSYNC_GPIO_NUM 25
#define HREF_GPIO_NUM 23
#define PCLK_GPIO_NUM 22
const char* ssid = "Your SSID";
const char* password = "Your PASSWORD";
const int SERVO_RESOLUTION = 16;
uint32_t duty;
//Websocket setting
WebSocketsServer webSocket = WebSocketsServer(81);
void onWebSocketEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t length) {
if(type == WStype_TEXT){
String cmd = (char*)payload;
Serial.println(cmd);
if(cmd == "0909"){
ledcWrite(2, 0);
ledcWrite(4, 4900);
}
else{
int x = map(cmd.substring(0, 2).toInt(), 0, 18, 2000, 7800);
int y = map(cmd.substring(2, 4).toInt(), 0, 18, 2000, 7800);
ledcWrite(2, y);
ledcWrite(4, x);
}
}
}
#define PART_BOUNDARY "123456789000000000000987654321"
static const char* _STREAM_CONTENT_TYPE = "multipart/x-mixed-replace;boundary=" PART_BOUNDARY;
static const char* _STREAM_BOUNDARY = "\r\n--" PART_BOUNDARY "\r\n";
static const char* _STREAM_PART = "Content-Type: image/jpeg\r\nContent-Length: %u\r\n\r\n";
httpd_handle_t stream_httpd = NULL;
static const char PROGMEM INDEX_HTML[] = R"rawliteral(
<!DOCTYPE html>
<html>
<body>
<div style="width:100vw; height:100vw">
<img id="img">
</div>
</body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/nipplejs/0.8.7/nipplejs.min.js"></script>
<script>
const ip = window.location.protocol + "//" + window.location.hostname;
const img = document.getElementById("img");
img.src = ip + "/stream";
const ws = new WebSocket(ip.replace("http", "ws") + ":81");
ws.onopen = function (msg) {
console.log("Connected");
};
ws.onclose = function (msg) {
console.log("Closed");
};
ws.onmessage = function (msg) {
console.log(msg.data);
};
const joystick = nipplejs.create({
mode: 'dynamic',
color: 'green',
size: 180
});
let start_x, start_y;
joystick.on('start', function (evt, obj) {
start_x = obj.position.x;
start_y = obj.position.y;
});
let cur_x, cur_y, x, y, command_prev;
joystick.on('move', function (evt, obj) {
cur_x = obj.position.x;
cur_y = obj.position.y;
//range : -50 ~ 50
x = Math.round(cur_x - start_x, 0);
y = Math.round(start_y - cur_y, 0);
//Make range to -4 ~ 22
x = Math.round((x / 3.846) + 9, 0);
y = Math.round((y / 3.846) + 9, 0);
//Trim to 0 ~ 18
if (x < 0) x = 0;
else if (x > 18) x = 18;
if (y < 0) y = 0;
else if (y > 18) y = 18;
//Make string command combination
const command = String(x).padStart(2, "0") + String(y).padStart(2, "0")
if (command != command_prev){
ws.send(command);
command_prev = command;
}
});
joystick.on('end', function (evt, obj) {
ws.send("0909");
});
</script>
</html>
)rawliteral";
static esp_err_t index_handler(httpd_req_t *req){
httpd_resp_set_type(req, "text/html");
return httpd_resp_send(req, (const char *)INDEX_HTML, strlen(INDEX_HTML));
}
static esp_err_t stream_handler(httpd_req_t *req){
camera_fb_t * fb = NULL;
esp_err_t res = ESP_OK;
size_t _jpg_buf_len = 0;
uint8_t * _jpg_buf = NULL;
char * part_buf[64];
res = httpd_resp_set_type(req, _STREAM_CONTENT_TYPE);
if(res != ESP_OK){
return res;
}
while(true){
fb = esp_camera_fb_get();
if (!fb) {
Serial.println("Camera capture failed");
res = ESP_FAIL;
} else {
if(fb->width > 400){
if(fb->format != PIXFORMAT_JPEG){
bool jpeg_converted = frame2jpg(fb, 80, &_jpg_buf, &_jpg_buf_len);
esp_camera_fb_return(fb);
fb = NULL;
if(!jpeg_converted){
Serial.println("JPEG compression failed");
res = ESP_FAIL;
}
} else {
_jpg_buf_len = fb->len;
_jpg_buf = fb->buf;
}
}
}
if(res == ESP_OK){
size_t hlen = snprintf((char *)part_buf, 64, _STREAM_PART, _jpg_buf_len);
res = httpd_resp_send_chunk(req, (const char *)part_buf, hlen);
}
if(res == ESP_OK){
res = httpd_resp_send_chunk(req, (const char *)_jpg_buf, _jpg_buf_len);
}
if(res == ESP_OK){
res = httpd_resp_send_chunk(req, _STREAM_BOUNDARY, strlen(_STREAM_BOUNDARY));
}
if(fb){
esp_camera_fb_return(fb);
fb = NULL;
_jpg_buf = NULL;
} else if(_jpg_buf){
free(_jpg_buf);
_jpg_buf = NULL;
}
if(res != ESP_OK){
break;
}
}
return res;
}
void startCameraServer(){
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
config.server_port = 80;
httpd_uri_t index_uri = {
.uri = "/",
.method = HTTP_GET,
.handler = index_handler,
.user_ctx = NULL
};
httpd_uri_t stream_uri = {
.uri = "/stream",
.method = HTTP_GET,
.handler = stream_handler,
.user_ctx = NULL
};
if (httpd_start(&stream_httpd, &config) == ESP_OK) {
httpd_register_uri_handler(stream_httpd, &index_uri);
httpd_register_uri_handler(stream_httpd, &stream_uri);
}
}
void setup() {
WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0); //disable brownout detector
Serial.begin(115200);
Serial.setDebugOutput(false);
ledcSetup(2, 50, SERVO_RESOLUTION); //channel, freq, resolution
ledcAttachPin(13, 2); // pin, channel
ledcSetup(4, 50, SERVO_RESOLUTION); //channel, freq, resolution
ledcAttachPin(12, 4); // pin, channel
camera_config_t config;
config.ledc_channel = LEDC_CHANNEL_0;
config.ledc_timer = LEDC_TIMER_0;
config.pin_d0 = Y2_GPIO_NUM;
config.pin_d1 = Y3_GPIO_NUM;
config.pin_d2 = Y4_GPIO_NUM;
config.pin_d3 = Y5_GPIO_NUM;
config.pin_d4 = Y6_GPIO_NUM;
config.pin_d5 = Y7_GPIO_NUM;
config.pin_d6 = Y8_GPIO_NUM;
config.pin_d7 = Y9_GPIO_NUM;
config.pin_xclk = XCLK_GPIO_NUM;
config.pin_pclk = PCLK_GPIO_NUM;
config.pin_vsync = VSYNC_GPIO_NUM;
config.pin_href = HREF_GPIO_NUM;
config.pin_sscb_sda = SIOD_GPIO_NUM;
config.pin_sscb_scl = SIOC_GPIO_NUM;
config.pin_pwdn = PWDN_GPIO_NUM;
config.pin_reset = RESET_GPIO_NUM;
config.xclk_freq_hz = 20000000;
config.pixel_format = PIXFORMAT_JPEG;
config.frame_size = FRAMESIZE_VGA;
config.jpeg_quality = 25;
config.fb_count = 2;
// Camera init
esp_err_t err = esp_camera_init(&config);
if (err != ESP_OK) {
Serial.printf("Camera init failed with error 0x%x", err);
return;
}
// Wi-Fi connection
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("");
Serial.println("WiFi connected");
Serial.print("Camera Stream Ready! Go to: http://");
Serial.print(WiFi.localIP());
Serial.println("");
// Start streaming web server
startCameraServer();
// Start websocekt server
webSocket.begin();
webSocket.onEvent(onWebSocketEvent);
}
void loop() {
webSocket.loop();
}
기타 문의사항은 댓글을 남겨주세요.
감사합니다.
728x90
'하드웨어 > 아두이노' 카테고리의 다른 글
[아두이노] i2c 기반 모터쉴드 사용법(Wemos D1 mini motor shield) (0) | 2021.04.07 |
---|---|
[아두이노] 아두이노 IDE 환경설정 (ESP8266, ESP32) (0) | 2021.04.05 |
[아두이노] ESP32-CAM 1년 사용 후기 (0) | 2021.03.30 |
[아두이노] ESP32 시리얼 통신 2개 사용하기 (0) | 2021.03.10 |
[아두이노] ESP32-CAM 웹서버 CORS 문제 해결하기 (0) | 2021.03.01 |
댓글