1. Overview ESP32-CAM
ESP32-CAM adalah modul kompak berbasis ESP32 dengan kamera OV2640 built-in, slot microSD, dan LED flash. Dengan harga di bawah Rp50.000, modul ini menjadi solusi paling populer untuk proyek kamera IoT murah.
Spesifikasi ESP32-CAM
| Spesifikasi | Detail |
|---|---|
| Processor | ESP32-S (Xtensa dual-core 240MHz) |
| RAM | 520 KB SRAM + 4 MB PSRAM (opsional) |
| Flash | 4 MB |
| Kamera | OV2640 (2MP, JPEG output) |
| Resolusi | UXGA (1600x1200) hingga QQVGA (160x120) |
| WiFi | 802.11 b/g/n (2.4 GHz) |
| Bluetooth | BLE 4.2 |
| GPIO | 9 pin (terbatas karena kamera) |
| SD Card | MicroSD (FAT32, max 4GB) |
| Dimensi | 27mm à 40.5mm à 4.5mm |
ESP32-CAM menggunakan banyak GPIO internal untuk kamera, sehingga tersisa hanya 9 GPIO yang bisa dipakai user (GPIO 0, 1, 3, 4, 12, 13, 14, 15, 16). GPIO 0 harus HIGH saat normal boot (LOW saat flashing). GPIO 1 dan 3 adalah UART TX/RX.
- â Harga murah (~30rb)
- â PSRAM 4MB built-in
- â LED flash on-board
- â MicroSD slot
- â Tidak ada USB-to-serial
- â Pin terbatas
- â USB OTG built-in
- â RAM lebih besar (512KB + PSRAM)
- â AI acceleration
- â OV5640 (5MP) support
- â Lebih mahal (~60rb)
- â Ekosistem lebih muda
2. Setup Hardware & Wiring
ESP32-CAM tidak memiliki USB-to-serial onboard, jadi kita memerlukan FTDI adapter atau board ESP32-CAM-MB (yang sudah include USB) untuk memprogramnya.
Wiring dengan FTDI Adapter
| ESP32-CAM Pin | FTDI Pin | Keterangan |
|---|---|---|
| 5V | 5V / VCC | Power supply |
| GND | GND | Ground |
| U0R (GPIO3) | TX | UART receive |
| U0T (GPIO1) | RX | UART transmit |
| GPIO0 | GND (saat flash) | Flash mode (lepas setelah flash) |
Untuk mem-flash: hubungkan GPIO0 ke GND lalu tekan RESET. Untuk menjalankan program: lepas GPIO0 dari GND lalu tekan RESET. Board ESP32-CAM-MB melakukan ini otomatis.
PlatformIO Configuration
[env:esp32cam]
platform = espressif32
board = esp32cam
framework = arduino
monitor_speed = 115200
board_build.partitions = huge_app.csv
; Tambahkan PSRAM
build_flags =
-DBOARD_HAS_PSRAM
-mfix-esp32-psram-cache-issue
; Upload via FTDI
upload_speed = 460800
upload_port = COM3
3. OV2640 Camera Configuration
OV2640 adalah sensor kamera 2MP yang mendukung output JPEG hardware. Di ESP32-CAM, konfigurasi kamera ditangani oleh library esp_camera.h.
#include "esp_camera.h"
// Pin mapping untuk AI-Thinker ESP32-CAM
#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
bool initCamera() {
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_sccb_sda = SIOD_GPIO_NUM;
config.pin_sccb_scl = SIOC_GPIO_NUM;
config.pin_pwdn = PWDN_GPIO_NUM;
config.pin_reset = RESET_GPIO_NUM;
config.xclk_freq_hz = 20000000; // 20 MHz clock
config.pixel_format = PIXFORMAT_JPEG;
config.grab_mode = CAMERA_GRAB_LATEST;
config.fb_location = CAMERA_FB_IN_PSRAM;
config.jpeg_quality = 12;
config.fb_count = 2; // Double buffer untuk streaming
// Resolusi berdasarkan PSRAM
if (psramFound()) {
config.frame_size = FRAMESIZE_UXGA; // 1600x1200
config.jpeg_quality = 10;
config.fb_count = 2;
Serial.println("PSRAM ditemukan â resolusi UXGA");
} else {
config.frame_size = FRAMESIZE_SVGA; // 800x600
config.jpeg_quality = 12;
config.fb_count = 1;
config.fb_location = CAMERA_FB_IN_DRAM;
Serial.println("PSRAM tidak ditemukan â resolusi SVGA");
}
esp_err_t err = esp_camera_init(&config);
if (err != ESP_OK) {
Serial.printf("Camera init GAGAL: 0x%x\n", err);
return false;
}
// Fine-tune sensor settings
sensor_t *s = esp_camera_sensor_get();
if (s) {
s->set_brightness(s, 1); // -2 to 2
s->set_contrast(s, 1); // -2 to 2
s->set_saturation(s, 0); // -2 to 2
s->set_whitebal(s, 1); // Auto white balance
s->set_awb_gain(s, 1); // AWB gain
s->set_wb_mode(s, 0); // 0=Auto, 1=Sunny, 2=Cloudy, 3=Office, 4=Home
s->set_exposure_ctrl(s, 1); // Auto exposure
s->set_aec2(s, 1); // AEC DSP
s->set_gain_ctrl(s, 1); // Auto gain
s->set_agc_gain(s, 0); // 0-30
s->set_gainceiling(s, (gainceiling_t)6); // 2x-128x
s->set_bpc(s, 1); // Black pixel correct
s->set_wpc(s, 1); // White pixel correct
s->set_lenc(s, 1); // Lens correction
s->set_hmirror(s, 0); // Horizontal mirror
s->set_vflip(s, 0); // Vertical flip
s->set_dcw(s, 1); // Downsize EN
}
Serial.println("Camera OK!");
return true;
}
void captureAndSave() {
camera_fb_t *fb = esp_camera_fb_get();
if (!fb) {
Serial.println("Capture gagal");
return;
}
Serial.printf("Captured: %dx%d, %d bytes\n",
fb->width, fb->height, fb->len);
// Simpan ke SD card
File file = SD_MMC.open("/photo.jpg", FILE_WRITE);
if (file) {
file.write(fb->buf, fb->len);
file.close();
Serial.println("Tersimpan di SD card!");
}
esp_camera_fb_return(fb);
}
Resolusi yang Didukung OV2640
| Framesize | Resolusi | Ukuran JPEG | FPS (est.) |
|---|---|---|---|
| FRAMESIZE_QQVGA | 160Ã120 | ~2-5 KB | 30+ |
| FRAMESIZE_QVGA | 320Ã240 | ~5-15 KB | 25-30 |
| FRAMESIZE_VGA | 640Ã480 | ~15-40 KB | 15-25 |
| FRAMESIZE_SVGA | 800Ã600 | ~30-70 KB | 10-15 |
| FRAMESIZE_XGA | 1024Ã768 | ~50-120 KB | 5-10 |
| FRAMESIZE_UXGA | 1600Ã1200 | ~80-200 KB | 2-5 |
4. MJPEG Video Streaming
MJPEG (Motion JPEG) adalah format streaming yang paling cocok untuk ESP32-CAM. Setiap frame dikirim sebagai JPEG individual, sehingga tidak perlu codec kompresi video yang berat.
#include "esp_camera.h"
#include <WiFi.h>
#include <WebServer.h>
WebServer server(80);
// MJPEG streaming handler
void handleStream() {
WiFiClient client = server.client();
String response = "HTTP/1.1 200 OK\r\n"
"Content-Type: multipart/x-mixed-replace;boundary=frame\r\n"
"\r\n";
client.print(response);
Serial.println("Stream client terhubung");
while (client.connected()) {
camera_fb_t *fb = esp_camera_fb_get();
if (!fb) {
Serial.println("Capture gagal");
break;
}
// Kirim boundary dan header JPEG
String header = "--frame\r\n"
"Content-Type: image/jpeg\r\n"
"Content-Length: " + String(fb->len) + "\r\n"
"\r\n";
client.print(header);
client.write(fb->buf, fb->len);
client.print("\r\n");
esp_camera_fb_return(fb);
// Yield untuk mencegah WDT reset
yield();
// Rate limiting: ~10 FPS
delay(100);
}
Serial.println("Stream client terputus");
}
// Single capture handler
void handleCapture() {
camera_fb_t *fb = esp_camera_fb_get();
if (!fb) {
server.send(500, "text/plain", "Capture failed");
return;
}
server.sendHeader("Content-Disposition", "inline; filename=capture.jpg");
server.send_P(200, "image/jpeg", (const char *)fb->buf, fb->len);
esp_camera_fb_return(fb);
}
void setup() {
Serial.begin(115200);
// Init camera
if (!initCamera()) {
Serial.println("Camera init failed!");
return;
}
// Connect WiFi
WiFi.begin("SSID", "PASSWORD");
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.printf("\nWiFi: %s\n", WiFi.localIP().toString().c_str());
// Setup web routes
server.on("/", HTTP_GET, handleRoot);
server.on("/stream", HTTP_GET, handleStream);
server.on("/capture", HTTP_GET, handleCapture);
server.on("/control", HTTP_GET, handleControl);
server.begin();
Serial.println("Server siap di http://" + WiFi.localIP().toString());
}
void loop() {
server.handleClient();
}
HTML Viewer untuk MJPEG Stream
<!-- Embedded HTML di ESP32-CAM -->
const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>ESP32-CAM</title>
<style>
body { font-family: Arial; background: #1a1a2e; color: #fff;
text-align: center; margin: 0; padding: 20px; }
.stream-container { margin: 20px auto; max-width: 800px; }
img { width: 100%; border-radius: 12px; box-shadow: 0 4px 20px rgba(0,0,0,0.3); }
.controls { margin: 20px 0; }
button { padding: 10px 20px; margin: 5px; border: none;
border-radius: 8px; background: #4ecdc4; color: #fff;
cursor: pointer; font-size: 16px; }
button:hover { background: #45b7aa; }
select { padding: 10px; border-radius: 8px; font-size: 16px; }
.info { background: #16213e; padding: 15px; border-radius: 10px; margin: 10px; }
</style>
</head>
<body>
<h1>đˇ ESP32-CAM Live</h1>
<div class="stream-container">
<img id="stream" src="/stream" alt="Live Stream">
</div>
<div class="controls">
<button onclick="capturePhoto()">đ¸ Ambil Foto</button>
<select id="resolution" onchange="setResolution(this.value)">
<option value="0">UXGA (1600x1200)</option>
<option value="5" selected>SVGA (800x600)</option>
<option value="8">VGA (640x480)</option>
<option value="10">QVGA (320x240)</option>
</select>
<select id="quality" onchange="setQuality(this.value)">
<option value="10">Kualitas Tinggi</option>
<option value="15" selected>Kualitas Sedang</option>
<option value="20">Kualitas Rendah</option>
</select>
</div>
<div class="info">
<span id="fps">FPS: --</span> |
<span id="res">Resolusi: --</span>
</div>
<script>
function capturePhoto() {
window.open('/capture', '_blank');
}
function setResolution(val) {
fetch('/control?var=framesize&val=' + val);
}
function setQuality(val) {
fetch('/control?var=quality&val=' + val);
}
</script>
</body>
</html>
)rawliteral";
5. Web Server & Control Panel
Kita bisa membangun web server lengkap di ESP32-CAM yang menyediakan halaman kontrol dengan pengaturan resolusi, brightness, contrast, dan parameter kamera lainnya.
// Handler untuk parameter kamera via HTTP
void handleControl() {
if (!server.hasArg("var") || !server.hasArg("val")) {
server.send(400, "text/plain", "Missing parameters");
return;
}
String var = server.arg("var");
int val = server.arg("val").toInt();
sensor_t *s = esp_camera_sensor_get();
if (!s) {
server.send(500, "text/plain", "Sensor not available");
return;
}
int res = 0;
if (var == "framesize") {
res = s->set_framesize(s, (framesize_t)val);
} else if (var == "quality") {
res = s->set_quality(s, val);
} else if (var == "brightness") {
res = s->set_brightness(s, val);
} else if (var == "contrast") {
res = s->set_contrast(s, val);
} else if (var == "saturation") {
res = s->set_saturation(s, val);
} else if (var == "hmirror") {
res = s->set_hmirror(s, val);
} else if (var == "vflip") {
res = s->set_vflip(s, val);
} else if (var == "flash") {
// Kontrol LED flash
int flashPin = 4;
if (val == 0) {
analogWrite(flashPin, 0);
} else {
analogWrite(flashPin, val * 25); // 1-10 level
}
res = 0;
} else {
server.send(400, "text/plain", "Unknown variable");
return;
}
if (res == 0) {
server.send(200, "text/plain", "OK");
} else {
server.send(500, "text/plain", "Set failed");
}
}
// Status handler â info sistem
void handleStatus() {
String json = "{";
json += "\"ip\":\"" + WiFi.localIP().toString() + "\",";
json += "\"rssi\":" + String(WiFi.RSSI()) + ",";
json += "\"freeHeap\":" + String(ESP.getFreeHeap()) + ",";
json += "\"psram\":" + String(ESP.getFreePsram()) + ",";
json += "\"uptime\":" + String(millis() / 1000);
json += "}";
server.send(200, "application/json", json);
}
6. Face Detection
ESP32 memiliki built-in face detection engine yang menggunakan hardware accelerator. Fitur ini tersedia melalui library fd_forward.h dan bisa mendeteksi beberapa wajah secara real-time.
#include "fd_forward.h"
#include "fr_forward.h"
#include "esp_camera.h"
// Inisialisasi face detection
static mtmn_config_t initFaceDetection() {
mtmn_config_t mtmn_config = {0};
mtmn_config.type = FAST;
mtmn_config.min_face = 80; // Minimum face size (px)
mtmn_config.pyramid = 0.707; // Pyramid scaling factor
mtmn_config.pyramid_times = 4; // Pyramid levels
mtmn_config.p_threshold.score = 0.6;
mtdm_config.p_threshold.nms = 0.7;
mtmn_config.p_threshold.candidate_number = 20;
mtmn_config.r_threshold.score = 0.7;
mtmn_config.r_threshold.nms = 0.7;
mtmn_config.r_threshold.candidate_number = 10;
mtmn_config.o_threshold.score = 0.7;
mtmn_config.o_threshold.nms = 0.7;
mtmn_config.o_threshold.candidate_number = 1;
return mtmn_config;
}
// Detect faces dari camera frame
void detectFaces(camera_fb_t *fb) {
static mtmn_config_t mtmn_config = initFaceDetection();
// Konversi JPEG ke RGB888
dl_matrix3du_t *image_matrix = dl_matrix3du_alloc(1, fb->width, fb->height, 3);
if (!image_matrix) {
Serial.println("Gagal alokasi matrix");
return;
}
bool converted = fmt2rgb888(fb->buf, fb->len, PIXFORMAT_JPEG, image_matrix->item);
if (!converted) {
dl_matrix3du_free(image_matrix);
Serial.println("Konversi gagal");
return;
}
// Jalankan face detection
box_array_t *net_boxes = face_detect(image_matrix, &mtmn_config);
if (net_boxes && net_boxes->len > 0) {
Serial.printf("Terdeteksi %d wajah!\n", net_boxes->len);
for (int i = 0; i < net_boxes->len; i++) {
int x = net_boxes->box[i].box_p[0];
int y = net_boxes->box[i].box_p[1];
int w = net_boxes->box[i].box_p[2] - x;
int h = net_boxes->box[i].box_p[3] - y;
float score = net_boxes->box[i].score[0];
Serial.printf(" Wajah %d: x=%d y=%d %dx%d score=%.2f\n",
i+1, x, y, w, h, score);
// Bisa diintegrasi dengan notifikasi Telegram
if (score > 0.8) {
sendTelegramAlert(fb);
}
}
dl_lib_free(net_boxes->box);
dl_lib_free(net_boxes);
}
dl_matrix3du_free(image_matrix);
}
Face detection pada ESP32 memerlukan resolusi QVGA (320Ã240) atau QQVGA (160Ã120) untuk performa real-time. Pada resolusi VGA atau lebih tinggi, proses deteksi memerlukan waktu 200-500ms per frame. Gunakan resolusi rendah untuk face detection, resolusi tinggi untuk capture foto.
7. Telegram Bot Integration
Telegram Bot API adalah cara terbaik untuk menerima notifikasi dan foto dari ESP32-CAM. Kita bisa membuat bot yang mengirim foto ketika wajah terdeteksi atau motion terdeteksi.
Langkah Setup Telegram Bot
- Buka Telegram dan cari @BotFather
- Ketik
/newbotdan ikuti instruksi - Salin Bot Token yang diberikan
- Ketik
/startke bot kamu untuk mendapatkan Chat ID - Akses
https://api.telegram.org/bot<TOKEN>/getUpdatesuntuk mendapatkan Chat ID
#include <WiFiClientSecure.h>
#include <HTTPClient.h>
// Ganti dengan token dan chat ID kamu
const char* BOT_TOKEN = "123456789:ABCdefGhIjKlMnOpQrStUvWxYz";
const char* CHAT_ID = "987654321";
// Kirim foto ke Telegram
bool sendTelegramPhoto(camera_fb_t *fb) {
WiFiClientSecure client;
client.setInsecure(); // Skip SSL verification (hanya untuk development)
if (!client.connect("api.telegram.org", 443)) {
Serial.println("Koneksi Telegram gagal");
return false;
}
// Buat multipart form data
String boundary = "----ESP32CAM" + String(millis());
// Header
String header = "POST /bot" + String(BOT_TOKEN) +
"/sendPhoto HTTP/1.1\r\n" +
"Host: api.telegram.org\r\n" +
"Content-Type: multipart/form-data; boundary=" + boundary + "\r\n";
// Form fields
String chatIdField = "--" + boundary + "\r\n" +
"Content-Disposition: form-data; name=\"chat_id\"\r\n\r\n" +
String(CHAT_ID) + "\r\n";
String captionField = "--" + boundary + "\r\n" +
"Content-Disposition: form-data; name=\"caption\"\r\n\r\n" +
"đˇ ESP32-CAM Alert! " + String(millis()/1000) + "s\r\n";
String photoField = "--" + boundary + "\r\n" +
"Content-Disposition: form-data; name=\"photo\"; filename=\"capture.jpg\"\r\n" +
"Content-Type: image/jpeg\r\n\r\n";
String footer = "\r\n--" + boundary + "--\r\n";
// Hitung total content length
int contentLength = chatIdField.length() + captionField.length() +
photoField.length() + fb->len + footer.length();
header += "Content-Length: " + String(contentLength) + "\r\n\r\n";
// Kirim request
client.print(header);
client.print(chatIdField);
client.print(captionField);
client.write(fb->buf, fb->len);
client.print(footer);
// Baca response
unsigned long timeout = millis();
while (client.connected() && !client.available()) {
if (millis() - timeout > 10000) {
Serial.println("Timeout!");
return false;
}
delay(10);
}
String response = client.readString();
bool success = response.indexOf("\"ok\":true") > 0;
Serial.printf("Telegram photo: %s\n", success ? "OK" : "FAIL");
return success;
}
// Kirim text message
void sendTelegramMessage(String message) {
HTTPClient http;
String url = "https://api.telegram.org/bot" + String(BOT_TOKEN) +
"/sendMessage?chat_id=" + String(CHAT_ID) +
"&text=" + message;
http.begin(url);
int code = http.GET();
Serial.printf("Telegram msg: %d\n", code);
http.end();
}
// Poll untuk pesan dari Telegram (command handler)
void checkTelegramCommands() {
HTTPClient http;
static int lastUpdateId = 0;
String url = "https://api.telegram.org/bot" + String(BOT_TOKEN) +
"/getUpdates?offset=" + String(lastUpdateId + 1) +
"&timeout=1";
http.begin(url);
int code = http.GET();
if (code == 200) {
String response = http.getString();
// Parse dan handle commands
if (response.indexOf("\"text\":\"/photo\"") > 0) {
camera_fb_t *fb = esp_camera_fb_get();
if (fb) {
sendTelegramPhoto(fb);
esp_camera_fb_return(fb);
}
} else if (response.indexOf("\"text\":\"/status\"") > 0) {
String msg = "đ ESP32-CAM Status:\n";
msg += "Free Heap: " + String(ESP.getFreeHeap()) + " bytes\n";
msg += "Uptime: " + String(millis() / 1000) + "s\n";
msg += "WiFi RSSI: " + String(WiFi.RSSI()) + " dBm";
sendTelegramMessage(msg);
}
// Update offset
int idx = response.lastIndexOf("\"update_id\":");
if (idx > 0) {
String idStr = response.substring(idx + 13);
lastUpdateId = idStr.toInt();
}
}
http.end();
}
8. Motion Detection & Alert
Selain face detection, kita juga bisa mendeteksi perubahan gerakan dengan membandingkan frame secara berurutan. Ini berguna untuk sistem keamanan yang mengirim foto ketika ada pergerakan.
#include "img_converters.h"
#include "fb_gfx.h"
// Motion detection dengan frame differencing
class MotionDetector {
private:
uint8_t *prevFrame;
int frameSize;
int threshold;
unsigned long lastAlert;
public:
MotionDetector(int w, int h, int thresh = 30) {
frameSize = w * h;
prevFrame = (uint8_t *)malloc(frameSize);
threshold = thresh;
lastAlert = 0;
memset(prevFrame, 0, frameSize);
}
// Deteksi motion dari frame grayscale
bool detect(camera_fb_t *fb) {
// Konversi ke grayscale
uint8_t *gray = (uint8_t *)malloc(fb->width * fb->height);
// Simplified: ambil setiap 3 byte (R) dari RGB
for (int i = 0; i < frameSize && i * 3 < fb->len; i++) {
gray[i] = fb->buf[i * 3]; // Ambil channel R saja
}
// Hitung perbedaan pixel
int diffCount = 0;
for (int i = 0; i < frameSize; i++) {
int diff = abs((int)gray[i] - (int)prevFrame[i]);
if (diff > threshold) {
diffCount++;
}
}
// Simpan frame sebelumnya
memcpy(prevFrame, gray, frameSize);
free(gray);
// Hitung persentase perubahan
float changePercent = (float)diffCount / frameSize * 100;
bool motionDetected = changePercent > 5.0; // 5% perubahan
if (motionDetected) {
Serial.printf("Motion: %.1f%% perubahan\n", changePercent);
}
return motionDetected;
}
// Cooldown: kirim alert tidak lebih sering dari 30 detik
bool shouldAlert() {
if (millis() - lastAlert > 30000) {
lastAlert = millis();
return true;
}
return false;
}
};
// Penggunaan dalam loop
MotionDetector motion(320, 240, 30);
void loop() {
server.handleClient();
checkTelegramCommands();
camera_fb_t *fb = esp_camera_fb_get();
if (fb) {
if (motion.detect(fb) && motion.shouldAlert()) {
Serial.println("đ¨ Motion detected!");
sendTelegramPhoto(fb);
sendTelegramMessage("đ¨ Gerakan terdeteksi!");
}
esp_camera_fb_return(fb);
}
delay(200); // Cek setiap 200ms
}
9. Optimasi Performa
- Double buffering: Gunakan
fb_count = 2agar capture berikutnya bisa dimulai sambil frame sebelumnya masih dikirim - CAMERA_GRAB_LATEST: Selalu ambil frame terbaru, lewati frame lama
- Reduce resolusi untuk face detection: Gunakan QQVGA (160Ã120) untuk face detection, UXGA untuk foto
- WiFi power management: Matikan WiFi sleep untuk latency lebih rendah, tapi lebih boros listrik
- PSRAM allocation: Gunakan PSRAM untuk frame buffer agar SRAM bisa dipakai untuk kode
- WDT timeout: Tambahkan
yield()dalam loop panjang untuk mencegah watchdog reset
Untuk streaming yang stabil, gunakan resolusi VGA (640Ã480) dengan JPEG quality 15. Ini memberikan balance yang baik antara kualitas dan frame rate (~15 FPS). Jika hanya butuh face detection, QVGA (320Ã240) dengan quality 20 sudah cukup.