721 lines
21 KiB
C++
721 lines
21 KiB
C++
#include <Arduino.h>
|
|
#include <WiFi.h>
|
|
#include <WebServer.h>
|
|
#include <HTTPClient.h>
|
|
#include "esp_camera.h"
|
|
#include "time.h"
|
|
#include "SPIFFS.h" // Tambahkan SPIFFS untuk penyimpanan file
|
|
|
|
// WiFi credentials
|
|
const char* ssid = "didinganteng";
|
|
const char* password = "didin123";
|
|
|
|
// NTP Server untuk waktu
|
|
const char* ntpServer = "pool.ntp.org";
|
|
const long gmtOffset_sec = 25200; // GMT+7 (WIB) in seconds (7*3600)
|
|
const int daylightOffset_sec = 0;
|
|
|
|
// Web server on port 80
|
|
WebServer server(80);
|
|
|
|
// Server Laravel (ganti dengan alamat server Laravel Anda)
|
|
const char* laravelServerUrl = "https://monitoring-anggrek.oyi.web.id/api/esp32cam/upload"; // Ganti dengan IP komputer Anda
|
|
|
|
// Status variables
|
|
bool cameraInitialized = false;
|
|
unsigned long startTime = 0;
|
|
// Menghilangkan timeout 5 menit
|
|
// const unsigned long TIMEOUT = 300000; // 5 menit (300000 ms) sebelum sleep
|
|
|
|
// Variabel untuk pengambilan gambar otomatis
|
|
const unsigned long AUTO_CAPTURE_DELAY = 5000; // 5 detik setelah boot
|
|
bool hasAutoCapture = false; // Flag untuk menandai sudah auto capture atau belum
|
|
|
|
// Variabel untuk streaming
|
|
bool isStreaming = false; // Flag untuk status streaming
|
|
const int streamingFrameRate = 10; // Frame rate untuk streaming (fps)
|
|
const int streamingDelay = 1000 / streamingFrameRate; // Delay antara frame dalam ms
|
|
|
|
// Variabel untuk pengaturan kamera
|
|
framesize_t streamResolution = FRAMESIZE_SVGA; // Default 800x600
|
|
int streamQuality = 10; // Default quality (0-63, lower is better)
|
|
|
|
// Variabel untuk pengiriman ulang foto
|
|
const int MAX_RETRY_PHOTOS = 5; // Maksimal foto yang disimpan sementara
|
|
const unsigned long RETRY_INTERVAL = 30000; // Interval pengiriman ulang (30 detik)
|
|
unsigned long lastRetryTime = 0; // Waktu terakhir mencoba mengirim ulang
|
|
int pendingPhotoCount = 0; // Jumlah foto yang menunggu dikirim
|
|
bool isRetryScheduled = false; // Flag untuk menandai jadwal pengiriman ulang
|
|
|
|
// ESP32 Camera pins
|
|
#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
|
|
|
|
// Helper function to add CORS headers to all responses
|
|
void setCorsHeaders() {
|
|
server.sendHeader("Access-Control-Allow-Origin", "*");
|
|
server.sendHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
server.sendHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
|
}
|
|
|
|
// Helper function to initialize SPIFFS
|
|
bool initSPIFFS() {
|
|
if (!SPIFFS.begin(true)) { // Format SPIFFS if mounting fails
|
|
Serial.println("SPIFFS gagal diinisialisasi");
|
|
return false;
|
|
}
|
|
Serial.println("SPIFFS berhasil diinisialisasi");
|
|
|
|
// Bersihkan file foto lama jika ada
|
|
cleanupPendingPhotos();
|
|
|
|
return true;
|
|
}
|
|
|
|
// Helper function untuk membersihkan file foto yang pending
|
|
void cleanupPendingPhotos() {
|
|
// Cek jumlah foto pending
|
|
int count = 0;
|
|
File root = SPIFFS.open("/");
|
|
File file = root.openNextFile();
|
|
|
|
while (file) {
|
|
String fileName = file.name();
|
|
if (fileName.startsWith("/photo_") && fileName.endsWith(".jpg")) {
|
|
count++;
|
|
Serial.println("Found pending photo: " + fileName);
|
|
}
|
|
file = root.openNextFile();
|
|
}
|
|
|
|
pendingPhotoCount = count;
|
|
Serial.println("Jumlah foto pending: " + String(pendingPhotoCount));
|
|
|
|
// Jika ada foto pending, jadwalkan pengiriman ulang
|
|
if (pendingPhotoCount > 0) {
|
|
isRetryScheduled = true;
|
|
Serial.println("Pengiriman ulang foto terjadwal");
|
|
}
|
|
}
|
|
|
|
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_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;
|
|
|
|
// Initialize with high quality
|
|
config.frame_size = FRAMESIZE_SVGA; // 800x600
|
|
config.jpeg_quality = 10; // 0-63, lower is better quality
|
|
config.fb_count = 2; // Meningkatkan jumlah frame buffer untuk streaming
|
|
|
|
// Camera init
|
|
esp_err_t err = esp_camera_init(&config);
|
|
if (err != ESP_OK) {
|
|
Serial.printf("Kamera gagal diinisialisasi, error 0x%x", err);
|
|
return false;
|
|
}
|
|
|
|
Serial.println("Kamera berhasil diinisialisasi");
|
|
return true;
|
|
}
|
|
|
|
void connectToWifi() {
|
|
Serial.print("Menghubungkan ke WiFi...");
|
|
WiFi.begin(ssid, password);
|
|
|
|
int attempts = 0;
|
|
while (WiFi.status() != WL_CONNECTED && attempts < 20) {
|
|
delay(500);
|
|
Serial.print(".");
|
|
attempts++;
|
|
}
|
|
|
|
if (WiFi.status() == WL_CONNECTED) {
|
|
Serial.println();
|
|
Serial.println("WiFi terhubung!");
|
|
Serial.print("IP Address: ");
|
|
Serial.println(WiFi.localIP());
|
|
Serial.print("Subnet Mask: ");
|
|
Serial.println(WiFi.subnetMask());
|
|
Serial.print("Gateway IP: ");
|
|
Serial.println(WiFi.gatewayIP());
|
|
} else {
|
|
Serial.println();
|
|
Serial.println("Gagal terhubung ke WiFi. Restart ESP32...");
|
|
delay(1000);
|
|
ESP.restart();
|
|
}
|
|
}
|
|
|
|
void initTime() {
|
|
configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
|
|
struct tm timeinfo;
|
|
if (!getLocalTime(&timeinfo)) {
|
|
Serial.println("Gagal mendapatkan waktu dari NTP server");
|
|
return;
|
|
}
|
|
|
|
Serial.println("NTP waktu diinisialisasi");
|
|
Serial.print("Waktu saat ini: ");
|
|
Serial.print(&timeinfo, "%A, %d-%m-%Y %H:%M:%S");
|
|
Serial.println();
|
|
}
|
|
|
|
// Simpan foto ke SPIFFS untuk dikirim nanti
|
|
bool savePhotoForLater(camera_fb_t *fb) {
|
|
if (pendingPhotoCount >= MAX_RETRY_PHOTOS) {
|
|
Serial.println("Terlalu banyak foto pending, tidak bisa menyimpan lagi");
|
|
return false;
|
|
}
|
|
|
|
// Buat nama file dengan timestamp
|
|
char fileName[32];
|
|
struct tm timeinfo;
|
|
if(getLocalTime(&timeinfo)){
|
|
sprintf(fileName, "/photo_%04d%02d%02d_%02d%02d%02d.jpg",
|
|
timeinfo.tm_year + 1900, timeinfo.tm_mon + 1, timeinfo.tm_mday,
|
|
timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec);
|
|
} else {
|
|
sprintf(fileName, "/photo_%d.jpg", millis());
|
|
}
|
|
|
|
// Simpan ke SPIFFS
|
|
File file = SPIFFS.open(fileName, FILE_WRITE);
|
|
if (!file) {
|
|
Serial.println("Gagal membuka file untuk menulis");
|
|
return false;
|
|
}
|
|
|
|
if (file.write(fb->buf, fb->len) != fb->len) {
|
|
Serial.println("Gagal menulis file");
|
|
file.close();
|
|
return false;
|
|
}
|
|
|
|
file.close();
|
|
Serial.println("Foto berhasil disimpan di SPIFFS: " + String(fileName));
|
|
pendingPhotoCount++;
|
|
isRetryScheduled = true;
|
|
|
|
return true;
|
|
}
|
|
|
|
// Fungsi untuk mengirim ulang foto yang tersimpan di SPIFFS
|
|
void retryPendingPhotos() {
|
|
if (pendingPhotoCount <= 0) {
|
|
isRetryScheduled = false;
|
|
return;
|
|
}
|
|
|
|
if (WiFi.status() != WL_CONNECTED) {
|
|
Serial.println("Tidak ada koneksi WiFi, menunda pengiriman ulang");
|
|
return;
|
|
}
|
|
|
|
Serial.println("Mencoba mengirim foto yang tertunda...");
|
|
|
|
File root = SPIFFS.open("/");
|
|
File file = root.openNextFile();
|
|
bool anySuccess = false;
|
|
|
|
while (file && pendingPhotoCount > 0) {
|
|
String fileName = file.name();
|
|
if (fileName.startsWith("/photo_") && fileName.endsWith(".jpg")) {
|
|
Serial.println("Mengirim foto: " + fileName);
|
|
|
|
// Baca file
|
|
if (file.size() > 0) {
|
|
uint8_t *buffer = (uint8_t*) malloc(file.size());
|
|
if (buffer) {
|
|
size_t size = file.read(buffer, file.size());
|
|
|
|
// Kirim ke server
|
|
HTTPClient http;
|
|
http.begin(laravelServerUrl);
|
|
http.addHeader("Content-Type", "image/jpeg");
|
|
|
|
int httpResponseCode = http.POST(buffer, size);
|
|
free(buffer);
|
|
|
|
if (httpResponseCode > 0) {
|
|
String response = http.getString();
|
|
Serial.println("HTTP Response code: " + String(httpResponseCode));
|
|
Serial.println("Response: " + response);
|
|
|
|
// Jika sukses, hapus file
|
|
String fullFileName = fileName;
|
|
file.close();
|
|
if (SPIFFS.remove(fullFileName)) {
|
|
Serial.println("File berhasil dihapus: " + fullFileName);
|
|
pendingPhotoCount--;
|
|
anySuccess = true;
|
|
} else {
|
|
Serial.println("Gagal menghapus file: " + fullFileName);
|
|
}
|
|
} else {
|
|
Serial.print("Error pada HTTP request: ");
|
|
Serial.println(httpResponseCode);
|
|
}
|
|
|
|
http.end();
|
|
}
|
|
}
|
|
|
|
if (!anySuccess) {
|
|
// Jika tidak ada yang berhasil dikirim, kita hentikan dulu
|
|
// untuk menghemat resource dan mencoba lagi nanti
|
|
break;
|
|
}
|
|
}
|
|
|
|
file = root.openNextFile();
|
|
}
|
|
|
|
// Update status
|
|
if (pendingPhotoCount <= 0) {
|
|
isRetryScheduled = false;
|
|
Serial.println("Semua foto tertunda berhasil dikirim");
|
|
} else {
|
|
Serial.println("Masih ada " + String(pendingPhotoCount) + " foto tertunda");
|
|
}
|
|
}
|
|
|
|
// Rute untuk halaman utama
|
|
void handleRoot() {
|
|
setCorsHeaders();
|
|
String html = "<html><body>";
|
|
html += "<h1>ESP32-CAM Monitoring Anggrek</h1>";
|
|
html += "<p>Akses <a href='/capture'>capture</a> untuk mengambil foto</p>";
|
|
// Hilangkan link sleep
|
|
// html += "<p>Akses <a href='/sleep'>sleep</a> untuk masuk mode deep sleep</p>";
|
|
html += "<p>Akses <a href='/status'>status</a> untuk melihat status perangkat</p>";
|
|
html += "<p>Akses <a href='/stream'>stream</a> untuk melihat live streaming</p>";
|
|
|
|
struct tm timeinfo;
|
|
if (getLocalTime(&timeinfo)) {
|
|
char timeString[50];
|
|
strftime(timeString, sizeof(timeString), "%A, %d-%m-%Y %H:%M:%S", &timeinfo);
|
|
html += "<p>Waktu saat ini: " + String(timeString) + "</p>";
|
|
}
|
|
|
|
html += "<p>Waktu hidup: " + String((millis() - startTime) / 1000) + " detik</p>";
|
|
|
|
// Tambahkan info foto pending
|
|
html += "<p>Foto tertunda: " + String(pendingPhotoCount) + "</p>";
|
|
|
|
html += "</body></html>";
|
|
|
|
server.send(200, "text/html", html);
|
|
}
|
|
|
|
// Rute untuk mengambil foto
|
|
void handleCapture() {
|
|
setCorsHeaders();
|
|
if (!cameraInitialized) {
|
|
server.send(500, "text/plain", "Kamera tidak diinisialisasi");
|
|
return;
|
|
}
|
|
|
|
camera_fb_t *fb = NULL;
|
|
|
|
// Ambil foto
|
|
fb = esp_camera_fb_get();
|
|
if (!fb) {
|
|
Serial.println("Gagal mengambil foto");
|
|
server.send(500, "text/plain", "Gagal mengambil foto");
|
|
return;
|
|
}
|
|
|
|
Serial.println("Foto berhasil diambil");
|
|
Serial.print("Ukuran: ");
|
|
Serial.print(fb->len);
|
|
Serial.println(" bytes");
|
|
|
|
// Kirim foto sebagai respons HTTP
|
|
server.sendHeader("Content-Type", "image/jpeg");
|
|
server.sendHeader("Content-Disposition", "inline; filename=capture.jpg");
|
|
server.sendHeader("Content-Length", String(fb->len));
|
|
server.send_P(200, "image/jpeg", (const char *)fb->buf, fb->len);
|
|
|
|
// Bebaskan memori
|
|
esp_camera_fb_return(fb);
|
|
|
|
Serial.println("Foto berhasil dikirim ke client");
|
|
}
|
|
|
|
// Rute untuk status perangkat
|
|
void handleStatus() {
|
|
setCorsHeaders();
|
|
String status = "{";
|
|
|
|
// Informasi sistem
|
|
status += "\"heap_size\":" + String(ESP.getFreeHeap()) + ",";
|
|
status += "\"uptime\":" + String(millis() / 1000) + ",";
|
|
|
|
// Informasi WiFi
|
|
status += "\"wifi\":{";
|
|
status += "\"connected\":" + String(WiFi.status() == WL_CONNECTED ? "true" : "false") + ",";
|
|
status += "\"ip\":\"" + WiFi.localIP().toString() + "\",";
|
|
status += "\"rssi\":" + String(WiFi.RSSI());
|
|
status += "},";
|
|
|
|
// Informasi kamera
|
|
status += "\"camera\":{";
|
|
status += "\"initialized\":" + String(cameraInitialized ? "true" : "false") + ",";
|
|
status += "\"streaming\":" + String(isStreaming ? "true" : "false");
|
|
status += "},";
|
|
|
|
// Informasi foto pending
|
|
status += "\"pending_photos\":{";
|
|
status += "\"count\":" + String(pendingPhotoCount) + ",";
|
|
status += "\"retry_scheduled\":" + String(isRetryScheduled ? "true" : "false");
|
|
status += "}";
|
|
|
|
status += "}";
|
|
|
|
server.sendHeader("Content-Type", "application/json");
|
|
server.send(200, "application/json", status);
|
|
}
|
|
|
|
// Handler untuk streaming MJPEG
|
|
void handleStream() {
|
|
if (!cameraInitialized) {
|
|
setCorsHeaders();
|
|
server.send(500, "text/plain", "Kamera tidak diinisialisasi");
|
|
return;
|
|
}
|
|
|
|
Serial.println("Permintaan streaming diterima");
|
|
|
|
// Set header HTTP untuk streaming MJPEG
|
|
WiFiClient client = server.client();
|
|
|
|
// Tambahkan CORS headers manuallly (tanpa DefaultHeaders)
|
|
client.println("HTTP/1.1 200 OK");
|
|
client.println("Content-Type: multipart/x-mixed-replace; boundary=frame");
|
|
client.println("Access-Control-Allow-Origin: *");
|
|
client.println("Connection: keep-alive");
|
|
client.println();
|
|
|
|
// Set flag streaming
|
|
isStreaming = true;
|
|
Serial.println("Streaming dimulai");
|
|
|
|
// Loop untuk mengirim frame selama klien terhubung dan streaming aktif
|
|
while (client.connected() && isStreaming) {
|
|
camera_fb_t *fb = esp_camera_fb_get();
|
|
if (!fb) {
|
|
Serial.println("Capture gagal selama streaming");
|
|
delay(100);
|
|
continue;
|
|
}
|
|
|
|
client.println("--frame");
|
|
client.println("Content-Type: image/jpeg");
|
|
client.println("Content-Length: " + String(fb->len));
|
|
client.println();
|
|
client.write(fb->buf, fb->len);
|
|
client.println();
|
|
|
|
// Bebaskan memori
|
|
esp_camera_fb_return(fb);
|
|
|
|
// Delay sesuai frame rate
|
|
delay(streamingDelay);
|
|
}
|
|
|
|
// Jika keluar dari loop, streaming dihentikan
|
|
isStreaming = false;
|
|
Serial.println("Streaming dihentikan");
|
|
}
|
|
|
|
// Handler untuk menghentikan streaming
|
|
void handleStopStream() {
|
|
isStreaming = false;
|
|
setCorsHeaders();
|
|
Serial.println("Permintaan menghentikan streaming diterima");
|
|
server.send(200, "text/plain", "Streaming dihentikan");
|
|
}
|
|
|
|
// Fungsi untuk mengirim gambar ke server Laravel
|
|
void sendImageToServer() {
|
|
if (!cameraInitialized) {
|
|
Serial.println("Kamera tidak diinisialisasi, tidak bisa mengirim gambar");
|
|
return;
|
|
}
|
|
|
|
camera_fb_t *fb = NULL;
|
|
|
|
// Ambil foto
|
|
fb = esp_camera_fb_get();
|
|
if (!fb) {
|
|
Serial.println("Gagal mengambil foto untuk dikirim ke server");
|
|
return;
|
|
}
|
|
|
|
Serial.println("Foto berhasil diambil untuk dikirim ke server");
|
|
Serial.print("Ukuran: ");
|
|
Serial.print(fb->len);
|
|
Serial.println(" bytes");
|
|
|
|
// Cek koneksi WiFi
|
|
if (WiFi.status() != WL_CONNECTED) {
|
|
Serial.println("Tidak ada koneksi WiFi, menyimpan foto untuk dikirim nanti");
|
|
bool saved = savePhotoForLater(fb);
|
|
if (saved) {
|
|
Serial.println("Foto disimpan untuk dikirim nanti");
|
|
} else {
|
|
Serial.println("Gagal menyimpan foto untuk dikirim nanti");
|
|
}
|
|
esp_camera_fb_return(fb);
|
|
return;
|
|
}
|
|
|
|
// Kirim foto ke server Laravel
|
|
HTTPClient http;
|
|
http.begin(laravelServerUrl);
|
|
http.addHeader("Content-Type", "image/jpeg");
|
|
|
|
int httpResponseCode = http.POST(fb->buf, fb->len);
|
|
|
|
if (httpResponseCode > 0) {
|
|
String response = http.getString();
|
|
Serial.println("HTTP Response code: " + String(httpResponseCode));
|
|
Serial.println("Response: " + response);
|
|
} else {
|
|
Serial.print("Error pada HTTP request: ");
|
|
Serial.println(httpResponseCode);
|
|
|
|
// Jika gagal kirim, simpan untuk dikirim nanti
|
|
bool saved = savePhotoForLater(fb);
|
|
if (saved) {
|
|
Serial.println("Foto disimpan untuk dikirim nanti");
|
|
} else {
|
|
Serial.println("Gagal menyimpan foto untuk dikirim nanti");
|
|
}
|
|
}
|
|
|
|
http.end();
|
|
|
|
// Bebaskan memori
|
|
esp_camera_fb_return(fb);
|
|
|
|
Serial.println("Proses pengiriman gambar ke server selesai");
|
|
}
|
|
|
|
// Fungsi untuk pengambilan gambar otomatis
|
|
void autoCapture() {
|
|
Serial.println("Melakukan pengambilan gambar otomatis...");
|
|
sendImageToServer();
|
|
hasAutoCapture = true;
|
|
Serial.println("Pengambilan gambar otomatis selesai");
|
|
}
|
|
|
|
// Handler untuk CORS preflight requests
|
|
void handleOptions() {
|
|
setCorsHeaders();
|
|
server.send(204);
|
|
}
|
|
|
|
// Handler untuk mengubah pengaturan kamera
|
|
void handleCameraSettings() {
|
|
setCorsHeaders();
|
|
|
|
// Ambil parameter
|
|
String resolution = server.arg("resolution");
|
|
String quality = server.arg("quality");
|
|
|
|
bool changed = false;
|
|
|
|
// Ubah resolusi jika diperlukan
|
|
if (resolution.length() > 0) {
|
|
framesize_t newResolution = FRAMESIZE_SVGA; // Default
|
|
|
|
if (resolution == "UXGA") newResolution = FRAMESIZE_UXGA; // 1600x1200
|
|
else if (resolution == "SXGA") newResolution = FRAMESIZE_SXGA; // 1280x1024
|
|
else if (resolution == "XGA") newResolution = FRAMESIZE_XGA; // 1024x768
|
|
else if (resolution == "SVGA") newResolution = FRAMESIZE_SVGA; // 800x600
|
|
else if (resolution == "VGA") newResolution = FRAMESIZE_VGA; // 640x480
|
|
else if (resolution == "CIF") newResolution = FRAMESIZE_CIF; // 400x296
|
|
else if (resolution == "QVGA") newResolution = FRAMESIZE_QVGA; // 320x240
|
|
else if (resolution == "HQVGA") newResolution = FRAMESIZE_HQVGA; // 240x176
|
|
else if (resolution == "QQVGA") newResolution = FRAMESIZE_QQVGA; // 160x120
|
|
|
|
if (newResolution != streamResolution) {
|
|
streamResolution = newResolution;
|
|
changed = true;
|
|
|
|
// Terapkan ke sensor
|
|
sensor_t *s = esp_camera_sensor_get();
|
|
if (s) {
|
|
s->set_framesize(s, streamResolution);
|
|
Serial.println("Resolusi diubah ke " + resolution);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Ubah kualitas jika diperlukan
|
|
if (quality.length() > 0) {
|
|
int newQuality = quality.toInt();
|
|
|
|
if (newQuality >= 0 && newQuality <= 63 && newQuality != streamQuality) {
|
|
streamQuality = newQuality;
|
|
changed = true;
|
|
|
|
// Terapkan ke sensor
|
|
sensor_t *s = esp_camera_sensor_get();
|
|
if (s) {
|
|
s->set_quality(s, streamQuality);
|
|
Serial.println("Kualitas gambar diubah ke " + quality);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Kirim response
|
|
String response = "{";
|
|
response += "\"success\":" + String(changed ? "true" : "false") + ",";
|
|
response += "\"resolution\":\"";
|
|
|
|
switch (streamResolution) {
|
|
case FRAMESIZE_UXGA: response += "UXGA"; break;
|
|
case FRAMESIZE_SXGA: response += "SXGA"; break;
|
|
case FRAMESIZE_XGA: response += "XGA"; break;
|
|
case FRAMESIZE_SVGA: response += "SVGA"; break;
|
|
case FRAMESIZE_VGA: response += "VGA"; break;
|
|
case FRAMESIZE_CIF: response += "CIF"; break;
|
|
case FRAMESIZE_QVGA: response += "QVGA"; break;
|
|
case FRAMESIZE_HQVGA: response += "HQVGA"; break;
|
|
case FRAMESIZE_QQVGA: response += "QQVGA"; break;
|
|
default: response += "Unknown"; break;
|
|
}
|
|
|
|
response += "\",";
|
|
response += "\"quality\":" + String(streamQuality);
|
|
response += "}";
|
|
|
|
server.sendHeader("Content-Type", "application/json");
|
|
server.send(200, "application/json", response);
|
|
}
|
|
|
|
// Handler untuk mencoba pengiriman ulang foto secara manual
|
|
void handleRetryPhotos() {
|
|
setCorsHeaders();
|
|
retryPendingPhotos();
|
|
server.send(200, "text/plain", "Pengiriman ulang foto dimulai");
|
|
}
|
|
|
|
void setup() {
|
|
Serial.begin(115200);
|
|
delay(1000); // Tunggu serial siap
|
|
|
|
Serial.println("\n\n=== ESP32-CAM Monitoring Anggrek ===");
|
|
Serial.println("Starting as Web Server");
|
|
|
|
// Catat waktu mulai
|
|
startTime = millis();
|
|
|
|
// Inisialisasi SPIFFS
|
|
bool spiffsReady = initSPIFFS();
|
|
|
|
// Inisialisasi kamera
|
|
cameraInitialized = initCamera();
|
|
|
|
// Hubungkan ke WiFi
|
|
connectToWifi();
|
|
|
|
// Inisialisasi waktu
|
|
initTime();
|
|
|
|
// Konfigurasi rute server
|
|
server.on("/", handleRoot);
|
|
server.on("/capture", handleCapture);
|
|
server.on("/status", handleStatus);
|
|
|
|
// Tambahkan rute untuk streaming
|
|
server.on("/stream", HTTP_GET, handleStream);
|
|
server.on("/stopstream", HTTP_GET, handleStopStream);
|
|
|
|
// Tambahkan rute untuk pengambilan gambar dan pengiriman ke server
|
|
server.on("/send-to-server", []() {
|
|
setCorsHeaders();
|
|
sendImageToServer();
|
|
server.send(200, "text/plain", "Gambar berhasil dikirim ke server");
|
|
});
|
|
|
|
// Tambahkan rute untuk mencoba mengirim ulang foto
|
|
server.on("/retry-photos", HTTP_GET, handleRetryPhotos);
|
|
|
|
// Tambahkan rute untuk mengubah pengaturan kamera
|
|
server.on("/camera-settings", HTTP_GET, handleCameraSettings);
|
|
|
|
// Handle CORS preflight requests
|
|
server.on("/capture", HTTP_OPTIONS, handleOptions);
|
|
server.on("/status", HTTP_OPTIONS, handleOptions);
|
|
server.on("/stream", HTTP_OPTIONS, handleOptions);
|
|
server.on("/stopstream", HTTP_OPTIONS, handleOptions);
|
|
server.on("/send-to-server", HTTP_OPTIONS, handleOptions);
|
|
server.on("/retry-photos", HTTP_OPTIONS, handleOptions);
|
|
server.on("/camera-settings", HTTP_OPTIONS, handleOptions);
|
|
|
|
// Start server
|
|
server.begin();
|
|
Serial.println("HTTP server dimulai");
|
|
Serial.println("Buka http://" + WiFi.localIP().toString() + " di browser untuk mengakses ESP32-CAM");
|
|
Serial.println("Pengambilan gambar otomatis akan dilakukan dalam 5 detik...");
|
|
Serial.println("Streaming tersedia di http://" + WiFi.localIP().toString() + "/stream");
|
|
}
|
|
|
|
void loop() {
|
|
// Tangani request yang masuk
|
|
server.handleClient();
|
|
|
|
// Cek jika sudah waktunya untuk pengambilan gambar otomatis
|
|
if (!hasAutoCapture && (millis() - startTime > AUTO_CAPTURE_DELAY)) {
|
|
autoCapture();
|
|
}
|
|
|
|
// Cek jika perlu mengirim ulang foto yang tersimpan
|
|
if (isRetryScheduled && pendingPhotoCount > 0 && WiFi.status() == WL_CONNECTED) {
|
|
unsigned long currentTime = millis();
|
|
if (currentTime - lastRetryTime > RETRY_INTERVAL) {
|
|
lastRetryTime = currentTime;
|
|
retryPendingPhotos();
|
|
}
|
|
}
|
|
|
|
// Beri waktu untuk WiFi stack
|
|
delay(1);
|
|
}
|