전체코드를 업로드 한 뒤 웹서버 접속이 안되는 문제 발생시 아래 코드 업로드
카메라 XCLK(GPIO0) 와 OLED SDA(GPIO0) 가 물리적으로 같이 묶여 있어서 문제 발생
MODE_CAMERA에 0 넣고 업로드(IP 확인 후) 1넣고 업로드
< robo 수정 코드
교재용 코드가 라이브러리 등 정상작동이 되지 않아서 수정함.
// 카메라 반전 설정
sensor_t * s = esp_camera_sensor_get();
s->set_vflip(s, 1); // 상하 반전
s->set_hmirror(s, 1); // 좌우 반전
카메라 반전 시 설정 코드 추가
✔️ 1. #include <...>
👉 표준 라이브러리 또는 설치된 외부 라이브러리를 포함
< >는 Arduino IDE가 설치된 기본 라이브러리 경로 또는
사용자가 라이브러리 매니저로 설치한 라이브러리 폴더에서 파일을 찾습니다.
Arduino 기본 제공 라이브러리
Arduino 라이브러리 매니저로 설치한 라이브러리
을 포함할 때 사용합니다.
✔️ 2. #include "..."
👉 프로젝트 폴더(스케치 폴더) 또는 사용자 추가 파일을 포함
" "는 현재 프로젝트(.ino 파일이 있는 폴더) 또는
사용자가 추가한 라이브러리 폴더에서 파일을 찾습니다.
내가 직접 가져온 헤더 파일
스케치와 같은 폴더에 있는 헤더
특정 라이브러리의 헤더 중 Arduino가 기본 경로에서 찾지 못하는 파일
을 읽을 때 사용합니다.
"파일명.h" 를 사용하면:
1️⃣ 현재 프로젝트 폴더에서 먼저 찾고
2️⃣ 사용자 라이브러리 폴더(libraries/) 를 찾고
3️⃣ 그래도 없으면 Arduino 기본 폴더를 찾습니다.
✔️ 1. map() 함수 기본 형태 map(value, fromLow, fromHigh, toLow, toHigh)
▶️ 의미
value: 변환하고 싶은 원래 값
fromLow ~ fromHigh: value가 속한 입력 범위
toLow ~ toHigh: 변환하고 싶은 출력 범위
즉,
입력값(value)이 원래 범위에서 어느 위치에 있는지 계산해서 → 새로운 범위의 값으로 바꿔줍니다.
✔️ 2. 예시로 쉽게 이해하기
예:
0~100% 속도를
0~255 모터 PWM(출력) 값으로 바꾸고 싶다면
map(40, 0, 100, 0, 255)
계산 과정
40%는 전체 100% 중 40%
255의 40% → 102가 됩니다.
그래서 아래 코드처럼 되는 거예요:
int car_speed = map(40, 0, 100, 0, 255);
출력값은 약 102.
✔️ 3. map() 함수를 학생 눈높이로 설명하면?
🚗 “속도 조절 다이얼” 비유
다이얼(0~100%)을 돌리면
실제 모터는 0~255 사이의 값(PWM)으로 동작함
→ map()이 “100% 스케일을 255 스케일로 바꿔주는 변환기” 역할을 한다
✔️ 4. map() 함수 실제 사용 예제들
✔️ ① 조이스틱 입력(0~1023)을 LED 밝기(0~255)로 변환
int ledValue = map(analogRead(A0), 0, 1023, 0, 255);
analogWrite(9, ledValue);
✔️ ② 온도센서 값(예: 20~40℃)을 서보 모터 각도(0~180°)로 변환
int angle = map(temp, 20, 40, 0, 180);
servo.write(angle);
✔️ ③ 초음파 센서 거리(0~300cm)를 그래프 바(0~100%)로 변환
int percent = map(distance, 0, 300, 0, 100);
✔️ 5. map()에서 주의할 점
⚠️ 1) map()은 정수로 계산함
소수점이 필요하면 직접 float 계산을 해야 함.
⚠️ 2) 값이 범위를 벗어나면 그대로 넘겨줌
예) map(150, 0, 100, 0, 255);
→ 150이 입력범위(0~100)를 넘었지만 지도 방식 그대로 계산돼서 255보다 큰 값이 나올 수 있음.
→ 필요하면 constrain()과 함께 사용 map(constrain(speed, 0, 100), 0, 100, 0, 255);
✔️ 6. 학생용 정리 한 줄 버전
map()은 숫자를 한 범위에서 다른 범위로 바꿔주는 함수이다.
예: 0~100% → 0~255 값으로 변환.
“이 OLED는 주소가 0x3C이고, SDA는 0번 핀, SCL은 2번 핀에 연결되어 있고, 화면 크기(해상도)는 128×32라고 설정하는 코드입니다.”
👉 **I2C 방식의 SSD1306 OLED 화면을 사용할 것이라고 선언하는 코드
OLED의 I2C 주소(0x3C) 는 사용자가 “정하는 것”이 아니라, OLED 모듈 안에 이미 정해져 있는 값이에요. 즉, 제품 자체가 가지고 있는 고유 주소
✔️ 1. I2C 장치는 모두 “주소(ID)”를 가지고 있다
I2C는 SDA(데이터), SCL(클럭) 두 개의 선으로 여러 장치를 동시에 연결하는 방식이라서
각 장치가 서로 충돌하지 않도록 고유 주소(7비트)를 가지고 있어야 해요.
OLED(SSD1306)는 보통 다음 두 주소 중 하나를 사용합니다(0x3C 0x3D) 즉, 제조사나 모듈 구조에 따라 두 주소 중 하나로 정해져 있습니다.
✔️ 2. OLED 주소는 하드웨어(회로) 구성으로 결정된다
SSD1306 OLED 모듈에는 A0 또는 SA0(또는 D/C) 라는 핀이 있습니다.
대부분의 상업용 OLED 모듈은 기본값이 GND로 연결됨 → 그래서 기본 주소가 0x3C
✔️ 3. 그럼 “내 OLED 주소가 0x3C인지 0x3D인지” 어떻게 확인할까?
아두이노에 아래 코드 업로드:
▶ 실행하면 OLED 주소가 자동으로 표시됨 예) I2C 장치 발견: 0x3C
#if와 #endif는 C/C++(Arduino 포함)에서 사용하는 전처리기 조건문이야.
즉, 컴파일되기 전에 코드 포함 여부를 결정하는 기능이다.
아주 쉽게 말하면:
“특정 조건이 참이면 이 코드 넣어라, 아니면 넣지 마라”
#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";
위 세 줄은 **ESP32 카메라 스트리밍(웹캠 스트림)**에 반드시 필요한 HTTP 멀티파트(MJPEG) 형식 문자열을 미리 만들어 둔 “상수 문자열”이야.
브라우저가 영상 스트림을 이해하려면 정확한 형식의 **헤더 + 바운더리(boundary)**가 필요
static const char* _STREAM_CONTENT_TYPE = "multipart/x-mixed-replace;boundary=" PART_BOUNDARY;
HTTP 응답의 타입을 "멀티파트(MJPEG 스트림)"이라고 선언하는 것.
"multipart/x-mixed-replace" → 끊임없이 이미지가 들어오는 스트림 형식
boundary=123456… → 각 프레임을 구분하는 구획선
이게 있어야 웹 브라우저가 "아, 이건 동영상처럼 반복되는 이미지 스트림이구나!" 라고 이해함.
static const char* _STREAM_BOUNDARY = "\r\n--" PART_BOUNDARY "\r\n";
한 프레임 끝나고 다음 프레임 시작할 때 사용되는 구분선.
예를 들어 boundary가 "123456789..." 이라면:
--123456789000000000000987654321
이 줄이 나오면:
➡ “여기서부터 다음 JPEG 이미지가 온다!” 라고 브라우저가 알게 됨.
static const char* _STREAM_PART = "Content-Type: image/jpeg\r\nContent-Length: %u\r\n\r\n";
프레임마다 붙는 헤더 형식.
Content-Type: image/jpeg → 다음 데이터는 JPEG임
Content-Length: %u → JPEG 크기를 숫자로 집어넣음 (%u 자리)
프레임 보내기 직전에 이렇게 채운다.
예시:
Content-Type: image/jpeg
Content-Length: 5321
(JPEG 데이터)
스트림 전송 순서는 다음과 같아:
--boundary
Content-Type: image/jpeg
Content-Length: 30000
[JPEG 데이터]
--boundary
Content-Type: image/jpeg
Content-Length: 29800
[JPEG 데이터]
...
브라우저는 이 반복되는 구조를 보고 “영상 스트림으로 재생”함.
이 HTML 파일을 프로그램 플래시 메모리에 저장해두고, 문자열 그대로 사용하기 위해 Raw Literal로 선언한다.
static const char PROGMEM INDEX_HTML[] = R"rawliteral(
<html>
...
</html>
)rawliteral";
(사용)httpd_resp_send(req, (const char *)INDEX_HTML, strlen(INDEX_HTML));
HTML 문서를 문자열 배열로 선언한다는 뜻.
static const char INDEX_HTML[]
static: 다른 파일에서 접근할 수 없는 내부 전용
const: 내용이 변경되지 않는 읽기 전용
char[]: 문자열 배열
즉,
➡ 수정 불가능한 HTML 문자열을 프로그램 내부에 하나만 만들어 둔다
Arduino (AVR 계열)에서 문자열을 **플래시 메모리(프로그램 메모리)**에 저장한다는 의미.
ESP32는 사실 PROGMEM 없어도 자동으로 플래시에 저장하지만 ESP32 Arduino에서도 PROGMEM을 사용하면
코드 가독성 유지
RAM 사용 최소화
긴 HTML, CSS, JS를 저장할 때 안정적
이런 장점이 있어.
이 부분이 핵심!
C++의 Raw String Literal(원시 문자열 리터럴) 문법이야.
문자열 안에 ", \n, \t 등을 그대로 쓸 수 있음
역슬래시(\)를 escape하지 않아도 됨
긴 HTML, CSS, JavaScript를 그대로 넣을 수 있음
IDE에서 보기 쉽고 실수할 일이 없음
예시 비교:
char html[] = "<html>\n<body>\n<h1>Title</h1>\n</body>\n</html>";
R"rawliteral(
<html>
<body>
<h1>Title</h1>
</body>
</html>
)rawliteral"
ESP32의 모든 함수들이 “성공/실패” 상태를 반환할 때 사용하는 정수 타입(enum 기반) 이다.
ESP-IDF(ESP32 공식 SDK)에서 제공하는 표준 오류 코드 타입이고, Arduino-ESP32에서도 그대로 사용한다.
즉,
➡ "함수가 제대로 실행됐는지, 어떤 오류가 났는지" 알려주는 자료형
ESP-IDF HTTP 서버에서 “특정 URL(경로)에 어떤 함수를 연결할 것인가” 를 정의하는 구조체(struct) 입니다.
이 상자 안에는
어떤 URL인가? (.uri)
어떤 HTTP method인가? (.method)
어떤 함수를 실행할 것인가? (.handler)
추가 데이터가 있는가? (.user_ctx)
이 4가지 정보가 들어 있습니다.
ESP-IDF 공식 정의는 다음과 같습니다:
typedef struct httpd_uri {
const char *uri; // URL path
httpd_method_t method; // GET, POST, PUT ...
esp_err_t (*handler)(httpd_req_t *r); // 요청 처리 함수
void *user_ctx; // 사용자 정의 데이터 전달 용
} httpd_uri_t;
.uri = "/action"
이 값은:
클라이언트가 접근할 때 사용할 URL
예: http://ESP32-IP/action
💡 코드를 보면 /, /action, /stream 세 개가 설정되어 있음.
.method = HTTP_GET
가능한 옵션:
HTTP_GET : 서버에게 **데이터를 요청(GET)**함. / “정보를 주세요!”
URL 뒤에 쿼리 붙음: /action?go=stop
서버 상태를 바꾸면 안 되는 요청이 원칙
✔ ESP32에서의 사용 예시
/ → 웹 페이지 가져오기
/capture → 카메라 사진 한 장 요청
/action?go=forward → 제어 명령 전달
HTTP_POST: 서버에 데이터를 보낸다(POST). / "여기 데이터 있어요. 처리해주세요!”
보통 폼 제출, 설정 저장, 업로드에 사용
GET보다 많은 양의 데이터를 보낼 수 있음
Wi-Fi 설정 입력값 저장
센서 데이터 업로드
파일 업로드
HTTP_DELETE : 서버에게 리소스를 삭제해달라고 요청 / “이걸 없애주세요!”
URL로 특정 항목을 지우는 데 사용됨
보안 이슈 때문에 잘 쓰지 않지만 표준에 존재
/file?name=test.txt → 파일 삭제
/device/logs → 저장된 로그 제거
HTTP_PUT : 서버에 리소스를 새로 만들거나 덮어씌움(생성/전체 업데이트) / “이걸 새로 세팅하거나 통째로 바꿔주세요!”
전체 값을 교체할 때 사용
POST와 비슷하지만 의미가 더 정확함
✔ ESP32 예시
/config에 새로운 설정 전체를 저장
/motor/state에 전체 상태값을 덮어쓰기
즉, 이 URL은 GET 요청만 허용하겠다! 라고 지정하는 것.
.handler = cmd_handler
그 URL로 요청이 들어오면 ESP32가 이 함수를 자동으로 호출합니다.
예:
/action → cmd_handler()
/ → index_handler()
/stream → stream_handler()
.user_ctx = NULL
필요한 경우 특정 데이터를 실어보낼 수 있는 공간입니다.
예:
.user_ctx = (void*)my_motor_struct;
그럼 cmd_handler에서 이 값을 다시 받아 사용할 수 있습니다.
httpd_uri_t index_uri = {
.uri = "/",
.method = HTTP_GET,
.handler = index_handler,
.user_ctx = NULL
};
→ “/로 요청이 오면 index_handler 실행”
httpd_uri_t cmd_uri = {
.uri = "/action",
.method = HTTP_GET,
.handler = cmd_handler,
.user_ctx = NULL
};
→ 자동차 제어용 URL
httpd_uri_t stream_uri = {
.uri = "/stream",
.method = HTTP_GET,
.handler = stream_handler,
.user_ctx = NULL
};
→ 웹캠 영상 스트리밍 처리 URL
이번에는 httpd_register_uri_handler() 함수를 한 줄처럼 “그냥 쓰는 함수”가 아니라, 정확히 무엇을 하는 함수인지 내부 구조까지 완벽하게 설명해드릴게요.
즉, “이 URL로 요청이 오면, 이 핸들러 함수를 실행해라” 라고 ESP32 웹서버에 등록하는 함수입니다.
브라우저·Python·앱에서 ESP32에 요청을 보낼 때 어떤 URL이 어떤 함수로 연결될지 결정하는 “라우터” 역할을 합니다.
ESP-IDF 문서에 있는 선언:
esp_err_t httpd_register_uri_handler(httpd_handle_t handle,
const httpd_uri_t *uri_handler);
handle: 시작된 HTTP 서버의 핸들(주소). httpd_start() 후에 얻음
uri_handler : 등록하고 싶은 URL + method + handler 함수 묶음 (httpd_uri_t)
httpd_start(&camera_httpd, &config);
이렇게 서버가 켜진 상태여야 함.
httpd_uri_t cmd_uri = {
.uri = "/action",
.method = HTTP_GET,
.handler = cmd_handler,
.user_ctx = NULL
};
이런 구조체는 “라우팅 규칙 1개”을 의미함.
httpd_register_uri_handler(camera_httpd, &cmd_uri);
이 순간부터 ESP32 HTTP 서버는 /action URL을 공식적으로 인식합니다.
누가 이 URL을 부르면?
→ cmd_handler() 함수가 실행됨!
static
이 함수의 사용 범위를 이 파일(.ino 또는 .cpp) 안으로 제한하겠다는 뜻이야.
같은 프로젝트 안에 다른 파일이 있더라도, 거기서는 index_handler를 직접 쓸 수 없음.
esp_err_t
ESP-IDF/ESP32에서 자주 쓰는 에러 코드 타입이야.
ESP_OK, ESP_FAIL, ESP_ERR_xxx 이런 값들을 리턴할 수 있어.
즉, “이 함수는 실행 결과가 성공인지 실패인지 나타내는 값을 돌려준다”라고 보면 됨.
index_handler
함수 이름.
이 함수는 보통 웹서버에 **URI 핸들러(콜백)**로 등록돼서 / 같은 주소 요청이 오면 실행돼.
(httpd_req_t *req)
httpd_req_t는 ESP-IDF/esp_http_server에서 미리 정의된 HTTP 요청 정보 구조체 타입이고,
*req는 그걸 가리키는 포인터.(req는 바꿀 수 있음. httpd_req_t *request / httpd_req_t *abc 등)
여기 안에 “어떤 주소로 접속했는지, 헤더는 뭐였는지, 응답은 어디로 보내야 하는지” 같은 정보가 들어있어.
이 줄은 HTTP 응답의 Content-Type(콘텐츠 타입)을 설정하는 코드야.
httpd_resp_set_type(...)
“이 응답은 어떤 종류의 데이터인지”를 브라우저에게 알려줌.
req
지금 처리 중인 그 HTTP 요청에 대한 응답 객체.
"text/html"
“이 응답 내용은 HTML 문서입니다”라고 브라우저에게 알려주는 문자열.
그래서 브라우저가 이걸 웹페이지로 렌더링해 줌.
만약 "image/jpeg"라면 이미지를 보여주려 할 거고, "text/plain"이면 그냥 텍스트로 보여줌.
이 줄은 실제 HTML 데이터를 브라우저로 보내고, 그 결과를 반환하는 코드야.
하나씩 쪼개보면:
위쪽에 보통 이렇게 정의돼 있었지?
static const char PROGMEM INDEX_HTML[] = R"rawliteral(
... HTML 내용 ...
)rawliteral";
즉, INDEX_HTML는 플래시 메모리에 저장된 HTML 문자열 배열이야.
이 안에 <html> ... </html> 전체 페이지 소스가 들어 있음.
INDEX_HTML의 타입이 const char[] 또는 const char PROGMEM[] 같은 형태라서
함수 인자 타입(const char *)에 딱 맞추기 위해 형변환(cast) 한 거야.
실제로는 “이 HTML 문자열의 시작 주소를 가리키는 포인터”라고 보면 돼.
INDEX_HTML 문자열의 길이(바이트 수) 를 구함.
httpd_resp_send는 “몇 바이트를 보낼지” 알아야 하니까 길이를 같이 전달하는 거야.
역할:
req에 대한 HTTP 응답으로, INDEX_HTML 내용을 그대로 브라우저에게 전송.
내부적으로는:
HTTP 헤더 + 바디(HTML)를 만들어서 클라이언트에 전송.
반환값:
성공하면 ESP_OK
실패하면 ESP_FAIL 또는 다른 에러 코드.
index_handler 함수의 반환값으로 httpd_resp_send(...) 결과를 그대로 돌려줌.
즉, 웹서버는
ESP_OK → “응답 잘 보냈네, 성공!”
그 외 값 → “문제 생김”
이런 식으로 판단 가능.
클라이언트가 http://IP/ 로 접속
ESP32 HTTP 서버가 / URI에 등록된 index_handler를 호출
index_handler:
응답 타입을 text/html로 설정
INDEX_HTML 안의 HTML 문서를 통째로 브라우저에 전송
그 결과(성공/실패 코드)를 리턴
브라우저는 받은 HTML을 화면에 렌더링 → 우리가 만든 조종 화면이 뜸 🚗📷